Browse Source

merged search_api submodule

Bachir Soussi Chiadmi 9 years ago
parent
commit
8ff8fcd8da
80 changed files with 22570 additions and 0 deletions
  1. 517 0
      sites/all/modules/contrib/search/search_api/CHANGELOG.txt
  2. 339 0
      sites/all/modules/contrib/search/search_api/LICENSE.txt
  3. 399 0
      sites/all/modules/contrib/search/search_api/README.txt
  4. 39 0
      sites/all/modules/contrib/search/search_api/boosts-and-queryconditon.patch
  5. 125 0
      sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/README.txt
  6. 209 0
      sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/example_service.php
  7. 276 0
      sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/plugins/facetapi/adapter.inc
  8. 229 0
      sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/plugins/facetapi/query_type_date.inc
  9. 175 0
      sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/plugins/facetapi/query_type_term.inc
  10. 31 0
      sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/search_api_facetapi.api.php
  11. 17 0
      sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/search_api_facetapi.info
  12. 13 0
      sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/search_api_facetapi.install
  13. 434 0
      sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/search_api_facetapi.module
  14. 114 0
      sites/all/modules/contrib/search/search_api/contrib/search_api_views/README.txt
  15. 276 0
      sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/display_facet_block.inc
  16. 141 0
      sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_argument.inc
  17. 161 0
      sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_argument_date.inc
  18. 106 0
      sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_argument_fulltext.inc
  19. 85 0
      sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_argument_more_like_this.inc
  20. 31 0
      sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_argument_string.inc
  21. 104 0
      sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_argument_taxonomy_term.inc
  22. 119 0
      sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter.inc
  23. 35 0
      sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_boolean.inc
  24. 91 0
      sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_date.inc
  25. 211 0
      sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_entity.inc
  26. 213 0
      sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_fulltext.inc
  27. 59 0
      sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_language.inc
  28. 315 0
      sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_options.inc
  29. 294 0
      sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_taxonomy_term.inc
  30. 20 0
      sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_text.inc
  31. 77 0
      sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_user.inc
  32. 35 0
      sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_sort.inc
  33. 128 0
      sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/plugin_cache.inc
  34. 683 0
      sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/query.inc
  35. 34 0
      sites/all/modules/contrib/search/search_api/contrib/search_api_views/search_api_views.api.php
  36. 35 0
      sites/all/modules/contrib/search/search_api/contrib/search_api_views/search_api_views.info
  37. 105 0
      sites/all/modules/contrib/search/search_api/contrib/search_api_views/search_api_views.install
  38. 67 0
      sites/all/modules/contrib/search/search_api/contrib/search_api_views/search_api_views.module
  39. 295 0
      sites/all/modules/contrib/search/search_api/contrib/search_api_views/search_api_views.views.inc
  40. BIN
      sites/all/modules/contrib/search/search_api/disabled.png
  41. BIN
      sites/all/modules/contrib/search/search_api/enabled.png
  42. 185 0
      sites/all/modules/contrib/search/search_api/includes/callback.inc
  43. 322 0
      sites/all/modules/contrib/search/search_api/includes/callback_add_aggregation.inc
  44. 217 0
      sites/all/modules/contrib/search/search_api/includes/callback_add_hierarchy.inc
  45. 34 0
      sites/all/modules/contrib/search/search_api/includes/callback_add_url.inc
  46. 105 0
      sites/all/modules/contrib/search/search_api/includes/callback_add_viewed_entity.inc
  47. 90 0
      sites/all/modules/contrib/search/search_api/includes/callback_bundle_filter.inc
  48. 46 0
      sites/all/modules/contrib/search/search_api/includes/callback_comment_access.inc
  49. 126 0
      sites/all/modules/contrib/search/search_api/includes/callback_language_control.inc
  50. 98 0
      sites/all/modules/contrib/search/search_api/includes/callback_node_access.inc
  51. 45 0
      sites/all/modules/contrib/search/search_api/includes/callback_node_status.inc
  52. 65 0
      sites/all/modules/contrib/search/search_api/includes/callback_role_filter.inc
  53. 686 0
      sites/all/modules/contrib/search/search_api/includes/datasource.inc
  54. 144 0
      sites/all/modules/contrib/search/search_api/includes/datasource_entity.inc
  55. 259 0
      sites/all/modules/contrib/search/search_api/includes/datasource_external.inc
  56. 34 0
      sites/all/modules/contrib/search/search_api/includes/exception.inc
  57. 967 0
      sites/all/modules/contrib/search/search_api/includes/index_entity.inc
  58. 465 0
      sites/all/modules/contrib/search/search_api/includes/processor.inc
  59. 403 0
      sites/all/modules/contrib/search/search_api/includes/processor_highlight.inc
  60. 142 0
      sites/all/modules/contrib/search/search_api/includes/processor_html_filter.inc
  61. 20 0
      sites/all/modules/contrib/search/search_api/includes/processor_ignore_case.inc
  62. 107 0
      sites/all/modules/contrib/search/search_api/includes/processor_stopwords.inc
  63. 114 0
      sites/all/modules/contrib/search/search_api/includes/processor_tokenizer.inc
  64. 20 0
      sites/all/modules/contrib/search/search_api/includes/processor_transliteration.inc
  65. 1013 0
      sites/all/modules/contrib/search/search_api/includes/query.inc
  66. 396 0
      sites/all/modules/contrib/search/search_api/includes/server_entity.inc
  67. 465 0
      sites/all/modules/contrib/search/search_api/includes/service.inc
  68. 229 0
      sites/all/modules/contrib/search/search_api/search_api.admin.css
  69. 2224 0
      sites/all/modules/contrib/search/search_api/search_api.admin.inc
  70. 207 0
      sites/all/modules/contrib/search/search_api/search_api.admin.js
  71. 591 0
      sites/all/modules/contrib/search/search_api/search_api.api.php
  72. 400 0
      sites/all/modules/contrib/search/search_api/search_api.drush.inc
  73. 42 0
      sites/all/modules/contrib/search/search_api/search_api.info
  74. 994 0
      sites/all/modules/contrib/search/search_api/search_api.install
  75. 3012 0
      sites/all/modules/contrib/search/search_api/search_api.module
  76. 89 0
      sites/all/modules/contrib/search/search_api/search_api.rules.inc
  77. 1125 0
      sites/all/modules/contrib/search/search_api/search_api.test
  78. 18 0
      sites/all/modules/contrib/search/search_api/tests/search_api_test.info
  79. 55 0
      sites/all/modules/contrib/search/search_api/tests/search_api_test.install
  80. 379 0
      sites/all/modules/contrib/search/search_api/tests/search_api_test.module

+ 517 - 0
sites/all/modules/contrib/search/search_api/CHANGELOG.txt

@@ -0,0 +1,517 @@
+Search API 1.11 (12/25/2013):
+-----------------------------
+- #1879196 by drunken monkey: Fixed invalid old indexes causing errors.
+- #2155127 by drunken monkey: Clarified the scope of the "Node access" and
+  "Exclude unpublished nodes" data alterations.
+- #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 (12/09/2013):
+-----------------------------
+- #2130819 by drunken monkey, Bojhan: Added UI improvements for the "View" tabs.
+- #2152327 by sirtet, miro_dietiker: Fixed typo in help text for drush sapi-c.
+- #2144531 by drunken monkey: Fixed cloning of queries to clone filters, too.
+- #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 (10/23/2013):
+----------------------------
+- #2113277 by moonray, drunken monkey: Fixed date facet count for active item.
+- #2086783 by drunken monkey: Removed Views field handlers for "virtual" fields.
+- #2114593 by drunken monkey: Added list of floats to test module.
+- #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 (09/01/2013):
+----------------------------
+- #1414048 by drunken monkey: Fixed exception in views.inc removes all Search
+  API tables.
+- #1921690 by drunken monkey: Fixed stale Views cache when indexed fields
+  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 (07/01/2013):
+----------------------------
+- #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 (05/29/2013):
+----------------------------
+- #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 (05/04/2013):
+----------------------------
+- #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 (01/09/2013):
+----------------------------
+- #1827272 by drunken monkey: Fixed regression introduced by #1777710.
+- #1807622 by drunken monkey: Fixed definition of the default node index.
+- #1818948 by das-peter: Fixed endless loop in
+  search_api_index_specific_items_delayed().
+- #1406808 by Haza, drunken monkey: Added support for date popup in exposed
+  filters.
+- #1823916 by aschiwi: Fixed batch_sise typos.
+
+Search API 1.3 (10/10/2012):
+----------------------------
+- Patch by mr.baileys: Fixed "enable" function doesn't use security tokens.
+- #1318904 by becw, das-peter, orakili, drunken monkey: Added improved handling
+  for NULL values in Views.
+- #1306008 by Damien Tournoud, drunken monkey: Fixed handling of negative
+  facets.
+- #1182912 by drunken monkey, sepgil: Added Rules action for indexing entities.
+- #1507882 by jsacksick: Added "Exclude unpublished nodes" data alteration.
+- #1225620 by drunken monkey: Added Batch API integration for the "Index now"
+  functionality.
+- #1777710 by dasjo: Remove dependency on $_GET['q'] for determining base paths.
+- #1715238 by jsacksick: Fixed fulltext argument handler field list is broken.
+- #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):
+----------------------------
+- #1368548 by das-peter: Do not index views results by entity id.
+- #1422750 by drunken monkey, sepgil: Fixed illegal modification of entity
+  objects.
+- #1363114 by nbucknor: Fixed inclusion of upper bound in range facets.
+- #1580780 by drunken monkey: Fixed default regexps of the Tokenizer.
+- #1468678 by znerol: Fixed erroneous use of Drupal multibyte wrapper functions.
+- #1600986 by DamienMcKenna: Fixed dependencies of exported search servers.
+- #1569874 by cpliakas: Fixed removal/adding of facets when indexed fields are
+  changed.
+- #1528436 by jsacksick, drunken monkey: Fixed handling of exportable entities.
+
+Search API 1.1 (05/23/2012):
+----------------------------
+- Fixed escaping of error messages.
+- #1330506 by drunken monkey: Removed the old Facets module.
+- #1504318 by peximo: Fixed Views pager offset.
+- #1141488 by moonray, drunken monkey: Added option to use multiple values with
+  contextual filters.
+- #1535726 by bojanz, joelpittet: Fixed arguments for
+  $service->configurationFormValidate() for empty forms.
+- #1400882 by mh86: Fixed "Index hierarchy" for "All parents".
+
+Search API 1.0 (12/15/2011):
+----------------------------
+- #1350322 by drunken monkey: Fixed regressions introduced with cron queue
+  indexing.
+- #1352292 by das-peter, drunken monkey: Use Search API specific table groups in
+  Views integration.
+- #1351524 by das-peter: Made Views result row indexing more robust.
+- #1194362 by Damien Tournoud: Fixed click sort added to non-existent Views
+  fields.
+- #1347150 by drunken monkey: Fixed fields access of Views facets block display.
+- #1345972 by Krasnyj, drunken monkey: Added handling of large item amounts to
+  trackItemInsert().
+- #1324182 by dereine, drunken monkey: Fixed indexing author when node access is
+  enabled.
+- #1215526 by cpliakas, drunken monkey: Added support for the "Bundle" facet
+  dependency plugin.
+- #1337292 by drunken monkey: Fixed facet dependency system.
+
+Search API 1.0, RC 1 (11/10/2011):
+----------------------------------
+API changes:
+- #1260834 by drunken monkey: Added a way to define custom data types.
+- #1308638 by drunken monkey: Reduce size of stored index settings.
+- #1291346 by drunken monkey: Expose SearchApiQuery::preExecute() and
+  postExecute().
+- #955088 by dereine, drunken monkey: Provide (additional) access functionality.
+- #1064884 by drunken monkey: Added support for indexing non-entities.
+
+Others:
+- #1304026 by drunken monkey: Utilize Facet API's 'include default facets' key
+  in searcher definitions.
+- #1231512 by drunken monkey: Use real Relationships instead of level magic in
+  Views integration.
+- #1260768 by drunken monkey: Move "Search pages" into its own project.
+- #1260812 by drunken monkey: Move "Database search" into its own project.
+- #1287602 by drunken monkey: Fixed „Index items immediately“ to delay indexing
+  on insert, too.
+- #1319500 by drunken monkey: Remove items after unsuccessful loads.
+- #1297958 by drunken monkey: Fixed wrong facet operator used for range facets.
+- #1305736 by drunken monkey: Fixed notice for unset Views group operator.
+- #1263214 by drunken monkey: Fixed indexing with „Index items immediately“
+  indexes old entity state.
+- #1228726 by drunken monkey, mh86: Increased size of 'options' fields in
+  database.
+- #1295144 by katbailey: Added alter hook for Facet API search keys.
+- #1294828 by drunken monkey: Fixed accidental presence of good OOP coding
+  standards in Views integration.
+- #1291376 by drunken monkey: Expose
+  SearchApiFacetapiAdapter::getCurrentSearch().
+- #1198764 by morningtime, drunken monkey: Fixed handling of Views filter
+  groups.
+- #1286500 by drunken monkey: Fixed „Search IDs” setting for facets not saved.
+- #1278780 by dereine, drunken monkey: Fixed status field requirement for node
+  access.
+- #1182614 by katbailey, cpliakas, drunken monkey, thegreat, das-peter: Added
+  Facet API integration.
+- #1278592 by das-peter: Fixed default view mode for non-entites or entities
+  without view modes.
+- #1251674 by Nick_vh: Fixed handling of empty fulltext keys in Views.
+- #1145306 by Nick_vh, drunken monkey: Fixed handling of multiple filters in the
+  database service class.
+- #1264164 by das-peter: Fixed the definition of the external data source
+  controller's trackItemChange() method.
+- #1262362 by drunken monkey: Fixed error handling for orphaned facets.
+- #1233426 by atlea: Fixed dirty and queued items don't get removed from the
+  tracking table when deleted.
+- #1258240 by drunken monkey: Fixed more overlooked entity type assumptions.
+- #1213698 by drunken monkey: Added a data alteration for indexing complete
+  hierarchies.
+- #1252208 by tedfordgif: Fixed superfluous query chars in active facet links.
+- #1224564 by drunken monkey: Added user language as a filter in Views.
+- #1242614 by jsacksick: Fixed division by zero in drush_search_api_status().
+- #1250168 by drunken monkey: Fixed deleted items aren't removed from servers.
+- #1236642 by jsacksick, drunken monkey: Fixed stale static cache of
+  search_api_get_item_type_info().
+- #1237348 by drunken monkey: Added a "Language control" data alteration.
+- #1214846 by drunken monkey, Kender: Fixed overlong table names when DB prefix
+  is used.
+- #1232478 by Damien Tournoud, drunken monkey: Fixed update of field type
+  settings for DB backend and index.
+- #1229772 by drunken monkey: Fixed order in which items are indexed.
+- #946624 by drunken monkey: Adapted to use a cron queue for indexing.
+- #1217702 by Amitaibu, drunken monkey: Added documentation on facet URLs.
+- #1214862 by drunken monkey: Added bundle-specific fields for related entities.
+- #1204964 by klausi: Fixed default index status is not overridden on saving.
+- #1191442 by drunken monkey: Fixed facets block view showing only tid.
+- #1161532 by drunken monkey: Fixed discerning between delete and revert in
+  hook_*_delete().
+
+Search API 1.0, Beta 10 (06/20/2011):
+-------------------------------------
+API changes:
+- #1068342 by drunken monkey: Added a 'fields to run on' option for processors.
+
+Others:
+- #1190086 by drunken monkey: Fixed crash in hook_entity_insert().
+- #1190324 by drunken monkey: Adapted to API change in Entity API.
+- #1168684 by drunken monkey: Added improved tokenizer defaults for English.
+- #1163096 by drunken monkey: Fixed cached types for DB servers aren't correctly
+  updated.
+- #1133864 by agentrickard, awolfey, greg.1.anderson, drunken monkey: Added
+  Drush integration.
+
+Search API 1.0, Beta 9 (06/06/2011):
+------------------------------------
+API changes:
+- #1089758 by becw, drunken monkey: Updated Views field handlers to utilize new
+  features.
+- #1150260 by drunken monkey: Added a way to let processors and data alterations
+  decide on which indexes they can run.
+- #1138992 by becw, drunken monkey: Added read-only indexes.
+
+Others:
+- #1179990 by j0rd: Fixed facet blocks don't correctly respect the "unlimited"
+  setting.
+- #1138650 by klausi, Amitaibu, drunken monkey: Fixed PHP strict warnings.
+- #1111852 by miiimooo, drunken monkey: Added a 'More like this' feature.
+- #1171360 by jbguerraz, drunken monkey: Added possibility to restrict the
+  options to display in an exposed options filter.
+- #1161676 by awolfey, drunken monkey: Added Stopwords processor.
+- #1166514 by drunken monkey: Fixed parseKeys() to handle incomplete quoting.
+- #1161966 by JoeMcGuire: Added Search API Spellcheck support for Pages.
+- #1118416 by drunken monkey: Added option to index entities instantly after
+  they are saved.
+- #1153298 by JoeMcGuire, drunken monkey: Added option getter and setter to
+  Views query handler.
+- #1147466 by awolfey: Added excerpt Views field.
+- #1152432 by morningtime: Fixed strict warnings in render() functions.
+- #1144400 by drunken monkey: Fixed use of entity_exportable_schema_fields() in
+  hook_schema().
+- #1141206 by drunken monkey: Fixed "quantity" variable for Search page pager.
+- #1117074 by drunken monkey: Fixed handling of overlong tokens by DB backend.
+- #1124548 by drunken monkey: Fixed syntax error in search_api.admin.inc.
+- #1134296 by klausi: Fixed check for NULL in SearchApiDbService->convert().
+- #1123604 by drunken monkey, fago: Added generalized "aggregation" data
+  alteration.
+- #1129226 by drunken monkey: Fixed incorrect handling of facets deactivated for
+  some search IDs.
+- #1086492 by drunken monkey: Fixed inadequate warnings when using the "Facets
+  block" display with wrong base table.
+- #1109308 by drunken monkey : Added option to choose between display of field
+  or facet name in "Current search" block.
+- #1120850 by drunken monkey, fangel: Fixed type of related entities in nested
+  lists.
+
+Search API 1.0, Beta 8 (04/02/2011):
+------------------------------------
+API changes:
+- #1012878 by drunken monkey: Added a way to index an entity directly.
+- #1109130 by drunken monkey: Added better structure for Views field rendering.
+
+Others:
+- #1018384 by drunken monkey: Fixed Views field names to not contain colons.
+- #1105704 by drunken monkey: Fixed exposed sorts always sort on 'top' sort.
+- #1104056 by drunken monkey: Added "Current search" support for non-facet
+  filters.
+- #1103814 by drunken monkey: Fixed missing argument for extractFields().
+- #1081084 by drunken monkey: Fixed notices in add_fulltext_field alteration.
+- #1091074 by drunken monkey, ygerasimov: Added machine names to "related
+  entities" list.
+- #1066278 by ygerasimov, drunken monkey: Removed
+  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):
+------------------------------------
+- #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):
+------------------------------------
+API changes:
+- #1075810 by drunken monkey: Added API function for marking entities as dirty.
+- #1043456 by drunken monkey: Added form validation/submission for plugins.
+- #1048032 by drunken monkey: Added a hook for altering the indexed items.
+
+Others:
+- #1068334 by drunken monkey: Added a data alteration for indexing the viewed
+  entity.
+- #1080376 by drunken monkey: Fixed "Current search" block field names.
+- #1076170 by drunken monkey: Added a Views display plugin for facet blocks.
+- #1064518 by drunken monkey: Added support for entity-based Views handlers.
+- #1080004 by drunken monkey: Fixed confusing "Current search" block layout.
+- #1071894 by drunken monkey: Fixed incorrect handling of boolean facets.
+- #1078590 by fago: Added check to skip default index creation when installed
+  via installation profile.
+- #1018366 by drunken monkey: Added option to hide active facet links.
+- #1058410 by drunken monkey: Added overhauled display of search results.
+- #1013632 by drunken monkey: Added facet support for the database backend.
+- #1069184: "Current search" block passes query parameters wrongly.
+- #1038016 by fago: Error upon indexing inaccessible list properties.
+- #1005532: Adaption to Entity API change (new optionsList() parameter).
+- #1057224 by TimLeytens: Problem with entity_uri('file').
+- #1051286: Show type/boost options only for indexed fields.
+- #1049978: Provide a "More" link for facets.
+- #1039250: Improve facet block titles.
+- #1043492: Problems with default (exported) entities.
+- #1037916 by fago: Updates must not call API functions.
+- #1032708 by larskleiner: Notice: Undefined variable: blocks.
+- #1032404 by larskleiner: Notice: Undefined index: fields.
+- #1032032 by pillarsdotnet: search_api_enable() aborts with a database error
+  on install.
+- #1026496: status doesn't get set properly when creating entities.
+- #1027992 by TimLeytens: Filter indexed items based on bundle.
+- #1024194 by TimLeytens: Provide a search block for each page.
+- #1028042: Change {search_api_item}.index_id back to an INT.
+- #1021664: Paged views results empty when adding facet.
+- #872912: Write tests.
+- #1013018: Make the "Fulltext field" data alteration more useful.
+- #1024514: Error when preprocessing muli-valued fulltext fields.
+- #1020372: CSS classes for facets.
+
+Search API 1.0, Beta 5 (01/05/2011):
+------------------------------------
+API changes:
+- #917998: Enhance data alterations by making them objects.
+- #991632: Incorporate newly available entity hooks.
+- #963062: Make facets exportable.
+
+Others:
+- #1018544: includes/entity.inc mentioned in a few places.
+- #1011458: Move entity and processor classes into individual files.
+- #1012478: HTML in node bodies is escaped in Views.
+- #1014548: Add newly required database fields for entities.
+- #915174: Remove unnecessary files[] declarations from .info files.
+- #988780: Merge of entity modules.
+- #997852: Service config oddities.
+- #994948: "Add index" results in blank page.
+- #993470: Unnecessary warning when no valid keys or filters are given.
+- #986412: Notice: Undefined index: value in theme_search_api_page_result().
+- #987928: EntityDBExtendable::__sleep() is gone.
+- #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):
+------------------------------------
+API changes:
+- #976876: Move Solr module into its own project.
+- #962582: Cross-entity searches (API addition).
+- #939482 by fago: Fix exportables.
+- #939092: Several API changes regarding service class methods.
+- #939414: Enhanced service class descriptions. [soft API change]
+- #939464: Documented Entity API's module and status properties.
+- #939092: Changed private members to protected in all classes.
+- #936360: Make servers and indexes exportable.
+
+Others:
+- #966512: "Time ago" option for Views date fields (+bug fix for missing value).
+- #965318: Lots of notices if entities are missing in Views.
+- #961210: Hide error messages.
+- #963756: Array to string conversion error.
+- #961276: Some random bugs.
+- #961122: Exportability UI fixes.
+- #913858: Fix adding properties that are lists of entities.
+- #961210: Don't hide error messages.
+- #961122: Display configuration status when viewing entities.
+- #889286: EntityAPIController::load() produces WSoD sometimes.
+- #958378 by marvil07: "Clear index" is broken
+- #955892: Typo in search_api_solr.install.
+- #951830: "List of language IDs" context suspicious.
+- #939414: Rename "data-alter callbacks" to "data alterations".
+- #939460: Views integration troubles.
+- #945754: Fix server and index machine name inputs.
+- #943578: Duplicate fields on service creation.
+- #709892: Invoke hook_entity_delete() on entity deletions.
+- #939414: Set fields provided by data-alter callbacks to "indexed" by default.
+- #939414: Provide a default node index upon installation.
+- #939822 by fago: Support fields.
+- #939442: Bad data type defaults [string for fields with options].
+- #939482: Override export() to work with "magic" __get fields.
+- #939442: Bad data type defaults.
+- #939414: Improved descriptions for processors.
+- #939414: Removed the "Call hook" data alter callback.
+- #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):
+------------------------------------
+- API mostly stable.
+- Five contrib modules exist:
+  - search_api_db
+  - search_api_solr
+  - search_api_page
+  - search_api_views
+  - search_api_facets

+ 339 - 0
sites/all/modules/contrib/search/search_api/LICENSE.txt

@@ -0,0 +1,339 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                    GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+                            NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License along
+    with this program; if not, write to the Free Software Foundation, Inc.,
+    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.

+ 399 - 0
sites/all/modules/contrib/search/search_api/README.txt

@@ -0,0 +1,399 @@
+Search API
+----------
+
+This module provides a framework for easily creating searches on any entity
+known to Drupal, using any kind of search engine. For site administrators, it is
+a great alternative to other search solutions, since it already incorporates
+facetting support and the ability to use the Views module for displaying search
+results, filters, etc. Also, with the Apache Solr integration [1], a
+high-performance search engine is available for use with the Search API.
+
+If you need help with the module, please post to the project's issue queue [2].
+
+[1] http://drupal.org/project/search_api_solr
+[2] http://drupal.org/project/issues/search_api
+
+
+Content:
+ - Glossary
+ - Information for users
+ - Information for developers
+ - Included components
+
+
+Glossary
+--------
+
+Terms as used in this module.
+
+- Service class:
+  A type of search engine, e.g. using the database, Apache Solr,
+  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.
+- 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
+  still need to be indexed (or re-indexed, if they were updated). Needs to lie
+  on a server in order to be really used (although configuration is independent
+  of a server).
+- Item type:
+  A type of data which can be indexed (i.e., for which indexes can be created).
+  Most entity types (like Content, User, Taxonomy term, etc.) are available, but
+  possibly also other types provided by contrib modules.
+- Entity:
+  One object of data, usually stored in the database. Might for example
+  be a node, a user or a file.
+- Field:
+  A defined property of an entity, like a node's title or a user's mail address.
+  All fields have defined datatypes. However, for indexing purposes the user
+  might choose to index a property under a different data type than defined.
+- Data type:
+  Determines how a field is indexed. While "Fulltext" fields can be completely
+  searched for keywords, other fields can only be used for filtering. They will
+  also be converted to fit their respective value ranges.
+  How types other than "Fulltext" are handled depends on the service class used.
+  Its documentation should state how the type-selection affect the indexed
+  content. However, service classes will always be able to handle all data
+  types, it is just possible that the type doesn't affect the indexing at all
+  (apart from "Fulltext vs. the rest").
+- Boost:
+  Number determining how important a certain field is, when searching for
+  fulltext keywords. The higher the value is, the more important is the field.
+  E.g., when the node title has a boost of 5.0 and the node body a boost of 1.0,
+  keywords found in the title will increase the score as much as five keywords
+  found in the body. Of course, this has only an effect when the score is used
+  (for sorting or other purposes). It has no effect on other parts of the search
+  result.
+- Data alteration:
+  A component that is used when indexing data. It can add additional fields to
+  the indexed entity or prevent certain entities from being indexed. Fields
+  added by callbacks have to be enabled on the "Fields" page to be of any use,
+  but this is done by default.
+- Processor:
+  An object that is used for preprocessing indexed data as well as search
+  queries, and for postprocessing search results. Usually only work on fulltext
+  fields to control how content is indexed and searched. E.g., processors can be
+  used to make searches case-insensitive, to filter markup out of indexed
+  content, etc.
+
+
+Information for users
+---------------------
+
+IMPORTANT: Access checks
+  In general, the Search API doesn't contain any access checks for search
+  results. It is your responsibility to ensure that only accessible search
+  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 "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
+  checks will automatically be executed at search time, showing only those
+  results that a user can view. Some search types (e.g., search views) also
+  provide the option to disable these access checks for individual searches.
+  Please note, however, that these access checks use the indexed data, while
+  usually the current data is displayed to users. Therefore, users might still
+  see inappropriate content as long as items aren't indexed in their latest
+  state. If you can't allow this for your site, please use the index's "Index
+  immediately" feature (explained below) or possibly custom solutions for
+  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")
+which can be found at [3]).
+
+[3] http://drupal.org/project/search_api_db
+
+- Creating a server
+  (Configuration > Search API > Add server)
+
+The most basic thing you have to create is a search server for indexing content.
+Go to Configuration > Search API in the administration pages and select
+"Add server". Name and description are usually only shown to administrators and
+can be used to differentiate between several servers, or to explain a server's
+use to other administrators (for larger sites). Disabling a server makes it
+unusable for indexing and searching and can e.g. be used if the underlying
+search engine is temporarily unavailable.
+The "service class" is the most important option here, since it lets you select
+which backend the search server will use. This cannot be changed after the
+server is created.
+Depending on the selected service class, further, service-specific settings will
+be available. For details on those settings, consult the respective service's
+documentation.
+
+- Creating an index
+  (Configuration > Search API > Add index)
+
+For adding a search index, choose "Add index" on the Search API administration
+page. Name, description and "enabled" status serve the exact same purpose as
+for servers.
+The most important option in this form is the indexed entity type. Every index
+contains data on only a single type of entities, e.g. nodes, users or taxonomy
+terms. This is therefore the only option that cannot be changed afterwards.
+The server on which the index lies determines where the data will actually be
+indexed. It doesn't affect any other settings of the index and can later be
+changed with the only drawback being that the index' content will have to be
+indexed again. You can also select a server that is at the moment disabled, or
+choose to let the index lie on no server at all, for the time being. Note,
+however, that you can only create enabled indexes on an enabled server. Also,
+disabling a server will disable all indexes that lie on it.
+The "Index items immediately" option specifies that you want items to be
+directly re-indexed after being changed, instead of waiting for the next cron
+run. Use this if it is important that users see no stale data in searches, and
+only when your setup enables relatively fast indexing.
+Lastly, the "Cron batch size" option allows you to set whether items will be
+indexed when cron runs (as long as the index is enabled), and how many items
+will be indexed in a single batch. The best value for this setting depends on
+how time-consuming indexing is for your setup, which in turn depends mostly on
+the server used and the enabled data alterations. You should set it to a number
+of items which can easily be indexed in 10 seconds' time. Items can also be
+indexed manually, or directly when they are changed, so even if this is set to
+0, the index can still be used.
+
+- Indexed fields
+  (Configuration > Search API > [Index name] > Fields)
+
+Here you can select which of the entities' fields will be indexed, and how.
+Fields added by (enabled) data alterations will be available here, too.
+Without selecting fields to index, the index will be useless and also won't be
+available for searches. Select the "Fulltext" data type for fields which you
+want search for keywords, and other data types when you want to use the field
+for filtering (e.g., as facets). The "Item language" field will always be
+indexed as it contains important information for processors and hooks.
+You can also add fields of related entities here, via the "Add related fields"
+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.
+
+- 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
+further explanations).
+Data alterations usually only add one or more fields to the entity and their
+order is mostly irrelevant.
+The order of processors, however, often is important. Read the processors'
+descriptions or consult their documentation for determining how to use them most
+effectively.
+
+- Index status
+  (Configuration > Search API > [Index name] > Status)
+
+On this page you can view how much of the entities are already indexed and also
+control indexing. With the "Index now" button (displayed only when there are
+still unindexed items) you can directly index a certain number of "dirty" items
+(i.e., items not yet indexed in their current state). Setting "-1" as the number
+will index all of those items, similar to the cron batch size setting.
+When you change settings that could affect indexing, and the index is not
+automatically marked for re-indexing, you can do this manually with the
+"Re-index content" button. All items in the index will be marked as dirty and be
+re-indexed when subsequently indexing items (either manually or via cron runs).
+Until all content is re-indexed, the old data will still show up in searches.
+This is different with the "Clear index" button. All items will be marked as
+dirty and additionally all data will be removed from the index. Therefore,
+searches won't show any results until items are re-indexed, after clearing an
+index. Use this only if completely wrong data has been indexed. It is also done
+automatically when the index scheme or server settings change too drastically to
+keep on using the old data.
+
+- Hidden settings
+
+search_api_index_worker_callback_runtime:
+  By changing this variable, you can determine the time (in seconds) the Search
+  API will spend indexing (for all indexes combined) in each cron run. The
+  default is 15 seconds.
+
+
+Information for developers
+--------------------------
+
+ | NOTE:
+ | For modules providing new entities: In order for your entities to become
+ | searchable with the Search API, your module will need to implement
+ | hook_entity_property_info() in addition to the normal hook_entity_info().
+ | hook_entity_property_info() is documented in the entity module.
+ | For making certain non-entities searchable, see "Item type" below.
+ | 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 [4].
+ |
+ | [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
+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. 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
+with doc comments for a developer to find the right methods to override or
+implement.
+
+- Service class
+  Interface: SearchApiServiceInterface
+  Base class: SearchApiAbstractService
+  Hook: hook_search_api_service_info()
+
+The service classes are the heart of the API, since they allow data to be
+indexed on different search servers. Since these are quite some work to get
+right, you should probably make sure a service class for a specific search
+engine doesn't exist already before programming it yourself.
+When your module supplies a service class, please make sure to provide
+documentation (at least a README.txt) that clearly states the datatypes it
+supports (and in what manner), how a direct query (a query where the keys are
+a single string, instead of an array) is parsed and possible limitations of the
+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 provided by [5] for an example implementation.
+
+[5] http://drupal.org/project/search_api_db
+
+- Query class
+  Interface: SearchApiQueryInterface
+  Base class: SearchApiQuery
+
+You can also override the query class' behaviour for your service class. You
+can, for example, change key parsing behaviour, add additional parse modes
+specific to your service, or override methods so the information is stored more
+suitable for your service.
+For the query class to become available (other than through manual creation),
+you need a custom service class where you override the query() method to return
+an instance of your query class.
+
+- Item type
+  Interface: SearchApiDataSourceControllerInterface
+  Base class: SearchApiAbstractDataSourceController
+  Hook: hook_search_api_item_type_info()
+
+If you want to index some data which is not defined as an entity, you can
+specify it as a new item type here. For defining a new item type, you have to
+create a data source controller for the type and track new, changed and deleted
+items of the type by calling the search_api_track_item_*() functions.
+An instance of the data source controller class will then be used by indexes
+when handling items of your newly-defined type.
+
+If you want to make external data that is indexed on some search server
+available to the Search API, there is a handy base class for your data source
+controller (SearchApiExternalDataSourceController in
+includes/datasource_external.inc) which you can extend. For a minimal use case,
+you will then only have to define the available fields that can be retrieved by
+the server.
+
+- Data type
+  Hook: hook_search_api_data_type_info()
+
+You can specify new data types for indexing fields. These new types can then be
+selected on indexes' „Fields“ tabs. You just have to implement the hook,
+returning some information on your data type, and specify in your module's
+documentation the format of your data type and how it should be used.
+
+For a custom data type to have an effect, in most cases the server's service
+class has to support that data type. A service class can advertize its support
+of a data type by declaring support for the "search_api_data_type_TYPE" feature
+in its supportsFeature() method. If this support isn't declared, a fallback data
+type is automatically used instead of the custom one.
+
+If a field is indexed with a custom data type, its entry in the index's options
+array will have the selected type in "real_type", while "type" contains the
+fallback type (which is always one of the default data types, as returned by
+search_api_default_field_types().
+
+- Data-alter callbacks
+  Interface: SearchApiAlterCallbackInterface
+  Base class: SearchApiAbstractAlterCallback
+  Hook: hook_search_api_alter_callback_info()
+
+Data alter callbacks can be used to change the field data of indexed items, or
+to prevent certain items from being indexed. They are only used when indexing,
+or when selecting the fields to index. For adding additional information to
+search results, you have to use a processor.
+Data-alter callbacks are called "data alterations" in the UI.
+
+- Processors
+  Interface: SearchApiProcessorInterface
+  Base class: SearchApiAbstractProcessor
+  Hook: hook_search_api_processor_info()
+
+Processors are used for altering the data when indexing or searching. The exact
+specifications are available in the interface's doc comments. Just note that the
+processor description should clearly state assumptions or restrictions on input
+types (e.g. only tokenized text), item language, etc. and explain concisely what
+effect it will have on searches.
+See the processors in includes/processor.inc for examples.
+
+
+Included components
+-------------------
+
+- Data alterations
+
+  * URL field
+    Provides a field with the URL for displaying the entity.
+  * Aggregated fields
+    Offers the ability to add additional fields to the entity, containing the
+    data from one or more other fields. Use this, e.g., to have a single field
+    containing all data that should be searchable, or to make the text from a
+    string field, like a taxonomy term, also fulltext-searchable.
+    The type of aggregation can be selected from a set of values: you can, e.g.,
+    collect the text data of all contained fields, or add them up, count their
+    values, etc.
+  * Bundle filter
+    Enables the admin to prevent entities from being indexed based on their
+    bundle (content type for nodes, vocabulary for taxonomy terms, etc.).
+  * Complete entity view
+    Adds a field containing the whole HTML content of the entity as it is viewed
+    on the site. The view mode used can be selected.
+    Note, however, that this might not work for entities of all types. All core
+    entities except files are supported, though.
+  * Index hierarchy
+    Allows to index a hierarchical field along with all its parents. Most
+    importantly, this can be used to index taxonomy term references along with
+    all parent terms. This way, when an item, e.g., has the term "New York", it
+    will also be matched when filtering for "USA" or "North America".
+
+- Processors
+
+  * Ignore case
+    Makes all fulltext searches (and, optionally, also filters on string values)
+    case-insensitive. Some servers might do this automatically, for others this
+    should probably always be activated.
+  * HTML filter
+    Strips HTML tags from fulltext fields and decodes HTML entities. If you are
+    indexing HTML content (like node bodies) and the search server doesn't
+    handle HTML on its own, this should be activated to avoid indexing HTML
+    tags, as well as to give e.g. terms appearing in a heading a higher boost.
+  * Tokenizer
+    This processor allows you to specify how indexed fulltext content is split
+    into seperate tokens – which characters are ignored and which treated as
+    white-space that seperates words.
+  * Stopwords
+    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.
+
+- Additional modules
+
+  * Search views
+    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.
+
+[6] http://drupal.org/project/views

+ 39 - 0
sites/all/modules/contrib/search/search_api/boosts-and-queryconditon.patch

@@ -0,0 +1,39 @@
+diff --git a/search_api.admin.inc b/search_api.admin.inc
+index 5fbc8d8..9a5122e 100644
+--- a/search_api.admin.inc
++++ b/search_api.admin.inc
+@@ -1480,8 +1480,8 @@ function search_api_admin_index_fields(array $form, array &$form_state, SearchAp
+   $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.1', '0.2', '0.3', '0.5', '0.8', '1.0', '2.0', '3.0', '5.0', '8.0', '13.0', '21.0', '5000', '10000', '20000', '40000', '80000', '160000', '320000'));
++  $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', '100', '1000', '1010', '1020', '1030', '1040', '1050', '1060'));
+   $form_state['index'] = $index;
+   $form['#theme'] = 'search_api_admin_fields_table';
+   $form['#tree'] = TRUE;
+diff --git a/search_api.module b/search_api.module
+index bba0681..ba27465 100644
+--- a/search_api.module
++++ b/search_api.module
+@@ -1444,7 +1444,7 @@ function _search_api_query_add_node_access($account, SearchApiQueryInterface $qu
+       $query->filter($filter);
+     }
+     else {
+-      $query->condition('status', NODE_PUBLISHED);
++      // $query->condition('status', NODE_PUBLISHED);
+     }
+     // Filter by node access grants.
+     $filter = $query->createFilter('OR');
+@@ -1636,6 +1636,10 @@ function search_api_extract_fields(EntityMetadataWrapper $wrapper, array $fields
+   foreach ($nested as $prefix => $nested_fields) {
+     if (isset($wrapper->$prefix)) {
+       $nested_fields = search_api_extract_fields($wrapper->$prefix, $nested_fields, $value_options);
++      # http://drupal.org/node/1873910#comment-6876200
++      // $subwrapper = $wrapper->$prefix;
++      // $subwrapper->language( $wrapper->language->value() );
++      // $nested_fields = search_api_extract_fields($subwrapper, $nested_fields, $value_options);
+       foreach ($nested_fields as $field => $info) {
+         $fields["$prefix:$field"] = $info;
+       }

+ 125 - 0
sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/README.txt

@@ -0,0 +1,125 @@
+Search facets
+-------------
+
+This module allows you to create facetted searches for any search executed via
+the Search API, no matter if executed by a search page, a view or any other
+module. The only thing you'll need is a search service class that supports the
+"search_api_facets" feature. Currently, the "Database search" and "Solr search"
+modules supports this.
+
+This module is built on the Facet API [1], which is needed for this module to
+work.
+
+[1] http://drupal.org/project/facetapi
+
+
+Information for site builders
+-----------------------------
+
+For creating a facetted search, you first need a search. Create or find some
+page displaying Search API search results, either via a search page, a view or
+by any other means. Now go to the configuration page for the index on which
+this search is executed.
+If the index lies on a server supporting facets (and if this module is enabled),
+you'll notice a "Facets" tab. Click it and it will take you to the index' facet
+configuration page. You'll see a table containing all indexed fields and options
+for enabling and configuring facets for them.
+For a detailed explanation of the available options, please refer to the Facet
+API documentation.
+
+- Creating facets via the URL
+
+Facets can be added to a search (for which facets are activated) by passing
+appropriate GET parameters in the URL. Assuming you have an indexed field with
+the machine name "field_price", you can filter on it in the following ways:
+
+- Filter for a specific value. For finding only results that have a price of
+  exactly 100, pass the following $options to url() or l():
+
+  $options['query']['f'][] = 'field_price:100';
+
+  Or manually append the following GET parameter to a URL:
+
+  ?f[0]=field_price:100
+
+- Search for values in a specified range. The following example will only return
+  items that have a price greater than or equal to 100 and lower than 500.
+
+  Code: $options['query']['f'][] = 'field_price:[100 TO 500]';
+  URL:  ?f[0]=field_price%3A%5B100%20TO%20500%5D
+
+- Search for values above a value. The next example will find results which have
+  a price greater than or equal to 100. The asterisk (*) stands for "unlimited",
+  meaning that there is no upper limit. Filtering for values lower than a
+  certain value works equivalently.
+
+  Code: $options['query']['f'][] = 'field_price:[100 TO *]';
+  URL:  ?f[0]=field_price%3A%5B100%20TO%20%2A%5D
+
+- Search for missing values. This example will filter out all items which have
+  any value at all in the price field, and will therefore only list items on
+  which this field was omitted. (This naturally only makes sense for fields
+  that aren't required.)
+
+  Code: $options['query']['f'][] = 'field_price:!';
+  URL:  ?f[0]=field_price%3A%21
+
+- Search for present values. The following example will only return items which
+  have the price field set (regardless of the actual value). You can see that it
+  is actually just a range filter with unlimited lower and upper bound.
+
+  Code: $options['query']['f'][] = 'field_price:[* TO *]';
+  URL:  ?f[0]=field_price%3A%5B%2A%20TO%20%2A%5D
+
+Note: When filtering a field whose machine name contains a colon (e.g.,
+"author:roles"), you'll have to additionally URL-encode the field name in these
+filter values:
+  Code: $options['query']['f'][] = rawurlencode('author:roles') . ':100';
+  URL:  ?f[0]=author%253Aroles%3A100
+
+- Issues
+
+If you find any bugs or shortcomings while using this module, please file an
+issue in the project's issue queue [1], using the "Facets" component.
+
+[1] http://drupal.org/project/issues/search_api
+
+
+Information for developers
+--------------------------
+
+- Features
+
+If you are the developer of a SearchApiServiceInterface implementation and want
+to support facets with your service class, too, you'll have to support the
+"search_api_facets" feature. You can find details about the necessary additions
+to your class in the example_servive.php file. In short, you'll just, when
+executing a query, have to return facet terms and counts according to the
+query's "search_api_facets" option, if present.
+In order for the module to be able to tell that your server supports facets,
+you will also have to change your service's supportsFeature() method to
+something like the following:
+  public function supportsFeature($feature) {
+    return $feature == 'search_api_facets';
+  }
+
+There is also a second feature defined by this module, namely
+"search_api_facets_operator_or", for supporting "OR" facets. The requirements
+for this feature are also explained in the example_servive.php file.
+
+- Query option
+
+The facets created follow the "search_api_base_path" option on the search query.
+If set, this path will be used as the base path from which facet links will be
+created. This can be used to show facets on pages without searches – e.g., as a
+landing page.
+
+- Hidden variable
+
+The module uses one hidden variable, "search_api_facets_search_ids", to keep
+track of the search IDs of searches executed for a given index. It is only
+updated when a facet is displayed for the respective search, so isn't really a
+reliable measure for this.
+In any case, if you e.g. did some test searches and now don't want them to show
+up in the block configuration forever after, just clear the variable:
+  variable_del("search_api_facets_search_ids")

+ 209 - 0
sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/example_service.php

@@ -0,0 +1,209 @@
+<?php
+
+/**
+ * @file
+ * Example implementation for a service class which supports facets.
+ */
+
+/**
+ * Example class explaining how facets can be supported by a service class.
+ *
+ * This class defines the "search_api_facets" and
+ * "search_api_facets_operator_or" features. Read the method documentation and
+ * inline comments in search() to learn how they can be supported by a service
+ * class.
+ */
+abstract class SearchApiFacetapiExampleService extends SearchApiAbstractService {
+
+  /**
+   * 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.
+   *
+   * If the service class supports facets, it should return TRUE if called with
+   * the feature name "search_api_facets". If it also supports "OR" facets, it
+   * should also return TRUE if called with "search_api_facets_operator_or".
+   *
+   * @param string $feature
+   *   The name of the optional feature.
+   *
+   * @return boolean
+   *   TRUE if this service knows and supports the specified feature. FALSE
+   *   otherwise.
+   */
+  public function supportsFeature($feature) {
+    $supported = array(
+      'search_api_facets' => TRUE,
+      'search_api_facets_operator_or' => TRUE,
+    );
+    return isset($supported[$feature]);
+  }
+
+  /**
+   * Executes a search on the server represented by this object.
+   *
+   * If the service class supports facets, it should check for an additional
+   * option on the query object:
+   * - search_api_facets: An array of facets to return along with the results
+   *   for this query. The array is keyed by an arbitrary string which should
+   *   serve as the facet's unique identifier for this search. The values are
+   *   arrays with the following keys:
+   *   - field: The field to construct facets for.
+   *   - limit: The maximum number of facet terms to return. 0 or an empty
+   *     value means no limit.
+   *   - min_count: The minimum number of results a facet value has to have in
+   *     order to be returned.
+   *   - missing: If TRUE, a facet for all items with no value for this field
+   *     should be returned (if it conforms to limit and min_count).
+   *   - operator: (optional) If the service supports "OR" facets and this key
+   *     contains the string "or", the returned facets should be "OR" facets. If
+   *     the server doesn't support "OR" facets, this key can be ignored.
+   *
+   * The basic principle of facets is explained quite well in the
+   * @link http://en.wikipedia.org/wiki/Faceted_search Wikipedia article on
+   * "Faceted search" @endlink. Basically, you should return for each field
+   * filter values which would yield some results when used with the search.
+   * E.g., if you return for a field $field the term $term with $count results,
+   * the given $query along with
+   *   $query->condition($field, $term)
+   * should yield exactly (or about) $count results.
+   *
+   * For "OR" facets, all existing filters on the facetted field should be
+   * ignored for computing the facets.
+   *
+   * @param $query
+   *   The SearchApiQueryInterface object to execute.
+   *
+   * @return array
+   *   An associative array containing the search results, as required by
+   *   SearchApiQueryInterface::execute().
+   *   In addition, if the "search_api_facets" option is present on the query,
+   *   the results should contain an array of facets in the "search_api_facets"
+   *   key, as specified by the option. The facets array should be keyed by the
+   *   facets' unique identifiers, and contain a numeric array of facet terms,
+   *   sorted descending by result count. A term is represented by an array with
+   *   the following keys:
+   *   - count: Number of results for this term.
+   *   - filter: The filter to apply when selecting this facet term. A filter is
+   *     a string of one of the following forms:
+   *     - "VALUE": Filter by the literal value VALUE (always include the
+   *       quotes, not only for strings).
+   *     - [VALUE1 VALUE2]: Filter for a value between VALUE1 and VALUE2. Use
+   *       parantheses for excluding the border values and square brackets for
+   *       including them. An asterisk (*) can be used as a wildcard. E.g.,
+   *       (* 0) or [* 0) would be a filter for all negative values.
+   *     - !: Filter for items without a value for this field (i.e., the
+   *       "missing" facet).
+   *
+   * @throws SearchApiException
+   *   If an error prevented the search from completing.
+   */
+  public function search(SearchApiQueryInterface $query) {
+    // We assume here that we have an AI search which understands English
+    // commands.
+
+    // First, create the normal search query, without facets.
+    $search = new SuperCoolAiSearch($query->getIndex());
+    $search->cmd('create basic search for the following query', $query);
+    $ret = $search->cmd('return search results in Search API format');
+
+    // Then, let's see if we should return any facets.
+    if ($facets = $query->getOption('search_api_facets')) {
+      // For the facets, we need all results, not only those in the specified
+      // range.
+      $results = $search->cmd('return unlimited search results as a set');
+      foreach ($facets as $id => $facet) {
+        $field = $facet['field'];
+        $limit = empty($facet['limit']) ? 'all' : $facet['limit'];
+        $min_count = $facet['min_count'];
+        $missing = $facet['missing'];
+        $or = isset($facet['operator']) && $facet['operator'] == 'or';
+
+        // If this is an "OR" facet, existing filters on the field should be
+        // ignored for computing the facets.
+        // You can ignore this if your service class doesn't support the
+        // "search_api_facets_operator_or" feature.
+        if ($or) {
+          // We have to execute another query (in the case of this hypothetical
+          // search backend, at least) to get the right result set to facet.
+          $tmp_search = new SuperCoolAiSearch($query->getIndex());
+          $tmp_search->cmd('create basic search for the following query', $query);
+          $tmp_search->cmd("remove all conditions for field $field");
+          $tmp_results = $tmp_search->cmd('return unlimited search results as a set');
+        }
+        else {
+          // Otherwise, we can just use the normal results.
+          $tmp_results = $results;
+        }
+
+        $filters = array();
+        if ($search->cmd("$field is a date or numeric field")) {
+          // For date, integer or float fields, range facets are more useful.
+          $ranges = $search->cmd("list $limit ranges of field $field in the following set", $tmp_results);
+          foreach ($ranges as $range) {
+            if ($range->getCount() >= $min_count) {
+              // Get the lower and upper bound of the range. * means unlimited.
+              $lower = $range->getLowerBound();
+              $lower = ($lower == SuperCoolAiSearch::RANGE_UNLIMITED) ? '*' : $lower;
+              $upper = $range->getUpperBound();
+              $upper = ($upper == SuperCoolAiSearch::RANGE_UNLIMITED) ? '*' : $upper;
+              // Then, see whether the bounds are included in the range. These
+              // can be specified independently for the lower and upper bound.
+              // Parentheses are used for exclusive bounds, square brackets are
+              // used for inclusive bounds.
+              $lowChar = $range->isLowerBoundInclusive() ? '[' : '(';
+              $upChar = $range->isUpperBoundInclusive() ? ']' : ')';
+              // Create the filter, which separates the bounds with a single
+              // space.
+              $filter = "$lowChar$lower $upper$upChar";
+              $filters[$filter] = $range->getCount();
+            }
+          }
+        }
+        else {
+          // Otherwise, we use normal single-valued facets.
+          $terms = $search->cmd("list $limit values of field $field in the following set", $tmp_results);
+          foreach ($terms as $term) {
+            if ($term->getCount() >= $min_count) {
+              // For single-valued terms, we just need to wrap them in quotes.
+              $filter = '"' . $term->getValue() . '"';
+              $filters[$filter] = $term->getCount();
+            }
+          }
+        }
+
+        // If we should also return a "missing" facet, compute that as the
+        // number of results without a value for the facet field.
+        if ($missing) {
+          $count = $search->cmd("return number of results without field $field in the following set", $tmp_results);
+          if ($count >= $min_count) {
+            $filters['!'] = $count;
+          }
+        }
+
+        // Sort the facets descending by result count.
+        arsort($filters);
+
+        // With the "missing" facet, we might have too many facet terms (unless
+        // $limit was empty and is therefore now set to "all"). If this is the
+        // case, remove those with the lowest number of results.
+        while (is_numeric($limit) && count($filters) > $limit) {
+          array_pop($filters);
+        }
+
+        // Now add the facet terms to the return value, as specified in the doc
+        // comment for this method.
+        foreach ($filters as $filter => $count) {
+          $ret['search_api_facets'][$id][] = array(
+            'count' => $count,
+            'filter' => $filter,
+          );
+        }
+      }
+    }
+
+    // Return the results, which now also includes the facet information.
+    return $ret;
+  }
+
+}

+ 276 - 0
sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/plugins/facetapi/adapter.inc

@@ -0,0 +1,276 @@
+<?php
+
+/**
+ * @file
+ * Classes used by the Facet API module.
+ */
+
+/**
+ * Facet API adapter for the Search API module.
+ */
+class SearchApiFacetapiAdapter extends FacetapiAdapter {
+
+  /**
+   * Cached value for the current search for this searcher, if any.
+   *
+   * @see getCurrentSearch()
+   *
+   * @var array
+   */
+  protected $current_search;
+
+  /**
+   * The active facet fields for the current search.
+   *
+   * @var array
+   */
+  protected $fields = array();
+
+  /**
+   * Returns the path to the admin settings for a given realm.
+   *
+   * @param $realm_name
+   *   The name of the realm.
+   *
+   * @return
+   *   The path to the admin settings.
+   */
+  public function getPath($realm_name) {
+    $base_path = 'admin/config/search/search_api';
+    $index_id = $this->info['instance'];
+    return $base_path . '/index/' . $index_id . '/facets/' . $realm_name;
+  }
+
+  /**
+   * Overrides FacetapiAdapter::getSearchPath().
+   */
+  public function getSearchPath() {
+    $search = $this->getCurrentSearch();
+    if ($search && $search[0]->getOption('search_api_base_path')) {
+      return $search[0]->getOption('search_api_base_path');
+    }
+    return $_GET['q'];
+  }
+
+  /**
+   * Allows the backend to initialize its query object before adding the facet filters.
+   *
+   * @param mixed $query
+   *   The backend's native object.
+   */
+  public function initActiveFilters($query) {
+    $search_id = $query->getOption('search id');
+    $index_id = $this->info['instance'];
+    $facets = facetapi_get_enabled_facets($this->info['name']);
+    $this->fields = array();
+
+    // We statically store the current search per facet so that we can correctly
+    // assign it when building the facets. See the build() method in the query
+    // type plugin classes.
+    $active = &drupal_static('search_api_facetapi_active_facets', array());
+    foreach ($facets as $facet) {
+      $options = $this->getFacet($facet)->getSettings()->settings;
+      // The 'default_true' option is a choice between "show on all but the
+      // selected searches" (TRUE) and "show for only the selected searches".
+      $default_true = isset($options['default_true']) ? $options['default_true'] : TRUE;
+      // The 'facet_search_ids' option is the list of selected searches that
+      // will either be excluded or for which the facet will exclusively be
+      // displayed.
+      $facet_search_ids = isset($options['facet_search_ids']) ? $options['facet_search_ids'] : array();
+
+      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.
+        }
+      }
+      elseif ($default_true) {
+        continue; // The 'facet_search_ids' in the settings are to be excluded.
+      }
+      $active[$facet['name']] = $search_id;
+      $this->fields[$facet['name']] = array(
+        'field'             => $facet['field'],
+        'limit'             => $options['hard_limit'],
+        'operator'          => $options['operator'],
+        'min_count'         => $options['facet_mincount'],
+        'missing'           => $options['facet_missing'],
+      );
+    }
+  }
+
+  /**
+   * Add the given facet to the query.
+   */
+  public function addFacet(array $facet, SearchApiQueryInterface $query) {
+    if (isset($this->fields[$facet['name']])) {
+      $options = &$query->getOptions();
+      $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;
+    }
+  }
+
+  /**
+   * Returns a boolean flagging whether $this->_searcher executed a search.
+   */
+  public function searchExecuted() {
+    return (bool) $this->getCurrentSearch();
+  }
+
+  /**
+   * Helper method for getting a current search for this searcher.
+   *
+   * @return array
+   *   The first matching current search, in the form specified by
+   *   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)) {
+      $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
+      // cannot match it up to the correct "current search".
+      // 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) = $search;
+        if ($query->getIndex()->machine_name == $index_id) {
+          $this->current_search = $search;
+        }
+      }
+    }
+    return $this->current_search;
+  }
+
+  /**
+   * Returns a boolean flagging whether facets in a realm shoud be displayed.
+   *
+   * Useful, for example, for suppressing sidebar blocks in some cases.
+   *
+   * @return
+   *   A boolean flagging whether to display a given realm.
+   */
+  public function suppressOutput($realm_name) {
+    // Not sure under what circumstances the output will need to be suppressed?
+    return FALSE;
+  }
+
+  /**
+   * Returns the search keys.
+   */
+  public function getSearchKeys() {
+    $search = $this->getCurrentSearch();
+    $keys = $search[0]->getOriginalKeys();
+    if (is_array($keys)) {
+      // This will happen nearly never when displaying the search keys to the
+      // user, so go with a simple work-around.
+      // If someone complains, we can easily add a method for printing them
+      // properly.
+      $keys = '[' . t('complex query') . ']';
+    }
+    drupal_alter('search_api_facetapi_keys', $keys, $search[0]);
+    return $keys;
+  }
+
+  /**
+   * Returns the number of total results found for the current search.
+   */
+  public function getResultCount() {
+    $search = $this->getCurrentSearch();
+    // Each search is an array with the query as the first element and the results
+    // array as the second.
+    if (isset($search[1])) {
+      return $search[1]['result count'];
+    }
+    return 0;
+  }
+
+  /**
+   * Allows for backend specific overrides to the settings form.
+   */
+  public function settingsForm(&$form, &$form_state) {
+    $facet = $form['#facetapi']['facet'];
+    $facet_settings = $this->getFacet($facet)->getSettings();
+    $options = $facet_settings->settings;
+    $search_ids = variable_get('search_api_facets_search_ids', array());
+    $search_ids = isset($search_ids[$this->info['instance']]) ? $search_ids[$this->info['instance']] : array();
+    if (count($search_ids) > 1) {
+      $form['global']['default_true'] = array(
+        '#type' => 'select',
+        '#title' => t('Display for searches'),
+        '#prefix' => '<div class="facetapi-global-setting">',
+        '#options' => array(
+          TRUE => t('For all except the selected'),
+          FALSE => t('Only for the selected'),
+        ),
+        '#default_value' => isset($options['default_true']) ? $options['default_true'] : TRUE,
+      );
+      $form['global']['facet_search_ids'] = array(
+        '#type' => 'select',
+        '#title' => t('Search IDs'),
+        '#suffix' => '</div>',
+        '#options' => $search_ids,
+        '#size' => min(4, count($search_ids)),
+        '#multiple' => TRUE,
+        '#default_value' => isset($options['facet_search_ids']) ? $options['facet_search_ids'] : array(),
+      );
+    }
+    else {
+      $form['global']['default_true'] = array(
+        '#type' => 'value',
+        '#value' => TRUE,
+      );
+      $form['global']['facet_search_ids'] = array(
+        '#type' => 'value',
+        '#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' => '<div class="facetapi-global-setting">',
+        '#suffix' => '</div>',
+        '#options' => $granularity_options,
+        '#default_value' => isset($options['date_granularity']) ? $options['date_granularity'] : FACETAPI_DATE_MINUTE,
+      );
+    }
+
+    // Add an "Exclude" option for terms.
+    if(!empty($facet['query types']) && in_array('term', $facet['query types'])) {
+      $form['global']['operator']['#weight'] = -2;
+      unset($form['global']['operator']['#suffix']);
+      $form['global']['exclude'] = array(
+        '#type' => 'checkbox',
+        '#title' => t('Exclude'),
+        '#description' => t('Make the search exclude selected facets, instead of restricting it to them.'),
+        '#suffix' => '</div>',
+        '#weight' => -1,
+        '#default_value' => !empty($options['exclude']),
+      );
+    }
+  }
+}

+ 229 - 0
sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/plugins/facetapi/query_type_date.inc

@@ -0,0 +1,229 @@
+<?php
+
+/**
+ * @file
+ * Date query type plugin for the Search API adapter.
+ */
+
+/**
+ * Plugin for "date" query types.
+ */
+class SearchApiFacetapiDate extends SearchApiFacetapiTerm implements FacetapiQueryTypeInterface {
+
+  /**
+   * Loads the include file containing the date API functions.
+   */
+  public function __construct(FacetapiAdapter $adapter, array $facet) {
+    module_load_include('date.inc', 'facetapi');
+    parent::__construct($adapter, $facet);
+  }
+
+  /**
+   * Returns the query type associated with the plugin.
+   *
+   * @return string
+   *   The query type.
+   */
+  static public function getType() {
+    return 'date';
+  }
+
+  /**
+   * Adds the filter to the query object.
+   *
+   * @param $query
+   *   An object containing the query in the backend's native API.
+   */
+  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']])) {
+      $options['search_api_facets'][$this->facet['name']]['limit'] = -1;
+    }
+
+    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);
+    }
+  }
+
+  /**
+   * Replacement callback for replacing ISO dates with timestamps.
+   */
+  public function replaceDateString($matches) {
+    return strtotime($matches[0]);
+  }
+
+  /**
+   * Initializes the facet's build array.
+   *
+   * @return array
+   *   The initialized render array.
+   */
+  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']])) {
+      return array();
+    }
+    $search_id = $search_ids[$facet['name']];
+    $build = array();
+    $search = search_api_current_search($search_id);
+    $results = $search[1];
+    if (!$results['result count']) {
+      return array();
+    }
+    // Gets total number of documents matched in search.
+    $total = $results['result count'];
+
+    // Most of the code below is copied from search_facetapi's implementation of
+    // this method.
+
+    // Executes query, iterates over results.
+    if (isset($results['search_api_facets']) && isset($results['search_api_facets'][$this->facet['name']])) {
+      $values = $results['search_api_facets'][$this->facet['name']];
+      foreach ($values as $value) {
+        if ($value['count']) {
+          $filter = $value['filter'];
+          // We only process single values further. The "missing" filter and
+          // range filters will be passed on unchanged.
+          if ($filter == '!') {
+            $build[$filter]['#count'] = $value['count'];
+          }
+          elseif ($filter[0] == '"') {
+            $filter = substr($value['filter'], 1, -1);
+            if ($filter) {
+              $raw_values[$filter] = $value['count'];
+            }
+          }
+          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'];
+          }
+        }
+      }
+    }
+
+    // Get the finest level of detail we're allowed to drill down to.
+    $settings = $facet->getSettings()->settings;
+    $granularity = isset($settings['date_granularity']) ? $settings['date_granularity'] : FACETAPI_DATE_MINUTE;
+
+    // Gets active facets, starts building hierarchy.
+    $parent = $gap = NULL;
+    $active_items = $this->adapter->getActiveItems($this->facet);
+    foreach ($active_items as $value => $item) {
+      // If the item is active, the count is the result set count.
+      $build[$value] = array('#count' => $total);
+
+      // Gets next "gap" increment.
+      if ($value[0] != '[' || $value[strlen($value) - 1] != ']' || !($pos = strpos($value, ' TO '))) {
+        continue;
+      }
+      $start = substr($value, 1, $pos);
+      $end = substr($value, $pos + 4, -1);
+      $date_gap = facetapi_get_date_gap($start, $end);
+      $gap = facetapi_get_next_date_gap($date_gap, $granularity);
+
+      // If there is a previous item, there is a parent, uses a reference so the
+      // arrays are populated when they are updated.
+      if (NULL !== $parent) {
+        $build[$parent]['#item_children'][$value] = &$build[$value];
+        $build[$value]['#item_parents'][$parent] = $parent;
+      }
+
+      // Stores the last value iterated over.
+      $parent = $value;
+    }
+    if (empty($raw_values)) {
+      return $build;
+    }
+    ksort($raw_values);
+
+    // Mind the gap! Calculates gap from min and max timestamps.
+    $timestamps = array_keys($raw_values);
+    if (NULL === $parent) {
+      if (count($raw_values) > 1) {
+        $gap = facetapi_get_timestamp_gap(min($timestamps), max($timestamps));
+        // Array of numbers used to determine whether the next gap is smaller than
+        // the minimum gap allowed in the drilldown.
+        $gap_numbers = array(
+          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 and minimum gap, checks if the gap
+        // is within the limit set by the $granularity parameter.
+        if ($gap_numbers[$gap] < $gap_numbers[$granularity]) {
+          $gap = $granularity;
+        }
+      }
+      else {
+        $gap = $granularity;
+      }
+    }
+
+    // Converts all timestamps to dates in ISO 8601 format.
+    $dates = array();
+    foreach ($timestamps as $timestamp) {
+      $dates[$timestamp] = facetapi_isodate($timestamp, $gap);
+    }
+
+    // Treat each date as the range start and next date as the range end.
+    $range_end = array();
+    $previous = NULL;
+    foreach (array_unique($dates) as $date) {
+      if (NULL !== $previous) {
+        $range_end[$previous] = facetapi_get_next_date_increment($previous, $gap);
+      }
+      $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.
+    foreach ($raw_values as $value => $count) {
+      $new_value = '[' . $dates[$value] . ' TO ' . $range_end[$dates[$value]] . ']';
+      if (!isset($build[$new_value])) {
+        $build[$new_value] = array('#count' => $count);
+      }
+      // 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;
+      }
+
+      // Adds parent information if not already set.
+      if (NULL !== $parent && $parent != $new_value) {
+        $build[$parent]['#item_children'][$new_value] = &$build[$new_value];
+        $build[$new_value]['#item_parents'][$parent] = $parent;
+      }
+    }
+
+    return $build;
+  }
+}

+ 175 - 0
sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/plugins/facetapi/query_type_term.inc

@@ -0,0 +1,175 @@
+<?php
+
+/**
+ * @file
+ * Term query type plugin for the Apache Solr adapter.
+ */
+
+/**
+ * Plugin for "term" query types.
+ */
+class SearchApiFacetapiTerm extends FacetapiQueryType implements FacetapiQueryTypeInterface {
+
+  /**
+   * Returns the query type associated with the plugin.
+   *
+   * @return string
+   *   The query type.
+   */
+  static public function getType() {
+    return 'term';
+  }
+
+  /**
+   * Adds the filter to the query object.
+   *
+   * @param SearchApiQueryInterface $query
+   *   An object containing the query in the backend's native API.
+   */
+  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;
+    }
+
+    // Retrieve the active facet filters.
+    $active = $this->adapter->getActiveItems($this->facet);
+    if (empty($active)) {
+      return;
+    }
+
+    // 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';
+    }
+    else {
+      throw new SearchApiException(t('Unknown facet operator %operator.', array('%operator' => $operator)));
+    }
+    $tags = array('facet:' . $this->facet['field']);
+    $facet_filter = $query->createFilter($conjunction, $tags);
+
+    foreach ($active as $filter => $filter_array) {
+      $field = $this->facet['field'];
+      $this->addFacetFilter($facet_filter, $field, $filter);
+    }
+
+    // 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) {
+    // Test if this filter should be negated.
+    $settings = $this->adapter->getFacet($this->facet)->getSettings();
+    $exclude = !empty($settings->settings['exclude']);
+    // Integer (or other nun-string) filters might mess up some of the following
+    // comparison expressions.
+    $filter = (string) $filter;
+    if ($filter == '!') {
+      $query_filter->condition($field, NULL, $exclude ? '<>' : '=');
+    }
+    elseif ($filter[0] == '[' && $filter[strlen($filter) - 1] == ']' && ($pos = strpos($filter, ' TO '))) {
+      $lower = trim(substr($filter, 1, $pos));
+      $upper = trim(substr($filter, $pos + 4, -1));
+      if ($lower == '*' && $upper == '*') {
+        $query_filter->condition($field, NULL, $exclude ? '=' : '<>');
+      }
+      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
+          // bound) and therefore have to make sure that we have an AND
+          // conjunction for those.
+          if ($upper != '*' && !($query_filter instanceof SearchApiQueryInterface || $query_filter->getConjunction() === 'AND')) {
+            $original_query_filter = $query_filter;
+            $query_filter = new SearchApiQueryFilter('AND');
+          }
+          $query_filter->condition($field, $lower, '>=');
+        }
+        if ($upper != '*') {
+          $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, $exclude ? '<>' : '=');
+    }
+    if (isset($original_query_filter)) {
+      $original_query_filter->filter($query_filter);
+    }
+  }
+
+  /**
+   * Initializes the facet's build array.
+   *
+   * @return array
+   *   The initialized render array.
+   */
+  public function build() {
+    $facet = $this->adapter->getFacet($this->facet);
+    // The current search per facet is stored in a static variable (during
+    // 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']])) {
+      return array();
+    }
+    $search_id = $search_ids[$facet['name']];
+    $search = search_api_current_search($search_id);
+    $build = array();
+    $results = $search[1];
+    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
+        // representations, convert the format here. (The missing facet can
+        // stay the same.)
+        if ($filter[0] == '"') {
+          $filter = substr($filter, 1, -1);
+        }
+        elseif ($filter != '!') {
+          // This is a range filter.
+          $filter = substr($filter, 1, -1);
+          $pos = strpos($filter, ' ');
+          if ($pos !== FALSE) {
+            $filter = '[' . substr($filter, 0, $pos) . ' TO ' . substr($filter, $pos + 1) . ']';
+          }
+        }
+        $build[$filter] = array(
+          '#count' => $value['count'],
+        );
+      }
+    }
+    return $build;
+  }
+
+}

+ 31 - 0
sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/search_api_facetapi.api.php

@@ -0,0 +1,31 @@
+<?php
+
+/**
+ * @file
+ * Hooks provided by the Search facets module.
+ */
+
+/**
+ * @addtogroup hooks
+ * @{
+ */
+
+/**
+ * Lets modules alter the search keys that are returned to FacetAPI and used
+ * in the current search block and breadcrumb trail.
+ *
+ * @param string $keys
+ *   The string representing the user's current search query.
+ * @param SearchApiQuery $query
+ *   The SearchApiQuery object for the current search.
+ */
+function hook_search_api_facetapi_keys_alter(&$keys, $query) {
+  if ($keys == '[' . t('all items') . ']') {
+    // Change $keys to something else, perhaps based on filters in the query
+    // object.
+  }
+}
+
+/**
+ * @} End of "addtogroup hooks".
+ */

+ 17 - 0
sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/search_api_facetapi.info

@@ -0,0 +1,17 @@
+name = Search facets
+description = "Integrate the Search API with the Facet API to provide facetted searches."
+dependencies[] = search_api
+dependencies[] = facetapi
+core = 7.x
+package = Search
+
+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-12-25
+version = "7.x-1.11"
+core = "7.x"
+project = "search_api"
+datestamp = "1387965506"
+

+ 13 - 0
sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/search_api_facetapi.install

@@ -0,0 +1,13 @@
+<?php
+
+/**
+ * @file
+ * Install, update and uninstall functions for the Search facets module.
+ */
+
+/**
+ * Implements hook_uninstall().
+ */
+function search_api_facetapi_uninstall() {
+  variable_del('search_api_facets_search_ids');
+}

+ 434 - 0
sites/all/modules/contrib/search/search_api/contrib/search_api_facetapi/search_api_facetapi.module

@@ -0,0 +1,434 @@
+<?php
+
+/**
+ * @file
+ * Integrates the Search API with the Facet API.
+ */
+
+/**
+ * Implements hook_menu().
+ */
+function search_api_facetapi_menu() {
+  // We need to handle our own menu paths for facets because we need a facet
+  // configuration page per index.
+  $first = TRUE;
+  foreach (facetapi_get_realm_info() as $realm_name => $realm) {
+    if ($first) {
+      $first = FALSE;
+      $items['admin/config/search/search_api/index/%search_api_index/facets'] = array(
+        'title'            => 'Facets',
+        'page callback'    => 'search_api_facetapi_settings',
+        'page arguments'   =>  array($realm_name, 5),
+        'weight'           => -1,
+        'access arguments' => array('administer search_api'),
+        'type'             => MENU_LOCAL_TASK,
+        'context'          => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
+      );
+      $items['admin/config/search/search_api/index/%search_api_index/facets/' . $realm_name] = array(
+        'title'            => $realm['label'],
+        'type'             => MENU_DEFAULT_LOCAL_TASK,
+        'weight'           => $realm['weight'],
+      );
+    }
+    else {
+      $items['admin/config/search/search_api/index/%search_api_index/facets/' . $realm_name] = array(
+        'title'            => $realm['label'],
+        'page callback'    => 'search_api_facetapi_settings',
+        'page arguments'   => array($realm_name, 5),
+        'access arguments' => array('administer search_api'),
+        'type'             => MENU_LOCAL_TASK,
+        'context'          => MENU_CONTEXT_PAGE | MENU_CONTEXT_INLINE,
+        'weight'           => $realm['weight'],
+      );
+    }
+  }
+
+  return $items;
+}
+
+/**
+ * Implements hook_facetapi_searcher_info().
+ */
+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')) {
+      $searcher_name = 'search_api@' . $index->machine_name;
+      $info[$searcher_name] = array(
+        'label' => t('Search service: @name', array('@name' => $index->name)),
+        'adapter' => 'search_api',
+        'instance' => $index->machine_name,
+        'types' => array($index->item_type),
+        'path' => '',
+        'supports facet missing' => TRUE,
+        '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;
+}
+
+/**
+ * Implements hook_facetapi_facet_info().
+ */
+function search_api_facetapi_facetapi_facet_info(array $searcher_info) {
+  $facet_info = array();
+  if ('search_api' == $searcher_info['adapter']) {
+    $index = search_api_index_load($searcher_info['instance']);
+    if (!empty($index->options['fields'])) {
+      $wrapper = $index->entityWrapper();
+      $bundle_key = NULL;
+      if ($index->getEntityType() && ($entity_info = entity_get_info($index->getEntityType())) && !empty($entity_info['bundle keys']['bundle'])) {
+        $bundle_key = $entity_info['bundle keys']['bundle'];
+      }
+
+      // Some type-specific settings. Allowing to set some additional callbacks
+      // (and other settings) in the map options allows for easier overriding by
+      // other modules.
+      $type_settings = array(
+        'taxonomy_term' => array(
+          'hierarchy callback' => 'search_api_facetapi_get_taxonomy_hierarchy',
+        ),
+        'date' => array(
+          'query type' => 'date',
+          'map options' => array(
+            'map callback' => 'facetapi_map_date',
+          ),
+        ),
+      );
+
+      // Iterate through the indexed fields to set the facetapi settings for
+      // each one.
+      foreach ($index->getFields() as $key => $field) {
+        $field['key'] = $key;
+        // Determine which, if any, of the field type-specific options will be
+        // used for this field.
+        $type = isset($field['entity_type']) ? $field['entity_type'] : $field['type'];
+        $type_settings += array($type => array());
+
+        $facet_info[$key] = $type_settings[$type] + array(
+          'label' => $field['name'],
+          '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'),
+          ),
+          'dependency plugins' => array('role'),
+          'facet missing allowed' => TRUE,
+          'facet mincount allowed' => TRUE,
+          'map callback' => 'search_api_facetapi_facet_map_callback',
+          'map options' => array(),
+          'field type' => $type,
+        );
+        if ($type === 'date') {
+          $facet_info[$key]['description'] .= ' ' . t('(Caution: This may perform very poorly for large result sets.)');
+        }
+        $facet_info[$key]['map options'] += array(
+          'field' => $field,
+          'index id' => $index->machine_name,
+          'value callback' => '_search_api_facetapi_facet_create_label',
+        );
+        // Find out whether this property is a Field API field.
+        if (strpos($key, ':') === FALSE) {
+          if (isset($wrapper->$key)) {
+            $property_info = $wrapper->$key->info();
+            if (!empty($property_info['field'])) {
+              $facet_info[$key]['field api name'] = $key;
+            }
+          }
+        }
+
+        // Add bundle information, if applicable.
+        if ($bundle_key) {
+          if ($key === $bundle_key) {
+            // Set entity type this field contains bundle information for.
+            $facet_info[$key]['field api bundles'][] = $index->getEntityType();
+          }
+          else {
+            // Add "bundle" as possible dependency plugin.
+            $facet_info[$key]['dependency plugins'][] = 'bundle';
+          }
+        }
+      }
+    }
+  }
+  return $facet_info;
+}
+
+/**
+ * Implements hook_facetapi_adapters().
+ */
+function search_api_facetapi_facetapi_adapters() {
+  return array(
+    'search_api' => array(
+      'handler' => array(
+        'class' => 'SearchApiFacetapiAdapter',
+      ),
+    ),
+  );
+}
+
+/**
+ * Implements hook_facetapi_query_types().
+ */
+function search_api_facetapi_facetapi_query_types() {
+  return array(
+    'search_api_term' => array(
+      'handler' => array(
+        'class' => 'SearchApiFacetapiTerm',
+        'adapter' => 'search_api',
+      ),
+    ),
+    'search_api_date' => array(
+      'handler' => array(
+        'class' => 'SearchApiFacetapiDate',
+        'adapter' => 'search_api',
+      ),
+    ),
+  );
+}
+
+/**
+ * Implements hook_search_api_query_alter().
+ *
+ * Adds Facet API support to the query.
+ */
+function search_api_facetapi_search_api_query_alter($query) {
+  $index = $query->getIndex();
+  if ($index->server()->supportsFeature('search_api_facets')) {
+    // This is the main point of communication between the facet system and the
+    // search back-end - it makes the query respond to active facets.
+    $searcher = 'search_api@' . $index->machine_name;
+    $adapter = facetapi_adapter_load($searcher);
+    if ($adapter) {
+      $adapter->addActiveFilters($query);
+    }
+  }
+}
+
+/**
+ * Menu callback for the facet settings page.
+ */
+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')) {
+    return array('#markup' => t('This index uses a server that does not support facet functionality.'));
+  }
+  $searcher_name = 'search_api@' . $index->machine_name;
+  module_load_include('inc', 'facetapi', 'facetapi.admin');
+  return drupal_get_form('facetapi_realm_settings_form', $searcher_name, $realm_name);
+}
+
+/**
+ * Gets hierarchy information for taxonomy terms.
+ *
+ * Used as a hierarchy callback in search_api_facetapi_facetapi_facet_info().
+ *
+ * Internally just uses facetapi_get_taxonomy_hierarchy(), but makes sure that
+ * our special "!" value is not passed.
+ *
+ * @param array $values
+ *   An array containing the term IDs.
+ *
+ * @return array
+ *   An associative array mapping term IDs to parent IDs (where parents could be
+ *   found).
+ */
+function search_api_facetapi_get_taxonomy_hierarchy(array $values) {
+  $values = array_filter($values, 'is_numeric');
+  return $values ? facetapi_get_taxonomy_hierarchy($values) : array();
+}
+
+/**
+ * Map callback for all search_api facet fields.
+ *
+ * @param array $values
+ *   The values to map.
+ * @param array $options
+ *   An associative array containing:
+ *   - 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 filter values to their labels.
+ */
+function search_api_facetapi_facet_map_callback(array $values, array $options = array()) {
+  $map = array();
+  // See if we have an additional map callback.
+  if (isset($options['map callback']) && is_callable($options['map callback'])) {
+    $map = call_user_func($options['map callback'], $values, $options);
+  }
+
+  // Then look at all unmapped values and save information for them.
+  $mappable_values = array();
+  $ranges = array();
+  foreach ($values as $value) {
+    $value = (string) $value;
+    if (isset($map[$value])) {
+      continue;
+    }
+    if ($value == '!') {
+      // The "missing" filter is usually always the same, but we allow an easy
+      // override via the "missing label" map option.
+      $map['!'] = isset($options['missing label']) ? $options['missing label'] : '(' . t('none') . ')';
+      continue;
+    }
+    $length = strlen($value);
+    if ($length > 5 && $value[0] == '[' && $value[$length - 1] == ']' && ($pos = strpos($value, ' TO '))) {
+      // This is a range filter.
+      $lower = trim(substr($value, 1, $pos));
+      $upper = trim(substr($value, $pos + 4, -1));
+      if ($lower != '*') {
+        $mappable_values[$lower] = TRUE;
+      }
+      if ($upper != '*') {
+        $mappable_values[$upper] = TRUE;
+      }
+      $ranges[$value] = array(
+        'lower' => $lower,
+        'upper' => $upper,
+      );
+    }
+    else {
+      // A normal, single-value filter.
+      $mappable_values[$value] = TRUE;
+    }
+  }
+
+  if ($mappable_values) {
+    $map += call_user_func($options['value callback'], array_keys($mappable_values), $options);
+  }
+
+  foreach ($ranges as $value => $range) {
+    $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');
+    }
+    elseif ($lower == '*') {
+      $map[$value] = "< $upper";
+    }
+    elseif ($upper == '*') {
+      $map[$value] = "> $lower";
+    }
+    else {
+      $map[$value] = "$lower – $upper";
+    }
+  }
+
+  return $map;
+}
+
+/**
+ * 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);
+    foreach ($entities as $id => $entity) {
+      $label = entity_label($type, $entity);
+      if ($label) {
+        $map[$id] = $label;
+      }
+    }
+    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;
+      break;
+    }
+    $wrapper = $wrapper->$part;
+    while (($info = $wrapper->info()) && search_api_is_list_type($info['type'])) {
+      $wrapper = $wrapper[0];
+    }
+  }
+  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, 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');
+        break;
+      case 'date':
+        $v = is_numeric($value) ? $value : strtotime($value);
+        $map[$value] = format_date($v, 'short');
+        break;
+      case 'duration':
+        $map[$value] = format_interval($value);
+        break;
+    }
+  }
+  return $map;
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ */
+function search_api_facetapi_form_search_api_admin_index_fields_alter(&$form, &$form_state) {
+  $form['#submit'][] = 'search_api_facetapi_search_api_admin_index_fields_submit';
+}
+
+/**
+ * Form submission handler for search_api_admin_index_fields().
+ */
+function search_api_facetapi_search_api_admin_index_fields_submit($form, &$form_state) {
+  // Clears this searcher's cached facet definitions.
+  $cid = 'facetapi:facet_info:search_api@' . $form_state['index']->machine_name . ':';
+  cache_clear_all($cid, 'cache', TRUE);
+}

+ 114 - 0
sites/all/modules/contrib/search/search_api/contrib/search_api_views/README.txt

@@ -0,0 +1,114 @@
+Search API Views integration
+----------------------------
+
+This module integrates the Search API with the popular Views module [1],
+allowing users to create views with filters, arguments, sorts and fields based
+on any search index.
+
+[1] http://drupal.org/project/views
+
+"More like this" feature
+------------------------
+This module defines the "More like this" feature (feature key: "search_api_mlt")
+that search service classes can implement. With a server supporting this, you
+can use the „More like this“ contextual filter to display a list of items
+related to a given item (usually, nodes similar to the node currently viewed).
+
+For developers:
+A service class that wants to support this feature has to check for a
+"search_api_mlt" option in the search() method. When present, it will be an
+array containing two keys:
+- id: The entity ID of the item to which related items should be searched.
+- fields: An array of indexed fields to use for testing the similarity of items.
+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.
+
+"Facets block" display
+----------------------
+Most features should be clear to users of Views. However, the module also
+provides a new display type, "Facets block", that might need some explanation.
+This display type is only available, if the „Search facets“ module is also
+enabled.
+
+The basic use of the block is to provide a list of links to the most popular
+filter terms (i.e., the ones with the most results) for a certain category. For
+example, you could provide a block listing the most popular authors, or taxonomy
+terms, linking to searches for those, to provide some kind of landing page.
+
+Please note that, due to limitations in Views, this display mode is shown for
+views of all base tables, even though it only works for views based on Search
+API indexes. For views of other base tables, this will just print an error
+message.
+The display will also always ignore the view's "Style" setting, selected fields
+and sorts, etc.
+
+To use the display, specify the base path of the search you want to link to
+(this enables you to also link to searches that aren't based on Views) and the
+facet field to use (any indexed field can be used here, there needn't be a facet
+defined for it). You'll then have the block available in the blocks
+administration and can enable and move it at leisure.
+Note, however, that the facet in question has to be enabled for the search page
+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.
+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.
+
+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
+prefixed with „Indexed“ in order to be distinguishable from fields on referenced
+nodes (or other entities). The data displayed normally still comes from the
+entity, not from the search index.

+ 276 - 0
sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/display_facet_block.inc

@@ -0,0 +1,276 @@
+<?php
+
+/**
+ * @file
+ * Display plugin for displaying the search facets in a block.
+ */
+
+/**
+ * Plugin class for displaying search facets in a block.
+ */
+class SearchApiViewsFacetsBlockDisplay extends views_plugin_display_block {
+  public function displays_exposed() {
+    return FALSE;
+  }
+  public function uses_exposed() {
+    return FALSE;
+  }
+
+  public function option_definition() {
+    $options = parent::option_definition();
+
+    $options['linked_path'] = array('default' => '');
+    $options['facet_field'] = '';
+    $options['hide_block'] = FALSE;
+
+    return $options;
+  }
+
+  public function options_form(&$form, &$form_state) {
+    parent::options_form($form, $form_state);
+
+    if (substr($this->view->base_table, 0, 17) != 'search_api_index_') {
+      return;
+    }
+
+    switch ($form_state['section']) {
+      case 'linked_path':
+        $form['#title'] .= t('Search page path');
+        $form['linked_path'] = array(
+          '#type' => 'textfield',
+          '#description' => t('The menu path to which search facets will link. Leave empty to use the current path.'),
+          '#default_value' => $this->get_option('linked_path'),
+        );
+        break;
+      case 'facet_field':
+        $form['facet_field'] = array(
+          '#type' => 'select',
+          '#title' => t('Facet field'),
+          '#options' => $this->getFieldOptions(),
+          '#default_value' => $this->get_option('facet_field'),
+        );
+        break;
+      case 'use_more':
+        $form['use_more']['#description'] = t('This will add a more link to the bottom of this view, which will link to the base path for the facet links.');
+        $form['use_more_always'] = array(
+          '#type' => 'value',
+          '#value' => $this->get_option('use_more_always'),
+        );
+        break;
+      case 'hide_block':
+        $form['hide_block'] = array(
+          '#type' => 'checkbox',
+          '#title' => t('Hide block'),
+          '#description' => t('Hide this block, but still execute the search. ' .
+              'Can be used to show native Facet API facet blocks linking to the search page specified above.'),
+          '#default_value' => $this->get_option('hide_block'),
+        );
+        break;
+    }
+  }
+
+  public function options_validate(&$form, &$form_state) {
+    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.'));
+    }
+  }
+
+  public function options_submit(&$form, &$form_state) {
+    parent::options_submit($form, $form_state);
+
+    switch ($form_state['section']) {
+      case 'linked_path':
+        $this->set_option('linked_path', $form_state['values']['linked_path']);
+        break;
+      case 'facet_field':
+        $this->set_option('facet_field', $form_state['values']['facet_field']);
+        break;
+      case 'hide_block':
+        $this->set_option('hide_block', $form_state['values']['hide_block']);
+        break;
+    }
+  }
+
+  public function options_summary(&$categories, &$options) {
+    parent::options_summary($categories, $options);
+
+    $options['linked_path'] = array(
+      'category' => 'block',
+      'title' => t('Search page path'),
+      'value' => $this->get_option('linked_path') ? $this->get_option('linked_path') : t('Use current path'),
+    );
+    $field_options = $this->getFieldOptions();
+    $options['facet_field'] = array(
+      'category' => 'block',
+      'title' => t('Facet field'),
+      'value' => $this->get_option('facet_field') ? $field_options[$this->get_option('facet_field')] : t('None'),
+    );
+    $options['hide_block'] = array(
+      'category' => 'block',
+      'title' => t('Hide block'),
+      'value' => $this->get_option('hide_block') ? t('Yes') : t('No'),
+    );
+  }
+
+  protected $field_options = NULL;
+
+  protected function getFieldOptions() {
+    if (!isset($this->field_options)) {
+      $index_id = substr($this->view->base_table, 17);
+      if (!($index_id && ($index = search_api_index_load($index_id)))) {
+        $table = views_fetch_data($this->view->base_table);
+        $table = empty($table['table']['base']['title']) ? $this->view->base_table : $table['table']['base']['title'];
+        throw new SearchApiException(t('The "Facets block" display cannot be used with a view for @basetable. ' .
+            'Please only use this display with base tables representing search indexes.',
+            array('@basetable' => $table)));
+      }
+      $this->field_options = array();
+      if (!empty($index->options['fields'])) {
+        foreach ($index->getFields() as $key => $field) {
+          $this->field_options[$key] = $field['name'];
+        }
+      }
+    }
+    return $this->field_options;
+  }
+
+  /**
+   * Render the 'more' link
+   */
+  public function render_more_link() {
+    if ($this->use_more()) {
+      $path = $this->get_option('linked_path');
+      $theme = views_theme_functions('views_more', $this->view, $this->display);
+      $path = check_url(url($path, array()));
+
+      return array(
+        '#theme' => $theme,
+        '#more_url' => $path,
+        '#link_text' => check_plain($this->use_more_text()),
+      );
+    }
+  }
+
+  public function query(){
+    parent::query();
+
+    $facet_field = $this->get_option('facet_field');
+    if (!$facet_field) {
+      return NULL;
+    }
+
+    $base_path = $this->get_option('linked_path');
+    if (!$base_path) {
+      $base_path = $_GET['q'];
+    }
+
+    $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')) {
+      // If we hide the block, we don't need this extra facet.
+      $query_options['search_api_facets']['search_api_views_facets_block'] = array(
+        'field' => $facet_field,
+        'limit' => $limit,
+        'missing' => FALSE,
+        'min_count' => 1,
+      );
+    }
+    $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();
+
+    if ($this->get_option('hide_block')) {
+      return NULL;
+    }
+
+    $results = $this->view->query->getSearchApiResults();
+
+    if (empty($results['search_api_facets']['search_api_views_facets_block'])) {
+      return NULL;
+    }
+    $terms = $results['search_api_facets']['search_api_views_facets_block'];
+
+    $filters = array();
+    foreach ($terms as $term) {
+      $filter = $term['filter'];
+      if ($filter[0] == '"') {
+        $filter = substr($filter, 1, -1);
+      }
+      elseif ($filter != '!') {
+        // This is a range filter.
+        $filter = substr($filter, 1, -1);
+        $pos = strpos($filter, ' ');
+        if ($pos !== FALSE) {
+          $filter = '[' . substr($filter, 0, $pos) . ' TO ' . substr($filter, $pos + 1) . ']';
+        }
+      }
+      $filters[$term['filter']] = $filter;
+    }
+
+    $index = $this->view->query->getIndex();
+    $options['field'] = $index->options['fields'][$facet_field];
+    $options['field']['key'] = $facet_field;
+    $options['index id'] = $index->machine_name;
+    $options['value callback'] = '_search_api_facetapi_facet_create_label';
+    $map = search_api_facetapi_facet_map_callback($filters, $options);
+
+    $facets = array();
+    $prefix = rawurlencode($facet_field) . ':';
+    foreach ($terms as $term) {
+      $name = $filter = $filters[$term['filter']];
+      if (isset($map[$filter])) {
+        $name = $map[$filter];
+      }
+      $query['f'][0] = $prefix . $filter;
+
+      // Initializes variables passed to theme hook.
+      $variables = array(
+        'text' => $name,
+        'path' => $this->view->query->getOption('search_api_base_path'),
+        'count' => $term['count'],
+        'options' => array(
+          'attributes' => array('class' => 'facetapi-inactive'),
+          'html' => FALSE,
+          'query' => $query,
+        ),
+      );
+
+      // Themes the link, adds row to facets.
+      $facets[] = array(
+        'class' => array('leaf'),
+        'data' => theme('facetapi_link_inactive', $variables),
+      );
+    }
+
+    if (!$facets) {
+      return NULL;
+    }
+
+    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;
+  }
+
+}

+ 141 - 0
sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_argument.inc

@@ -0,0 +1,141 @@
+<?php
+
+/**
+ * @file
+ * Contains SearchApiViewsHandlerArgument.
+ */
+
+/**
+ * Views argument handler class for handling all non-fulltext types.
+ */
+class SearchApiViewsHandlerArgument extends views_handler_argument {
+
+  /**
+   * The associated views query object.
+   *
+   * @var SearchApiViewsQuery
+   */
+  public $query;
+
+  /**
+   * The operator to use for multiple arguments.
+   *
+   * Either "and" or "or".
+   *
+   * @var string
+   *
+   * @see views_break_phrase
+   */
+  public $operator;
+
+  /**
+   * Determine if the argument can generate a breadcrumb
+   *
+   * @return boolean
+   */
+  // @todo Change and implement set_breadcrumb()?
+  public function uses_breadcrumb() {
+    return FALSE;
+  }
+
+  /**
+   * Provide a list of default behaviors for this argument if the argument
+   * is not present.
+   *
+   * Override this method to provide additional (or fewer) default behaviors.
+   */
+  public function default_actions($which = NULL) {
+    $defaults = array(
+      'ignore' => array(
+        'title' => t('Display all values'),
+        'method' => 'default_ignore',
+        'breadcrumb' => TRUE, // generate a breadcrumb to here
+      ),
+      'not found' => array(
+        'title' => t('Hide view / Page not found (404)'),
+        'method' => 'default_not_found',
+        'hard fail' => TRUE, // This is a hard fail condition
+      ),
+      'empty' => array(
+        'title' => t('Display empty text'),
+        'method' => 'default_empty',
+        'breadcrumb' => TRUE, // generate a breadcrumb to here
+      ),
+      'default' => array(
+        'title' => t('Provide default argument'),
+        'method' => 'default_default',
+        'form method' => 'default_argument_form',
+        'has default argument' => TRUE,
+        'default only' => TRUE, // this can only be used for missing argument, not validation failure
+      ),
+    );
+
+    if ($which) {
+      return isset($defaults[$which]) ? $defaults[$which] : NULL;
+    }
+    return $defaults;
+  }
+
+  public function option_definition() {
+    $options = parent::option_definition();
+
+    $options['break_phrase'] = array('default' => FALSE);
+    $options['not'] = array('default' => FALSE);
+
+    return $options;
+  }
+
+  public function options_form(&$form, &$form_state) {
+    parent::options_form($form, $form_state);
+
+    // Allow passing multiple values.
+    $form['break_phrase'] = array(
+      '#type' => 'checkbox',
+      '#title' => t('Allow multiple values'),
+      '#description' => t('If selected, users can enter multiple values in the form of 1+2+3 (for OR) or 1,2,3 (for AND).'),
+      '#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',
+    );
+  }
+
+  /**
+   * Set up the query for this argument.
+   *
+   * The argument sent may be found at $this->argument.
+   */
+  public function query($group_by = FALSE) {
+    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, $operator);
+        }
+        $this->query->filter($filter);
+      }
+    }
+    else {
+      $this->query->condition($this->real_field, reset($this->value), $operator);
+    }
+  }
+
+}

+ 161 - 0
sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_argument_date.inc

@@ -0,0 +1,161 @@
+<?php
+
+/**
+ * @file
+ * Contains the SearchApiViewsHandlerArgumentDate class.
+ */
+
+/**
+ * Defines a contextual filter searching for a date or date range.
+ */
+class SearchApiViewsHandlerArgumentDate extends SearchApiViewsHandlerArgument {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function query($group_by = FALSE) {
+    if (empty($this->value)) {
+      $this->fillValue();
+      if ($this->value === FALSE) {
+        $this->abort();
+        return;
+      }
+    }
+
+    $outer_conjunction = strtoupper($this->operator);
+
+    if (empty($this->options['not'])) {
+      $operator = '=';
+      $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);
+  }
+
+}

+ 106 - 0
sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_argument_fulltext.inc

@@ -0,0 +1,106 @@
+<?php
+
+/**
+ * @file
+ * Contains SearchApiViewsHandlerArgumentFulltext.
+ */
+
+/**
+ * Views argument handler class for handling fulltext fields.
+ */
+class SearchApiViewsHandlerArgumentFulltext extends SearchApiViewsHandlerArgument {
+
+  /**
+   * Specify the options this filter uses.
+   */
+  public function option_definition() {
+    $options = parent::option_definition();
+    $options['fields'] = array('default' => array());
+    $options['conjunction'] = array('default' => 'AND');
+    return $options;
+  }
+
+  /**
+   * Extend the options form a bit.
+   */
+  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(
+        '#type' => 'select',
+        '#title' => t('Searched fields'),
+        '#description' => t('Select the fields that will be searched. If no fields are selected, all available fulltext fields will be searched.'),
+        '#options' => $fields,
+        '#size' => min(4, count($fields)),
+        '#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(
+        '#type' => 'value',
+        '#value' => array(),
+      );
+    }
+  }
+
+  /**
+   * Set up the query for this argument.
+   *
+   * The argument sent may be found at $this->argument.
+   */
+  public function query($group_by = FALSE) {
+    if ($this->options['fields']) {
+      $this->query->fields($this->options['fields']);
+    }
+    if ($this->options['conjunction'] != 'AND') {
+      $this->query->setOption('conjunction', $this->options['conjunction']);
+    }
+
+    $old = $this->query->getOriginalKeys();
+    $this->query->keys($this->argument);
+    if ($old) {
+      $keys = &$this->query->getKeys();
+      if (is_array($keys)) {
+        $keys[] = $old;
+      }
+      elseif (is_array($old)) {
+        // We don't support such nonsense.
+      }
+      else {
+        $keys = "($old) ($keys)";
+      }
+    }
+  }
+
+  /**
+   * Helper method to get an option list of all available fulltext fields.
+   */
+  protected function getFulltextFields() {
+    $ret = array();
+    $index = search_api_index_load(substr($this->table, 17));
+    if (!empty($index->options['fields'])) {
+      $fields = $index->getFields();
+      foreach ($index->getFulltextFields() as $field) {
+        $ret[$field] = $fields[$field]['name'];
+      }
+    }
+    return $ret;
+  }
+
+}

+ 85 - 0
sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_argument_more_like_this.inc

@@ -0,0 +1,85 @@
+<?php
+
+/**
+ * @file
+ * Contains SearchApiViewsHandlerArgumentMoreLikeThis.
+ */
+
+/**
+ * Views argument handler providing a list of related items for search servers
+ * supporting the "search_api_mlt" feature.
+ */
+class SearchApiViewsHandlerArgumentMoreLikeThis extends SearchApiViewsHandlerArgument {
+
+  /**
+   * Specify the options this filter uses.
+   */
+  public function option_definition() {
+    $options = parent::option_definition();
+    unset($options['break_phrase']);
+    unset($options['not']);
+    $options['fields'] = array('default' => array());
+    return $options;
+  }
+
+  /**
+   * Extend the options form a bit.
+   */
+  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 (!empty($index->options['fields'])) {
+      $fields = array();
+      foreach ($index->getFields() as $key => $field) {
+        $fields[$key] = $field['name'];
+      }
+    }
+    if (!empty($fields)) {
+      $form['fields'] = array(
+        '#type' => 'select',
+        '#title' => t('Fields for Similarity'),
+        '#description' => t('Select the fields that will be used for finding similar content. If no fields are selected, all available fields will be used.'),
+        '#options' => $fields,
+        '#size' => min(8, count($fields)),
+        '#multiple' => TRUE,
+        '#default_value' => $this->options['fields'],
+      );
+    }
+    else {
+      $form['fields'] = array(
+        '#type' => 'value',
+        '#value' => array(),
+      );
+    }
+  }
+
+  /**
+   * Set up the query for this argument.
+   *
+   * The argument sent may be found at $this->argument.
+   */
+  public function query($group_by = FALSE) {
+    $server = $this->query->getIndex()->server();
+    if (!$server->supportsFeature('search_api_mlt')) {
+      $class = search_api_get_service_info($server->class);
+      watchdog('search_api_views', 'The search service "@class" does not offer "More like this" functionality.',
+          array('@class' => $class['name']), WATCHDOG_ERROR);
+      $this->query->abort();
+      return;
+    }
+    $fields = $this->options['fields'] ? $this->options['fields'] : array();
+    if (empty($fields)) {
+      foreach ($this->query->getIndex()->options['fields'] as $key => $field) {
+        $fields[] = $key;
+      }
+    }
+    $mlt = array(
+      'id' => $this->argument,
+      'fields' => $fields,
+    );
+    $this->query->getSearchApiQuery()->setOption('search_api_mlt', $mlt);
+  }
+}

+ 31 - 0
sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_argument_string.inc

@@ -0,0 +1,31 @@
+<?php
+
+/**
+ * @file
+ * Contains SearchApiViewsHandlerArgumentString.
+ */
+
+/**
+ * Views argument handler class for handling string fields.
+ */
+class SearchApiViewsHandlerArgumentString extends SearchApiViewsHandlerArgument {
+
+  /**
+   * Set up the query for this argument.
+   *
+   * The argument sent may be found at $this->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);
+  }
+
+}

+ 104 - 0
sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_argument_taxonomy_term.inc

@@ -0,0 +1,104 @@
+<?php
+
+/**
+ * @file
+ *   Contains the SearchApiViewsHandlerArgumentTaxonomyTerm class.
+ */
+
+/**
+ * Defines a contextual filter searching through all indexed taxonomy fields.
+ */
+class SearchApiViewsHandlerArgumentTaxonomyTerm extends SearchApiViewsHandlerArgument {
+
+  /**
+   * Set up the query for this argument.
+   *
+   * The argument sent may be found at $this->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);
+    }
+  }
+
+}

+ 119 - 0
sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter.inc

@@ -0,0 +1,119 @@
+<?php
+
+/**
+ * @file
+ * Contains SearchApiViewsHandlerFilter.
+ */
+
+/**
+ * Views filter handler base class for handling all "normal" cases.
+ */
+class SearchApiViewsHandlerFilter extends views_handler_filter {
+
+  /**
+   * The value to filter for.
+   *
+   * @var mixed
+   */
+  public $value;
+
+  /**
+   * The operator used for filtering.
+   *
+   * @var string
+   */
+  public $operator;
+
+  /**
+   * The associated views query object.
+   *
+   * @var SearchApiViewsQuery
+   */
+  public $query;
+
+  /**
+   * Provide a list of options for the operator form.
+   */
+  public function operator_options() {
+    return array(
+      '<' => 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'),
+      '>' => t('Is greater than'),
+      'empty' => t('Is empty'),
+      'not empty' => t('Is not empty'),
+    );
+  }
+
+  /**
+   * Provide a form for setting the filter value.
+   */
+  public function value_form(&$form, &$form_state) {
+    while (is_array($this->value) && count($this->value) < 2) {
+      $this->value = $this->value ? reset($this->value) : NULL;
+    }
+    $form['value'] = array(
+      '#type' => 'textfield',
+      '#title' => empty($form_state['exposed']) ? t('Value') : '',
+      '#size' => 30,
+      '#default_value' => isset($this->value) ? $this->value : '',
+    );
+
+    // 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 (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'),
+      );
+    }
+  }
+
+  /**
+   * Display the filter on the administrative summary
+   */
+  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');
+    }
+
+    return check_plain((string) $this->operator) . ' ' . check_plain((string) $this->value);
+  }
+
+  /**
+   * Add this filter to the query.
+   */
+  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']);
+    }
+    else {
+      while (is_array($this->value)) {
+        $this->value = $this->value ? reset($this->value) : NULL;
+      }
+      if (strlen($this->value) > 0) {
+        $this->query->condition($this->real_field, $this->value, $this->operator, $this->options['group']);
+      }
+    }
+  }
+
+}

+ 35 - 0
sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_boolean.inc

@@ -0,0 +1,35 @@
+<?php
+
+/**
+ * @file
+ * Contains SearchApiViewsHandlerFilterBoolean.
+ */
+
+/**
+ * Views filter handler class for handling fulltext fields.
+ */
+class SearchApiViewsHandlerFilterBoolean extends SearchApiViewsHandlerFilter {
+
+  /**
+   * Provide a list of options for the operator form.
+   */
+  public function operator_options() {
+    return array();
+  }
+
+  /**
+   * 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;
+    }
+    $form['value'] = array(
+      '#type' => 'select',
+      '#title' => empty($form_state['exposed']) ? t('Value') : '',
+      '#options' => array(1 => t('True'), 0 => t('False')),
+      '#default_value' => isset($this->value) ? $this->value : '',
+    );
+  }
+
+}

+ 91 - 0
sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_date.inc

@@ -0,0 +1,91 @@
+<?php
+
+/**
+ * @file
+ * Contains SearchApiViewsHandlerFilterDate.
+ */
+
+/**
+ * Views filter handler base class for handling all "normal" cases.
+ */
+class SearchApiViewsHandlerFilterDate extends SearchApiViewsHandlerFilter {
+
+  /**
+   * Add a "widget type" option.
+   */
+  public function option_definition() {
+    return parent::option_definition() + array(
+      'widget_type' => array('default' => 'default'),
+    );
+  }
+
+  /**
+   * If the date popup module is enabled, provide the extra option setting.
+   */
+  public function has_extra_options() {
+    if (module_exists('date_popup')) {
+      return TRUE;
+    }
+    return FALSE;
+  }
+
+  /**
+   * Add extra options if we allow the date popup widget.
+   */
+  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');
+      $form['widget_type'] = array(
+        '#type' => 'radios',
+        '#title' => t('Date selection form element'),
+        '#default_value' => $this->options['widget_type'],
+        '#options' => $widget_options,
+      );
+    }
+  }
+
+  /**
+   * Provide a form for setting the filter value.
+   */
+  public function value_form(&$form, &$form_state) {
+    parent::value_form($form, $form_state);
+
+    // 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')) {
+      $form['value']['#type'] = 'date_popup';
+      $form['value']['#date_format'] = 'm/d/Y';
+      unset($form['value']['#description']);
+    }
+    elseif (empty($form_state['exposed'])) {
+      $form['value']['#description'] = t('A date in any format understood by <a href="@doc-link">PHP</a>. For example, "@date1" or "@date2".', array(
+        '@doc-link' => 'http://php.net/manual/en/function.strtotime.php',
+        '@date1' => format_date(REQUEST_TIME, 'custom', 'Y-m-d H:i:s'),
+        '@date2' => 'now + 1 day',
+      ));
+    }
+  }
+
+  /**
+   * Add this filter to the query.
+   */
+  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']);
+    }
+    else {
+      while (is_array($this->value)) {
+        $this->value = $this->value ? reset($this->value) : NULL;
+      }
+      $v = is_numeric($this->value) ? $this->value : strtotime($this->value, REQUEST_TIME);
+      if ($v !== FALSE) {
+        $this->query->condition($this->real_field, $v, $this->operator, $this->options['group']);
+      }
+    }
+  }
+
+}

+ 211 - 0
sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_entity.inc

@@ -0,0 +1,211 @@
+<?php
+
+/**
+ * @file
+ * Contains SearchApiViewsHandlerFilterEntity.
+ */
+
+/**
+ * Views filter handler class for entities.
+ *
+ * Should be extended for specific entity types, such as
+ * SearchApiViewsHandlerFilterUser and SearchApiViewsHandlerFilterTaxonomyTerm.
+ *
+ * Based on views_handler_filter_term_node_tid.
+ */
+abstract class SearchApiViewsHandlerFilterEntity extends SearchApiViewsHandlerFilter {
+
+  /**
+   * If exposed form input was successfully validated, the entered entity IDs.
+   *
+   * @var array
+   */
+  protected $validated_exposed_input;
+
+  /**
+   * Validates entered entity labels and converts them to entity IDs.
+   *
+   * Since this can come from either the form or the exposed filter, this is
+   * abstracted out a bit so it can handle the multiple input sources.
+   *
+   * @param array $form
+   *   The form or form element for which any errors should be set.
+   * @param array $values
+   *   The entered user names to validate.
+   *
+   * @return array
+   *   The entity IDs corresponding to all entities that could be found.
+   */
+  abstract protected function validate_entity_strings(array &$form, array $values);
+
+  /**
+   * Transforms an array of entity IDs into a comma-separated list of labels.
+   *
+   * @param array $ids
+   *   The entity IDs to transform.
+   *
+   * @return string
+   *   A string containing the labels corresponding to the IDs, separated by
+   *   commas.
+   */
+  abstract protected function ids_to_strings(array $ids);
+
+  /**
+   * {@inheritdoc}
+   */
+  public function operator_options() {
+    $operators = array(
+      '=' => $this->isMultiValued() ? t('Is one of') : t('Is'),
+      'all of' => t('Is all of'),
+      '<>' => $this->isMultiValued() ? t('Is not one of') : t('Is not'),
+      'empty' => t('Is empty'),
+      'not empty' => t('Is not empty'),
+    );
+    if (!$this->isMultiValued()) {
+      unset($operators['all of']);
+    }
+    return $operators;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function option_definition() {
+    $options = parent::option_definition();
+
+    $options['expose']['multiple']['default'] = TRUE;
+
+    return $options;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function value_form(&$form, &$form_state) {
+    parent::value_form($form, $form_state);
+
+    if (!is_array($this->value)) {
+      $this->value = $this->value ? array($this->value) : array();
+    }
+
+    // Set the correct default value in case the admin-set value is used (and a
+    // value is present). The value is used if the form is either not exposed,
+    // or the exposed form wasn't submitted yet (there is
+    if ($this->value && (empty($form_state['input']) || !empty($form_state['input']['live_preview']))) {
+      $form['value']['#default_value'] = $this->ids_to_strings($this->value);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function value_validate($form, &$form_state) {
+    if (!empty($form['value'])) {
+      $value = &$form_state['values']['options']['value'];
+      $values = $this->isMultiValued($form_state['values']['options']) ? drupal_explode_tags($value) : array($value);
+      $ids = $this->validate_entity_strings($form['value'], $values);
+
+      if ($ids) {
+        $value = $ids;
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function accept_exposed_input($input) {
+    $rc = parent::accept_exposed_input($input);
+
+    if ($rc) {
+      // If we have previously validated input, override.
+      if ($this->validated_exposed_input) {
+        $this->value = $this->validated_exposed_input;
+      }
+    }
+
+    return $rc;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function exposed_validate(&$form, &$form_state) {
+    if (empty($this->options['exposed']) || empty($this->options['expose']['identifier'])) {
+      return;
+    }
+
+    $identifier = $this->options['expose']['identifier'];
+    $input = $form_state['values'][$identifier];
+
+    if ($this->options['is_grouped'] && isset($this->options['group_info']['group_items'][$input])) {
+      $this->operator = $this->options['group_info']['group_items'][$input]['operator'];
+      $input = $this->options['group_info']['group_items'][$input]['value'];
+    }
+
+    $values = $this->isMultiValued() ? drupal_explode_tags($input) : array($input);
+
+    if (!$this->options['is_grouped'] || ($this->options['is_grouped'] && ($input != 'All'))) {
+      $this->validated_exposed_input = $this->validate_entity_strings($form[$identifier], $values);
+    }
+    else {
+      $this->validated_exposed_input = FALSE;
+    }
+  }
+
+  /**
+   * Determines whether multiple user names can be entered into this filter.
+   *
+   * This is either the case if the form isn't exposed, or if the " Allow
+   * multiple selections" option is enabled.
+   *
+   * @param array $options
+   *   (optional) The options array to use. If not supplied, the options set on
+   *   this filter will be used.
+   *
+   * @return bool
+   *   TRUE if multiple values can be entered for this filter, FALSE otherwise.
+   */
+  protected function isMultiValued(array $options = array()) {
+    $options = $options ? $options : $this->options;
+    return empty($options['exposed']) || !empty($options['expose']['multiple']);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function admin_summary() {
+    $value = $this->value;
+    $this->value = empty($value) ? '' : $this->ids_to_strings($value);
+    $ret = parent::admin_summary();
+    $this->value = $value;
+    return $ret;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function query() {
+    if ($this->operator === 'empty') {
+      $this->query->condition($this->real_field, NULL, '=', $this->options['group']);
+    }
+    elseif ($this->operator === 'not empty') {
+      $this->query->condition($this->real_field, NULL, '<>', $this->options['group']);
+    }
+    elseif (is_array($this->value)) {
+      $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']);
+      }
+    }
+  }
+
+}

+ 213 - 0
sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_fulltext.inc

@@ -0,0 +1,213 @@
+<?php
+
+/**
+ * @file
+ * Contains SearchApiViewsHandlerFilterFulltext.
+ */
+
+/**
+ * Views filter handler class for handling fulltext fields.
+ */
+class SearchApiViewsHandlerFilterFulltext extends SearchApiViewsHandlerFilterText {
+
+  /**
+   * Displays the operator form, adding a description.
+   */
+  public function show_operator_form(&$form, &$form_state) {
+    $this->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;
+  }
+
+  /**
+   * Extend the options form a bit.
+   */
+  public function options_form(&$form, &$form_state) {
+    parent::options_form($form, $form_state);
+
+    $form['mode'] = array(
+      '#title' => t('Use as'),
+      '#type' => 'radios',
+      '#options' => array(
+        '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'],
+    );
+
+    $fields = $this->getFulltextFields();
+    if (!empty($fields)) {
+      $form['fields'] = array(
+        '#type' => 'select',
+        '#title' => t('Searched fields'),
+        '#description' => t('Select the fields that will be searched. If no fields are selected, all available fulltext fields will be searched.'),
+        '#options' => $fields,
+        '#size' => min(4, count($fields)),
+        '#multiple' => TRUE,
+        '#default_value' => $this->options['fields'],
+      );
+    }
+    else {
+      $form['fields'] = array(
+        '#type' => 'value',
+        '#value' => array(),
+      );
+    }
+    if (isset($form['expose'])) {
+      $form['expose']['#weight'] = -5;
+    }
+
+    $form['min_length'] = array(
+      '#title' => t('Minimum keyword length'),
+      '#description' => t('Minimum length of each word in the search keys. Leave empty to allow all words.'),
+      '#type' => 'textfield',
+      '#element_validate' => array('element_validate_integer_positive'),
+      '#default_value' => $this->options['min_length'],
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function exposed_validate(&$form, &$form_state) {
+    // Only validate exposed input.
+    if (empty($this->options['exposed']) || empty($this->options['expose']['identifier'])) {
+      return;
+    }
+
+    // We only need to validate if there is a minimum word length set.
+    if ($this->options['min_length'] < 2) {
+      return;
+    }
+
+    $identifier = $this->options['expose']['identifier'];
+    $input = &$form_state['values'][$identifier];
+
+    if ($this->options['is_grouped'] && isset($this->options['group_info']['group_items'][$input])) {
+      $this->operator = $this->options['group_info']['group_items'][$input]['operator'];
+      $input = &$this->options['group_info']['group_items'][$input]['value'];
+    }
+
+    // If there is no input, we're fine.
+    if (!trim($input)) {
+      return;
+    }
+
+    $words = preg_split('/\s+/', $input);
+    foreach ($words as $i => $word) {
+      if (drupal_strlen($word) < $this->options['min_length']) {
+        unset($words[$i]);
+      }
+    }
+    if (!$words) {
+      $vars['@count'] = $this->options['min_length'];
+      $msg = t('You must include at least one positive keyword with @count characters or more.', $vars);
+      form_error($form[$identifier], $msg);
+    }
+    $input = implode(' ', $words);
+  }
+
+  /**
+   * Add this filter to the query.
+   */
+  public function query() {
+    while (is_array($this->value)) {
+      $this->value = $this->value ? reset($this->value) : '';
+    }
+    // Catch empty strings entered by the user, but not "0".
+    if ($this->value === '') {
+      return;
+    }
+    $fields = $this->options['fields'];
+    $fields = $fields ? $fields : array_keys($this->getFulltextFields());
+
+    // If something already specifically set different fields, we silently fall
+    // back to mere filtering.
+    $filter = $this->options['mode'] == 'filter';
+    if (!$filter) {
+      $old = $this->query->getFields();
+      $filter = $old && (array_diff($old, $fields) || array_diff($fields, $old));
+    }
+
+    if ($filter) {
+      $filter = $this->query->createFilter('OR');
+      foreach ($fields as $field) {
+        $filter->condition($field, $this->value, $this->operator);
+      }
+      $this->query->filter($filter);
+      return;
+    }
+
+    // If the operator was set to OR or NOT, set OR as the conjunction. (It is
+    // also set for NOT since otherwise it would be "not all of these words".)
+    if ($this->operator != 'AND') {
+      $this->query->setOption('conjunction', $this->operator);
+    }
+
+    $this->query->fields($fields);
+    $old = $this->query->getOriginalKeys();
+    $this->query->keys($this->value);
+    if ($this->operator == 'NOT') {
+      $keys = &$this->query->getKeys();
+      if (is_array($keys)) {
+        $keys['#negation'] = TRUE;
+      }
+      else {
+        // We can't know how negation is expressed in the server's syntax.
+      }
+    }
+    if ($old) {
+      $keys = &$this->query->getKeys();
+      if (is_array($keys)) {
+        $keys[] = $old;
+      }
+      elseif (is_array($old)) {
+        // We don't support such nonsense.
+      }
+      else {
+        $keys = "($old) ($keys)";
+      }
+    }
+  }
+
+  /**
+   * Helper method to get an option list of all available fulltext fields.
+   */
+  protected function getFulltextFields() {
+    $fields = array();
+    $index = search_api_index_load(substr($this->table, 17));
+    if (!empty($index->options['fields'])) {
+      $f = $index->getFields();
+      foreach ($index->getFulltextFields() as $name) {
+        $fields[$name] = $f[$name]['name'];
+      }
+    }
+    return $fields;
+  }
+
+}

+ 59 - 0
sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_language.inc

@@ -0,0 +1,59 @@
+<?php
+
+/**
+ * @file
+ *   Contains the SearchApiViewsHandlerFilterLanguage class.
+ */
+
+/**
+ * Views filter handler class for handling the special "Item language" field.
+ *
+ * Definition items:
+ * - options: An array of possible values for this field.
+ */
+class SearchApiViewsHandlerFilterLanguage extends SearchApiViewsHandlerFilterOptions {
+
+  /**
+   * Provide a form for setting options.
+   */
+  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;
+  }
+
+  /**
+   * Add this filter to the query.
+   */
+  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;
+      }
+      elseif ($v == 'default') {
+        $this->value[$i] = language_default('language');
+      }
+    }
+    parent::query();
+  }
+
+}

+ 315 - 0
sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_options.inc

@@ -0,0 +1,315 @@
+<?php
+
+/**
+ * @file
+ * Contains the SearchApiViewsHandlerFilterOptions class.
+ */
+
+/**
+ * Views filter handler for fields with a limited set of possible values.
+ */
+class SearchApiViewsHandlerFilterOptions extends SearchApiViewsHandlerFilter {
+
+  /**
+   * Stores the values which are available on the form.
+   *
+   * @var array
+   */
+  protected $value_options = NULL;
+
+  /**
+   * The type of form element used to display the options.
+   *
+   * @var string
+   */
+  protected $value_form_type = 'checkboxes';
+
+  /**
+   * Retrieves a wrapper for this filter's field.
+   *
+   * @return EntityMetadataWrapper|null
+   *   A wrapper for the field which this filter uses.
+   */
+  protected function get_wrapper() {
+    if ($this->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() {
+    $options = array(
+      '=' => t('Is 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;
+  }
+
+  /**
+   * Set "reduce" option to FALSE by default.
+   */
+  public function expose_options() {
+    parent::expose_options();
+    $this->options['expose']['reduce'] = FALSE;
+  }
+
+  /**
+   * Add the "reduce" option to the exposed form.
+   */
+  public function expose_form(&$form, &$form_state) {
+    parent::expose_form($form, $form_state);
+    $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' => !empty($this->options['expose']['reduce']),
+    );
+  }
+
+  /**
+   * Define "reduce" option.
+   */
+  public function option_definition() {
+    $options = parent::option_definition();
+    $options['expose']['contains']['reduce'] = array('default' => FALSE);
+    return $options;
+  }
+
+  /**
+   * Reduce the options according to the selection.
+   */
+  protected function reduce_value_options() {
+    foreach ($this->value_options as $id => $option) {
+      if (!isset($this->options['value'][$id])) {
+        unset($this->value_options[$id]);
+      }
+    }
+    return $this->value_options;
+  }
+
+  /**
+   * Save set checkboxes.
+   */
+  public function value_submit($form, &$form_state) {
+    // Drupal's FAPI system automatically puts '0' in for any checkbox that
+    // was not set, and the key to the checkbox if it is set.
+    // Unfortunately, this means that if the key to that checkbox is 0,
+    // we are unable to tell if that checkbox was set or not.
+
+    // Luckily, the '#value' on the checkboxes form actually contains
+    // *only* a list of checkboxes that were set, and we can use that
+    // instead.
+
+    $form_state['values']['options']['value'] = $form['value']['#value'];
+  }
+
+  /**
+   * Provide a form for setting options.
+   */
+  public function value_form(&$form, &$form_state) {
+    $this->get_value_options();
+    if (!empty($this->options['expose']['reduce']) && !empty($form_state['exposed'])) {
+      $options = $this->reduce_value_options();
+    }
+    else {
+      $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($options)),
+      '#default_value' => is_array($this->value) ? $this->value : array(),
+    );
+
+    // 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 (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'),
+      );
+    }
+  }
+
+  /**
+   * Provides a summary of this filter's value for the admin UI.
+   */
+  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 (!is_array($this->value)) {
+      return;
+    }
+
+    $operator_options = $this->operator_options();
+    $operator = $operator_options[$this->operator];
+    $values = '';
+
+    // Remove every element which is not known.
+    $this->get_value_options();
+    foreach ($this->value as $i => $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');
+    }
+    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($operator);
+      $values = check_plain($this->value_options[reset($this->value)]);
+    }
+    else {
+      foreach ($this->value as $value) {
+        if ($values !== '') {
+          $values .= ', ';
+        }
+        if (drupal_strlen($values) > 20) {
+          $values .= '…';
+          break;
+        }
+        $values .= check_plain($this->value_options[$value]);
+      }
+    }
+
+    return $operator . (($values !== '') ? ' ' . $values : '');
+  }
+
+  /**
+   * Add this filter to the query.
+   */
+  public function query() {
+    if ($this->operator === 'empty') {
+      $this->query->condition($this->real_field, NULL, '=', $this->options['group']);
+      return;
+    }
+    if ($this->operator === 'not empty') {
+      $this->query->condition($this->real_field, NULL, '<>', $this->options['group']);
+      return;
+    }
+
+    // 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']);
+      }
+      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']);
+      }
+    }
+  }
+
+}

+ 294 - 0
sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_taxonomy_term.inc

@@ -0,0 +1,294 @@
+<?php
+
+/**
+ * @file
+ * Contains SearchApiViewsHandlerFilterTaxonomyTerm.
+ */
+
+/**
+ * Views filter handler class for taxonomy term entities.
+ *
+ * Based on views_handler_filter_term_node_tid.
+ */
+class SearchApiViewsHandlerFilterTaxonomyTerm extends SearchApiViewsHandlerFilterEntity {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function has_extra_options() {
+    return !empty($this->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['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);
+        $options = array();
+
+        if ($tree) {
+          foreach ($tree as $term) {
+            $choice = new stdClass();
+            $choice->option = array($term->tid => str_repeat('-', $term->depth) . $term->name);
+            $options[] = $choice;
+          }
+        }
+      }
+      else {
+        $options = array();
+        $query = db_select('taxonomy_term_data', 'td');
+        $query->innerJoin('taxonomy_vocabulary', 'tv', 'td.vid = tv.vid');
+        $query->fields('td');
+        $query->orderby('tv.weight');
+        $query->orderby('tv.name');
+        $query->orderby('td.weight');
+        $query->orderby('td.name');
+        $query->addTag('term_access');
+        if ($vocabulary) {
+          $query->condition('tv.machine_name', $vocabulary->machine_name);
+        }
+        $result = $query->execute();
+        foreach ($result as $term) {
+          $options[$term->tid] = $term->name;
+        }
+      }
+
+      $default_value = (array) $this->value;
+
+      if (!empty($form_state['exposed'])) {
+        $identifier = $this->options['expose']['identifier'];
+
+        if (!empty($this->options['expose']['reduce'])) {
+          $options = $this->reduce_value_options($options);
+
+          if (!empty($this->options['expose']['multiple']) && empty($this->options['expose']['required'])) {
+            $default_value = array();
+          }
+        }
+
+        if (empty($this->options['expose']['multiple'])) {
+          if (empty($this->options['expose']['required']) && (empty($default_value) || !empty($this->options['expose']['reduce']))) {
+            $default_value = 'All';
+          }
+          elseif (empty($default_value)) {
+            $keys = array_keys($options);
+            $default_value = array_shift($keys);
+          }
+          // Due to #1464174 there is a chance that array('') was saved in the
+          // admin ui. Let's choose a safe default value.
+          elseif ($default_value == array('')) {
+            $default_value = 'All';
+          }
+          else {
+            $copy = $default_value;
+            $default_value = array_shift($copy);
+          }
+        }
+      }
+      $form['value']['#type'] = 'select';
+      $form['value']['#multiple'] = TRUE;
+      $form['value']['#options'] = $options;
+      $form['value']['#size'] = min(9, count($options));
+      $form['value']['#default_value'] = $default_value;
+
+      if (!empty($form_state['exposed']) && isset($identifier) && !isset($form_state['input'][$identifier])) {
+        $form_state['input'][$identifier] = $default_value;
+      }
+    }
+  }
+
+  /**
+   * Reduces the available exposed options according to the selection.
+   */
+  protected function reduce_value_options(array $options) {
+    foreach ($options as $id => $option) {
+      if (empty($this->options['value'][$id])) {
+        unset($options[$id]);
+      }
+    }
+    return $options;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function value_validate($form, &$form_state) {
+    // We only validate if they've chosen the text field style.
+    if ($this->options['type'] != 'textfield') {
+      return;
+    }
+
+    parent::value_validate($form, $form_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function accept_exposed_input($input) {
+    if (empty($this->options['exposed'])) {
+      return TRUE;
+    }
+
+    // If view is an attachment and is inheriting exposed filters, then assume
+    // exposed input has already been validated.
+    if (!empty($this->view->is_attachment) && $this->view->display_handler->uses_exposed()) {
+      $this->validated_exposed_input = (array) $this->view->exposed_raw_input[$this->options['expose']['identifier']];
+    }
+
+    // If it's non-required and there's no value don't bother filtering.
+    if (!$this->options['expose']['required'] && empty($this->validated_exposed_input)) {
+      return FALSE;
+    }
+
+    return parent::accept_exposed_input($input);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function exposed_validate(&$form, &$form_state) {
+    if (empty($this->options['exposed']) || empty($this->options['expose']['identifier'])) {
+      return;
+    }
+
+    // We only validate if they've chosen the text field style.
+    if ($this->options['type'] != 'textfield') {
+      $input = $form_state['values'][$this->options['expose']['identifier']];
+      if ($this->options['is_grouped'] && isset($this->options['group_info']['group_items'][$input])) {
+        $input = $this->options['group_info']['group_items'][$input]['value'];
+      }
+
+      if ($input != 'All')  {
+        $this->validated_exposed_input = (array) $input;
+      }
+      return;
+    }
+
+    parent::exposed_validate($form, $form_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function validate_entity_strings(array &$form, array $values) {
+    if (empty($values)) {
+      return array();
+    }
+
+    $tids = array();
+    $names = array();
+    $missing = array();
+    foreach ($values as $value) {
+      $missing[strtolower($value)] = TRUE;
+      $names[] = $value;
+    }
+
+    if (!$names) {
+      return FALSE;
+    }
+
+    $query = db_select('taxonomy_term_data', 'td');
+    $query->innerJoin('taxonomy_vocabulary', 'tv', 'td.vid = tv.vid');
+    $query->fields('td');
+    $query->condition('td.name', $names);
+    if (!empty($this->definition['vocabulary'])) {
+      $query->condition('tv.machine_name', $this->definition['vocabulary']);
+    }
+    $query->addTag('term_access');
+    $result = $query->execute();
+    foreach ($result as $term) {
+      unset($missing[strtolower($term->name)]);
+      $tids[] = $term->tid;
+    }
+
+    if ($missing) {
+      if (!empty($this->options['error_message'])) {
+        form_error($form, format_plural(count($missing), 'Unable to find term: @terms', 'Unable to find terms: @terms', array('@terms' => implode(', ', array_keys($missing)))));
+      }
+      else {
+        // Add a bogus TID which will show an empty result for a positive filter
+        // and be ignored for an excluding one.
+        $tids[] = 0;
+      }
+    }
+
+    return $tids;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function expose_form(&$form, &$form_state) {
+    parent::expose_form($form, $form_state);
+    if ($this->options['type'] != 'select') {
+      unset($form['expose']['reduce']);
+    }
+    $form['error_message'] = array(
+      '#type' => 'checkbox',
+      '#title' => t('Display error message'),
+      '#description' => t('Display an error message if one of the entered terms could not be found.'),
+      '#default_value' => !empty($this->options['error_message']),
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function ids_to_strings(array $ids) {
+    return implode(', ', db_select('taxonomy_term_data', 'td')
+      ->fields('td', array('name'))
+      ->condition('td.tid', array_filter($ids))
+      ->execute()
+      ->fetchCol());
+  }
+
+}

+ 20 - 0
sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_text.inc

@@ -0,0 +1,20 @@
+<?php
+
+/**
+ * @file
+ * Contains SearchApiViewsHandlerFilterText.
+ */
+
+/**
+ * Views filter handler class for handling fulltext fields.
+ */
+class SearchApiViewsHandlerFilterText extends SearchApiViewsHandlerFilter {
+
+  /**
+   * Provide a list of options for the operator form.
+   */
+  public function operator_options() {
+    return array('=' => t('contains'), '<>' => t("doesn't contain"));
+  }
+
+}

+ 77 - 0
sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_filter_user.inc

@@ -0,0 +1,77 @@
+<?php
+
+/**
+ * @file
+ * Contains SearchApiViewsHandlerFilterUser.
+ */
+
+/**
+ * Views filter handler class for handling user entities.
+ *
+ * Based on views_handler_filter_user_name.
+ */
+class SearchApiViewsHandlerFilterUser extends SearchApiViewsHandlerFilterEntity {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function value_form(&$form, &$form_state) {
+    parent::value_form($form, $form_state);
+
+    // Set autocompletion.
+    $path = $this->isMultiValued() ? 'admin/views/ajax/autocomplete/user' : 'user/autocomplete';
+    $form['value']['#autocomplete_path'] = $path;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function ids_to_strings(array $ids) {
+    $names = array();
+    $args[':uids'] = array_filter($ids);
+    $result = db_query("SELECT uid, name FROM {users} u WHERE uid IN (:uids)", $args);
+    $result = $result->fetchAllKeyed();
+    foreach ($ids as $uid) {
+      if (!$uid) {
+        $names[] = variable_get('anonymous', t('Anonymous'));
+      }
+      elseif (isset($result[$uid])) {
+        $names[] = $result[$uid];
+      }
+    }
+    return implode(', ', $names);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function validate_entity_strings(array &$form, array $values) {
+    $uids = array();
+    $missing = array();
+    foreach ($values as $value) {
+      if (drupal_strtolower($value) === drupal_strtolower(variable_get('anonymous', t('Anonymous')))) {
+        $uids[] = 0;
+      }
+      else {
+        $missing[strtolower($value)] = $value;
+      }
+    }
+
+    if (!$missing) {
+      return $uids;
+    }
+
+    $result = db_query("SELECT * FROM {users} WHERE name IN (:names)", array(':names' => array_values($missing)));
+    foreach ($result as $account) {
+      unset($missing[strtolower($account->name)]);
+      $uids[] = $account->uid;
+    }
+
+    if ($missing) {
+      form_error($form, format_plural(count($missing), 'Unable to find user: @users', 'Unable to find users: @users', array('@users' => implode(', ', $missing))));
+    }
+
+    return $uids;
+  }
+
+}

+ 35 - 0
sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/handler_sort.inc

@@ -0,0 +1,35 @@
+<?php
+
+/**
+ * @file
+ * Contains SearchApiViewsHandlerSort.
+ */
+
+/**
+ * Class for sorting results according to a specified field.
+ */
+class SearchApiViewsHandlerSort extends views_handler_sort {
+
+  /**
+   * The associated views query object.
+   *
+   * @var SearchApiViewsQuery
+   */
+  public $query;
+
+  /**
+   * Called to add the sort to a query.
+   */
+  public function query() {
+    // When there are exposed sorts, the "exposed form" plugin will set
+    // $query->orderby to an empty array. Therefore, if that property is set,
+    // we here remove all previous sorts.
+    if (isset($this->query->orderby)) {
+      unset($this->query->orderby);
+      $sort = &$this->query->getSort();
+      $sort = array();
+    }
+    $this->query->sort($this->real_field, $this->options['order']);
+  }
+
+}

+ 128 - 0
sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/plugin_cache.inc

@@ -0,0 +1,128 @@
+<?php
+
+/**
+ * @file
+ * Contains the SearchApiViewsCache class.
+ */
+
+/**
+ * Plugin class for caching Search API views.
+ */
+class SearchApiViewsCache extends views_plugin_cache_time {
+
+  /**
+   * Static cache for get_results_key().
+   *
+   * @var string
+   */
+  protected $_results_key = NULL;
+
+  /**
+   * Static cache for getSearchApiQuery().
+   *
+   * @var SearchApiQueryInterface
+   */
+  protected $search_api_query = NULL;
+
+  /**
+   * Overrides views_plugin_cache::cache_set().
+   *
+   * Also stores Search API's internal search results.
+   */
+  public function cache_set($type) {
+    if ($type != 'results') {
+      return parent::cache_set($type);
+    }
+
+    $cid = $this->get_results_key();
+    $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' => $this->view->query->getSearchApiResults(),
+    );
+    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_results_key().
+   *
+   * Use the Search API query as the main source for the key.
+   */
+  public function get_results_key() {
+    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'],
+      );
+      // 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'];
+      }
+
+      $this->_results_key = $this->view->name . ':' . $this->display->id . ':results:' . md5(serialize($key_data));
+    }
+
+    return $this->_results_key;
+  }
+
+  /**
+   * Get 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;
+  }
+
+}

+ 683 - 0
sites/all/modules/contrib/search/search_api/contrib/search_api_views/includes/query.inc

@@ -0,0 +1,683 @@
+<?php
+
+/**
+ * @file
+ * Contains SearchApiViewsQuery.
+ */
+
+/**
+ * Views query class using a Search API index as the data source.
+ */
+class SearchApiViewsQuery extends views_plugin_query {
+
+  /**
+   * Number of results to display.
+   *
+   * @var int
+   */
+  protected $limit;
+
+  /**
+   * Offset of first displayed result.
+   *
+   * @var int
+   */
+  protected $offset;
+
+  /**
+   * The index this view accesses.
+   *
+   * @var SearchApiIndex
+   */
+  protected $index;
+
+  /**
+   * The query that will be executed.
+   *
+   * @var SearchApiQueryInterface
+   */
+  protected $query;
+
+  /**
+   * The results returned by the query, after it was executed.
+   *
+   * @var array
+   */
+  protected $search_api_results = array();
+
+  /**
+   * Array of all encountered errors.
+   *
+   * Each of these is fatal, meaning that a non-empty $errors property will
+   * result in an empty result being returned.
+   *
+   * @var array
+   */
+  protected $errors;
+
+  /**
+   * Whether to abort the search instead of executing it.
+   *
+   * @var bool
+   */
+  protected $abort = FALSE;
+
+  /**
+   * The names of all fields whose value is required by a handler.
+   *
+   * The format follows the same as Search API field identifiers (parent:child).
+   *
+   * @var array
+   */
+  protected $fields;
+
+  /**
+   * The query's sub-filters representing the different Views filter groups.
+   *
+   * @var array
+   */
+  protected $filters = array();
+
+  /**
+   * The conjunction with which multiple filter groups are combined.
+   *
+   * @var string
+   */
+  public $group_operator = 'AND';
+
+  /**
+   * Create the basic query object and fill with default values.
+   */
+  public function init($base_table, $base_field, $options) {
+    try {
+      $this->errors = array();
+      parent::init($base_table, $base_field, $options);
+      $this->fields = array();
+      if (substr($base_table, 0, 17) == 'search_api_index_') {
+        $id = substr($base_table, 17);
+        $this->index = search_api_index_load($id);
+        $this->query = $this->index->query(array(
+          'parse mode' => $this->options['parse_mode'],
+        ));
+      }
+    }
+    catch (Exception $e) {
+      $this->errors[] = $e->getMessage();
+    }
+  }
+
+  /**
+   * Add a field that should be retrieved from the results by this view.
+   *
+   * @param $field
+   *   The field's identifier, as used by the Search API. E.g., "title" for a
+   *   node's title, "author:name" for a node's author's name.
+   *
+   * @return SearchApiViewsQuery
+   *   The called object.
+   */
+  public function addField($field) {
+    $this->fields[$field] = TRUE;
+    return $field;
+  }
+
+  /**
+   * Add a sort to the query.
+   *
+   * @param $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
+   *   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);
+  }
+
+  /**
+   * Defines the options used by this query plugin.
+   *
+   * 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',
+      ),
+    );
+  }
+
+  /**
+   * Add settings for the UI.
+   *
+   * Adds an option for bypassing access checks.
+   */
+  public function options_form(&$form, &$form_state) {
+    parent::options_form($form, $form_state);
+
+    $form['search_api_bypass_access'] = array(
+      '#type' => 'checkbox',
+      '#title' => t('Bypass access checks'),
+      '#description' => t('If the underlying search index has access checks enabled, this option allows to disable them for this view.'),
+      '#default_value' => $this->options['search_api_bypass_access'],
+    );
+
+    if (entity_get_info($this->index->item_type)) {
+      $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) {
+    $this->view = $view;
+
+    // Setup the nested filter structure for this query.
+    if (!empty($this->where)) {
+      // If the different groups are combined with the OR operator, we have to
+      // add a new OR filter to the query to which the filters for the groups
+      // will be added.
+      if ($this->group_operator === 'OR') {
+        $base = $this->query->createFilter('OR');
+        $this->query->filter($base);
+      }
+      else {
+        $base = $this->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']);
+          if (!empty($group['conditions'])) {
+            foreach ($group['conditions'] as $condition) {
+              list($field, $value, $operator) = $condition;
+              $filter->condition($field, $value, $operator);
+            }
+          }
+          if (!empty($group['filters'])) {
+            foreach ($group['filters'] as $nested_filter) {
+              $filter->filter($nested_filter);
+            }
+          }
+          // If no group was given, the filters were already set on the query.
+          if ($group_id !== '') {
+            $base->filter($filter);
+          }
+        }
+      }
+    }
+
+    // Initialize the pager and let it modify the query to add limits.
+    $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);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function alter(&$view) {
+    parent::alter($view);
+    drupal_alter('search_api_views_query', $view, $this);
+  }
+
+  /**
+   * Executes the query and fills the associated view object with according
+   * values.
+   *
+   * Values to set: $view->result, $view->total_rows, $view->execute_time,
+   * $view->pager['current_page'].
+   */
+  public function execute(&$view) {
+    if ($this->errors || $this->abort) {
+      if (error_displayable()) {
+        foreach ($this->errors as $msg) {
+          drupal_set_message(check_plain($msg), 'error');
+        }
+      }
+      $view->result = array();
+      $view->total_rows = 0;
+      $view->execute_time = 0;
+      return;
+    }
+
+    // Calculate the "skip result count" option, if it wasn't already set to
+    // FALSE.
+    $skip_result_count = $this->query->getOption('skip result count', TRUE);
+    if ($skip_result_count) {
+      $skip_result_count = !$this->pager->use_count_query() && empty($view->get_total_rows);
+      $this->query->setOption('skip result count', $skip_result_count);
+    }
+
+    try {
+      // Trigger pager pre_execute().
+      $this->pager->pre_execute($this->query);
+
+      // 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.
+      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();
+      }
+      $view->result = array();
+      if (!empty($results['results'])) {
+        $this->addResults($results['results'], $view);
+      }
+      // We shouldn't use $results['performance']['complete'] here, since
+      // extracting the results probably takes considerable time as well.
+      $view->execute_time = microtime(TRUE) - $start;
+
+      // Trigger pager post_execute().
+      $this->pager->post_execute($view->result);
+    }
+    catch (Exception $e) {
+      $this->errors[] = $e->getMessage();
+      // Recursion to get the same error behaviour as above.
+      return $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.
+   */
+  protected function addResults(array $results, $view) {
+    $rows = array();
+    $missing = array();
+    $items = array();
+
+    // First off, we try to gather as much field values as possible without
+    // loading any items.
+    foreach ($results as $id => $result) {
+      if (!empty($this->options['entity_access'])) {
+        $entity = entity_load($this->index->item_type, array($id));
+        if (!entity_access('view', $this->index->item_type, $entity[$id])) {
+          continue;
+        }
+      }
+      $row = array();
+
+      // Include the loaded item for this result row, if present, or the item
+      // ID.
+      if (!empty($result['entity'])) {
+        $row['entity'] = $result['entity'];
+      }
+      else {
+        $row['entity'] = $id;
+      }
+
+      $row['_entity_properties']['search_api_relevance'] = $result['score'];
+      $row['_entity_properties']['search_api_excerpt'] = empty($result['excerpt']) ? '' : $result['excerpt'];
+
+      // Gather any fields from the search results.
+      if (!empty($result['fields'])) {
+        $row['_entity_properties'] += $result['fields'];
+      }
+
+      // Check whether we need to extract any properties from the result item.
+      $missing_fields = array_diff_key($this->fields, $row);
+      if ($missing_fields) {
+        $missing[$id] = $missing_fields;
+        if (is_object($row['entity'])) {
+          $items[$id] = $row['entity'];
+        }
+        else {
+          $ids[] = $id;
+        }
+      }
+
+      // Save the row values for adding them to the Views result afterwards.
+      $rows[$id] = (object) $row;
+    }
+
+    // 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;
+      }
+    }
+
+    // Finally, add all rows to the Views result set.
+    $view->result = array_values($rows);
+  }
+
+  /**
+   * Helper function for extracting all necessary fields from a result item.
+   *
+   * Usually, this method isn't needed anymore as the properties are now
+   * extracted by the field handlers themselves.
+   */
+  protected function extractFields(EntityMetadataWrapper $wrapper, array $all_fields) {
+    $fields = array();
+    foreach ($all_fields as $key => $true) {
+      $fields[$key]['type'] = 'string';
+    }
+    $fields = search_api_extract_fields($wrapper, $fields, array('sanitized' => TRUE));
+    $ret = array();
+    foreach ($all_fields as $key => $true) {
+      $ret[$key] = isset($fields[$key]['value']) ? $fields[$key]['value'] : '';
+    }
+    return $ret;
+  }
+
+  /**
+   * Returns the according entity objects for the given query results.
+   *
+   * This is necessary to support generic entity handlers and plugins with this
+   * query backend.
+   *
+   * If the current query isn't based on an entity type, the method will return
+   * an empty array.
+   */
+  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 $i => $wrapper) {
+      try {
+        // 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) {
+        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);
+  }
+
+  /**
+   * Returns the according metadata wrappers for the given query results.
+   *
+   * This is necessary to support generic entity handlers and plugins with this
+   * query backend.
+   */
+  public function get_result_wrappers($results, $relationship = NULL, $field = NULL) {
+    $entity_type = $this->index->getEntityType();
+    $wrappers = array();
+    $load_entities = array();
+    foreach ($results as $row_index => $row) {
+      if ($entity_type && 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;
+        }
+
+        $wrappers[$row_index] = $this->index->entityWrapper($row->entity);
+      }
+    }
+
+    // If the results are entities, we pre-load them to make use of a multiple
+    // load. (Otherwise, each result would be loaded individually.)
+    if (!empty($load_entities)) {
+      $entities = entity_load($entity_type, array_keys($load_entities));
+      foreach ($entities as $entity_id => $entity) {
+        $wrappers[$load_entities[$entity_id]] = $this->index->entityWrapper($entity);
+      }
+    }
+
+    // Apply the relationship, if necessary.
+    $type = $entity_type ? $entity_type : $this->index->item_type;
+    $selector_suffix = '';
+    if ($field && ($pos = strrpos($field, ':'))) {
+      $selector_suffix = substr($field, 0, $pos);
+    }
+    if ($selector_suffix || ($relationship && !empty($this->view->relationship[$relationship]))) {
+      // Use EntityFieldHandlerHelper to compute the correct data selector for
+      // the relationship.
+      $handler = (object) array(
+        'view' => $this->view,
+        'relationship' => $relationship,
+        'real_field' => '',
+      );
+      $selector = EntityFieldHandlerHelper::construct_property_selector($handler);
+      $selector .= ($selector ? ':' : '') . $selector_suffix;
+      list($type, $wrappers) = EntityFieldHandlerHelper::extract_property_multiple($wrappers, $selector);
+    }
+
+    return array($type, $wrappers);
+  }
+
+  /**
+   * API function for accessing the raw Search API query object.
+   *
+   * @return SearchApiQueryInterface
+   *   The search query object used internally by this handler.
+   */
+  public function getSearchApiQuery() {
+    return $this->query;
+  }
+
+  /**
+   * API function for accessing the raw Search API results.
+   *
+   * @return array
+   *   An associative array containing the search results, as specified by
+   *   SearchApiQueryInterface::execute().
+   */
+  public function getSearchApiResults() {
+    return $this->search_api_results;
+  }
+
+  //
+  // Query interface methods (proxy to $this->query)
+  //
+
+  public function createFilter($conjunction = 'AND', $tags = array()) {
+    if (!$this->errors) {
+      return $this->query->createFilter($conjunction, $tags);
+    }
+  }
+
+  public function keys($keys = NULL) {
+    if (!$this->errors) {
+      $this->query->keys($keys);
+    }
+    return $this;
+  }
+
+  public function fields(array $fields) {
+    if (!$this->errors) {
+      $this->query->fields($fields);
+    }
+    return $this;
+  }
+
+  /**
+   * Adds a nested filter to the search query object.
+   *
+   * If $group is given, the filter is added to the relevant filter group
+   * instead.
+   */
+  public function filter(SearchApiQueryFilterInterface $filter, $group = NULL) {
+    if (!$this->errors) {
+      $this->where[$group]['filters'][] = $filter;
+    }
+    return $this;
+  }
+
+  /**
+   * Set a condition on the search query object.
+   *
+   * If $group is given, the condition is added to the relevant filter group
+   * instead.
+   */
+  public function condition($field, $value, $operator = '=', $group = NULL) {
+    if (!$this->errors) {
+      $this->where[$group]['conditions'][] = array($field, $value, $operator);
+    }
+    return $this;
+  }
+
+  public function sort($field, $order = 'ASC') {
+    if (!$this->errors) {
+      $this->query->sort($field, $order);
+    }
+    return $this;
+  }
+
+  public function range($offset = NULL, $limit = NULL) {
+    if (!$this->errors) {
+      $this->query->range($offset, $limit);
+    }
+    return $this;
+  }
+
+  public function getIndex() {
+    return $this->index;
+  }
+
+  public function &getKeys() {
+    if (!$this->errors) {
+      return $this->query->getKeys();
+    }
+    $ret = NULL;
+    return $ret;
+  }
+
+  public function getOriginalKeys() {
+    if (!$this->errors) {
+      return $this->query->getOriginalKeys();
+    }
+  }
+
+  public function &getFields() {
+    if (!$this->errors) {
+      return $this->query->getFields();
+    }
+    $ret = NULL;
+    return $ret;
+  }
+
+  public function getFilter() {
+    if (!$this->errors) {
+      return $this->query->getFilter();
+    }
+  }
+
+  public function &getSort() {
+    if (!$this->errors) {
+      return $this->query->getSort();
+    }
+    $ret = NULL;
+    return $ret;
+  }
+
+  public function getOption($name) {
+    if (!$this->errors) {
+      return $this->query->getOption($name);
+    }
+  }
+
+  public function setOption($name, $value) {
+    if (!$this->errors) {
+      return $this->query->setOption($name, $value);
+    }
+  }
+
+  public function &getOptions() {
+    if (!$this->errors) {
+      return $this->query->getOptions();
+    }
+    $ret = NULL;
+    return $ret;
+  }
+
+}

+ 34 - 0
sites/all/modules/contrib/search/search_api/contrib/search_api_views/search_api_views.api.php

@@ -0,0 +1,34 @@
+<?php
+
+/**
+ * @file
+ * Hooks provided by the Search Views module.
+ */
+
+/**
+ * Alter the query before executing the query.
+ *
+ * @param view $view
+ *   The view object about to be processed.
+ * @param SearchApiViewsQuery $query
+ *   The Search API Views query to be altered.
+ *
+ * @see hook_views_query_alter()
+ */
+function hook_search_api_views_query_alter(view &$view, SearchApiViewsQuery &$query) {
+  // (Example assuming a view with an exposed filter on node title.)
+  // If the input for the title filter is a positive integer, filter against
+  // node ID instead of node title.
+  if ($view->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'],'=');
+        }
+      }
+    }
+  }
+}

+ 35 - 0
sites/all/modules/contrib/search/search_api/contrib/search_api_views/search_api_views.info

@@ -0,0 +1,35 @@
+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
+core = 7.x
+package = Search
+
+; 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_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_options.inc
+files[] = includes/handler_filter_taxonomy_term.inc
+files[] = includes/handler_filter_text.inc
+files[] = includes/handler_filter_user.inc
+files[] = includes/handler_sort.inc
+files[] = includes/plugin_cache.inc
+files[] = includes/query.inc
+
+; Information added by Drupal.org packaging script on 2013-12-25
+version = "7.x-1.11"
+core = "7.x"
+project = "search_api"
+datestamp = "1387965506"
+

+ 105 - 0
sites/all/modules/contrib/search/search_api/contrib/search_api_views/search_api_views.install

@@ -0,0 +1,105 @@
+<?php
+
+/**
+ * @file
+ * Install, update and uninstall functions for the search_api_views module.
+ */
+
+/**
+ * Updates all Search API views to use the new, specification-compliant identifiers.
+ */
+function search_api_views_update_7101() {
+  $tables = views_fetch_data();
+  // Contains arrays with real fields mapped to field IDs for each table.
+  $table_fields = array();
+  foreach ($tables as $key => $table) {
+    if (substr($key, 0, 17) != 'search_api_index_') {
+      continue;
+    }
+    foreach ($table as $field => $info) {
+      if (isset($info['real field']) && $field != $info['real field']) {
+        $table_fields[$key][$info['real field']] = $field;
+      }
+    }
+  }
+  if (!$table_fields) {
+    return;
+  }
+  foreach (views_get_all_views() as $view) {
+    if (empty($view->base_table) || empty($table_fields[$view->base_table])) {
+      continue;
+    }
+    $change = FALSE;
+    $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 &$display) {
+        $options = &$display->display_options;
+        if (isset($options['style_options']['grouping'])) {
+          $change |= _search_api_views_update_7101_helper($options['style_options']['grouping'], $fields);
+        }
+        if (isset($options['style_options']['columns'])) {
+          $change |= _search_api_views_update_7101_helper($options['style_options']['columns'], $fields);
+        }
+        if (isset($options['style_options']['info'])) {
+          $change |= _search_api_views_update_7101_helper($options['style_options']['info'], $fields);
+        }
+        if (isset($options['arguments'])) {
+          $change |= _search_api_views_update_7101_helper($options['arguments'], $fields);
+        }
+        if (isset($options['fields'])) {
+          $change |= _search_api_views_update_7101_helper($options['fields'], $fields);
+        }
+        if (isset($options['filters'])) {
+          $change |= _search_api_views_update_7101_helper($options['filters'], $fields);
+        }
+        if (isset($options['sorts'])) {
+          $change |= _search_api_views_update_7101_helper($options['sorts'], $fields);
+        }
+      }
+    }
+    if ($change) {
+      $view->save();
+    }
+  }
+}
+
+/**
+ * Helper function for replacing field identifiers.
+ *
+ * @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)) {
+    $change = FALSE;
+    $new_field = array();
+    foreach ($field as $k => $v) {
+      $new_k = $k;
+      $change |= _search_api_views_update_7101_helper($new_k, $fields);
+      $change |= _search_api_views_update_7101_helper($v, $fields);
+      $new_field[$new_k] = $v;
+    }
+    $field = $new_field;
+    return $change;
+  }
+  if (isset($fields[$field])) {
+    $field = $fields[$field];
+    return TRUE;
+  }
+  return FALSE;
+}
+
+/**
+ * Delete the now unnecessary "search_api_views_max_fields_depth" variable.
+ */
+function search_api_views_update_7102() {
+  variable_del('search_api_views_max_fields_depth');
+}

+ 67 - 0
sites/all/modules/contrib/search/search_api/contrib/search_api_views/search_api_views.module

@@ -0,0 +1,67 @@
+<?php
+
+/**
+ * @file
+ * Integrates the Search API with Views.
+ */
+
+/**
+ * Implements hook_views_api().
+ */
+function search_api_views_views_api() {
+  return array(
+    'api' => '3.0-alpha1',
+  );
+}
+
+/**
+ * Implements hook_search_api_index_insert().
+ */
+function search_api_views_search_api_index_insert() {
+  // Make the new index available for views.
+  views_invalidate_cache();
+}
+
+/**
+ * Implements hook_search_api_index_update().
+ */
+function search_api_views_search_api_index_update(SearchApiIndex $index) {
+  // Check whether index was disabled.
+  if (!$index->enabled && $index->original->enabled) {
+    _search_api_views_index_unavailable($index);
+  }
+
+  // Check whether the indexed fields changed.
+  $old_fields = $index->original->options + array('fields' => array());
+  $old_fields = $old_fields['fields'];
+  $new_fields = $index->options + array('fields' => array());
+  $new_fields = $new_fields['fields'];
+  if ($old_fields != $new_fields) {
+    views_invalidate_cache();
+  }
+}
+
+/**
+ * Implements hook_search_api_index_delete().
+ */
+function search_api_views_search_api_index_delete(SearchApiIndex $index) {
+  _search_api_views_index_unavailable($index);
+}
+
+/**
+ * Function for reacting to a disabled or deleted search index.
+ */
+function _search_api_views_index_unavailable(SearchApiIndex $index) {
+  $names = array();
+  $table = 'search_api_index_' . $index->machine_name;
+  foreach (views_get_all_views() as $name => $view) {
+    if (empty($view->disabled) && $view->base_table == $table) {
+      $names[] = $name;
+      // @todo: if ($index_deleted) $view->delete()?
+    }
+  }
+  if ($names) {
+    views_invalidate_cache();
+    drupal_set_message(t('The following views were using the index %name: @views. You should disable or delete them.', array('%name' => $index->name, '@views' => implode(', ', $names))), 'warning');
+  }
+}

+ 295 - 0
sites/all/modules/contrib/search/search_api/contrib/search_api_views/search_api_views.views.inc

@@ -0,0 +1,295 @@
+<?php
+
+/**
+ * @file
+ * Views hook implementations for the Search API Views module.
+ */
+
+/**
+ * Implements hook_views_data().
+ */
+function search_api_views_views_data() {
+  try {
+    $data = array();
+    $entity_types = entity_get_info();
+    foreach (search_api_index_load_multiple(FALSE) as $index) {
+      // Fill in base data.
+      $key = 'search_api_index_' . $index->machine_name;
+      $table = &$data[$key];
+      $type_info = search_api_get_item_type_info($index->item_type);
+      $table['table']['group'] = t('Indexed @entity_type', array('@entity_type' => $type_info['name']));
+      $table['table']['base'] = array(
+        'field' => 'search_api_id',
+        'index' => $index->machine_name,
+        'title' => $index->name,
+        'help' => t('Use the %name search index for filtering and retrieving data.', array('%name' => $index->name)),
+        'query class' => 'search_api_views_query',
+      );
+      if (isset($entity_types[$index->getEntityType()])) {
+        $table['table'] += array(
+          'entity type' => $index->getEntityType(),
+          'skip entity load' => TRUE,
+        );
+      }
+
+      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) {
+        $info = $property->info();
+        if ($info) {
+          entity_views_field_definition($key, $info, $table);
+        }
+      }
+
+      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;
+        $group = '';
+        $name = '';
+        $parts = explode(':', $key);
+        foreach ($parts as $i => $part) {
+          if (!isset($tmp->$part)) {
+            continue 2;
+          }
+          $tmp = $tmp->$part;
+          $info = $tmp->info();
+          $group = ($group ? $group . ' » ' . $name : ($name ? $name : ''));
+          $name = $info['label'];
+          if ($i < count($parts) - 1) {
+            // Unwrap lists.
+            $level = search_api_list_nesting_level($info['type']);
+            for ($j = 0; $j < $level; ++$j) {
+              $tmp = $tmp[0];
+            }
+          }
+        }
+        $id = _entity_views_field_identifier($key, $table);
+        if ($group) {
+          // @todo Entity type label instead of $group?
+          $table[$id]['group'] = $group;
+          $name = t('!field (indexed)', array('!field' => $name));
+        }
+        $table[$id]['title'] = $name;
+        $table[$id]['help'] = empty($info['description']) ? t('(No information available)') : $info['description'];
+        $table[$id]['type'] = $field['type'];
+        if ($id != $key) {
+          $table[$id]['real field'] = $key;
+        }
+        _search_api_views_add_handlers($key, $field, $tmp, $table);
+      }
+
+      // Special handlers
+      $table['search_api_language']['filter']['handler'] = 'SearchApiViewsHandlerFilterLanguage';
+
+      $table['search_api_id']['title'] = t('Entity ID');
+      $table['search_api_id']['help'] = t("The entity's ID.");
+      $table['search_api_id']['sort']['handler'] = 'SearchApiViewsHandlerSort';
+
+      $table['search_api_relevance']['group'] = t('Search');
+      $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']['handler'] = 'entity_views_handler_field_numeric';
+      $table['search_api_relevance']['field']['click sortable'] = TRUE;
+      $table['search_api_relevance']['sort']['handler'] = 'SearchApiViewsHandlerSort';
+
+      $table['search_api_excerpt']['group'] = t('Search');
+      $table['search_api_excerpt']['title'] = t('Excerpt');
+      $table['search_api_excerpt']['help'] = t('The search result excerpted to show found search terms.');
+      $table['search_api_excerpt']['field']['type'] = 'text';
+      $table['search_api_excerpt']['field']['handler'] = 'entity_views_handler_field_text';
+
+      $table['search_api_views_fulltext']['group'] = t('Search');
+      $table['search_api_views_fulltext']['title'] = t('Fulltext search');
+      $table['search_api_views_fulltext']['help'] = t('Search several or all fulltext fields at once.');
+      $table['search_api_views_fulltext']['filter']['handler'] = 'SearchApiViewsHandlerFilterFulltext';
+      $table['search_api_views_fulltext']['argument']['handler'] = 'SearchApiViewsHandlerArgumentFulltext';
+
+      $table['search_api_views_more_like_this']['group'] = t('Search');
+      $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 (isset($field_info['settings']['allowed_values'][0]['vocabulary'])) {
+            $vocabulary_fields[$field_info['settings']['allowed_values'][0]['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;
+  }
+  catch (Exception $e) {
+    watchdog_exception('search_api_views', $e);
+  }
+}
+
+/**
+ * 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'];
+  $inner_type = search_api_extract_inner_type($type);
+
+  if (strpos($id, ':')) {
+    entity_views_field_definition($id, $wrapper->info(), $table);
+  }
+  $id = _entity_views_field_identifier($id, $table);
+  $table += array($id => array());
+
+  if ($inner_type == 'text') {
+    $table[$id] += array(
+      'argument' => array(
+        'handler' => 'SearchApiViewsHandlerArgument',
+      ),
+      'filter' => array(
+        'handler' => 'SearchApiViewsHandlerFilterText',
+      ),
+    );
+    return;
+  }
+
+  $info = $wrapper->info();
+  if (isset($info['options list']) && is_callable($info['options list'])) {
+    $table[$id]['filter']['handler'] = 'SearchApiViewsHandlerFilterOptions';
+    $table[$id]['filter']['multi-valued'] = search_api_is_list_type($type);
+  }
+  elseif ($inner_type == 'boolean') {
+    $table[$id]['filter']['handler'] = 'SearchApiViewsHandlerFilterBoolean';
+  }
+  elseif ($inner_type == 'date') {
+    $table[$id]['filter']['handler'] = 'SearchApiViewsHandlerFilterDate';
+  }
+  elseif (isset($field['entity_type']) && $field['entity_type'] === 'user') {
+    $table[$id]['filter']['handler'] = 'SearchApiViewsHandlerFilterUser';
+  }
+  elseif (isset($field['entity_type']) && $field['entity_type'] === 'taxonomy_term') {
+    $table[$id]['filter']['handler'] = 'SearchApiViewsHandlerFilterTaxonomyTerm';
+    $info = $wrapper->info();
+    $field_info = field_info_field($info['name']);
+    // For the "Parent terms" and "All parent terms" properties, we can
+    // extrapolate the vocabulary from the parent in the selector. (E.g.,
+    // for "field_tags:parent" we can use the information of "field_tags".)
+    // Otherwise, we can't include any vocabulary information.
+    if (!$field_info && ($info['name'] == 'parent' || $info['name'] == 'parents_all')) {
+      if (!empty($table[$id]['real field'])) {
+        $parts = explode(':', $table[$id]['real field']);
+        $field_info = field_info_field($parts[count($parts) - 2]);
+      }
+    }
+    if (isset($field_info['settings']['allowed_values'][0]['vocabulary'])) {
+      $table[$id]['filter']['vocabulary'] = $field_info['settings']['allowed_values'][0]['vocabulary'];
+    }
+  }
+  else {
+    $table[$id]['filter']['handler'] = 'SearchApiViewsHandlerFilter';
+  }
+
+  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) {
+    $table[$id]['sort']['handler'] = 'SearchApiViewsHandlerSort';
+    if (isset($table[$id]['field'])) {
+      $table[$id]['field']['click sortable'] = TRUE;
+    }
+  }
+}
+
+/**
+ * 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',
+      ),
+    ),
+    '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,
+      ),
+    ),
+  );
+
+  if (module_exists('search_api_facetapi')) {
+    $ret['display']['search_api_views_facets_block'] = array(
+      'title' => t('Facets block'),
+      'help' => t('Display facets for this search as a block anywhere on the site.'),
+      'handler' => 'SearchApiViewsFacetsBlockDisplay',
+      'uses hook block' => TRUE,
+      'use ajax' => FALSE,
+      'use pager' => FALSE,
+      'use more' => TRUE,
+      'accept attachments' => TRUE,
+      'admin' => t('Facets block'),
+    );
+  }
+
+  return $ret;
+}

BIN
sites/all/modules/contrib/search/search_api/disabled.png


BIN
sites/all/modules/contrib/search/search_api/enabled.png


+ 185 - 0
sites/all/modules/contrib/search/search_api/includes/callback.inc

@@ -0,0 +1,185 @@
+<?php
+
+/**
+ * @file
+ * Contains base definitions for data alterations.
+ *
+ * Contains the SearchApiAlterCallbackInterface interface and the
+ * SearchApiAbstractAlterCallback class.
+ */
+
+/**
+ * Interface representing a Search API data-alter callback.
+ */
+interface SearchApiAlterCallbackInterface {
+
+  /**
+   * Construct a data-alter callback.
+   *
+   * @param SearchApiIndex $index
+   *   The index whose items will be altered.
+   * @param array $options
+   *   The callback options set for this index.
+   */
+  public function __construct(SearchApiIndex $index, array $options = array());
+
+  /**
+   * Check whether this data-alter callback is applicable for a certain index.
+   *
+   * This can be used for hiding the callback on the index's "Filters" tab. To
+   * avoid confusion, you should only use criteria that are immutable, such as
+   * the index's entity type. Also, since this is only used for UI purposes, you
+   * should not completely rely on this to ensure certain index configurations
+   * and at least throw an exception with a descriptive error message if this is
+   * violated on runtime.
+   *
+   * @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);
+
+  /**
+   * Display a form for configuring this callback.
+   *
+   * @return array
+   *   A form array for configuring this callback, or FALSE if no configuration
+   *   is possible.
+   */
+  public function configurationForm();
+
+  /**
+   * 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.
+   */
+  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.
+   */
+  public function configurationFormSubmit(array $form, array &$values, array &$form_state);
+
+  /**
+   * Alter items before indexing.
+   *
+   * Items which are removed from the array won't be indexed, but will be marked
+   * as clean for future indexing. This could for instance be used to implement
+   * some sort of access filter for security purposes (e.g., don't index
+   * unpublished nodes or comments).
+   *
+   * @param array $items
+   *   An array of items to be altered, keyed by item IDs.
+   */
+  public function alterItems(array &$items);
+
+  /**
+   * Declare the properties that are added to items by this callback.
+   *
+   * If one of the specified properties 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.
+   *
+   * CAUTION: Since this method is used when calling
+   * SearchApiIndex::getFields(), calling that method from inside propertyInfo()
+   * will lead to a recursion and should therefore be avoided.
+   *
+   * @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();
+
+}
+
+/**
+ * Abstract base class for data-alter callbacks.
+ *
+ * This class implements most methods with sensible defaults.
+ *
+ * Extending classes will at least have to implement the alterItems() method to
+ * make this work. If that method adds additional fields to the items,
+ * propertyInfo() has to be overridden, too.
+ */
+abstract class SearchApiAbstractAlterCallback implements SearchApiAlterCallbackInterface {
+
+  /**
+   * The index whose items will be altered.
+   *
+   * @var SearchApiIndex
+   */
+  protected $index;
+
+  /**
+   * The configuration options for this callback, if it has any.
+   *
+   * @var array
+   */
+  protected $options;
+
+  /**
+   * Implements SearchApiAlterCallbackInterface::__construct().
+   */
+  public function __construct(SearchApiIndex $index, array $options = array()) {
+    $this->index = $index;
+    $this->options = $options;
+  }
+
+  /**
+   * Implements SearchApiAlterCallbackInterface::supportsIndex().
+   *
+   * The default implementation always returns TRUE.
+   */
+  public function supportsIndex(SearchApiIndex $index) {
+    return TRUE;
+  }
+
+  /**
+   * Implements SearchApiAlterCallbackInterface::configurationForm().
+   */
+  public function configurationForm() {
+    return array();
+  }
+
+  /**
+   * Implements SearchApiAlterCallbackInterface::configurationFormValidate().
+   */
+  public function configurationFormValidate(array $form, array &$values, array &$form_state) { }
+
+  /**
+   * Implements SearchApiAlterCallbackInterface::configurationFormSubmit().
+   */
+  public function configurationFormSubmit(array $form, array &$values, array &$form_state) {
+    $this->options = $values;
+    return $values;
+  }
+
+  /**
+   * Implements SearchApiAlterCallbackInterface::propertyInfo().
+   */
+  public function propertyInfo() {
+    return array();
+  }
+
+}

+ 322 - 0
sites/all/modules/contrib/search/search_api/includes/callback_add_aggregation.inc

@@ -0,0 +1,322 @@
+<?php
+
+/**
+ * @file
+ * Contains SearchApiAlterAddAggregation.
+ */
+
+/**
+ * Search API data alteration callback that adds an URL field for all items.
+ */
+class SearchApiAlterAddAggregation extends SearchApiAbstractAlterCallback {
+
+  public function configurationForm() {
+    $form['#attached']['css'][] = drupal_get_path('module', 'search_api') . '/search_api.admin.css';
+
+    $fields = $this->index->getFields(FALSE);
+    $field_options = array();
+    foreach ($fields as $name => $field) {
+      $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'];
+
+    $types = $this->getTypes();
+    $type_descriptions = $this->getTypes('description');
+    $tmp = array();
+    foreach ($types as $type => $name) {
+      $tmp[$type] = array(
+        '#type' => 'item',
+        '#description' => $type_descriptions[$type],
+      );
+    }
+    $type_descriptions = $tmp;
+
+    $form['#id'] = 'edit-callbacks-search-api-alter-add-aggregation-settings';
+    $form['description'] = array(
+      '#markup' => t('<p>This data alteration lets you define additional fields that will be added to this index. ' .
+        'Each of these new fields will be an aggregation of one or more existing fields.</p>' .
+        '<p>To add a new aggregated field, click the "Add new field" button and then fill out the form.</p>' .
+        '<p>To remove a previously defined field, click the "Remove field" button.</p>' .
+        '<p>You can also change the names or contained fields of existing aggregated fields.</p>'),
+    );
+    $form['fields']['#prefix'] = '<div id="search-api-alter-add-aggregation-field-settings">';
+    $form['fields']['#suffix'] = '</div>';
+    if (isset($this->changes)) {
+      $form['fields']['#prefix'] .= '<div class="messages warning">All changes in the form will not be saved until the <em>Save configuration</em> button at the form bottom is clicked.</div>';
+    }
+    foreach ($additional as $name => $field) {
+      $form['fields'][$name] = array(
+        '#type' => 'fieldset',
+        '#title' => $field['name'] ? $field['name'] : t('New field'),
+        '#collapsible' => TRUE,
+        '#collapsed' => (boolean) $field['name'],
+      );
+      $form['fields'][$name]['name'] = array(
+        '#type' => 'textfield',
+        '#title' => t('New field name'),
+        '#default_value' => $field['name'],
+        '#required' => TRUE,
+      );
+      $form['fields'][$name]['type'] = array(
+        '#type' => 'select',
+        '#title' => t('Aggregation type'),
+        '#options' => $types,
+        '#default_value' => $field['type'],
+        '#required' => TRUE,
+      );
+      $form['fields'][$name]['type_descriptions'] = $type_descriptions;
+      foreach (array_keys($types) as $type) {
+        $form['fields'][$name]['type_descriptions'][$type]['#states']['visible'][':input[name="callbacks[search_api_alter_add_aggregation][settings][fields][' . $name . '][type]"]']['value'] = $type;
+      }
+      $form['fields'][$name]['fields'] = array_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(
+          '#type' => 'submit',
+          '#value' => t('Remove field'),
+          '#submit' => array('_search_api_add_aggregation_field_submit'),
+          '#limit_validation_errors' => array(),
+          '#name' => 'search_api_add_aggregation_remove_' . $name,
+          '#ajax' => array(
+            'callback' => '_search_api_add_aggregation_field_ajax',
+            'wrapper' => 'search-api-alter-add-aggregation-field-settings',
+          ),
+        ),
+      );
+    }
+    $form['actions']['#type'] = 'actions';
+    $form['actions']['add_field'] = array(
+      '#type' => 'submit',
+      '#value' => t('Add new field'),
+      '#submit' => array('_search_api_add_aggregation_field_submit'),
+      '#limit_validation_errors' => array(),
+      '#ajax' => array(
+        'callback' => '_search_api_add_aggregation_field_ajax',
+        'wrapper' => 'search-api-alter-add-aggregation-field-settings',
+      ),
+    );
+    return $form;
+  }
+
+  public function configurationFormValidate(array $form, array &$values, array &$form_state) {
+    unset($values['actions']);
+    if (empty($values['fields'])) {
+      return;
+    }
+    foreach ($values['fields'] as $name => $field) {
+      $fields = $values['fields'][$name]['fields'] = array_values(array_filter($field['fields']));
+      unset($values['fields'][$name]['actions']);
+      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.'));
+      }
+    }
+  }
+
+  public function configurationFormSubmit(array $form, array &$values, array &$form_state) {
+    if (empty($values['fields'])) {
+      return array();
+    }
+    $index_fields = $this->index->getFields(FALSE);
+    foreach ($values['fields'] as $name => $field) {
+      if (!$field['name']) {
+        unset($values['fields'][$name]);
+      }
+      else {
+        $values['fields'][$name]['description'] = $this->fieldDescription($field, $index_fields);
+      }
+    }
+    $this->options = $values;
+    return $values;
+  }
+
+  public function alterItems(array &$items) {
+    if (!$items) {
+      return;
+    }
+    if (isset($this->options['fields'])) {
+      $types = $this->getTypes('type');
+      foreach ($items as $item) {
+        $wrapper = $this->index->entityWrapper($item);
+        foreach ($this->options['fields'] as $name => $field) {
+          if ($field['name']) {
+            $required_fields = array();
+            foreach ($field['fields'] as $f) {
+              if (!isset($required_fields[$f])) {
+                $required_fields[$f]['type'] = $types[$field['type']];
+              }
+            }
+            $fields = search_api_extract_fields($wrapper, $required_fields);
+            $values = array();
+            foreach ($fields as $f) {
+              if (isset($f['value'])) {
+                $values[] = $f['value'];
+              }
+            }
+            $values = $this->flattenArray($values);
+
+            $this->reductionType = $field['type'];
+            $item->$name = array_reduce($values, array($this, 'reduce'), NULL);
+            if ($field['type'] == 'count' && !$item->$name) {
+              $item->$name = 0;
+            }
+          }
+        }
+      }
+    }
+  }
+
+  /**
+   * Helper method for reducing an array to a single value.
+   */
+  public function reduce($a, $b) {
+    switch ($this->reductionType) {
+      case 'fulltext':
+        return isset($a) ? $a . "\n\n" . $b : $b;
+      case 'sum':
+        return $a + $b;
+      case 'count':
+        return $a + 1;
+      case 'max':
+        return isset($a) ? max($a, $b) : $b;
+      case 'min':
+        return isset($a) ? min($a, $b) : $b;
+      case 'first':
+        return isset($a) ? $a : $b;
+    }
+  }
+
+  /**
+   * Helper method for flattening a multi-dimensional array.
+   */
+  protected function flattenArray(array $data) {
+    $ret = array();
+    foreach ($data as $item) {
+      if (!isset($item)) {
+        continue;
+      }
+      if (is_scalar($item)) {
+        $ret[] = $item;
+      }
+      else {
+        $ret = array_merge($ret, $this->flattenArray($item));
+      }
+    }
+    return $ret;
+  }
+
+  public function propertyInfo() {
+    $types = $this->getTypes('type');
+    $ret = array();
+    if (isset($this->options['fields'])) {
+      foreach ($this->options['fields'] as $name => $field) {
+        $ret[$name] = array(
+          'label' => $field['name'],
+          'description' => empty($field['description']) ? '' : $field['description'],
+          'type' => $types[$field['type']],
+        );
+      }
+    }
+    return $ret;
+  }
+
+  /**
+   * Helper method for creating a field description.
+   */
+  protected function fieldDescription(array $field, array $index_fields) {
+    $fields = array();
+    foreach ($field['fields'] as $f) {
+      $fields[] = isset($index_fields[$f]) ? $index_fields[$f]['name'] : $f;
+    }
+    $type = $this->getTypes();
+    $type = $type[$field['type']];
+    return t('A @type aggregation of the following fields: @fields.', array('@type' => $type, '@fields' => implode(', ', $fields)));
+  }
+
+  /**
+   * 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".
+   *
+   */
+  protected function getTypes($info = 'name') {
+    switch ($info) {
+      case 'name':
+        return array(
+          'fulltext' => t('Fulltext'),
+          'sum' => t('Sum'),
+          'count' => t('Count'),
+          'max' => t('Maximum'),
+          'min' => t('Minimum'),
+          'first' => t('First'),
+        );
+      case 'type':
+        return array(
+          'fulltext' => 'text',
+          'sum' => 'integer',
+          'count' => 'integer',
+          'max' => 'integer',
+          'min' => 'integer',
+          'first' => 'string',
+        );
+      case 'description':
+        return array(
+          'fulltext' => t('The Fulltext aggregation concatenates the text data of all contained fields.'),
+          'sum' => t('The Sum aggregation adds the values of all contained fields numerically.'),
+          'count' => t('The Count aggregation takes the total number of contained field values as the aggregated field value.'),
+          '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.'),
+        );
+    }
+  }
+
+  /**
+   * Submit helper callback for buttons in the callback's configuration form.
+   */
+  public function formButtonSubmit(array $form, array &$form_state) {
+    $button_name = $form_state['triggering_element']['#name'];
+    if ($button_name == 'op') {
+      for ($i = 1; isset($this->options['fields']['search_api_aggregation_' . $i]); ++$i) {
+      }
+      $this->options['fields']['search_api_aggregation_' . $i] = array(
+        'name' => '',
+        'type' => 'fulltext',
+        'fields' => array(),
+      );
+    }
+    else {
+      $field = substr($button_name, 34);
+      unset($this->options['fields'][$field]);
+    }
+    $form_state['rebuild'] = TRUE;
+    $this->changes = TRUE;
+  }
+
+}
+
+/**
+ * Submit function for buttons in the callback's configuration form.
+ */
+function _search_api_add_aggregation_field_submit(array $form, array &$form_state) {
+  $form_state['callbacks']['search_api_alter_add_aggregation']->formButtonSubmit($form, $form_state);
+}
+
+/**
+ * AJAX submit function for buttons in the callback's configuration form.
+ */
+function _search_api_add_aggregation_field_ajax(array $form, array &$form_state) {
+  return $form['callbacks']['settings']['search_api_alter_add_aggregation']['fields'];
+}

+ 217 - 0
sites/all/modules/contrib/search/search_api/includes/callback_add_hierarchy.inc

@@ -0,0 +1,217 @@
+<?php
+
+/**
+ * @file
+ * Contains SearchApiAlterAddHierarchy.
+ */
+
+/**
+ * Adds all ancestors for hierarchical fields.
+ */
+class SearchApiAlterAddHierarchy extends SearchApiAbstractAlterCallback {
+
+  /**
+   * Cached value for the hierarchical field options.
+   *
+   * @var array
+   *
+   * @see getHierarchicalFields()
+   */
+  protected $field_options;
+
+  /**
+   * Overrides SearchApiAbstractAlterCallback::supportsIndex().
+   *
+   * Returns TRUE only if any hierarchical fields are available.
+   */
+  public function supportsIndex(SearchApiIndex $index) {
+    return (bool) $this->getHierarchicalFields();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function configurationForm() {
+    $options = $this->getHierarchicalFields();
+    $this->options += array('fields' => array());
+    $form['fields'] = array(
+      '#title' => t('Hierarchical fields'),
+      '#description' => t('Select the fields which should be supplemented with their ancestors. ' .
+          'Each field is listed along with its children of the same type. ' .
+          'When selecting several child properties of a field, all those properties will be recursively added to that field. ' .
+          'Please note that you should de-select all fields before disabling this data alteration.'),
+      '#type' => 'select',
+      '#multiple' => TRUE,
+      '#size' => min(6, count($options, COUNT_RECURSIVE)),
+      '#options' => $options,
+      '#default_value' => $this->options['fields'],
+    );
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function configurationFormSubmit(array $form, array &$values, array &$form_state) {
+    // Change the saved type of fields in the index, if necessary.
+    if (!empty($this->index->options['fields'])) {
+      $fields = &$this->index->options['fields'];
+      $previous = drupal_map_assoc($this->options['fields']);
+      foreach ($values['fields'] as $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;
+        }
+      }
+      $new = drupal_map_assoc($values['fields']);
+      foreach ($previous as $field) {
+        list($key) = explode(':', $field);
+        if (empty($new[$field]) && isset($fields[$key]['type'])) {
+          $w = $this->index->entityWrapper(NULL, FALSE);
+          if (isset($w->$key)) {
+            $type = $w->$key->type();
+            $inner = search_api_extract_inner_type($fields[$key]['type']);
+            $fields[$key]['type'] = search_api_nest_type($inner, $type);
+            $change = TRUE;
+          }
+        }
+      }
+      if (isset($change)) {
+        $this->index->save();
+      }
+    }
+
+    return parent::configurationFormSubmit($form, $values, $form_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function alterItems(array &$items) {
+    if (empty($this->options['fields'])) {
+      return;
+    }
+    foreach ($items as $item) {
+      $wrapper = $this->index->entityWrapper($item, FALSE);
+
+      $values = array();
+      foreach ($this->options['fields'] as $field) {
+        list($key, $prop) = explode(':', $field);
+        if (!isset($wrapper->$key)) {
+          continue;
+        }
+        $child = $wrapper->$key;
+
+        $values += array($key => array());
+        $this->extractHierarchy($child, $prop, $values[$key]);
+      }
+      foreach ($values as $key => $value) {
+        $item->$key = $value;
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function propertyInfo() {
+    if (empty($this->options['fields'])) {
+      return array();
+    }
+
+    $ret = array();
+    $wrapper = $this->index->entityWrapper(NULL, FALSE);
+    foreach ($this->options['fields'] as $field) {
+      list($key, $prop) = explode(':', $field);
+      if (!isset($wrapper->$key)) {
+        continue;
+      }
+      $child = $wrapper->$key;
+      while (search_api_is_list_type($child->type())) {
+        $child = $child[0];
+      }
+      if (!isset($child->$prop)) {
+        continue;
+      }
+      if (!isset($ret[$key])) {
+        $ret[$key] = $child->info();
+        $type = search_api_extract_inner_type($ret[$key]['type']);
+        $ret[$key]['type'] = "list<$type>";
+        $ret[$key]['getter callback'] = 'entity_property_verbatim_get';
+        // The return value of info() has some additional internal values set,
+        // which we have to unset for the use here.
+        unset($ret[$key]['name'], $ret[$key]['parent'], $ret[$key]['langcode'], $ret[$key]['clear'],
+            $ret[$key]['property info alter'], $ret[$key]['property defaults']);
+      }
+      if (isset($ret[$key]['bundle'])) {
+        $info = $child->$prop->info();
+        if (empty($info['bundle']) || $ret[$key]['bundle'] != $info['bundle']) {
+          unset($ret[$key]['bundle']);
+        }
+      }
+    }
+    return $ret;
+  }
+
+  /**
+   * Finds all hierarchical fields for the current index.
+   *
+   * @return array
+   *   An array containing all hierarchical fields of the index, structured as
+   *   an options array grouped by primary field.
+   */
+  protected function getHierarchicalFields() {
+    if (!isset($this->field_options)) {
+      $this->field_options = array();
+      $wrapper = $this->index->entityWrapper(NULL, FALSE);
+      // Only entities can be indexed in hierarchies, as other properties don't
+      // have IDs that we can extract and store.
+      $entity_info = entity_get_info();
+      foreach ($wrapper as $key1 => $child) {
+        while (search_api_is_list_type($child->type())) {
+          $child = $child[0];
+        }
+        $info = $child->info();
+        $type = $child->type();
+        if (empty($entity_info[$type])) {
+          continue;
+        }
+        foreach ($child as $key2 => $prop) {
+          if (search_api_extract_inner_type($prop->type()) == $type) {
+            $prop_info = $prop->info();
+            $this->field_options[$info['label']]["$key1:$key2"] = $prop_info['label'];
+          }
+        }
+      }
+    }
+    return $this->field_options;
+  }
+
+  /**
+   * Extracts a hierarchy from a metadata wrapper by modifying $values.
+   */
+  public function extractHierarchy(EntityMetadataWrapper $wrapper, $property, array &$values) {
+    if (search_api_is_list_type($wrapper->type())) {
+      foreach ($wrapper as $w) {
+        $this->extractHierarchy($w, $property, $values);
+      }
+      return;
+    }
+    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.
+    }
+  }
+
+}

+ 34 - 0
sites/all/modules/contrib/search/search_api/includes/callback_add_url.inc

@@ -0,0 +1,34 @@
+<?php
+
+/**
+ * @file
+ * Contains SearchApiAlterAddUrl.
+ */
+
+/**
+ * Search API data alteration callback that adds an URL field for all items.
+ */
+class SearchApiAlterAddUrl extends SearchApiAbstractAlterCallback {
+
+  public function alterItems(array &$items) {
+    foreach ($items as &$item) {
+      $url = $this->index->datasource()->getItemUrl($item);
+      if (!$url) {
+        $item->search_api_url = NULL;
+        continue;
+      }
+      $item->search_api_url = url($url['path'], array('absolute' => TRUE) + $url['options']);
+    }
+  }
+
+  public function propertyInfo() {
+    return array(
+      'search_api_url' => array(
+        'label' => t('URI'),
+        'description' => t('An URI where the item can be accessed.'),
+        'type' => 'uri',
+      ),
+    );
+  }
+
+}

+ 105 - 0
sites/all/modules/contrib/search/search_api/includes/callback_add_viewed_entity.inc

@@ -0,0 +1,105 @@
+<?php
+
+/**
+ * @file
+ * Contains SearchApiAlterAddViewedEntity.
+ */
+
+/**
+ * Search API data alteration callback that adds an URL field for all items.
+ */
+class SearchApiAlterAddViewedEntity extends SearchApiAbstractAlterCallback {
+
+  /**
+   * Only support indexes containing entities.
+   *
+   * @see SearchApiAlterCallbackInterface::supportsIndex()
+   */
+  public function supportsIndex(SearchApiIndex $index) {
+    return (bool) $index->getEntityType();
+  }
+
+  public function configurationForm() {
+    $view_modes = array();
+    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) {
+      $form['mode'] = array(
+        '#type' => 'select',
+        '#title' => t('View mode'),
+        '#options' => $view_modes,
+        '#default_value' => $this->options['mode'],
+      );
+    }
+    else {
+      $form['mode'] = array(
+        '#type' => 'value',
+        '#value' => $this->options['mode'],
+      );
+      if ($view_modes) {
+        $form['note'] = array(
+          '#markup' => '<p>' . t('Entities of type %type have only a single view mode. ' .
+              'Therefore, no selection needs to be made.', array('%type' => $info['label'])) . '</p>',
+        );
+      }
+      else {
+        $form['note'] = array(
+          '#markup' => '<p>' . t('Entities of type %type have no defined view modes. ' .
+              'This might either mean that they are always displayed the same way, or that they cannot be processed by this alteration at all. ' .
+              'Please consider this when using this alteration.', array('%type' => $info['label'])) . '</p>',
+        );
+      }
+    }
+    return $form;
+  }
+
+  public function alterItems(array &$items) {
+    // Prevent session information from being saved while indexing.
+    drupal_save_session(FALSE);
+
+    // Force the current user to anonymous to prevent access bypass in search
+    // indexes.
+    $original_user = $GLOBALS['user'];
+    $GLOBALS['user'] = drupal_anonymous_user();
+
+    $type = $this->index->getEntityType();
+    $mode = empty($this->options['mode']) ? 'full' : $this->options['mode'];
+    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);
+        $text = render($render);
+        if (!$text) {
+          $item->search_api_viewed = NULL;
+          continue;
+        }
+        $item->search_api_viewed = $text;
+      }
+      catch (Exception $e) {
+        $item->search_api_viewed = NULL;
+      }
+    }
+
+    // Restore the user.
+    $GLOBALS['user'] = $original_user;
+    drupal_save_session(TRUE);
+  }
+
+  public function propertyInfo() {
+    return array(
+      'search_api_viewed' => array(
+        'label' => t('Entity HTML output'),
+        'description' => t('The whole HTML content of the entity when viewed.'),
+        'type' => 'text',
+      ),
+    );
+  }
+
+}

+ 90 - 0
sites/all/modules/contrib/search/search_api/includes/callback_bundle_filter.inc

@@ -0,0 +1,90 @@
+<?php
+
+/**
+ * @file
+ * Contains SearchApiAlterBundleFilter.
+ */
+
+/**
+ * Represents a data alteration that restricts entity indexes to some bundles.
+ */
+class SearchApiAlterBundleFilter extends SearchApiAbstractAlterCallback {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function supportsIndex(SearchApiIndex $index) {
+    return $index->getEntityType() && ($info = entity_get_info($index->getEntityType())) && self::hasBundles($info);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function alterItems(array &$items) {
+    $info = entity_get_info($this->index->getEntityType());
+    if (self::hasBundles($info) && isset($this->options['bundles'])) {
+      $bundles = array_flip($this->options['bundles']);
+      $default = (bool) $this->options['default'];
+      $bundle_prop = $info['entity keys']['bundle'];
+      foreach ($items as $id => $item) {
+        if (isset($bundles[$item->$bundle_prop]) == $default) {
+          unset($items[$id]);
+        }
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function configurationForm() {
+    $info = entity_get_info($this->index->getEntityType());
+    if (self::hasBundles($info)) {
+      $options = array();
+      foreach ($info['bundles'] as $bundle => $bundle_info) {
+        $options[$bundle] = isset($bundle_info['label']) ? $bundle_info['label'] : $bundle;
+      }
+      $form = array(
+        'default' => array(
+          '#type' => 'radios',
+          '#title' => t('Which items should be indexed?'),
+          '#default_value' => isset($this->options['default']) ? $this->options['default'] : 1,
+          '#options' => array(
+            1 => t('All but those from one of the selected bundles'),
+            0 => t('Only those from the selected bundles'),
+          ),
+        ),
+        'bundles' => array(
+          '#type' => 'select',
+          '#title' => t('Bundles'),
+          '#default_value' => isset($this->options['bundles']) ? $this->options['bundles'] : array(),
+          '#options' => $options,
+          '#size' => min(4, count($options)),
+          '#multiple' => TRUE,
+        ),
+      );
+    }
+    else {
+      $form = array(
+        'forbidden' => array(
+          '#markup' => '<p>' . t("Items indexed by this index don't have bundles and therefore cannot be filtered here.") . '</p>',
+        ),
+      );
+    }
+    return $form;
+  }
+
+  /**
+   * Determines whether a certain entity type has any bundles.
+   *
+   * @param array $entity_info
+   *   The entity type's entity_get_info() array.
+   *
+   * @return bool
+   *   TRUE if the entity type has bundles, FASLE otherwise.
+   */
+  protected static function hasBundles(array $entity_info) {
+    return !empty($entity_info['entity keys']['bundle']) && !empty($entity_info['bundles']);
+  }
+
+}

+ 46 - 0
sites/all/modules/contrib/search/search_api/includes/callback_comment_access.inc

@@ -0,0 +1,46 @@
+<?php
+/**
+ * @file
+ * Contains the SearchApiAlterCommentAccess class.
+ */
+
+/**
+ * Adds node access information to comment indexes.
+ */
+class SearchApiAlterCommentAccess extends SearchApiAlterNodeAccess {
+
+  /**
+   * Overrides SearchApiAlterNodeAccess::supportsIndex().
+   *
+   * Returns TRUE only for indexes on comments.
+   */
+  public function supportsIndex(SearchApiIndex $index) {
+    return $index->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);
+  }
+
+}

+ 126 - 0
sites/all/modules/contrib/search/search_api/includes/callback_language_control.inc

@@ -0,0 +1,126 @@
+<?php
+
+/**
+ * @file
+ * Contains SearchApiAlterLanguageControl.
+ */
+
+/**
+ * Search API data alteration callback that filters out items based on their
+ * bundle.
+ */
+class SearchApiAlterLanguageControl extends SearchApiAbstractAlterCallback {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(SearchApiIndex $index, array $options = array()) {
+    $options += array(
+      'lang_field' => '',
+      'languages' => array(),
+    );
+    parent::__construct($index, $options);
+  }
+
+  /**
+   * Overrides SearchApiAbstractAlterCallback::supportsIndex().
+   *
+   * Only returns TRUE if the system is multilingual.
+   *
+   * @see drupal_multilingual()
+   */
+  public function supportsIndex(SearchApiIndex $index) {
+    return drupal_multilingual();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function configurationForm() {
+    $form = array();
+
+    $wrapper = $this->index->entityWrapper();
+    $fields[''] = t('- Use default -');
+    foreach ($wrapper as $key => $property) {
+      if ($key == 'search_api_language') {
+        continue;
+      }
+      $type = $property->type();
+      // Only single-valued string properties make sense here. Also, nested
+      // properties probably don't make sense.
+      if ($type == 'text' || $type == 'token') {
+        $info = $property->info();
+        $fields[$key] = $info['label'];
+      }
+    }
+
+    if (count($fields) > 1) {
+      $form['lang_field'] = array(
+        '#type' => 'select',
+        '#title' => t('Language field'),
+        '#description' => t("Select the field which should be used to determine an item's language."),
+        '#options' => $fields,
+        '#default_value' => $this->options['lang_field'],
+      );
+    }
+
+    $languages[LANGUAGE_NONE] = t('Language neutral');
+    $list = language_list('enabled') + array(array(), array());
+    foreach (array($list[1], $list[0]) as $list) {
+      foreach ($list as $lang) {
+        $name = t($lang->name);
+        $native = $lang->native;
+        $languages[$lang->language] = ($name == $native) ? $name : "$name ($native)";
+        if (!$lang->enabled) {
+          $languages[$lang->language] .= ' [' . t('disabled') . ']';
+        }
+      }
+    }
+    $form['languages'] = array(
+      '#type' => 'checkboxes',
+      '#title' => t('Indexed languages'),
+      '#description' => t('Index only items in the selected languages. ' .
+          'When no language is selected, there will be no language-related restrictions.'),
+      '#options' => $languages,
+      '#default_value' => $this->options['languages'],
+    );
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function configurationFormSubmit(array $form, array &$values, array &$form_state) {
+    $values['languages'] = array_filter($values['languages']);
+    return parent::configurationFormSubmit($form, $values, $form_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function alterItems(array &$items) {
+    foreach ($items as $i => &$item) {
+      // Set item language, if a custom field was selected.
+      if ($field = $this->options['lang_field']) {
+        $wrapper = $this->index->entityWrapper($item);
+        if (isset($wrapper->$field)) {
+          try {
+            $item->search_api_language = $wrapper->$field->value();
+          }
+          catch (EntityMetadataWrapperException $e) {
+            // Something went wrong while accessing the language field. Probably
+            // doesn't really matter.
+          }
+        }
+      }
+      // Filter out items according to language, if any were selected.
+      if ($languages = $this->options['languages']) {
+        if (empty($languages[$item->search_api_language])) {
+          unset($items[$i]);
+        }
+      }
+    }
+  }
+
+}

+ 98 - 0
sites/all/modules/contrib/search/search_api/includes/callback_node_access.inc

@@ -0,0 +1,98 @@
+<?php
+/**
+ * @file
+ * Contains the SearchApiAlterNodeAccess class.
+ */
+
+/**
+ * Adds node access information to node indexes.
+ */
+class SearchApiAlterNodeAccess extends SearchApiAbstractAlterCallback {
+
+  /**
+   * Overrides SearchApiAbstractAlterCallback::supportsIndex().
+   *
+   * Returns TRUE only for indexes on nodes.
+   */
+  public function supportsIndex(SearchApiIndex $index) {
+    // Currently only node access is supported.
+    return $index->getEntityType() === 'node';
+  }
+
+  /**
+   * Overrides SearchApiAbstractAlterCallback::propertyInfo().
+   *
+   * Adds the "search_api_access_node" property.
+   */
+  public function propertyInfo() {
+    return array(
+      'search_api_access_node' => array(
+        'label' => t('Node access information'),
+        'description' => t('Data needed to apply node access.'),
+        'type' => 'list<token>',
+      ),
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function alterItems(array &$items) {
+    static $account;
+
+    if (!isset($account)) {
+      // Load the anonymous user.
+      $account = drupal_anonymous_user();
+    }
+
+    foreach ($items as $id => $item) {
+      $node = $this->getNode($item);
+      // Check whether all users have access to the node.
+      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' => $node->nid));
+
+        // Store all grants together with their realms in the item.
+        foreach ($result as $grant) {
+          $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[$id]->search_api_access_node = array('node_access__all');
+      }
+    }
+  }
+
+  /**
+   * 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.
+   */
+  public function configurationFormSubmit(array $form, array &$values, array &$form_state) {
+    $old_status = !empty($form_state['index']->options['data_alter_callbacks']['search_api_alter_node_access']['status']);
+    $new_status = !empty($form_state['values']['callbacks']['search_api_alter_node_access']['status']);
+
+    if (!$old_status && $new_status) {
+      $form_state['index']->options['fields']['status']['type'] = 'boolean';
+      $form_state['index']->options['fields']['author']['type'] = 'integer';
+      $form_state['index']->options['fields']['author']['entity_type'] = 'user';
+    }
+
+    return parent::configurationFormSubmit($form, $values, $form_state);
+  }
+
+}

+ 45 - 0
sites/all/modules/contrib/search/search_api/includes/callback_node_status.inc

@@ -0,0 +1,45 @@
+<?php
+
+/**
+ * @file
+ * Contains the SearchApiAlterNodeStatus class.
+ */
+
+/**
+ * Exclude unpublished nodes from node indexes.
+ */
+class SearchApiAlterNodeStatus extends SearchApiAbstractAlterCallback {
+
+  /**
+   * Check whether this data-alter callback is applicable for a certain index.
+   *
+   * 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) {
+    return $index->getEntityType() === 'node';
+  }
+
+  /**
+   * Alter items before indexing.
+   *
+   * Items which are removed from the array won't be indexed, but will be marked
+   * as clean for future indexing.
+   *
+   * @param array $items
+   *   An array of items to be altered, keyed by item IDs.
+   */
+  public function alterItems(array &$items) {
+    foreach ($items as $nid => &$item) {
+      if (empty($item->status)) {
+        unset($items[$nid]);
+      }
+    }
+  }
+
+}

+ 65 - 0
sites/all/modules/contrib/search/search_api/includes/callback_role_filter.inc

@@ -0,0 +1,65 @@
+<?php
+
+/**
+ * @file
+ * Contains the SearchApiAlterRoleFilter class.
+ */
+
+/**
+ * Data alteration that filters out users based on their role.
+ */
+class SearchApiAlterRoleFilter extends SearchApiAbstractAlterCallback {
+
+  /**
+   * Overrides SearchApiAbstractAlterCallback::supportsIndex().
+   *
+   * This plugin only supports indexes containing users.
+   */
+  public function supportsIndex(SearchApiIndex $index) {
+    return $index->getEntityType() == 'user';
+  }
+
+  /**
+   * Implements SearchApiAlterCallbackInterface::alterItems().
+   */
+  public function alterItems(array &$items) {
+    $roles = $this->options['roles'];
+    $default = (bool) $this->options['default'];
+    foreach ($items as $id => $account) {
+      $role_match = (count(array_diff_key($account->roles, $roles)) !== count($account->roles));
+      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;
+  }
+
+}

+ 686 - 0
sites/all/modules/contrib/search/search_api/includes/datasource.inc

@@ -0,0 +1,686 @@
+<?php
+
+/**
+ * @file
+ * Contains the SearchApiDataSourceControllerInterface as well as a default base class.
+ */
+
+/**
+ * Interface for all data source controllers for Search API indexes.
+ *
+ * Data source controllers encapsulate all operations specific to an item type.
+ * They are used for loading items, extracting item data, keeping track of the
+ * item status, etc.
+ *
+ * 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 {
+
+  /**
+   * Constructs a new data source controller.
+   *
+   * @param string $type
+   *   The item type for which this controller is created.
+   */
+  public function __construct($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();
+
+  /**
+   * Loads items of the type of this data source controller.
+   *
+   * @param array $ids
+   *   The IDs of the items to laod.
+   *
+   * @return array
+   *   The loaded items, keyed by ID.
+   *
+   * @throws SearchApiDataSourceException
+   *   If any error state was encountered.
+   */
+  public function loadItems(array $ids);
+
+  /**
+   * Creates a metadata wrapper for this datasource controller's type.
+   *
+   * @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
+   *   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.
+   *
+   * @throws SearchApiDataSourceException
+   *   If any error state was encountered.
+   *
+   * @see entity_metadata_wrapper()
+   */
+  public function getMetadataWrapper($item = NULL, array $info = array());
+
+  /**
+   * Retrieves the unique ID of an item.
+   *
+   * @param mixed $item
+   *   An item of this controller's type.
+   *
+   * @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);
+
+  /**
+   * Retrieves a human-readable label for an item.
+   *
+   * @param mixed $item
+   *   An item of this controller's type.
+   *
+   * @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);
+
+  /**
+   * Retrieves a URL at which the item can be viewed on the web.
+   *
+   * @param mixed $item
+   *   An item of this controller's type.
+   *
+   * @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);
+
+  /**
+   * 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 SearchApiIndex[] $indexes
+   *   The SearchApiIndex objects for which item tracking should be initialized.
+   *
+   * @throws SearchApiDataSourceException
+   *   If any error state was encountered.
+   */
+  public function startTracking(array $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 SearchApiIndex[] $indexes
+   *   The SearchApiIndex objects for which item tracking should be stopped.
+   *
+   * @throws SearchApiDataSourceException
+   *   If any error state was encountered.
+   */
+  public function stopTracking(array $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 SearchApiIndex[] $indexes
+   *   The indexes for which items should be tracked.
+   *
+   * @throws SearchApiDataSourceException
+   *   If any error state was encountered.
+   */
+  public function trackItemInsert(array $item_ids, array $indexes);
+
+  /**
+   * 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 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 SearchApiIndex[] $indexes
+   *   The indexes for which the change should be tracked.
+   * @param bool $dequeue
+   *   (deprecated) If set to TRUE, also change the status of queued items.
+   *   The concept of queued items will be removed in the Drupal 8 version of
+   *   this module.
+   *
+   * @throws SearchApiDataSourceException
+   *   If any error state was encountered.
+   */
+  public function trackItemChange($item_ids, array $indexes, $dequeue = FALSE);
+
+  /**
+   * 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 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 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);
+
+  /**
+   * Sets the tracking status of the given items to "indexed".
+   *
+   * @param array $item_ids
+   *   The IDs of the indexed items.
+   * @param SearchApiIndex $index
+   *   The index on which the items were indexed.
+   *
+   * @throws SearchApiDataSourceException
+   *   If any error state was encountered.
+   */
+  public function trackItemIndexed(array $item_ids, SearchApiIndex $index);
+
+  /**
+   * Stops tracking the index status for the given items on the given indexes.
+   *
+   * @param array $item_ids
+   *   The IDs of the removed items.
+   * @param SearchApiIndex[] $indexes
+   *   The indexes for which the deletions should be tracked.
+   *
+   * @throws SearchApiDataSourceException
+   *   If any error state was encountered.
+   */
+  public function trackItemDelete(array $item_ids, array $indexes);
+
+  /**
+   * 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
+   * ago should be favored.
+   *
+   * @param SearchApiIndex $index
+   *   The index for which changed items should be returned.
+   * @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);
+
+  /**
+   * Retrieves 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.
+   *
+   * @throws SearchApiDataSourceException
+   *   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();
+
+}
+
+/**
+ * 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
+ * this base class to save time, but can also implement the interface directly.
+ *
+ * A subclass will still have to provide implementations for the following
+ * methods:
+ * - getIdFieldInfo()
+ * - loadItems()
+ * - getMetadataWrapper() or getPropertyInfo()
+ * - startTracking() or getAllItemIds()
+ *
+ * The table used by default for tracking the index status of items is
+ * {search_api_item}. This can easily be changed, for example when an item type
+ * has non-integer IDs, by changing the $table property.
+ */
+abstract class SearchApiAbstractDataSourceController implements SearchApiDataSourceControllerInterface {
+
+  /**
+   * The item type for this controller instance.
+   */
+  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().
+   *
+   * @var array
+   */
+  protected $info;
+
+  /**
+   * The table used for tracking items. Set to NULL on subclasses to disable
+   * the default tracking for an item type, or change the property to use a
+   * different table for tracking.
+   *
+   * @var string
+   */
+  protected $table = 'search_api_item';
+
+  /**
+   * When using the default tracking mechanism: the name of the column on
+   * $this->table containing the item ID.
+   *
+   * @var string
+   */
+  protected $itemIdColumn = 'item_id';
+
+  /**
+   * When using the default tracking mechanism: the name of the column on
+   * $this->table containing the index ID.
+   *
+   * @var string
+   */
+  protected $indexIdColumn = 'index_id';
+
+  /**
+   * When using the default tracking mechanism: the name of the column on
+   * $this->table containing the indexing status.
+   *
+   * @var string
+   */
+  protected $changedColumn = 'changed';
+
+  /**
+   * {@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'];
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getEntityType() {
+    return $this->entityType;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getMetadataWrapper($item = NULL, array $info = array()) {
+    $info += $this->getPropertyInfo();
+    return entity_metadata_wrapper($this->entityType ? $this->entityType : $this->type, $item, $info);
+  }
+
+  /**
+   * 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<integer>',
+   *     ),
+   *   ),
+   * );
+   * @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 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)));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getItemId($item) {
+    $id_info = $this->getIdFieldInfo();
+    $field = $id_info['key'];
+    $wrapper = $this->getMetadataWrapper($item);
+    if (!isset($wrapper->$field)) {
+      return NULL;
+    }
+    $id = $wrapper->$field->value();
+    return $id ? $id : NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getItemLabel($item) {
+    $label = $this->getMetadataWrapper($item)->label();
+    return $label ? $label : NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getItemUrl($item) {
+    return 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);
+    // Insert all items as new.
+    $this->trackItemInsert($this->getAllItemIds(), $indexes);
+  }
+
+  /**
+   * 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)));
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function stopTracking(array $indexes) {
+    if (!$this->table) {
+      return;
+    }
+    // We could also use a single query with "IN" operator, but this method
+    // will mostly be called with only one index.
+    foreach ($indexes as $index) {
+      $this->checkIndex($index);
+      db_delete($this->table)
+        ->condition($this->indexIdColumn, $index->id)
+        ->execute();
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function trackItemInsert(array $item_ids, array $indexes) {
+    if (!$this->table) {
+      return;
+    }
+
+    // Since large amounts of items can overstrain the database, only add items
+    // in chunks.
+    foreach (array_chunk($item_ids, 1000) as $chunk) {
+      $insert = db_insert($this->table)
+        ->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,
+            $this->changedColumn => 1,
+          ));
+        }
+      }
+      $insert->execute();
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function trackItemChange($item_ids, array $indexes, $dequeue = FALSE) {
+    if (!$this->table) {
+      return;
+    }
+    $index_ids = array();
+    foreach ($indexes as $index) {
+      $this->checkIndex($index);
+      $index_ids[] = $index->id;
+    }
+    $update = db_update($this->table)
+      ->fields(array(
+        $this->changedColumn => REQUEST_TIME,
+      ))
+      ->condition($this->indexIdColumn, $index_ids, 'IN')
+      ->condition($this->changedColumn, 0, $dequeue ? '<=' : '=');
+    if ($item_ids !== FALSE) {
+      $update->condition($this->itemIdColumn, $item_ids, 'IN');
+    }
+    $update->execute();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function trackItemQueued($item_ids, SearchApiIndex $index) {
+    $this->checkIndex($index);
+    if (!$this->table) {
+      return;
+    }
+    $update = db_update($this->table)
+      ->fields(array(
+        $this->changedColumn => -1,
+      ))
+      ->condition($this->indexIdColumn, $index->id);
+    if ($item_ids !== FALSE) {
+      $update->condition($this->itemIdColumn, $item_ids, 'IN');
+    }
+    $update->execute();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function trackItemIndexed(array $item_ids, SearchApiIndex $index) {
+    if (!$this->table) {
+      return;
+    }
+    $this->checkIndex($index);
+    db_update($this->table)
+      ->fields(array(
+        $this->changedColumn => 0,
+      ))
+      ->condition($this->itemIdColumn, $item_ids, 'IN')
+      ->condition($this->indexIdColumn, $index->id)
+      ->execute();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function trackItemDelete(array $item_ids, array $indexes) {
+    if (!$this->table) {
+      return;
+    }
+    $index_ids = array();
+    foreach ($indexes as $index) {
+      $this->checkIndex($index);
+      $index_ids[] = $index->id;
+    }
+    db_delete($this->table)
+      ->condition($this->itemIdColumn, $item_ids, 'IN')
+      ->condition($this->indexIdColumn, $index_ids, 'IN')
+      ->execute();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getChangedItems(SearchApiIndex $index, $limit = -1) {
+    if ($limit == 0) {
+      return array();
+    }
+    $this->checkIndex($index);
+    $select = db_select($this->table, 't');
+    $select->addField('t', 'item_id');
+    $select->condition($this->indexIdColumn, $index->id);
+    $select->condition($this->changedColumn, 0, '>');
+    $select->orderBy($this->changedColumn, 'ASC');
+    if ($limit > 0) {
+      $select->range(0, $limit);
+    }
+    return $select->execute()->fetchCol();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getIndexStatus(SearchApiIndex $index) {
+    if (!$this->table) {
+      return array('indexed' => 0, 'total' => 0);
+    }
+    $this->checkIndex($index);
+    $indexed = db_select($this->table, 'i')
+      ->condition($this->indexIdColumn, $index->id)
+      ->condition($this->changedColumn, 0)
+      ->countQuery()
+      ->execute()
+      ->fetchField();
+    $total = db_select($this->table, 'i')
+      ->condition($this->indexIdColumn, $index->id)
+      ->countQuery()
+      ->execute()
+      ->fetchField();
+    return array('indexed' => $indexed, 'total' => $total);
+  }
+
+  /**
+   * 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 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'])
+      );
+      throw new SearchApiDataSourceException($msg);
+    }
+  }
+
+}

+ 144 - 0
sites/all/modules/contrib/search/search_api/includes/datasource_entity.inc

@@ -0,0 +1,144 @@
+<?php
+
+/**
+ * @file
+ * Contains the SearchApiEntityDataSourceController class.
+ */
+
+/**
+ * Represents a datasource for all entities known to the Entity API.
+ */
+class SearchApiEntityDataSourceController extends SearchApiAbstractDataSourceController {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getIdFieldInfo() {
+    $info = entity_get_info($this->entityType);
+    $properties = entity_get_property_info($this->entityType);
+    if (empty($info['entity keys']['id'])) {
+      throw new SearchApiDataSourceException(t("Entity type @type doesn't specify an ID key.", array('@type' => $info['label'])));
+    }
+    $field = $info['entity keys']['id'];
+    if (empty($properties['properties'][$field]['type'])) {
+      throw new SearchApiDataSourceException(t("Entity type @type doesn't specify a type for the @prop property.", array('@type' => $info['label'], '@prop' => $field)));
+    }
+    $type = $properties['properties'][$field]['type'];
+    if (search_api_is_list_type($type)) {
+      throw new SearchApiDataSourceException(t("Entity type @type uses list field @prop as its ID.", array('@type' => $info['label'], '@prop' => $field)));
+    }
+    if ($type == 'token') {
+      $type = 'string';
+    }
+    return array(
+      'key' => $field,
+      'type' => $type,
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function loadItems(array $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);
+      $unknown = array_keys(array_diff_key($ids, $items));
+      if ($unknown) {
+        search_api_track_item_delete($this->type, $unknown);
+      }
+    }
+    return $items;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getMetadataWrapper($item = NULL, array $info = array()) {
+    return entity_metadata_wrapper($this->entityType, $item, $info);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getItemId($item) {
+    $id = entity_id($this->entityType, $item);
+    return $id ? $id : NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getItemLabel($item) {
+    $label = entity_label($this->entityType, $item);
+    return $label ? $label : NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getItemUrl($item) {
+    if ($this->entityType == 'file') {
+      return array(
+        'path' => file_create_url($item->uri),
+        'options' => array(
+          'entity_type' => 'file',
+          'entity' => $item,
+        ),
+      );
+    }
+    $url = entity_uri($this->entityType, $item);
+    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);
+
+    $entity_info = entity_get_info($this->entityType);
+
+    if (!empty($entity_info['base table'])) {
+      // Use a subselect, which will probably be much faster than entity_load().
+
+      // Assumes that all entities use the "base table" property and the
+      // "entity keys[id]" in the same way as the default controller.
+      $id_field = $entity_info['entity keys']['id'];
+      $table = $entity_info['base table'];
+
+      // We could also use a single insert (with a JOIN in the nested query),
+      // but this method will be mostly called with a single index, anyways.
+      foreach ($indexes as $index) {
+        // Select all entity ids.
+        $query = db_select($table, 't');
+        $query->addField('t', $id_field, 'item_id');
+        $query->addExpression(':index_id', 'index_id', array(':index_id' => $index->id));
+        $query->addExpression('1', 'changed');
+
+        // INSERT ... SELECT ...
+        db_insert($this->table)
+          ->from($query)
+          ->execute();
+      }
+    }
+    else {
+      // In the absence of a 'base table', use the slow entity_load().
+      parent::startTracking($indexes);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getAllItemIds() {
+    return array_keys(entity_load($this->entityType));
+  }
+
+}

+ 259 - 0
sites/all/modules/contrib/search/search_api/includes/datasource_external.inc

@@ -0,0 +1,259 @@
+<?php
+
+/**
+ * @file
+ * Contains the SearchApiExternalDataSourceController class.
+ */
+
+/**
+ * Base class for data source controllers for external data sources.
+ *
+ * This data source controller is a base implementation for item types that
+ * represent external data, not directly accessible in Drupal. You can use this
+ * controller as a base class when you don't want to index items of the type via
+ * Drupal, but only want the search capabilities of the Search API. In addition
+ * you most probably also have to create a fitting service class for executing
+ * the actual searches.
+ *
+ * To use most of the functionality of the Search API and related modules, you
+ * 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.
+ */
+class SearchApiExternalDataSourceController extends SearchApiAbstractDataSourceController {
+
+  /**
+   * Return information on the ID field for this controller's type.
+   *
+   * This implementation will return a field named "id" of type "string". This
+   * can also be used if the item type in question has no IDs.
+   *
+   * @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.
+   */
+  public function getIdFieldInfo() {
+    return array(
+      'key' => 'id',
+      'type' => 'string',
+    );
+  }
+
+  /**
+   * Load items of the type of this data source controller.
+   *
+   * Always returns an empty array. If you want the items of your type to be
+   * loadable, specify a function here.
+   *
+   * @param array $ids
+   *   The IDs of the items to laod.
+   *
+   * @return array
+   *   The loaded items, keyed by ID.
+   */
+  public function loadItems(array $ids) {
+    return array();
+  }
+
+  /**
+   * Overrides SearchApiAbstractDataSourceController::getPropertyInfo().
+   *
+   * Only returns a single string ID field.
+   */
+  protected function getPropertyInfo() {
+    $info['property info']['id'] = array(
+      'label' => t('ID'),
+      'type' => 'string',
+    );
+
+    return $info;
+  }
+
+  /**
+   * Get the unique ID of an item.
+   *
+   * Always returns 1.
+   *
+   * @param $item
+   *   An item of this controller's type.
+   *
+   * @return
+   *   Either the unique ID of the item, or NULL if none is available.
+   */
+  public function getItemId($item) {
+    return 1;
+  }
+
+  /**
+   * Get a human-readable label for an item.
+   *
+   * Always returns NULL.
+   *
+   * @param $item
+   *   An item of this controller's type.
+   *
+   * @return
+   *   Either a human-readable label for the item, or NULL if none is available.
+   */
+  public function getItemLabel($item) {
+    return NULL;
+  }
+
+  /**
+   * Get a URL at which the item can be viewed on the web.
+   *
+   * Always returns NULL.
+   *
+   * @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.
+   */
+  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.
+   */
+  public function startTracking(array $indexes) {
+    return;
+  }
+
+  /**
+   * 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.
+   */
+  public function stopTracking(array $indexes) {
+    return;
+  }
+
+  /**
+   * 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.
+   */
+  public function trackItemInsert(array $item_ids, array $indexes) {
+    return;
+  }
+
+  /**
+   * Set the tracking status of the given items to "changed"/"dirty".
+   *
+   * @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.
+   */
+  public function trackItemChange($item_ids, array $indexes, $dequeue = FALSE) {
+    return;
+  }
+
+  /**
+   * 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.
+   */
+  public function trackItemIndexed(array $item_ids, SearchApiIndex $index) {
+    return;
+  }
+
+  /**
+   * 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.
+   */
+  public function trackItemDelete(array $item_ids, array $indexes) {
+    return;
+  }
+
+  /**
+   * 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.
+   */
+  public function getChangedItems(SearchApiIndex $index, $limit = -1) {
+    return array();
+  }
+
+  /**
+   * 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.
+   *
+   * @throws SearchApiDataSourceException
+   *   If the index doesn't use the same item type as this controller.
+   */
+  public function getIndexStatus(SearchApiIndex $index) {
+    return array(
+      'indexed' => 0,
+      'total' => 0,
+    );
+  }
+
+}

+ 34 - 0
sites/all/modules/contrib/search/search_api/includes/exception.inc

@@ -0,0 +1,34 @@
+<?php
+
+/**
+ * @file
+ * Contains SearchApiException.
+ */
+
+/**
+ * Represents an exception or error that occurred in some part of the Search API
+ * framework.
+ */
+class SearchApiException extends Exception {
+
+  /**
+   * Creates a new SearchApiException.
+   *
+   * @param $message
+   *   A string describing the cause of the exception.
+   */
+  public function __construct($message = NULL) {
+    if (!$message) {
+      $message = t('An error occcurred in the Search API framework.');
+    }
+    parent::__construct($message);
+  }
+
+}
+
+/**
+ * Represents an exception that occurred in a data source controller.
+ */
+class SearchApiDataSourceException extends SearchApiException {
+
+}

+ 967 - 0
sites/all/modules/contrib/search/search_api/includes/index_entity.inc

@@ -0,0 +1,967 @@
+<?php
+
+/**
+ * @file
+ * Contains SearchApiIndex.
+ */
+
+/**
+ * Class representing a search index.
+ */
+class SearchApiIndex extends Entity {
+
+  // Cache values, set when the corresponding methods are called for the first
+  // time.
+
+  /**
+   * Cached return value of datasource().
+   *
+   * @var SearchApiDataSourceControllerInterface
+   */
+  protected $datasource = NULL;
+
+  /**
+   * Cached return value of server().
+   *
+   * @var SearchApiServer
+   */
+  protected $server_object = NULL;
+
+  /**
+   * All enabled data alterations for this index.
+   *
+   * @var array
+   */
+  protected $callbacks = NULL;
+
+  /**
+   * All enabled processors for this index.
+   *
+   * @var array
+   */
+  protected $processors = NULL;
+
+  /**
+   * The properties added by data alterations on this index.
+   *
+   * @var array
+   */
+  protected $added_properties = NULL;
+
+  /**
+   * Static cache for the results of getFields().
+   *
+   * Can be accessed as follows: $this->fields[$only_indexed][$get_additional].
+   *
+   * @var array
+   */
+  protected $fields = array();
+
+  /**
+   * An array containing two arrays.
+   *
+   * At index 0, all fulltext fields of this index. At index 1, all indexed
+   * fulltext fields of this index.
+   *
+   * @var array
+   */
+  protected $fulltext_fields = array();
+
+  // Database values that will be set when object is loaded.
+
+  /**
+   * An integer identifying the index.
+   * Immutable.
+   *
+   * @var integer
+   */
+  public $id;
+
+  /**
+   * A name to be displayed for the index.
+   *
+   * @var string
+   */
+  public $name;
+
+  /**
+   * The machine name of the index.
+   * Immutable.
+   *
+   * @var string
+   */
+  public $machine_name;
+
+  /**
+   * A string describing the index' use to users.
+   *
+   * @var string
+   */
+  public $description;
+
+  /**
+   * The machine_name of the server with which data should be indexed.
+   *
+   * @var string
+   */
+  public $server;
+
+  /**
+   * The type of items stored in this index.
+   * Immutable.
+   *
+   * @var string
+   */
+  public $item_type;
+
+  /**
+   * An array of options for configuring this index. The layout is as follows:
+   * - cron_limit: The maximum number of items to be indexed per cron batch.
+   * - index_directly: Boolean setting whether entities are indexed immediately
+   *   after they are created or updated.
+   * - fields: An array of all indexed fields for this index. Keys are the field
+   *   identifiers, the values are arrays for specifying the field settings. The
+   *   structure of those arrays looks like this:
+   *   - type: The type set for this field. One of the types returned by
+   *     search_api_default_field_types().
+   *   - real_type: (optional) If a custom data type was selected for this
+   *     field, this type will be stored here, and "type" contain the fallback
+   *     default data type.
+   *   - boost: (optional) A boost value for terms found in this field during
+   *     searches. Usually only relevant for fulltext fields. Defaults to 1.0.
+   *   - entity_type (optional): If set, the type of this field is really an
+   *     entity. The "type" key will then just contain the primitive data type
+   *     of the ID field, meaning that servers will ignore this and merely index
+   *     the entity's ID. Components displaying this field, though, are advised
+   *     to use the entity label instead of the ID.
+   * - additional fields: An associative array with keys and values being the
+   *   field identifiers of related entities whose fields should be displayed.
+   * - data_alter_callbacks: An array of all data alterations available. Keys
+   *   are the alteration identifiers, the values are arrays containing the
+   *   settings for that data alteration. The inner structure looks like this:
+   *   - status: Boolean indicating whether the data alteration is enabled.
+   *   - weight: Used for sorting the data alterations.
+   *   - settings: Alteration-specific settings, configured via the alteration's
+   *     configuration form.
+   * - processors: An array of all processors available for the index. The keys
+   *   are the processor identifiers, the values are arrays containing the
+   *   settings for that processor. The inner structure looks like this:
+   *   - status: Boolean indicating whether the processor is enabled.
+   *   - weight: Used for sorting the processors.
+   *   - settings: Processor-specific settings, configured via the processor's
+   *     configuration form.
+   *
+   * @var array
+   */
+  public $options = array();
+
+  /**
+   * A flag indicating whether this index is enabled.
+   *
+   * @var integer
+   */
+  public $enabled = 1;
+
+  /**
+   * A flag indicating whether to write to this index.
+   *
+   * @var integer
+   */
+  public $read_only = 0;
+
+  /**
+   * Constructor as a helper to the parent constructor.
+   */
+  public function __construct(array $values = array()) {
+    parent::__construct($values, 'search_api_index');
+  }
+
+  /**
+   * Execute necessary tasks for a newly created index.
+   */
+  public function postCreate() {
+    if ($this->enabled) {
+      $this->queueItems();
+    }
+    if ($server = $this->server()) {
+      // Tell the server about the new index.
+      $server->addIndex($this);
+    }
+  }
+
+  /**
+   * Execute necessary tasks when the index is removed from the database.
+   */
+  public function postDelete() {
+    if ($server = $this->server()) {
+      $server->removeIndex($this);
+    }
+
+    // Stop tracking entities for indexing.
+    $this->dequeueItems();
+  }
+
+  /**
+   * Record entities to index.
+   */
+  public function queueItems() {
+    if (!$this->read_only) {
+      $this->datasource()->startTracking(array($this));
+    }
+  }
+
+  /**
+   * Remove all records of entities to index.
+   */
+  public function dequeueItems() {
+    $this->datasource()->stopTracking(array($this));
+  }
+
+  /**
+   * Saves this index to the database.
+   *
+   * 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.
+   */
+  public function save() {
+    if (empty($this->description)) {
+      $this->description = NULL;
+    }
+    if (empty($this->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;
+      $this->server = NULL;
+    }
+
+    return parent::save();
+  }
+
+  /**
+   * Helper method for updating entity properties.
+   *
+   * NOTE: You shouldn't change any properties of this object before calling
+   * this method, as this might lead to the fields not being saved correctly.
+   *
+   * @param array $fields
+   *   The new field values.
+   *
+   * @return int|false
+   *   SAVE_UPDATED on success, FALSE on failure, 0 if the fields already had
+   *   the specified values.
+   */
+  public function update(array $fields) {
+    $changeable = array('name' => 1, 'enabled' => 1, 'description' => 1, 'server' => 1, 'options' => 1, 'read_only' => 1);
+    $changed = FALSE;
+    foreach ($fields as $field => $value) {
+      if (isset($changeable[$field]) && $value !== $this->$field) {
+        $this->$field = $value;
+        $changed = TRUE;
+      }
+    }
+
+    // If there are no new values, just return 0.
+    if (!$changed) {
+      return 0;
+    }
+
+    // Reset the index's internal property cache to correctly incorporate new
+    // settings.
+    $this->resetCaches();
+
+    return $this->save();
+  }
+
+  /**
+   * Schedules this search index for re-indexing.
+   *
+   * @return bool
+   *   TRUE on success, FALSE on failure.
+   */
+  public function reindex() {
+    if (!$this->server || $this->read_only) {
+      return TRUE;
+    }
+    _search_api_index_reindex($this);
+    module_invoke_all('search_api_index_reindex', $this, FALSE);
+    return TRUE;
+  }
+
+  /**
+   * Clears this search index and schedules all of its items for re-indexing.
+   *
+   * @return bool
+   *   TRUE on success, FALSE on failure.
+   */
+  public function clear() {
+    if (!$this->server || $this->read_only) {
+      return TRUE;
+    }
+
+    $this->server()->deleteItems('all', $this);
+
+    _search_api_index_reindex($this);
+    module_invoke_all('search_api_index_reindex', $this, TRUE);
+    return TRUE;
+  }
+
+  /**
+   * Magic method for determining which fields should be serialized.
+   *
+   * Don't serialize properties that are basically only caches.
+   *
+   * @return array
+   *   An array of properties to be serialized.
+   */
+  public function __sleep() {
+    $ret = get_object_vars($this);
+    unset($ret['server_object'], $ret['datasource'], $ret['processors'], $ret['added_properties'], $ret['fulltext_fields']);
+    return array_keys($ret);
+  }
+
+  /**
+   * Get the controller object of the data source used by this index.
+   *
+   * @throws SearchApiException
+   *   If the specified item type or data source doesn't exist or is invalid.
+   *
+   * @return SearchApiDataSourceControllerInterface
+   *   The data source controller for this index.
+   */
+  public function datasource() {
+    if (!isset($this->datasource)) {
+      $this->datasource = search_api_get_datasource_controller($this->item_type);
+    }
+    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() {
+    return $this->datasource()->getEntityType();
+  }
+
+  /**
+   * Get the server this index lies on.
+   *
+   * @param $reset
+   *   Whether to reset the internal cache. Set to TRUE when the index' $server
+   *   property has just changed.
+   *
+   * @throws SearchApiException
+   *   If $this->server is set, but no server with that machine name exists.
+   *
+   * @return SearchApiServer
+   *   The server associated with this index, or NULL if this index currently
+   *   doesn't lie on a server.
+   */
+  public function server($reset = FALSE) {
+    if (!isset($this->server_object) || $reset) {
+      $this->server_object = $this->server ? search_api_server_load($this->server) : FALSE;
+      if ($this->server && !$this->server_object) {
+        throw new SearchApiException(t('Unknown server @server specified for index @name.', array('@server' => $this->server, '@name' => $this->machine_name)));
+      }
+    }
+    return $this->server_object ? $this->server_object : NULL;
+  }
+
+  /**
+   * Create a query object for this index.
+   *
+   * @param $options
+   *   Associative array of options configuring this query. See
+   *   SearchApiQueryInterface::__construct().
+   *
+   * @throws SearchApiException
+   *   If the index is currently disabled.
+   *
+   * @return SearchApiQueryInterface
+   *   A query object for searching this index.
+   */
+  public function query($options = array()) {
+    if (!$this->enabled) {
+      throw new SearchApiException(t('Cannot search on a disabled index.'));
+    }
+    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.
+   *
+   * @param array $items
+   *   An array of items to index.
+   *
+   * @return array
+   *   An array of the IDs of all items that should be marked as indexed.
+   */
+  public function index(array $items) {
+    if ($this->read_only) {
+      return array();
+    }
+    if (!$this->enabled) {
+      throw new SearchApiException(t("Couldn't index values on '@name' index (index is disabled)", array('@name' => $this->name)));
+    }
+    if (empty($this->options['fields'])) {
+      throw new SearchApiException(t("Couldn't index values on '@name' index (no fields selected)", array('@name' => $this->name)));
+    }
+    $fields = $this->options['fields'];
+    $custom_type_fields = array();
+    foreach ($fields as $field => $info) {
+      if (isset($info['real_type'])) {
+        $custom_type = search_api_extract_inner_type($info['real_type']);
+        if ($this->server()->supportsFeature('search_api_data_type_' . $custom_type)) {
+          $fields[$field]['type'] = $info['real_type'];
+          $custom_type_fields[$custom_type][$field] = search_api_list_nesting_level($info['real_type']);
+        }
+      }
+    }
+    if (empty($fields)) {
+      throw new SearchApiException(t("Couldn't index values on '@name' index (no fields selected)", array('@name' => $this->name)));
+    }
+
+    // Mark all items that are rejected as indexed.
+    $ret = array_keys($items);
+    drupal_alter('search_api_index_items', $items, $this);
+    if ($items) {
+      $this->dataAlter($items);
+    }
+    $ret = array_diff($ret, array_keys($items));
+
+    // Items that are rejected should also be deleted from the server.
+    if ($ret) {
+      $this->server()->deleteItems($ret, $this);
+    }
+    if (!$items) {
+      return $ret;
+    }
+
+    $data = array();
+    foreach ($items as $id => $item) {
+      $data[$id] = search_api_extract_fields($this->entityWrapper($item), $fields);
+      unset($items[$id]);
+      foreach ($custom_type_fields as $type => $type_fields) {
+        $info = search_api_get_data_type_info($type);
+        if (isset($info['conversion callback']) && is_callable($info['conversion callback'])) {
+          $callback = $info['conversion callback'];
+          foreach ($type_fields as $field => $nesting_level) {
+            if (isset($data[$id][$field]['value'])) {
+              $value = $data[$id][$field]['value'];
+              $original_type = $data[$id][$field]['original_type'];
+              $data[$id][$field]['value'] = _search_api_convert_custom_type($callback, $value, $original_type, $type, $nesting_level);
+            }
+          }
+        }
+      }
+    }
+
+    $this->preprocessIndexItems($data);
+
+    return array_merge($ret, $this->server()->indexItems($this, $data));
+  }
+
+  /**
+   * Calls data alteration hooks for a set of items, according to the index
+   * options.
+   *
+   * @param array $items
+   *   An array of items to be altered.
+   *
+   * @return SearchApiIndex
+   *   The called object.
+   */
+  public function dataAlter(array &$items) {
+    // First, execute our own search_api_language data alteration.
+    foreach ($items as &$item) {
+      $item->search_api_language = isset($item->language) ? $item->language : LANGUAGE_NONE;
+    }
+
+    foreach ($this->getAlterCallbacks() as $callback) {
+      $callback->alterItems($items);
+    }
+
+    return $this;
+  }
+
+  /**
+   * Property info alter callback that adds the infos of the properties added by
+   * data alter callbacks.
+   *
+   * @param EntityMetadataWrapper $wrapper
+   *   The wrapped data.
+   * @param $property_info
+   *   The original property info.
+   *
+   * @return array
+   *   The altered property info.
+   */
+  public function propertyInfoAlter(EntityMetadataWrapper $wrapper, array $property_info) {
+    if (entity_get_property_info($wrapper->type())) {
+      // Overwrite the existing properties with the list of properties including
+      // all fields regardless of the used bundle.
+      $property_info['properties'] = entity_get_all_property_info($wrapper->type());
+    }
+
+    if (!isset($this->added_properties)) {
+      $this->added_properties = array(
+        'search_api_language' => array(
+          'label' => t('Item language'),
+          'description' => t("A field added by the search framework to let components determine an item's language. Is always indexed."),
+          'type' => 'token',
+          '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.
+      foreach (array_reverse($this->getAlterCallbacks()) as $callback) {
+        $props = $callback->propertyInfo();
+        if ($props) {
+          $this->added_properties += $props;
+        }
+      }
+    }
+    // Let fields added by data-alter callbacks override default fields.
+    $property_info['properties'] = array_merge($property_info['properties'], $this->added_properties);
+
+    return $property_info;
+  }
+
+  /**
+   * Loads all enabled data alterations for this index in proper order.
+   *
+   * @return array
+   *   All enabled callbacks for this index, as SearchApiAlterCallbackInterface
+   *   objects.
+   */
+  public function getAlterCallbacks() {
+    if (isset($this->callbacks)) {
+      return $this->callbacks;
+    }
+
+    $this->callbacks = array();
+    if (empty($this->options['data_alter_callbacks'])) {
+      return $this->callbacks;
+    }
+    $callback_settings = $this->options['data_alter_callbacks'];
+    $infos = search_api_get_alter_callbacks();
+
+    foreach ($callback_settings as $id => $settings) {
+      if (empty($settings['status'])) {
+        continue;
+      }
+      if (empty($infos[$id]) || !class_exists($infos[$id]['class'])) {
+        watchdog('search_api', t('Undefined data alteration @class specified in index @name', array('@class' => $id, '@name' => $this->name)), NULL, WATCHDOG_WARNING);
+        continue;
+      }
+      $class = $infos[$id]['class'];
+      $callback = new $class($this, empty($settings['settings']) ? array() : $settings['settings']);
+      if (!($callback instanceof SearchApiAlterCallbackInterface)) {
+        watchdog('search_api', t('Unknown callback class @class specified for data alteration @name', array('@class' => $class, '@name' => $id)), NULL, WATCHDOG_WARNING);
+        continue;
+      }
+
+      $this->callbacks[$id] = $callback;
+    }
+    return $this->callbacks;
+  }
+
+  /**
+   * Loads all enabled processors for this index in proper order.
+   *
+   * @return array
+   *   All enabled processors for this index, as SearchApiProcessorInterface
+   *   objects.
+   */
+  public function getProcessors() {
+    if (isset($this->processors)) {
+      return $this->processors;
+    }
+
+    $this->processors = array();
+    if (empty($this->options['processors'])) {
+      return $this->processors;
+    }
+    $processor_settings = $this->options['processors'];
+    $infos = search_api_get_processors();
+
+    foreach ($processor_settings as $id => $settings) {
+      if (empty($settings['status'])) {
+        continue;
+      }
+      if (empty($infos[$id]) || !class_exists($infos[$id]['class'])) {
+        watchdog('search_api', t('Undefined processor @class specified in index @name', array('@class' => $id, '@name' => $this->name)), NULL, WATCHDOG_WARNING);
+        continue;
+      }
+      $class = $infos[$id]['class'];
+      $processor = new $class($this, isset($settings['settings']) ? $settings['settings'] : array());
+      if (!($processor instanceof SearchApiProcessorInterface)) {
+        watchdog('search_api', t('Unknown processor class @class specified for processor @name', array('@class' => $class, '@name' => $id)), NULL, WATCHDOG_WARNING);
+        continue;
+      }
+
+      $this->processors[$id] = $processor;
+    }
+    return $this->processors;
+  }
+
+  /**
+   * Preprocess data items for indexing. Data added by data alter callbacks will
+   * be available on the items.
+   *
+   * Typically, a preprocessor will execute its preprocessing (e.g. stemming,
+   * n-grams, word splitting, stripping stop words, etc.) only on the items'
+   * fulltext fields. Other fields should usually be left untouched.
+   *
+   * @param array $items
+   *   An array of items to be preprocessed for indexing.
+   *
+   * @return SearchApiIndex
+   *   The called object.
+   */
+  public function preprocessIndexItems(array &$items) {
+    foreach ($this->getProcessors() as $processor) {
+      $processor->preprocessIndexItems($items);
+    }
+    return $this;
+  }
+
+
+  /**
+   * Preprocess a search query.
+   *
+   * The same applies as when preprocessing indexed items: typically, only the
+   * fulltext search keys should be processed, queries on specific fields should
+   * usually not be altered.
+   *
+   * @param SearchApiQuery $query
+   *   The object representing the query to be executed.
+   *
+   * @return SearchApiIndex
+   *   The called object.
+   */
+  public function preprocessSearchQuery(SearchApiQuery $query) {
+    foreach ($this->getProcessors() as $processor) {
+      $processor->preprocessSearchQuery($query);
+    }
+    return $this;
+  }
+
+  /**
+   * Postprocess search results before display.
+   *
+   * If a class is used for both pre- and post-processing a search query, the
+   * same object will be used for both calls (so preserving some data or state
+   * locally is possible).
+   *
+   * @param array $response
+   *   An array containing the search results. See
+   *   SearchApiServiceInterface->search() for the detailed format.
+   * @param SearchApiQuery $query
+   *   The object representing the executed query.
+   *
+   * @return SearchApiIndex
+   *   The called object.
+   */
+  public function postprocessSearchResults(array &$response, SearchApiQuery $query) {
+    // Postprocessing is done in exactly the opposite direction than preprocessing.
+    foreach (array_reverse($this->getProcessors()) as $processor) {
+      $processor->postprocessSearchResults($response, $query);
+    }
+    return $this;
+  }
+
+  /**
+   * Returns a list of all known fields for this index.
+   *
+   * @param $only_indexed (optional)
+   *   Return only indexed fields, not all known fields. Defaults to TRUE.
+   * @param $get_additional (optional)
+   *   Return not only known/indexed fields, but also related entities whose
+   *   fields could additionally be added to the index.
+   *
+   * @return array
+   *   An array of all known fields for this index. Keys are the field
+   *   identifiers, the values are arrays for specifying the field settings. The
+   *   structure of those arrays looks like this:
+   *   - name: The human-readable name for the field.
+   *   - description: A description of the field, if available.
+   *   - indexed: Boolean indicating whether the field is indexed or not.
+   *   - type: The type set for this field. One of the types returned by
+   *     search_api_default_field_types().
+   *   - real_type: (optional) If a custom data type was selected for this
+   *     field, this type will be stored here, and "type" contain the fallback
+   *     default data type.
+   *   - boost: A boost value for terms found in this field during searches.
+   *     Usually only relevant for fulltext fields.
+   *   - entity_type (optional): If set, the type of this field is really an
+   *     entity. The "type" key will then contain "integer", meaning that
+   *     servers will ignore this and merely index the entity's ID. Components
+   *     displaying this field, though, are advised to use the entity label
+   *     instead of the ID.
+   *   If $get_additional is TRUE, this array is encapsulated in another
+   *   associative array, which contains the above array under the "fields" key,
+   *   and a list of related entities (field keys mapped to names) under the
+   *   "additional fields" key.
+   */
+  public function getFields($only_indexed = TRUE, $get_additional = FALSE) {
+    $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";
+      $cache = cache_get($cid);
+      if ($cache) {
+        $this->fields[$only_indexed][$get_additional] = $cache->data;
+      }
+    }
+
+    // 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();
+
+      // 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;
+        }
+      }
+
+      // 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]);
+    }
+
+    return $this->fields[$only_indexed][$get_additional];
+  }
+
+  /**
+   * Convenience method for getting all of this index's fulltext fields.
+   *
+   * @param boolean $only_indexed
+   *   If set to TRUE, only the indexed fulltext fields will be returned.
+   *
+   * @return array
+   *   An array containing all (or all indexed) fulltext fields defined for this
+   *   index.
+   */
+  public function getFulltextFields($only_indexed = TRUE) {
+    $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);
+      foreach ($fields as $key => $field) {
+        if (search_api_is_text_type($field['type'])) {
+          $this->fulltext_fields[$i][] = $key;
+        }
+      }
+    }
+    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.
+   *
+   * @param $item
+   *   Unless NULL, an item of this index's item type which should be wrapped.
+   * @param $alter
+   *   Whether to apply the index's active data alterations on the property
+   *   information used. To also apply the data alteration to the wrapped item,
+   *   execute SearchApiIndex::dataAlter() on it before calling this method.
+   *
+   * @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.
+   */
+  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);
+  }
+
+  /**
+   * Helper method to load items from the type lying on this index.
+   *
+   * @param array $ids
+   *   The IDs of the items to load.
+   *
+   * @return array
+   *   The requested items, as loaded by the data source.
+   *
+   * @see SearchApiDataSourceControllerInterface::loadItems()
+   */
+  public function loadItems(array $ids) {
+    return $this->datasource()->loadItems($ids);
+  }
+
+  /**
+   * Reset internal static caches.
+   *
+   * Should be used when things like fields or data alterations change to avoid
+   * using stale data.
+   */
+  public function resetCaches() {
+    $this->datasource = NULL;
+    $this->server_object = NULL;
+    $this->callbacks = NULL;
+    $this->processors = NULL;
+    $this->added_properties = NULL;
+    $this->fields = array();
+    $this->fulltext_fields = array();
+  }
+
+}

+ 465 - 0
sites/all/modules/contrib/search/search_api/includes/processor.inc

@@ -0,0 +1,465 @@
+<?php
+
+/**
+ * @file
+ * Contains SearchApiProcessorInterface and SearchApiAbstractProcessor.
+ */
+
+/**
+ * Interface representing a Search API pre- and/or post-processor.
+ *
+ * While processors are enabled or disabled for both pre- and postprocessing at
+ * once, many processors will only need to run in one of those two phases. Then,
+ * the other method(s) should simply be left blank. A processor should make it
+ * clear in its description or documentation when it will run and what effect it
+ * will have.
+ * Usually, processors preprocessing indexed items will likewise preprocess
+ * search queries, so these two methods should mostly be implemented either both
+ * or neither.
+ */
+interface SearchApiProcessorInterface {
+
+  /**
+   * Construct a processor.
+   *
+   * @param SearchApiIndex $index
+   *   The index for which processing is done.
+   * @param array $options
+   *   The processor options set for this index.
+   */
+  public function __construct(SearchApiIndex $index, array $options = array());
+
+  /**
+   * Check whether this processor is applicable for a certain index.
+   *
+   * This can be used for hiding the processor on the index's "Filters" tab. To
+   * avoid confusion, you should only use criteria that are immutable, such as
+   * the index's item type. Also, since this is only used for UI purposes, you
+   * should not completely rely on this to ensure certain index configurations
+   * and at least throw an exception with a descriptive error message if this is
+   * violated on runtime.
+   *
+   * @param SearchApiIndex $index
+   *   The index to check for.
+   *
+   * @return boolean
+   *   TRUE if the processor can run on the given index; FALSE otherwise.
+   */
+  public function supportsIndex(SearchApiIndex $index);
+
+  /**
+   * Display a form for configuring this processor.
+   * Since forcing users to specify options for disabled processors makes no
+   * sense, none of the form elements should have the '#required' attribute set.
+   *
+   * @return array
+   *   A form array for configuring this processor, or FALSE if no configuration
+   *   is possible.
+   */
+  public function configurationForm();
+
+  /**
+   * 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.
+   */
+  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.
+   */
+  public function configurationFormSubmit(array $form, array &$values, array &$form_state);
+
+  /**
+   * Preprocess data items for indexing.
+   *
+   * Typically, a preprocessor will execute its preprocessing (e.g. stemming,
+   * n-grams, word splitting, stripping stop words, etc.) only on the items'
+   * search_api_fulltext fields, if set. Other fields should usually be left
+   * untouched.
+   *
+   * @param array $items
+   *   An array of items to be preprocessed for indexing, formatted as specified
+   *   by SearchApiServiceInterface::indexItems().
+   */
+  public function preprocessIndexItems(array &$items);
+
+  /**
+   * Preprocess a search query.
+   *
+   * The same applies as when preprocessing indexed items: typically, only the
+   * fulltext search keys should be processed, queries on specific fields should
+   * usually not be altered.
+   *
+   * @param SearchApiQuery $query
+   *   The object representing the query to be executed.
+   */
+  public function preprocessSearchQuery(SearchApiQuery $query);
+
+  /**
+   * Postprocess search results before display.
+   *
+   * If a class is used for both pre- and post-processing a search query, the
+   * same object will be used for both calls (so preserving some data or state
+   * locally is possible).
+   *
+   * @param array $response
+   *   An array containing the search results. See the return value of
+   *   SearchApiQueryInterface->execute() for the detailed format.
+   * @param SearchApiQuery $query
+   *   The object representing the executed query.
+   */
+  public function postprocessSearchResults(array &$response, SearchApiQuery $query);
+
+}
+
+/**
+ * Abstract processor implementation that provides an easy framework for only
+ * processing specific fields.
+ *
+ * Simple processors can just override process(), while others might want to
+ * override the other process*() methods, and test*() (for restricting
+ * processing to something other than all fulltext data).
+ */
+abstract class SearchApiAbstractProcessor implements SearchApiProcessorInterface {
+
+  /**
+   * @var SearchApiIndex
+   */
+  protected $index;
+
+  /**
+   * @var array
+   */
+  protected $options;
+
+  /**
+   * Constructor, saving its arguments into properties.
+   */
+  public function __construct(SearchApiIndex $index, array $options = array()) {
+    $this->index   = $index;
+    $this->options = $options;
+  }
+
+  public function supportsIndex(SearchApiIndex $index) {
+    return TRUE;
+  }
+
+  public function configurationForm() {
+    $form['#attached']['css'][] = drupal_get_path('module', 'search_api') . '/search_api.admin.css';
+
+    $fields = $this->index->getFields();
+    $field_options = array();
+    $default_fields = array();
+    if (isset($this->options['fields'])) {
+      $default_fields = drupal_map_assoc(array_keys($this->options['fields']));
+    }
+    foreach ($fields as $name => $field) {
+      $field_options[$name] = $field['name'];
+      if (!empty($default_fields[$name]) || (!isset($this->options['fields']) && $this->testField($name, $field))) {
+        $default_fields[$name] = $name;
+      }
+    }
+
+    $form['fields'] = array(
+      '#type' => 'checkboxes',
+      '#title' => t('Fields to run on'),
+      '#options' => $field_options,
+      '#default_value' => $default_fields,
+      '#attributes' => array('class' => array('search-api-checkboxes-list')),
+    );
+
+    return $form;
+  }
+
+  public function configurationFormValidate(array $form, array &$values, array &$form_state) {
+    $fields = array_filter($values['fields']);
+    if ($fields) {
+      $fields = array_fill_keys($fields, TRUE);
+    }
+    $values['fields'] = $fields;
+  }
+
+  public function configurationFormSubmit(array $form, array &$values, array &$form_state) {
+    $this->options = $values;
+    return $values;
+  }
+
+  /**
+   * Calls processField() for all appropriate fields.
+   */
+  public function preprocessIndexItems(array &$items) {
+    foreach ($items as &$item) {
+      foreach ($item as $name => &$field) {
+        if ($this->testField($name, $field)) {
+          $this->processField($field['value'], $field['type']);
+        }
+      }
+    }
+  }
+
+  /**
+   * Calls processKeys() for the keys and processFilters() for the filters.
+   */
+  public function preprocessSearchQuery(SearchApiQuery $query) {
+    $keys = &$query->getKeys();
+    $this->processKeys($keys);
+    $filter = $query->getFilter();
+    $filters = &$filter->getFilters();
+    $this->processFilters($filters);
+  }
+
+  /**
+   * Does nothing.
+   */
+  public function postprocessSearchResults(array &$response, SearchApiQuery $query) {
+    return;
+  }
+
+  /**
+   * Method for preprocessing field data.
+   *
+   * Calls process() either for the whole text, or each token, depending on the
+   * type. Also takes care of extracting list values and of fusing returned
+   * tokens back into a one-dimensional array.
+   */
+  protected function processField(&$value, &$type) {
+    if (!isset($value) || $value === '') {
+      return;
+    }
+    if (substr($type, 0, 5) == 'list<') {
+      $inner_type = $t = $t1 = substr($type, 5, -1);
+      foreach ($value as &$v) {
+        $t1 = $inner_type;
+        $this->processField($v, $t1);
+        // If one value got tokenized, all others have to follow.
+        if ($t1 != $inner_type) {
+          $t = $t1;
+        }
+      }
+      if ($t == 'tokens') {
+        foreach ($value as $i => &$v) {
+          if (!$v) {
+            unset($value[$i]);
+            continue;
+          }
+          if (!is_array($v)) {
+            $v = array(array('value' => $v, 'score' => 1));
+          }
+        }
+      }
+      $type = "list<$t>";
+      return;
+    }
+    if ($type == 'tokens') {
+      foreach ($value as &$token) {
+        $this->processFieldValue($token['value']);
+      }
+    }
+    else {
+      $this->processFieldValue($value);
+    }
+    if (is_array($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);
+      }
+    }
+  }
+
+  /**
+   * Internal helper function for normalizing tokens.
+   */
+  protected function normalizeTokens($tokens, $score = 1) {
+    $ret = array();
+    foreach ($tokens as $token) {
+      if (empty($token['value']) && !is_numeric($token['value'])) {
+        // Filter out empty tokens.
+        continue;
+      }
+      if (!isset($token['score'])) {
+        $token['score'] = $score;
+      }
+      else {
+        $token['score'] *= $score;
+      }
+      if (is_array($token['value'])) {
+        foreach ($this->normalizeTokens($token['value'], $token['score']) as $t) {
+          $ret[] = $t;
+        }
+      }
+      else {
+        $ret[] = $token;
+      }
+    }
+    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.
+   */
+  protected function processKeys(&$keys) {
+    if (is_array($keys)) {
+      foreach ($keys as $key => &$v) {
+        if (element_child($key)) {
+          $this->processKeys($v);
+          if (!$v && !is_numeric($v)) {
+            unset($keys[$key]);
+          }
+        }
+      }
+    }
+    else {
+      $this->processKey($keys);
+    }
+  }
+
+  /**
+   * Method for preprocessing query filters.
+   */
+  protected function processFilters(array &$filters) {
+    $fields = $this->index->options['fields'];
+    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 {
+        $child_filters = &$f->getFilters();
+        $this->processFilters($child_filters);
+      }
+    }
+  }
+
+  /**
+   * @param $name
+   *   The field's machine name.
+   * @param array $field
+   *   The field's information.
+   *
+   * @return
+   *   TRUE, iff the field should be processed.
+   */
+  protected function testField($name, array $field) {
+    if (empty($this->options['fields'])) {
+      return $this->testType($field['type']);
+    }
+    return !empty($this->options['fields'][$name]);
+  }
+
+  /**
+   * @return
+   *   TRUE, iff the type should be processed.
+   */
+  protected function testType($type) {
+    return search_api_is_text_type($type, array('text', 'tokens'));
+  }
+
+  /**
+   * Called for processing a single text element in a field. The default
+   * implementation just calls process().
+   *
+   * $value can either be left a string, or changed into an array of tokens. A
+   * token is an associative array containing:
+   * - value: Either the text inside the token, or a nested array of tokens. The
+   *   score of nested tokens will be multiplied by their parent's score.
+   * - score: The relative importance of the token, as a float, with 1 being
+   *   the default.
+   */
+  protected function processFieldValue(&$value) {
+    $this->process($value);
+  }
+
+  /**
+   * Called for processing a single search keyword. The default implementation
+   * just calls process().
+   *
+   * $value can either be left a string, or be changed into a nested keys array,
+   * as defined by SearchApiQueryInterface.
+   */
+  protected function processKey(&$value) {
+    $this->process($value);
+  }
+
+  /**
+   * Called for processing a single filter value. The default implementation
+   * just calls process().
+   *
+   * $value has to remain a string.
+   */
+  protected function processFilterValue(&$value) {
+    $this->process($value);
+  }
+
+  /**
+   * Function that is ultimately called for all text by the standard
+   * implementation, and does nothing by default.
+   *
+   * @param $value
+   *   The value to preprocess as a string. Can be manipulated directly, nothing
+   *   has to be returned. Since this can be called for all value types, $value
+   *   has to remain a string.
+   */
+  protected function process(&$value) {
+
+  }
+
+}

+ 403 - 0
sites/all/modules/contrib/search/search_api/includes/processor_highlight.inc

@@ -0,0 +1,403 @@
+<?php
+
+/**
+ * @file
+ * Contains the SearchApiHighlight class.
+ */
+
+/**
+ * Processor for highlighting search results.
+ */
+class SearchApiHighlight extends SearchApiAbstractProcessor {
+
+  /**
+   * PREG regular expression for a word boundary.
+   *
+   * We highlight around non-indexable or CJK characters.
+   *
+   * @var string
+   */
+  protected static $boundary;
+
+  /**
+   * PREG regular expression for splitting words.
+   *
+   * We highlight around non-indexable or CJK characters.
+   *
+   * @var string
+   */
+  protected static $split;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(SearchApiIndex $index, array $options = array()) {
+    parent::__construct($index, $options);
+
+    $cjk = '\x{1100}-\x{11FF}\x{3040}-\x{309F}\x{30A1}-\x{318E}' .
+        '\x{31A0}-\x{31B7}\x{31F0}-\x{31FF}\x{3400}-\x{4DBF}\x{4E00}-\x{9FCF}' .
+        '\x{A000}-\x{A48F}\x{A4D0}-\x{A4FD}\x{A960}-\x{A97F}\x{AC00}-\x{D7FF}' .
+        '\x{F900}-\x{FAFF}\x{FF21}-\x{FF3A}\x{FF41}-\x{FF5A}\x{FF66}-\x{FFDC}' .
+        '\x{20000}-\x{2FFFD}\x{30000}-\x{3FFFD}';
+    self::$boundary = '(?:(?<=[' . PREG_CLASS_UNICODE_WORD_BOUNDARY . $cjk . '])|(?=[' . PREG_CLASS_UNICODE_WORD_BOUNDARY . $cjk . ']))';
+    self::$split = '/[' . PREG_CLASS_UNICODE_WORD_BOUNDARY . $cjk . ']+/iu';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function configurationForm() {
+    $this->options += array(
+      'prefix' => '<strong>',
+      'suffix' => '</strong>',
+      'excerpt' => TRUE,
+      'excerpt_length' => 256,
+      'highlight' => 'always',
+    );
+
+    $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,
+          ),
+        ),
+      ),
+    );
+    $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'],
+    );
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function configurationFormValidate(array $form, array &$values, array &$form_state) {
+    // Overridden so $form['fields'] is not checked.
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function postprocessSearchResults(array &$response, SearchApiQuery $query) {
+    if (!$response['result count'] || !($keys = $this->getKeywords($query))) {
+      return;
+    }
+
+    foreach ($response['results'] as $id => &$result) {
+      if ($this->options['excerpt']) {
+        $text = array();
+        $fields = $this->getFulltextFields($response['results'], $id);
+        foreach ($fields as $data) {
+          if (is_array($data)) {
+            $text = array_merge($text, $data);
+          }
+          else {
+            $text[] = $data;
+          }
+        }
+        $result['excerpt'] = $this->createExcerpt(implode("\n\n", $text), $keys);
+      }
+      if ($this->options['highlight'] != 'never') {
+        $fields = $this->getFulltextFields($response['results'], $id, $this->options['highlight'] == 'always');
+        foreach ($fields as $field => $data) {
+          if (is_array($data)) {
+            foreach ($data as $i => $text) {
+              $result['fields'][$field][$i] = $this->highlightField($text, $keys);
+            }
+          }
+          else {
+            $result['fields'][$field] = $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 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, $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());
+    $fulltext_fields = $this->index->getFulltextFields();
+    // We only need detailed fields data if $load is TRUE.
+    $fields = $load ? $this->index->getFields() : array();
+    $needs_extraction = array();
+    foreach ($fulltext_fields as $field) {
+      if (array_key_exists($field, $result['fields'])) {
+        $data[$field] = $result['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);
+
+    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
+   *   A string containing HTML for the excerpt.
+   */
+  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);
+
+    // Extract fragments around keywords.
+    // First we collect ranges of text around each keyword, starting/ending
+    // at spaces, trying to get to the requested length.
+    // If the sum of all fragments is too short, we look for second occurrences.
+    $ranges = array();
+    $included = array();
+    $length = 0;
+    $workkeys = $keys;
+    while ($length < $this->options['excerpt_length'] && count($workkeys)) {
+      foreach ($workkeys as $k => $key) {
+        if ($length >= $this->options['excerpt_length']) {
+          break;
+        }
+        // Remember occurrence of key so we can skip over it if more occurrences
+        // are desired.
+        if (!isset($included[$key])) {
+          $included[$key] = 0;
+        }
+        // Locate a keyword (position $p, always >0 because $text starts with a
+        // space).
+        $p = 0;
+        if (preg_match('/' . self::$boundary . $key . self::$boundary . '/iu', $text, $match, PREG_OFFSET_CAPTURE, $included[$key])) {
+          $p = $match[0][1];
+        }
+        // Now locate a space in front (position $q) and behind it (position $s),
+        // leaving about 60 characters extra before and after for context.
+        // Note that a space was added to the front and end of $text above.
+        if ($p) {
+          if (($q = strpos(' ' . $text, ' ', max(0, $p - 61))) !== FALSE) {
+            $end = substr($text . ' ', $p, 80);
+            if (($s = strrpos($end, ' ')) !== FALSE) {
+              // Account for the added spaces.
+              $q = max($q - 1, 0);
+              $s = min($s, strlen($end) - 1);
+              $ranges[$q] = $p + $s;
+              $length += $p + $s - $q;
+              $included[$key] = $p + 1;
+            }
+            else {
+              unset($workkeys[$k]);
+            }
+          }
+          else {
+            unset($workkeys[$k]);
+          }
+        }
+        else {
+          unset($workkeys[$k]);
+        }
+      }
+    }
+
+    if (count($ranges) == 0) {
+      // We didn't find any keyword matches, so just return NULL.
+      return NULL;
+    }
+
+    // Sort the text ranges by starting position.
+    ksort($ranges);
+
+    // Now we collapse overlapping text ranges into one. The sorting makes it O(n).
+    $newranges = array();
+    foreach ($ranges as $from2 => $to2) {
+      if (!isset($from1)) {
+        $from1 = $from2;
+        $to1 = $to2;
+        continue;
+      }
+      if ($from2 <= $to1) {
+        $to1 = max($to1, $to2);
+      }
+      else {
+        $newranges[$from1] = $to1;
+        $from1 = $from2;
+        $to1 = $to2;
+      }
+    }
+    $newranges[$from1] = $to1;
+
+    // Fetch text
+    $out = array();
+    foreach ($newranges as $from => $to) {
+      $out[] = substr($text, $from, $to - $from);
+    }
+
+    // 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);
+
+    return $this->highlightField($text, $keys);
+  }
+
+  /**
+   * 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.
+   *
+   * @return string
+   *   The field's text with all occurrences of search keywords highlighted.
+   */
+  protected function highlightField($text, array $keys) {
+    $replace = $this->options['prefix'] . '\0' . $this->options['suffix'];
+    $keys = implode('|', array_map('preg_quote', $keys));
+    $text = preg_replace('/' . self::$boundary . '(' . $keys . ')' . self::$boundary . '/iu', $replace, ' ' . $text . ' ');
+    return substr($text, 1, -1);
+  }
+
+}

+ 142 - 0
sites/all/modules/contrib/search/search_api/includes/processor_html_filter.inc

@@ -0,0 +1,142 @@
+<?php
+
+/**
+ * @file
+ * Contains SearchApiHtmlFilter.
+ */
+
+/**
+ * Processor for stripping HTML from indexed fulltext data. Supports assigning
+ * custom boosts for any HTML element.
+ */
+// @todo Process query?
+class SearchApiHtmlFilter extends SearchApiAbstractProcessor {
+
+  /**
+   * @var array
+   */
+  protected $tags;
+
+  public function __construct(SearchApiIndex $index, array $options = array()) {
+    parent::__construct($index, $options);
+    $this->options += array(
+      'title' => FALSE,
+      'alt'   => TRUE,
+      'tags'  => "h1 = 5\n" .
+          "h2 = 3\n" .
+          "h3 = 2\n" .
+          "strong = 2\n" .
+          "b = 2\n" .
+          "em = 1.5\n" .
+          'u = 1.5',
+    );
+    $this->tags = drupal_parse_info_format($this->options['tags']);
+    // Specifying empty tags doesn't make sense.
+    unset($this->tags['br'], $this->tags['hr']);
+  }
+
+  public function configurationForm() {
+    $form = parent::configurationForm();
+    $form += array(
+      'title' => array(
+        '#type' => 'checkbox',
+        '#title' => t('Index title attribute'),
+        '#description' => t('If set, the contents of title attributes will be indexed.'),
+        '#default_value' => $this->options['title'],
+      ),
+      'alt' => array(
+        '#type' => 'checkbox',
+        '#title' => t('Index alt attribute'),
+        '#description' => t('If set, the alternative text of images will be indexed.'),
+        '#default_value' => $this->options['alt'],
+      ),
+      'tags' => array(
+        '#type' => 'textarea',
+        '#title' => t('Tag boosts'),
+        '#description' => t('Specify special boost values for certain HTML elements, in <a href="@link">INI file format</a>. ' .
+            'The boost values of nested elements are multiplied, elements not mentioned will have the default boost value of 1. ' .
+            'Assign a boost of 0 to ignore the text content of that HTML element.',
+            array('@link' => url('http://api.drupal.org/api/function/drupal_parse_info_format/7'))),
+        '#default_value' => $this->options['tags'],
+      ),
+    );
+
+    return $form;
+  }
+
+  public function configurationFormValidate(array $form, array &$values, array &$form_state) {
+    parent::configurationFormValidate($form, $values, $form_state);
+
+    if (empty($values['tags'])) {
+      return;
+    }
+    $tags = drupal_parse_info_format($values['tags']);
+    $errors = array();
+    foreach ($tags as $key => $value) {
+      if (is_array($value)) {
+        $errors[] = t("Boost value for tag &lt;@tag&gt; can't be an array.", array('@tag' => $key));
+      }
+      elseif (!is_numeric($value)) {
+        $errors[] = t("Boost value for tag &lt;@tag&gt; must be numeric.", array('@tag' => $key));
+      }
+      elseif ($value < 0) {
+        $errors[] = t('Boost value for tag &lt;@tag&gt; must be non-negative.', array('@tag' => $key));
+      }
+    }
+    if ($errors) {
+      form_error($form['tags'], implode("<br />\n", $errors));
+    }
+  }
+
+  protected function processFieldValue(&$value) {
+    $text = str_replace(array('<', '>'), array(' <', '> '), $value); // Let removed tags still delimit words.
+    if ($this->options['title']) {
+      $text = preg_replace('/(<[-a-z_]+[^>]+)\btitle\s*=\s*("([^"]+)"|\'([^\']+)\')([^>]*>)/i', '$1 $5 $3$4 ', $text);
+    }
+    if ($this->options['alt']) {
+      $text = preg_replace('/<img\b[^>]+\balt\s*=\s*("([^"]+)"|\'([^\']+)\')[^>]*>/i', ' <img>$2$3</img> ', $text);
+    }
+    if ($this->tags) {
+      $text = strip_tags($text, '<' . implode('><', array_keys($this->tags)) . '>');
+      $value = $this->parseText($text);
+    }
+    else {
+      $value = strip_tags($text);
+    }
+  }
+
+  protected function parseText(&$text, $active_tag = NULL, $boost = 1) {
+    $ret = array();
+    while (($pos = strpos($text, '<')) !== FALSE) {
+      if ($boost && $pos > 0) {
+        $ret[] = array(
+          'value' => html_entity_decode(substr($text, 0, $pos), ENT_QUOTES, 'UTF-8'),
+          'score' => $boost,
+        );
+      }
+      $text = substr($text, $pos + 1);
+      preg_match('#^(/?)([-:_a-zA-Z]+)#', $text, $m);
+      $text = substr($text, strpos($text, '>') + 1);
+      if ($m[1]) {
+        // Closing tag.
+        if ($active_tag && $m[2] == $active_tag) {
+          return $ret;
+        }
+      }
+      else {
+        // Opening tag => recursive call.
+        $inner_boost = $boost * (isset($this->tags[$m[2]]) ? $this->tags[$m[2]] : 1);
+        $ret = array_merge($ret, $this->parseText($text, $m[2], $inner_boost));
+      }
+    }
+    if ($text) {
+      $ret[] = array(
+        'value' => html_entity_decode($text, ENT_QUOTES, 'UTF-8'),
+        'score' => $boost,
+      );
+      $text = '';
+    }
+    return $ret;
+  }
+
+}

+ 20 - 0
sites/all/modules/contrib/search/search_api/includes/processor_ignore_case.inc

@@ -0,0 +1,20 @@
+<?php
+
+/**
+ * @file
+ * Contains SearchApiIgnoreCase.
+ */
+
+/**
+ * Processor for making searches case-insensitive.
+ */
+class SearchApiIgnoreCase extends SearchApiAbstractProcessor {
+
+  protected function process(&$value) {
+    // We don't touch integers, NULL values or the like.
+    if (is_string($value)) {
+      $value = drupal_strtolower($value);
+    }
+  }
+
+}

+ 107 - 0
sites/all/modules/contrib/search/search_api/includes/processor_stopwords.inc

@@ -0,0 +1,107 @@
+<?php
+
+/**
+ * @file
+ * Contains SearchApiStopWords.
+ */
+
+/**
+ * Processor for removing stopwords from index and search terms.
+ */
+class SearchApiStopWords extends SearchApiAbstractProcessor {
+
+  /**
+   * Holds all words ignored for the last query.
+   *
+   * @var array
+   */
+  protected $ignored = array();
+
+  public function configurationForm() {
+    $form = parent::configurationForm();
+
+    $form += array(
+      'help' => array(
+        '#markup' => '<p>' . t('Provide a stopwords file or enter the words in this form. If you do both, both will be used. Read about !stopwords.', array('!stopwords' => l(t('stop words'), "http://en.wikipedia.org/wiki/Stop_words"))) . '</p>',
+      ),
+      'file' => array(
+        '#type' => 'textfield',
+        '#title' => t('Stopwords file'),
+        '#description' => t('This must be a stream-type description like <code>public://stopwords/stopwords.txt</code> or <code>http://example.com/stopwords.txt</code> or <code>private://stopwords.txt</code>.'),
+      ),
+      'stopwords' => array(
+        '#type' => 'textarea',
+        '#title' => t('Stopwords'),
+        '#description' => t('Enter a space and/or linebreak separated list of stopwords that will be removed from content before it is indexed and from search terms before searching.'),
+        '#default_value' => t("but\ndid\nthe this that those\netc"),
+      ),
+    );
+
+    if (!empty($this->options)) {
+      $form['file']['#default_value'] = $this->options['file'];
+      $form['stopwords']['#default_value'] = $this->options['stopwords'];
+    }
+    return $form;
+  }
+
+  public function configurationFormValidate(array $form, array &$values, array &$form_state) {
+    parent::configurationFormValidate($form, $values, $form_state);
+
+    $uri = $values['file'];
+    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)));
+    }
+  }
+
+  public function process(&$value) {
+    $stopwords = $this->getStopWords();
+    if (empty($stopwords) || !is_string($value)) {
+      return;
+    }
+    $words = preg_split('/\s+/', $value);
+    foreach ($words as $sub_key => $sub_value) {
+      if (isset($stopwords[$sub_value])) {
+        unset($words[$sub_key]);
+        $this->ignored[] = $sub_value;
+      }
+    }
+    $value = implode(' ', $words);
+  }
+
+  public function preprocessSearchQuery(SearchApiQuery $query) {
+    $this->ignored = array();
+    parent::preprocessSearchQuery($query);
+  }
+
+  public function postprocessSearchResults(array &$response, SearchApiQuery $query) {
+    if ($this->ignored) {
+      if (isset($response['ignored'])) {
+        $response['ignored'] = array_merge($response['ignored'], $this->ignored);
+      }
+      else {
+        $response['ignored'] = $this->ignored;
+      }
+    }
+  }
+
+  /**
+   * @return
+   *   An array whose keys are the stopwords set in either the file or the text
+   *   field.
+   */
+  protected function getStopWords() {
+    if (isset($this->stopwords)) {
+      return $this->stopwords;
+    }
+    $file_words = $form_words = array();
+    if (!empty($this->options['file']) && $stopwords_file = file_get_contents($this->options['file'])) {
+      $file_words = preg_split('/\s+/', $stopwords_file);
+    }
+    if (!empty($this->options['stopwords'])) {
+      $form_words = preg_split('/\s+/', $this->options['stopwords']);
+    }
+    $this->stopwords = array_flip(array_merge($file_words, $form_words));
+    return $this->stopwords;
+  }
+}

+ 114 - 0
sites/all/modules/contrib/search/search_api/includes/processor_tokenizer.inc

@@ -0,0 +1,114 @@
+<?php
+
+/**
+ * @file
+ * Contains SearchApiTokenizer.
+ */
+
+/**
+ * Processor for tokenizing fulltext data by replacing (configurable)
+ * non-letters with spaces.
+ */
+class SearchApiTokenizer extends SearchApiAbstractProcessor {
+
+  /**
+   * @var string
+   */
+  protected $spaces;
+
+  /**
+   * @var string
+   */
+  protected $ignorable;
+
+  public function configurationForm() {
+    $form = parent::configurationForm();
+
+    // Only make fulltext fields available as options.
+    $fields = $this->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',
+        '#title' => t('Whitespace characters'),
+        '#description' => t('Specify the characters that should be regarded as whitespace and therefore used as word-delimiters. ' .
+            'Specify the characters as a <a href="@link">PCRE character class</a>. ' .
+            'Note: For non-English content, the default setting might not be suitable.',
+            array('@link' => url('http://www.php.net/manual/en/regexp.reference.character-classes.php'))),
+        '#default_value' => "[^[:alnum:]]",
+      ),
+      'ignorable' => array(
+        '#type' => 'textfield',
+        '#title' => t('Ignorable characters'),
+        '#description' => t('Specify characters which should be removed from fulltext fields and search strings (e.g., "-"). The same format as above is used.'),
+        '#default_value' => "[']",
+      ),
+    );
+
+    if (!empty($this->options)) {
+      $form['spaces']['#default_value'] = $this->options['spaces'];
+      $form['ignorable']['#default_value'] = $this->options['ignorable'];
+    }
+
+    return $form;
+  }
+
+  public function configurationFormValidate(array $form, array &$values, array &$form_state) {
+    parent::configurationFormValidate($form, $values, $form_state);
+
+    $spaces = str_replace('/', '\/', $values['spaces']);
+    $ignorable = str_replace('/', '\/', $values['ignorable']);
+    if (@preg_match('/(' . $spaces . ')+/u', '') === FALSE) {
+      $el = $form['spaces'];
+      form_error($el, $el['#title'] . ': ' . t('The entered text is no valid regular expression.'));
+    }
+    if (@preg_match('/(' . $ignorable . ')+/u', '') === FALSE) {
+      $el = $form['ignorable'];
+      form_error($el, $el['#title'] . ': ' . t('The entered text is no valid regular expression.'));
+    }
+  }
+
+  protected function processFieldValue(&$value) {
+    $this->prepare();
+    if ($this->ignorable) {
+      $value = preg_replace('/(' . $this->ignorable . ')+/u', '', $value);
+    }
+    if ($this->spaces) {
+      $arr = preg_split('/(' . $this->spaces . ')+/u', $value);
+      if (count($arr) > 1) {
+        $value = array();
+        foreach ($arr as $token) {
+          $value[] = array('value' => $token);
+        }
+      }
+    }
+  }
+
+  protected function process(&$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);
+      }
+    }
+  }
+
+  protected function prepare() {
+    if (!isset($this->spaces)) {
+      $this->spaces = str_replace('/', '\/', $this->options['spaces']);
+      $this->ignorable = str_replace('/', '\/', $this->options['ignorable']);
+    }
+  }
+
+}

+ 20 - 0
sites/all/modules/contrib/search/search_api/includes/processor_transliteration.inc

@@ -0,0 +1,20 @@
+<?php
+
+/**
+ * @file
+ * Contains SearchApiTransliteration.
+ */
+
+/**
+ * Processor for making searches insensitive to accents and other non-ASCII characters.
+ */
+class SearchApiTransliteration extends SearchApiAbstractProcessor {
+
+  protected function process(&$value) {
+    // We don't touch integers, NULL values or the like.
+    if (is_string($value)) {
+      $value = transliteration_get($value, '', language_default('language'));
+    }
+  }
+
+}

+ 1013 - 0
sites/all/modules/contrib/search/search_api/includes/query.inc

@@ -0,0 +1,1013 @@
+<?php
+
+/**
+ * @file
+ * Contains SearchApiQueryInterface and SearchApiQuery.
+ */
+
+/**
+ * Interface representing a search query on an Search API index.
+ *
+ * Methods not returning something else will return the object itself, so calls
+ * can be chained.
+ */
+interface SearchApiQueryInterface {
+
+  /**
+   * Constructs a new search query.
+   *
+   * @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.
+   *   - 'skip result count': If present and set to TRUE, the result's
+   *     "result count" key will not be needed. Service classes can check for
+   *     this option to possibly avoid executing expensive operations to compute
+   *     the result count in cases where it is not needed.
+   *   - search_api_access_account: The account which will be used for entity
+   *     access checks, if available and enabled for the index.
+   *   - search_api_bypass_access: If set to TRUE, entity access checks will be
+   *     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.
+   */
+  public function __construct(SearchApiIndex $index, array $options = array());
+
+  /**
+   * Retrieves the parse modes supported by this query class.
+   *
+   * @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.
+   */
+  public function parseModes();
+
+  /**
+   * Creates a new filter to use with this query object.
+   *
+   * @param string $conjunction
+   *   The conjunction to use for the filter - either 'AND' or 'OR'.
+   * @param $tags
+   *   (Optional) An arbitrary set of tags. Can be used to identify this filter
+   *   down the line if necessary. This is primarily used by the facet system
+   *   to support OR facet queries.
+   *
+   * @return SearchApiQueryFilterInterface
+   *   A filter object that is set to use the specified conjunction.
+   */
+  public function createFilter($conjunction = 'AND', $tags = array());
+
+  /**
+   * 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 array|string|null $keys
+   *   A string with the unparsed search keys, or NULL to use no search keys.
+   *
+   * @return SearchApiQueryInterface
+   *   The called object.
+   */
+  public function keys($keys = NULL);
+
+  /**
+   * Sets the fields that will be searched for the search keys.
+   *
+   * If this is not called, all fulltext fields will be searched.
+   *
+   * @param array $fields
+   *   An array containing fulltext fields that should be searched.
+   *
+   * @return SearchApiQueryInterface
+   *   The called object.
+   *
+   * @throws SearchApiException
+   *   If one of the fields isn't of type "text".
+   */
+  // @todo Allow calling with NULL.
+  public function fields(array $fields);
+
+  /**
+   * Adds a subfilter to this query's filter.
+   *
+   * @param SearchApiQueryFilterInterface $filter
+   *   A SearchApiQueryFilter object that should be added as a subfilter.
+   *
+   * @return SearchApiQueryInterface
+   *   The called object.
+   */
+  public function filter(SearchApiQueryFilterInterface $filter);
+
+  /**
+   * Adds a new ($field $operator $value) condition filter.
+   *
+   * @param string $field
+   *   The field to filter on, e.g. 'title'.
+   * @param mixed $value
+   *   The value the field should have (or be related to by the 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.
+   *   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 SearchApiQueryInterface
+   *   The called object.
+   */
+  public function condition($field, $value, $operator = '=');
+
+  /**
+   * Adds a sort directive to this search query.
+   *
+   * 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 string $order
+   *   The order to sort items in - either 'ASC' or 'DESC'.
+   *
+   * @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.
+   *
+   * @param int|null $offset
+   *   The zero-based offset of the first result returned.
+   * @param int|null $limit
+   *   The number of results to return.
+   *
+   * @return SearchApiQueryInterface
+   *   The called object.
+   */
+  public function range($offset = NULL, $limit = NULL);
+
+  /**
+   * 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, 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.
+   *     - 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.
+   */
+  public function execute();
+
+  /**
+   * 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.
+   */
+  public function preExecute();
+
+  /**
+   * 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 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();
+
+  /**
+   * 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
+   *   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.
+   *
+   * @see keys()
+   */
+  public function &getKeys();
+
+  /**
+   * 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 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();
+
+  /**
+   * Retrieves an option set on this search query.
+   *
+   * @param string $name
+   *   The name of an option.
+   * @param mixed $default
+   *   The value to return if the specified option is not set.
+   *
+   * @return mixed
+   *   The value of the option with the specified name, if set. NULL otherwise.
+   */
+  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 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.
+   */
+  public function &getOptions();
+
+}
+
+/**
+ * Provides a standard implementation of the SearchApiQueryInterface.
+ */
+class SearchApiQuery implements SearchApiQueryInterface {
+
+  /**
+   * 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.
+   *
+   * @var mixed
+   */
+  protected $keys;
+
+  /**
+   * The unprocessed search keys, as passed to the keys() method.
+   *
+   * @var mixed
+   */
+  protected $orig_keys;
+
+  /**
+   * The fields that will be searched for the keys.
+   *
+   * @var array
+   */
+  protected $fields;
+
+  /**
+   * The search filter associated with this query.
+   *
+   * @var SearchApiQueryFilterInterface
+   */
+  protected $filter;
+
+  /**
+   * The sort associated with this query.
+   *
+   * @var array
+   */
+  protected $sort;
+
+  /**
+   * Search options configuring this query.
+   *
+   * @var array
+   */
+  protected $options;
+
+  /**
+   * Flag for whether preExecute() was already called for this query.
+   *
+   * @var bool
+   */
+  protected $pre_execute = FALSE;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(SearchApiIndex $index, array $options = array()) {
+    if (empty($index->options['fields'])) {
+      throw new SearchApiException(t("Can't search an index which hasn't got any fields defined."));
+    }
+    if (empty($index->enabled)) {
+      throw new SearchApiException(t("Can't search a disabled index."));
+    }
+    if (isset($options['parse mode'])) {
+      $modes = $this->parseModes();
+      if (!isset($modes[$options['parse mode']])) {
+        throw new SearchApiException(t('Unknown parse mode: @mode.', array('@mode' => $options['parse mode'])));
+      }
+    }
+    $this->index = $index;
+    $this->options = $options + array(
+      'conjunction' => 'AND',
+      'parse mode' => 'terms',
+      'filter class' => 'SearchApiQueryFilter',
+      'search id' => __CLASS__,
+    );
+    $this->filter = $this->createFilter('AND');
+    $this->sort = array();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function parseModes() {
+    $modes['direct'] = array(
+      'name' => t('Direct query'),
+      'description' => t("Don't parse the query, just hand it to the search server unaltered. " .
+          "Might fail if the query contains syntax errors in regard to the specific server's query syntax."),
+    );
+    $modes['single'] = array(
+      'name' => t('Single term'),
+      'description' => t('The query is interpreted as a single keyword, maybe containing spaces or special characters.'),
+    );
+    $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.'),
+    );
+    // @todo Add fourth mode for complicated expressions, e.g.: »"vanilla ice" OR (love NOT hate)«
+    return $modes;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function parseKeys($keys, $mode) {
+    if ($keys === NULL || is_array($keys)) {
+      return $keys;
+    }
+    $keys = '' . $keys;
+    switch ($mode) {
+      case 'direct':
+        return $keys;
+
+      case 'single':
+        return array('#conjunction' => $this->options['conjunction'], $keys);
+
+      case 'terms':
+        $ret = preg_split('/\s+/u', $keys);
+        $quoted = FALSE;
+        $str = '';
+        foreach ($ret as $k => $v) {
+          if (!$v) {
+            continue;
+          }
+          if ($quoted) {
+            if (substr($v, -1) == '"') {
+              $v = substr($v, 0, -1);
+              $str .= ' ' . $v;
+              $ret[$k] = $str;
+              $quoted = FALSE;
+            }
+            else {
+              $str .= ' ' . $v;
+              unset($ret[$k]);
+            }
+          }
+          elseif ($v[0] == '"') {
+            $len = strlen($v);
+            if ($len > 1 && $v[$len-1] == '"') {
+              $ret[$k] = substr($v, 1, -1);
+            }
+            else {
+              $str = substr($v, 1);
+              $quoted = TRUE;
+              unset($ret[$k]);
+            }
+          }
+        }
+        if ($quoted) {
+          $ret[] = $str;
+        }
+        $ret['#conjunction'] = $this->options['conjunction'];
+        return array_filter($ret);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function createFilter($conjunction = 'AND', $tags = array()) {
+    $filter_class = $this->options['filter class'];
+    return new $filter_class($conjunction, $tags);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function keys($keys = NULL) {
+    $this->orig_keys = $keys;
+    if (isset($keys)) {
+      $this->keys = $this->parseKeys($keys, $this->options['parse mode']);
+    }
+    else {
+      $this->keys = NULL;
+    }
+    return $this;
+  }
+  /**
+   * {@inheritdoc}
+   */
+  public function fields(array $fields) {
+    $fulltext_fields = $this->index->getFulltextFields();
+    foreach (array_diff($fields, $fulltext_fields) as $field) {
+      throw new SearchApiException(t('Trying to search on field @field which is no indexed fulltext field.', array('@field' => $field)));
+    }
+    $this->fields = $fields;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function filter(SearchApiQueryFilterInterface $filter) {
+    $this->filter->filter($filter);
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function condition($field, $value, $operator = '=') {
+    $this->filter->condition($field, $value, $operator);
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function sort($field, $order = 'ASC') {
+    $fields = $this->index->options['fields'];
+    $fields += array(
+      'search_api_relevance' => array('type' => 'decimal'),
+      'search_api_id' => array('type' => 'integer'),
+    );
+    if (empty($fields[$field])) {
+      throw new SearchApiException(t('Trying to sort on unknown field @field.', array('@field' => $field)));
+    }
+    $type = $fields[$field]['type'];
+    if (search_api_is_list_type($type) || search_api_is_text_type($type)) {
+      throw new SearchApiException(t('Trying to sort on field @field of illegal type @type.', array('@field' => $field, '@type' => $type)));
+    }
+    $order = strtoupper(trim($order)) == 'DESC' ? 'DESC' : 'ASC';
+    $this->sort[$field] = $order;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function range($offset = NULL, $limit = NULL) {
+    $this->options['offset'] = $offset;
+    $this->options['limit'] = $limit;
+    return $this;
+  }
+
+
+  /**
+   * {@inheritdoc}
+   */
+  public function execute() {
+    $start = microtime(TRUE);
+
+    // Prepare the query for execution by the server.
+    $this->preExecute();
+
+    $pre_search = microtime(TRUE);
+
+    // Execute query.
+    $response = $this->index->server()->search($this);
+
+    $post_search = microtime(TRUE);
+
+    // Postprocess the search results.
+    $this->postExecute($response);
+
+    $end = microtime(TRUE);
+    $response['performance']['complete'] = $end - $start;
+    $response['performance']['hooks'] = $response['performance']['complete'] - ($post_search - $pre_search);
+
+    // Store search for later retrieval for facets, etc.
+    search_api_current_search(NULL, $this, $response);
+
+    return $response;
+  }
+
+  /**
+   * 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) {
+      $languages[] = LANGUAGE_NONE;
+    }
+
+    $languages = drupal_map_assoc($languages);
+    $langs_to_add = $languages;
+    $filters = $this->filter->getFilters();
+    while ($filters && $langs_to_add) {
+      $filter = array_shift($filters);
+      if (is_array($filter)) {
+        if ($filter[0] == 'search_api_language' && $filter[2] == '=') {
+          $lang = $filter[1];
+          if (isset($languages[$lang])) {
+            unset($langs_to_add[$lang]);
+          }
+          else {
+            throw new SearchApiException(t('Impossible combination of filters and languages. There is a filter for "@language", but allowed languages are: "@languages".', array('@language' => $lang, '@languages' => implode('", "', $languages))));
+          }
+        }
+      }
+      else {
+        if ($filter->getConjunction() == 'AND') {
+          $filters += $filter->getFilters();
+        }
+      }
+    }
+    if ($langs_to_add) {
+      if (count($langs_to_add) == 1) {
+        $this->condition('search_api_language', reset($langs_to_add));
+      }
+      else {
+        $filter = $this->createFilter('OR');
+        foreach ($langs_to_add as $lang) {
+          $filter->condition('search_api_language', $lang);
+        }
+        $this->filter($filter);
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function preExecute() {
+    // 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);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function postExecute(array &$results) {
+    // Postprocess results.
+    $this->index->postprocessSearchResults($results, $this);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getIndex() {
+    return $this->index;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function &getKeys() {
+    return $this->keys;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getOriginalKeys() {
+    return $this->orig_keys;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function &getFields() {
+    return $this->fields;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFilter() {
+    return $this->filter;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function &getSort() {
+    return $this->sort;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getOption($name, $default = NULL) {
+    return array_key_exists($name, $this->options) ? $this->options[$name] : $default;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setOption($name, $value) {
+    $old = $this->getOption($name);
+    $this->options[$name] = $value;
+    return $old;
+  }
+
+  /**
+   * {@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;
+  }
+
+}
+
+/**
+ * Represents a filter on a search query.
+ *
+ * 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 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', array $tags = array());
+
+  /**
+   * Sets this filter's conjunction.
+   *
+   * @param string $conjunction
+   *   The conjunction to use for this filter - either 'AND' or 'OR'.
+   *
+   * @return SearchApiQueryFilterInterface
+   *   The called object.
+   */
+  public function setConjunction($conjunction);
+
+  /**
+   * Adds a subfilter.
+   *
+   * @param SearchApiQueryFilterInterface $filter
+   *   A SearchApiQueryFilterInterface object that should be added as a
+   *   subfilter.
+   *
+   * @return SearchApiQueryFilterInterface
+   *   The called object.
+   */
+  public function filter(SearchApiQueryFilterInterface $filter);
+
+  /**
+   * Adds a new ($field $operator $value) condition.
+   *
+   * @param string $field
+   *   The field to filter on, e.g. 'title'.
+   * @param mixed $value
+   *   The value the field should have (or be related to by the 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.
+   *   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 SearchApiQueryFilterInterface
+   *   The called object.
+   */
+  public function condition($field, $value, $operator = '=');
+
+  /**
+   * 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.
+   */
+  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();
+
+}
+
+/**
+ * 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.
+   *
+   * @var array
+   */
+  protected $filters;
+
+  /**
+   * String specifying this filter's conjunction ('AND' or 'OR').
+   *
+   * @var string
+   */
+  protected $conjunction;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct($conjunction = 'AND', array $tags = array()) {
+    $this->setConjunction($conjunction);
+    $this->filters = array();
+    $this->tags = drupal_map_assoc($tags);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setConjunction($conjunction) {
+    $this->conjunction = strtoupper(trim($conjunction)) == 'OR' ? 'OR' : 'AND';
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function filter(SearchApiQueryFilterInterface $filter) {
+    $this->filters[] = $filter;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function condition($field, $value, $operator = '=') {
+    $this->filters[] = array($field, $value, $operator);
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConjunction() {
+    return $this->conjunction;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function &getFilters() {
+    return $this->filters;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function hasTag($tag) {
+    return isset($this->tags[$tag]);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function &getTags() {
+    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;
+      }
+    }
+  }
+
+}

+ 396 - 0
sites/all/modules/contrib/search/search_api/includes/server_entity.inc

@@ -0,0 +1,396 @@
+<?php
+
+/**
+ * @file
+ * Contains SearchApiServer.
+ */
+
+/**
+ * Class representing a search server.
+ *
+ * This can handle the same calls as defined in the SearchApiServiceInterface
+ * and pass it on to the service implementation appropriate for this server.
+ */
+class SearchApiServer extends Entity {
+
+  /* Database values that will be set when object is loaded: */
+
+  /**
+   * The primary identifier for a server.
+   *
+   * @var integer
+   */
+  public $id = 0;
+
+  /**
+   * The displayed name for a server.
+   *
+   * @var string
+   */
+  public $name = '';
+
+  /**
+   * The machine name for a server.
+   *
+   * @var string
+   */
+  public $machine_name = '';
+
+  /**
+   * The displayed description for a server.
+   *
+   * @var string
+   */
+  public $description = '';
+
+  /**
+   * The id of the service class to use for this server.
+   *
+   * @var string
+   */
+  public $class = '';
+
+  /**
+   * The options used to configure the service object.
+   *
+   * @var array
+   */
+  public $options = array();
+
+  /**
+   * A flag indicating whether the server is enabled.
+   *
+   * @var integer
+   */
+  public $enabled = 1;
+
+  /**
+   * Proxy object for invoking service methods.
+   *
+   * @var SearchApiServiceInterface
+   */
+  protected $proxy;
+
+  /**
+   * Constructor as a helper to the parent constructor.
+   */
+  public function __construct(array $values = array()) {
+    parent::__construct($values, 'search_api_server');
+  }
+
+  /**
+   * Helper method for updating entity properties.
+   *
+   * NOTE: You shouldn't change any properties of this object before calling
+   * this method, as this might lead to the fields not being saved correctly.
+   *
+   * @param array $fields
+   *   The new field values.
+   *
+   * @return int|false
+   *   SAVE_UPDATED on success, FALSE on failure, 0 if the fields already had
+   *   the specified values.
+   */
+  public function update(array $fields) {
+    $changeable = array('name' => 1, 'enabled' => 1, 'description' => 1, 'options' => 1);
+    $changed = FALSE;
+    foreach ($fields as $field => $value) {
+      if (isset($changeable[$field]) && $value !== $this->$field) {
+        $this->$field = $value;
+        $changed = TRUE;
+      }
+    }
+    // If there are no new values, just return 0.
+    if (!$changed) {
+      return 0;
+    }
+    return $this->save();
+  }
+
+  /**
+   * Magic method for determining which fields should be serialized.
+   *
+   * Serialize all properties except the proxy object.
+   *
+   * @return array
+   *   An array of properties to be serialized.
+   */
+  public function __sleep() {
+    $ret = get_object_vars($this);
+    unset($ret['proxy'], $ret['status'], $ret['module'], $ret['is_new']);
+    return array_keys($ret);
+  }
+
+  /**
+   * Helper method for ensuring the proxy object is set up.
+   */
+  protected function ensureProxy() {
+    if (!isset($this->proxy)) {
+      $class = search_api_get_service_info($this->class);
+      if ($class && class_exists($class['class'])) {
+        if (empty($this->options)) {
+          // We always have to provide the options.
+          $this->options = array();
+        }
+        $this->proxy = new $class['class']($this);
+      }
+      if (!($this->proxy instanceof SearchApiServiceInterface)) {
+        throw new SearchApiException(t('Search server with machine name @name specifies illegal service class @class.', array('@name' => $this->machine_name, '@class' => $this->class)));
+      }
+    }
+  }
+
+  /**
+   * Reacts to calls of undefined methods on this object.
+   *
+   * If the service class defines additional methods, not specified in the
+   * SearchApiServiceInterface interface, then they are called via this magic
+   * method.
+   */
+  public function __call($name, $arguments = array()) {
+    $this->ensureProxy();
+    return call_user_func_array(array($this->proxy, $name), $arguments);
+  }
+
+  // Proxy methods
+
+  // For increased clarity, and since some parameters are passed by reference,
+  // we don't use the __call() magic method for those. This also gives us the
+  // opportunity to do additional error handling.
+
+  /**
+   * Form constructor for the server configuration form.
+   *
+   * @see SearchApiServiceInterface::configurationForm()
+   */
+  public function configurationForm(array $form, array &$form_state) {
+    $this->ensureProxy();
+    return $this->proxy->configurationForm($form, $form_state);
+  }
+
+  /**
+   * Validation callback for the form returned by configurationForm().
+   *
+   * @see SearchApiServiceInterface::configurationFormValidate()
+   */
+  public function configurationFormValidate(array $form, array &$values, array &$form_state) {
+    $this->ensureProxy();
+    return $this->proxy->configurationFormValidate($form, $values, $form_state);
+  }
+
+  /**
+   * Submit callback for the form returned by configurationForm().
+   *
+   * @see SearchApiServiceInterface::configurationFormSubmit()
+   */
+  public function configurationFormSubmit(array $form, array &$values, array &$form_state) {
+    $this->ensureProxy();
+    return $this->proxy->configurationFormSubmit($form, $values, $form_state);
+  }
+
+  /**
+   * Determines whether this service class supports a given feature.
+   *
+   * @see SearchApiServiceInterface::supportsFeature()
+   */
+  public function supportsFeature($feature) {
+    $this->ensureProxy();
+    return $this->proxy->supportsFeature($feature);
+  }
+
+  /**
+   * Displays this server's settings.
+   *
+   * @see SearchApiServiceInterface::viewSettings()
+   */
+  public function viewSettings() {
+    $this->ensureProxy();
+    return $this->proxy->viewSettings();
+  }
+
+  /**
+   * Reacts to the server's creation.
+   *
+   * @see SearchApiServiceInterface::postCreate()
+   */
+  public function postCreate() {
+    $this->ensureProxy();
+    return $this->proxy->postCreate();
+  }
+
+  /**
+   * Notifies this server that its fields are about to be updated.
+   *
+   * @see SearchApiServiceInterface::postUpdate()
+   */
+  public function postUpdate() {
+    $this->ensureProxy();
+    return $this->proxy->postUpdate();
+  }
+
+  /**
+   * Notifies this server that it is about to be deleted from the database.
+   *
+   * @see SearchApiServiceInterface::preDelete()
+   */
+  public function preDelete() {
+    $this->ensureProxy();
+    return $this->proxy->preDelete();
+  }
+
+  /**
+   * Adds a new index to this server.
+   *
+   * If an exception in the service class implementation of this method occcurs,
+   * it will be caught and the operation saved as an pending server task.
+   *
+   * @see SearchApiServiceInterface::addIndex()
+   * @see search_api_server_tasks_add()
+   */
+  public function addIndex(SearchApiIndex $index) {
+    $this->ensureProxy();
+    try {
+      $this->proxy->addIndex($index);
+    }
+    catch (SearchApiException $e) {
+      $vars = array(
+        '%server' => $this->name,
+        '%index' => $index->name,
+      );
+      watchdog_exception('search_api', $e, '%type while adding index %index to server %server: !message in %function (line %line of %file).', $vars);
+      search_api_server_tasks_add($this, __FUNCTION__, $index);
+    }
+  }
+
+  /**
+   * Notifies the server that the field settings for the index have changed.
+   *
+   * If the service class implementation of the method returns TRUE, this will
+   * automatically take care of marking the items on the index for re-indexing.
+   *
+   * If an exception in the service class implementation of this method occcurs,
+   * it will be caught and the operation saved as an pending server task.
+   *
+   * @see SearchApiServiceInterface::fieldsUpdated()
+   * @see search_api_server_tasks_add()
+   */
+  public function fieldsUpdated(SearchApiIndex $index) {
+    $this->ensureProxy();
+    try {
+      if ($this->proxy->fieldsUpdated($index)) {
+        _search_api_index_reindex($index);
+        return TRUE;
+      }
+    }
+    catch (SearchApiException $e) {
+      $vars = array(
+        '%server' => $this->name,
+        '%index' => $index->name,
+      );
+      watchdog_exception('search_api', $e, '%type while updating the fields of index %index on server %server: !message in %function (line %line of %file).', $vars);
+      search_api_server_tasks_add($this, __FUNCTION__, $index, isset($index->original) ? $index->original : NULL);
+    }
+    return FALSE;
+  }
+
+  /**
+   * Removes an index from this server.
+   *
+   * If an exception in the service class implementation of this method occcurs,
+   * it will be caught and the operation saved as an pending server task.
+   *
+   * @see SearchApiServiceInterface::removeIndex()
+   * @see search_api_server_tasks_add()
+   */
+  public function removeIndex($index) {
+    // When removing an index from a server, it doesn't make any sense anymore to
+    // delete items from it, or react to other changes.
+    search_api_server_tasks_delete(NULL, $this, $index);
+
+    $this->ensureProxy();
+    try {
+      $this->proxy->removeIndex($index);
+    }
+    catch (SearchApiException $e) {
+      $vars = array(
+        '%server' => $this->name,
+        '%index' => is_object($index) ? $index->name : $index,
+      );
+      watchdog_exception('search_api', $e, '%type while removing index %index from server %server: !message in %function (line %line of %file).', $vars);
+      search_api_server_tasks_add($this, __FUNCTION__, $index);
+    }
+  }
+
+  /**
+   * Indexes the specified items.
+   *
+   * @see SearchApiServiceInterface::indexItems()
+   */
+  public function indexItems(SearchApiIndex $index, array $items) {
+    $this->ensureProxy();
+    return $this->proxy->indexItems($index, $items);
+  }
+
+  /**
+   * Deletes indexed items from this server.
+   *
+   * If an exception in the service class implementation of this method occcurs,
+   * it will be caught and the operation saved as an pending server task.
+   *
+   * @see SearchApiServiceInterface::deleteItems()
+   * @see search_api_server_tasks_add()
+   */
+  public function deleteItems($ids = 'all', SearchApiIndex $index = NULL) {
+    $this->ensureProxy();
+    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();
+  }
+
+}

+ 465 - 0
sites/all/modules/contrib/search/search_api/includes/service.inc

@@ -0,0 +1,465 @@
+<?php
+
+/**
+ * @file
+ * Contains SearchApiServiceInterface and SearchApiAbstractService.
+ */
+
+/**
+ * Interface defining the methods search services have to implement.
+ *
+ * Before a service object is used, the corresponding server's data will be read
+ * from the database (see SearchApiAbstractService for a list of fields).
+ *
+ * Most methods in this interface (where any change in data occurs) can throw a
+ * SearchApiException. The server entity class SearchApiServer catches these
+ * exceptions and uses the server tasks system to assure that the action is
+ * later retried.
+ */
+interface SearchApiServiceInterface {
+
+  /**
+   * Constructs a service object.
+   *
+   * This will set the server configuration used with this service.
+   *
+   * @param SearchApiServer $server
+   *   The server object for this service.
+   */
+  public function __construct(SearchApiServer $server);
+
+  /**
+   * Form constructor for the server configuration form.
+   *
+   * Might be called with an incomplete server (no ID). In this case, the form
+   * is displayed for the initial creation of the server.
+   *
+   * @param array $form
+   *   The server options part of the form.
+   * @param array $form_state
+   *   The current form state.
+   *
+   * @return array
+   *   A form array for setting service-specific options.
+   */
+  public function configurationForm(array $form, array &$form_state);
+
+  /**
+   * Validation callback for the form returned by configurationForm().
+   *
+   * $form_state['server'] will contain the server that is created or edited.
+   * Use form_error() to flag errors on form elements.
+   *
+   * @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 should set the options of this service' server according to
+   * $values.
+   *
+   * @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);
+
+  /**
+   * Determines whether this service class supports a given feature.
+   *
+   * Features are optional extensions to Search API functionality and usually
+   * defined and used by third-party modules.
+   *
+   * 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 bool
+   *   TRUE if this service knows and supports the specified feature. FALSE
+   *   otherwise.
+   */
+  public function supportsFeature($feature);
+
+  /**
+   * Displays this server's settings.
+   *
+   * Output can be HTML or a render array, a <dl> 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.
+   *
+   * @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.
+   *
+   * 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
+   * present in the database anymore at this point.
+   */
+  public function preDelete();
+
+  /**
+   * 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);
+
+  /**
+   * 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 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);
+
+  /**
+   * 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
+   * $index->server.
+   *
+   * 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);
+
+  /**
+   * Indexes the specified items.
+   *
+   * @param SearchApiIndex $index
+   *   The search index for which items should be indexed.
+   * @param array $items
+   *   An array of items to be indexed, keyed by their id. The values are
+   *   associative arrays of the fields to be stored, where each field is an
+   *   array with the following keys:
+   *   - type: One of the data types recognized by the Search API, or the
+   *     special type "tokens" for fulltext fields.
+   *   - original_type: The original type of the property, as defined by the
+   *     datasource controller for the index's item type.
+   *   - value: The value to index.
+   *
+   *   The special field "search_api_language" contains the item's language and
+   *   should always be indexed.
+   *
+   *   The value of fields with the "tokens" type is an array of tokens. Each
+   *   token is an array containing the following keys:
+   *   - value: The word that the token represents.
+   *   - score: A score for the importance of that word.
+   *
+   * @return array
+   *   An array of the ids of all items that were successfully indexed.
+   *
+   * @throws SearchApiException
+   *   If indexing was prevented by a fundamental configuration error.
+   */
+  public function indexItems(SearchApiIndex $index, array $items);
+
+  /**
+   * 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
+   * indexes on this server.
+   *
+   * @param $ids
+   *   Either an array containing the ids of the items that should be deleted,
+   *   or 'all' if all items should be deleted. Other formats might be
+   *   recognized by implementing classes, but these are not standardized.
+   * @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);
+
+  /**
+   * Creates a query object for searching on an index lying on this server.
+   *
+   * @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.
+   */
+  public function query(SearchApiIndex $index, $options = array());
+
+  /**
+   * Executes a search on the server represented by this object.
+   *
+   * @param $query
+   *   The SearchApiQueryInterface object to execute.
+   *
+   * @return array
+   *   An associative array containing the search results, as required by
+   *   SearchApiQueryInterface::execute().
+   *
+   * @throws SearchApiException
+   *   If an error prevented the search from completing.
+   */
+  public function search(SearchApiQueryInterface $query);
+
+}
+
+/**
+ * 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 {
+
+  /**
+   * @var SearchApiServer
+   */
+  protected $server;
+
+  /**
+   * Direct reference to the server's $options property.
+   *
+   * @var array
+   */
+  protected $options = array();
+
+  /**
+   * Implements SearchApiServiceInterface::__construct().
+   *
+   * The default implementation sets $this->server and $this->options.
+   */
+  public function __construct(SearchApiServer $server) {
+    $this->server = $server;
+    $this->options = &$server->options;
+  }
+
+  /**
+   * Implements SearchApiServiceInterface::__construct().
+   *
+   * Returns an empty form by default.
+   */
+  public function configurationForm(array $form, array &$form_state) {
+    return array();
+  }
+
+  /**
+   * Implements SearchApiServiceInterface::__construct().
+   *
+   * Does nothing by default.
+   */
+  public function configurationFormValidate(array $form, array &$values, array &$form_state) {
+    return;
+  }
+
+  /**
+   * 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.
+   */
+  public function configurationFormSubmit(array $form, array &$values, array &$form_state) {
+    if (!empty($this->options)) {
+      $values += $this->options;
+    }
+    $this->options = $values;
+  }
+
+  /**
+   * Implements SearchApiServiceInterface::__construct().
+   *
+   * The default implementation always returns FALSE.
+   */
+  public function supportsFeature($feature) {
+    return FALSE;
+  }
+
+  /**
+   * Implements SearchApiServiceInterface::__construct().
+   *
+   * The default implementation does a crude output as a definition list, with
+   * option names taken from the configuration form.
+   */
+  public function viewSettings() {
+    $output = '';
+    $form = $form_state = array();
+    $option_form = $this->configurationForm($form, $form_state);
+    $option_names = array();
+    foreach ($option_form as $key => $element) {
+      if (isset($element['#title']) && isset($this->options[$key])) {
+        $option_names[$key] = $element['#title'];
+      }
+    }
+
+    foreach ($option_names as $key => $name) {
+      $value = $this->options[$key];
+      $output .= '<dt>' . check_plain($name) . '</dt>' . "\n";
+      $output .= '<dd>' . nl2br(check_plain(print_r($value, TRUE))) . '</dd>' . "\n";
+    }
+
+    return $output ? "<dl>\n$output</dl>" : '';
+  }
+
+  /**
+   * 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.
+   */
+  public function postCreate() {
+    return;
+  }
+
+  /**
+   * Implements SearchApiServiceInterface::__construct().
+   *
+   * The default implementation always returns FALSE.
+   */
+  public function postUpdate() {
+    return FALSE;
+  }
+
+  /**
+   * 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);
+    }
+  }
+
+  /**
+   * Implements SearchApiServiceInterface::__construct().
+   *
+   * Does nothing, by default.
+   */
+  public function addIndex(SearchApiIndex $index) {
+    return;
+  }
+
+  /**
+   * Implements SearchApiServiceInterface::__construct().
+   *
+   * The default implementation always returns FALSE.
+   */
+  public function fieldsUpdated(SearchApiIndex $index) {
+    return FALSE;
+  }
+
+  /**
+   * Implements SearchApiServiceInterface::__construct().
+   *
+   * By default, removes all items from that index.
+   */
+  public function removeIndex($index) {
+    if (is_object($index) && empty($index->read_only)) {
+      $this->deleteItems('all', $index);
+    }
+  }
+
+  /**
+   * Implements SearchApiServiceInterface::__construct().
+   *
+   * The default implementation returns a SearchApiQuery object.
+   */
+  public function query(SearchApiIndex $index, $options = array()) {
+    return new SearchApiQuery($index, $options);
+  }
+
+}

+ 229 - 0
sites/all/modules/contrib/search/search_api/search_api.admin.css

@@ -0,0 +1,229 @@
+/**
+ * @file
+ * Styles for Search API admin pages.
+ */
+
+/*
+ * OVERVIEW
+ */
+
+.search-api-overview td.search-api-status {
+  text-align: center;
+}
+
+.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;
+}
+
+/* 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;
+}
+
+/**
+ * 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;
+}

+ 2224 - 0
sites/all/modules/contrib/search/search_api/search_api.admin.inc

@@ -0,0 +1,2224 @@
+<?php
+
+/**
+ * @file
+ * Administration page callbacks for the Search API module.
+ */
+
+/**
+ * Page callback that shows an overview of defined servers and indexes.
+ *
+ * @see search_api_menu()
+ */
+function search_api_admin_overview() {
+  $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');
+
+  $servers = search_api_server_load_multiple(FALSE);
+  $indexes = array();
+  // When any entity was not normally created in the database, then show status
+  // for all.
+  $show_config_status = FALSE;
+  foreach (search_api_index_load_multiple(FALSE) as $index) {
+    $indexes[$index->server][$index->machine_name] = $index;
+    if (!$show_config_status && $index->status != ENTITY_CUSTOM) {
+      $show_config_status = TRUE;
+    }
+  }
+  // Show disabled servers after enabled ones.
+  foreach ($servers as $id => $server) {
+    if (!$server->enabled) {
+      unset($servers[$id]);
+      $servers[$id] = $server;
+    }
+    if (!$show_config_status && $server->status != ENTITY_CUSTOM) {
+      $show_config_status = TRUE;
+    }
+  }
+
+  $rows = array();
+  $t_server = array('data' => t('Server'), 'colspan' => 2);
+  $t_index = t('Index');
+  $t_enabled['data'] = array(
+    '#theme' => 'image',
+    '#path' => $base_path . 'enabled.png',
+    '#alt' => t('enabled'),
+    '#title' => t('enabled'),
+  );
+  $t_enabled['class'] = array('search-api-status');
+  $t_disabled['data'] = array(
+    '#theme' => 'image',
+    '#path' => $base_path . 'disabled.png',
+    '#alt' => t('disabled'),
+    '#title' => t('disabled'),
+  );
+  $t_disabled['class'] = array('search-api-status');
+  $t_enable = t('Enable');
+  $pre_server = 'admin/config/search/search_api/server';
+  $pre_index = 'admin/config/search/search_api/index';
+  $enable = '/enable';
+  foreach ($servers as $server) {
+    $url = $pre_server . '/' . $server->machine_name;
+    $row = array();
+    $row[] = $server->enabled ? $t_enabled : $t_disabled;
+    if ($show_config_status) {
+      $row[] = theme('entity_status', array('status' => $server->status));
+    }
+    $row[] = $t_server;
+    $row[] = l($server->name, $url);
+    $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;
+        $row = array();
+        $row[] = $index->enabled ? $t_enabled : $t_disabled;
+        if ($show_config_status) {
+          $row[] = theme('entity_status', array('status' => $index->status));
+        }
+        $row[] = ' ';
+        $row[] = $t_index;
+        $row[] = l($index->name, $url);
+        $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);
+      }
+    }
+  }
+  if (!empty($indexes[''])) {
+    foreach ($indexes[''] as $index) {
+      $url = $pre_index . '/' . $index->machine_name;
+      $row = array();
+      $row[] = $t_disabled;
+      if ($show_config_status) {
+        $row[] = theme('entity_status', array('status' => $index->status));
+      }
+      $row[] = array('data' => $t_index, 'colspan' => 2);
+      $row[] = l($index->name, $url);
+      $links = menu_contextual_links('search-api-index', $pre_index, array($index->machine_name));
+      $row[] = theme('search_api_dropbutton', array('links' => $links));
+      $rows[] = _search_api_deep_copy($row);
+    }
+  }
+
+  $header = array();
+  $header[] = t('Status');
+  if ($show_config_status) {
+    $header[] = t('Configuration');
+  }
+  $header[] = array('data' => t('Type'), 'colspan' => 2);
+  $header[] = t('Name');
+  $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.'),
+  );
+}
+
+/**
+ * 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
+ *   HTML for the dropbutton link list.
+ */
+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 "<div class=\"dropbutton-wrapper\">
+  <div class=\"dropbutton-widget\">
+    $list
+  </div>
+</div>";
+}
+
+/**
+ * Form callback showing a form for adding a server.
+ */
+function search_api_admin_add_server(array $form, array &$form_state) {
+  drupal_set_title(t('Add server'));
+
+  $class = empty($form_state['values']['class']) ? '' : $form_state['values']['class'];
+  $form_state['server'] = entity_create('search_api_server', array());
+
+  if (empty($form_state['storage']['step_one'])) {
+    $form['name'] = array(
+      '#type' => 'textfield',
+      '#title' => t('Server name'),
+      '#description' => t('Enter the displayed name for the new server.'),
+      '#maxlength' => 50,
+      '#required' => TRUE,
+    );
+
+    $form['machine_name'] = array(
+      '#type' => 'machine_name',
+      '#maxlength' => 50,
+      '#machine_name' => array(
+        'exists' => 'search_api_server_load',
+      ),
+    );
+
+    $form['enabled'] = array(
+      '#type' => 'checkbox',
+      '#title' => t('Enabled'),
+      '#description' => t('Select if the new server will be enabled after creation.'),
+      '#default_value' => TRUE,
+    );
+    $form['description'] = array(
+      '#type' => 'textarea',
+      '#title' => t('Server description'),
+      '#description' => t('Enter a description for the new server.'),
+    );
+    $form['class'] = array(
+      '#type' => 'select',
+      '#title' => t('Service class'),
+      '#description' => t('Choose a service class to use for this server.'),
+      '#options' => array('' => '< ' . t('Choose a service class') . ' >'),
+      '#required' => TRUE,
+      '#default_value' => $class,
+      '#ajax' => array(
+        'callback' => 'search_api_admin_add_server_ajax_callback',
+        'wrapper' => 'search-api-class-options',
+      ),
+    );
+  }
+  elseif (!$class) {
+    $class = $form_state['storage']['step_one']['class'];
+  }
+
+  foreach (search_api_get_service_info() as $id => $info) {
+    if (empty($form_state['storage']['step_one'])) {
+      $form['class']['#options'][$id] = $info['name'];
+    }
+
+    if (!$class || $class != $id) {
+      continue;
+    }
+
+    $service = NULL;
+    if (class_exists($info['class'])) {
+      $service = new $info['class']($form_state['server']);
+    }
+    if (!($service instanceof SearchApiServiceInterface)) {
+      watchdog('search_api', t('Service class @id specifies an illegal class: @class', array('@id' => $id, '@class' => $info['class'])), NULL, WATCHDOG_ERROR);
+      continue;
+    }
+    $service_form = isset($form['options']['form']) ? $form['options']['form'] : array();
+    $service_form = $service->configurationForm($service_form, $form_state);
+    $form['options']['form'] = $service_form ? $service_form : array('#markup' => t('There are no configuration options for this service class.'));
+    $form['options']['class']['#type'] = 'value';
+    $form['options']['class']['#value'] = $class;
+    $form['options']['#type'] = 'fieldset';
+    $form['options']['#tree'] = TRUE;
+    $form['options']['#collapsible'] = TRUE;
+    $form['options']['#title'] = $info['name'];
+    $form['options']['#description'] = $info['description'];
+  }
+  $form['options']['#prefix'] = '<div id="search-api-class-options">';
+  $form['options']['#suffix'] = '</div>';
+
+  $form['submit'] = array(
+    '#type' => 'submit',
+    '#value' => t('Create server'),
+  );
+
+  return $form;
+}
+
+/**
+ * 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 handler for adding a server.
+ *
+ * Validates the machine name and calls the service class' validation handler.
+ */
+function search_api_admin_add_server_validate(array $form, array &$form_state) {
+  if (!empty($form_state['values']['machine_name'])) {
+    $name = $form_state['values']['machine_name'];
+    if (is_numeric($name)) {
+      form_set_error('machine_name', t('The machine name must not be a pure number.'));
+    }
+  }
+
+  if (empty($form_state['values']['options']['class'])) {
+    return;
+  }
+  $class = $form_state['values']['options']['class'];
+  $info = search_api_get_service_info($class);
+  $service = NULL;
+  if (class_exists($info['class'])) {
+    $service = new $info['class']($form_state['server']);
+  }
+  if (!($service instanceof SearchApiServiceInterface)) {
+    form_set_error('class', t('There seems to be something wrong with the selected service class.'));
+    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);
+}
+
+/**
+ * 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);
+  $values = $form_state['values'];
+
+  if (!empty($form_state['storage']['step_one'])) {
+    $values += $form_state['storage']['step_one'];
+    unset($form_state['storage']);
+  }
+
+  if (empty($values['options']) || ($values['class'] != $values['options']['class'])) {
+    unset($values['options']);
+    $form_state['storage']['step_one'] = $values;
+    $form_state['rebuild'] = TRUE;
+    drupal_set_message(t('Please configure the used service.'));
+    return;
+  }
+
+  $options = isset($values['options']['form']) ? $values['options']['form'] : array();
+  unset($values['options']);
+  $form_state['server']  = $server = entity_create('search_api_server', $values);
+  $server->configurationFormSubmit($form['options']['form'], $options, $form_state);
+  $server->save();
+  $form_state['redirect'] = 'admin/config/search/search_api/server/' . $server->machine_name;
+  drupal_set_message(t('The server was successfully created.'));
+}
+
+/**
+ * Title callback for viewing or editing a server or index.
+ */
+function search_api_admin_item_title($object) {
+  return $object->name;
+}
+
+/**
+ * Page callback: Displays information about a server.
+ *
+ * @param SearchApiServer $server
+ *   The server to display.
+ * @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)) {
+    if ($action == 'enable') {
+      if (isset($_GET['token']) && drupal_valid_token($_GET['token'], $server->machine_name)) {
+        if ($server->update(array('enabled' => 1))) {
+          drupal_set_message(t('The server was successfully enabled.'));
+        }
+        else {
+          drupal_set_message(t('The server could not be enabled. Check the logs for details.'), 'error');
+        }
+        drupal_goto('admin/config/search/search_api/server/' . $server->machine_name);
+      }
+      else {
+        return MENU_ACCESS_DENIED;
+      }
+    }
+    else {
+      $ret = drupal_get_form('search_api_admin_confirm', 'server', $action, $server);
+      if (!empty($ret['actions'])) {
+        return $ret;
+      }
+    }
+  }
+
+  drupal_set_title(search_api_admin_item_title($server));
+  $class = search_api_get_service_info($server->class);
+  $options = $server->viewSettings();
+  $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;
+}
+
+/**
+ * Returns HTML for displaying a server.
+ *
+ * @param array $variables
+ *   An associative array containing:
+ *   - id: The server's id.
+ *   - name: The server's name.
+ *   - 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) {
+  $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 = '';
+
+  if ($description) {
+    $output .= '<p class="description">' . nl2br(check_plain($description)) . '</p>';
+  }
+
+  // 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];
+
+  if ($enabled) {
+    $class = 'ok';
+    $info = t('enabled (!disable_link)', array('!disable_link' => l(t('disable'), 'admin/config/search/search_api/server/' . $machine_name . '/disable')));
+  }
+  else {
+    $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))))));
+  }
+  $label = t('Status');
+  $rows[] = _search_api_deep_copy($row);
+  $class = '';
+
+  $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 ($indexes) {
+    $label = t('Search indexes');
+    $info = render($indexes);
+    $rows[] = _search_api_deep_copy($row);
+  }
+
+  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);
+    }
+  }
+
+  $theme['rows'] = $rows;
+  $theme['attributes']['class'][] = 'search-api-summary';
+  $theme['attributes']['class'][] = 'search-api-server-summary';
+  $theme['attributes']['class'][] = 'system-status-report';
+  $output .= theme('table', $theme);
+
+  return $output;
+}
+
+/**
+ * Form constructor for completely clearing a server.
+ *
+ * @param SearchApiServer $server
+ *   The server for which the form is displayed.
+ *
+ * @ingroup forms
+ *
+ * @see search_api_server_status_form_submit()
+ */
+function search_api_server_status_form(array $form, array &$form_state, SearchApiServer $server) {
+  $form_state['server'] = $server;
+
+  $form['clear'] = array(
+    '#type' => 'submit',
+    '#value' => t('Delete all indexed data on this server'),
+  );
+
+  return $form;
+}
+
+/**
+* Form submission handler for search_api_server_status_form().
+*/
+function search_api_server_status_form_submit(array $form, array &$form_state) {
+  $server_id = $form_state['server']->machine_name;
+  $form_state['redirect'] = "admin/config/search/search_api/server/$server_id/clear";
+}
+
+/**
+ * Form constructor for editing a server's settings.
+ *
+ * @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;
+
+  $form['name'] = array(
+    '#type' => 'textfield',
+    '#title' => t('Server name'),
+    '#description' => t('Enter the displayed name for the  server.'),
+    '#maxlength' => 50,
+    '#default_value' => $server->name,
+    '#required' => TRUE,
+  );
+  $form['enabled'] = array(
+    '#type' => 'checkbox',
+    '#title' => t('Enabled'),
+    '#default_value' => $server->enabled,
+  );
+  $form['description'] = array(
+    '#type' => 'textarea',
+    '#title' => t('Server description'),
+    '#description' => t('Enter a description for the new server.'),
+    '#default_value' => $server->description,
+  );
+
+  $class = search_api_get_service_info($server->class);
+
+  $service_options = array();
+  $service_options = $server->configurationForm($service_options, $form_state);
+  if ($service_options) {
+    $form['options']['form'] = $service_options;
+  }
+  $form['options']['#type'] = 'fieldset';
+  $form['options']['#tree'] = TRUE;
+  $form['options']['#collapsible'] = TRUE;
+  $form['options']['#title'] = $class['name'];
+  $form['options']['#description'] = $class['description'];
+
+  $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;
+}
+
+/**
+ * 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);
+}
+
+/**
+ * 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);
+  $values = $form_state['values'];
+
+  $server = $form_state['server'];
+  if (isset($values['options'])) {
+    $server->configurationFormSubmit($form['options']['form'], $values['options']['form'], $form_state);
+  }
+  unset($values['options']);
+
+  $server->update($values);
+  $form_state['redirect'] = 'admin/config/search/search_api/server/' . $server->machine_name;
+  drupal_set_message(t('The search server was successfully edited.'));
+}
+
+/**
+ * Form submission handler for search_api_admin_server_edit().
+ *
+ * Handles the 'Delete' button on the server and index edit forms.
+ *
+ * @see search_api_admin_server_edit()
+ * @see search_api_admin_index_edit()
+ */
+function search_api_admin_form_delete_submit($form, &$form_state) {
+  $destination = array();
+  if (isset($_GET['destination'])) {
+    $destination = drupal_get_destination();
+    unset($_GET['destination']);
+  }
+  if (isset($form_state['server'])) {
+    $server = $form_state['server'];
+    $form_state['redirect'] = array('admin/config/search/search_api/server/' . $server->machine_name . '/delete', array('query' => $destination));
+  }
+  elseif (isset($form_state['index'])) {
+    $index = $form_state['index'];
+    $form_state['redirect'] = array('admin/config/search/search_api/index/' . $index->machine_name . '/delete', array('query' => $destination));
+  }
+}
+
+/**
+ * Form constructor for adding an index.
+ *
+ * @ingroup forms
+ *
+ * @see search_api_admin_add_index_validate()
+ * @see search_api_admin_add_index_submit()
+ */
+function search_api_admin_add_index(array $form, array &$form_state) {
+  drupal_set_title(t('Add index'));
+
+  $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',
+    ),
+  );
+
+  $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['enabled'] = array(
+    '#type' => 'checkbox',
+    '#title' => t('Enabled'),
+    '#description' => t('This will only take effect if the selected server is also enabled.'),
+    '#default_value' => TRUE,
+  );
+  $form['description'] = array(
+    '#type' => 'textarea',
+    '#title' => t('Index description'),
+  );
+  $form['server'] = array(
+    '#type' => 'select',
+    '#title' => t('Server'),
+    '#description' => t('Select the server this index should reside on.'),
+    '#default_value' => '',
+    '#options' => array('' => t('< No server >'))
+  );
+  $servers = search_api_server_load_multiple(FALSE, array('enabled' => 1));
+  // List enabled servers first.
+  foreach ($servers as $server) {
+    $form['server']['#options'][$server->machine_name] = $server->name;
+  }
+  $form['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')),
+  );
+
+  $form['submit'] = array(
+    '#type' => 'submit',
+    '#value' => t('Create index'),
+  );
+
+  return $form;
+}
+
+/**
+ * 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'];
+  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.'));
+  }
+}
+
+/**
+ * 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 index is enabled is done in the
+  // SearchApiIndex::save() method.
+  search_api_index_insert($values);
+
+  drupal_set_message(t('The index was successfully created. Please set up its indexed fields now.'));
+  $form_state['redirect'] = 'admin/config/search/search_api/index/' . $values['machine_name'] . '/fields';
+}
+
+/**
+ * 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, $action = NULL) {
+  if (!empty($action)) {
+    if ($action == 'enable') {
+      if (isset($_GET['token']) && drupal_valid_token($_GET['token'], $index->machine_name)) {
+        if ($index->update(array('enabled' => 1))) {
+          drupal_set_message(t('The index was successfully enabled.'));
+        }
+        else {
+          drupal_set_message(t('The index could not be enabled. Check the logs for details.'), 'error');
+        }
+        drupal_goto('admin/config/search/search_api/index/' . $index->machine_name);
+      }
+      else {
+        return MENU_ACCESS_DENIED;
+      }
+    }
+    else {
+      $ret = drupal_get_form('search_api_admin_confirm', 'index', $action, $index);
+      if (!empty($ret['actions'])) {
+        return $ret;
+      }
+    }
+  }
+
+  $status = search_api_index_status($index);
+  $ret['view'] = array(
+    '#theme' => 'search_api_index',
+    '#id' => $index->id,
+    '#name' => $index->name,
+    '#machine_name' => $index->machine_name,
+    '#description' => $index->description,
+    '#item_type' => $index->item_type,
+    '#enabled' => $index->enabled,
+    '#server' => $index->server(),
+    '#options' => $index->options,
+    '#fields' => $index->getFields(),
+    '#indexed_items' => $status['indexed'],
+    '#on_server' => _search_api_get_items_on_server($index),
+    '#total_items' => $status['total'],
+    '#status' => $index->status,
+    '#read_only' => $index->read_only,
+  );
+  if ($index->enabled && !$index->read_only) {
+    $ret['form'] = drupal_get_form('search_api_admin_index_status_form', $index, $status);
+  }
+  return $ret;
+}
+
+/**
+ * Returns HTML for a search index.
+ *
+ * @param array $variables
+ *   An associative array containing:
+ *   - id: The index's id.
+ *   - name: The index' name.
+ *   - machine_name: The index' machine name.
+ *   - description: The index' description.
+ *   - item_type: The type of items stored in this index.
+ *   - enabled: Boolean indicating whether the index is enabled.
+ *   - server: The server this index currently rests on, if any.
+ *   - options: The index' options, like cron limit.
+ *   - fields: All indexed fields of the index.
+ *   - indexed_items: The number of items already indexed in their latest
+ *     version on this index.
+ *   - on_server: The number of items actually indexed on the server.
+ *   - total_items: The total number of items that have to be indexed for this
+ *     index.
+ *   - status: The entity configuration status (in database, in code, etc.).
+ *   - read_only: Boolean indicating whether this index is read only.
+ *
+ * @return string
+ *   HTML for a search index.
+ *
+ * @ingroup themeable
+ */
+function theme_search_api_index(array $variables) {
+  $machine_name = $variables['machine_name'];
+  $description = $variables['description'];
+  $enabled = $variables['enabled'];
+  $item_type = $variables['item_type'];
+  $server = $variables['server'];
+  $options = $variables['options'];
+  $status = $variables['status'];
+  $indexed_items = $variables['indexed_items'];
+  $on_server = $variables['on_server'];
+  $total_items = $variables['total_items'];
+
+  // First, output the index description if there is one set.
+  $output = '';
+
+  if ($description) {
+    $output .= '<p class="description">' . nl2br(check_plain($description)) . '</p>';
+  }
+
+  // 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 {
+    $info = t('disabled');
+  }
+  $label = t('Status');
+  $rows[] = _search_api_deep_copy($row);
+  $class = '';
+
+  $label = t('Item type');
+  $type = search_api_get_item_type_info($item_type);
+  $item_type = !empty($type['name']) ? $type['name'] : $item_type;
+  $info = check_plain($item_type);
+  $rows[] = _search_api_deep_copy($row);
+
+  if ($server) {
+    $label = t('Server');
+    $info = l($server->name, 'admin/config/search/search_api/server/' . $server->machine_name);
+    $rows[] = _search_api_deep_copy($row);
+  }
+
+  if ($enabled) {
+    $options += array('cron_limit' => SEARCH_API_DEFAULT_CRON_LIMIT);
+    if ($options['cron_limit']) {
+      $class = 'ok';
+      $info = format_plural(
+        $options['cron_limit'],
+        'During cron runs, 1 item will be indexed per batch.',
+        'During cron runs, @count items will be indexed per batch.'
+      );
+    }
+    else {
+      $class = 'warning';
+      $info = t('No items will be indexed during cron runs.');
+    }
+    $label = t('Cron batch size');
+    $rows[] = _search_api_deep_copy($row);
+
+    $theme = array(
+      'percent' => (int) (100 * $indexed_items / $total_items),
+      'message' => t('@indexed/@total indexed', array('@indexed' => $indexed_items, '@total' => $total_items)),
+    );
+    $output .= '<h3>' . t('Index status') . '</h3>';
+    $output .= '<div class="search-api-index-status">' . theme('progress_bar', $theme) . '</div>';
+
+    $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. (<a href="@url">More information</a>)', 'There are @count items indexed on the server for this index. (<a href="@url">More information</a>)', $vars);
+    $class = '';
+    $label = t('Server index status');
+    $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);
+  }
+
+  $theme['rows'] = $rows;
+  $theme['attributes']['class'][] = 'search-api-summary';
+  $theme['attributes']['class'][] = 'search-api-index-summary';
+  $theme['attributes']['class'][] = 'system-status-report';
+  $output .= theme('table', $theme);
+
+  return $output;
+}
+
+/**
+ * Form constructor for an index status form.
+ *
+ * Should only be used for enabled indexes which aren't read-only.
+ *
+ * @param SearchApiIndex $index
+ *   The index whose status should be displayed.
+ * @param array $status
+ *   The indexing status of the index, as returned by search_api_index_status().
+ *
+ * @ingroup forms
+ *
+ * @see search_api_admin_index_status_form_validate()
+ * @see search_api_admin_index_status_form_submit()
+ */
+function search_api_admin_index_status_form(array $form, array &$form_state, SearchApiIndex $index, array $status) {
+  $form['#attached']['css'][] = drupal_get_path('module', 'search_api') . '/search_api.admin.css';
+  $form_state['index'] = $index;
+
+  $form['index'] = array(
+    '#type' => 'fieldset',
+    '#title' => t('Index now'),
+  );
+  $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;
+  }
+
+  $form['index']['button'] = array(
+    '#type' => 'submit',
+    '#value' => t('Index now'),
+    '#disabled' => !$allow_indexing,
+  );
+  $form['index']['total'] = array(
+    '#type' => 'value',
+    '#value' => $status['total'],
+  );
+  $form['index']['remaining'] = array(
+    '#type' => 'value',
+    '#value' => $status['total'] - $status['indexed'],
+  );
+  $form['index']['all'] = array(
+    '#type' => 'value',
+    '#value' => $all,
+  );
+
+  $form['reindex'] = array(
+    '#type' => 'submit',
+    '#value' => t('Queue all items for reindexing'),
+    '#prefix' => '<div>',
+    '#suffix' => '</div>',
+  );
+  $form['clear'] = array(
+    '#type' => 'submit',
+    '#value' => t('Clear all indexed data'),
+    '#prefix' => '<div>',
+    '#suffix' => '</div>',
+  );
+
+  return $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) {
+  $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;
+    }
+  }
+}
+
+/**
+ * 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) {
+  $values = $form_state['values'];
+  $index = $form_state['index'];
+  $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('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');
+      }
+      break;
+
+    case t('Queue all items for reindexing'):
+      $form_state['redirect'] .= '/reindex';
+      break;
+
+    case t('Clear all indexed data'):
+      $form_state['redirect'] .= '/clear';
+      break;
+  }
+}
+
+/**
+ * Form constructor for editing an index's settings.
+ *
+ * @param SearchApiIndex $index
+ *   The index to edit.
+ *
+ * @ingroup forms
+ *
+ * @see search_api_admin_index_edit_submit()
+ */
+function search_api_admin_index_edit(array $form, array &$form_state, SearchApiIndex $index) {
+  $form_state['index'] = $index;
+
+  $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,
+    '#default_value' => $index->name,
+    '#required' => 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),
+  );
+  $form['description'] = array(
+    '#type' => 'textarea',
+    '#title' => t('Index description'),
+    '#default_value' => $index->description,
+  );
+  $form['server'] = array(
+    '#type' => 'select',
+    '#title' => t('Server'),
+    '#description' => t('Select the server this index should reside on.'),
+    '#default_value' => $index->server,
+    '#options' => array('' => t('< No server >'))
+  );
+  $servers = search_api_server_load_multiple(FALSE, array('enabled' => 1));
+  // List enabled servers first.
+  foreach ($servers as $server) {
+    $form['server']['#options'][$server->machine_name] = $server->name;
+  }
+  $form['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' => $index->read_only,
+  );
+  $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' => !empty($index->options['index_directly']),
+    '#states' => array(
+      'invisible' => array(':input[name="read_only"]' => array('checked' => TRUE)),
+    ),
+  );
+  $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' => isset($index->options['cron_limit']) ? $index->options['cron_limit'] : SEARCH_API_DEFAULT_CRON_LIMIT,
+    '#size' => 4,
+    '#attributes' => array('class' => array('search-api-cron-limit')),
+    '#element_validate' => array('_element_validate_integer'),
+    '#states' => array(
+      'invisible' => array(':input[name="read_only"]' => array('checked' => TRUE)),
+    ),
+  );
+
+  $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;
+}
+
+/**
+ * Form submission handler for search_api_admin_index_edit().
+ */
+function search_api_admin_index_edit_submit(array $form, array &$form_state) {
+  form_state_values_clean($form_state);
+
+  $values = $form_state['values'];
+  $index = $form_state['index'];
+  $values['options'] += $index->options;
+
+  $ret = $index->update($values);
+  $form_state['redirect'] = 'admin/config/search/search_api/index/' . $index->machine_name;
+  if ($ret) {
+    drupal_set_message(t('The search index was successfully edited.'));
+  }
+  else {
+    drupal_set_message(t('No values were changed.'));
+  }
+}
+
+/**
+ * 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()
+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();
+  $options = empty($index->options) ? array() : $index->options;
+
+  $form_state['index'] = $index;
+  $form['#tree'] = TRUE;
+  $form['#attached']['js'][] = drupal_get_path('module', 'search_api') . '/search_api.admin.js';
+
+  // Callbacks
+
+  $callbacks = empty($options['data_alter_callbacks']) ? array() : $options['data_alter_callbacks'];
+  $callback_objects = isset($form_state['callbacks']) ? $form_state['callbacks'] : array();
+  foreach ($callback_info as $name => $callback) {
+    if (!isset($callbacks[$name])) {
+      $callbacks[$name]['status'] = 0;
+      $callbacks[$name]['weight'] = $callback['weight'];
+    }
+    $settings = empty($callbacks[$name]['settings']) ? array() : $callbacks[$name]['settings'];
+    if (empty($callback_objects[$name]) && class_exists($callback['class'])) {
+      $callback_objects[$name] = new $callback['class']($index, $settings);
+    }
+    if (!(class_exists($callback['class']) && $callback_objects[$name] instanceof SearchApiAlterCallbackInterface)) {
+      watchdog('search_api', t('Data alteration @id specifies illegal callback class @class.', array('@id' => $name, '@class' => $callback['class'])), NULL, WATCHDOG_WARNING);
+      unset($callback_info[$name]);
+      unset($callbacks[$name]);
+      unset($callback_objects[$name]);
+      continue;
+    }
+    if (!$callback_objects[$name]->supportsIndex($index)) {
+      unset($callback_info[$name]);
+      unset($callbacks[$name]);
+      unset($callback_objects[$name]);
+      continue;
+    }
+  }
+  $form_state['callbacks'] = $callback_objects;
+  $form['#callbacks'] = $callbacks;
+  $form['callbacks'] = array(
+    '#type' => 'fieldset',
+    '#title' => t('Data alterations'),
+    '#description' => t('Select the alterations that will be executed on indexed items, and their order.'),
+    '#collapsible' => TRUE,
+  );
+
+  // Callback status.
+  $form['callbacks']['status'] = array(
+    '#type' => 'item',
+    '#title' => t('Enabled data alterations'),
+    '#prefix' => '<div class="search-api-status-wrapper">',
+    '#suffix' => '</div>',
+  );
+  foreach ($callback_info as $name => $callback) {
+    $form['callbacks']['status'][$name] = array(
+      '#type' => 'checkbox',
+      '#title' => $callback['name'],
+      '#default_value' => $callbacks[$name]['status'],
+      '#parents' => array('callbacks', $name, 'status'),
+      '#description' => $callback['description'],
+      '#weight' => $callback['weight'],
+    );
+  }
+
+  // Callback order (tabledrag).
+  $form['callbacks']['order'] = array(
+    '#type' => 'item',
+    '#title' => t('Data alteration processing order'),
+    '#theme' => 'search_api_admin_item_order',
+    '#table_id' => 'search-api-callbacks-order-table',
+  );
+  foreach ($callback_info as $name => $callback) {
+    $form['callbacks']['order'][$name]['item'] = array(
+      '#markup' => $callback['name'],
+    );
+    $form['callbacks']['order'][$name]['weight'] = array(
+      '#type' => 'weight',
+      '#delta' => 50,
+      '#default_value' => $callbacks[$name]['weight'],
+      '#parents' => array('callbacks', $name, 'weight'),
+    );
+    $form['callbacks']['order'][$name]['#weight'] = $callbacks[$name]['weight'];
+  }
+
+  // Callback settings.
+  $form['callbacks']['settings_title'] = array(
+    '#type' => 'item',
+    '#title' => t('Callback settings'),
+  );
+  $form['callbacks']['settings'] = array(
+    '#type' => 'vertical_tabs',
+  );
+
+  foreach ($callback_info as $name => $callback) {
+    $settings_form = $callback_objects[$name]->configurationForm();
+    if (!empty($settings_form)) {
+      $form['callbacks']['settings'][$name] = array(
+        '#type' => 'fieldset',
+        '#title' => $callback['name'],
+        '#parents' => array('callbacks', $name, 'settings'),
+        '#weight' => $callback['weight'],
+      );
+      $form['callbacks']['settings'][$name] += $settings_form;
+    }
+  }
+
+  // Processors
+
+  $processors = empty($options['processors']) ? array() : $options['processors'];
+  $processor_objects = isset($form_state['processors']) ? $form_state['processors'] : array();
+  foreach ($processor_info as $name => $processor) {
+    if (!isset($processors[$name])) {
+      $processors[$name]['status'] = 0;
+      $processors[$name]['weight'] = $processor['weight'];
+    }
+    $settings = empty($processors[$name]['settings']) ? array() : $processors[$name]['settings'];
+    if (empty($processor_objects[$name]) && class_exists($processor['class'])) {
+      $processor_objects[$name] = new $processor['class']($index, $settings);
+    }
+    if (!(class_exists($processor['class']) && $processor_objects[$name] instanceof SearchApiProcessorInterface)) {
+      watchdog('search_api', t('Processor @id specifies illegal processor class @class.', array('@id' => $name, '@class' => $processor['class'])), NULL, WATCHDOG_WARNING);
+      unset($processor_info[$name]);
+      unset($processors[$name]);
+      unset($processor_objects[$name]);
+      continue;
+    }
+    if (!$processor_objects[$name]->supportsIndex($index)) {
+      unset($processor_info[$name]);
+      unset($processors[$name]);
+      unset($processor_objects[$name]);
+      continue;
+    }
+  }
+  $form_state['processors'] = $processor_objects;
+  $form['#processors'] = $processors;
+  $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.'),
+    '#collapsible' => TRUE,
+  );
+
+  // Processor status.
+  $form['processors']['status'] = array(
+    '#type' => 'item',
+    '#title' => t('Enabled processors'),
+    '#prefix' => '<div class="search-api-status-wrapper">',
+    '#suffix' => '</div>',
+  );
+  foreach ($processor_info as $name => $processor) {
+    $form['processors']['status'][$name] = array(
+      '#type' => 'checkbox',
+      '#title' => $processor['name'],
+      '#default_value' => $processors[$name]['status'],
+      '#parents' => array('processors', $name, 'status'),
+      '#description' => $processor['description'],
+      '#weight' => $processor['weight'],
+    );
+  }
+
+  // Processor order (tabledrag).
+  $form['processors']['order'] = array(
+    '#type' => 'item',
+    '#title' => t('Processor processing order'),
+    '#description' => t('Set the order in which preprocessing will be done at index and search time. ' .
+        'Postprocessing of search results will be in the exact opposite direction.'),
+    '#theme' => 'search_api_admin_item_order',
+    '#table_id' => 'search-api-processors-order-table',
+  );
+  foreach ($processor_info as $name => $processor) {
+    $form['processors']['order'][$name]['item'] = array(
+      '#markup' => $processor['name'],
+    );
+    $form['processors']['order'][$name]['weight'] = array(
+      '#type' => 'weight',
+      '#delta' => 50,
+      '#default_value' => $processors[$name]['weight'],
+      '#parents' => array('processors', $name, 'weight'),
+    );
+    $form['processors']['order'][$name]['#weight'] = $processors[$name]['weight'];
+  }
+
+  // Processor settings.
+  $form['processors']['settings_title'] = array(
+    '#type' => 'item',
+    '#title' => t('Processor settings'),
+  );
+  $form['processors']['settings'] = array(
+    '#type' => 'vertical_tabs',
+  );
+
+  foreach ($processor_info as $name => $processor) {
+    $settings_form = $processor_objects[$name]->configurationForm();
+    if (!empty($settings_form)) {
+      $form['processors']['settings'][$name] = array(
+        '#type' => 'fieldset',
+        '#title' => $processor['name'],
+        '#parents' => array('processors', $name, 'settings'),
+        '#weight' => $processor['weight'],
+      );
+      $form['processors']['settings'][$name] += $settings_form;
+    }
+  }
+
+  $form['actions'] = array('#type' => 'actions');
+  $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Save configuration'));
+
+  return $form;
+}
+
+/**
+ * Returns HTML for a processor/callback order form.
+ *
+ * @param array $variables
+ *   An associative array containing:
+ *   - element: A render element representing the form.
+ */
+function theme_search_api_admin_item_order(array $variables) {
+  $element = $variables['element'];
+
+  $rows = array();
+  foreach (element_children($element, TRUE) as $name) {
+    $element[$name]['weight']['#attributes']['class'][] = 'search-api-order-weight';
+    $rows[] = array(
+      'data' => array(
+        drupal_render($element[$name]['item']),
+        drupal_render($element[$name]['weight']),
+      ),
+      'class' => array('draggable'),
+    );
+  }
+  $output = drupal_render_children($element);
+  $output .= theme('table', array('rows' => $rows, 'attributes' => array('id' => $element['#table_id'])));
+  drupal_add_tabledrag($element['#table_id'], 'order', 'sibling', 'search-api-order-weight', NULL, NULL, TRUE);
+
+  return $output;
+}
+
+/**
+ * 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.
+  foreach ($form_state['callbacks'] as $name => $callback) {
+    if (isset($form['callbacks']['settings'][$name]) && isset($form_state['values']['callbacks'][$name]['settings'])) {
+      $callback->configurationFormValidate($form['callbacks']['settings'][$name], $form_state['values']['callbacks'][$name]['settings'], $form_state);
+    }
+  }
+  foreach ($form_state['processors'] as $name => $processor) {
+    if (isset($form['processors']['settings'][$name]) && isset($form_state['values']['processors'][$name]['settings'])) {
+      $processor->configurationFormValidate($form['processors']['settings'][$name], $form_state['values']['processors'][$name]['settings'], $form_state);
+    }
+  }
+}
+
+/**
+ * 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'];
+
+  $options = empty($index->options) ? array() : $index->options;
+
+  // Store callback and processor settings.
+  foreach ($form_state['callbacks'] as $name => $callback) {
+    $callback_form = isset($form['callbacks']['settings'][$name]) ? $form['callbacks']['settings'][$name] : array();
+    $values['callbacks'][$name] += array('settings' => array());
+    $values['callbacks'][$name]['settings'] = $callback->configurationFormSubmit($callback_form, $values['callbacks'][$name]['settings'], $form_state);
+  }
+  foreach ($form_state['processors'] as $name => $processor) {
+    $processor_form = isset($form['processors']['settings'][$name]) ? $form['processors']['settings'][$name] : array();
+    $values['processors'][$name] += array('settings' => array());
+    $values['processors'][$name]['settings'] = $processor->configurationFormSubmit($processor_form, $values['processors'][$name]['settings'], $form_state);
+  }
+
+  $types = search_api_field_types();
+  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'])) {
+      if ($values['callbacks'][$name]['status']) {
+        // Callback was just enabled, add its fields.
+        $properties = $callback->propertyInfo();
+        if ($properties) {
+          foreach ($properties as $key => $field) {
+            $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 alteration.
+              continue;
+            }
+            if ($inner == 'token' || (search_api_is_text_type($inner) && !empty($field['options list']))) {
+              $old = $type;
+              $type = 'string';
+              while (search_api_is_list_type($old)) {
+                $old = substr($old, 5, -1);
+                $type = "list<$type>";
+              }
+            }
+            $index->options['fields'][$key] = array(
+              'type' => $type,
+            );
+          }
+        }
+      }
+      else {
+        // Callback was just disabled, remove its fields.
+        $properties = $callback->propertyInfo();
+        if ($properties) {
+          foreach ($properties as $key => $field) {
+            unset($index->options['fields'][$key]);
+          }
+        }
+
+      }
+    }
+  }
+
+  if (!isset($options['data_alter_callbacks']) || !isset($options['processors'])
+      || $options['data_alter_callbacks'] != $values['callbacks']
+      || $options['processors'] != $values['processors']) {
+    $index->options['data_alter_callbacks'] = $values['callbacks'];
+    $index->options['processors'] = $values['processors'];
+
+    // Save the already sorted arrays to avoid having to sort them at each use.
+    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();
+
+    $index->save();
+    $index->reindex();
+    drupal_set_message(t("The indexing workflow was successfully edited. All content was scheduled for re-indexing so the new settings can take effect."));
+  }
+  else {
+    drupal_set_message(t('No values were changed.'));
+  }
+
+  $form_state['redirect'] = 'admin/config/search/search_api/index/' . $index->machine_name . '/workflow';
+}
+
+/**
+ * Sort callback sorting array elements by their "weight" key, if present.
+ *
+ * @see element_sort()
+ */
+function search_api_admin_element_compare($a, $b) {
+  $a_weight = (is_array($a) && isset($a['weight'])) ? $a['weight'] : 0;
+  $b_weight = (is_array($b) && isset($b['weight'])) ? $b['weight'] : 0;
+  if ($a_weight == $b_weight) {
+    return 0;
+  }
+  return ($a_weight < $b_weight) ? -1 : 1;
+}
+
+/**
+ * Form constructor for setting the indexed fields.
+ *
+ * @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);
+  $fields = $options['fields'];
+  $additional = $options['additional fields'];
+
+  // An array of option arrays for types, keyed by nesting level.
+  $types = array(0 => search_api_field_types());
+  $entity_types = entity_get_info();
+  //$boosts = drupal_map_assoc(array('0.1', '0.2', '0.3', '0.5', '0.8', '1.0', '2.0', '3.0', '5.0', '8.0', '13.0', '21.0'));
+  $boosts = drupal_map_assoc(array('0.1', '0.2', '0.3', '0.5', '0.8', '1.0', '2.0', '3.0', '5.0', '8.0', '13.0', '21.0', '100', '1000', '1010', '1020', '1030', '1040', '1050', '1060'));
+   
+  $fulltext_types = array(0 => array('text'));
+  // Add all custom data types with fallback "text" to fulltext types as well.
+  foreach (search_api_get_data_type_info() as $id => $type) {
+    if ($type['fallback'] != 'text') {
+      continue;
+    }
+    $fulltext_types[0][] = $id;
+  }
+
+  $form_state['index'] = $index;
+  $form['#theme'] = 'search_api_admin_fields_table';
+  $form['#tree'] = TRUE;
+  $form['description'] = array(
+    '#type' => 'item',
+    '#title' => t('Select fields to index'),
+    '#description' => t('<p>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 <sup>1</sup>) 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.</p>' .
+        '<p>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.</p>'),
+  );
+  if ($index->server) {
+    $form['description']['#description'] .= '<p>' . t('Check the <a href="@server-url">' . "server's</a> service class description for details.",
+        array('@server-url' => url('admin/config/search/search_api/server/' . $index->server))) . '</p>';
+  }
+  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'] .= ' <sup><a href="#note-multi-valued" class="note-ref">1</a></sup>';
+      $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',
+        '#value' => $info['description'],
+      );
+    }
+    $form['fields'][$key]['indexed'] = array(
+      '#type' => 'checkbox',
+      '#default_value' => $info['indexed'],
+    );
+    if (empty($info['entity_type'])) {
+      // 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.
+          $types[$level][$type_prefix . $type . $type_suffix] = $name;
+        }
+        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(
+        '#type' => 'select',
+        '#options' => $types[$level],
+        '#default_value' => isset($info['real_type']) ? $info['real_type'] : $info['type'],
+        '#states' => array(
+          'visible' => array(
+            $css_key . '-indexed' => array('checked' => TRUE),
+          ),
+        ),
+      );
+      $form['fields'][$key]['boost'] = array(
+        '#type' => 'select',
+        '#options' => $boosts,
+        '#default_value' => $info['boost'],
+        '#states' => array(
+          'visible' => array(
+            $css_key . '-indexed' => array('checked' => TRUE),
+          ),
+        ),
+      );
+      // 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.
+      $label = $entity_types[$info['entity_type']]['label'];
+      if (!isset($entity_description_added)) {
+        $form['description']['#description'] .= '<p>' .
+            t('Note that indexing an entity-valued field (like %field, which has type %type) directly will only index the entity ID. ' .
+            'This will be used for filtering and also sorting (which might not be what you expect). ' .
+            'The entity label will usually be used when displaying the field, though. ' .
+            'Use the "Add related fields" option at the bottom for indexing other fields of related entities.',
+            array('%field' => $info['name'], '%type' => $label)) . '</p>';
+        $entity_description_added = TRUE;
+      }
+      $form['fields'][$key]['type'] = array(
+        '#type' => 'value',
+        '#value' => $info['type'],
+      );
+      $form['fields'][$key]['entity_type'] = array(
+        '#type' => 'value',
+        '#value' => $info['entity_type'],
+      );
+      $form['fields'][$key]['type_name'] = array(
+        '#markup' => check_plain($label),
+      );
+      $form['fields'][$key]['boost'] = array(
+        '#type' => 'value',
+        '#value' => $info['boost'],
+      );
+      $form['fields'][$key]['boost_text'] = array(
+        '#markup' => '&nbsp;',
+      );
+    }
+    if ($key == 'search_api_language') {
+      // Is treated specially to always index the language.
+      $form['fields'][$key]['type']['#default_value'] = 'string';
+      $form['fields'][$key]['type']['#disabled'] = TRUE;
+      $form['fields'][$key]['boost']['#default_value'] = '1.0';
+      $form['fields'][$key]['boost']['#disabled'] = TRUE;
+      $form['fields'][$key]['indexed']['#default_value'] = 1;
+      $form['fields'][$key]['indexed']['#disabled'] = TRUE;
+    }
+  }
+
+  if (!empty($multi_valued_field_present)) {
+    $form['note']['#markup'] = '<div id="note-multi-valued"><small><sup>1</sup> ' . t('Multi-valued field') . '</small></div>';
+  }
+
+  $form['submit'] = array(
+    '#type' => 'submit',
+    '#value' => t('Save changes'),
+  );
+
+  if ($additional) {
+    reset($additional);
+    $form['additional'] = array(
+      '#type' => 'fieldset',
+      '#title' => t('Add related fields'),
+      '#description' => t('There are entities related to entities of this type. ' .
+          'You can add their fields to the list above so they can be indexed too.') . '<br />',
+      '#collapsible' => TRUE,
+      '#collapsed' => TRUE,
+      '#attributes' => array('class' => array('container-inline')),
+      'field' => array(
+        '#type' => 'select',
+        '#options' => $additional,
+        '#default_value' => key($additional),
+      ),
+      'add' => array(
+        '#type' => 'submit',
+        '#value' => t('Add fields'),
+      ),
+    );
+  }
+
+  return $form;
+}
+
+/**
+ * Helper function for building the field list for an index.
+ *
+ * @deprecated Use SearchApiIndex::getFields() instead.
+ */
+function _search_api_admin_get_fields(SearchApiIndex $index, EntityMetadataWrapper $wrapper) {
+  $fields = empty($index->options['fields']) ? array() : $index->options['fields'];
+  $additional = array();
+  $entity_types = entity_get_info();
+
+  // First we need all already added prefixes.
+  $added = array();
+  foreach (array_keys($fields) as $key) {
+    $key = substr($key, 0, strrpos($key, ':'));
+    $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.
+
+  // 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])) {
+          if (isset($fields[$key])) {
+            // This field is already known in the index configuration.
+            $fields[$key]['name'] = $prefix_name . $info['label'];
+            $fields[$key]['description'] = empty($info['description']) ? NULL : $info['description'];
+            $flat[$key] = $fields[$key];
+            // Update its type.
+            if (isset($entity_types[$type])) {
+              // Always enforce the proper entity type.
+              $flat[$key]['type'] = $info['type'];
+            }
+            else {
+              // Else, only update the nesting level.
+              $set_type = search_api_extract_inner_type(isset($flat[$key]['real_type']) ? $flat[$key]['real_type'] : $flat[$key]['type']);
+              $flat[$key]['type'] = $info['type'];
+              $flat[$key]['real_type'] = search_api_nest_type($set_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 (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]);
+    }
+  }
+
+  $options = array();
+  $options['fields'] = $flat;
+  $options['additional fields'] = $additional;
+  return $options;
+}
+
+/**
+ * Returns HTML for a field list form.
+ *
+ * @param array $variables
+ *   An associative array containing:
+ *   - element: A render element representing the form.
+ */
+function theme_search_api_admin_fields_table($variables) {
+  $form = $variables['element'];
+  $header = array(t('Field'), t('Machine name'), t('Indexed'), t('Type'), t('Boost'));
+
+  $rows = array();
+  foreach (element_children($form['fields']) as $name) {
+    $row = array();
+    foreach (element_children($form['fields'][$name]) as $field) {
+      if ($cell = render($form['fields'][$name][$field])) {
+        $row[] = $cell;
+      }
+    }
+    if (empty($form['fields'][$name]['description']['#value'])) {
+      $rows[] = _search_api_deep_copy($row);
+    }
+    else {
+      $rows[] = array(
+        'data' => $row,
+        'title' => strip_tags($form['fields'][$name]['description']['#value']),
+      );
+    }
+  }
+
+  $note = isset($form['note']) ? $form['note'] : '';
+  $submit = $form['submit'];
+  $additional = isset($form['additional']) ? $form['additional'] : FALSE;
+  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);
+  }
+
+  return $output;
+}
+
+/**
+ * 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();
+  if ($form_state['values']['op'] == t('Save changes')) {
+    $fields = $form_state['values']['fields'];
+    $default_types = search_api_default_field_types();
+    $custom_types = search_api_get_data_type_info();
+    foreach ($fields as $name => $field) {
+      if (empty($field['indexed'])) {
+        unset($fields[$name]);
+      }
+      else {
+        // Don't store the description. "indexed" is implied.
+        unset($fields[$name]['description'], $fields[$name]['indexed']);
+        // For non-default types, set type to the fallback and only real_type to
+        // the custom type.
+        $inner_type = search_api_extract_inner_type($field['type']);
+        if (!isset($default_types[$inner_type])) {
+          $fields[$name]['real_type'] = $field['type'];
+          $fields[$name]['type'] = search_api_nest_type($custom_types[$inner_type]['fallback'], $field['type']);
+        }
+        // Boost defaults to 1.0.
+        if ($field['boost'] == '1.0') {
+          unset($fields[$name]['boost']);
+        }
+      }
+    }
+    $options['fields'] = $fields;
+    unset($options['additional fields']);
+    $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.'));
+    }
+    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';
+    }
+    else {
+      drupal_set_message(t('Please set up the indexing workflow.'));
+      $form_state['redirect'] = 'admin/config/search/search_api/index/' . $index->machine_name . '/workflow';
+    }
+    return;
+  }
+  // Adding a related entity's fields.
+  $prefix = $form_state['values']['additional']['field'];
+  $options['additional fields'][$prefix] = $prefix;
+  $ret = $index->update(array('options' => $options));
+
+  if ($ret) {
+    drupal_set_message(t('The available fields were successfully changed.'));
+  }
+  else {
+    drupal_set_message(t('No values were changed.'));
+  }
+  $form_state['redirect'] = 'admin/config/search/search_api/index/' . $index->machine_name . '/fields';
+}
+
+/**
+ * Form constructor for a generic confirmation form.
+ *
+ * @param $type
+ *   The type of entity (not the real "entity type"). Either "server" or
+ *   "index".
+ * @param $action
+ *   The action that would be executed for this entity after confirming. 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.
+ */
+function search_api_admin_confirm(array $form, array &$form_state, $type, $action, Entity $entity) {
+  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. <strong>Use with caution!</strong>'),
+            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 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(
+              t('Revert server @name', array('@name' => $entity->name)),
+              t('Do you really want to revert this server?'),
+              t('This will revert all settings for this server back to the defaults. This action cannot be undone.'),
+              t('The server settings have been successfully reverted.'),
+            );
+          }
+          else {
+            $text = array(
+              t('Delete server @name', array('@name' => $entity->name)),
+              t('Do you really want to delete this server?'),
+              t('This will delete the server and disable all associated indexes. ' .
+                  "Searches on these indexes won't be available until they are moved to another server and re-enabled."),
+              t('The server was successfully deleted.'),
+            );
+          }
+          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)),
+            t('Do you really want to disable this index?'),
+            t("Searches on this index won't be available until it is re-enabled."),
+            t('The index was successfully disabled.'),
+          );
+          break;
+
+        case 'delete':
+          if ($entity->hasStatus(ENTITY_OVERRIDDEN)) {
+            $text = array(
+              t('Revert index @name', array('@name' => $entity->name)),
+              t('Do you really want to revert this index?'),
+              t('This will revert all settings on this index back to the defaults. This action cannot be undone.'),
+              t('The index settings have been successfully reverted.'),
+            );
+          }
+          else {
+            $text = array(
+              t('Delete index @name', array('@name' => $entity->name)),
+              t('Do you really want to delete this index?'),
+              t('This will remove the index from the server and delete all settings. ' .
+                  'All data on this index will be lost.'),
+              t('The index has been successfully deleted.'),
+            );
+          }
+          break;
+
+        default:
+          return FALSE;
+      }
+      break;
+    default:
+      return FALSE;
+  }
+
+  $form = array(
+    'type' => array(
+      '#type' => 'value',
+      '#value' => $type,
+    ),
+    'action' => array(
+      '#type' => 'value',
+      '#value' => $action,
+    ),
+    'id' => array(
+      '#type' => 'value',
+      '#value' => $entity->machine_name,
+    ),
+    'message' => array(
+      '#type' => 'value',
+      '#value' => $text[3],
+    ),
+  );
+  $desc = "<h3>{$text[1]}</h3><p>{$text[2]}</p>";
+  return confirm_form($form, $text[0], "admin/config/search/search_api/$type/{$entity->machine_name}", $desc);
+}
+
+/**
+ * Submit function for search_api_admin_confirm().
+ */
+function search_api_admin_confirm_submit(array $form, array &$form_state) {
+  $values = $form_state['values'];
+
+  $type = $values['type'];
+  $action = $values['action'];
+  $id = $values['id'];
+
+  $function = "search_api_{$type}_{$action}";
+  if ($function($id)) {
+    drupal_set_message($values['message']);
+  }
+  else {
+    drupal_set_message(t('An error has occurred while performing the desired action. Check the logs for details.'), 'error');
+  }
+
+  $form_state['redirect'] = $action == 'delete'
+      ? "admin/config/search/search_api"
+      : "admin/config/search/search_api/$type/$id";
+}

+ 207 - 0
sites/all/modules/contrib/search/search_api/search_api.admin.js

@@ -0,0 +1,207 @@
+/**
+ * @file
+ * Javascript enhancements for the Search API admin pages.
+ */
+
+(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 () {
+      var $checkbox = $(this);
+      // Retrieve the tabledrag row belonging to this processor.
+      var $row = $('#' + $checkbox.attr('id').replace(/-status$/, '-weight'), context).closest('tr');
+      // Retrieve the vertical tab belonging to this processor.
+      var $tab = $('#' + $checkbox.attr('id').replace(/-status$/, '-settings'), context).data('verticalTab');
+
+      // Bind click handler to this checkbox to conditionally show and hide the
+      // filter's tableDrag row and vertical tab pane.
+      $checkbox.bind('click.searchApiUpdate', function () {
+        if ($checkbox.is(':checked')) {
+          $row.show();
+          if ($tab) {
+            $tab.tabShow().updateSummary();
+          }
+        }
+        else {
+          $row.hide();
+          if ($tab) {
+            $tab.tabHide().updateSummary();
+          }
+        }
+        // Restripe table after toggling visibility of table row.
+        Drupal.tableDrag['search-api-' + $checkbox.attr('id').replace(/^edit-([^-]+)-.*$/, '$1') + '-order-table'].restripeTable();
+      });
+
+      // Attach summary for configurable items (only for screen-readers).
+      if ($tab) {
+        $tab.fieldset.drupalSetSummary(function (tabContext) {
+          return $checkbox.is(':checked') ? Drupal.t('Enabled') : Drupal.t('Disabled');
+        });
+      }
+
+      // Trigger our bound click handler to update elements to initial state.
+      $checkbox.triggerHandler('click.searchApiUpdate');
+    });
+  }
+};
+
+/**
+ * Processes elements with the .dropbutton class on page load.
+ */
+Drupal.behaviors.searchApiDropButton = {
+  attach: function (context, settings) {
+    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));
+      }
+      // 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 '<li class="dropbutton-toggle"><button type="button" role="button"><span class="dropbutton-arrow"><span class="visually-hidden">' + options.title + '</span></span></button></li>';
+  }
+});
+
+// Expose constructor in the public space.
+Drupal.DropButton = DropButton;
+
+})(jQuery);

+ 591 - 0
sites/all/modules/contrib/search/search_api/search_api.api.php

@@ -0,0 +1,591 @@
+<?php
+
+/**
+ * @file
+ * Hooks provided by the Search API module.
+ */
+
+/**
+ * @addtogroup hooks
+ * @{
+ */
+
+/**
+ * Defines one or more search service classes a module offers.
+ *
+ * Note: The ids should be valid PHP identifiers.
+ *
+ * @return array
+ *   An associative array of search service classes, keyed by a unique
+ *   identifier and containing associative arrays with the following keys:
+ *   - name: The service class' translated name.
+ *   - 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
+ *     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(
+    'name' => t('Some Service'),
+    'description' => t('Service for some search engine.'),
+    'class' => 'SomeServiceClass',
+    // Unknown keys can later be read by the object for additional information.
+    'init args' => array('foo' => 'Foo', 'bar' => 42),
+  );
+  $services['example_other'] = array(
+    'name' => t('Other Service'),
+    'description' => t('Service for another search engine.'),
+    'class' => 'OtherServiceClass',
+  );
+
+  return $services;
+}
+
+/**
+ * Alter the Search API service info.
+ *
+ * Modules may implement this hook to alter the information that defines Search
+ * 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) {
+    $service_info[$id]['class'] = 'MyProxyServiceClass';
+    $service_info[$id]['example_original_class'] = $info['class'];
+  }
+}
+
+/**
+ * Define new types of items that can be searched.
+ *
+ * This hook allows modules to define their own item types, for which indexes
+ * can then be created. (Note that the Search API natively provides support for
+ * all entity types that specify property information, so they should not be
+ * added here. You should therefore also not use an existing entity type as the
+ * identifier of a new item type.)
+ *
+ * The main part of defining a new item type is implementing its data source
+ * controller class, which is responsible for loading items, providing metadata
+ * and tracking existing items. The module defining a certain item type is also
+ * responsible for observing creations, updates and deletions of items of that
+ * type and notifying the Search API of them by calling
+ * search_api_track_item_insert(), search_api_track_item_change() and
+ * search_api_track_item_delete(), as appropriate.
+ * The only other restriction for item types is that they have to have a single
+ * item ID field, with a scalar value. This is, e.g., used to track indexed
+ * items.
+ *
+ * Note, however, that you can also define item types where some of these
+ * conditions are not met, as long as you are aware that some functionality of
+ * the Search API and related modules might then not be available for that type.
+ *
+ * @return array
+ *   An associative array keyed by item type identifier, and containing type
+ *   information arrays with at least the following keys:
+ *   - name: A human-readable name for the type.
+ *   - 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.
+ *
+ * @see hook_search_api_item_type_info_alter()
+ */
+function hook_search_api_item_type_info() {
+  // Copied from search_api_search_api_item_type_info().
+  $types = array();
+
+  foreach (entity_get_property_info() as $type => $property_info) {
+    if ($info = entity_get_info($type)) {
+      $types[$type] = array(
+        'name' => $info['label'],
+        'datasource controller' => 'SearchApiEntityDataSourceController',
+        'entity_type' => $type,
+      );
+    }
+  }
+
+  return $types;
+}
+
+/**
+ * Alter the 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, 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.
+ *
+ * @see hook_search_api_item_type_info()
+ */
+function hook_search_api_item_type_info_alter(array &$infos) {
+  // Adds a boolean value is_entity to all type options telling whether the item
+  // type represents an entity type.
+  foreach ($infos as $type => $info) {
+    $info['is_entity'] = (bool) entity_get_info($type);
+  }
+}
+
+/**
+ * Define new data types for indexed properties.
+ *
+ * New data types will appear as new option for the „Type“ field on indexes'
+ * „Fields“ tabs. Whether choosing a custom data type will have any effect
+ * depends on the server on which the data is indexed.
+ *
+ * @return array
+ *   An array containing custom data type definitions, keyed by their type
+ *   identifier and containing the following keys:
+ *   - name: The human-readable name of the type.
+ *   - fallback: (optional) One of the default data types (the keys from
+ *     search_api_default_field_types()) which should be used as a fallback if
+ *     the server doesn't support this data type. Defaults to "string".
+ *   - conversion callback: (optional) If specified, a callback function for
+ *     converting raw values to the given type, if possible. For the contract
+ *     of such a callback, see example_data_type_conversion().
+ *
+ * @see hook_search_api_data_type_info_alter()
+ * @see search_api_get_data_type_info()
+ * @see example_data_type_conversion()
+ */
+function hook_search_api_data_type_info() {
+  return array(
+    'example_type' => array(
+      'name' => t('Example type'),
+      // Could be omitted, as "string" is the default.
+      'fallback' => 'string',
+      'conversion callback' => 'example_data_type_conversion',
+    ),
+  );
+}
+
+/**
+ * Alter the data type info.
+ *
+ * Modules may implement this hook to alter the information that defines a data
+ * type, or to add/remove some entirely. All properties that are available in
+ * hook_search_api_data_type_info() can be altered here.
+ *
+ * @param array $infos
+ *   The data type info array, keyed by type identifier.
+ *
+ * @see hook_search_api_data_type_info()
+ */
+function hook_search_api_data_type_info_alter(array &$infos) {
+  $infos['example_type']['name'] .= ' 2';
+}
+
+/**
+ * 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.
+ *
+ * Data-alter callbacks (which are called "Data alterations" in the UI) are
+ * classes implementing the SearchApiAlterCallbackInterface interface.
+ *
+ * @see SearchApiAlterCallbackInterface
+ *
+ * @return array
+ *   An associative array keyed by the callback IDs and containing arrays with
+ *   the following keys:
+ *   - name: The name to display for this callback.
+ *   - description: A short description of what the callback does.
+ *   - class: The callback class.
+ *   - weight: (optional) Defines the order in which callbacks are displayed
+ *     (and, therefore, invoked) by default. Defaults to 0.
+ */
+function hook_search_api_alter_callback_info() {
+  $callbacks['example_random_alter'] = array(
+    'name' => t('Random alteration'),
+    'description' => t('Alters all passed item data completely randomly.'),
+    'class' => 'ExampleRandomAlter',
+    'weight' => 100,
+  );
+  $callbacks['example_add_comments'] = array(
+    'name' => t('Add comments'),
+    'description' => t('For nodes and similar entities, adds comments.'),
+    'class' => 'ExampleAddComments',
+  );
+
+  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
+ * time to pre-process item data or the search query, and at search time to
+ * post-process the returned search results.
+ *
+ * @see SearchApiProcessorInterface
+ *
+ * @return array
+ *   An associative array keyed by the processor id and containing arrays
+ *   with the following keys:
+ *   - name: The name to display for this processor.
+ *   - description: A short description of what the processor does at each
+ *     phase.
+ *   - class: The processor class, which has to implement the
+ *     SearchApiProcessorInterface interface.
+ *   - weight: (optional) Defines the order in which processors are displayed
+ *     (and, therefore, invoked) by default. Defaults to 0.
+ */
+function hook_search_api_processor_info() {
+  $callbacks['example_processor'] = array(
+    'name' => t('Example processor'),
+    'description' => t('Pre- and post-processes data in really cool ways.'),
+    'class' => 'ExampleSearchApiProcessor',
+    'weight' => -1,
+  );
+  $callbacks['example_processor_minimal'] = array(
+    'name' => t('Example processor 2'),
+    'description' => t('Processor with minimal description.'),
+    'class' => 'ExampleSearchApiProcessor2',
+  );
+
+  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.
+ *
+ * Please be aware that generally preventing the indexing of certain items is
+ * deprecated. This is better done with data alterations, which can easily be
+ * configured and only added to indexes where this behaviour is wanted.
+ * If your module will use this hook to reject certain items from indexing,
+ * please document this clearly to avoid confusion.
+ *
+ * @param array $items
+ *   The entities that will be indexed (before calling any data alterations).
+ * @param SearchApiIndex $index
+ *   The search index on which items will be indexed.
+ */
+function hook_search_api_index_items_alter(array &$items, SearchApiIndex $index) {
+  foreach ($items as $id => $item) {
+    if ($id % 5 == 0) {
+      unset($items[$id]);
+    }
+  }
+  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.
+ */
+function hook_search_api_query_alter(SearchApiQueryInterface $query) {
+  // Exclude entities with ID 0. (Assume the ID field is always indexed.)
+  if ($query->getIndex()->getEntityType()) {
+    $info = entity_get_info($query->getIndex()->getEntityType());
+    $query->condition($info['entity keys']['id'], 0, '!=');
+  }
+}
+
+/**
+ * Act on search servers when they are loaded.
+ *
+ * @param array $servers
+ *   An array of loaded SearchApiServer objects.
+ */
+function hook_search_api_server_load(array $servers) {
+  foreach ($servers as $server) {
+    db_insert('example_search_server_access')
+      ->fields(array(
+        'server' => $server->machine_name,
+        'access_time' => REQUEST_TIME,
+      ))
+      ->execute();
+  }
+}
+
+/**
+ * A new search server was created.
+ *
+ * @param SearchApiServer $server
+ *   The new server.
+ */
+function hook_search_api_server_insert(SearchApiServer $server) {
+  db_insert('example_search_server')
+    ->fields(array(
+      'server' => $server->machine_name,
+      'insert_time' => REQUEST_TIME,
+    ))
+    ->execute();
+}
+
+/**
+ * A search server was edited, enabled or disabled.
+ *
+ * @param SearchApiServer $server
+ *   The edited server.
+ */
+function hook_search_api_server_update(SearchApiServer $server) {
+  if ($server->name != $server->original->name) {
+    db_insert('example_search_server_name_update')
+      ->fields(array(
+        'server' => $server->machine_name,
+        'update_time' => REQUEST_TIME,
+      ))
+      ->execute();
+  }
+}
+
+/**
+ * A search server was deleted.
+ *
+ * @param SearchApiServer $server
+ *   The deleted server.
+ */
+function hook_search_api_server_delete(SearchApiServer $server) {
+  db_insert('example_search_server_update')
+    ->fields(array(
+      'server' => $server->machine_name,
+      'update_time' => REQUEST_TIME,
+    ))
+    ->execute();
+  db_delete('example_search_server')
+    ->condition('server', $server->machine_name)
+    ->execute();
+}
+
+/**
+* Define default search servers.
+*
+* @return array
+*   An array of default search servers, keyed by machine names.
+*
+* @see hook_default_search_api_server_alter()
+*/
+function hook_default_search_api_server() {
+  $defaults['main'] = entity_create('search_api_server', array(
+    'name' => 'Main server',
+    'machine_name' => 'main',// Must be same as the used array key.
+    // Other properties ...
+  ));
+  return $defaults;
+}
+
+/**
+* Alter default search servers.
+*
+* @param array $defaults
+*   An array of default search servers, keyed by machine names.
+*
+* @see hook_default_search_api_server()
+*/
+function hook_default_search_api_server_alter(array &$defaults) {
+  $defaults['main']->name = 'Customized main server';
+}
+
+/**
+ * Act on search indexes when they are loaded.
+ *
+ * @param array $indexes
+ *   An array of loaded SearchApiIndex objects.
+ */
+function hook_search_api_index_load(array $indexes) {
+  foreach ($indexes as $index) {
+    db_insert('example_search_index_access')
+      ->fields(array(
+        'index' => $index->machine_name,
+        'access_time' => REQUEST_TIME,
+      ))
+      ->execute();
+  }
+}
+
+/**
+ * A new search index was created.
+ *
+ * @param SearchApiIndex $index
+ *   The new index.
+ */
+function hook_search_api_index_insert(SearchApiIndex $index) {
+  db_insert('example_search_index')
+    ->fields(array(
+      'index' => $index->machine_name,
+      'insert_time' => REQUEST_TIME,
+    ))
+    ->execute();
+}
+
+/**
+ * A search index was edited in any way.
+ *
+ * @param SearchApiIndex $index
+ *   The edited index.
+ */
+function hook_search_api_index_update(SearchApiIndex $index) {
+  if ($index->name != $index->original->name) {
+    db_insert('example_search_index_name_update')
+      ->fields(array(
+        'index' => $index->machine_name,
+        'update_time' => REQUEST_TIME,
+      ))
+      ->execute();
+  }
+}
+
+/**
+ * A search index was scheduled for reindexing
+ *
+ * @param SearchApiIndex $index
+ *   The edited index.
+ * @param $clear
+ *   Boolean indicating whether the index was also cleared.
+ */
+function hook_search_api_index_reindex(SearchApiIndex $index, $clear = FALSE) {
+  db_insert('example_search_index_reindexed')
+    ->fields(array(
+      'index' => $index->id,
+      'update_time' => REQUEST_TIME,
+    ))
+    ->execute();
+}
+
+/**
+ * A search index was deleted.
+ *
+ * @param SearchApiIndex $index
+ *   The deleted index.
+ */
+function hook_search_api_index_delete(SearchApiIndex $index) {
+  db_insert('example_search_index_update')
+    ->fields(array(
+      'index' => $index->machine_name,
+      'update_time' => REQUEST_TIME,
+    ))
+    ->execute();
+  db_delete('example_search_index')
+    ->condition('index', $index->machine_name)
+    ->execute();
+}
+
+/**
+* Define default search indexes.
+*
+* @return array
+*   An array of default search indexes, keyed by machine names.
+*
+* @see hook_default_search_api_index_alter()
+*/
+function hook_default_search_api_index() {
+  $defaults['main'] = entity_create('search_api_index', array(
+    'name' => 'Main index',
+    'machine_name' => 'main',// Must be same as the used array key.
+    // Other properties ...
+  ));
+  return $defaults;
+}
+
+/**
+* Alter default search indexes.
+*
+* @param array $defaults
+*   An array of default search indexes, keyed by machine names.
+*
+* @see hook_default_search_api_index()
+*/
+function hook_default_search_api_index_alter(array &$defaults) {
+  $defaults['main']->name = 'Customized main index';
+}
+
+/**
+ * @} End of "addtogroup hooks".
+ */
+
+/**
+ * Convert a raw value from an entity to a custom data type.
+ *
+ * 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
+ *   The raw, single value, as extracted from an entity wrapper.
+ * @param $original_type
+ *   The original Entity API type of the value.
+ * @param $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
+ *   The converted value, if a conversion could be executed. NULL otherwise.
+ *
+ * @see hook_search_api_data_type_info()
+ */
+function example_data_type_conversion($value, $original_type, $type) {
+  if ($type === 'example_type') {
+    // The example_type type apparently requires a rather complex data format.
+    return array(
+      'value' => $value,
+      'original' => $original_type,
+    );
+  }
+  // Someone used this callback for another, unknown type. Return NULL.
+  // (Normally, you can just assume that the/a correct type is given.)
+  return NULL;
+}

+ 400 - 0
sites/all/modules/contrib/search/search_api/search_api.drush.inc

@@ -0,0 +1,400 @@
+<?php
+
+/**
+ * @file
+ * Drush commands for SearchAPI.
+ *
+ * Original file by agentrickard for Palantir.net
+ */
+
+/**
+ * Implements hook_drush_command().
+ */
+function search_api_drush_command() {
+  $items = array();
+
+  $items['search-api-list'] = array(
+    'description' => 'List all search indexes.',
+    'examples' => array(
+      'drush searchapi-list' => dt('List all search indexes.'),
+      'drush sapi-l' => dt('Alias to list all search indexes.'),
+    ),
+    '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(
+      'drush searchapi-status' => dt('Show the status of all search indexes.'),
+      'drush sapi-s' => dt('Alias to show the status of all search indexes.'),
+      'drush sapi-s 1' => dt('Show the status of the search index with the ID !id.', array('!id' => 1)),
+      'drush sapi-s default_node_index' => dt('Show the status of 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.'),
+    ),
+    'aliases' => array('sapi-s'),
+  );
+
+  $items['search-api-index'] = array(
+    'description' => 'Index items for one or all enabled search_api indexes.',
+    'examples' => array(
+      'drush searchapi-index' => dt('Index items for all enabled indexes.'),
+      'drush sapi-i' => dt('Alias to index items for all enabled indexes.'),
+      'drush sapi-i 1' => dt('Index items for the index with the ID !id.', array('!id' => 1)),
+      '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)),
+    ),
+    'arguments' => array(
+      'index_id' => dt('The numeric ID or machine name of an index.'),
+      '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."),
+    ),
+    'aliases' => array('sapi-i'),
+  );
+
+  $items['search-api-reindex'] = array(
+    'description' => 'Force reindexing of one or all search indexes, without clearing existing index data.',
+    'examples' => array(
+      'drush searchapi-reindex' => dt('Schedule all search indexes for reindexing.'),
+      'drush sapi-r' => dt('Alias to schedule all search indexes for reindexing .'),
+      'drush sapi-r 1' => dt('Schedule the search index with the ID !id for re-indexing.', array('!id' => 1)),
+      'drush sapi-r default_node_index' => dt('Schedule the search index with the machine name !name for re-indexing.', array('!name' => 'default_node_index')),
+    ),
+    'arguments' => array(
+      'index_id' => dt('The numeric ID or machine name of an index.'),
+    ),
+    'aliases' => array('sapi-r'),
+  );
+
+  $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-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.'),
+    ),
+    'aliases' => array('sapi-c'),
+  );
+
+  return $items;
+}
+
+
+/**
+ * List all search indexes.
+ */
+function drush_search_api_list() {
+  if (search_api_drush_static(__FUNCTION__)) {
+    return;
+  }
+  // See search_api_list_indexes()
+  $indexes = search_api_index_load_multiple(FALSE);
+  if (empty($indexes)) {
+    drush_print(dt('There are no indexes present.'));
+    return;
+  }
+  $rows[] = array(
+    dt('Id'),
+    dt('Name'),
+    dt('Index'),
+    dt('Server'),
+    dt('Type'),
+    dt('Status'),
+    dt('Limit'),
+  );
+  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') . ')';
+    $row = array(
+      $index->id,
+      $index->name,
+      $index->machine_name,
+      $server,
+      $type,
+      $index->enabled ? t('enabled') : t('disabled'),
+      $index->options['cron_limit'],
+    );
+    $rows[] = $row;
+  }
+  drush_print_table($rows);
+}
+
+/**
+ * Enable index(es).
+ *
+ * @param string|integer $index_id
+ *   The index name or id which should be enabled.
+ */
+function drush_search_api_enable($index_id = NULL) {
+  if (search_api_drush_static(__FUNCTION__)) {
+    return;
+  }
+  $indexes = search_api_drush_get_index($index_id);
+  if (empty($indexes)) {
+    return;
+  }
+  foreach ($indexes as $index) {
+    if (!$index->enabled) {
+      drush_log(dt("Enabling index !index and queueing items for indexing.", array('!index' => $index->name)), 'notice');
+      if (search_api_index_enable($index->id)) {
+        drush_log(dt("The index !index was successfully enabled.", array('!index' => $index->name)), 'ok');
+      }
+      else {
+        drush_log(dt("Error enabling index !index.", array('!index' => $index->name)), 'error');
+      }
+    }
+    else {
+      drush_log(dt("The index !index is already enabled.", array('!index' => $index->name)), 'error');
+    }
+  }
+}
+
+/**
+ * Disable index(es).
+ *
+ * @param string|integer $index_id
+ *   The index name or id which should be disabled.
+ */
+function drush_search_api_disable($index_id = NULL) {
+  if (search_api_drush_static(__FUNCTION__)) {
+    return;
+  }
+  $indexes = search_api_drush_get_index($index_id);
+  if (empty($indexes)) {
+    return;
+  }
+  foreach ($indexes as $index) {
+    if ($index->enabled) {
+      if (search_api_index_disable($index->id)) {
+        drush_log(dt("The index !index was successfully disabled.", array('!index' => $index->name)), 'ok');
+      }
+      else {
+        drush_log(dt("Error disabling index !index.", array('!index' => $index->name)), 'error');
+      }
+    }
+    else {
+      drush_log(dt("The index !index is already disabled.", array('!index' => $index->name)), 'error');
+    }
+  }
+}
+
+/**
+ * Display index status.
+ */
+function drush_search_api_status($index_id = NULL) {
+  if (search_api_drush_static(__FUNCTION__)) {
+    return;
+  }
+  $indexes = search_api_drush_get_index($index_id);
+  if (empty($indexes)) {
+    return;
+  }
+  // See search_api_index_status()
+  $rows = array(array(
+    dt('Id'),
+    dt('Index'),
+    dt('% Complete'),
+    dt('Indexed'),
+    dt('Total'),
+  ));
+  foreach ($indexes as $index) {
+    $status = search_api_index_status($index);
+    $complete = ($status['total'] > 0) ? 100 * round($status['indexed'] / $status['total'], 3) . '%' : '-';
+    $row = array(
+      $index->id,
+      $index->name,
+      $complete,
+      $status['indexed'],
+      $status['total'],
+    );
+    $rows[] = $row;
+  }
+  drush_print_table($rows);
+}
+
+/**
+ * Index items.
+ *
+ * @param string|integer $index_id
+ *   The index name or id for which items should be indexed.
+ * @param integer $limit
+ *   Maximum number of items to index.
+ * @param integer $batch_size
+ *   Number of items to index per batch.
+ */
+function drush_search_api_index($index_id = NULL, $limit = NULL, $batch_size = NULL) {
+  if (search_api_drush_static(__FUNCTION__)) {
+    return;
+  }
+  $indexes = search_api_drush_get_index($index_id);
+  if (empty($indexes)) {
+    return;
+  }
+  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'];
+    if ($remaining <= 0) {
+      drush_log(dt("The index !index is up to date.", array('!index' => $index->name)), 'ok');
+      continue;
+    }
+
+    // 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();
+    }
+  }
+}
+
+/**
+ * Copy of formal_plural that works with drush as 't' may not be available.
+ */
+function _search_api_drush_format_plural($count, $singular, $plural, array $args = array(), array $options = array()) {
+  $args['@count'] = $count;
+  if ($count == 1) {
+    return dt($singular, $args, $options);
+  }
+
+  // Get the plural index through the gettext formula.
+  $index = (function_exists('locale_get_plural')) ? locale_get_plural($count, isset($options['langcode']) ? $options['langcode'] : NULL) : -1;
+  // If the index cannot be computed, use the plural as a fallback (which
+  // allows for most flexiblity with the replaceable @count value).
+  if ($index < 0) {
+    return dt($plural, $args, $options);
+  }
+  else {
+    switch ($index) {
+      case "0":
+        return dt($singular, $args, $options);
+      case "1":
+        return dt($plural, $args, $options);
+      default:
+        unset($args['@count']);
+        $args['@count[' . $index . ']'] = $count;
+        return dt(strtr($plural, array('@count' => '@count[' . $index . ']')), $args, $options);
+    }
+  }
+}
+
+/**
+ * Mark for re-indexing.
+ */
+function drush_search_api_reindex($index_id = NULL) {
+  if (search_api_drush_static(__FUNCTION__)) {
+    return;
+  }
+  $indexes = search_api_drush_get_index($index_id);
+  if (empty($indexes)) {
+    return;
+  }
+  // See search_api_index_reindex()
+  foreach ($indexes as $index) {
+    $index->reindex();
+    drush_log(dt('!index was successfully marked for re-indexing.', array('!index' => $index->machine_name)), 'ok');
+  }
+}
+
+/**
+ * Clear an index.
+ */
+function drush_search_api_clear($index_id = NULL) {
+  if (search_api_drush_static(__FUNCTION__)) {
+    return;
+  }
+  $indexes = search_api_drush_get_index($index_id);
+  if (empty($indexes)) {
+    return;
+  }
+  // See search_api_index_reindex()
+  foreach ($indexes as $index) {
+    $index->clear();
+    drush_log(dt('!index was successfully cleared.', array('!index' => $index->machine_name)), 'ok');
+  }
+}
+
+/**
+ * Helper function to return an index or all indexes as an array.
+ *
+ * @param $index_id
+ *   (optional) The provided index id.
+ *
+ * @return
+ *   An array of indexes.
+ */
+function search_api_drush_get_index($index_id = NULL) {
+  $ids = isset($index_id) ? array($index_id) : FALSE;
+  $indexes = search_api_index_load_multiple($ids);
+  if (empty($indexes)) {
+    drush_set_error(dt('Invalid index_id or no indexes present. Listing all indexes:'));
+    drush_print();
+    drush_search_api_list();
+  }
+  return $indexes;
+}
+
+/**
+ * Static lookup to prevent Drush 4 from running twice.
+ *
+ * @see http://drupal.org/node/704848
+ */
+function search_api_drush_static($function) {
+  static $index = array();
+  if (isset($index[$function])) {
+    return TRUE;
+  }
+  $index[$function] = TRUE;
+}

+ 42 - 0
sites/all/modules/contrib/search/search_api/search_api.info

@@ -0,0 +1,42 @@
+name = Search API
+description = "Provides a generic API for modules offering search capabilites."
+dependencies[] = entity
+core = 7.x
+package = Search
+
+files[] = search_api.test
+files[] = includes/callback.inc
+files[] = includes/callback_add_aggregation.inc
+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/datasource.inc
+files[] = includes/datasource_entity.inc
+files[] = includes/datasource_external.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_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-12-25
+version = "7.x-1.11"
+core = "7.x"
+project = "search_api"
+datestamp = "1387965506"
+

+ 994 - 0
sites/all/modules/contrib/search/search_api/search_api.install

@@ -0,0 +1,994 @@
+<?php
+
+/**
+ * @file
+ * Install, update and uninstall functions for the Search API module.
+ */
+
+/**
+ * Implements hook_schema().
+ */
+function search_api_schema() {
+  $schema['search_api_server'] = array(
+    'description' => 'Stores all search servers created through the Search API.',
+    'fields' => array(
+      'id' => array(
+        'description' => 'The primary identifier for a server.',
+        'type' => 'serial',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+      ),
+      'name' => array(
+        'description' => 'The displayed name for a server.',
+        'type' => 'varchar',
+        'length' => 50,
+        'not null' => TRUE,
+      ),
+      'machine_name' => array(
+        'description' => 'The machine name for a server.',
+        'type' => 'varchar',
+        'length' => 50,
+        'not null' => TRUE,
+      ),
+      'description' => array(
+        'description' => 'The displayed description for a server.',
+        'type' => 'text',
+        'not null' => FALSE,
+      ),
+      'class' => array(
+        'description' => 'The id of the service class to use for this server.',
+        'type' => 'varchar',
+        'length' => 50,
+        'not null' => TRUE,
+      ),
+      'options' => array(
+        'description' => 'The options used to configure the service object.',
+        'type' => 'text',
+        'size' => 'medium',
+        'serialize' => TRUE,
+        'not null' => TRUE,
+      ),
+      'enabled' => array(
+        'description' => 'A flag indicating whether the server is enabled.',
+        'type' => 'int',
+        'size' => 'tiny',
+        'not null' => TRUE,
+        'default' => 1,
+      ),
+      'status' => array(
+        'description' => 'The exportable status of the entity.',
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 0x01,
+        'size' => 'tiny',
+      ),
+      'module' => array(
+        'description' => 'The name of the providing module if the entity has been defined in code.',
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => FALSE,
+      ),
+    ),
+    'indexes' => array(
+      'enabled' => array('enabled'),
+    ),
+    'unique keys' => array(
+      'machine_name' => array('machine_name'),
+    ),
+    'primary key' => array('id'),
+  );
+
+  $schema['search_api_index'] = array(
+    'description' => 'Stores all search indexes on a {search_api_server}.',
+    'fields' => array(
+      'id' => array(
+        'description' => 'An integer identifying the index.',
+        'type' => 'serial',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+      ),
+      'name' => array(
+        'description' => 'A name to be displayed for the index.',
+        'type' => 'varchar',
+        'length' => 50,
+        'not null' => TRUE,
+      ),
+      'machine_name' => array(
+        'description' => 'The machine name of the index.',
+        'type' => 'varchar',
+        'length' => 50,
+        'not null' => TRUE,
+      ),
+      'description' => array(
+        'description' => "A string describing the index' use to users.",
+        'type' => 'text',
+        'not null' => FALSE,
+      ),
+      'server' => array(
+        'description' => 'The {search_api_server}.machine_name with which data should be indexed.',
+        'type' => 'varchar',
+        'length' => 50,
+        'not null' => FALSE,
+      ),
+      'item_type' => array(
+        'description' => 'The type of items stored in this index.',
+        'type' => 'varchar',
+        'length' => 50,
+        'not null' => TRUE,
+      ),
+      'options' => array(
+        'description' => 'An array of additional arguments configuring this index.',
+        'type' => 'text',
+        'size' => 'medium',
+        'serialize' => TRUE,
+        'not null' => TRUE,
+      ),
+      'enabled' => array(
+        'description' => 'A flag indicating whether this index is enabled.',
+        'type' => 'int',
+        'size' => 'tiny',
+        'not null' => TRUE,
+        'default' => 1,
+      ),
+      'read_only' => array(
+        'description' => 'A flag indicating whether to write to this index.',
+        'type' => 'int',
+        'size' => 'tiny',
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'status' => array(
+        'description' => 'The exportable status of the entity.',
+        'type' => 'int',
+        'not null' => TRUE,
+        'default' => 0x01,
+        'size' => 'tiny',
+      ),
+      'module' => array(
+        'description' => 'The name of the providing module if the entity has been defined in code.',
+        'type' => 'varchar',
+        'length' => 255,
+        'not null' => FALSE,
+      ),
+    ),
+    'indexes' => array(
+      'item_type' => array('item_type'),
+      'server' => array('server'),
+      'enabled' => array('enabled'),
+    ),
+    'unique keys' => array(
+      'machine_name' => array('machine_name'),
+    ),
+    'primary key' => array('id'),
+  );
+
+  $schema['search_api_item'] = array(
+    'description' => 'Stores the items which should be indexed for each index, and their status.',
+    'fields' => array(
+      'item_id' => array(
+        'description' => "The item's entity id (e.g. {node}.nid for nodes).",
+        'type' => 'int',
+        'unsigned' => TRUE,
+        '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_install().
+ *
+ * Creates a default node index if the module is installed manually.
+ */
+function search_api_install() {
+  // In case the module is installed via an installation profile, a batch is
+  // active and we skip that.
+  if (batch_get()) {
+    return;
+  }
+
+  $name = t('Default node index');
+  $values = array(
+    'name' => $name,
+    'machine_name' => preg_replace('/[^a-z0-9]+/', '_', drupal_strtolower($name)),
+    'description' => t('An automatically created search index for indexing node data. Might be configured to specific needs.'),
+    'server' => NULL,
+    'item_type' => 'node',
+    'options' => array(
+      'index_directly' => 1,
+      'cron_limit' => '50',
+      'data_alter_callbacks' => array(
+        'search_api_alter_node_access' => array(
+          'status' => 1,
+          'weight' => '0',
+          'settings' => array(),
+        ),
+      ),
+      'processors' => array(
+        'search_api_case_ignore' => array(
+          'status' => 1,
+          'weight' => '0',
+          'settings' => array(
+            'strings' => 0,
+          ),
+        ),
+        'search_api_html_filter' => array(
+          'status' => 1,
+          'weight' => '10',
+          'settings' => array(
+            'title' => 0,
+            'alt' => 1,
+            'tags' => "h1 = 5\n" .
+                "h2 = 3\n" .
+                "h3 = 2\n" .
+                "strong = 2\n" .
+                "b = 2\n" .
+                "em = 1.5\n" .
+                "u = 1.5",
+          ),
+        ),
+        'search_api_tokenizer' => array(
+          'status' => 1,
+          'weight' => '20',
+          'settings' => array(
+            'spaces' => '[^\\p{L}\\p{N}]',
+            'ignorable' => '[-]',
+          ),
+        ),
+      ),
+      'fields' => array(
+        'type' => array(
+          'type' => 'string',
+        ),
+        'title' => array(
+          'type' => 'text',
+          'boost' => '5.0',
+        ),
+        'promote' => array(
+          'type' => 'boolean',
+        ),
+        'sticky' => array(
+          'type' => 'boolean',
+        ),
+        'created' => array(
+          'type' => 'date',
+        ),
+        'changed' => array(
+          'type' => 'date',
+        ),
+        'author' => array(
+          'type' => 'integer',
+          'entity_type' => 'user',
+        ),
+        'comment_count' => array(
+          'type' => 'integer',
+        ),
+        'search_api_language' => array(
+          'type' => 'string',
+        ),
+        'body:value' => array(
+          'type' => 'text',
+        ),
+      ),
+    ),
+  );
+  search_api_index_insert($values);
+  drupal_set_message('The Search API module was installed. A new default node index was created.');
+}
+
+/**
+ * Implements hook_enable().
+ *
+ * Mark all items as "dirty", since we can't know whether they are.
+ */
+function search_api_enable() {
+  $types = array();
+  foreach (search_api_index_load_multiple(FALSE) as $index) {
+    if ($index->enabled) {
+      $types[$index->item_type][] = $index;
+    }
+  }
+  foreach ($types as $type => $indexes) {
+    $controller = search_api_get_datasource_controller($type);
+    $controller->startTracking($indexes);
+  }
+}
+
+/**
+ * Implements hook_disable().
+ */
+function search_api_disable() {
+  $types = array();
+  foreach (search_api_index_load_multiple(FALSE) as $index) {
+    $types[$index->item_type][] = $index;
+  }
+  foreach ($types as $type => $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.
+    }
+  }
+}
+
+/**
+ * Implements hook_uninstall().
+ */
+function search_api_uninstall() {
+  variable_del('search_api_index_worker_callback_runtime');
+}
+
+/**
+ * Update function that adds the machine names for servers and indexes.
+ */
+function search_api_update_7101() {
+  $tx = db_transaction();
+  try {
+    // Servers
+    $spec = array(
+      'description' => 'The machine name for a server.',
+      'type' => 'varchar',
+      'length' => 50,
+      'not null' => TRUE,
+      'default' => '',
+    );
+    db_add_field('search_api_server', 'machine_name', $spec);
+
+    $names = array();
+    $servers = db_select('search_api_server', 's')
+      ->fields('s')
+      ->execute();
+    foreach ($servers as $server) {
+      $base = $name = drupal_strtolower(preg_replace('/[^a-z0-9]+/i', '_', $server->name));
+      $i = 0;
+      while (isset($names[$name])) {
+        $name = $base . '_' . ++$i;
+      }
+      $names[$name] = TRUE;
+      db_update('search_api_server')
+        ->fields(array('machine_name' => $name))
+        ->condition('id', $server->id)
+        ->execute();
+    }
+
+    db_add_unique_key('search_api_server', 'machine_name', array('machine_name'));
+
+    //Indexes
+    $spec = array(
+      'description' => 'The machine name of the index.',
+      'type' => 'varchar',
+      'length' => 50,
+      'not null' => TRUE,
+      'default' => '',
+    );
+    db_add_field('search_api_index', 'machine_name', $spec);
+
+    $names = array();
+    $indexes = db_select('search_api_index', 'i')
+      ->fields('i')
+      ->execute();
+    foreach ($indexes as $index) {
+      $base = $name = drupal_strtolower(preg_replace('/[^a-z0-9]+/i', '_', $index->name));
+      $i = 0;
+      while (isset($names[$name])) {
+        $name = $base . '_' . ++$i;
+      }
+      $names[$name] = TRUE;
+      db_update('search_api_index')
+        ->fields(array('machine_name' => $name))
+        ->condition('id', $index->id)
+        ->execute();
+    }
+
+    db_add_unique_key('search_api_index', 'machine_name', array('machine_name'));
+  }
+  catch (Exception $e) {
+    $tx->rollback();
+    try {
+      db_drop_field('search_api_server', 'machine_name');
+      db_drop_field('search_api_index', 'machine_name');
+    }
+    catch (Exception $e1) {
+      // Ignore.
+    }
+    throw new DrupalUpdateException(t('An exception occurred during the update: @msg.', array('@msg' => $e->getMessage())));
+  }
+}
+
+/**
+ * Update replacing IDs with machine names for foreign keys.
+ * {search_api_index}.server and {search_api_item}.index_id are altered.
+ */
+function search_api_update_7102() {
+  // Update of search_api_index:
+  $indexes = array();
+  $select = db_select('search_api_index', 'i')->fields('i');
+  foreach ($select->execute() as $index) {
+    $indexes[$index->id] = $index;
+  }
+  $servers = db_select('search_api_server', 's')->fields('s', array('id', 'machine_name'))->execute()->fetchAllKeyed();
+
+  db_drop_index('search_api_index', 'server');
+  db_drop_field('search_api_index', 'server');
+  $spec = array(
+    'description' => 'The {search_api_server}.machine_name with which data should be indexed.',
+    'type' => 'varchar',
+    'length' => 50,
+    'not null' => FALSE,
+  );
+  db_add_field('search_api_index', 'server', $spec);
+
+  foreach ($indexes as $index) {
+    db_update('search_api_index')
+      ->fields(array('server' => $servers[$index->server]))
+      ->condition('id', $index->id)
+      ->execute();
+  }
+  db_add_index('search_api_index', 'server', array('server'));
+
+  // Update of search_api_item:
+  db_drop_index('search_api_item', 'indexing');
+  db_drop_primary_key('search_api_item');
+  $spec = array(
+    'description' => 'The {search_api_index}.machine_name this item belongs to.',
+    'type' => 'varchar',
+    'length' => 50,
+    'not null' => TRUE,
+  );
+  $keys_new = array(
+    'indexes' => array(
+      'indexing' => array('index_id', 'changed'),
+    ),
+    'primary key' => array('item_id', 'index_id'),
+  );
+  db_change_field('search_api_item', 'index_id', 'index_id', $spec, $keys_new);
+
+  foreach ($indexes as $index) {
+    // We explicitly forbid numeric machine names, therefore we don't have to
+    // worry about conflicts here.
+    db_update('search_api_item')
+      ->fields(array(
+        'index_id' => $index->machine_name,
+      ))
+      ->condition('index_id', $index->id)
+      ->execute();
+  }
+}
+
+/**
+ * Add the database fields newly required for entities by the Entity API.
+ */
+function search_api_update_7103() {
+  if (!function_exists('entity_exportable_schema_fields')) {
+    throw new DrupalUpdateException(t('Please update the Entity API module first.'));
+  }
+  foreach (array('search_api_server', 'search_api_index') as $table) {
+    foreach (entity_exportable_schema_fields() as $field => $spec) {
+      db_add_field($table, $field, $spec);
+    }
+  }
+}
+
+/**
+ * Initialize the "Fields to run on" settings for processors.
+ */
+function search_api_update_7107() {
+  $rows = db_select('search_api_index', 'i')
+    ->fields('i', array('id', 'options'))
+    ->execute()
+    ->fetchAllKeyed();
+  foreach ($rows as $id => $options) {
+    $opt = unserialize($options);
+    $processors = &$opt['processors'];
+    // Only update our own processors, don't mess with others.
+    $check_processors = array(
+      'search_api_case_ignore' => 1,
+      'search_api_html_filter' => 1,
+      'search_api_tokenizer' => 1,
+    );
+    foreach (array_intersect_key($processors, $check_processors) as $name => $info) {
+      $types = array('text');
+      if (!empty($info['settings']['strings'])) {
+        $types[] = 'string';
+        unset($processors[$name]['settings']['strings']);
+      }
+      foreach ($opt['fields'] as $field => $info) {
+        if ($info['indexed'] && search_api_is_text_type($info['type'], $types)) {
+          $processors[$name]['settings']['fields'][$field] = $field;
+        }
+      }
+    }
+    $opt = serialize($opt);
+    if ($opt != $options) {
+      db_update('search_api_index')
+        ->fields(array(
+          'options' => $opt,
+        ))
+        ->condition('id', $id)
+        ->execute();
+    }
+  }
+}
+
+/**
+ * Change {search_api_item}.index_id back to the index' numeric ID.
+ */
+function search_api_update_7104() {
+  $select = db_select('search_api_index', 'i')->fields('i');
+  foreach ($select->execute() as $index) {
+    // We explicitly forbid numeric machine names, therefore we don't have to
+    // worry about conflicts here.
+    db_update('search_api_item')
+      ->fields(array(
+        'index_id' => $index->id,
+      ))
+      ->condition('index_id', $index->machine_name)
+      ->execute();
+  }
+
+  // Update primary key and index.
+  db_drop_index('search_api_item', 'indexing');
+  db_drop_primary_key('search_api_item');
+  $spec = array(
+    'description' => 'The {search_api_index}.id this item belongs to.',
+    'type' => 'int',
+    'unsigned' => TRUE,
+    'not null' => TRUE,
+  );
+  $keys_new = array(
+    'indexes' => array(
+      'indexing' => array('index_id', 'changed'),
+    ),
+    'primary key' => array('item_id', 'index_id'),
+  );
+  db_change_field('search_api_item', 'index_id', 'index_id', $spec, $keys_new);
+}
+
+/**
+ * Remove all empty aggregated fields for the search_api_alter_add_fulltext data
+ * alterations.
+ */
+function search_api_update_7105() {
+  $rows = db_select('search_api_index', 'i')
+    ->fields('i', array('id', 'options'))
+    ->execute()
+    ->fetchAllKeyed();
+  foreach ($rows as $id => $options) {
+    $opt = unserialize($options);
+    if (isset($opt['data_alter_callbacks']['search_api_alter_add_fulltext']['settings']['fields'])) {
+      foreach ($opt['data_alter_callbacks']['search_api_alter_add_fulltext']['settings']['fields'] as $name => $field) {
+        if (empty($field['name']) || empty($field['fields'])) {
+          unset($opt['data_alter_callbacks']['search_api_alter_add_fulltext']['settings']['fields'][$name]);
+        }
+      }
+    }
+    $opt = serialize($opt);
+    if ($opt != $options) {
+      db_update('search_api_index')
+        ->fields(array(
+          'options' => $opt,
+        ))
+        ->condition('id', $id)
+        ->execute();
+    }
+  }
+}
+
+/**
+ * Update the settings for the "Aggregated fields" data alteration.
+ */
+function search_api_update_7106() {
+  $rows = db_select('search_api_index', 'i')
+    ->fields('i')
+    ->execute()
+    ->fetchAll();
+  foreach ($rows as $row) {
+    $opt = unserialize($row->options);
+    $callbacks = &$opt['data_alter_callbacks'];
+    if (isset($callbacks['search_api_alter_add_fulltext'])) {
+      $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 &$info) {
+          if (!isset($info['type'])) {
+            $info['type'] = 'fulltext';
+          }
+        }
+      }
+    }
+    $opt = serialize($opt);
+    if ($opt != $row->options) {
+      // Mark the entity as overridden, in case it has been defined in code
+      // only.
+      $row->status |= 0x01;
+      db_update('search_api_index')
+        ->fields(array(
+          'options' => $opt,
+          'status' => $row->status,
+        ))
+        ->condition('id', $row->id)
+        ->execute();
+    }
+  }
+}
+
+/**
+ * Add "read only" property to Search API index entities.
+ */
+function search_api_update_7108() {
+  $db_field = array(
+    'description' => 'A flag indicating whether to write to this index.',
+    'type' => 'int',
+    'size' => 'tiny',
+    'not null' => TRUE,
+    'default' => 0,
+  );
+  db_add_field('search_api_index', 'read_only', $db_field);
+  return t('Added a "read only" property to index entities.');
+}
+
+/**
+ * Clear entity info cache, as entity controller classes hae changed.
+ */
+function search_api_update_7109() {
+  cache_clear_all('entity_info:', 'cache', TRUE);
+}
+
+/**
+ * Rename the "entity_type" field to "item_type" in the {search_api_index} table.
+ */
+function search_api_update_7110() {
+  $table = 'search_api_index';
+  // This index isn't used anymore.
+  db_drop_index($table, 'entity_type');
+  // Rename the "item_type" field (and change the description).
+  $item_type = array(
+    'description' => 'The type of items stored in this index.',
+    'type' => 'varchar',
+    'length' => 50,
+    'not null' => TRUE,
+  );
+  // Also add the new "item_type" index, while we're at it.
+  $keys_new['indexes']['item_type'] = array('item_type');
+  db_change_field($table, 'entity_type', 'item_type', $item_type, $keys_new);
+  // Mark all indexes in code as "OVERRIDDEN".
+  db_update($table)
+    ->fields(array(
+      'status' => 0x03,
+    ))
+    ->condition('status', 0x02)
+    ->execute();
+  // Clear entity info caches.
+  cache_clear_all('*', 'cache', TRUE);
+}
+
+/**
+ * Change the definition of the {search_api_item}.changed field.
+ */
+function search_api_update_7111() {
+  $spec = 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,
+  );
+  db_change_field('search_api_item', 'changed', 'changed', $spec);
+}
+
+/**
+ * Changes the size of the {search_api_index}.options and {search_api_server}.options fields to "medium".
+ */
+function search_api_update_7112() {
+  $spec = array(
+    'description' => 'The options used to configure the service object.',
+    'type' => 'text',
+    'size' => 'medium',
+    'serialize' => TRUE,
+    'not null' => TRUE,
+  );
+  db_change_field('search_api_server', 'options', 'options', $spec);
+  $spec = array(
+    'description' => 'An array of additional arguments configuring this index.',
+    'type' => 'text',
+    'size' => 'medium',
+    'serialize' => TRUE,
+    'not null' => TRUE,
+  );
+  db_change_field('search_api_index', 'options', 'options', $spec);
+}
+
+/**
+ * Removes superfluous data from the stored index options.
+ */
+function search_api_update_7113() {
+  $indexes = db_select('search_api_index', 'i')
+    ->fields('i')
+    ->execute();
+  foreach ($indexes as $index) {
+    $options = unserialize($index->options);
+    // Weed out fields settings.
+    if (!empty($options['fields'])) {
+      foreach ($options['fields'] as $key => $field) {
+        if (isset($field['indexed']) && !$field['indexed']) {
+          unset($options['fields'][$key]);
+          continue;
+        }
+        unset($options['fields'][$key]['name'], $options['fields'][$key]['indexed']);
+        if (isset($field['boost']) && $field['boost'] == '1.0') {
+          unset($options['fields'][$key]['boost']);
+        }
+      }
+    }
+    // Weed out processor settings.
+    if (!empty($options['processors'])) {
+      // Only weed out settings for our own processors.
+      $processors = array('search_api_case_ignore', 'search_api_html_filter', 'search_api_tokenizer', 'search_api_stopwords');
+      foreach ($processors as $key) {
+        if (empty($options['processors'][$key])) {
+          continue;
+        }
+        $processor = $options['processors'][$key];
+        if (empty($processor['settings']['fields'])) {
+          continue;
+        }
+        $fields = array_filter($processor['settings']['fields']);
+        if ($fields) {
+          $fields = array_combine($fields, array_fill(0, count($fields), TRUE));
+        }
+        $options['processors'][$key]['settings']['fields'] = $fields;
+      }
+    }
+    // Weed out settings for the „Aggregated fields“ data alteration.
+    if (!empty($options['data_alter_callbacks']['search_api_alter_add_aggregation']['settings']['fields'])) {
+      unset($options['data_alter_callbacks']['search_api_alter_add_aggregation']['settings']['actions']);
+      $aggregated_fields = &$options['data_alter_callbacks']['search_api_alter_add_aggregation']['settings']['fields'];
+      foreach ($aggregated_fields as $key => $field) {
+        unset($aggregated_fields[$key]['actions']);
+        if (!empty($field['fields'])) {
+          $aggregated_fields[$key]['fields'] = array_values(array_filter($field['fields']));
+        }
+      }
+    }
+    $options = serialize($options);
+    if ($options != $index->options) {
+      // Mark the entity as overridden, in case it has been defined in code
+      // only.
+      $index->status |= 0x01;
+      db_update('search_api_index')
+        ->fields(array(
+          'options' => $options,
+          'status' => $index->status,
+        ))
+        ->condition('id', $index->id)
+        ->execute();
+    }
+  }
+}
+
+/**
+ * Sanitize watchdog messages.
+ */
+function search_api_update_7114() {
+  if (db_table_exists('watchdog')) {
+    try {
+      $entries = db_select('watchdog', 'w')
+        ->fields('w', array('wid', 'message'))
+        ->condition('type', 'search_api')
+        ->execute();
+      foreach ($entries as $entry) {
+        db_update('watchdog')
+          ->fields(array(
+            'message' => check_plain($entry->message),
+          ))
+          ->condition('wid', $entry->wid)
+          ->execute();
+      }
+    }
+    catch (Exception $e) {
+      throw new DrupalUpdateException(t('An exception occurred during the update: @msg.', array('@msg' => $e->getMessage())));
+    }
+  }
+}
+
+/**
+ * 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) {
+      $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();
+  }
+}

+ 3012 - 0
sites/all/modules/contrib/search/search_api/search_api.module

@@ -0,0 +1,3012 @@
+<?php
+
+/**
+ * @file
+ * Provides a flexible framework for implementing search servives.
+ */
+
+/**
+ * Default number of items indexed per cron batch for each enabled index.
+ */
+define('SEARCH_API_DEFAULT_CRON_LIMIT', 50);
+
+/**
+ * Implements hook_menu().
+ */
+function search_api_menu() {
+  $pre = 'admin/config/search/search_api';
+  $items[$pre] = array(
+    'title' => 'Search API',
+    'description' => 'Create and configure search engines.',
+    'page callback' => 'search_api_admin_overview',
+    'access arguments' => array('administer search_api'),
+    'file' => 'search_api.admin.inc',
+  );
+  $items[$pre . '/overview'] = array(
+    'title' => 'Overview',
+    'type' => MENU_DEFAULT_LOCAL_TASK,
+    'weight' => -10,
+  );
+  $items[$pre . '/add_server'] = array(
+    'title' => 'Add server',
+    'description' => 'Create a new search server.',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('search_api_admin_add_server'),
+    'access arguments' => array('administer search_api'),
+    'file' => 'search_api.admin.inc',
+    'weight' => -1,
+    'type' => MENU_LOCAL_ACTION,
+  );
+  $items[$pre . '/add_index'] = array(
+    'title' => 'Add index',
+    'description' => 'Create a new search index.',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('search_api_admin_add_index'),
+    'access arguments' => array('administer search_api'),
+    'file' => 'search_api.admin.inc',
+    'type' => MENU_LOCAL_ACTION,
+  );
+  $items[$pre . '/server/%search_api_server'] = array(
+    'title' => 'View server',
+    'title callback' => 'search_api_admin_item_title',
+    'title arguments' => array(5),
+    'description' => 'View server details.',
+    'page callback' => 'search_api_admin_server_view',
+    'page arguments' => array(5),
+    'access arguments' => array('administer search_api'),
+    'file' => 'search_api.admin.inc',
+  );
+  $items[$pre . '/server/%search_api_server/view'] = array(
+    'title' => 'View',
+    'type' => MENU_DEFAULT_LOCAL_TASK,
+    'weight' => -10,
+  );
+  $items[$pre . '/server/%search_api_server/edit'] = array(
+    'title' => 'Edit',
+    'description' => 'Edit server details.',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('search_api_admin_server_edit', 5),
+    'access arguments' => array('administer search_api'),
+    'file' => 'search_api.admin.inc',
+    'weight' => -1,
+    'type' => MENU_LOCAL_TASK,
+    'context' => MENU_CONTEXT_INLINE | MENU_CONTEXT_PAGE,
+  );
+  $items[$pre . '/server/%search_api_server/disable'] = array(
+    'title' => 'Disable',
+    'description' => 'Disable index.',
+    'page callback' => 'search_api_admin_server_view',
+    'page arguments' => array(5, 6),
+    'access callback' => 'search_api_access_disable_page',
+    'access arguments' => array(5),
+    'file' => 'search_api.admin.inc',
+    'type' => MENU_LOCAL_TASK,
+    'context' => MENU_CONTEXT_INLINE,
+    'weight' => 8,
+  );
+  $items[$pre . '/server/%search_api_server/delete'] = array(
+    'title' => 'Delete',
+    'title callback' => 'search_api_title_delete_page',
+    'title arguments' => array(5),
+    'description' => 'Delete server.',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('search_api_admin_confirm', 'server', 'delete', 5),
+    'access callback' => 'search_api_access_delete_page',
+    'access arguments' => array(5),
+    'file' => 'search_api.admin.inc',
+    'type' => MENU_LOCAL_TASK,
+    'context' => MENU_CONTEXT_INLINE,
+    'weight' => 10,
+  );
+  $items[$pre . '/index/%search_api_index'] = array(
+    'title' => 'View index',
+    'title callback' => 'search_api_admin_item_title',
+    'title arguments' => array(5),
+    'description' => 'View index details.',
+    'page callback' => 'search_api_admin_index_view',
+    'page arguments' => array(5),
+    'access arguments' => array('administer search_api'),
+    'file' => 'search_api.admin.inc',
+  );
+  $items[$pre . '/index/%search_api_index/view'] = array(
+    'title' => 'View',
+    'type' => MENU_DEFAULT_LOCAL_TASK,
+    'weight' => -10,
+  );
+  $items[$pre . '/index/%search_api_index/edit'] = array(
+    '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',
+    'type' => MENU_LOCAL_TASK,
+    'context' => MENU_CONTEXT_INLINE | MENU_CONTEXT_PAGE,
+    'weight' => -6,
+  );
+  $items[$pre . '/index/%search_api_index/fields'] = array(
+    'title' => 'Fields',
+    'description' => 'Select indexed fields.',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('search_api_admin_index_fields', 5),
+    'access arguments' => array('administer search_api'),
+    'file' => 'search_api.admin.inc',
+    'type' => MENU_LOCAL_TASK,
+    'context' => MENU_CONTEXT_INLINE | MENU_CONTEXT_PAGE,
+    'weight' => -4,
+  );
+  $items[$pre . '/index/%search_api_index/workflow'] = array(
+    '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',
+    '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',
+    'title callback' => 'search_api_title_delete_page',
+    'title arguments' => array(5),
+    'description' => 'Delete index.',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('search_api_admin_confirm', 'index', 'delete', 5),
+    'access callback' => 'search_api_access_delete_page',
+    '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[] = "<h2 id=\"$id\">$name</h2>\n$description";
+      }
+      $output = '';
+      if ($classes) {
+        $output .= '<p>' . t('The following service classes are available for creating a search server.') . "</p>\n";
+        $output .= implode("\n\n", $classes);
+      }
+      return $output;
+    case 'admin/config/search/search_api':
+      return '<p>' . t('A search server and search index are used to execute searches. Several indexes can exist per server.<br />You need at least one server and one index to create searches on your site.') . '</p>';
+  }
+}
+
+/**
+ * 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_processor_info' => $hook_info,
+    'search_api_index_items_alter' => $hook_info,
+    'search_api_items_indexed' => $hook_info,
+    'search_api_query_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,
+      'name' => '',
+      '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',
+  );
+  $themes['search_api_index'] = array(
+    'variables' => array(
+      'id' => NULL,
+      'name' => '',
+      'machine_name' => '',
+      'description' => NULL,
+      'item_type' => NULL,
+      'enabled' => NULL,
+      'server' => NULL,
+      'options' => array(),
+      'fields' => array(),
+      'indexed_items' => 0,
+      'on_server' => 0,
+      'total_items' => 0,
+      'status' => ENTITY_CUSTOM,
+      'read_only' => 0,
+    ),
+    'file' => 'search_api.admin.inc',
+  );
+  $themes['search_api_admin_item_order'] = array(
+    'render element' => 'element',
+    'file' => 'search_api.admin.inc',
+  );
+  $themes['search_api_admin_fields_table'] = array(
+    'render element' => 'element',
+    'file' => 'search_api.admin.inc',
+  );
+
+  return $themes;
+}
+
+/**
+ * Implements hook_permission().
+ */
+function search_api_permission() {
+  return array(
+    'administer search_api' => array(
+      'title' => t('Administer Search API'),
+      'description' => t('Create and configure Search API servers and indexes.'),
+    ),
+  );
+}
+
+/**
+ * Implements hook_cron().
+ *
+ * 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() {
+  // 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;
+      $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);
+          }
+        }
+        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);
+        }
+      }
+      if  (!$num) {
+        // Couldn't index any items => stop indexing for this index in this
+        // cron run.
+        unset($indexes[$id]);
+      }
+    }
+    $first_pass = FALSE;
+  }
+}
+
+/**
+ * Implements hook_entity_info().
+ */
+function search_api_entity_info() {
+  $info['search_api_server'] = array(
+    'label' => t('Search server'),
+    'controller class' => 'EntityAPIControllerExportable',
+    'metadata controller class' => FALSE,
+    '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(
+      'id' => 'id',
+      'label' => 'name',
+      'name' => 'machine_name',
+    ),
+  );
+  $info['search_api_index'] = array(
+    'label' => t('Search index'),
+    'controller class' => 'EntityAPIControllerExportable',
+    'metadata controller class' => FALSE,
+    '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(
+      'id' => 'id',
+      'label' => 'name',
+      'name' => 'machine_name',
+    ),
+  );
+
+  return $info;
+}
+
+/**
+ * Implements hook_entity_property_info().
+ */
+function search_api_entity_property_info() {
+  $info['search_api_server']['properties'] = array(
+    'id' => array(
+      'label' => t('ID'),
+      'type' => 'integer',
+      'description' => t('The primary identifier for a server.'),
+      'schema field' => 'id',
+      'validation callback' => 'entity_metadata_validate_integer_positive',
+    ),
+    'name' => array(
+      'label' => t('Name'),
+      'type' => 'text',
+      'description' => t('The displayed name for a server.'),
+      'schema field' => 'name',
+      'required' => TRUE,
+    ),
+    'machine_name' => array(
+      'label' => t('Machine name'),
+      'type' => 'token',
+      'description' => t('The internally used machine name for a server.'),
+      'schema field' => 'machine_name',
+      'required' => TRUE,
+    ),
+    'description' => array(
+      'label' => t('Description'),
+      'type' => 'text',
+      'description' => t('The displayed description for a server.'),
+      'schema field' => 'description',
+      'sanitize' => 'filter_xss',
+    ),
+    'class' => array(
+      'label' => t('Service class'),
+      'type' => 'text',
+      'description' => t('The ID of the service class to use for this server.'),
+      'schema field' => 'class',
+      'required' => TRUE,
+    ),
+    'enabled' => array(
+      'label' => t('Enabled'),
+      'type' => 'boolean',
+      '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(
+      'label' => t('ID'),
+      'type' => 'integer',
+      'description' => t('An integer identifying the index.'),
+      'schema field' => 'id',
+      'validation callback' => 'entity_metadata_validate_integer_positive',
+    ),
+    'name' => array(
+      'label' => t('Name'),
+      'type' => 'text',
+      'description' => t('A name to be displayed for the index.'),
+      'schema field' => 'name',
+      'required' => TRUE,
+    ),
+    'machine_name' => array(
+      'label' => t('Machine name'),
+      'type' => 'token',
+      'description' => t('The internally used machine name for an index.'),
+      'schema field' => 'machine_name',
+      'required' => TRUE,
+    ),
+    'description' => array(
+      'label' => t('Description'),
+      'type' => 'text',
+      'description' => t("A string describing the index' use to users."),
+      'schema field' => 'description',
+      'sanitize' => 'filter_xss',
+    ),
+    'server' => array(
+      'label' => t('Server ID'),
+      'type' => 'token',
+      'description' => t('The machine name of the search_api_server with which data should be indexed.'),
+      'schema field' => 'server',
+    ),
+    'server_entity' => array(
+      'label' => t('Server'),
+      'type' => 'search_api_server',
+      'description' => t('The search_api_server with which data should be indexed.'),
+      'getter callback' => 'search_api_index_get_server',
+    ),
+    'item_type' => array(
+      'label' => t('Item type'),
+      'type' => 'token',
+      'description' => t('The type of items stored in this index.'),
+      'schema field' => 'item_type',
+      'required' => TRUE,
+    ),
+    'enabled' => array(
+      'label' => t('Enabled'),
+      'type' => 'boolean',
+      'description' => t('A flag indicating whether the index is enabled.'),
+      'schema field' => 'enabled',
+    ),
+    'read_only' => array(
+      'label' => t('Read only'),
+      'type' => 'boolean',
+      '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;
+}
+
+/**
+ * Implements hook_search_api_server_insert().
+ *
+ * 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();
+}
+
+/**
+ * Implements hook_search_api_server_update().
+ *
+ * Calls the server's postUpdate() method and marks all of this server's indexes
+ * for reindexing, if necessary.
+ */
+function search_api_search_api_server_update(SearchApiServer $server) {
+  if ($server->postUpdate()) {
+    foreach (search_api_index_load_multiple(FALSE, array('server' => $server->machine_name)) as $index) {
+      $index->reindex();
+    }
+  }
+  if (!empty($server->original) && $server->enabled != $server->original->enabled) {
+    if ($server->enabled) {
+      search_api_server_tasks_check($server);
+    }
+    else {
+      foreach (search_api_index_load_multiple(FALSE, array('server' => $server->machine_name)) as $index) {
+        $index->update(array('enabled' => 0, 'server' => NULL));
+      }
+    }
+  }
+}
+
+/**
+ * Implements hook_search_api_server_delete().
+ *
+ * Calls the preDelete() method for the server.
+ */
+function search_api_search_api_server_delete(SearchApiServer $server) {
+  // Only react on real delete, not revert.
+  if ($server->hasStatus(ENTITY_IN_CODE)) {
+    $reverts = &drupal_static(__FUNCTION__, array());
+    $reverts[$server->machine_name] = $server;
+    return;
+  }
+
+  $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);
+}
+
+/**
+ * Implements hook_search_api_index_insert().
+ *
+ * Adds the index to its server (if any) and starts tracking indexed items (if
+ * 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();
+}
+
+/**
+ * Implements hook_search_api_index_update().
+ */
+function search_api_search_api_index_update(SearchApiIndex $index) {
+  // Call the datasource update function with the table this module provides.
+  search_api_index_update_datasource($index, 'search_api_item');
+
+  // If the server was changed, we have to call the appropriate service class
+  // hook methods.
+  if ($index->server != $index->original->server) {
+    // Server changed - inform old and new ones.
+    if ($index->original->server) {
+      $old_server = search_api_server_load($index->original->server);
+      // The server might have changed because the old one was deleted:
+      if ($old_server) {
+        $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.
+      $new_server->addIndex($index);
+    }
+
+    // We also have to re-index all content.
+    _search_api_index_reindex($index);
+  }
+
+  // If the fields were changed, call the appropriate service class hook method
+  // and re-index the content, if necessary. Also, clear the fields cache.
+  $old_fields = $index->original->options + array('fields' => array());
+  $old_fields = $old_fields['fields'];
+  $new_fields = $index->options + array('fields' => array());
+  $new_fields = $new_fields['fields'];
+  if ($old_fields != $new_fields) {
+    cache_clear_all($index->getCacheId(), 'cache', TRUE);
+    if ($index->server) {
+      $index->server()->fieldsUpdated($index);
+    }
+  }
+
+  // If additional fields changed, clear the index's specific cache which
+  // includes them.
+  $old_additional = $index->original->options + array('additional fields' => array());
+  $old_additional = $old_additional['additional fields'];
+  $new_additional = $index->options + array('additional fields' => array());
+  $new_additional = $new_additional['additional fields'];
+  if ($old_additional != $new_additional) {
+    cache_clear_all($index->getCacheId() . '-0-1', 'cache');
+  }
+
+  // If the index's enabled or read-only status is being changed, queue or
+  // dequeue items for indexing.
+  if (!$index->read_only && $index->enabled != $index->original->enabled) {
+    if ($index->enabled) {
+      $index->queueItems();
+    }
+    else {
+      $index->dequeueItems();
+    }
+  }
+  elseif ($index->read_only != $index->original->read_only) {
+    if ($index->read_only) {
+      $index->dequeueItems();
+    }
+    else {
+      $index->queueItems();
+    }
+  }
+}
+
+/**
+ * Implements hook_search_api_index_delete().
+ *
+ * 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();
+}
+
+/**
+ * Implements hook_features_export_alter().
+ *
+ * Adds dependency information for exported servers.
+ */
+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';
+    $classes = array();
+    foreach (module_implements('search_api_service_info') as $module) {
+      $function = $module . '_' . $hook;
+      $engines = $function();
+      foreach ($engines as $service => $specs) {
+        $classes[$service] = $module;
+      }
+    }
+
+    // Check all of the exported server specifications.
+    foreach ($export['features']['search_api_server'] as $server_name) {
+      // Load the server's object.
+      $server = search_api_server_load($server_name);
+      $module = $classes[$server->class];
+
+      // Ensure that the module responsible for the server object is listed as
+      // a dependency.
+      if (!isset($export['dependencies'][$module])) {
+        $export['dependencies'][$module] = $module;
+      }
+    }
+
+    // Ensure the dependencies list is still sorted alphabetically.
+    ksort($export['dependencies']);
+  }
+}
+
+/**
+ * Implements hook_system_info_alter().
+ *
+ * Checks if the module provides any search item types or service classes. If it
+ * does, and there are search indexes using those item types, respectively
+ * servers using those service classes, the module is set to "required".
+ *
+ * Heavily borrowed from field_system_info_alter().
+ *
+ * @see hook_search_api_item_type_info()
+ */
+function search_api_system_info_alter(&$info, $file, $type) {
+  if ($type != 'module' || $file->name == 'search_api') {
+    return;
+  }
+  // Check for defined item types.
+  if (module_hook($file->name, 'search_api_item_type_info')) {
+    $types = array();
+    foreach (search_api_get_item_type_info() as $type => $type_info) {
+      if ($type_info['module'] == $file->name) {
+        $types[] = $type;
+      }
+    }
+    if ($types) {
+      $sql = 'SELECT machine_name, name FROM {search_api_index} WHERE item_type IN (:types)';
+      $indexes = db_query($sql, array(':types' => $types))->fetchAllKeyed();
+      if ($indexes) {
+        $info['required'] = TRUE;
+
+        $links = array();
+        foreach ($indexes as $id => $name) {
+          $url = url("admin/config/search/search_api/index/$id");
+          $links[] = '<a href="' . check_plain($url) . '">' . check_plain($name) . '</a>';
+        }
+
+        $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[] = '<a href="' . check_plain($url) . '">' . check_plain($name) . '</a>';
+        }
+
+        $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().
+ *
+ * This is implemented on behalf of the SearchApiEntityDataSourceController
+ * datasource controller and calls search_api_track_item_insert() for the
+ * inserted items.
+ *
+ * @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
+  // the tracking table. This would lead to a duplicate-key issue, if we would
+  // continue.
+  // We also only react on entity operations for types with property
+  // information, as we don't provide search integration for the others.
+  if ($type == 'search_api_index' || !entity_get_property_info($type)) {
+    return;
+  }
+  list($id) = entity_extract_ids($type, $entity);
+  if (isset($id)) {
+    search_api_track_item_insert($type, array($id));
+  }
+}
+
+/**
+ * Implements hook_entity_update().
+ *
+ * This is implemented on behalf of the SearchApiEntityDataSourceController
+ * datasource controller and calls search_api_track_item_change() for the
+ * updated items.
+ *
+ * @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
+  // we don't provide search integration for the others.
+  if (!entity_get_property_info($type)) {
+    return;
+  }
+  list($id) = entity_extract_ids($type, $entity);
+  if (isset($id)) {
+    search_api_track_item_change($type, array($id));
+  }
+}
+
+/**
+ * Implements hook_entity_delete().
+ *
+ * This is implemented on behalf of the SearchApiEntityDataSourceController
+ * datasource controller and calls search_api_track_item_delete() for the
+ * deleted items.
+ *
+ * @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
+  // we don't provide search integration for the others.
+  if (!entity_get_property_info($type)) {
+    return;
+  }
+  list($id) = entity_extract_ids($type, $entity);
+  if (isset($id)) {
+    search_api_track_item_delete($type, array($id));
+  }
+}
+
+/**
+ * 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().
+ *
+ * Adds item types for all entity types with property information.
+ */
+function search_api_search_api_item_type_info() {
+  $types = array();
+
+  foreach (entity_get_property_info() as $type => $property_info) {
+    if ($info = entity_get_info($type)) {
+      $types[$type] = array(
+        'name' => $info['label'],
+        'datasource controller' => 'SearchApiEntityDataSourceController',
+        'entity_type' => $type,
+      );
+    }
+  }
+
+  return $types;
+}
+
+/**
+ * Implements hook_modules_enabled().
+ */
+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() {
+  // 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');
+}
+
+/**
+ * Implements hook_search_api_alter_callback_info().
+ */
+function search_api_search_api_alter_callback_info() {
+  $callbacks['search_api_alter_bundle_filter'] = array(
+    'name' => t('Bundle filter'),
+    'description' => t('Exclude items from indexing based on their bundle (content type, vocabulary, …).'),
+    'class' => 'SearchApiAlterBundleFilter',
+    // 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."),
+    'class' => 'SearchApiAlterAddUrl',
+  );
+  $callbacks['search_api_alter_add_aggregation'] = array(
+    'name' => t('Aggregated fields'),
+    'description' => t('Gives you the ability to define additional fields, containing data from one or more other fields.'),
+    'class' => 'SearchApiAlterAddAggregation',
+  );
+  $callbacks['search_api_alter_add_viewed_entity'] = array(
+    'name' => t('Complete entity view'),
+    'description' => t('Adds an additional field containing the whole HTML content of the entity when viewed.'),
+    'class' => 'SearchApiAlterAddViewedEntity',
+  );
+  $callbacks['search_api_alter_add_hierarchy'] = array(
+    'name' => t('Index hierarchy'),
+    'description' => t('Allows to index hierarchical fields along with all their ancestors.'),
+    'class' => 'SearchApiAlterAddHierarchy',
+  );
+  $callbacks['search_api_alter_language_control'] = array(
+    'name' => t('Language control'),
+    'description' => t('Lets you determine the language of items in the index.'),
+    'class' => 'SearchApiAlterLanguageControl',
+  );
+  $callbacks['search_api_alter_node_access'] = array(
+    'name' => t('Node access'),
+    'description' => t('Add node access information to the index. <strong>Caution:</strong> 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. <strong>Caution:</strong> 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. <strong>Caution:</strong> 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',
+  );
+
+  return $callbacks;
+}
+
+/**
+ * Implements hook_search_api_processor_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 fulltext or string fields.'),
+    'class' => 'SearchApiIgnoreCase',
+  );
+  $processors['search_api_html_filter'] = array(
+    'name' => t('HTML filter'),
+    'description' => t('Strips HTML tags from fulltext fields and decodes HTML entities. ' .
+        'Use this processor when indexing HTML data, e.g., node bodies for certain text formats.<br />' .
+        'The processor also allows to boost (or ignore) the contents of specific elements.'),
+    '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. ' .
+        'This processor allows you to specify which characters make up words and which characters should be ignored, using regular expression syntax. ' .
+        'Otherwise it is up to the search server implementation to decide how to split indexed fulltext data.'),
+    'class' => 'SearchApiTokenizer',
+    'weight' => 20,
+  );
+  $processors['search_api_stopwords'] = array(
+    'name' => t('Stopwords'),
+    'description' => t('This processor prevents certain words from being indexed and removes them from search terms. ' .
+        'For best results, it should only be executed after tokenizing.'),
+    'class' => 'SearchApiStopWords',
+    'weight' => 30,
+  );
+  $processors['search_api_highlighting'] = array(
+    'name' => t('Highlighting'),
+    'description' => t('Adds highlighting for search results.'),
+    'class' => 'SearchApiHighlight',
+    'weight' => 35,
+  );
+
+  return $processors;
+}
+
+/**
+ * Inserts new unindexed items for all indexes on the specified type.
+ *
+ * @param string $type
+ *   The item type of the new items.
+ * @param array $item_ids
+ *   The IDs of the new items.
+ */
+function search_api_track_item_insert($type, array $item_ids) {
+  $conditions = array(
+    'enabled' => 1,
+    'item_type' => $type,
+    'read_only' => 0,
+  );
+  $indexes = search_api_index_load_multiple(FALSE, $conditions);
+  if (!$indexes) {
+    return;
+  }
+
+  search_api_get_datasource_controller($type)->trackItemInsert($item_ids, $indexes);
+
+  foreach ($indexes as $index) {
+    if (!empty($index->options['index_directly'])) {
+      search_api_index_specific_items_delayed($index, $item_ids);
+    }
+  }
+}
+
+/**
+ * Mark the items with the specified IDs as "dirty", i.e., as needing to be reindexed.
+ *
+ * For indexes for which items should be indexed immediately, the items are
+ * indexed directly, instead.
+ *
+ * @param $type
+ *   The type of items, specific to the data source.
+ * @param array $item_ids
+ *   The IDs of the items to be marked dirty.
+ */
+function search_api_track_item_change($type, array $item_ids) {
+  $conditions = array(
+    'enabled' => 1,
+    'item_type' => $type,
+    'read_only' => 0,
+  );
+  $indexes = search_api_index_load_multiple(FALSE, $conditions);
+  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);
+      }
+    }
+  }
+}
+
+/**
+ * Marks items as queued for indexing for the specified index.
+ *
+ * @param SearchApiIndex $index
+ *   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);
+}
+
+/**
+ * Marks items as successfully indexed for the specified index.
+ *
+ * @param SearchApiIndex $index
+ *   The index on which items were indexed.
+ * @param array $item_ids
+ *   The ids of the indexed items.
+ */
+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);
+}
+
+/**
+ * Removes items from all indexes.
+ *
+ * @param $type
+ *   The type of the items.
+ * @param array $item_ids
+ *   The IDs of the deleted items.
+ */
+function search_api_track_item_delete($type, array $item_ids) {
+  // First, delete the item from the tracking table.
+  $conditions = array(
+    'enabled' => 1,
+    'item_type' => $type,
+    'read_only' => 0,
+  );
+  $indexes = search_api_index_load_multiple(FALSE, $conditions);
+  if ($indexes) {
+    search_api_get_datasource_controller($type)->trackItemDelete($item_ids, $indexes);
+  }
+
+  // Then, delete it from all servers. Servers of disabled indexes have to be
+  // considered, too!
+  unset($conditions['enabled']);
+  foreach (search_api_index_load_multiple(FALSE, $conditions) as $index) {
+    if ($index->server) {
+      $server = $index->server();
+      $server->deleteItems($item_ids, $index);
+    }
+  }
+}
+
+/**
+ * Checks for pending tasks on one or all enabled search servers.
+ *
+ * @param SearchApiServer|null $server
+ *   (optional) The server whose tasks should be checked. If not given, the
+ *   tasks for all enabled servers are checked.
+ *
+ * @return bool
+ *   TRUE if all tasks (for the specific server, if $server was given) were
+ *   executed successfully, or if there were no tasks. FALSE if there are still
+ *   pending tasks.
+ */
+function search_api_server_tasks_check(SearchApiServer $server = NULL) {
+  $select = db_select('search_api_task', 't')
+    ->fields('t')
+    // Only retrieve tasks we can handle.
+    ->condition('t.type', array('addIndex', 'fieldsUpdated', 'removeIndex', 'deleteItems'));
+  if ($server) {
+    $select->condition('t.server_id', $server->machine_name);
+  }
+  else {
+    $select->innerJoin('search_api_server', 's', 't.server_id = s.machine_name AND s.enabled = 1');
+    // By ordering by the server, we can later just load them when we reach them
+    // while looping through the tasks. It is very unlikely there will be tasks
+    // for more than one or two servers, so a *_load_multiple() probably
+    // wouldn't bring any significant advantages, but complicate the code.
+    $select->orderBy('t.server_id');
+  }
+  // Store a count query for later checking whether all tasks were processed
+  // successfully.
+  $count_query = $select->countQuery();
+
+  // Sometimes the order of tasks might be important, so make sure to order by
+  // the task ID (which should be in order of insertion).
+  $select->orderBy('t.id');
+  $tasks = $select->execute();
+
+  $executed_tasks = array();
+  foreach ($tasks as $task) {
+    if (!$server || $server->machine_name != $task->server_id) {
+      $server = search_api_server_load($task->server_id);
+      if (!$server) {
+        continue;
+      }
+    }
+    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;
+    }
+    $executed_tasks[] = $task->id;
+  }
+
+  // If there were no tasks (we recognized), return TRUE.
+  if (!$executed_tasks) {
+    return TRUE;
+  }
+  // Otherwise, delete the executed tasks and check if new tasks were created.
+  search_api_server_tasks_delete($executed_tasks);
+  return $count_query->execute()->fetchField() === 0;
+}
+
+/**
+ * Adds an entry into a server's list of pending tasks.
+ *
+ * @param SearchApiServer $server
+ *   The server for which a task should be remembered.
+ * @param $type
+ *   The type of task to perform.
+ * @param SearchApiIndex|string|null $index
+ *   (optional) If applicable, the index to which the task pertains (or its
+ *   machine name).
+ * @param mixed $data
+ *   (optional) If applicable, some further data necessary for the task.
+ */
+function search_api_server_tasks_add(SearchApiServer $server, $type, $index = NULL, $data = NULL) {
+  db_insert('search_api_task')
+    ->fields(array(
+      'server_id' => $server->machine_name,
+      'type' => $type,
+      'index_id' => $index ? (is_object($index) ? $index->machine_name : $index) : NULL,
+      'data' => isset($data) ? serialize($data) : NULL,
+    ))
+    ->execute();
+}
+
+/**
+ * Removes pending server tasks from the list.
+ *
+ * @param array|null $ids
+ *   (optional) The IDs of the pending server tasks to delete. Set to NULL
+ *   to not filter by IDs.
+ * @param SearchApiServer|null $server
+ *   (optional) A server for which the tasks should be deleted. Set to NULL to
+ *   delete tasks from all servers.
+ * @param SearchApiIndex|string|null $index
+ *   (optional) An index (or its machine name) for which the tasks should be
+ *   deleted. Set to NULL to delete tasks for all indexes.
+ */
+function search_api_server_tasks_delete(array $ids = NULL, SearchApiServer $server = NULL, $index = NULL) {
+  $delete = db_delete('search_api_task');
+  if ($ids) {
+    $delete->condition('id', $ids);
+  }
+  if ($server) {
+    $delete->condition('server_id', $server->machine_name);
+  }
+  if ($index) {
+    $delete->condition('index_id', $index->machine_name);
+  }
+  $delete->execute();
+}
+
+/**
+ * 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 (!_search_api_settings_equals($fields, $index->options['fields'])) {
+      $options = $index->options;
+      $options['fields'] = $fields;
+      $index->update(array('options' => $options));
+    }
+  }
+}
+
+/**
+ * Test two setting arrays (or individual settings) for equality.
+ *
+ * While a simple == also works in some cases, this function takes into account
+ * that the order of keys (usually) doesn't matter in settings arrays.
+ *
+ * @param mixed $setting1
+ *   The first setting (array).
+ * @param mixed $setting2
+ *   The second setting (array).
+ *
+ * @return bool
+ *   TRUE if both settings are identical, FALSE otherwise.
+ */
+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 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.
+ */
+function search_api_index_items(SearchApiIndex $index, $limit = -1) {
+  // Don't try to index on read-only indexes.
+  if ($index->read_only) {
+    return 0;
+  }
+
+  $ids = search_api_get_items_to_index($index, $limit);
+  return $ids ? count(search_api_index_specific_items($index, $ids)) : 0;
+}
+
+/**
+ * Indexes the specified items on the given index.
+ *
+ * Items which were successfully indexed are marked as such afterwards.
+ *
+ * @param SearchApiIndex $index
+ *   The index on which items should be indexed.
+ * @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.
+ */
+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) {
+    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) {
+    search_api_track_item_indexed($index, $indexed);
+    // If some items could not be indexed, we don't want to try re-indexing
+    // them right away, so we mark them as "freshly" changed. Sadly, there is
+    // no better way than to mark them as indexed first...
+    if (count($indexed) < count($ids)) {
+      // Believe it or not but this is actually quite faster than the equivalent
+      // $diff = array_diff($ids, $indexed);
+      $diff = array_keys(array_diff_key(array_flip($ids), array_flip($indexed)));
+      $index->datasource()->trackItemIndexed($diff, $index);
+      $index->datasource()->trackItemChange($diff, array($index));
+    }
+  }
+  return $indexed;
+}
+
+/**
+ * Queues items for indexing at the end of the page request.
+ *
+ * @param SearchApiIndex $index
+ *   The index on which items should be indexed.
+ * @param array $ids
+ *   The IDs of the items which should be indexed.
+ *
+ * @return array
+ *   The current contents of the queue, as a reference.
+ *
+ * @see search_api_index_specific_items()
+ * @see _search_api_index_queued_items()
+ */
+function &search_api_index_specific_items_delayed(SearchApiIndex $index = NULL, array $ids = array()) {
+  // We cannot use drupal_static() here because the static cache is reset during
+  // batch processing, which breaks batch handling.
+  static $queue = array();
+  static $registered = FALSE;
+
+  // Only register the shutdown function once.
+  if (empty($registered)) {
+    drupal_register_shutdown_function('_search_api_index_queued_items');
+    $registered = TRUE;
+  }
+
+  // Allow for empty call to just retrieve the queue.
+  if ($index && $ids) {
+    $index_id = $index->machine_name;
+    $queue += array($index_id => array());
+    $queue[$index_id] += drupal_map_assoc($ids);
+  }
+
+  return $queue;
+}
+
+/**
+ * Returns a list of items that need to be indexed for the specified index.
+ *
+ * @param SearchApiIndex $index
+ *   The index for which items should be retrieved.
+ * @param $limit
+ *   The maximum number of items to retrieve. -1 means no limit.
+ *
+ * @return array
+ *   An array of IDs of items that need to be indexed.
+ */
+function search_api_get_items_to_index(SearchApiIndex $index, $limit = -1) {
+  if ($limit == 0) {
+    return array();
+  }
+  return $index->datasource()->getChangedItems($index, $limit);
+}
+
+/**
+ * Creates a search query on a specified search index.
+ *
+ * @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.
+ *
+ * @return SearchApiQueryInterface
+ *   An object for searching on the specified index.
+ */
+function search_api_query($id, array $options = array()) {
+  $index = search_api_index_load($id);
+  if (!$index) {
+    throw new SearchApiException(t('Unknown index with ID @id.', array('@id' => $id)));
+  }
+  return $index->query($options);
+}
+
+/**
+ * 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.
+ *
+ * @param $search_id
+ *   For pages displaying multiple searches, an optional ID identifying the
+ *   search in questions. When storing a search, this is filled automatically,
+ *   unless it is manually set.
+ * @param SearchApiQuery $query
+ *   When storing an executed search, the query that was executed. NULL
+ *   otherwise.
+ * @param array $results
+ *   When storing an executed search, the returned results as specified by
+ *   SearchApiQueryInterface::execute(). An empty array, otherwise.
+ *
+ * @return array
+ *   If a search with the specified ID was executed, an array containing
+ *   ($query, $results) as used in this function's parameters. If $search_id is
+ *   NULL, an array of all executed searches will be returned, keyed by ID.
+ */
+function search_api_current_search($search_id = NULL, SearchApiQuery $query = NULL, array $results = array()) {
+  $searches = &drupal_static(__FUNCTION__, array());
+
+  if (isset($query)) {
+    if (!isset($search_id)) {
+      $search_id = $query->getOption('search id');
+    }
+    $base = $search_id;
+    $i = 0;
+    while (isset($searches[$search_id])) {
+      $search_id = $base . '-' . ++$i;
+    }
+    $searches[$search_id] = array($query, $results);
+  }
+
+  if (isset($search_id)) {
+    return isset($searches[$search_id]) ? $searches[$search_id] : NULL;
+  }
+  return $searches;
+}
+
+/**
+ * Returns all field types recognized by the Search API framework.
+ *
+ * @return array
+ *   An associative array with all recognized types as keys, mapped to their
+ *   translated display names.
+ *
+ * @see search_api_default_field_types()
+ * @see search_api_get_data_type_info()
+ */
+function search_api_field_types() {
+  $types = search_api_default_field_types();
+  foreach (search_api_get_data_type_info() as $id => $type) {
+    $types[$id] = $type['name'];
+  }
+  return $types;
+}
+
+/**
+ * Returns the default field types recognized by the Search API framework.
+ *
+ * @return array
+ *   An associative array with the default types as keys, mapped to their
+ *   translated display names.
+ */
+function search_api_default_field_types() {
+  return array(
+    'text' => t('Fulltext'),
+    'string' => t('String'),
+    'integer' => t('Integer'),
+    'decimal' => t('Decimal'),
+    'date' => t('Date'),
+    'duration' => t('Duration'),
+    'boolean' => t('Boolean'),
+    'uri' => t('URI'),
+  );
+}
+
+/**
+ * Returns either all custom field type definitions, or a specific one.
+ *
+ * @param $type
+ *   If specified, the type whose definition should be returned.
+ *
+ * @return array
+ *   If $type was not given, an array containing all custom data types, in the
+ *   format specified by hook_search_api_data_type_info().
+ *   Otherwise, the definition for the given type, or NULL if it is unknown.
+ *
+ * @see hook_search_api_data_type_info()
+ */
+function search_api_get_data_type_info($type = NULL) {
+  $types = &drupal_static(__FUNCTION__);
+  if (!isset($types)) {
+    $default_types = search_api_default_field_types();
+    $types = module_invoke_all('search_api_data_type_info');
+    $types = $types ? $types : array();
+    foreach ($types as &$type_info) {
+      if (!isset($type_info['fallback']) || !isset($default_types[$type_info['fallback']])) {
+        $type_info['fallback'] = 'string';
+      }
+    }
+    drupal_alter('search_api_data_type_info', $types);
+  }
+  if (isset($type)) {
+    return isset($types[$type]) ? $types[$type] : NULL;
+  }
+  return $types;
+}
+
+/**
+ * Returns either a list of all available service infos, or a specific one.
+ *
+ * @see hook_search_api_service_info()
+ *
+ * @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. 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)) {
+    // 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;
+      }
+    }
+
+    // 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)) {
+    return isset($services[$id]) ? $services[$id] : NULL;
+  }
+  return $services;
+}
+
+/**
+ * Returns information for either all item types, or a specific one.
+ *
+ * @param string|null $type
+ *   If set, the item type whose information should be returned.
+ *
+ * @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(), with the
+ *   addition of a "module" key specifying the module that adds a certain type.
+ *
+ * @see hook_search_api_item_type_info()
+ */
+function search_api_get_item_type_info($type = NULL) {
+  $types = &drupal_static(__FUNCTION__);
+
+  if (!isset($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)) {
+    return isset($types[$type]) ? $types[$type] : NULL;
+  }
+  return $types;
+}
+
+/**
+ * Get a data source controller object for the specified type.
+ *
+ * @param $type
+ *   The type whose data source controller should be returned.
+ *
+ * @return SearchApiDataSourceControllerInterface
+ *   The type's data source controller.
+ *
+ * @throws SearchApiException
+ *   If the type is unknown or specifies an invalid data source controller.
+ */
+function search_api_get_datasource_controller($type) {
+  $datasources = &drupal_static(__FUNCTION__, array());
+  if (empty($datasources[$type])) {
+    $info = search_api_get_item_type_info($type);
+    if (isset($info['datasource controller']) && class_exists($info['datasource controller'])) {
+      $datasources[$type] = new $info['datasource controller']($type);
+    }
+    if (empty($datasources[$type]) || !($datasources[$type] instanceof SearchApiDataSourceControllerInterface)) {
+      unset($datasources[$type]);
+      throw new SearchApiException(t('Unknown or invalid item type @type.', array('@type' => $type)));
+    }
+  }
+  return $datasources[$type];
+}
+
+/**
+ * Returns a list of all available data alter callbacks.
+ *
+ * @see hook_search_api_alter_callback_info()
+ *
+ * @return array
+ *   An array of all available data alter callbacks, keyed by function name.
+ */
+function search_api_get_alter_callbacks() {
+  $callbacks = &drupal_static(__FUNCTION__);
+
+  if (!isset($callbacks)) {
+    $callbacks = module_invoke_all('search_api_alter_callback_info');
+
+    // Fill optional settings with default values.
+    foreach ($callbacks as $id => $callback) {
+      $callbacks[$id] += array('weight' => 0);
+    }
+
+    // Invoke alter hook.
+    drupal_alter('search_api_alter_callback_info', $callbacks);
+  }
+
+  return $callbacks;
+}
+
+/**
+ * Returns a list of all available pre- and post-processors.
+ *
+ * @see hook_search_api_processor_info()
+ *
+ * @return array
+ *   An array of all available processors, keyed by id.
+ */
+function search_api_get_processors() {
+  $processors = &drupal_static(__FUNCTION__);
+
+  if (!isset($processors)) {
+    $processors = module_invoke_all('search_api_processor_info');
+
+    // Fill optional settings with default values.
+    foreach ($processors as $id => $processor) {
+      $processors[$id] += array('weight' => 0);
+    }
+
+    // Invoke alter hook.
+    drupal_alter('search_api_processor_info', $processors);
+  }
+
+  return $processors;
+}
+
+/**
+ * Implements hook_search_api_query_alter().
+ *
+ * Adds node access to the query, if enabled.
+ *
+ * @param SearchApiQueryInterface $query
+ *   The SearchApiQueryInterface object representing the search query.
+ */
+function search_api_search_api_query_alter(SearchApiQueryInterface $query) {
+  global $user;
+  $index = $query->getIndex();
+  // Only add node access if the necessary fields are indexed in the index, and
+  // unless disabled explicitly by the query.
+  $type = $index->getEntityType();
+  if (!empty($index->options['data_alter_callbacks']["search_api_alter_{$type}_access"]['status']) && !$query->getOption('search_api_bypass_access')) {
+    $account = $query->getOption('search_api_access_account', $user);
+    if (is_numeric($account)) {
+      $account = user_load($account);
+    }
+    if (is_object($account)) {
+      try {
+        _search_api_query_add_node_access($account, $query, $type);
+      }
+      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', $user)), WATCHDOG_WARNING);
+    }
+  }
+}
+
+/**
+ * Adds a node access filter to a search query, if applicable.
+ *
+ * @param object $account
+ *   The user object, who searches.
+ * @param SearchApiQueryInterface $query
+ *   The query to which a node access filter should be added, if applicable.
+ * @param string $type
+ *   (optional) The type of search – either "node" or "comment". Defaults to
+ *   "node".
+ *
+ * @throws SearchApiException
+ *   If not all necessary fields are indexed on the index.
+ */
+function _search_api_query_add_node_access($account, SearchApiQueryInterface $query, $type = 'node') {
+  // Don't do anything if the user can access all content.
+  if (user_access('bypass node access', $account)) {
+    return;
+  }
+
+  $is_comment = ($type == 'comment');
+
+  // Check whether the necessary fields are indexed.
+  $fields = $query->getIndex()->options['fields'];
+  $required = array('search_api_access_node', 'status');
+  if (!$is_comment) {
+    $required[] = 'author';
+  }
+  foreach ($required as $field) {
+    if (empty($fields[$field])) {
+      $vars['@field'] = $field;
+      $vars['@index'] = $query->getIndex()->name;
+      throw new SearchApiException(t('Required field @field not indexed on index @index. Could not perform access checks.', $vars));
+    }
+  }
+
+  // If the user cannot access content/comments at all, return no results.
+  if (!user_access('access content', $account) || ($is_comment && !user_access('access content', $account))) {
+    // Simple hack for returning no results.
+    $query->condition('status', 0);
+    $query->condition('status', 1);
+    watchdog('search_api', 'User @name tried to execute a search, but cannot access content.', array('@name' => theme('username', array('account' => $account))), WATCHDOG_NOTICE);
+    return;
+  }
+
+  // Filter by the "published" status.
+  $published = $is_comment ? COMMENT_PUBLISHED : NODE_PUBLISHED;
+  if (!$is_comment && user_access('view own unpublished content')) {
+    $filter = $query->createFilter('OR');
+    $filter->condition('status', $published);
+    $filter->condition('author', $account->uid);
+    $query->filter($filter);
+  }
+  else {
+    // /!\ in previous patches i commented the next line, why ? maybe will have to do it again
+    $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);
+}
+
+/**
+ * Determines whether a field of the given type contains text data.
+ *
+ * Can also be used to find other types.
+ *
+ * @param string $type
+ *   The type for which to check.
+ * @param array $allowed
+ *   Optionally, an array of allowed types.
+ *
+ * @return
+ *   TRUE if $type is either one of the specified types, or a list of such
+ *   values. FALSE otherwise.
+ *
+ * @see search_api_extract_inner_type()
+ */
+function search_api_is_text_type($type, array $allowed = array('text')) {
+  return array_search(search_api_extract_inner_type($type), $allowed) !== FALSE;
+}
+
+/**
+ * Utility function for determining whether a field of the given type contains
+ * a list of any kind.
+ *
+ * @param $type
+ *   A string containing the type to check.
+ *
+ * @return
+ *   TRUE iff $type is a list type ("list<*>").
+ */
+function search_api_is_list_type($type) {
+  return substr($type, 0, 5) == 'list<';
+}
+
+/**
+ * Utility function for determining the nesting level of a list type.
+ *
+ * @param $type
+ *   A string containing the type to check.
+ *
+ * @return
+ *   The nesting level of the type. 0 for singular types, 1 for lists of
+ *   singular types, etc.
+ */
+function search_api_list_nesting_level($type) {
+  $level = 0;
+  while (search_api_is_list_type($type)) {
+    $type = substr($type, 5, -1);
+    ++$level;
+  }
+  return $level;
+}
+
+/**
+ * Utility function for nesting a type to the same level as another type.
+ * I.e., after <code>$t = search_api_nest_type($type, $nested_type);</code> is
+ * executed, the following statements will always be true:
+ * @code
+ * search_api_list_nesting_level($t) == search_api_list_nesting_level($nested_type);
+ * search_api_extract_inner_type($t) == search_api_extract_inner_type($type);
+ * @endcode
+ *
+ * @param $type
+ *   The type to wrap.
+ * @param $nested_type
+ *   Another type, determining the nesting level.
+ *
+ * @return
+ *   A list version of $type, as specified above.
+ */
+function search_api_nest_type($type, $nested_type) {
+  while (search_api_is_list_type($nested_type)) {
+    $nested_type = substr($nested_type, 5, -1);
+    $type = "list<$type>";
+  }
+  return $type;
+}
+
+/**
+ * Utility function for extracting the contained primitive type of a list type.
+ *
+ * @param $type
+ *   A string containing the list type to process.
+ *
+ * @return
+ *   A string containing the primitive type contained within the list, e.g.
+ *   "text" for "list<text>" (or for "list<list<text>>"). If $type is no list
+ *   type, it is returned unchanged.
+ */
+function search_api_extract_inner_type($type) {
+  while (search_api_is_list_type($type)) {
+    $type = substr($type, 5, -1);
+  }
+  return $type;
+}
+
+/**
+ * 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
+ * uncoditionally 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 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 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()) {
+  // 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 &$info) {
+      $info['value'] = array();
+      $info['original_type'] = $info['type'];
+    }
+    unset($info);
+    try {
+      foreach ($wrapper as $w) {
+        $nested_fields = search_api_extract_fields($w, $fields, $value_options);
+        foreach ($nested_fields as $field => $info) {
+          if (isset($info['value'])) {
+            $fields[$field]['value'][] = $info['value'];
+          }
+          if (isset($info['original_type'])) {
+            $fields[$field]['original_type'] = $info['original_type'];
+          }
+        }
+      }
+    }
+    catch (EntityMetadataWrapperException $e) {
+      // Catch exceptions caused by not set list values.
+    }
+    return $fields;
+  }
+
+  $nested = array();
+  $entity_infos = entity_get_info();
+  foreach ($fields as $field => &$info) {
+    $pos = strpos($field, ':');
+    if ($pos === FALSE) {
+      // Set "defaults" in case an error occurs later.
+      $info['value'] = NULL;
+      $info['original_type'] = $info['type'];
+      if (isset($wrapper->$field)) {
+        try {
+          $info['value'] = $wrapper->$field->value($value_options);
+          // For fulltext fields with options, also include the option labels.
+          if (search_api_is_text_type($info['type']) && $wrapper->$field->optionsList('view')) {
+            _search_api_add_option_values($info['value'], $wrapper->$field->optionsList('view'));
+          }
+          $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.
+        }
+      }
+    }
+    else {
+      list($prefix, $key) = explode(':', $field, 2);
+      $nested[$prefix][$key] = $info;
+    }
+  }
+  unset($info);
+
+  foreach ($nested as $prefix => $nested_fields) {
+    if (isset($wrapper->$prefix)) {
+      $nested_fields = search_api_extract_fields($wrapper->$prefix, $nested_fields, $value_options);
+      # http://drupal.org/node/1873910#comment-6876200
+      // $subwrapper = $wrapper->$prefix;
+      // $subwrapper->language( $wrapper->language->value() );
+      // $nested_fields = search_api_extract_fields($subwrapper, $nested_fields, $value_options);  
+      foreach ($nested_fields as $field => $info) {
+        $fields["$prefix:$field"] = $info;
+      }
+    }
+    else {
+      foreach ($nested_fields as &$info) {
+        $info['value'] = NULL;
+        $info['original_type'] = $info['type'];
+      }
+    }
+  }
+  return $fields;
+}
+
+/**
+ * Helper method for adding additional text data to fields with an option list.
+ */
+function _search_api_add_option_values(&$value, array $options) {
+  if (is_array($value)) {
+    foreach ($value as &$v) {
+      _search_api_add_option_values($v, $options);
+    }
+    return;
+  }
+  if (is_scalar($value) && isset($options[$value])) {
+    $value .= ' ' . $options[$value];
+  }
+}
+
+/**
+ * Helper method for extracting the ID (and possibly label) of an entity-valued field.
+ */
+function _search_api_extract_entity_value(EntityMetadataWrapper $wrapper, $fulltext = FALSE) {
+  $v = $wrapper->value();
+  if (is_array($v)) {
+    $ret = array();
+    foreach ($wrapper as $item) {
+      $values = _search_api_extract_entity_value($item, $fulltext);
+      if ($values) {
+        $ret[] = $values;
+      }
+    }
+    return $ret;
+  }
+  if ($v) {
+    $ret = $wrapper->getIdentifier();
+    if ($fulltext && ($label = $wrapper->label())) {
+      $ret .= ' ' . $label;
+    }
+    return $ret;
+  }
+  return NULL;
+}
+
+/**
+ * Load the search server with the specified id.
+ *
+ * @param $id
+ *   The search server's id.
+ * @param $reset
+ *   Whether to reset the internal cache.
+ *
+ * @return SearchApiServer
+ *   An object representing the server with the specified id.
+ */
+function search_api_server_load($id, $reset = FALSE) {
+  $ret = search_api_server_load_multiple(array($id), array(), $reset);
+  return $ret ? reset($ret) : FALSE;
+}
+
+/**
+ * Load multiple servers at once, determined by IDs or machine names, or by
+ * other conditions.
+ *
+ * @see entity_load()
+ *
+ * @param array|false $ids
+ *   An array of server IDs or machine names, or FALSE to load all servers.
+ * @param array $conditions
+ *   An array of conditions on the {search_api_server} table in the form
+ *   'field' => $value.
+ * @param bool $reset
+ *   Whether to reset the internal entity_load cache.
+ *
+ * @return SearchApiServer[]
+ *   An array of server objects keyed by machine name.
+ */
+function search_api_server_load_multiple($ids = array(), $conditions = array(), $reset = FALSE) {
+  $servers = entity_load('search_api_server', $ids, $conditions, $reset);
+  return entity_key_array_by_property($servers, 'machine_name');
+}
+
+/**
+ * Entity uri callback.
+ */
+function search_api_server_url(SearchApiServer $server) {
+  return array(
+    'path' => 'admin/config/search/search_api/server/' . $server->machine_name,
+    'options' => array(),
+  );
+}
+
+/**
+ * Title callback for determining which title should be displayed for the
+ * "delete" local task.
+ *
+ * @param Entity $entity
+ *   The server or index for which the menu link is displayed.
+ *
+ * @return string
+ *   A translated version of either "Delete" or "Revert".
+ */
+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.
+ *
+ * @param Entity $entity
+ *   The server or index for which the access to the delete page is checked.
+ *
+ * @return
+ *   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.
+ *
+ * @param array $values
+ *   An array containing the values to be inserted.
+ *
+ * @return
+ *   The newly inserted server's id, or FALSE on error.
+ */
+function search_api_server_insert(array $values) {
+  $server = entity_create('search_api_server', $values);
+  $server->is_new = TRUE;
+  $server->save();
+  return $server->id;
+}
+
+/**
+ * Changes a server's settings.
+ *
+ * @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 int|false
+ *   1 if fields were changed, 0 if the fields already had the desired values.
+ *   FALSE on failure.
+ */
+function search_api_server_edit($id, array $fields) {
+  $server = search_api_server_load($id, TRUE);
+  $ret = $server->update($fields);
+  return $ret ? 1 : $ret;
+}
+
+/**
+ * Enables a search server.
+ *
+ * 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 int|false
+ *   1 on success, 0 or FALSE on failure.
+ */
+function search_api_server_enable($id) {
+  $server = search_api_server_load($id, TRUE);
+  $ret = $server->update(array('enabled' => 1));
+  return $ret ? 1 : $ret;
+}
+
+/**
+ * Disables a search server.
+ *
+ * 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 int|false
+ *   1 on success, 0 or FALSE on failure.
+ */
+function search_api_server_disable($id) {
+  $server = search_api_server_load($id, TRUE);
+  $ret = $server->update(array('enabled' => 0));
+  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
+ *   1 on success, 0 or FALSE on failure.
+ */
+function search_api_server_delete($id) {
+  $server = search_api_server_load($id, TRUE);
+  $server->delete();
+  return 1;
+}
+
+/**
+ * Loads the Search API index with the specified id.
+ *
+ * @param $id
+ *   The index' id.
+ * @param $reset
+ *   Whether to reset the internal cache.
+ *
+ * @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 reset($ret);
+}
+
+/**
+ * Load multiple indexes at once, determined by IDs or machine names, or by
+ * other conditions.
+ *
+ * @see entity_load()
+ *
+ * @param array|false $ids
+ *   An array of index IDs or machine names, or FALSE to load all indexes.
+ * @param array $conditions
+ *   An array of conditions on the {search_api_index} table in the form
+ *   'field' => $value.
+ * @param bool $reset
+ *   Whether to reset the internal entity_load cache.
+ *
+ * @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();
+  $indexes = entity_load('search_api_index', $ids, $conditions, $reset);
+  return entity_key_array_by_property($indexes, 'machine_name');
+}
+
+/**
+ * Determines a search index' indexing status.
+ *
+ * @param SearchApiIndex $index
+ *   The index whose indexing status should be determined.
+ *
+ * @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.
+ */
+function search_api_index_status(SearchApiIndex $index) {
+  return $index->datasource()->getIndexStatus($index);
+}
+
+/**
+ * Entity uri callback.
+ */
+function search_api_index_url(SearchApiIndex $index) {
+  return array(
+    'path' => 'admin/config/search/search_api/index/' . $index->machine_name,
+    'options' => array(),
+  );
+}
+
+/**
+ * Returns an index's server.
+ *
+ * Used as a property getter callback for the index's "server_entity" prioperty
+ * in search_api_entity_property_info().
+ *
+ * @param SearchApiIndex $index
+ *   The index whose server should be returned.
+ *
+ * @return SearchApiServer
+ *   The server this index currently resides on, or NULL if the index is
+ * currently unassigned.
+ */
+function search_api_index_get_server(SearchApiIndex $index) {
+  return $index->server();
+}
+
+/**
+ * Returns an options list for the "status" property.
+ *
+ * Used as an options list callback in search_api_entity_property_info().
+ *
+ * @return array
+ *   An array of options, as defined by hook_options_list().
+ */
+function search_api_status_options_list() {
+  return array(
+    ENTITY_CUSTOM => t('Custom'),
+    ENTITY_IN_CODE => t('Default'),
+    ENTITY_OVERRIDDEN => t('Overridden'),
+    ENTITY_FIXED => t('Fixed'),
+  );
+}
+
+/**
+ * Inserts a new search index into the database.
+ *
+ * @param array $values
+ *   An array containing the values to be inserted.
+ *
+ * @return
+ *   The newly inserted index' id, or FALSE on error.
+ */
+function search_api_index_insert(array $values) {
+  $index = entity_create('search_api_index', $values);
+  $index->is_new = TRUE;
+  $index->save();
+  return $index->id;
+}
+
+/**
+ * Changes an index' settings.
+ *
+ * @param int|string $id
+ *   The edited index' ID or machine name.
+ * @param array $fields
+ *   The new field values to set.
+ *
+ * @return int|false
+ *   1 if fields were changed, 0 if the fields already had the desired values.
+ *   FALSE on failure.
+ */
+function search_api_index_edit($id, array $fields) {
+  $index = search_api_index_load($id, TRUE);
+  $ret = $index->update($fields);
+  return $ret ? 1 : $ret;
+}
+
+/**
+ * Changes an index' indexed field settings.
+ *
+ * @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 int|false
+ *   1 if the field settings were changed, 0 if they already had the desired
+ *   values. FALSE on failure.
+ */
+function search_api_index_edit_fields($id, array $fields) {
+  $index = search_api_index_load($id, TRUE);
+  $options = $index->options;
+  $options['fields'] = $fields;
+  $ret = $index->update(array('options' => $options));
+  return $ret ? 1 : $ret;
+}
+
+/**
+ * Enables a search index.
+ *
+ * @param $id
+ *   The ID or machine name of the index to enable.
+ *
+ * @throws SearchApiException
+ *   If the index' server isn't enabled.
+ *
+ * @return
+ *   1 on success, 0 or FALSE on failure.
+ */
+function search_api_index_enable($id) {
+  $index = search_api_index_load($id, TRUE);
+  $ret = $index->update(array('enabled' => 1));
+  return $ret ? 1 : $ret;
+}
+
+/**
+ * Disables a search index.
+ *
+ * @param $id
+ *   The ID or machine name of the index to disable.
+ *
+ * @return
+ *   1 on success, 0 or FALSE on failure.
+ */
+function search_api_index_disable($id) {
+  $index = search_api_index_load($id, TRUE);
+  $ret = $index->update(array('enabled' => 0));
+  return $ret ? 1 : $ret;
+}
+
+/**
+ * Schedules a search index for re-indexing.
+ *
+ * @param $id
+ *   The ID or machine name of the index to re-index.
+ *
+ * @return
+ *   TRUE on success, FALSE on failure.
+ */
+function search_api_index_reindex($id) {
+  $index = search_api_index_load($id);
+  return $index->reindex();
+}
+
+/**
+ * Helper method for marking all items on an index as needing re-indexing.
+ *
+ * @param SearchApiIndex $index
+ *   The index whose items should be re-indexed.
+ */
+function _search_api_index_reindex(SearchApiIndex $index) {
+  $index->datasource()->trackItemChange(FALSE, array($index), TRUE);
+}
+
+/**
+ * Clears a search index and schedules all of its items for re-indexing.
+ *
+ * @param $id
+ *   The ID or machine name of the index to clear.
+ *
+ * @return
+ *   TRUE on success, FALSE on failure.
+ */
+function search_api_index_clear($id) {
+  $index = search_api_index_load($id);
+  return $index->clear();
+}
+
+/**
+ * Deletes a search index.
+ *
+ * @param $id
+ *   The ID or machine name of the index to delete.
+ *
+ * @return
+ *   TRUE on success, FALSE on failure.
+ */
+function search_api_index_delete($id) {
+  $index = search_api_index_load($id);
+  if (!$index) {
+    return FALSE;
+  }
+  $index->delete();
+  return TRUE;
+}
+
+/**
+ * Options list callback for search indexes.
+ *
+ * @return array
+ *   An array of search index machine names mapped to their human-readable
+ *   names.
+ */
+function search_api_index_options_list() {
+  $ret = array(
+    NULL => '- ' . t('All') . ' -',
+  );
+  foreach (search_api_index_load_multiple(FALSE) as $id => $index) {
+    $ret[$id] = $index->name;
+  }
+  return $ret;
+}
+
+/**
+ * Shutdown function which indexes all queued items, if any.
+ */
+function _search_api_index_queued_items() {
+  $queue = &search_api_index_specific_items_delayed();
+
+  try {
+    if ($queue) {
+      $indexes = search_api_index_load_multiple(array_keys($queue));
+      foreach ($indexes as $index_id => $index) {
+        search_api_index_specific_items($index, $queue[$index_id]);
+      }
+    }
+
+    // Reset the queue so we don't index the items twice by accident.
+    $queue = array();
+  }
+  catch (SearchApiException $e) {
+    watchdog_exception('search_api', $e);
+  }
+}
+
+/**
+ * Helper function to be used as a "property info alter" callback.
+ *
+ * If a wrapped entity is passed to this function, all its available properties
+ * and fields, regardless of bundle, are added to the wrapper.
+ */
+function _search_api_wrapper_add_all_properties(EntityMetadataWrapper $wrapper, array $property_info) {
+  if ($properties = entity_get_all_property_info($wrapper->type())) {
+    $property_info['properties'] = $properties;
+  }
+  return $property_info;
+}
+
+/**
+ * Helper function for converting data to a custom type.
+ */
+function _search_api_convert_custom_type($callback, $value, $original_type, $type, $nesting_level) {
+  if ($nesting_level == 0) {
+    return call_user_func($callback, $value, $original_type, $type);
+  }
+  if (!is_array($value)) {
+    return NULL;
+  }
+  --$nesting_level;
+  $values = array();
+  foreach ($value as $v) {
+    $v = _search_api_convert_custom_type($callback, $v, $original_type, $type, $nesting_level);
+    if (isset($v) && !(is_array($v) && !$v)) {
+      $values[] = $v;
+    }
+  }
+  return $values;
+}
+
+/**
+ * Determines the number of items indexed on a server for a certain index.
+ *
+ * Used as a helper function in search_api_admin_index_view().
+ *
+ * @param SearchApiIndex $index
+ *   The index
+ *
+ * @return int
+ *   The number of items found on the server for this index, if the latter is
+ *   enabled. 0 otherwise.
+ */
+function _search_api_get_items_on_server(SearchApiIndex $index) {
+  if (!$index->enabled) {
+    return 0;
+  }
+  // We want the raw count, without facets or other filters. Therefore we don't
+  // use the query's execute() method but pass it straight to the server for
+  // evaluation. Since this circumvents the normal preprocessing, which sets the
+  // fields (on which some service classes might even rely when there are no
+  // keywords), we set them manually here.
+  $query = $index->query()
+    ->fields(array())
+    ->range(0, 0);
+  $response = $index->server()->search($query);
+  return $response['result count'];
+}
+
+/**
+ * Returns a deep copy of the input array.
+ *
+ * The behavior of PHP regarding arrays with references pointing to it is rather
+ * weird. Therefore, we use this helper function in theme_search_api_index() to
+ * create safe copies of such arrays.
+ *
+ * @param array $array
+ *   The array to copy.
+ *
+ * @return array
+ *   A deep copy of the array.
+ */
+function _search_api_deep_copy(array $array) {
+  $copy = array();
+  foreach ($array as $k => $v) {
+    if (is_array($v)) {
+      $copy[$k] = _search_api_deep_copy($v);
+    }
+    elseif (is_object($v)) {
+      $copy[$k] = clone $v;
+    }
+    elseif ($v) {
+      $copy[$k] = $v;
+    }
+  }
+  return $copy;
+}
+
+/**
+ * Creates and sets a batch for indexing items.
+ *
+ * @param SearchApiIndex $index
+ *   The index for which items should be indexed.
+ * @param int $batch_size
+ *   Number of items to index per batch.
+ * @param int $limit
+ *   Maximum number of items to index. Negative values mean "no limit".
+ * @param int $remaining
+ *   Remaining items to index.
+ * @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) {
+    $t = !empty($drush) ? 'dt' : 't';
+
+    if ($limit < 0 || $limit > $remaining) {
+      $limit = $remaining;
+    }
+    if ($batch_size < 0) {
+      $batch_size = $remaining;
+    }
+    $batch = array(
+      'title' => $t('Indexing items'),
+      'operations' => array(
+        array('_search_api_batch_indexing_callback', array($index, $batch_size, $limit, $drush)),
+      ),
+      'progress_message' => $t('Completed about @percentage% of the indexing operation.'),
+      'finished' => '_search_api_batch_indexing_finished',
+      'file' => drupal_get_path('module', 'search_api') . '/search_api.module',
+    );
+    batch_set($batch);
+    return TRUE;
+  }
+  return FALSE;
+}
+
+/**
+ * Batch API callback for the indexing functionality.
+ *
+ * @param SearchApiIndex $index
+ *   The index for which items should be indexed.
+ * @param integer $batch_size
+ *   Number of items to index per batch.
+ * @param integer $limit
+ *   Maximum number of items to index.
+ * @param boolean $drush
+ *   Boolean specifying whether this was called from drush or not.
+ * @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, &$context) {
+  // Persistent data among batch runs.
+  if (!isset($context['sandbox']['limit'])) {
+    $context['sandbox']['limit'] = $limit;
+    $context['sandbox']['batch_size'] = $batch_size;
+    $context['sandbox']['progress'] = 0;
+  }
+
+  // Persistent data for results.
+  if (!isset($context['results']['indexed'])) {
+    $context['results']['indexed'] = 0;
+    $context['results']['not indexed'] = 0;
+    $context['results']['drush'] = $drush;
+  }
+
+  // Number of items to index for this run.
+  $to_index = min($context['sandbox']['limit'] - $context['sandbox']['progress'], $context['sandbox']['batch_size']);
+
+  // Index the items.
+  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) {
+    $format_plural = $context['results']['drush'] === TRUE ? '_search_api_drush_format_plural' : 'format_plural';
+    $context['message'] = $format_plural($context['results']['indexed'], 'Successfully indexed 1 item.', 'Successfully indexed @count items.');
+  }
+
+  // Some items couldn't be indexed.
+  if ($indexed !== $to_index) {
+    $context['results']['not indexed'] += $to_index - $indexed;
+  }
+
+  $context['sandbox']['progress'] += $to_index;
+
+  // Everything has been indexed.
+  if ($indexed === 0 || $context['sandbox']['progress'] >= $context['sandbox']['limit']) {
+    $context['finished'] = 1;
+  }
+  else {
+    $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['limit'];
+  }
+}
+
+/**
+ * Batch API finishing callback for the indexing functionality.
+ *
+ * @param boolean $success
+ *   Whether the batch finished successfully.
+ * @param array $results
+ *   Detailed informations about the result.
+ */
+function _search_api_batch_indexing_finished($success, $results) {
+  // Check if called from drush.
+  if (!empty($results['drush'])) {
+    $drupal_set_message = 'drush_log';
+    $format_plural = '_search_api_drush_format_plural';
+    $t = 'dt';
+    $success_message = 'success';
+  }
+  else {
+    $drupal_set_message = 'drupal_set_message';
+    $format_plural = 'format_plural';
+    $t = 't';
+    $success_message = 'status';
+  }
+
+  // Display result messages.
+  if ($success) {
+    if (!empty($results['indexed'])) {
+      $drupal_set_message($format_plural($results['indexed'], 'Successfully indexed 1 item.', 'Successfully indexed @count items.'), $success_message);
+
+      if (!empty($results['not indexed'])) {
+       $drupal_set_message($format_plural($results['not indexed'], '1 item could not be indexed. Check the logs for details.', '@count items could not be indexed. Check the logs for details.'), 'warning');
+      }
+    }
+    else {
+      $drupal_set_message($t("Couldn't index items. Check the logs for details."), 'error');
+    }
+  }
+  else {
+    $drupal_set_message($t("An error occurred while trying to index items. Check the logs for details."), 'error');
+  }
+
+}

+ 89 - 0
sites/all/modules/contrib/search/search_api/search_api.rules.inc

@@ -0,0 +1,89 @@
+<?php
+
+/**
+ * @file
+ * Search API Rules integration.
+ */
+
+
+/**
+ * Implements hook_rules_action_info().
+ */
+function search_api_rules_action_info() {
+  $items['search_api_index'] = array (
+   'parameter' => array(
+      'entity' => array(
+        'type' => 'entity',
+        'label' => t('Entity'),
+        'description' => t('The item to index.'),
+      ),
+      'index' => array(
+        'type' => 'search_api_index',
+        'label' => t('Index'),
+        'description' => t('The index on which the item should be indexed. Leave blank to index on all indexes for this item type.'),
+        'optional' => TRUE,
+        'options list' => 'search_api_index_options_list',
+      ),
+      'index_immediately' => array(
+        'type' => 'boolean',
+        'label' => t('Index immediately'),
+        'description' => t('Activate for indexing the item right away, otherwise it will only be marked as dirty and indexed during the next cron run.'),
+        'optional' => TRUE,
+        'default value' => TRUE,
+        'restriction' => 'input',
+      ),
+    ),
+    'group' => t('Search API'),
+    'access callback' => '_search_api_rules_access',
+    'label' => t('Index an entity'),
+    'base' => '_search_api_rules_action_index',
+  );
+  return $items;
+}
+
+/**
+ * Rules access callback for search api actions.
+ */
+function _search_api_rules_access() {
+  return user_access('administer search_api');
+}
+
+/**
+ * Rules action for indexing an item.
+ */
+function _search_api_rules_action_index(EntityDrupalWrapper $wrapper, SearchApiIndex $index = NULL, $index_immediately = TRUE) {
+  $type = $wrapper->type();
+  $item_ids = array($wrapper->getIdentifier());
+
+  if (empty($index) && !$index_immediately) {
+    search_api_track_item_change($type, $item_ids);
+    return;
+  }
+
+  if ($index) {
+    $indexes = array($index);
+  }
+  else {
+    $conditions = array(
+      'enabled' => 1,
+      'item_type' => $type,
+      'read_only' => 0,
+    );
+    $indexes = search_api_index_load_multiple(FALSE, $conditions);
+    if (!$indexes) {
+      return;
+    }
+  }
+  if ($index_immediately) {
+    foreach ($indexes as $index) {
+      search_api_index_specific_items_delayed($index, $item_ids);
+    }
+  }
+  else {
+    search_api_get_datasource_controller($type)->trackItemChange($item_ids, $indexes);
+  }
+}
+
+function _search_api_rules_action_index_help() {
+  return t('Queues an item for reindexing. If "index immediately" is disabled then the item will be indexed during the next cron run.');
+}

+ 1125 - 0
sites/all/modules/contrib/search/search_api/search_api.test

@@ -0,0 +1,1125 @@
+<?php
+
+/**
+ * @file
+ * Contains the SearchApiWebTest and the SearchApiUnitTest classes.
+ */
+
+/**
+ * Class for testing Search API functionality via the UI.
+ */
+class SearchApiWebTest extends DrupalWebTestCase {
+
+  /**
+   * The machine name of the created test server.
+   *
+   * @var string
+   */
+  protected $server_id;
+
+  /**
+   * The machine name of the created test index.
+   *
+   * @var string
+   */
+  protected $index_id;
+
+  /**
+   * Overrides DrupalWebTestCase::assertText().
+   *
+   * Changes the default message to be just the text checked for.
+   */
+  protected function assertText($text, $message = '', $group = 'Other') {
+    return parent::assertText($text, $message ? $message : $text, $group);
+  }
+
+  /**
+   * Overrides DrupalWebTestCase::drupalGet().
+   *
+   * Additionally asserts that the HTTP request returned a 200 status code.
+   */
+  protected function drupalGet($path, array $options = array(), array $headers = array()) {
+    $ret = parent::drupalGet($path, $options, $headers);
+    $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, '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',
+      'description' => 'Tests basic functions of the Search API, like creating, editing and deleting servers and indexes.',
+      'group' => 'Search API',
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp('entity', 'search_api', 'search_api_test');
+  }
+
+  /**
+   * Tests correct admin UI, indexing and search behavior.
+   *
+   * We only use a single test method to avoid wasting ressources on setting up
+   * the test environment multiple times. This will be the only method called
+   * by the Simpletest framework (since the method name starts with "test"). It
+   * in turn calls other methdos that set up the environment in a certain way
+   * and then run tests on it.
+   */
+  public function testFramework() {
+    $this->drupalLogin($this->drupalCreateUser(array('administer search_api')));
+    $this->insertItems();
+    $this->createIndex();
+    $this->insertItems();
+    $this->createServer();
+    $this->checkOverview();
+    $this->enableIndex();
+    $this->searchNoResults();
+    $this->indexItems();
+    $this->searchSuccess();
+    $this->checkIndexingOrder();
+    $this->editServer();
+    $this->clearIndex();
+    $this->searchNoResults();
+    $this->deleteServer();
+    $this->disableModules();
+  }
+
+  /**
+   * Returns the test server in use by this test case.
+   *
+   * @return SearchApiServer
+   *   The test server.
+   */
+  protected function server() {
+    return search_api_server_load($this->server_id, TRUE);
+  }
+
+  /**
+   * Returns the test index in use by this test case.
+   *
+   * @return SearchApiIndex
+   *   The test index.
+   */
+  protected function index() {
+    return search_api_index_load($this->index_id, TRUE);
+  }
+
+  /**
+   * Inserts some test items into the database, via the test module.
+   *
+   * @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();
+    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, $number, "$number items successfully inserted.");
+  }
+
+  /**
+   * 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'));
+  }
+
+  /**
+   * Creates a test index via the UI and tests whether this works correctly.
+   */
+  protected function createIndex() {
+    $values = array(
+      'name' => '',
+      'item_type' => '',
+      'enabled' => 1,
+      'description' => 'An index used for testing.',
+      'server' => '',
+      'options[cron_limit]' => 5,
+    );
+    $this->drupalPost('admin/config/search/search_api/add_index', $values, t('Create index'));
+    $this->assertText(t('!name field is required.', array('!name' => t('Index name'))));
+    $this->assertText(t('!name field is required.', array('!name' => t('Item type'))));
+
+    $this->index_id = $id = 'test_index';
+    $values = array(
+      'name' => 'Search API test index',
+      'machine_name' => $id,
+      'item_type' => 'search_api_test',
+      'enabled' => 1,
+      'description' => 'An index used for testing.',
+      'server' => '',
+      'options[cron_limit]' => 1,
+    );
+    $this->drupalPost(NULL, $values, t('Create index'));
+
+    $this->assertText(t('The index was successfully created. Please set up its indexed fields now.'), 'The index was successfully created.');
+    $found = strpos($this->getUrl(), 'admin/config/search/search_api/index/' . $id) !== FALSE;
+    $this->assertTrue($found, 'Correct redirect.');
+    $index = $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.'), 'Successfully added fields.');
+    $this->assertText('Parent » ID', 'Added fields are displayed.');
+
+    $values = array(
+      'fields[id][type]' => 'integer',
+      'fields[id][boost]' => '1.0',
+      'fields[id][indexed]' => 1,
+      'fields[title][type]' => 'text',
+      'fields[title][boost]' => '5.0',
+      'fields[title][indexed]' => 1,
+      'fields[body][type]' => 'text',
+      'fields[body][boost]' => '1.0',
+      'fields[body][indexed]' => 1,
+      'fields[type][type]' => 'string',
+      'fields[type][boost]' => '1.0',
+      'fields[type][indexed]' => 1,
+      'fields[parent:id][type]' => 'integer',
+      'fields[parent:id][boost]' => '1.0',
+      'fields[parent:id][indexed]' => 1,
+      'fields[parent:title][type]' => 'text',
+      'fields[parent:title][boost]' => '5.0',
+      'fields[parent:title][indexed]' => 1,
+      'fields[parent:body][type]' => 'text',
+      'fields[parent:body][boost]' => '1.0',
+      'fields[parent:body][indexed]' => 1,
+      'fields[parent:type][type]' => 'string',
+      'fields[parent:type][boost]' => '1.0',
+      '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.'), 'Field settings saved.');
+
+    $values = array(
+      'callbacks[search_api_alter_add_url][status]' => 1,
+      'callbacks[search_api_alter_add_url][weight]' => 0,
+      'callbacks[search_api_alter_add_aggregation][status]' => 1,
+      'callbacks[search_api_alter_add_aggregation][weight]' => 10,
+      'processors[search_api_case_ignore][status]' => 1,
+      'processors[search_api_case_ignore][weight]' => 0,
+      'processors[search_api_case_ignore][settings][fields][title]' => 1,
+      'processors[search_api_case_ignore][settings][fields][body]' => 1,
+      'processors[search_api_case_ignore][settings][fields][parent:title]' => 1,
+      'processors[search_api_case_ignore][settings][fields][parent:body]' => 1,
+      'processors[search_api_tokenizer][status]' => 1,
+      'processors[search_api_tokenizer][weight]' => 20,
+      'processors[search_api_tokenizer][settings][spaces]' => '[^\p{L}\p{N}]',
+      'processors[search_api_tokenizer][settings][ignorable]' => '[-]',
+      'processors[search_api_tokenizer][settings][fields][title]' => 1,
+      'processors[search_api_tokenizer][settings][fields][body]' => 1,
+      'processors[search_api_tokenizer][settings][fields][parent:title]' => 1,
+      'processors[search_api_tokenizer][settings][fields][parent:body]' => 1,
+    );
+    $this->drupalPost(NULL, $values, t('Add new field'));
+    $values = array(
+      'callbacks[search_api_alter_add_aggregation][settings][fields][search_api_aggregation_1][name]' => 'Test fulltext field',
+      'callbacks[search_api_alter_add_aggregation][settings][fields][search_api_aggregation_1][type]' => 'fulltext',
+      'callbacks[search_api_alter_add_aggregation][settings][fields][search_api_aggregation_1][fields][title]' => 1,
+      'callbacks[search_api_alter_add_aggregation][settings][fields][search_api_aggregation_1][fields][body]' => 1,
+      'callbacks[search_api_alter_add_aggregation][settings][fields][search_api_aggregation_1][fields][parent:title]' => 1,
+      '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 indexing workflow was successfully edited. All content was scheduled for re-indexing so the new settings can take effect."), 'Workflow successfully edited.');
+
+    $this->drupalGet("admin/config/search/search_api/index/$id");
+    $this->assertTitle('Search API test index | Drupal', 'Correct title when viewing index.');
+    $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' => '',
+      'enabled' => 1,
+      'description' => 'A server used for testing.',
+      'class' => '',
+    );
+    $this->drupalPost('admin/config/search/search_api/add_server', $values, t('Create server'));
+    $this->assertText(t('!name field is required.', array('!name' => t('Server name'))));
+    $this->assertText(t('!name field is required.', array('!name' => t('Service class'))));
+
+    $this->server_id = $id = 'test_server';
+    $values = array(
+      'name' => 'Search API test server',
+      'machine_name' => $id,
+      'enabled' => 1,
+      'description' => 'A server used for testing.',
+      'class' => 'search_api_test_service',
+    );
+    $this->drupalPost(NULL, $values, t('Create server'));
+
+    $values2 = array(
+      'options[form][test]' => 'search_api_test foo bar',
+    );
+    $this->drupalPost(NULL, $values2, t('Create server'));
+
+    $this->assertText(t('The server was successfully created.'));
+    $found = strpos($this->getUrl(), 'admin/config/search/search_api/server/' . $id) !== FALSE;
+    $this->assertTrue($found, 'Correct redirect.');
+    $server = $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.');
+  }
+
+  /**
+   * Checks whether the server and index are correctly listed in the overview.
+   */
+  protected function checkOverview() {
+    $this->drupalGet('admin/config/search/search_api');
+    $this->assertText('Search API test server', 'Server displayed.');
+    $this->assertText('Search API test index', 'Index displayed.');
+    $this->assertNoText(t('There are no search servers or indexes defined yet.'), '"No servers" message not displayed.');
+  }
+
+  /**
+   * Moves the index onto the server and enables it.
+   */
+  protected function enableIndex() {
+    $values = array(
+      'server' => $this->server_id,
+    );
+    $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', 'Server displayed.');
+
+    $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() {
+    $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->checkIndexStatus();
+
+    // Here we test the indexing + the warning message when some items
+    // 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.'), '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, 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))) . '/', '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.');
+
+    // 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."), "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() {
+    $results = $this->doSearch();
+    $this->assertEqual($results['result count'], 10, 'Correct search result count returned after indexing.');
+    $this->assertEqual(array_keys($results['results']), array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10), 'Correct search results returned after indexing.');
+
+    $results = $this->doSearch(2, 4);
+    $this->assertEqual($results['result count'], 10, 'Correct search result count with ranged query.');
+    $this->assertEqual(array_keys($results['results']), array(3, 4, 5, 6), 'Correct search results with ranged query.');
+  }
+
+  /**
+   * 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',
+      'description' => 'test-description-bar',
+      'options[form][test]' => 'test-test-baz',
+    );
+    $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', '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}", array(), t('Clear all indexed data'));
+    $this->drupalPost(NULL, array(), t('Confirm'));
+    $this->assertText(t('The index was successfully cleared.'));
+    $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', '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'), FALSE);
+    $this->assertFalse(module_exists('search_api_test'), 'Test module was successfully disabled.');
+    module_disable(array('search_api'), FALSE);
+    $this->assertFalse(module_exists('search_api'), 'Search API module was successfully disabled.');
+
+    drupal_uninstall_modules(array('search_api_test'), FALSE);
+    $this->assertEqual(drupal_get_installed_schema_version('search_api_test', TRUE), SCHEMA_UNINSTALLED, 'Test module was successfully uninstalled.');
+    $this->assertFalse(db_table_exists('search_api_test'), 'Test module table was successfully removed.');
+    drupal_uninstall_modules(array('search_api'), FALSE);
+    $this->assertEqual(drupal_get_installed_schema_version('search_api', TRUE), SCHEMA_UNINSTALLED, 'Search API module was successfully uninstalled.');
+    $this->assertFalse(db_table_exists('search_api_server'), 'Search server table was successfully removed.');
+    $this->assertFalse(db_table_exists('search_api_index'), 'Search index table was successfully removed.');
+    $this->assertFalse(db_table_exists('search_api_item'), 'Index items table was successfully removed.');
+    $this->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.');
+  }
+
+}
+
+/**
+ * Class with unit tests testing small fragments of the Search API.
+ *
+ * Due to severe limitations for "real" unit tests, this still has to be a
+ * subclass of 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);
+    }
+    else {
+      return parent::assertEqual($first, $second, $message, $group);
+    }
+  }
+
+  /**
+   * 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;
+    }
+    $first  = array_merge($first);
+    $second = array_merge($second);
+    foreach ($first as $key => $value) {
+      if (!array_key_exists($key, $second) || !$this->deepEquals($value, $second[$key])) {
+        return FALSE;
+      }
+      unset($second[$key]);
+    }
+    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',
+      'description' => 'Tests some independent components of the Search API, like the processors.',
+      'group' => 'Search API',
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp('entity', 'search_api');
+    $this->index = entity_create('search_api_index', array(
+      'id' => 1,
+      'name' => 'test',
+      'enabled' => 1,
+      'item_type' => 'user',
+      'options' => array(
+        'fields' => array(
+          'name' => array(
+            'type' => 'text',
+          ),
+          'mail' => array(
+            'type' => 'string',
+          ),
+          'search_api_language' => array(
+            'type' => 'string',
+          ),
+        ),
+      ),
+    ));
+  }
+
+  /**
+   * 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();
+  }
+
+  /**
+   * Checks whether the keys are parsed correctly by the query class.
+   */
+  protected function checkQueryParseKeys() {
+    $options['parse mode'] = 'direct';
+    $mode = &$options['parse mode'];
+    $query = new SearchApiQuery($this->index, $options);
+
+    $query->keys('foo');
+    $this->assertEqual($query->getKeys(), 'foo', '"Direct query" parse mode, test 1.');
+    $query->keys('foo bar');
+    $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"', '"Direct query" parse mode, test 3.');
+
+    $mode = 'single';
+    $query = new SearchApiQuery($this->index, $options);
+
+    $query->keys('foo');
+    $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'), '"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"'), '"Single term" parse mode, test 3.');
+
+    $mode = 'terms';
+    $query = new SearchApiQuery($this->index, $options);
+
+    $query->keys('foo');
+    $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'), '"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'), '"Multiple terms" parse mode, test 3.');
+    // http://drupal.org/node/1468678
+    $query->keys('"Münster"');
+    $this->assertEqual($query->getKeys(), array('#conjunction' => 'AND', 'Münster'), '"Multiple terms" parse mode, test 4.');
+  }
+
+  /**
+   * Tests the functionality of the "Ignore case" processor.
+   */
+  protected function checkIgnoreCaseProcessor() {
+    $orig = 'Foo bar BaZ, ÄÖÜÀÁ<>»«.';
+    $processed = drupal_strtolower($orig);
+    $items = array(
+      1 => array(
+        'name' => array(
+          'type' => 'text',
+          'original_type' => 'text',
+          'value' => $orig,
+        ),
+        'mail' => array(
+          'type' => 'string',
+          'original_type' => 'text',
+          'value' => $orig,
+        ),
+        'search_api_language' => array(
+          'type' => 'string',
+          'original_type' => 'string',
+          'value' => LANGUAGE_NONE,
+        ),
+      ),
+    );
+    $keys1 = $keys2 = array(
+      'foo',
+      'bar baz',
+      'foobar1',
+      '#conjunction' => 'AND',
+    );
+    $filters1 = array(
+      array('name', 'foo', '='),
+      array('mail', 'BAR', '='),
+    );
+    $filters2 = array(
+      array('name', 'foo', '='),
+      array('mail', 'bar', '='),
+    );
+
+    $processor = new SearchApiIgnoreCase($this->index, array('fields' => array('name' => 'name')));
+    $tmp = $items;
+    $processor->preprocessIndexItems($tmp);
+    $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, '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, '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, 'Search keys were processed correctly.');
+    $this->assertEqual($query->getFilter()->getFilters(), $filters2, 'Filters were processed correctly.');
+  }
+
+  /**
+   * Tests the functionality of the "Tokenizer" processor.
+   */
+  protected function checkTokenizer() {
+    $orig = 'Foo bar1 BaZ,  La-la-la.';
+    $processed1 = array(
+      array(
+        'value' => 'Foo',
+        'score' => 1,
+      ),
+      array(
+        'value' => 'bar1',
+        'score' => 1,
+      ),
+      array(
+        'value' => 'BaZ',
+        'score' => 1,
+      ),
+      array(
+        'value' => 'Lalala',
+        'score' => 1,
+      ),
+    );
+    $processed2 = array(
+      array(
+        'value' => 'Foob',
+        'score' => 1,
+      ),
+      array(
+        'value' => 'r1B',
+        'score' => 1,
+      ),
+      array(
+        'value' => 'Z,L',
+        'score' => 1,
+      ),
+      array(
+        'value' => 'l',
+        'score' => 1,
+      ),
+      array(
+        'value' => 'l',
+        'score' => 1,
+      ),
+      array(
+        'value' => '.',
+        'score' => 1,
+      ),
+    );
+    $items = array(
+      1 => array(
+        'name' => array(
+          'type' => 'text',
+          'original_type' => 'text',
+          'value' => $orig,
+        ),
+        'search_api_language' => array(
+          'type' => 'string',
+          'original_type' => 'string',
+          'value' => LANGUAGE_NONE,
+        ),
+      ),
+    );
+
+    $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, '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', '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, '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', 'Search keys were processed correctly.');
+  }
+
+  /**
+   * Tests the functionality of the "HTML filter" processor.
+   */
+  protected function checkHtmlFilter() {
+    $orig = <<<END
+This is <em lang="en" title =
+"something">a test</em>.
+How to write <strong>links to <em>other sites</em></strong>: &lt;a href="URL" title="MOUSEOVER TEXT"&gt;TEXT&lt;/a&gt;.
+&lt; signs can be <A HREF="http://example.com/topic/html-escapes" TITLE =  'HTML &quot;escapes&quot;'
+TARGET = '_blank'>escaped</A> with "&amp;lt;".
+<img src = "foo.png" alt = "someone's image" />
+END;
+    $tags = <<<END
+em = 1.5
+strong = 2
+END;
+    $processed1 = array(
+      array('value' => 'This', 'score' => 1),
+      array('value' => 'is', 'score' => 1),
+      array('value' => 'something', 'score' => 1.5),
+      array('value' => 'a', 'score' => 1.5),
+      array('value' => 'test', 'score' => 1.5),
+      array('value' => 'How', 'score' => 1),
+      array('value' => 'to', 'score' => 1),
+      array('value' => 'write', 'score' => 1),
+      array('value' => 'links', 'score' => 2),
+      array('value' => 'to', 'score' => 2),
+      array('value' => 'other', 'score' => 3),
+      array('value' => 'sites', 'score' => 3),
+      array('value' => '<a', 'score' => 1),
+      array('value' => 'href="URL"', 'score' => 1),
+      array('value' => 'title="MOUSEOVER', 'score' => 1),
+      array('value' => 'TEXT">TEXT</a>', 'score' => 1),
+      array('value' => '<', 'score' => 1),
+      array('value' => 'signs', 'score' => 1),
+      array('value' => 'can', 'score' => 1),
+      array('value' => 'be', 'score' => 1),
+      array('value' => 'HTML', 'score' => 1),
+      array('value' => '"escapes"', 'score' => 1),
+      array('value' => 'escaped', 'score' => 1),
+      array('value' => 'with', 'score' => 1),
+      array('value' => '"&lt;"', 'score' => 1),
+      array('value' => 'someone\'s', 'score' => 1),
+      array('value' => 'image', 'score' => 1),
+    );
+    $items = array(
+      1 => array(
+        'name' => array(
+          'type' => 'text',
+          'original_type' => 'text',
+          'value' => $orig,
+        ),
+        'search_api_language' => array(
+          'type' => 'string',
+          'original_type' => 'string',
+          'value' => LANGUAGE_NONE,
+        ),
+      ),
+    );
+
+    $tmp = $items;
+    $processor = new SearchApiHtmlFilter($this->index, array('fields' => array('name' => 'name'), 'title' => TRUE, 'alt' => TRUE, 'tags' => $tags));
+    $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, 'Text was correctly processed.');
+  }
+
+}

+ 18 - 0
sites/all/modules/contrib/search/search_api/tests/search_api_test.info

@@ -0,0 +1,18 @@
+
+name = Search API test
+description = "Some dummy implementations for testing the Search API."
+core = 7.x
+package = Search
+
+dependencies[] = search_api
+
+files[] = search_api_test.module
+
+hidden = TRUE
+
+; Information added by Drupal.org packaging script on 2013-12-25
+version = "7.x-1.11"
+core = "7.x"
+project = "search_api"
+datestamp = "1387965506"
+

+ 55 - 0
sites/all/modules/contrib/search/search_api/tests/search_api_test.install

@@ -0,0 +1,55 @@
+<?php
+
+/**
+ * @file
+ * Install, update and uninstall functions for the Search API test module.
+ */
+
+/**
+ * Implements hook_schema().
+ */
+function search_api_test_schema() {
+  $schema['search_api_test'] = array(
+    'description' => 'Stores instances of a test entity.',
+    'fields' => array(
+      'id' => array(
+        'description' => 'The primary identifier for an item.',
+        'type' => 'serial',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+      ),
+      'title' => array(
+        'description' => 'The title of the item.',
+        'type' => 'varchar',
+        'length' => 50,
+        'not null' => FALSE,
+      ),
+      'body' => array(
+        'description' => 'A text belonging to the item.',
+        'type' => 'text',
+        'not null' => FALSE,
+      ),
+      'type' => array(
+        'description' => 'A string identifying the type of item.',
+        'type' => 'varchar',
+        'length' => 50,
+        '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'),
+  );
+
+  return $schema;
+}

+ 379 - 0
sites/all/modules/contrib/search/search_api/tests/search_api_test.module

@@ -0,0 +1,379 @@
+<?php
+
+/**
+ * @file
+ * Test functions and classes for testing the Search API.
+ */
+
+/**
+ * Implements hook_menu().
+ */
+function search_api_test_menu() {
+  return array(
+    'search_api_test/insert' => array(
+      'title' => 'Insert item',
+      'page callback' => 'drupal_get_form',
+      'page arguments' => array('search_api_test_insert_item'),
+      'access callback' => TRUE,
+    ),
+    'search_api_test/view/%search_api_test' => array(
+      'title' => 'View item',
+      'page callback' => 'search_api_test_view',
+      'page arguments' => array(2),
+      'access callback' => TRUE,
+    ),
+    '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,
+    ),
+  );
+}
+
+/**
+ * Form callback for inserting an item.
+ */
+function search_api_test_insert_item(array $form, array &$form_state) {
+  return array(
+    'id' => array(
+      '#type' => 'textfield',
+    ),
+    'title' => array(
+      '#type' => 'textfield',
+    ),
+    'body' => array(
+      '#type' => 'textarea',
+    ),
+    'type' => array(
+      '#type' => 'textfield',
+    ),
+    'keywords' => array(
+      '#type' => 'textfield',
+    ),
+    'prices' => array(
+      '#type' => 'textfield',
+    ),
+    'submit' => array(
+      '#type' => 'submit',
+      '#value' => t('Save'),
+    ),
+  );
+}
+
+/**
+ * Submit callback for search_api_test_insert_item().
+ */
+function search_api_test_insert_item_submit(array $form, array &$form_state) {
+  form_state_values_clean($form_state);
+  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');
+}
+
+/**
+ * Load handler for search_api_test entities.
+ */
+function search_api_test_load($id) {
+  $ret = entity_load('search_api_test', array($id));
+  return $ret ? array_shift($ret) : NULL;
+}
+
+/**
+ * Menu callback for displaying search_api_test entities.
+ */
+function search_api_test_view($entity) {
+  return nl2br(check_plain(print_r($entity, TRUE)));
+}
+
+/**
+ * Menu callback for marking a "search_api_test" entity as changed.
+ */
+function search_api_test_touch($entity) {
+  module_invoke_all('entity_update', $entity, 'search_api_test');
+}
+
+/**
+ * Menu callback for marking a "search_api_test" entity as changed.
+ */
+function search_api_test_delete($entity) {
+  db_delete('search_api_test')->condition('id', $entity->id)->execute();
+  module_invoke_all('entity_delete', $entity, 'search_api_test');
+}
+
+/**
+ * Implements hook_entity_info().
+ */
+function search_api_test_entity_info() {
+  return array(
+    'search_api_test' => array(
+      'label' => 'Search API test entity',
+      'base table' => 'search_api_test',
+      'uri callback' => 'search_api_test_uri',
+      'entity keys' => array(
+        'id' => 'id',
+      ),
+    ),
+  );
+}
+
+/**
+ * Implements hook_entity_property_info().
+ */
+function search_api_test_entity_property_info() {
+  $info['search_api_test']['properties'] = array(
+    'id' => array(
+      'label' => 'ID',
+      'type' => 'integer',
+      'description' => 'The primary identifier for a server.',
+    ),
+    'title' => array(
+      'label' => 'Title',
+      'type' => 'text',
+      'description' => 'The title of the item.',
+      'required' => TRUE,
+    ),
+    'body' => array(
+      'label' => 'Body',
+      'type' => 'text',
+      'description' => 'A text belonging to the item.',
+      'sanitize' => 'filter_xss',
+      'required' => TRUE,
+    ),
+    'type' => array(
+      'label' => 'Type',
+      'type' => 'text',
+      'description' => 'A string identifying the type of item.',
+      'required' => TRUE,
+    ),
+    'parent' => array(
+      'label' => 'Parent',
+      'type' => 'search_api_test',
+      'description' => "The item's parent.",
+      'getter callback' => 'search_api_test_parent',
+    ),
+    'keywords' => array(
+      'label' => 'Keywords',
+      'type' => 'list<string>',
+      'description' => 'An optional collection of keywords describing the item.',
+      'getter callback' => 'search_api_test_list_callback',
+    ),
+    'prices' => array(
+      'label' => 'Prices',
+      'type' => 'list<decimal>',
+      'description' => 'An optional list of prices.',
+      'getter callback' => 'search_api_test_list_callback',
+    ),
+  );
+
+  return $info;
+}
+
+/**
+ * URI callback for test entity.
+ */
+function search_api_test_uri($entity) {
+  return array(
+    'path' => 'search_api_test/' . $entity->id,
+  );
+}
+
+/**
+ * Parent callback.
+ */
+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().
+ */
+function search_api_test_search_api_service_info() {
+  $services['search_api_test_service'] = array(
+    'name' => 'search_api_test_service',
+    'description' => 'search_api_test_service description',
+    'class' => 'SearchApiTestService',
+  );
+  return $services;
+}
+
+/**
+ * Test service class.
+ */
+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(
+        '#type' => 'textfield',
+        '#title' => 'Test option',
+      ),
+    );
+
+    if (!empty($this->options)) {
+      $form['test']['#default_value'] = $this->options['test'];
+    }
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addIndex(SearchApiIndex $index) {
+    $this->checkErrorState();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function fieldsUpdated(SearchApiIndex $index) {
+    $this->checkErrorState();
+    return db_query('SELECT COUNT(*) FROM {search_api_test}')->fetchField() > 0;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function removeIndex($index) {
+    $this->checkErrorState();
+    parent::removeIndex($index);
+  }
+
+  /**
+   * Implements SearchApiServiceInterface::indexItems().
+   *
+   * 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);
+    asort($this->options['indexes'][$index->machine_name]);
+    $this->server->save();
+
+    return $ids;
+  }
+
+  /**
+   * 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();
+      }
+      else {
+        $this->options['indexes'] = array();
+      }
+    }
+    else {
+      foreach ($ids as $id) {
+        unset($this->options['indexes'][$index->machine_name][$id]);
+      }
+    }
+    $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();
+    $index_id = $query->getIndex()->machine_name;
+    if (empty($this->options['indexes'][$index_id])) {
+      return array(
+        'result count' => 0,
+        'results' => array(),
+      );
+    }
+    $items = $this->options['indexes'][$index_id];
+    $min = isset($options['offset']) ? $options['offset'] : 0;
+    $max = $min + (isset($options['limit']) ? $options['limit'] : count($items));
+    $i = 0;
+    $ret['result count'] = count($items);
+    $ret['results'] = array();
+    foreach ($items as $id) {
+      ++$i;
+      if ($i > $max) {
+        break;
+      }
+      if ($i > $min) {
+        $ret['results'][$id] = array(
+          'id' => $id,
+          'score' => 1,
+        );
+      }
+    }
+    return $ret;
+  }
+
+  /**
+   * 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();
+    }
+  }
+
+}