Преглед на файлове

started edlp_search module : displaying a draft of the form

Bachir Soussi Chiadmi преди 6 години
родител
ревизия
ff495add52
променени са 100 файла, в които са добавени 14045 реда и са изтрити 0 реда
  1. 457 0
      sites/all/modules/contrib/search/search_api/CHANGELOG.txt
  2. 339 0
      sites/all/modules/contrib/search/search_api/LICENSE.txt
  3. 112 0
      sites/all/modules/contrib/search/search_api/README.txt
  4. 36 0
      sites/all/modules/contrib/search/search_api/composer.json
  5. 4 0
      sites/all/modules/contrib/search/search_api/config/install/search_api.settings.yml
  6. 96 0
      sites/all/modules/contrib/search/search_api/config/optional/tour.tour.search-api-index-fields.yml
  7. 65 0
      sites/all/modules/contrib/search/search_api/config/optional/tour.tour.search-api-index-form.yml
  8. 40 0
      sites/all/modules/contrib/search/search_api/config/optional/tour.tour.search-api-index-processors.yml
  9. 104 0
      sites/all/modules/contrib/search/search_api/config/optional/tour.tour.search-api-index.yml
  10. 41 0
      sites/all/modules/contrib/search/search_api/config/optional/tour.tour.search-api-server-form.yml
  11. 48 0
      sites/all/modules/contrib/search/search_api/config/optional/tour.tour.search-api-server.yml
  12. 30 0
      sites/all/modules/contrib/search/search_api/config/schema/search_api.datasource.schema.yml
  13. 95 0
      sites/all/modules/contrib/search/search_api/config/schema/search_api.index.schema.yml
  14. 230 0
      sites/all/modules/contrib/search/search_api/config/schema/search_api.processor.schema.yml
  15. 16 0
      sites/all/modules/contrib/search/search_api/config/schema/search_api.schema.yml
  16. 33 0
      sites/all/modules/contrib/search/search_api/config/schema/search_api.server.schema.yml
  17. 7 0
      sites/all/modules/contrib/search/search_api/config/schema/search_api.tracker.schema.yml
  18. 294 0
      sites/all/modules/contrib/search/search_api/config/schema/search_api.views.schema.yml
  19. 93 0
      sites/all/modules/contrib/search/search_api/css/search_api.admin.css
  20. 8 0
      sites/all/modules/contrib/search/search_api/drush.services.yml
  21. 49 0
      sites/all/modules/contrib/search/search_api/js/search_api.processors.js
  22. 60 0
      sites/all/modules/contrib/search/search_api/modules/search_api_db/README.txt
  23. 1 0
      sites/all/modules/contrib/search/search_api/modules/search_api_db/config/install/search_api_db.settings.yml
  24. 23 0
      sites/all/modules/contrib/search/search_api/modules/search_api_db/config/schema/search_api_db.backend.schema.yml
  25. 7 0
      sites/all/modules/contrib/search/search_api/modules/search_api_db/config/schema/search_api_db.schema.yml
  26. 38 0
      sites/all/modules/contrib/search/search_api/modules/search_api_db/search_api_db.api.php
  27. 13 0
      sites/all/modules/contrib/search/search_api/modules/search_api_db/search_api_db.info.yml
  28. 78 0
      sites/all/modules/contrib/search/search_api/modules/search_api_db/search_api_db.install
  29. 17 0
      sites/all/modules/contrib/search/search_api/modules/search_api_db/search_api_db.module
  30. 22 0
      sites/all/modules/contrib/search/search_api/modules/search_api_db/search_api_db.services.yml
  31. 23 0
      sites/all/modules/contrib/search/search_api/modules/search_api_db/search_api_db_defaults/README.txt
  32. 43 0
      sites/all/modules/contrib/search/search_api/modules/search_api_db/search_api_db_defaults/config/optional/core.entity_view_display.node.article.search_index.yml
  33. 42 0
      sites/all/modules/contrib/search/search_api/modules/search_api_db/search_api_db_defaults/config/optional/core.entity_view_display.node.article.search_result.yml
  34. 24 0
      sites/all/modules/contrib/search/search_api/modules/search_api_db/search_api_db_defaults/config/optional/core.entity_view_display.node.page.search_index.yml
  35. 28 0
      sites/all/modules/contrib/search/search_api/modules/search_api_db/search_api_db_defaults/config/optional/core.entity_view_display.node.page.search_result.yml
  36. 188 0
      sites/all/modules/contrib/search/search_api/modules/search_api_db/search_api_db_defaults/config/optional/search_api.index.default_index.yml
  37. 16 0
      sites/all/modules/contrib/search/search_api/modules/search_api_db/search_api_db_defaults/config/optional/search_api.server.default_server.yml
  38. 159 0
      sites/all/modules/contrib/search/search_api/modules/search_api_db/search_api_db_defaults/config/optional/views.view.search_content.yml
  39. 20 0
      sites/all/modules/contrib/search/search_api/modules/search_api_db/search_api_db_defaults/search_api_db_defaults.info.yml
  40. 85 0
      sites/all/modules/contrib/search/search_api/modules/search_api_db/search_api_db_defaults/search_api_db_defaults.install
  41. 181 0
      sites/all/modules/contrib/search/search_api/modules/search_api_db/search_api_db_defaults/tests/src/Functional/IntegrationTest.php
  42. 17 0
      sites/all/modules/contrib/search/search_api/modules/search_api_db/src/DatabaseCompatibility/CaseSensitiveDatabase.php
  43. 73 0
      sites/all/modules/contrib/search/search_api/modules/search_api_db/src/DatabaseCompatibility/DatabaseCompatibilityHandlerInterface.php
  44. 72 0
      sites/all/modules/contrib/search/search_api/modules/search_api_db/src/DatabaseCompatibility/GenericDatabase.php
  45. 39 0
      sites/all/modules/contrib/search/search_api/modules/search_api_db/src/DatabaseCompatibility/MySql.php
  46. 2738 0
      sites/all/modules/contrib/search/search_api/modules/search_api_db/src/Plugin/search_api/backend/Database.php
  47. 67 0
      sites/all/modules/contrib/search/search_api/modules/search_api_db/src/Tests/DatabaseTestsTrait.php
  48. 67 0
      sites/all/modules/contrib/search/search_api/modules/search_api_db/src/Tests/Update/SearchApiDbUpdate8102Test.php
  49. 59 0
      sites/all/modules/contrib/search/search_api/modules/search_api_db/tests/fixtures/update/search-api-db-base.php
  50. 62 0
      sites/all/modules/contrib/search/search_api/modules/search_api_db/tests/fixtures/update/search-api-db-update-8102.php
  51. 15 0
      sites/all/modules/contrib/search/search_api/modules/search_api_db/tests/search_api_db_test_autocomplete/config/install/search_api_autocomplete.search.search_api_db_test_autocomplete.yml
  52. 15 0
      sites/all/modules/contrib/search/search_api/modules/search_api_db/tests/search_api_db_test_autocomplete/search_api_db_test_autocomplete.info.yml
  53. 70 0
      sites/all/modules/contrib/search/search_api/modules/search_api_db/tests/search_api_db_test_autocomplete/src/Plugin/search_api_autocomplete/search/TestSearch.php
  54. 40 0
      sites/all/modules/contrib/search/search_api/modules/search_api_db/tests/src/FunctionalJavascript/IntegrationTest.php
  55. 144 0
      sites/all/modules/contrib/search/search_api/modules/search_api_db/tests/src/Kernel/AutocompleteTest.php
  56. 753 0
      sites/all/modules/contrib/search/search_api/modules/search_api_db/tests/src/Kernel/BackendTest.php
  57. 18 0
      sites/all/modules/contrib/search/search_api/modules/search_api_views_taxonomy/search_api_views_taxonomy.info.yml
  58. 24 0
      sites/all/modules/contrib/search/search_api/modules/search_api_views_taxonomy/search_api_views_taxonomy.install
  59. 254 0
      sites/all/modules/contrib/search/search_api/phpcs.xml
  60. 354 0
      sites/all/modules/contrib/search/search_api/search_api.api.php
  61. 449 0
      sites/all/modules/contrib/search/search_api/search_api.drush.inc
  62. 14 0
      sites/all/modules/contrib/search/search_api/search_api.info.yml
  63. 281 0
      sites/all/modules/contrib/search/search_api/search_api.install
  64. 13 0
      sites/all/modules/contrib/search/search_api/search_api.libraries.yml
  65. 15 0
      sites/all/modules/contrib/search/search_api/search_api.links.action.yml
  66. 5 0
      sites/all/modules/contrib/search/search_api/search_api.links.menu.yml
  67. 27 0
      sites/all/modules/contrib/search/search_api/search_api.links.task.yml
  68. 671 0
      sites/all/modules/contrib/search/search_api/search_api.module
  69. 3 0
      sites/all/modules/contrib/search/search_api/search_api.permissions.yml
  70. 34 0
      sites/all/modules/contrib/search/search_api/search_api.plugin_type.yml
  71. 242 0
      sites/all/modules/contrib/search/search_api/search_api.routing.yml
  72. 94 0
      sites/all/modules/contrib/search/search_api/search_api.services.yml
  73. 506 0
      sites/all/modules/contrib/search/search_api/search_api.theme.inc
  74. 828 0
      sites/all/modules/contrib/search/search_api/search_api.views.inc
  75. 44 0
      sites/all/modules/contrib/search/search_api/src/Annotation/SearchApiBackend.php
  76. 60 0
      sites/all/modules/contrib/search/search_api/src/Annotation/SearchApiDataType.php
  77. 44 0
      sites/all/modules/contrib/search/search_api/src/Annotation/SearchApiDatasource.php
  78. 58 0
      sites/all/modules/contrib/search/search_api/src/Annotation/SearchApiDisplay.php
  79. 44 0
      sites/all/modules/contrib/search/search_api/src/Annotation/SearchApiParseMode.php
  80. 56 0
      sites/all/modules/contrib/search/search_api/src/Annotation/SearchApiProcessor.php
  81. 44 0
      sites/all/modules/contrib/search/search_api/src/Annotation/SearchApiTracker.php
  82. 86 0
      sites/all/modules/contrib/search/search_api/src/Backend/BackendInterface.php
  83. 327 0
      sites/all/modules/contrib/search/search_api/src/Backend/BackendPluginBase.php
  84. 36 0
      sites/all/modules/contrib/search/search_api/src/Backend/BackendPluginManager.php
  85. 216 0
      sites/all/modules/contrib/search/search_api/src/Backend/BackendSpecificInterface.php
  86. 458 0
      sites/all/modules/contrib/search/search_api/src/Commands/SearchApiCommands.php
  87. 8 0
      sites/all/modules/contrib/search/search_api/src/ConsoleException.php
  88. 43 0
      sites/all/modules/contrib/search/search_api/src/Contrib/RowsOfMultiValueFields.php
  89. 49 0
      sites/all/modules/contrib/search/search_api/src/Contrib/ViewsBulkOperationsEventSubscriber.php
  90. 46 0
      sites/all/modules/contrib/search/search_api/src/Controller/ExecuteTasksAccessCheck.php
  91. 183 0
      sites/all/modules/contrib/search/search_api/src/Controller/IndexController.php
  92. 75 0
      sites/all/modules/contrib/search/search_api/src/Controller/ServerController.php
  93. 65 0
      sites/all/modules/contrib/search/search_api/src/Controller/TaskController.php
  94. 64 0
      sites/all/modules/contrib/search/search_api/src/DataType/DataTypeInterface.php
  95. 89 0
      sites/all/modules/contrib/search/search_api/src/DataType/DataTypePluginBase.php
  96. 119 0
      sites/all/modules/contrib/search/search_api/src/DataType/DataTypePluginManager.php
  97. 241 0
      sites/all/modules/contrib/search/search_api/src/Datasource/DatasourceInterface.php
  98. 158 0
      sites/all/modules/contrib/search/search_api/src/Datasource/DatasourcePluginBase.php
  99. 36 0
      sites/all/modules/contrib/search/search_api/src/Datasource/DatasourcePluginManager.php
  100. 68 0
      sites/all/modules/contrib/search/search_api/src/Display/DisplayDeriverBase.php

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

@@ -0,0 +1,457 @@
+Search API 1.7 (2018-02-23):
+----------------------------
+- #2922525 by drunken monkey, borisson_: Changed "Index items immediately" to
+  delay indexing until the end of the page request.
+- #2930720 by drunken monkey, borisson_: Added a UI for rebuilding the tracking
+  table for an index.
+- #2912246 by drunken monkey: Fixed inconsistent array indices in query
+  languages.
+- #2939405 by bserem, borisson_, drunken monkey, yoroy: Improved UI text on
+  what is included in the index.
+- #2943705 by kevin.dutra, drunken monkey, borisson_: Fixed performance
+  problems with LIFO tracker.
+- #2942846 by idebr, drunken monkey, borisson_: Added empty/not empty operators
+  to Views fulltext field filters.
+- #2940255 by drunken monkey: Updated the DB autocomplete implementation to the
+  stable API version.
+- #2938646 by drunken monkey, Johnny vd Laar: Fixed item-boosts in database
+  backend.
+- #2939085 by mattgill, drunken monkey: Fixed fatal error in Views when trying
+  to set keywords conjunction on aborted query.
+- #2933811 by drunken monkey: Fixed coding standards in new Drush code.
+- #2932347 by drunken monkey, ghaya: Fixed case insensitive matching for
+  highlighting non-ASCII text.
+- #2938288 by drunken monkey: Fixed problems with PHP 7.2.
+- #2934321 by Graber, drunken monkey: Fixed Views total for views with pager
+  offset.
+- #2932347 by drunken monkey, ghaya: Fixed case insensitive matching for
+  highlighting non-ASCII text.
+- #2933309 by drunken monkey, mkalkbrenner: Made some helper methods in the
+  CommandHelper class public.
+- #2928279 by drunken monkey, borisson_: Added a test for the default tracker
+  plugin.
+
+Search API 1.6 (2017-12-24):
+----------------------------
+- #2669962 by kevin.dutra, drunken monkey, kristofferwiklund, borisson_: Added
+  option to change the order in which items are indexed.
+- #2917399 by drunken monkey, andypost, borisson_, zenimagine: Fixed definition
+  of Views taxonomy term plugins.
+- #2922874 by drunken monkey, borisson_: Fixed DBMS compatibility handling when
+  a non-default database is used with the Database backend.
+- #2923910 by George Bills, drunken monkey, eft, borisson_, robin.ingelbrecht:
+  Fixed handling of array-valued Views fulltext filter input.
+- #2914478 by borisson_, mpp, drunken monkey, pfrenssen, claudiu.cristea,
+  kevin.dutra, SylvainM, jhedstrom, bircher: Added integration with Drush 9.
+- #2926733 by drunken monkey, borisson_: Fixed indexing of leading/trailing
+  whitespace in fulltext tokens on database backend.
+- #2922024 by drunken monkey, borisson_: Fixed Stemmer incorrectly processing
+  non-English searches.
+- #2931730 by drunken monkey: Adapted tests to changes in drupal_set_message().
+- #2929739 by drunken monkey, borisson_: Improved adherence to coding standards.
+- #2927748 by mkalkbrenner, mstiem, drunken monkey: Fixed Views field handler
+  for multiple processor-defined fields returned by the server.
+- #2928944 by drunken monkey: Removed assert() calls with string parameters.
+- #2923976 by Martijn Houtman, drunken monkey: Fixed facets with random sorting
+  on Database backend.
+- #2921582 by chr.fritsch, drunken monkey: Fixed saving of "database_text" form
+  field for Database servers.
+- #2919884 by samuel.mortenson, borisson_, drunken monkey: Improved performance
+  for saving an index's reindexing state.
+- #2855254 by dev.patrick, drunken monkey, borisson_: Removed the remaining
+  @codingStandardsIgnoreFile directive.
+- #2916754 by chr.fritsch, borisson_, drunken monkey: Fixed failure to detect
+  whether a view is executed in the current request.
+- #2910638 by drunken monkey, borisson_, phillipHG: Fixed handling of
+  entity-valued processor properties.
+- #2917036 by drunken monkey, borisson_: Fixed illegal return values in
+  Database::getAutocompleteSuggestions().
+- #2916225 by tilenav, borisson_, drunken monkey: Replaced manual config
+  setting with installConfig() in our Kernel tests.
+- #2917779 by mkalkbrenner, edurenye, drunken monkey: Fixed fatal errors in
+  views on PHP 5.
+- #2905562 by drunken monkey, borisson_: Increased the minimum Core version to
+  8.4.
+- #2905562 by drunken monkey, borisson_: Increased the minimum Core version to
+  8.4.
+- #2916208 by drunken monkey: Fixed AJAX display of plugin forms.
+- #2278433 by drunken monkey, borisson_: Added option to Views field handlers
+  to use highlighted fields data.
+- #2278433 by drunken monkey, borisson_: Added option to Views field handlers
+  to use highlighted fields data.
+
+Search API 1.5 (2017-10-14):
+----------------------------
+- #2913688 by kevin.dutra, borisson_, drunken monkey: Fixed display plugin for
+  Views pages with contextual filters.
+- #2911734 by drunken monkey, borisson_, mkalkbrenner: Fixed Views display of
+  fields returned from the backend.
+- #2910918 by drunken monkey, borisson_: Fixed error in fields configuration
+  form.
+- #2650986 by drunken monkey, borisson_: Fixed various problems with Views
+  field handling.
+- #2907518 by drunken monkey, jrockowitz, borisson_: Fixed index
+  creation/update via CLI on large sites.
+- #2908440 by claudiu.cristea: Fixed UnsavedIndexConfiguration documentation.
+- #2899678 by drunken monkey, Graber, borisson_: Added support for VBO.
+- #2909153 by drunken monkey, borisson_: Fixed fatal error for test fails under
+  certain conditions.
+- #2907756 by drunken monkey, bojanz: Fixed "Entity status" processor to
+  support other publishable entity types.
+- #2904377 by drunken monkey, borisson_: Added Autocomplete tests for the DB
+  backend.
+- #2907943 by drunken monkey: Fixed warnings on PHP 5.
+- #2906099 by drunken monkey: Fixed use of field's underlying property label in
+  Views.
+- #2896073 by drunken monkey, New Zeal: Fixed warnings when indexing empty text
+  fields.
+- #2898082 by drunken monkey: Re-organized our test class namespaces.
+- #2907334 by drunken monkey: Fixed some more Drupal coding standards
+  violations.
+- #2903407 by drunken monkey, alan-ps: Added #optional to form containers where
+  appropriate.
+
+Search API 1.4 (2017-09-07):
+----------------------------
+- #2905117 by drunken monkey: Fixed problems with multi-valued
+  processor-generated fields on Solr.
+- #2884034 by drunken monkey: Fixed highlighting for processed keywords.
+- #2902947 by drunken monkey: Fixed "Thousands marker" setting for Views fields.
+- #2902907 by drunken monkey, juagarc4: Fixed indexing of multibyte text on
+  MySQL.
+- #2903805 by drunken monkey: Disabled fields processors for hidden fields.
+- #2903014 by drunken monkey: Fixed "Enable for all fields" processor option.
+- #2904976 by drunken monkey: Fixed the current test fails.
+- #2902810 by drunken monkey, borisson_: Made query serialization in tests
+  safer.
+- #2903834 by drunken monkey: Adapted to latest changes in Autocomplete module.
+- #2903633 by drunken monkey: Fixed current test fails.
+- #2899920 by kfritsche: Fixed stored value of boolean Views filters.
+- #2896312 by kerasai, drunken monkey: Fixed problems with Views options list
+  filter.
+- #2896878 by drunken monkey: Fixed test fails due to latest Core changes.
+- #2665476 by ericgsmith, drunken monkey: Fixed paging in cached views.
+
+Search API 1.3 (2017-07-19):
+----------------------------
+- #2888584 by drunken monkey: Fixed index description for "cron batch size" of
+  -1.
+- #2895142 by drunken monkey: Made our test classes more useful for other
+  modules.
+- #2851436 by Erik Frèrejean, andywhale, beltofte, drunken monkey: Added a
+  "Type-specific boosting" processor.
+- #2889426 by cspitzlay, drunken monkey: Fixed test fails on Drupal 8.3.
+- #2891387 by drunken monkey: Fixed some new code style issues.
+- #2891246 by drunken monkey: Fixed error detection in plugin helper.
+- #2884451 by drunken monkey: Fixed missing primary key for denormalized index
+  tables in DB backend.
+- #2886978 by drunken monkey: Fixed tracking of entities in disabled languages.
+
+Search API 1.2 (2017-06-25):
+----------------------------
+- #2881198 by jzavrl, drunken monkey, borisson_: Added property paths to "Add
+  fields" form.
+- #2883475 by Matthijs, drunken monkey, borisson_: Added support for "Language
+  not applicable".
+- #2883807 by LammensJ, drunken monkey: Fixed warning in Views UI for
+  contextual filters.
+- #2880239 by acbramley, drunken monkey, borisson_: Fixed logic for multiple
+  sorts on the same field.
+- #2889426 by drunken monkey: Fixed test fails for latest Core changes.
+- #2884720 by blake.thompson: Added a display plugin for the Views Embed
+  display type.
+- #2882347 by mkalkbrenner: Fixed forms to not have unserializable properties.
+- #2676468 by acbramley, drunken monkey, borisson_: Fixed stale cache in search
+  view with tag-based caching.
+- #2876398 by drunken monkey: Fixed operator description of Views "Fulltext
+  search" filter.
+- #2881945 by drunken monkey: Removed unnecessary check in
+  AddURL::addFieldValues().
+- #2886981 by drunken monkey: Fixed test fails for latest Core changes.
+- #2880026 by Anton4yk, drunken monkey: Fixed bugs in entity datasource's item
+  discovery code for edge cases.
+- #2881631 by jzavrl, drunken monkey, borisson_: Added alphabetic sort for
+  properties in "Add fields" form.
+- #2867809 by drunken monkey, borisson_: Added an option to enable a processor
+  for all compatible fields.
+- #2878974 by acbramley: Added the date Views argument plugin.
+- #2840274 by drunken monkey, borisson_: Updated the Core dependency to 8.3.
+- #2874895 by harsha012, drunken monkey, borisson_: Fixed OOP code to use the
+  t() method instead of the global t() function.
+- #2624876 by drunken monkey, borisson_: Added a query option for "properties
+  to retrieve".
+- #2682949 by drunken monkey, borisson_: Adapted to changes in the Autocomplete
+  module.
+
+Search API 1.1 (2017-05-10):
+----------------------------
+- #2858303 by beluoctavian, drunken monkey: Fixed "is empty" Views filters for
+  taxonomy term references.
+- #2873246 by drunken monkey: Fixed a second error for cached aborted search
+  queries in Views.
+- #2862289 by hoebekewim, drunken monkey: Fixed oversized column for fulltext
+  fields in denormalized index table.
+- #2859683 by drunken monkey: Added a note to fields processors explaining that
+  per-field keywords processing isn't supported.
+- #2871497 by drunken monkey: Fixed validation of "Whitespace characters"
+  setting for the Tokenizer processor.
+- #2868704 by drunken monkey: Fixed old removed fields being present on query
+  object.
+- #2863955 by alan-ps, drunken monkey, borisson_: Fixed unsupported processors
+  remaining enabled.
+- #2870988 by sahilsharma011, c.nish2k3, kala4ek: Removed translations from all
+  tests.
+- #2871030 by drunken monkey: Fixed error for cached aborted search queries in
+  Views.
+- #2230935 by shkiper, drunken monkey: Added the "search-api-server-clear"
+  Drush command.
+
+Search API 1.0 (2017-04-26):
+----------------------------
+- #2871145 by opdavies, drunken monkey, borisson_: Fixed link to php.net for
+  PCRE reference.
+- #2543472 by drunken monkey: Fixed indexing of multiple indexes via Drush.
+- #2871549 by drunken monkey: Added note about backwards compatibility to
+  README.txt.
+
+Search API 1.0, RC 4 (2017-04-21):
+----------------------------------
+- #2869121 by drunken monkey: Fixed fatal error when required fulltext filter is
+  in exposed form block.
+
+Search API 1.0, RC 3 (2017-04-20):
+----------------------------------
+- #2869121 by drunken monkey, Wim Leers, wouter.adem, borisson_: Added improved
+  "Required" handling for the Views "Fulltext search" filter.
+- #2870782 by drunken monkey: Fixed the tests for Drupal 8.2.
+- #2868851 by acbramley, drunken monkey: Fixed wrong interface used for loggers.
+- #2868427 by dbjpanda, drunken monkey: Fixed use of d.o URL alias in
+  README.txt.
+
+Search API 1.0, RC 2 (2017-04-10):
+----------------------------------
+- #2846932 by drunken monkey, killua99, borisson_: Fixed error when changing
+  boost values with a Postgres database backend.
+- #2866454 by phenaproxima, drunken monkey: Fixed problems in update 8103.
+- #2844945 by drunken monkey: Fixed uncaught exception when adding too many
+  fields.
+
+Search API 1.0, RC 1 (2017-04-09):
+----------------------------------
+- #2776659 by drunken monkey, drholera: Removed the deprecated Utility methods.
+- #2268809 by drunken monkey: Converted all arrays in the code to use the short
+  syntax.
+- #2863736 by fran seva, drunken monkey: Added a setDataDefinition() method to
+  the Field class.
+- #2867118 by drunken monkey: Fixed reported coding standards problems.
+- #2839932 by drunken monkey,  borisson_, bmcclure, jacktonkin: Fixed Views
+  problems with "Rendered item" fields.
+- #2765317 by vasike: Added a "Last" aggregation for aggregated fields.
+- #2861657 by drunken monkey: Removed remaining usages of "e.g.".
+
+Search API 1.0, Beta 5 (2017-04-02):
+------------------------------------
+- #2842029 by vasike, drunken monkey: Added a "plugin helper" service for
+  creating index plugins.
+- #2863253 by drunken monkey: Added hook infos for all our hooks.
+- #2856050 by StryKaizer, drunken monkey, marthinal, borisson_: Added getPath()
+  to display plugins and deprecated getUrl().
+- #2856003 by borisson_, drunken monkey: Added "index" and "path" to search
+  display annotation definition.
+- #2842557 by StryKaizer, Boobaa, drunken monkey, borisson_: Fixed "is rendered
+  on current page" checks for Views displays with contextual filters.
+- #2855157 by dbjpanda: Fixed a small CSS error.
+- #2842007 by shashank.mundhra: Fixed DB comment of search_api_item.status
+  column.
+- #2682369 by Alumei, drunken monkey, swentel, Crell, stBorchert, prics,
+  borisson_: Fixed problems with overridden config entities.
+- #2645882 by drunken monkey, borisson_: Fixed "items could not be indexed"
+  message for "Index now".
+- #2641388 by mallezie, drunken monkey, borisson_, janusman: Added various UX
+  improvements for the "Fields" tab.
+- #2861587 by alexpott: Fixed the DB/Defaults integration tests.
+- #2855758 by StryKaizer, drunken monkey: Fixed "is rendered" checks for Views
+  block displays.
+- #2855444 by drunken monkey: Added language-specific test for the "Rendered
+  item" processor.
+- #2794295 by isramv, drunken monkey, alan-ps: Fixed default index indexing term
+  names instead of IDs.
+- #2343161 by alan-ps, drunken monkey: Added per-datasource indexing stats.
+- #2857017 by shkiper: Fixed copy-paste errors in js/index-active-formatters.js.
+- #2311039 by drunken monkey: Fixed missing dependency injection for all form
+  and plugin classes.
+- #2745655 by drunken monkey: Added test for (NOT) NULL conditions on fulltext
+  fields in DB backend.
+- #2574889 by drunken monkey, ChristianAdamski: Added Tour module integration.
+- #2814925 by kducharm, drunken monkey, Cyberwolf: Fixed indexing of computed
+  fields.
+- #2852807 by alan-ps: Fixed CheckStyle warnings in this project.
+- #2659868 by borisson_, drunken monkey: Fixed CacheabilityTest to make sure it
+  displays the rendered entities.
+- #2853049 by Blanca.Esqueda: Fixed config schema error for Views date filters.
+- #2847810 by JayKandari: Added Stemmer to ProcessorIntegrationTest.
+- #2753667 by pfrenssen, drunken monkey, borisson_, idimopoulos, sandervd,
+  sardara: Improved Views cache plugins and their cache metadata.
+- #2843854 by becw, drunken monkey: Fixed date filters in search views.
+- #2753763 by drholera, drunken monkey: Added a logging trait which can also
+  log exceptions.
+- #2850025 by alan-ps: Added a "label_collection" property for our entity types.
+- #2851533 by drunken monkey: Adapted the tests to the latest Core changes.
+- #2491175 by drunken monkey, ptmkenny: Added a new "Entity status" processor
+  to replace "Node status".
+- #2681273 by drunken monkey, borisson_: Added plugin descriptions consistently
+  to the UI.
+- #2642792 by drunken monkey: Fixed and expanded the RenderedItemTest.
+- #2844527 by ayalon, drunken monkey: Added some contextual filters for Views.
+- #2847307 by JayKandari, drunken monkey: Removed "Display" suffix from our
+  search display plugins.
+- #2849507 by drunken monkey: Fixed test fails on Drupal 8.4.
+- #2846255 by drunken monkey: Fixed displayed order of processors in UI.
+- #2656916 by drunken monkey: Removed unused field dependency calculation
+  method.
+- #2844192 by BR0kEN, drunken monkey: Fixed failure in DB backend when setting
+  field boost from 0 to other value.
+- #2847588 by Nick_vh, drunken monkey: Improved the description of the
+  search_api_views_taxonomy module.
+- #2843632 by beram, drunken monkey: Fixed "contains any" for Views fulltext
+  contextual filter.
+- #2842971 by jespermb, borisson_: Fixed wrong URL for block search displays.
+- #2842546 by alan-ps: Fixed Drush commands for enabling and disabling indexes.
+- #2840675 by nicrodgers, drunken monkey: Added support for Views filters with
+  option lists.
+- #2841550 by drunken monkey: Fixed execution of pending tasks.
+- #2841827 by umed91, drunken monkey: Fixed some code style issues.
+- #2682615 by drunken monkey: Added tests for object serialization and cloning.
+- #2794879 by drunken monkey: Added property paths to the "Fields" tab.
+- #2840261 by alan-ps: Fixed usage of outdated hash functions.
+
+Search API 1.0, Beta 4 (2016-10-24):
+------------------------------------
+- #2839981 by borisson_, drunken monkey: Fixed search ID for non-page views.
+- #2834350 by alan-ps: Fixed missing field restriction in DB backend
+  autocomplete code.
+- #2809469 by StryKaizer, borisson_, drunken monkey, pfrenssen: Fixed
+  incompatible cache modes used for Search API views.
+- #2839142 by borisson_, drunken monkey: Added new test for Defaults module
+  index creation.
+- #2831436 by borisson_, drunken monkey: Added dependency information to
+  display plugins.
+- #2836994 by dermario: Fixed order of facets in DB backend unreliable.
+- #2839445 by borisson_: Fixed test failure in for some Core versions.
+- #2833229 by timcosgrove: Removed array type hinting for batch contexts.
+- #2656052 by drunken monkey: Removed the "plugin_id"/"settings" sub-level of
+  index plugin settings.
+- #2837099 by drunken monkey: Fixed the tests in latest Drupal 8.3 dev version.
+- #2709351 by drunken monkey: Fixed invalid processor configurations when field
+  configuration changes.
+- #2829351 by SchnWalter, drunken monkey, borisson_: Fixed
+  $entity->search_api_skip_tracking for nodes.
+- #2827961 by drunken monkey, Steven Jones: Fixed problems with giant scores in
+  DB backend.
+- #2823985 by felribeiro, dermario, borisson_: Added an integration test for
+  the "Role filter" processor.
+- #2832015 by mkalkbrenner: Removed translations from functional tests.
+- #2828184 by ekes: Fixed bug in Stemmer config form validation.
+- #2828848 by mkalkbrenner, drunken monkey, borisson_: Prepared IntegrationTest
+  to be executed with different backends.
+- #2828148 by ekes, borisson_, drunken monkey: Fixed problem when stemming
+  multiple words at once.
+- #2826822 by drunken monkey: Fixed use of illegal typed data type "text".
+- #2826308 by drunken monkey, mkalkbrenner: Fixed unwanted filter in Views
+  tests.
+- #2734897 by niko-, drunken monkey: Added tests for preIndexSave()
+  implementations.
+- #2650364 by drunken monkey: Added a "skip access checks" option to the Views
+  relationship plugin.
+- #2826160 by keboca, drunken monkey: Fixed config form of "Role filter"
+  processor to be more robust.
+- #2753815 by niko-, drunken monkey, itsekhmistro, borisson_: Ported Simpletest
+  web tests to BrowserTestBase tests.
+- #2358065 by tstoeckler: Added the option for highlighting of partial matches
+  to the processor.
+- #2470837 by sinn, drunken monkey: Added documentation on why to disable Core
+  Search.
+- #2767609 by drunken monkey, borisson_: Added backend tests for empty value
+  conditions.
+- #2789431 by phenaproxima, drunken monkey: Added possibility to display
+  processor-generated fields in Views.
+- #2822553 by niko-: Removed a test assertion that failed when run via GUI.
+- #2821445 by drunken monkey: Fixed warning in HTML filter for non-HTML fields.
+- #2733185 by sinn, drunken monkey: Added documentation and tests for tracker
+  and display plugin alter hooks.
+- #2824326 by Jo Fitzgerald: Fixed generated autocomplete suggestions of the DB
+  backend.
+- #2824932 by mkalkbrenner: Fixed incorrect indexing call in backend tests.
+- #2821498 by stBorchert: Increased our required minimum Core version to 8.2.
+- #2816979 by drunken monkey: Added click-sorting for indexed fields in Views.
+- #2779159 by mark_fullmer, drunken monkey, borisson_: Added a Stemmer
+  processor.
+- #2799497 by drunken monkey: Added a getter for the Views query's "where"
+  property.
+- #2574583 by drunken monkey: Fixed loading of entities from excluded bundles.
+- #2795861 by sinn: Removed some deprecated methods.
+- #2748323 by sinn: Fixed comment reference to removed method
+  alterPropertyDefinitions().
+- #2809753 by drunken monkey: Fixed issues with multiple OR facets.
+- #2821955 by mkalkbrenner: Adapted processor test base for use with other
+  backends.
+- #2819637 by alan-ps: Renamed use of the DB layer rollback() method to
+  rollBack().
+
+Search API 1.0, Beta 3 (2016-10-24):
+------------------------------------
+- #2625152 by jhedstrom, drunken monkey, borisson_, mpp, stijn.blomme,
+  Rodlangh: Added an "Index hierarchy" processor.
+- #2818621 by alan-ps: Fixed overly accurate index status percentage.
+- #2792277 by drunken monkey: Fixed issues during config syncing of our
+  entities.
+- #2813525 by drunken monkey, alan-ps: Fixed incorrect indexing of
+  nodes/comments with excluded bundle.
+- #2803701 by drunken monkey, rbayliss: Fixed strict warnings from
+  UnsavedConfigurationFormTrait.
+- #2711017 by drunken monkey: Adapted Core's UncacheableDependencyTrait.
+- #2690229 by drunken monkey: Adapted Core's SubformState solution.
+- #2575641 by rbayliss, dazz, drunken monkey: Fixed behavior of "Save and edit"
+  button for indexes.
+- #2769021 by drunken monkey: Added the generated Search API query to the Views
+  preview.
+- #2817341 by mkalkbrenner, drunken monkey: Added PluginDependencyTrait to
+  ConfigurablePluginBase.
+- #2809211 by cristiroma: Fixed size of text fields on "Fields" tab.
+- #2684465 by Dropa, david.gil, drunken monkey: Fixed indexing of
+  non-translated entity references.
+- #2695627 by dermario, drunken monkey: Added support for (NOT) IN and (NOT)
+  BETWEEN operators to Views.
+- #2782577 by drunken monkey, zuhair_ak: Fixed extraction of configurable
+  properties in processors.
+
+Search API 1.0, Beta 2 (2016-09-28):
+------------------------------------
+- #2798643 by drunken monkey, Berdir: Fixed handling of enforced dependencies
+  for search indexes.
+- #2799475 by borisson_, drunken monkey: Added support for Views block and REST
+  displays in the Views search display deriver.
+- #2763161 by drunken monkey, borisson_: Fixed cache issues with Views search
+  display plugins.
+- #2800011 by drunken monkey, borisson_: Fixed display of hidden properties
+  when adding fields.
+- #2794093 by drunken monkey, borisson_, kamalrajsahu21: Fixed the processor
+  reordering CSS.
+- #2640982 by drunken monkey, borisson_: Fixed "unsaved changes" code in the
+  Fields UI.
+- #2727697 by drunken monkey, borisson_: Fixed serialization of modified
+  indexes.
+- #2747767 by joachim: Changed the "Aggregation type" form element to radios.
+- #2565621 by LKS90, drunken monkey: Added a test for the database defaults
+  submodule.
+- #2684465 by drunken monkey, marthinal: Fixed indexing of related entities on
+  multilingual sites.
+- #2566241 by drunken monkey: Fixed index tracker select default value.
+- #2555177 by drunken monkey: Fixed empty bundle selects in datasource config
+  forms.
+
+Search API 1.0, Beta 1 (2016-09-05):
+------------------------------------
+First Beta release of the project's Drupal 8 version. The API can be considered
+mostly stable and an upgrade path will be provided for all data structure
+changes from this point forward.

+ 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.

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

@@ -0,0 +1,112 @@
+CONTENTS OF THIS FILE
+---------------------
+ * Introduction
+ * Requirements
+ * Installation
+ * Configuration
+ * Developers
+ * Maintainers
+
+INTRODUCTION
+------------
+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 faceting support (with [1]) and the ability to use the Views module
+for displaying search results, filters, etc. Also, with the Apache Solr
+integration [2], a high-performance search engine is available for this module.
+
+[1] https://www.drupal.org/project/facets
+[2] https://www.drupal.org/project/search_api_solr
+
+Developers, on the other hand, will be impressed by the large flexibility and
+numerous ways of extension the module provides. Hence, the growing number of
+additional contrib modules, providing additional functionality or helping users
+customize some aspects of the search process.
+  * For a full description of the module, visit the project page:
+   https://www.drupal.org/project/search_api
+  * To submit bug reports and feature suggestions, or to track changes:
+   https://www.drupal.org/project/issues/search_api
+
+REQUIREMENTS
+------------
+No other modules are required.
+
+INSTALLATION
+------------
+Install as you would normally install a contributed Drupal module. For further
+information, see:
+   https://www.drupal.org/docs/8/extending-drupal-8/installing-modules
+
+CONFIGURATION
+-------------
+After installation, for a quick start, just install the "Database Search
+Defaults" module provided with this project. This will automatically set up a
+search view for node content, using a database server for indexing.
+
+Otherwise, you need to enable at least a module providing integration with a
+search backend (like database, Solr, Elasticsearch, …). Possible options are
+listed at [3].
+
+Then, go to
+  /admin/config/search/search-api
+on your site and create a search server and search index. Afterwards, you can
+create a view based on your index to enable users to search the content you
+configured to be indexed. More details are available online in the handbook [4].
+There, you can also find answers to frequently asked questions and common
+pitfalls to avoid.
+
+[3] https://www.drupal.org/docs/8/modules/search-api/getting-started/server-backends-and-features
+[4] https://www.drupal.org/docs/8/modules/search-api/getting-started
+
+DEVELOPERS
+----------
+
+The Search API provides a lot of ways for developers to extend or customize the
+framework.
+
+- Hooks
+  All available hooks are listed in search_api.api.php.
+- Events
+  Currently, only the Search API's task system (for reliably executing necessary
+  system tasks) makes use of events. Every time a task is executed, an event
+  will be fired based on the task's type and the sub-system that scheduled the
+  task is responsible for reacting to it. This system is extensible and can
+  therefore also easily be used by contrib modules based on the Search API. For
+  details, see the description of the \Drupal\search_api\Task\TaskManager class,
+  and the other classes in src/Task for examples.
+- Plugins
+  The Search API defines several plugin types, all listed in its
+  search_api.plugin_type.yml file. Here is a list of them, along with the
+  directory in which you can find there definition files (interface, plugin base
+  and plugin manager):
+  - Backends: src/Backend
+  - Datasources: src/Datasource
+  - Data types: src/DataType
+  - Displays: src/Display
+  - ParseModes: src/ParseMode
+  - Processors: src/Processor
+  - Trackers: src/Tracker
+  The display plugins are a bit of a special case there, because they aren't
+  really "extending" the framework, but are rather a way of telling the Search
+  API (and all modules integrating with it) about search pages your module
+  defines. They can then be used to provide, for example, faceting support for
+  those pages. Therefore, if your module provides any search pages, it's a good
+  idea to provide display plugins for them. For an example (for Views pages),
+  see \Drupal\search_api\Plugin\search_api\display\ViewsPage.
+
+The handbook documentation for developers is available at [5].
+
+[5] https://www.drupal.org/docs/8/modules/search-api/developer-documentation
+
+To know which parts of the module can be relied upon as its public API, please
+read the "Drupal 8 backwards compatibility and internal API policy" [6] and the
+module's issue regarding potential module-specific changes to that policy [7].
+
+[6] https://www.drupal.org/core/d8-bc-policy
+[7] https://www.drupal.org/node/2871549
+
+MAINTAINERS
+-----------
+Current maintainers:
+  * Thomas Seidl (drunken monkey) - https://www.drupal.org/u/drunken-monkey

+ 36 - 0
sites/all/modules/contrib/search/search_api/composer.json

@@ -0,0 +1,36 @@
+{
+  "name": "drupal/search_api",
+  "description": "Provides a generic framework for modules offering search capabilities.",
+  "type": "drupal-module",
+  "homepage": "https://www.drupal.org/project/search_api",
+  "authors": [
+    {
+      "name": "Thomas Seidl",
+      "homepage": "https://www.drupal.org/u/drunken-monkey"
+    },
+    {
+      "name": "Nick Veenhof",
+      "homepage": "https://www.drupal.org/u/nick_vh"
+    },
+    {
+      "name": "See other contributors",
+      "homepage":"https://www.drupal.org/node/790418/committers"
+    }
+  ],
+  "support": {
+    "issues": "https://www.drupal.org/project/issues/search_api",
+    "irc": "irc://irc.freenode.org/drupal-search-api",
+    "source": "http://git.drupal.org/project/search_api.git"
+  },
+  "license": "GPL-2.0+",
+  "require-dev": {
+    "drupal/search_api_autocomplete": "@dev"
+  },
+  "extra": {
+    "drush": {
+      "services": {
+        "drush.services.yml": "^9"
+      }
+    }
+  }
+}

+ 4 - 0
sites/all/modules/contrib/search/search_api/config/install/search_api.settings.yml

@@ -0,0 +1,4 @@
+default_cron_limit: 50
+cron_worker_runtime: 15
+default_tracker: 'default'
+tracking_page_size: 100

+ 96 - 0
sites/all/modules/contrib/search/search_api/config/optional/tour.tour.search-api-index-fields.yml

@@ -0,0 +1,96 @@
+id: search-api-index-fields
+module: search_api
+label: Fields indexed in this index
+langcode: en
+routes:
+  - route_name: entity.search_api_index.fields
+dependencies:
+  module:
+    - search_api
+tips:
+  search-api-index-fields-introduction:
+    id: search-api-index-fields-introduction
+    plugin: text
+    label: Fields indexed in this index
+    body: This page lists which fields are indexed in this index, grouped by datasource. (Datasource-independent fields are listed under "General".) Indexed fields can be used to add filters or sorting to views or other search displays based on the index. Fields with type "Fulltext" can also be used for fulltext searching.
+    weight: 1
+  search-api-index-fields-add:
+    id: search-api-index-fields-add
+    plugin: text
+    label: Add fields
+    body: With the "Add fields" button you can add additional fields to this index.
+    weight: 2
+    attributes:
+      data-class: button-action[data-drupal-selector="edit-add-field"]
+  search-api-index-fields-label:
+    id: search-api-index-fields-label
+    plugin: text
+    label: Label
+    body: A label for the field that will be used to refer to the field in most places in the user interface.
+    weight: 3
+    attributes:
+      data-class: details-wrapper:nth(0) table thead th:nth(0)
+  search-api-index-fields-machine-name:
+    id: search-api-index-fields-machine-name
+    plugin: text
+    label: Machine name
+    body: The internal ID to use for this field. Can safely be ignored by inexperienced users in most cases. Changing a field's machine name requires reindexing of the index.
+    weight: 4
+    attributes:
+      data-class: details-wrapper:nth(0) table thead th:nth(1)
+  search-api-index-fields-property-path:
+    id: search-api-index-fields-property-path
+    plugin: text
+    label: Property path
+    body: The internal relationship linking the indexed item to the field, with links being separated by colons (:). This can be useful information for advanced users, but can otherwise be ignored.
+    weight: 5
+    attributes:
+      data-class: details-wrapper:nth(0) table thead th:nth(2)
+  search-api-index-fields-type:
+    id: search-api-index-fields-type
+    plugin: text
+    label: Type
+    body: The data type to use when indexing the field. Determines how a field can be used in searches. For information on the available types, see the <a href="#search-api-data-types-table">"Data types" box</a> at the bottom of the page.
+    weight: 6
+    attributes:
+      data-class: details-wrapper:nth(0) table thead th:nth(3)
+  search-api-index-fields-boost:
+    id: search-api-index-fields-boost
+    plugin: text
+    label: Boost
+    body: Only applicable for fulltext fields. Determines how "important" the field is compared to other fulltext fields, to influence scoring of fulltext searches.
+    weight: 7
+    attributes:
+      data-class: details-wrapper:nth(0) table thead th:nth(4)
+  search-api-index-fields-edit:
+    id: search-api-index-fields-edit
+    plugin: text
+    label: Edit field
+    body: Some fields have additional configuration available, in which case an "Edit" link is displayed in the "Operations" column.
+    weight: 8
+    attributes:
+      data-class: details-wrapper:nth(0) table tbody td:nth(5) a
+  search-api-index-fields-remove:
+    id: search-api-index-fields-remove
+    plugin: text
+    label: Remove field
+    body: "Removes a field from the index again. (Note: Sometimes, a field is required (for example, by a processor) and cannot be removed.)"
+    weight: 9
+    attributes:
+      data-class: details-wrapper:nth(0) table tbody td:nth(6) a
+  search-api-index-fields-submit:
+    id: search-api-index-fields-submit
+    plugin: text
+    label: Save changes
+    body: This saves all changes made to the fields for this index. Until this button is pressed, all added, changed or removed fields will only be stored temporarily and not effect the actual index used in the rest of the site.
+    weight: 10
+    attributes:
+      data-id: edit-actions-submit
+  search-api-index-fields-cancel:
+    id: search-api-index-fields-cancel
+    plugin: text
+    label: Cancel changes
+    body: If you have made changes to the index's fields but not yet saved them, the "Cancel" link lets you discard those changes.
+    weight: 10
+    attributes:
+      data-id: edit-actions-cancel

+ 65 - 0
sites/all/modules/contrib/search/search_api/config/optional/tour.tour.search-api-index-form.yml

@@ -0,0 +1,65 @@
+id: search-api-index-form
+module: search_api
+label: 'Add or edit a Search API index'
+langcode: en
+routes:
+  - route_name: entity.search_api_index.add_form
+  - route_name: entity.search_api_index.edit_form
+dependencies:
+  module:
+    - search_api
+tips:
+  search-api-index-form-introduction:
+    id: search-api-index-form-introduction
+    plugin: text
+    label: Adding or editing an index
+    body: This form can be used to edit an existing index or add a new index to your site. Indexes define a set of data that will be indexed and can then be searched.
+    weight: 1
+  search-api-index-form-name:
+    id: search-api-index-form-name
+    plugin: text
+    label: Index name
+    body: Enter a name to identify this index. For example, "Content index". This will only be displayed in the admin user interface.
+    weight: 2
+    attributes:
+      data-id: edit-name
+  search-api-index-form-datasources:
+    id: search-api-index-form-datasources
+    plugin: text
+    label: Datasources
+    body: Datasources define the types of items that will be indexed in this index. By default, all content entities (like content, comments and taxonomy terms) will be available here, but modules can also add their own.
+    weight: 3
+    attributes:
+      data-id: edit-datasources
+  search-api-index-form-tracker:
+    id: search-api-index-form-tracker
+    plugin: text
+    label: Tracker
+    body: An index's tracker is the system that keeps track of which items there are available for the index, and which of them still need to be indexed. Changing the tracker of an existing index will lead to reindexing of all items.
+    weight: 4
+    attributes:
+      data-id: edit-tracker
+  search-api-index-form-server:
+    id: search-api-index-form-server
+    plugin: text
+    label: Server
+    body: The search server that the index should use for indexing and searching. If no server is selected here, the index cannot be enabled. An index can only have one server, but a server can have any number of indexes.
+    weight: 5
+    attributes:
+      data-id: edit-server
+  search-api-index-form-description:
+    id: search-api-index-form-description
+    plugin: text
+    label: Index description
+    body: Optionally, enter a description to explain the function of the index in more detail. This will only be displayed in the admin user interface.
+    weight: 6
+    attributes:
+      data-id: edit-description
+  search-api-index-form-options:
+    id: search-api-index-form-options
+    plugin: text
+    label: Advanced options
+    body: These options allow more detailed configuration of index behavior, but can usually safely be ignored by inexperienced users.
+    weight: 7
+    attributes:
+      data-id: edit-options

+ 40 - 0
sites/all/modules/contrib/search/search_api/config/optional/tour.tour.search-api-index-processors.yml

@@ -0,0 +1,40 @@
+id: search-api-index-processors
+module: search_api
+label: Processors used for this index
+langcode: en
+routes:
+  - route_name: entity.search_api_index.processors
+dependencies:
+  module:
+    - search_api
+tips:
+  search-api-index-processors-introduction:
+    id: search-api-index-processors-introduction
+    plugin: text
+    label: Processors used for this index
+    body: Processors customize different aspects of an index's functionality. They can keep items from being indexed, change how certain fields are indexed and influence searches.
+    weight: 1
+  search-api-index-processors-enable:
+    id: search-api-index-processors-enable
+    plugin: text
+    label: Enable processors
+    body: "This lists all processors available for this index and lets you choose the ones that should be active. (Note: Some processors cannot be disabled.)"
+    weight: 2
+    attributes:
+      data-id: edit-status
+  search-api-index-processors-weights:
+    id: search-api-index-processors-weights
+    plugin: text
+    label: Processor order
+    body: This shows you which enabled processors will be active in the different parts of the indexing/searching workflow, and lets you re-arrange them. This should usually not be necessary, and only be used by advanced users as some processors will lead to unexpected results when used in the wrong order.
+    weight: 3
+    attributes:
+      data-id: edit-weights
+  search-api-index-processors-settings:
+    id: search-api-index-processors-settings
+    plugin: text
+    label: Processor settings
+    body: Some processors have additional configuration available, which you are able to change here.
+    weight: 4
+    attributes:
+      data-class: form-type-vertical-tabs

+ 104 - 0
sites/all/modules/contrib/search/search_api/config/optional/tour.tour.search-api-index.yml

@@ -0,0 +1,104 @@
+id: search-api-index
+module: search_api
+label: Information about an index
+langcode: en
+routes:
+  - route_name: entity.search_api_index.canonical
+dependencies:
+  module:
+    - search_api
+tips:
+  search-api-index-introduction:
+    id: search-api-index-introduction
+    plugin: text
+    label: Information about an index
+    body: This page shows a summary of a search index and its status.
+    weight: 1
+  search-api-index-index-status:
+    id: search-api-index-index-status
+    plugin: text
+    label: Index status
+    body: This gives a summary about how many items are known for this index, and how many have been indexed in their latest version. Items that are not indexed yet cannot be found by searches.
+    weight: 2
+    attributes:
+      data-class: search-api-index-status
+  search-api-index-status:
+    id: search-api-index-status
+    plugin: text
+    label: Status
+    body: Shows whether the index is currently enabled or disabled.
+    weight: 3
+    attributes:
+      data-class: search-api-index-summary--status
+  search-api-index-datasources:
+    id: search-api-index-datasources
+    plugin: text
+    label: Datasources
+    body: Lists all datasources that are enabled for this index.
+    weight: 4
+    attributes:
+      data-class: search-api-index-summary--datasource
+  search-api-index-tracker:
+    id: search-api-index-tracker
+    plugin: text
+    label: Tracker
+    body: The tracker used by the index. Only one ("Default") is available by default.
+    weight: 5
+    attributes:
+      data-class: search-api-index-summary--tracker
+  search-api-index-server:
+    id: search-api-index-server
+    plugin: text
+    label: Server
+    body: If the index is attached to a server, this server is listed here.
+    weight: 6
+    attributes:
+      data-class: search-api-index-summary--server
+  search-api-index-server-index-status:
+    id: search-api-index-server-index-status
+    plugin: text
+    label: Server index status
+    body: For enabled indexes, the number of items that can actually be retrieved from the server is listed here. For reasons why this number might differ from the number under "Index status", <a href="https://www.drupal.org/node/2009804#server-index-status">see the module's documentation</a>.
+    weight: 7
+    attributes:
+      data-class: search-api-index-summary--server-index-status
+  search-api-index-cron-batch-size:
+    id: search-api-index-cron-batch-size
+    plugin: text
+    label: Cron batch size
+    body: The number of items that will be indexed at once during cron runs.
+    weight: 8
+    attributes:
+      data-class: search-api-index-summary--cron-batch-size
+  search-api-index-index-now:
+    id: search-api-index-remove
+    plugin: text
+    label: Start indexing now
+    body: The "Start indexing now" form allows indexing items manually right away, with a batch process. Otherwise, items are only indexed during cron runs. The form might be disabled if indexing is currently not possible for some reason, or not necessary.
+    weight: 9
+    attributes:
+      data-id: edit-index
+  search-api-index-tracking:
+    id: search-api-index-tracking
+    plugin: text
+    label: Track items for index
+    body: In certain situations, the index's tracker doesn't have the latest state of the items available for indexing. This will be automatically rectified during cron runs, but can also be manually triggered here, with the "Track now" button.
+    weight: 10
+    attributes:
+      data-id: edit-tracking
+  search-api-index-reindex:
+    id: search-api-index-reindex
+    plugin: text
+    label: Queue all items for reindexing
+    body: This will queue all items on this index for reindexing. Previously indexed data will remain on the search server, so searches on this index will continue to yield results.
+    weight: 11
+    attributes:
+      data-id: edit-reindex
+  search-api-index-clear:
+    id: search-api-index-clear
+    plugin: text
+    label: Clear all indexed data
+    body: This will remove all indexed content for this index from the search server and queue it for reindexing. Searches on this index will not return any results until items are reindexed.
+    weight: 12
+    attributes:
+      data-id: edit-clear

+ 41 - 0
sites/all/modules/contrib/search/search_api/config/optional/tour.tour.search-api-server-form.yml

@@ -0,0 +1,41 @@
+id: search-api-server-form
+module: search_api
+label: 'Add or edit a Search API server'
+langcode: en
+routes:
+  - route_name: entity.search_api_server.add_form
+  - route_name: entity.search_api_server.edit_form
+dependencies:
+  module:
+    - search_api
+tips:
+  search-api-server-form-introduction:
+    id: search-api-server-form-introduction
+    plugin: text
+    label: Adding or editing a Server
+    body: This form can be used to edit an existing server or add a new server to your site. Servers will hold your indexed data.
+    weight: 1
+  search-api-server-form-name:
+    id: search-api-server-form-name
+    plugin: text
+    label: Server name
+    body: Enter a name to identify this server. For example, "Solr server". This will only be displayed in the admin user interface.
+    weight: 2
+    attributes:
+      data-id: edit-name
+  search-api-server-form-description:
+    id: search-api-server-form-description
+    plugin: text
+    label: Server description
+    body: Optionally, enter a description to explain the function of the server in more detail. This will only be displayed in the admin user interface.
+    weight: 3
+    attributes:
+      data-id: edit-description
+  search-api-server-form-backend:
+    id: search-api-server-form-backend
+    plugin: text
+    label: Server backend
+    body: Servers can be based on different technologies. These are called "backends". A server uses exactly one backend and cannot change it later. You can make the "Database" backend available by enabling the "Database Search" module. Another very common backend is <a href="https://www.drupal.org/project/search_api_solr">"Solr"</a>, which requires to be set up separately.
+    weight: 4
+    attributes:
+      data-id: edit-backend

+ 48 - 0
sites/all/modules/contrib/search/search_api/config/optional/tour.tour.search-api-server.yml

@@ -0,0 +1,48 @@
+id: search-api-server
+module: search_api
+label: Information about a server
+langcode: en
+routes:
+  - route_name: entity.search_api_server.canonical
+dependencies:
+  module:
+    - search_api
+tips:
+  search-api-server-introduction:
+    id: search-api-server-introduction
+    plugin: text
+    label: Information about a server
+    body: This page shows a summary of a search server.
+    weight: 1
+  search-api-server-status:
+    id: search-api-server-status
+    plugin: text
+    label: Status
+    body: Shows whether the server is currently enabled or disabled.
+    weight: 2
+    attributes:
+      data-class: search-api-server-summary--status
+  search-api-server-backend:
+    id: search-api-server-backend
+    plugin: text
+    label: Backend class
+    body: The backend plugin used for this server. The backend plugin determines how items are indexed and searched – for example, using the database or an Apache Solr server.
+    weight: 3
+    attributes:
+      data-class: search-api-server-summary--backend
+  search-api-server-indexes:
+    id: search-api-server-indexes
+    plugin: text
+    label: Search indexes
+    body: Lists all search indexes that are attached to this server.
+    weight: 4
+    attributes:
+      data-class: search-api-server-summary--indexes
+  search-api-server-clear:
+    id: search-api-server-clear
+    plugin: text
+    label: Delete all indexed data
+    body: This will permanently remove all data currently indexed on this server for indexes that aren't read-only. Items are queued for reindexing. Until reindexing occurs, searches for the affected indexes will not return any results.
+    weight: 5
+    attributes:
+      data-id: edit-clear

+ 30 - 0
sites/all/modules/contrib/search/search_api/config/schema/search_api.datasource.schema.yml

@@ -0,0 +1,30 @@
+"plugin.plugin_configuration.search_api_datasource.entity:*":
+  type: mapping
+  label: 'Entity datasource configuration'
+  mapping:
+    bundles:
+      type: mapping
+      label: 'Bundles'
+      mapping:
+        default:
+          type: boolean
+          label: 'Whether to exclude (TRUE) or include (FALSE) the selected bundles.'
+        selected:
+          type: sequence
+          label: 'The selected bundles'
+          sequence:
+            type: string
+            label: 'A bundle machine name'
+    languages:
+      type: mapping
+      label: 'Languages'
+      mapping:
+        default:
+          type: boolean
+          label: 'Whether to exclude (TRUE) or include (FALSE) the selected languages.'
+        selected:
+          type: sequence
+          label: 'The selected languages'
+          sequence:
+            type: string
+            label: 'A language code'

+ 95 - 0
sites/all/modules/contrib/search/search_api/config/schema/search_api.index.schema.yml

@@ -0,0 +1,95 @@
+search_api.index.*:
+  type: config_entity
+  label : 'Search index'
+  mapping:
+    id:
+      type: string
+      label: 'ID'
+    name:
+      type: label
+      label: Name'
+    uuid:
+      type: string
+      label: 'UUID'
+    description:
+      type: text
+      label: 'Description'
+    read_only:
+      type: boolean
+      label: 'Read-only'
+    field_settings:
+      type: sequence
+      label: 'Indexed fields'
+      sequence:
+        type: mapping
+        label: field
+        mapping:
+          label:
+            type: string
+            label: 'A label for the field'
+          datasource_id:
+            type: string
+            label: 'The datasource ID of the field'
+          property_path:
+            type: string
+            label: 'The property path of the field'
+          type:
+            type: string
+            label: 'The data type of the field'
+          boost:
+            type: float
+            label: 'The boost of the field'
+          configuration:
+            type: search_api.property_configuration.[%parent.property_path]
+          indexed_locked:
+            type: boolean
+            label: 'Whether the field is locked or can be removed'
+          type_locked:
+            type: boolean
+            label: 'Whether the field''s data type is locked or can be changed'
+          hidden:
+            type: boolean
+            label: 'Whether the field should appear in the UI'
+          dependencies:
+            type: config_dependencies
+            label: 'The field''s dependencies'
+    datasource_settings:
+      type: sequence
+      label: 'Datasource settings'
+      sequence:
+        type: plugin.plugin_configuration.search_api_datasource.[%key]
+        label: 'The configuration for a single datasource'
+    processor_settings:
+      type: sequence
+      label: 'Processor settings'
+      sequence:
+        type: plugin.plugin_configuration.search_api_processor.[%key]
+        label: 'The configuration for a single processor'
+    tracker_settings:
+      type: sequence
+      label: 'Tracker settings'
+      sequence:
+        type: plugin.plugin_configuration.search_api_tracker.[%key]
+        label: 'The configuration for the tracker'
+    options:
+      type: mapping
+      label: 'Options'
+      mapping:
+        cron_limit:
+          type: integer
+          label: 'Cron batch size'
+        index_directly:
+          type: boolean
+          label: 'Index items immediately'
+    server:
+      type: string
+      label: 'Server ID'
+    status:
+      type: boolean
+      label: 'Status'
+    langcode:
+      type: string
+      label: 'Language code'
+    dependencies:
+      type: config_dependencies
+      label: 'Dependencies'

+ 230 - 0
sites/all/modules/contrib/search/search_api/config/schema/search_api.processor.schema.yml

@@ -0,0 +1,230 @@
+# Base definitions for processors
+
+search_api.default_processor_configuration:
+  type: mapping
+  label: 'Default processor configuration'
+  mapping:
+    weights:
+      type: sequence
+      label: 'The processor''s weights for the different processing stages'
+      sequence:
+        type: integer
+        label: 'The processor''s weight for this stage'
+
+search_api.fields_processor_configuration:
+  type: search_api.default_processor_configuration
+  label: 'Fields processor configuration'
+  mapping:
+    all_fields:
+      type: boolean
+      label: 'Enabled for all supported fields'
+    fields:
+      type: sequence
+      label: 'The selected fields'
+      sequence:
+        type: string
+        label: 'Selected field'
+
+# Default for any processor without specific configuration
+plugin.plugin_configuration.search_api_processor.*:
+  type: search_api.default_processor_configuration
+
+# Definitions for individual processors
+
+plugin.plugin_configuration.search_api_processor.hierarchy:
+  type: search_api.default_processor_configuration
+  label: 'Hierarchy processor configuration'
+  mapping:
+    fields:
+      type: sequence
+      label: 'Fields for which to add the hierarchy'
+      sequence:
+        type: string
+        label: 'Field ID'
+
+plugin.plugin_configuration.search_api_processor.highlight:
+  type: search_api.default_processor_configuration
+  label: 'Highlight processor configuration'
+  mapping:
+    prefix:
+      type: string
+      label: 'Text/HTML that will be prepended to all occurrences of search keywords in highlighted text'
+    suffix:
+      type: string
+      label: 'Text/HTML that will be appended to all occurrences of search keywords in highlighted text'
+    excerpt:
+      type: boolean
+      label: 'When enabled, an excerpt will be created for searches with keywords, containing all occurrences of keywords in a fulltext field.'
+    excerpt_length:
+      type: integer
+      label: 'The requested length of the excerpt, in characters'
+    exclude_fields:
+      type: sequence
+      label: 'Fields excluded from excerpt'
+      sequence:
+        type: string
+        label: 'An excluded field''s ID'
+    highlight:
+      type: string
+      label: 'Defines whether returned fields should be highlighted (always/if returned/never).'
+    highlight_partial:
+      type: boolean
+      label: 'Whether matches in parts of words should be highlighted'
+
+plugin.plugin_configuration.search_api_processor.html_filter:
+  type: search_api.fields_processor_configuration
+  label: 'HTML filter processor configuration'
+  mapping:
+    title:
+      type: boolean
+      label: 'Title'
+    alt:
+      type: boolean
+      label: 'Alt'
+    tags:
+      type: sequence
+      label: 'Tag boosts'
+      sequence:
+        type: integer
+        label: Boost
+
+plugin.plugin_configuration.search_api_processor.ignorecase:
+  type: search_api.fields_processor_configuration
+  label: 'Ignore case processor configuration'
+
+plugin.plugin_configuration.search_api_processor.ignore_character:
+  type: search_api.fields_processor_configuration
+  label: 'Ignore Character processor configuration'
+  mapping:
+    ignorable:
+      type: string
+      label: 'Regular expression for characters it should ignore to stem'
+    strip:
+      type: mapping
+      label: 'Configurable characters to ignore'
+      mapping:
+        character_sets:
+          type: sequence
+          label: 'Configuration of the character sets'
+          sequence:
+            type: ignore
+            label: 'Character set'
+
+plugin.plugin_configuration.search_api_processor.stemmer:
+  type: search_api.fields_processor_configuration
+  label: 'Stemmer processor configuration'
+  mapping:
+    exceptions:
+      type: sequence
+      label: 'Stemming exceptions'
+      sequence:
+        type: string
+        label: Exception
+
+plugin.plugin_configuration.search_api_processor.role_filter:
+  type: search_api.default_processor_configuration
+  label: 'Role filter processor configuration'
+  mapping:
+    default:
+      type: boolean
+      label: 'Default'
+    roles:
+      type: sequence
+      label: 'The selected roles'
+      sequence:
+        type: string
+        label: 'The role name'
+
+plugin.plugin_configuration.search_api_processor.stopwords:
+  type: search_api.fields_processor_configuration
+  label: 'Stopwords processor configuration'
+  mapping:
+    stopwords:
+      type: sequence
+      label: 'entered stopwords'
+      sequence:
+        type: string
+        label: Stopword
+
+plugin.plugin_configuration.search_api_processor.tokenizer:
+  type: search_api.fields_processor_configuration
+  label: 'Tokenizer processor configuration'
+  mapping:
+    spaces:
+      type: string
+      label: 'Regular expression for spaces'
+    ignorable:
+      type: string
+      label: 'Regular expression for ignorable characters'
+    overlap_cjk:
+      type: integer
+      label: 'Defines if simple CJK handling should be enabled.'
+    minimum_word_size:
+      type: string
+      label: 'Defines the minimum word size'
+
+plugin.plugin_configuration.search_api_processor.transliteration:
+  type: search_api.fields_processor_configuration
+  label: 'Transliteration processor configuration'
+
+plugin.plugin_configuration.search_api_processor.type_boost:
+  type: search_api.default_processor_configuration
+  label: 'Type-specific boosting processor configuration'
+  mapping:
+    boosts:
+      type: sequence
+      label: 'Boost settings'
+      sequence:
+        type: mapping
+        label: 'Datasource boost settings'
+        mapping:
+          datasource_boost:
+            type: float
+            label: 'Base boost for the datasource'
+          bundle_boosts:
+            type: sequence
+            label: 'Bundle-specific boosts'
+            sequence:
+              type: float
+              label: 'The boost value for this bundle'
+
+# Definitions for property configuration
+
+search_api.property_configuration.*:
+  type: mapping
+  label: 'Default field configuration'
+  mapping: {}
+
+search_api.property_configuration.aggregated_field:
+  type: mapping
+  label: 'Aggregated field configuration'
+  mapping:
+    type:
+      type: string
+      label: 'The type of the aggregation'
+    fields:
+      type: sequence
+      label: 'The properties to be aggregated'
+      sequence:
+        type: string
+        label: 'A property that should be part of the aggregation'
+
+search_api.property_configuration.rendered_item:
+  type: mapping
+  label: 'Rendered item processor configuration'
+  mapping:
+    roles:
+      type: sequence
+      label: 'The selected roles'
+      sequence:
+        type: string
+        label: 'The user roles which will be active when the entity is rendered'
+    view_mode:
+      type: sequence
+      label: 'The selected view modes for each datasource, by bundle'
+      sequence:
+        type: sequence
+        label: 'The selected view modes for the datasource, by bundle'
+        sequence:
+          type: string
+          label: 'The view mode used to render the entity for the specified bundle'

+ 16 - 0
sites/all/modules/contrib/search/search_api/config/schema/search_api.schema.yml

@@ -0,0 +1,16 @@
+search_api.settings:
+  type: config_object
+  label: 'Search API settings'
+  mapping:
+    default_cron_limit:
+      type: integer
+      label: 'Default cron batch size'
+    cron_worker_runtime:
+      type: integer
+      label: 'Maximum runtime for the cron worker'
+    default_tracker:
+      type: string
+      label: 'The default tracker plugin'
+    tracking_page_size:
+      type: integer
+      label: 'The number of entities loaded at once when adding items to the tracker for a newly created index'

+ 33 - 0
sites/all/modules/contrib/search/search_api/config/schema/search_api.server.schema.yml

@@ -0,0 +1,33 @@
+search_api.server.*:
+  type: config_entity
+  label : 'Search server'
+  mapping:
+    id:
+      type: string
+      label: 'ID'
+    name:
+      type: label
+      label: 'Name'
+    uuid:
+      type: string
+      label: 'UUID'
+    description:
+      type: text
+      label: 'Description'
+    status:
+      type: boolean
+      label: 'Status'
+    backend:
+      type: string
+      label: 'Backend Plugin ID'
+    backend_config:
+      type: plugin.plugin_configuration.search_api_backend.[%parent.backend]
+    langcode:
+      type: string
+      label: 'Language code'
+    dependencies:
+      type: config_dependencies
+      label: 'Dependencies'
+
+plugin.plugin_configuration.search_api_backend.*:
+  type: mapping

+ 7 - 0
sites/all/modules/contrib/search/search_api/config/schema/search_api.tracker.schema.yml

@@ -0,0 +1,7 @@
+plugin.plugin_configuration.search_api_tracker.default:
+  type: mapping
+  label: 'Entity tracker configuration'
+  mapping:
+    indexing_order:
+      type: string
+      label: 'Indexing order'

+ 294 - 0
sites/all/modules/contrib/search/search_api/config/schema/search_api.views.schema.yml

@@ -0,0 +1,294 @@
+views.query.search_api_query:
+  type: views_query
+  label: 'Search API query'
+  mapping:
+    bypass_access:
+      type: boolean
+      label: If the underlying search index has access checks enabled, this option allows you to disable them for this view.
+    skip_access:
+      type: boolean
+      label: Do not execute additional access checks for all items in the search results.
+
+views.row.search_api:
+  type: views_row
+  label: 'Search API rendered item'
+  mapping:
+    view_modes:
+      type: sequence
+      label: View modes for each datasource
+      sequence:
+        type: sequence
+        label: A list of all the bundles and their configured view mode
+        sequence:
+          type: string
+          label: View mode for the specific bundle
+
+views.cache.search_api_time:
+  type: views_cache
+  label: 'Time-based caching (Search API)'
+  mapping:
+    options:
+      type: mapping
+      label: 'Cache options'
+      mapping:
+        results_lifespan:
+          type: integer
+          label: 'The length of time raw query results should be cached.'
+        results_lifespan_custom:
+          type: integer
+          label: 'Length of time in seconds raw query results should be cached.'
+        output_lifespan:
+          type: integer
+          label: 'The length of time rendered HTML output should be cached.'
+        output_lifespan_custom:
+          type: integer
+          label: 'Length of time in seconds rendered HTML output should be cached.'
+
+views.cache.search_api_tag:
+  type: views_cache
+  label: 'Tag-based caching (Search API)'
+  mapping:
+    options:
+      type: mapping
+      label: 'Cache options'
+
+
+views.argument.search_api:
+  type: views.argument.numeric
+  label: 'Search API'
+
+views.argument.search_api_fulltext:
+  type: views.argument.search_api
+  label: 'Search API more like this'
+  mapping:
+    parse_mode:
+      type: string
+      label: 'Parse mode'
+    conjunction:
+      type: string
+      label: 'Conjunction'
+    fields:
+      type: sequence
+      label: 'Fields'
+      sequence:
+        type: string
+        label: 'Field'
+
+views.argument.search_api_more_like_this:
+  type: views_argument
+  label: 'Search API more like this'
+  mapping:
+    fields:
+      type: sequence
+      label: 'Fields'
+      sequence:
+        type: string
+        label: 'Field'
+
+views.argument.search_api_term:
+  type: views.argument.search_api
+  label: 'Search API taxonomy term'
+
+views.field.search_api:
+  type: views_field
+  label: 'Search API standard'
+  mapping:
+    link_to_item:
+      type: boolean
+      label: 'Link to item'
+    use_highlighting:
+      type: boolean
+      label: 'Use highlighted field data'
+    multi_type:
+      type: string
+      label: 'Handling of multiple values'
+    multi_separator:
+      type: string
+      label: 'Separator for multiple values'
+
+views.field.search_api_boolean:
+  type: views.field.boolean
+  label: 'Search API boolean'
+  mapping:
+    link_to_item:
+      type: boolean
+      label: 'Link to item'
+    use_highlighting:
+      type: boolean
+      label: 'Use highlighted field data'
+    multi_type:
+      type: string
+      label: 'Handling of multiple values'
+    multi_separator:
+      type: string
+      label: 'Separator for multiple values'
+
+views.field.search_api_date:
+  type: views.field.date
+  label: 'Search API date'
+  mapping:
+    link_to_item:
+      type: boolean
+      label: 'Link to item'
+    use_highlighting:
+      type: boolean
+      label: 'Use highlighted field data'
+    multi_type:
+      type: string
+      label: 'Handling of multiple values'
+    multi_separator:
+      type: string
+      label: 'Separator for multiple values'
+
+views.field.search_api_entity:
+  type: views.field.search_api
+  label: 'Search API entity reference'
+  mapping:
+    display_methods:
+      type: sequence
+      label: 'Display settings'
+      sequence:
+        type: mapping
+        label: 'Display settings for bundle'
+        mapping:
+          display_method:
+            type: string
+            label: 'Display method'
+          view_mode:
+            type: string
+            label: 'View mode'
+
+views.field.search_api_field:
+  type: views.field.field
+  label: 'Search API entity field'
+  mapping:
+    field_rendering:
+      type: boolean
+      label: 'Use entity field rendering'
+    fallback_handler:
+      type: string
+      label: 'Fallback handler'
+    fallback_options:
+      type: views.field.[%parent.fallback_handler]
+      label: 'Options for fallback handler'
+
+views.field.search_api_numeric:
+  type: views.field.numeric
+  label: 'Search API boolean'
+  mapping:
+    link_to_item:
+      type: boolean
+      label: 'Link to item'
+    use_highlighting:
+      type: boolean
+      label: 'Use highlighted field data'
+    multi_type:
+      type: string
+      label: 'Handling of multiple values'
+    multi_separator:
+      type: string
+      label: 'Separator for multiple values'
+    format_plural_values:
+      type: sequence
+      label: 'Pluralized strings'
+      sequence:
+        type: string
+        label: 'Singular/Plural string'
+
+views.filter.search_api_boolean:
+  type: views.filter.boolean
+  label: 'Search API boolean'
+
+views.filter.search_api_datasource:
+  type: views.filter.search_api_options
+  label: 'Search API datasource'
+
+views.filter.search_api_fulltext:
+  type: views_filter
+  label: 'Search API fulltext search'
+  mapping:
+    parse_mode:
+      type: string
+      label: Parse mode
+    min_length:
+      type: integer
+      label: Minimum search string length
+    fields:
+      type: sequence
+      label: Fields to search on
+      sequence:
+        type: string
+        label: Field name
+
+views.filter.search_api_language:
+  type: views.filter.language
+  label: 'Search API language'
+
+views.filter.search_api_numeric:
+  type: views.filter.numeric
+  label: 'Search API numeric'
+
+views.filter.search_api_options:
+  type: views.filter.many_to_one
+  label: 'Search API options'
+
+views.filter.search_api_string:
+  type: views.filter.search_api_numeric
+  label: 'Search API string'
+
+views.filter.search_api_term:
+  type: views.filter.taxonomy_index_tid
+  label: 'Search API taxonomy term'
+
+views.filter.search_api_text:
+  type: views.filter.search_api_string
+  label: 'Search API text'
+
+views.filter.search_api_user:
+  type: views.filter.user_name
+  label: 'Search API user'
+
+views.filter_value.search_api_boolean:
+  type: string
+
+views.filter_value.search_api_datasource:
+  type: string
+
+views.filter_value.search_api_date:
+  type: views.filter_value.date
+  label: 'Search API date'
+
+views.filter_value.search_api_fulltext:
+  type: string
+
+views.filter_value.search_api_language:
+  type: string
+
+views.filter_value.search_api_numeric:
+  type: views.filter_value.numeric
+
+views.filter_value.search_api_options:
+  type: sequence
+  sequence:
+    type: string
+    label: 'Value'
+
+views.filter_value.search_api_string:
+  type: views.filter_value.numeric
+
+views.filter_value.search_api_term:
+  type: string
+
+views.filter_value.search_api_text:
+  type: views.filter_value.numeric
+
+views.filter_value.search_api_user:
+  type: string
+
+views.relationship.search_api:
+  type: views_relationship
+  label: 'Search API'
+  mapping:
+    skip_access:
+      type: boolean
+      label: Do not execute access checks for entities referenced via this relationship.

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

@@ -0,0 +1,93 @@
+/**
+ * @file
+ * Administration styles for the Search API module.
+ */
+
+/*
+ * Search API overview page
+ */
+.search-api-entity-list {
+  margin-bottom: 2em;
+}
+
+.search-api-entity-list tr {
+  border-bottom: none;
+}
+
+.search-api-list-server {
+  border-top: 1px solid #e6e4df;
+}
+
+.search-api-list-index:last-of-type {
+  border-bottom: 1px solid #e6e4df;
+}
+
+.search-api-list-server .search-api-type,
+.search-api-list-server .search-api-title {
+  font-weight: bold;
+}
+
+.search-api-list-index .search-api-type {
+  padding-left: 3em;
+}
+
+.search-api-list-disabled .search-api-type,
+.search-api-list-disabled .search-api-title {
+  font-style: italic;
+}
+
+/*
+ * Server view page
+ */
+.search-api-server-summary ul.inline {
+  margin: 0;
+}
+
+.search-api-server-summary ul.inline li {
+  padding-left: 0;
+}
+
+details .messages {
+  margin-left: 20px;
+  margin-right: 20px;
+}
+
+/*
+ * Index summary page
+ */
+.search-api-index-status {
+  margin-bottom: 6em;
+}
+
+/* Remove animation */
+.search-api-index-status .progress__bar {
+  background-image: none;
+}
+
+.search-api-index-summary {
+  margin-bottom: 2em;
+}
+
+/*
+ * Processors page
+ */
+
+.search-api-stage-wrapper.form-item {
+  float: left;
+  box-sizing: border-box;
+  width: 32.66%;
+  min-width: 17em;
+  margin-right: 1%;
+}
+
+.search-api-stage-wrapper.form-item:last-child {
+  margin-right: 0;
+}
+
+/*
+ * Miscellaneous
+ */
+.search-api-checkboxes-list .form-checkboxes {
+  max-height: 30em;
+  overflow: auto;
+}

+ 8 - 0
sites/all/modules/contrib/search/search_api/drush.services.yml

@@ -0,0 +1,8 @@
+services:
+  search_api.commands:
+    class: \Drupal\search_api\Commands\SearchApiCommands
+    arguments:
+      - '@entity_type.manager'
+      - '@module_handler'
+    tags:
+      - { name: drush.command }

+ 49 - 0
sites/all/modules/contrib/search/search_api/js/search_api.processors.js

@@ -0,0 +1,49 @@
+/**
+ * @file
+ * Attaches show/hide functionality to checkboxes in the "Processor" tab.
+ */
+
+(function ($) {
+
+  'use strict';
+
+  Drupal.behaviors.searchApiProcessor = {
+    attach: function (context, settings) {
+      $('.search-api-status-wrapper input.form-checkbox', context).each(function () {
+        var $checkbox = $(this);
+        var processor_id = $checkbox.data('id');
+
+        var $rows = $('.search-api-processor-weight--' + processor_id, context);
+        var tab = $('.search-api-processor-settings-' + processor_id, context).data('verticalTab');
+
+        // Bind a click handler to this checkbox to conditionally show and hide
+        // the processor's table row and vertical tab pane.
+        $checkbox.on('click.searchApiUpdate', function () {
+          if ($checkbox.is(':checked')) {
+            $rows.show();
+            if (tab) {
+              tab.tabShow().updateSummary();
+            }
+          }
+          else {
+            $rows.hide();
+            if (tab) {
+              tab.tabHide().updateSummary();
+            }
+          }
+        });
+
+        // Attach summary for configurable items (only for screen-readers).
+        if (tab) {
+          tab.details.drupalSetSummary(function () {
+            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');
+      });
+    }
+  };
+
+})(jQuery);

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

@@ -0,0 +1,60 @@
+Database Search
+---------------
+
+This module provides a database-based implementation of the Search API. The
+database and target to use for storing and accessing the indexes can be selected
+when creating a new server.
+
+All Search API data types are supported by using appropriate SQL data types for
+their respective columns.
+
+The "direct" parse mode for queries will result in a simple splitting of the
+query string into keys. Additionally, search keys containing whitespace will be
+split for all parse modes, as searching for phrases is currently not supported.
+
+Supported optional features
+---------------------------
+
+- search_api_autocomplete
+  Introduced by module: search_api_autocomplete
+  Lets you add autocompletion capabilities to search forms on the site. (See
+  also "Hidden variables" below for backend-specific customization.)
+  NOTE: Due to internal database restrictions, this will perform significantly
+  better if only a single field is used for autocompletion.
+- search_api_facets
+  Introduced by module: facets
+  Allows you to create faceted searches for dynamically filtering search
+  results.
+- search_api_facets_operator_or
+  Introduced by module: facets
+  Allows the use of the "OR" operator for facets.
+
+If you feel some backend option is missing, or have other ideas for improving
+this implementation, please file a feature request in the project's issue queue,
+at [1], using the "Database search" component.
+
+[1] http://drupal.org/project/issues/search_api
+
+Known problems
+--------------
+
+Using facets and autocomplete suggestions with a database server will only work
+if the database user Drupal is using has the "CREATE TEMPORARY TABLES"
+permission (or similar, in DBMSs other than MySQL).
+
+Developer information
+---------------------
+
+Database queries for searches with this module are tagged with
+"search_api_db_search" to allow easy altering. As metadata, such database
+queries will have the Search API query object set as "search_api_query", and the
+field settings of the server for the corresponding search index as
+"search_api_db_fields".
+
+Hidden configuration
+--------------------
+
+- search_api_db.settings.autocomplete_max_occurrences (default: 0.9)
+  By default, keywords that occur in more than 90% of results are ignored for
+  autocomplete suggestions. This setting lets you modify that behavior by
+  providing your own ratio. Use 1 or greater to use all suggestions.

+ 1 - 0
sites/all/modules/contrib/search/search_api/modules/search_api_db/config/install/search_api_db.settings.yml

@@ -0,0 +1 @@
+autocomplete_max_occurrences: 0.9

+ 23 - 0
sites/all/modules/contrib/search/search_api/modules/search_api_db/config/schema/search_api_db.backend.schema.yml

@@ -0,0 +1,23 @@
+plugin.plugin_configuration.search_api_backend.search_api_db:
+  type: mapping
+  label: 'Search API DB settings'
+  mapping:
+    database:
+      type: 'string'
+      label: 'Name of the database we are connecting to'
+    min_chars:
+      type: 'integer'
+      label: 'Minimum length of indexed words'
+    partial_matches:
+      type: 'boolean'
+      label: 'Whether to also search on parts of a word'
+    autocomplete:
+      type: mapping
+      label: Autcomplete configuration
+      mapping:
+        suggest_suffix:
+          type: boolean
+          label: Suggest suffix
+        suggest_words:
+          type: boolean
+          label: Suggest words

+ 7 - 0
sites/all/modules/contrib/search/search_api/modules/search_api_db/config/schema/search_api_db.schema.yml

@@ -0,0 +1,7 @@
+search_api_db.settings:
+  type: config_object
+  label: 'Search API DB settings'
+  mapping:
+    autocomplete_max_occurrences:
+      type: float
+      label: 'Maximum occurrences of a term until autocomplete suggestions ignores it'

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

@@ -0,0 +1,38 @@
+<?php
+
+/**
+ * @file
+ * Hooks provided by the Database Search module.
+ */
+
+/**
+ * @addtogroup hooks
+ * @{
+ */
+
+/**
+ * Preprocess a search's database query before it is executed.
+ *
+ * This allows other modules to alter the DB query before a count query (or
+ * facet queries, or other related queries) are constructed from it.
+ *
+ * @param \Drupal\Core\Database\Query\SelectInterface $db_query
+ *   The database query to be executed for the search. Will have "item_id" and
+ *   "score" columns in its result.
+ * @param \Drupal\search_api\Query\QueryInterface $query
+ *   The search query that is being executed.
+ *
+ * @see \Drupal\search_api_db\Plugin\search_api\backend\Database::preQuery()
+ */
+function hook_search_api_db_query_alter(\Drupal\Core\Database\Query\SelectInterface &$db_query, \Drupal\search_api\Query\QueryInterface $query) {
+  // If the option was set on the query, add additional SQL conditions.
+  if ($custom = $query->getOption('custom_sql_conditions')) {
+    foreach ($custom as $condition) {
+      $db_query->condition($condition['field'], $condition['value'], $condition['operator']);
+    }
+  }
+}
+
+/**
+ * @} End of "addtogroup hooks".
+ */

+ 13 - 0
sites/all/modules/contrib/search/search_api/modules/search_api_db/search_api_db.info.yml

@@ -0,0 +1,13 @@
+type: module
+name: Database Search
+description: Offers an implementation of the Search API that uses database tables for indexing content.
+package: Search
+# core: 8.x
+dependencies:
+  - search_api:search_api
+
+# Information added by Drupal.org packaging script on 2018-02-23
+version: '8.x-1.7'
+core: '8.x'
+project: 'search_api'
+datestamp: 1519387691

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

@@ -0,0 +1,78 @@
+<?php
+
+/**
+ * @file
+ * Install, update and uninstall functions for the Database Search module.
+ */
+
+use Drupal\Core\Database\Database;
+use Drupal\Core\Database\SchemaObjectExistsException;
+use Drupal\Core\Utility\UpdateException;
+
+/**
+ * Reduces the length of sort-value columns for fulltext fields to 30.
+ */
+function search_api_db_update_8101() {
+  // @see https://www.drupal.org/node/2862289
+  $key_value = \Drupal::keyValue('search_api_db.indexes');
+
+  foreach ($key_value->getAll() as $db_info) {
+    // Use the correct database from the server's backend configuration.
+    $database = \Drupal::config('search_api.server.' . $db_info['server'])
+      ->get('backend_config.database');
+    if (!$database) {
+      continue;
+    }
+    list($key, $target) = explode(':', $database, 2);
+    $schema = Database::getConnection($target, $key)->schema();
+
+    $table = $db_info['index_table'];
+    foreach ($db_info['field_tables'] as $field_info) {
+      $column = $field_info['column'];
+      if ($field_info['type'] === 'text'
+          && $schema->fieldExists($table, $column)) {
+        $spec = [
+          'type' => 'varchar',
+          'length' => 30,
+          'description' => "The field's value for this item",
+        ];
+        $schema->changeField($table, $column, $column, $spec);
+      }
+    }
+  }
+
+  return t('Fulltext field database columns updated.');
+}
+
+/**
+ * Adds primary keys to denormalized index tables.
+ */
+function search_api_db_update_8102() {
+  // @see https://www.drupal.org/node/2884451
+  $key_value = \Drupal::keyValue('search_api_db.indexes');
+
+  foreach ($key_value->getAll() as $db_info) {
+    // Use the correct database from the server's backend configuration.
+    $database = \Drupal::config('search_api.server.' . $db_info['server'])
+      ->get('backend_config.database');
+    if (!$database) {
+      continue;
+    }
+    list($key, $target) = explode(':', $database, 2);
+    $schema = Database::getConnection($target, $key)->schema();
+
+    $table = $db_info['index_table'];
+    try {
+      $schema->addPrimaryKey($table, ['item_id']);
+    }
+    catch (SchemaObjectExistsException $e) {
+      // Primary key was already added, maybe by a conscientious site admin.
+      // Nothing to do here in that case.
+    }
+    catch (\Exception $e) {
+      throw new UpdateException("Could not add a primary key to table {{$table}}: " . $e->getMessage(), 0, $e);
+    }
+  }
+
+  return t('Primary keys added to all denormalized index tables.');
+}

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

@@ -0,0 +1,17 @@
+<?php
+
+/**
+ * @file
+ * Contains hook implementations for the Database Search module.
+ */
+
+/**
+ * Implements hook_hook_info().
+ */
+function search_api_db_hook_info() {
+  return [
+    'search_api_db_query_alter' => [
+      'group' => 'search_api',
+    ],
+  ];
+}

+ 22 - 0
sites/all/modules/contrib/search/search_api/modules/search_api_db/search_api_db.services.yml

@@ -0,0 +1,22 @@
+services:
+  logger.channel.search_api_db:
+    parent: logger.channel_base
+    arguments: ['search_api_db']
+
+  search_api_db.database_compatibility:
+    class: Drupal\search_api_db\DatabaseCompatibility\GenericDatabase
+    arguments: ['@database', '@transliteration']
+    tags:
+      - { name: backend_overridable }
+
+  mysql.search_api_db.database_compatibility:
+    class: Drupal\search_api_db\DatabaseCompatibility\MySql
+    arguments: ['@database', '@transliteration']
+
+  pgsql.search_api_db.database_compatibility:
+    class: Drupal\search_api_db\DatabaseCompatibility\CaseSensitiveDatabase
+    arguments: ['@database', '@transliteration']
+
+  sqlite.search_api_db.database_compatibility:
+    class: Drupal\search_api_db\DatabaseCompatibility\CaseSensitiveDatabase
+    arguments: ['@database', '@transliteration']

+ 23 - 0
sites/all/modules/contrib/search/search_api/modules/search_api_db/search_api_db_defaults/README.txt

@@ -0,0 +1,23 @@
+Database Search Defaults
+------------------------
+
+This module provides a default setup for the Search API, for searching node
+content through a view, using a database server for indexing.
+
+By installing this module on your site, the required configuration will be set
+up on the site. Other than that, this module has no functionality. You can
+(and should, for performance reasons) uninstall it again immediately after
+installing, to just get the search set up.
+
+Due to Drupal's configuration model, subsequent updates to the configuration
+deployed with this module won't be applied to existing configuration.
+
+The search view will be set up at this path:
+  /search/content
+
+You can view (and customize) the installed search configuration under these
+paths:
+  Server: /admin/config/search/search-api/server/default_server
+  Index: /admin/config/search/search-api/index/default_index
+  View: /admin/structure/views/view/search_content
+    (if the "Views UI" module is installed)

+ 43 - 0
sites/all/modules/contrib/search/search_api/modules/search_api_db/search_api_db_defaults/config/optional/core.entity_view_display.node.article.search_index.yml

@@ -0,0 +1,43 @@
+langcode: en
+status: true
+dependencies:
+  config:
+    - core.entity_view_mode.node.search_index
+    - field.field.node.article.body
+    - field.field.node.article.comment
+    - field.field.node.article.field_image
+    - field.field.node.article.field_tags
+    - node.type.article
+  module:
+    - comment
+    - text
+    - user
+id: node.article.search_index
+targetEntityType: node
+bundle: article
+mode: search_index
+content:
+  body:
+    type: text_default
+    weight: 0
+    settings: {  }
+    third_party_settings: {  }
+    label: hidden
+  comment:
+    type: comment_default
+    weight: 2
+    settings:
+      pager_id: 0
+    third_party_settings: {  }
+    label: hidden
+  field_tags:
+    type: entity_reference_label
+    weight: 1
+    settings:
+      link: false
+    third_party_settings: {  }
+    label: hidden
+hidden:
+  field_image: true
+  langcode: true
+  links: true

+ 42 - 0
sites/all/modules/contrib/search/search_api/modules/search_api_db/search_api_db_defaults/config/optional/core.entity_view_display.node.article.search_result.yml

@@ -0,0 +1,42 @@
+langcode: en
+status: true
+dependencies:
+  config:
+    - core.entity_view_mode.node.search_result
+    - field.field.node.article.body
+    - field.field.node.article.comment
+    - field.field.node.article.field_image
+    - field.field.node.article.field_tags
+    - node.type.article
+  module:
+    - image
+    - text
+    - user
+id: node.article.search_result
+targetEntityType: node
+bundle: article
+mode: search_result
+content:
+  body:
+    type: text_summary_or_trimmed
+    weight: 1
+    settings:
+      trim_length: 300
+    third_party_settings: {  }
+    label: hidden
+  field_image:
+    type: image
+    weight: 0
+    settings:
+      image_style: thumbnail
+      image_link: content
+    third_party_settings: {  }
+    label: hidden
+  links:
+    weight: 2
+    settings: {  }
+    third_party_settings: {  }
+hidden:
+  comment: true
+  field_tags: true
+  langcode: true

+ 24 - 0
sites/all/modules/contrib/search/search_api/modules/search_api_db/search_api_db_defaults/config/optional/core.entity_view_display.node.page.search_index.yml

@@ -0,0 +1,24 @@
+langcode: en
+status: true
+dependencies:
+  config:
+    - core.entity_view_mode.node.search_index
+    - field.field.node.page.body
+    - node.type.page
+  module:
+    - text
+    - user
+id: node.page.search_index
+targetEntityType: node
+bundle: page
+mode: search_index
+content:
+  body:
+    type: text_default
+    weight: 0
+    settings: {  }
+    third_party_settings: {  }
+    label: hidden
+hidden:
+  langcode: true
+  links: true

+ 28 - 0
sites/all/modules/contrib/search/search_api/modules/search_api_db/search_api_db_defaults/config/optional/core.entity_view_display.node.page.search_result.yml

@@ -0,0 +1,28 @@
+langcode: en
+status: true
+dependencies:
+  config:
+    - core.entity_view_mode.node.search_result
+    - field.field.node.page.body
+    - node.type.page
+  module:
+    - text
+    - user
+id: node.page.search_result
+targetEntityType: node
+bundle: page
+mode: search_result
+content:
+  body:
+    type: text_summary_or_trimmed
+    weight: 100
+    settings:
+      trim_length: 300
+    third_party_settings: {  }
+    label: hidden
+  links:
+    weight: 101
+    settings: {  }
+    third_party_settings: {  }
+hidden:
+  langcode: true

+ 188 - 0
sites/all/modules/contrib/search/search_api/modules/search_api_db/search_api_db_defaults/config/optional/search_api.index.default_index.yml

@@ -0,0 +1,188 @@
+id: default_index
+name: 'Default content index'
+description: 'Default content index created by the Database Search Defaults module'
+read_only: false
+field_settings:
+  title:
+    label: Title
+    type: text
+    datasource_id: 'entity:node'
+    property_path: title
+    boost: 8
+  rendered_item:
+    label: 'Rendered item'
+    type: text
+    property_path: rendered_item
+    configuration:
+      roles:
+        anonymous: anonymous
+      view_mode:
+        'entity:node':
+          article: search_index
+          page: search_index
+  created:
+    label: 'Authored on'
+    type: date
+    datasource_id: 'entity:node'
+    property_path: created
+  changed:
+    label: Changed
+    type: date
+    datasource_id: 'entity:node'
+    property_path: changed
+  status:
+    label: 'Publishing status'
+    type: boolean
+    datasource_id: 'entity:node'
+    property_path: status
+    index_locked: true
+    type_locked: true
+  sticky:
+    label: 'Sticky at top of lists'
+    type: boolean
+    datasource_id: 'entity:node'
+    property_path: sticky
+  field_tags:
+    label: Tags
+    type: integer
+    datasource_id: 'entity:node'
+    property_path: 'field_tags'
+  author:
+    label: 'Author name'
+    type: string
+    datasource_id: 'entity:node'
+    property_path: 'uid:entity:name'
+  uid:
+    label: 'Author ID'
+    type: integer
+    datasource_id: 'entity:node'
+    property_path: uid
+    index_locked: true
+    type_locked: true
+  node_grants:
+    label: 'Node access information'
+    type: string
+    property_path: search_api_node_grants
+    index_locked: true
+    type_locked: true
+    hidden: true
+  type:
+    label: 'Content type'
+    type: string
+    datasource_id: 'entity:node'
+    property_path: type
+processor_settings:
+  add_url:
+    weights:
+      preprocess_index: -30
+  aggregated_field:
+    weights:
+      add_properties: 20
+  content_access:
+    weights:
+      preprocess_index: -6
+      preprocess_query: -4
+  html_filter:
+    weights:
+      preprocess_index: -3
+      preprocess_query: -6
+    fields:
+      - rendered_item
+    title: true
+    alt: true
+    tags:
+      h1: 5
+      h2: 3
+      h3: 2
+      string: 2
+      b: 2
+  ignorecase:
+    weights:
+      preprocess_index: -5
+      preprocess_query: -8
+    fields:
+      - rendered_item
+      - title
+  entity_status:
+    weights:
+      preprocess_index: -10
+  rendered_item:
+    weights:
+      add_properties: 0
+      pre_index_save: -10
+  stopwords:
+    weights:
+      preprocess_query: -10
+      postprocess_query: -10
+    fields:
+      - rendered_item
+      - title
+    stopwords:
+      - a
+      - an
+      - and
+      - are
+      - as
+      - at
+      - be
+      - but
+      - by
+      - for
+      - if
+      - in
+      - into
+      - is
+      - it
+      - 'no'
+      - not
+      - of
+      - 'on'
+      - or
+      - s
+      - such
+      - t
+      - that
+      - the
+      - their
+      - then
+      - there
+      - these
+      - they
+      - this
+      - to
+      - was
+      - will
+      - with
+  tokenizer:
+    weights:
+      preprocess_index: -2
+      preprocess_query: -5
+    fields:
+      - rendered_item
+      - title
+    spaces: ''
+    overlap_cjk: 1
+    minimum_word_size: '3'
+  transliteration:
+    weights:
+      preprocess_index: -4
+      preprocess_query: -7
+    fields:
+      - rendered_item
+      - title
+options:
+  index_directly: true
+  cron_limit: 50
+datasource_settings:
+  'entity:node': {  }
+tracker_settings:
+  'default': {  }
+server: default_server
+status: true
+langcode: en
+dependencies:
+  config:
+    - field.field.node.article.field_tags
+    - search_api.server.default_server
+  module:
+    - node

+ 16 - 0
sites/all/modules/contrib/search/search_api/modules/search_api_db/search_api_db_defaults/config/optional/search_api.server.default_server.yml

@@ -0,0 +1,16 @@
+id: default_server
+name: 'Database Server'
+description: 'Default database server created by the Database Search Defaults module'
+status: true
+backend: search_api_db
+backend_config:
+  database: 'default:default'
+  min_chars: 3
+  partial_matches: false
+  autocomplete:
+    suggest_suffix: true
+    suggest_words: true
+langcode: en
+dependencies:
+  module:
+    - search_api_db

+ 159 - 0
sites/all/modules/contrib/search/search_api/modules/search_api_db/search_api_db_defaults/config/optional/views.view.search_content.yml

@@ -0,0 +1,159 @@
+id: search_content
+langcode: en
+status: true
+dependencies:
+  module:
+    - search_api
+  config:
+    - core.entity_view_mode.node.search_result
+    - core.entity_view_display.node.article.search_result
+    - core.entity_view_display.node.page.search_result
+    - search_api.index.default_index
+label: 'Search content'
+module: views
+description: 'A search page preconfigured to search through the content of your site'
+tag: ''
+base_table: search_api_index_default_index
+base_field: search_api_id
+core: 8.x
+display:
+  default:
+    display_plugin: default
+    id: default
+    display_title: Master
+    position: 0
+    display_options:
+      access:
+        type: none
+        options: {  }
+      cache:
+        type: none
+        options: {  }
+      query:
+        type: search_api_query
+        options: {  }
+      exposed_form:
+        type: input_required
+        options:
+          submit_button: Search
+          reset_button: false
+          reset_button_label: Reset
+          exposed_sorts_label: 'Sort by'
+          expose_sort_order: true
+          sort_asc_label: Asc
+          sort_desc_label: Desc
+          text_input_required: 'Please enter some keywords to search.'
+          text_input_required_format: basic_html
+      pager:
+        type: mini
+        options:
+          items_per_page: 10
+          offset: 0
+          id: 0
+          total_pages: null
+          tags:
+            previous: '‹ previous'
+            next: 'next ›'
+          expose:
+            items_per_page: false
+            items_per_page_label: 'Items per page'
+            items_per_page_options: '5, 10, 25, 50'
+            items_per_page_options_all: false
+            items_per_page_options_all_label: '- All -'
+            offset: false
+            offset_label: Offset
+      style:
+        type: default
+      row:
+        type: search_api
+        options:
+          view_modes:
+            'entity:node':
+              article: search_result
+              page: search_result
+      filters:
+        search_api_fulltext:
+          id: search_api_fulltext
+          table: search_api_index_default_index
+          field: search_api_fulltext
+          relationship: none
+          group_type: group
+          admin_label: ''
+          operator: and
+          value: ''
+          group: 1
+          exposed: true
+          expose:
+            operator_id: search_op
+            label: Search
+            description: ''
+            use_operator: false
+            operator: search_op
+            identifier: keys
+            required: true
+            remember: false
+            multiple: false
+            remember_roles:
+              authenticated: authenticated
+              anonymous: '0'
+              administrator: '0'
+          is_grouped: false
+          group_info:
+            label: ''
+            description: ''
+            identifier: ''
+            optional: true
+            widget: select
+            multiple: false
+            remember: false
+            default_group: All
+            default_group_multiple: {  }
+            group_items: {  }
+          parse_mode: terms
+          min_length: 3
+          fields: {  }
+          plugin_id: search_api_fulltext
+      sorts: {  }
+      title: 'Search Content'
+      header:
+        result:
+          id: result
+          table: views
+          field: result
+          relationship: none
+          group_type: group
+          admin_label: ''
+          empty: false
+          content: 'Displaying results @start - @end of @total'
+          plugin_id: result
+      footer: {  }
+      empty: {  }
+      relationships: {  }
+      arguments: {  }
+      display_extenders: {  }
+    cache_metadata:
+      contexts:
+        - 'languages:language_content'
+        - 'languages:language_interface'
+        - url
+        - url.query_args
+      cacheable: false
+      max-age: 0
+      tags: {  }
+  page_1:
+    display_plugin: page
+    id: page_1
+    display_title: Page
+    position: 1
+    display_options:
+      display_extenders: {  }
+      path: search/content
+    cache_metadata:
+      contexts:
+        - 'languages:language_content'
+        - 'languages:language_interface'
+        - url
+        - url.query_args
+      cacheable: false
+      max-age: 0
+      tags: {  }

+ 20 - 0
sites/all/modules/contrib/search/search_api/modules/search_api_db/search_api_db_defaults/search_api_db_defaults.info.yml

@@ -0,0 +1,20 @@
+type: module
+name: Database Search Defaults
+description: Enable this module for a best-practice default setup of Search API with the Database backend. After installation it is recommended to uninstall this module again for performance reasons. The provided configuration will not be removed.
+package: Search
+# core: 8.x
+dependencies:
+  - drupal:comment
+  - drupal:field
+  - drupal:image
+  - drupal:node
+  - drupal:text
+  - drupal:user
+  - drupal:views
+  - search_api:search_api_db
+
+# Information added by Drupal.org packaging script on 2018-02-23
+version: '8.x-1.7'
+core: '8.x'
+project: 'search_api'
+datestamp: 1519387691

+ 85 - 0
sites/all/modules/contrib/search/search_api/modules/search_api_db/search_api_db_defaults/search_api_db_defaults.install

@@ -0,0 +1,85 @@
+<?php
+
+/**
+ * @file
+ * Install, update and uninstall functions for the DB Search Defaults module.
+ */
+
+use Drupal\node\Entity\NodeType;
+
+/**
+ * Implements hook_requirements().
+ */
+function search_api_db_defaults_requirements($phase) {
+  $requirements = [];
+
+  if ($phase == 'install') {
+    $node_types = NodeType::loadMultiple();
+    $required_types = [
+      'article' => ['body', 'comment', 'field_tags', 'field_image'],
+      'page' => ['body'],
+    ];
+
+    /** @var \Drupal\Core\Entity\EntityFieldManager $entity_field_manager */
+    $entity_field_manager = \Drupal::service('entity_field.manager');
+
+    foreach ($required_types as $required_type_id => $required_fields) {
+      if (!isset($node_types[$required_type_id])) {
+        $requirements['search_api_db_defaults:' . $required_type_id] = [
+          'severity' => REQUIREMENT_ERROR,
+          'description' => t('Content type @content_type not found. Database Search Defaults module could not be installed.', ['@content_type' => $required_type_id]),
+        ];
+      }
+      else {
+        // Check if all the fields are here.
+        $fields = $entity_field_manager->getFieldDefinitions('node', $required_type_id);
+        foreach ($required_fields as $required_field) {
+          if (!isset($fields[$required_field])) {
+            $requirements['search_api_db_defaults:' . $required_type_id . ':' . $required_field] = [
+              'severity' => REQUIREMENT_ERROR,
+              'description' => t('Field @field in content type @node_type not found. Database Search Defaults module could not be installed', ['@node_type' => $required_type_id, '@field' => $required_field]),
+            ];
+          }
+        }
+      }
+    }
+
+    if (\Drupal::moduleHandler()->moduleExists('search_api_db')) {
+      $entities_to_check = [
+        'search_api_index' => 'default_index',
+        'search_api_server' => 'default_server',
+        'view' => 'search_content',
+      ];
+
+      /** @var \Drupal\Core\Entity\EntityTypeManager $entity_type_manager */
+      $entity_type_manager = \Drupal::service('entity_type.manager');
+      foreach ($entities_to_check as $entity_type => $entity_id) {
+        // Find out if the entity is already in place. If so, fail to install the
+        // module.
+        $entity_storage = $entity_type_manager->getStorage($entity_type);
+        $entity_storage->resetCache();
+        $entity = $entity_storage->load($entity_id);
+
+        if ($entity) {
+          $requirements['search_api_db_defaults:defaults_exist'] = [
+            'severity' => REQUIREMENT_ERROR,
+            'description' => t('It looks like the default setup provided by this module already exists on your site. Cannot re-install module.'),
+          ];
+          break;
+        }
+      }
+    }
+  }
+
+  return $requirements;
+}
+
+/**
+ * Implements hook_install().
+ */
+function search_api_db_defaults_install() {
+  // Clear the display plugin cache after installation so the plugin for the new
+  // view (display) gets found.
+  \Drupal::service('plugin.manager.search_api.display')
+    ->clearCachedDefinitions();
+}

+ 181 - 0
sites/all/modules/contrib/search/search_api/modules/search_api_db/search_api_db_defaults/tests/src/Functional/IntegrationTest.php

@@ -0,0 +1,181 @@
+<?php
+
+namespace Drupal\Tests\search_api_db_defaults\Functional;
+
+use Drupal\Component\Render\FormattableMarkup;
+use Drupal\comment\Tests\CommentTestTrait;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\field\Tests\EntityReference\EntityReferenceTestTrait;
+use Drupal\search_api\Entity\Index;
+use Drupal\search_api\Entity\Server;
+use Drupal\Tests\search_api\Functional\SearchApiBrowserTestBase;
+
+/**
+ * Tests the correct installation of the default configs.
+ *
+ * @group search_api
+ */
+class IntegrationTest extends SearchApiBrowserTestBase {
+
+  use StringTranslationTrait, CommentTestTrait, EntityReferenceTestTrait;
+
+  /**
+   * The profile to install as a basis for testing.
+   *
+   * @var string
+   */
+  protected $profile = 'standard';
+
+  /**
+   * A non-admin user used for this test.
+   *
+   * @var \Drupal\Core\Session\AccountInterface
+   */
+  protected $authenticatedUser;
+
+  /**
+   * An admin user used for this test.
+   *
+   * @var \Drupal\Core\Session\AccountInterface
+   */
+  protected $adminUser;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+
+    // Create user with content access permission to see if the view is
+    // accessible, and an admin to do the setup.
+    $this->authenticatedUser = $this->drupalCreateUser();
+    $this->adminUser = $this->drupalCreateUser([], NULL, TRUE);
+  }
+
+  /**
+   * Tests whether the default search was correctly installed.
+   */
+  public function testInstallAndDefaultSetupWorking() {
+    $this->drupalLogin($this->adminUser);
+
+    // Installation invokes a batch and this breaks it.
+    \Drupal::state()->set('search_api_use_tracking_batch', FALSE);
+
+    // Install the search_api_db_defaults module.
+    $edit_enable = [
+      'modules[search_api_db_defaults][enable]' => TRUE,
+    ];
+    $this->drupalPostForm('admin/modules', $edit_enable, 'Install');
+
+    $this->assertSession()->pageTextContains('Some required modules must be enabled');
+
+    $this->drupalPostForm(NULL, [], 'Continue');
+
+    $this->assertSession()->pageTextContains('2 modules have been enabled: Database Search Defaults, Database Search');
+
+    $this->rebuildContainer();
+
+    $this->drupalPostForm('admin/config/search/search-api/server/default_server/edit', [], 'Save');
+    $this->assertSession()->pageTextContains('The server was successfully saved.');
+
+    $server = Server::load('default_server');
+    $this->assertTrue($server, 'Server can be loaded');
+
+    $index = Index::load('default_index');
+    $this->assertTrue($index, 'Index can be loaded');
+
+    $this->drupalLogin($this->authenticatedUser);
+    $this->drupalGet('search/content');
+    $this->assertSession()->statusCodeEquals(200);
+    $this->drupalLogin($this->adminUser);
+
+    $title = 'Test node title';
+    $edit = [
+      'title[0][value]' => $title,
+      'body[0][value]' => 'This is test content for the Search API to index.',
+    ];
+    $this->drupalPostForm('node/add/article', $edit, 'Save');
+
+    $this->drupalLogout();
+    $this->drupalGet('search/content');
+    $this->assertSession()->pageTextContains('Please enter some keywords to search.');
+    $this->assertSession()->pageTextNotContains($title);
+    $this->assertSession()->responseNotContains('Error message');
+    $this->submitForm([], 'Search');
+    $this->assertSession()->pageTextNotContains($title);
+    $this->assertSession()->responseNotContains('Error message');
+    $this->submitForm(['keys' => 'test'], 'Search');
+    $this->assertSession()->pageTextContains($title);
+    $this->assertSession()->responseNotContains('Error message');
+
+    // Uninstall the module.
+    $this->drupalLogin($this->adminUser);
+    $edit_disable = [
+      'uninstall[search_api_db_defaults]' => TRUE,
+    ];
+    $this->drupalPostForm('admin/modules/uninstall', $edit_disable, 'Uninstall');
+    $this->submitForm([], 'Uninstall');
+    $this->rebuildContainer();
+    $this->assertFalse($this->container->get('module_handler')->moduleExists('search_api_db_defaults'), 'Search API DB Defaults module uninstalled.');
+
+    // Check if the server is found in the Search API admin UI.
+    $this->drupalGet('admin/config/search/search-api/server/default_server');
+    $this->assertSession()->statusCodeEquals(200);
+
+    // Check if the index is found in the Search API admin UI.
+    $this->drupalGet('admin/config/search/search-api/index/default_index');
+    $this->assertSession()->statusCodeEquals(200);
+
+    // Check that saving any of the index's config forms works fine.
+    foreach (['edit', 'fields', 'processors'] as $tab) {
+      $submit = $tab == 'fields' ? 'Save changes' : 'Save';
+      $this->drupalGet("admin/config/search/search-api/index/default_index/$tab");
+      $this->submitForm([], $submit);
+      $this->assertSession()->statusCodeEquals(200);
+    }
+
+    $this->drupalLogin($this->authenticatedUser);
+    $this->drupalGet('search/content');
+    $this->assertSession()->statusCodeEquals(200);
+    $this->drupalLogin($this->adminUser);
+
+    // Enable the module again. This should fail because the either the index
+    // or the server or the view was found.
+    $this->drupalPostForm('admin/modules', $edit_enable, 'Install');
+    $this->assertSession()->pageTextContains('It looks like the default setup provided by this module already exists on your site. Cannot re-install module.');
+
+    // Delete all the entities that we would fail on if they exist.
+    $entities_to_remove = [
+      'search_api_index' => 'default_index',
+      'search_api_server' => 'default_server',
+      'view' => 'search_content',
+    ];
+    /** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager */
+    $entity_type_manager = \Drupal::service('entity_type.manager');
+    foreach ($entities_to_remove as $entity_type => $entity_id) {
+      /** @var \Drupal\Core\Entity\EntityStorageInterface $entity_storage */
+      $entity_storage = $entity_type_manager->getStorage($entity_type);
+      $entity_storage->resetCache();
+      $entities = $entity_storage->loadByProperties(['id' => $entity_id]);
+
+      if (!empty($entities[$entity_id])) {
+        $entities[$entity_id]->delete();
+      }
+    }
+
+    // Delete the article content type.
+    $this->drupalGet('node/1/delete');
+    $this->submitForm([], 'Delete');
+    $this->drupalGet('admin/structure/types/manage/article');
+    $this->clickLink('Delete');
+    $this->assertSession()->statusCodeEquals(200);
+    $this->submitForm([], 'Delete');
+
+    // Try to install search_api_db_defaults module and test if it failed
+    // because there was no content type "article".
+    $this->drupalPostForm('admin/modules', $edit_enable, 'Install');
+    $success_text = new FormattableMarkup('Content type @content_type not found. Database Search Defaults module could not be installed.', ['@content_type' => 'article']);
+    $this->assertSession()->pageTextContains($success_text);
+  }
+
+}

+ 17 - 0
sites/all/modules/contrib/search/search_api/modules/search_api_db/src/DatabaseCompatibility/CaseSensitiveDatabase.php

@@ -0,0 +1,17 @@
+<?php
+
+namespace Drupal\search_api_db\DatabaseCompatibility;
+
+/**
+ * Represents a database whose tables are, by default, case-sensitive.
+ */
+class CaseSensitiveDatabase extends GenericDatabase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function preprocessIndexValue($value, $type = 'text') {
+    return $value;
+  }
+
+}

+ 73 - 0
sites/all/modules/contrib/search/search_api/modules/search_api_db/src/DatabaseCompatibility/DatabaseCompatibilityHandlerInterface.php

@@ -0,0 +1,73 @@
+<?php
+
+namespace Drupal\search_api_db\DatabaseCompatibility;
+
+use Drupal\Core\Database\Connection;
+
+/**
+ * Bundles methods for resolving DBMS-specific differences.
+ *
+ * @internal This interface and all implementing classes are just used by the
+ *   search_api_db module for internal purposes. They should not be relied upon
+ *   in other modules.
+ */
+interface DatabaseCompatibilityHandlerInterface {
+
+  /**
+   * Retrieves the database connection this compatibility handler is based upon.
+   *
+   * @return \Drupal\Core\Database\Connection
+   *   The database connection.
+   */
+  public function getDatabase();
+
+  /**
+   * Creates a clone of this service for the given database.
+   *
+   * @param \Drupal\Core\Database\Connection $database
+   *   A database of a type compatible with this class.
+   *
+   * @return static
+   *   A clone of this service class for the given database.
+   */
+  public function getCloneForDatabase(Connection $database);
+
+  /**
+   * Reacts to a new table being created.
+   *
+   * @param string $table
+   *   The name of the table.
+   * @param string $type
+   *   (optional) The type of table. One of "index" (for the denormalized table
+   *   for an entire index), "text" (for an index's fulltext data table) and
+   *   "field" (for field-specific tables).
+   *
+   * @throws \Drupal\search_api\SearchApiException
+   *   Thrown if any error occurs that should abort the current action. Internal
+   *   errors that can be ignored should just be logged.
+   */
+  public function alterNewTable($table, $type = 'text');
+
+  /**
+   * Determines the canonical base form of a value.
+   *
+   * For example, when the table is case-insensitive, the value should always be
+   * lowercased (or always uppercased) to arrive at the canonical base form.
+   *
+   * If tables of the given type use binary comparison in this database, the
+   * value should not be changed.
+   *
+   * @param string $value
+   *   A string to be indexed or searched for.
+   * @param string $type
+   *   (optional) The type of table. One of "index" (for the denormalized table
+   *   for an entire index), "text" (for an index's fulltext data table) and
+   *   "field" (for field-specific tables).
+   *
+   * @return string
+   *   The value in its canonical base form, which won't clash with any other
+   *   canonical base form when inserted into a table of the given type.
+   */
+  public function preprocessIndexValue($value, $type = 'text');
+
+}

+ 72 - 0
sites/all/modules/contrib/search/search_api/modules/search_api_db/src/DatabaseCompatibility/GenericDatabase.php

@@ -0,0 +1,72 @@
+<?php
+
+namespace Drupal\search_api_db\DatabaseCompatibility;
+
+use Drupal\Component\Transliteration\TransliterationInterface;
+use Drupal\Component\Utility\Unicode;
+use Drupal\Core\Database\Connection;
+
+/**
+ * Represents any database for which no specifics are known.
+ */
+class GenericDatabase implements DatabaseCompatibilityHandlerInterface {
+
+  /**
+   * The connection to the database.
+   *
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $database;
+
+  /**
+   * The transliteration service to use.
+   *
+   * @var \Drupal\Component\Transliteration\TransliterationInterface
+   */
+  protected $transliterator;
+
+  /**
+   * Constructs a GenericDatabase object.
+   *
+   * @param \Drupal\Core\Database\Connection $database
+   *   The connection to the database.
+   * @param \Drupal\Component\Transliteration\TransliterationInterface $transliterator
+   *   The transliteration service to use.
+   */
+  public function __construct(Connection $database, TransliterationInterface $transliterator) {
+    $this->database = $database;
+    $this->transliterator = $transliterator;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDatabase() {
+    return $this->database;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCloneForDatabase(Connection $database) {
+    $service = clone $this;
+    $service->database = $database;
+    return $service;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function alterNewTable($table, $type = 'text') {}
+
+  /**
+   * {@inheritdoc}
+   */
+  public function preprocessIndexValue($value, $type = 'text') {
+    if ($type == 'text') {
+      return $value;
+    }
+    return Unicode::strtolower($this->transliterator->transliterate($value));
+  }
+
+}

+ 39 - 0
sites/all/modules/contrib/search/search_api/modules/search_api_db/src/DatabaseCompatibility/MySql.php

@@ -0,0 +1,39 @@
+<?php
+
+namespace Drupal\search_api_db\DatabaseCompatibility;
+
+use Drupal\Core\Database\DatabaseException;
+use Drupal\search_api\SearchApiException;
+
+/**
+ * Represents a MySQL-based database.
+ */
+class MySql extends GenericDatabase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function alterNewTable($table, $type = 'text') {
+    // The Drupal MySQL integration defaults to using a 4-byte-per-character
+    // encoding, which would make it impossible to use our normal 255 characters
+    // long varchar fields in a primary key (since that would exceed the key's
+    // maximum size). Therefore, we have to convert all tables to the "utf8"
+    // character set – but we only want to make fulltext tables case-sensitive.
+    $charset = $type === 'text' ? 'utf8mb4' : 'utf8';
+    $collation = $type === 'text' ? 'utf8mb4_bin' : 'utf8_general_ci';
+    try {
+      $this->database->query("ALTER TABLE {{$table}} CONVERT TO CHARACTER SET '$charset' COLLATE '$collation'");
+    }
+    catch (\PDOException $e) {
+      $class = get_class($e);
+      $message = $e->getMessage();
+      throw new SearchApiException("$class while trying to change collation of $type search data table '$table': $message", 0, $e);
+    }
+    catch (DatabaseException $e) {
+      $class = get_class($e);
+      $message = $e->getMessage();
+      throw new SearchApiException("$class while trying to change collation of $type search data table '$table': $message", 0, $e);
+    }
+  }
+
+}

+ 2738 - 0
sites/all/modules/contrib/search/search_api/modules/search_api_db/src/Plugin/search_api/backend/Database.php

@@ -0,0 +1,2738 @@
+<?php
+
+namespace Drupal\search_api_db\Plugin\search_api\backend;
+
+use Drupal\Component\Utility\Crypt;
+use Drupal\Component\Utility\Html;
+use Drupal\Component\Utility\Unicode;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Database\Database as CoreDatabase;
+use Drupal\Core\Database\DatabaseException;
+use Drupal\Core\Database\Query\Condition;
+use Drupal\Core\Database\Query\SelectInterface;
+use Drupal\Core\Datetime\DateFormatterInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\KeyValueStore\KeyValueStoreInterface;
+use Drupal\Core\Logger\RfcLogLevel;
+use Drupal\Core\Plugin\PluginFormInterface;
+use Drupal\Core\Render\Element;
+use Drupal\search_api\Backend\BackendPluginBase;
+use Drupal\search_api\DataType\DataTypePluginManager;
+use Drupal\search_api\Entity\Index;
+use Drupal\search_api\IndexInterface;
+use Drupal\search_api\Item\FieldInterface;
+use Drupal\search_api\Item\ItemInterface;
+use Drupal\search_api\Plugin\PluginFormTrait;
+use Drupal\search_api\Plugin\search_api\data_type\value\TextToken;
+use Drupal\search_api\Query\ConditionGroupInterface;
+use Drupal\search_api\Query\QueryInterface;
+use Drupal\search_api\SearchApiException;
+use Drupal\search_api\Utility\DataTypeHelper;
+use Drupal\search_api_autocomplete\SearchInterface;
+use Drupal\search_api_autocomplete\Suggestion\SuggestionFactory;
+use Drupal\search_api_db\DatabaseCompatibility\DatabaseCompatibilityHandlerInterface;
+use Drupal\search_api_db\DatabaseCompatibility\GenericDatabase;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Indexes and searches items using the database.
+ *
+ * Database SELECT queries issued by this service class will be marked with tags
+ * according to their context. The following are used:
+ * - search_api_db_search: For all queries that are based on a search query.
+ * - search_api_db_facets_base: For the query which creates a temporary results
+ *   table to be used for facetting. (Is always used in conjunction with
+ *   "search_api_db_search".)
+ * - search_api_db_facet: For queries on the temporary results table for
+ *   determining the items of a specific facet.
+ * - search_api_db_facet_all: For queries to return all indexed values for a
+ *   specific field. Is used when a facet has a "min_count" of 0.
+ * - search_api_db_autocomplete: For queries which create a temporary results
+ *   table to be used for computing autocomplete suggestions. (Is always used in
+ *   conjunction with "search_api_db_search".)
+ *
+ * The following metadata will be present for those SELECT queries:
+ * - search_api_query: The Search API query object. (Always present.)
+ * - search_api_db_fields: Internal storage information for the indexed fields,
+ *   as used by this service class. (Always present.)
+ * - search_api_db_facet: The settings array of the facet currently being
+ *   computed. (Present for "search_api_db_facet" and "search_api_db_facet_all"
+ *   queries.)
+ * - search_api_db_autocomplete: An array containing the parameters of the
+ *   getAutocompleteSuggestions() call, except "query". (Present for
+ *   "search_api_db_autocomplete" queries.)
+ *
+ * @SearchApiBackend(
+ *   id = "search_api_db",
+ *   label = @Translation("Database"),
+ *   description = @Translation("Indexes items in the database. Supports several advanced features, but should not be used for large sites.")
+ * )
+ */
+class Database extends BackendPluginBase implements PluginFormInterface {
+
+  use PluginFormTrait;
+
+  /**
+   * Multiplier for scores to have precision when converted from float to int.
+   */
+  const SCORE_MULTIPLIER = 1000;
+
+  /**
+   * The ID of the key-value store in which the indexes' DB infos are stored.
+   */
+  const INDEXES_KEY_VALUE_STORE_ID = 'search_api_db.indexes';
+
+  /**
+   * The database connection to use for this server.
+   *
+   * @var \Drupal\Core\Database\Connection
+   */
+  protected $database;
+
+  /**
+   * DBMS compatibility handler for this type of database.
+   *
+   * @var \Drupal\search_api_db\DatabaseCompatibility\DatabaseCompatibilityHandlerInterface
+   */
+  protected $dbmsCompatibility;
+
+  /**
+   * The module handler to use.
+   *
+   * @var \Drupal\Core\Extension\ModuleHandlerInterface|null
+   */
+  protected $moduleHandler;
+
+  /**
+   * The config factory to use.
+   *
+   * @var \Drupal\Core\Config\ConfigFactoryInterface|null
+   */
+  protected $configFactory;
+
+  /**
+   * The data type plugin manager to use.
+   *
+   * @var \Drupal\search_api\DataType\DataTypePluginManager
+   */
+  protected $dataTypePluginManager;
+
+  /**
+   * The key-value store to use.
+   *
+   * @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface
+   */
+  protected $keyValueStore;
+
+  /**
+   * The transliteration service to use.
+   *
+   * @var \Drupal\Component\Transliteration\TransliterationInterface
+   */
+  protected $transliterator;
+
+  /**
+   * The date formatter.
+   *
+   * @var \Drupal\Core\Datetime\DateFormatterInterface|null
+   */
+  protected $dateFormatter;
+
+  /**
+   * The data type helper.
+   *
+   * @var \Drupal\search_api\Utility\DataTypeHelper|null
+   */
+  protected $dataTypeHelper;
+
+  /**
+   * The keywords ignored during the current search query.
+   *
+   * @var array
+   */
+  protected $ignored = [];
+
+  /**
+   * All warnings for the current search query.
+   *
+   * @var array
+   */
+  protected $warnings = [];
+
+  /**
+   * Constructs a Database object.
+   *
+   * @param array $configuration
+   *   A configuration array containing settings for this backend.
+   * @param string $plugin_id
+   *   The plugin_id for the plugin instance.
+   * @param mixed $plugin_definition
+   *   The plugin implementation definition.
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+
+    if (isset($configuration['database'])) {
+      list($key, $target) = explode(':', $configuration['database'], 2);
+      // @todo Can we somehow get the connection in a dependency-injected way?
+      $this->database = CoreDatabase::getConnection($target, $key);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    /** @var static $backend */
+    $backend = parent::create($container, $configuration, $plugin_id, $plugin_definition);
+
+    $backend->setModuleHandler($container->get('module_handler'));
+    $backend->setConfigFactory($container->get('config.factory'));
+    $backend->setDataTypePluginManager($container->get('plugin.manager.search_api.data_type'));
+    $backend->setLogger($container->get('logger.channel.search_api_db'));
+    $backend->setKeyValueStore($container->get('keyvalue')->get(self::INDEXES_KEY_VALUE_STORE_ID));
+    $backend->setDateFormatter($container->get('date.formatter'));
+    $backend->setDataTypeHelper($container->get('search_api.data_type_helper'));
+
+    // For a new backend plugin, the database might not be set yet. In that case
+    // we of course also don't need a DBMS compatibility handler.
+    $database = $backend->getDatabase();
+    if ($database) {
+      $dbms_compatibility_handler = $container->get('search_api_db.database_compatibility');
+      // Make sure that we actually provide a handler for the right database,
+      // otherwise create the right service manually. (This is the case if the
+      // user didn't pick the default database.)
+      if ($dbms_compatibility_handler->getDatabase() != $database) {
+        $database_type = $database->databaseType();
+        $service_id = "$database_type.search_api_db.database_compatibility";
+        if ($container->has($service_id)) {
+          /** @var \Drupal\search_api_db\DatabaseCompatibility\DatabaseCompatibilityHandlerInterface $dbms_compatibility_handler */
+          $dbms_compatibility_handler = $container->get($service_id);
+          $dbms_compatibility_handler = $dbms_compatibility_handler->getCloneForDatabase($database);
+        }
+        else {
+          $dbms_compatibility_handler = new GenericDatabase($database, $container->get('transliteration'));
+        }
+      }
+      $backend->setDbmsCompatibilityHandler($dbms_compatibility_handler);
+    }
+
+    return $backend;
+  }
+
+  /**
+   * Retrieves the database connection used by this backend.
+   *
+   * @return \Drupal\Core\Database\Connection
+   *   The database connection.
+   */
+  public function getDatabase() {
+    return $this->database;
+  }
+
+  /**
+   * Returns the module handler to use for this plugin.
+   *
+   * @return \Drupal\Core\Extension\ModuleHandlerInterface
+   *   The module handler.
+   */
+  public function getModuleHandler() {
+    return $this->moduleHandler ?: \Drupal::moduleHandler();
+  }
+
+  /**
+   * Sets the module handler to use for this plugin.
+   *
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler to use for this plugin.
+   *
+   * @return $this
+   */
+  public function setModuleHandler(ModuleHandlerInterface $module_handler) {
+    $this->moduleHandler = $module_handler;
+    return $this;
+  }
+
+  /**
+   * Returns the config factory to use for this plugin.
+   *
+   * @return \Drupal\Core\Config\ConfigFactoryInterface
+   *   The config factory.
+   */
+  public function getConfigFactory() {
+    return $this->configFactory ?: \Drupal::configFactory();
+  }
+
+  /**
+   * Sets the config factory to use for this plugin.
+   *
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The config factory to use for this plugin.
+   *
+   * @return $this
+   */
+  public function setConfigFactory(ConfigFactoryInterface $config_factory) {
+    $this->configFactory = $config_factory;
+    return $this;
+  }
+
+  /**
+   * Retrieves the data type plugin manager.
+   *
+   * @return \Drupal\search_api\DataType\DataTypePluginManager
+   *   The data type plugin manager.
+   */
+  public function getDataTypePluginManager() {
+    return $this->dataTypePluginManager ?: \Drupal::service('plugin.manager.search_api.data_type');
+  }
+
+  /**
+   * Sets the data type plugin manager.
+   *
+   * @param \Drupal\search_api\DataType\DataTypePluginManager $data_type_plugin_manager
+   *   The new data type plugin manager.
+   *
+   * @return $this
+   */
+  public function setDataTypePluginManager(DataTypePluginManager $data_type_plugin_manager) {
+    $this->dataTypePluginManager = $data_type_plugin_manager;
+    return $this;
+  }
+
+  /**
+   * Retrieves the key-value store to use.
+   *
+   * @return \Drupal\Core\KeyValueStore\KeyValueStoreInterface
+   *   The key-value store.
+   */
+  public function getKeyValueStore() {
+    return $this->keyValueStore ?: \Drupal::keyValue(self::INDEXES_KEY_VALUE_STORE_ID);
+  }
+
+  /**
+   * Sets the key-value store to use.
+   *
+   * @param \Drupal\Core\KeyValueStore\KeyValueStoreInterface $key_value_store
+   *   The key-value store.
+   *
+   * @return $this
+   */
+  public function setKeyValueStore(KeyValueStoreInterface $key_value_store) {
+    $this->keyValueStore = $key_value_store;
+    return $this;
+  }
+
+  /**
+   * Retrieves the date formatter.
+   *
+   * @return \Drupal\Core\Datetime\DateFormatterInterface
+   *   The date formatter.
+   */
+  public function getDateFormatter() {
+    return $this->dateFormatter ?: \Drupal::service('date.formatter');
+  }
+
+  /**
+   * Sets the date formatter.
+   *
+   * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
+   *   The new date formatter.
+   *
+   * @return $this
+   */
+  public function setDateFormatter(DateFormatterInterface $date_formatter) {
+    $this->dateFormatter = $date_formatter;
+    return $this;
+  }
+
+  /**
+   * Retrieves the data type helper.
+   *
+   * @return \Drupal\search_api\Utility\DataTypeHelper
+   *   The data type helper.
+   */
+  public function getDataTypeHelper() {
+    return $this->dataTypeHelper ?: \Drupal::service('search_api.data_type_helper');
+  }
+
+  /**
+   * Sets the data type helper.
+   *
+   * @param \Drupal\search_api\Utility\DataTypeHelper $data_type_helper
+   *   The new data type helper.
+   *
+   * @return $this
+   */
+  public function setDataTypeHelper(DataTypeHelper $data_type_helper) {
+    $this->dataTypeHelper = $data_type_helper;
+    return $this;
+  }
+
+  /**
+   * Retrieves the DBMS compatibility handler.
+   *
+   * @return \Drupal\search_api_db\DatabaseCompatibility\DatabaseCompatibilityHandlerInterface
+   *   The DBMS compatibility handler.
+   */
+  public function getDbmsCompatibilityHandler() {
+    return $this->dbmsCompatibility;
+  }
+
+  /**
+   * Sets the DBMS compatibility handler.
+   *
+   * @param \Drupal\search_api_db\DatabaseCompatibility\DatabaseCompatibilityHandlerInterface $handler
+   *   The DBMS compatibility handler.
+   *
+   * @return $this
+   */
+  protected function setDbmsCompatibilityHandler(DatabaseCompatibilityHandlerInterface $handler) {
+    $this->dbmsCompatibility = $handler;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function defaultConfiguration() {
+    return [
+      'database' => NULL,
+      'min_chars' => 1,
+      'partial_matches' => FALSE,
+      'autocomplete' => [
+        'suggest_suffix' => TRUE,
+        'suggest_words' => TRUE,
+      ],
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+    // Discern between creation and editing of a server, since we don't allow
+    // the database to be changed later on.
+    if (!$this->configuration['database']) {
+      $options = [];
+      $key = $target = '';
+      foreach (CoreDatabase::getAllConnectionInfo() as $key => $targets) {
+        foreach ($targets as $target => $info) {
+          $options[$key]["$key:$target"] = "$key » $target";
+        }
+      }
+      if (count($options) > 1 || count(reset($options)) > 1) {
+        $form['database'] = [
+          '#type' => 'select',
+          '#title' => $this->t('Database'),
+          '#description' => $this->t('Select the database key and target to use for storing indexing information in. Cannot be changed after creation.'),
+          '#options' => $options,
+          '#default_value' => 'default:default',
+          '#required' => TRUE,
+        ];
+      }
+      else {
+        $form['database'] = [
+          '#type' => 'value',
+          '#value' => "$key:$target",
+        ];
+      }
+    }
+    else {
+      $form = [
+        'database' => [
+          '#type' => 'value',
+          '#title' => $this->t('Database'),
+          '#value' => $this->configuration['database'],
+        ],
+        'database_text' => [
+          '#type' => 'item',
+          '#title' => $this->t('Database'),
+          '#plain_text' => str_replace(':', ' > ', $this->configuration['database']),
+          '#input' => FALSE,
+        ],
+      ];
+    }
+
+    $form['min_chars'] = [
+      '#type' => 'select',
+      '#title' => $this->t('Minimum word length'),
+      '#description' => $this->t('The minimum number of characters a word must consist of to be indexed'),
+      '#options' => array_combine(
+        [1, 2, 3, 4, 5, 6],
+        [1, 2, 3, 4, 5, 6]
+      ),
+      '#default_value' => $this->configuration['min_chars'],
+    ];
+
+    $form['partial_matches'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Search on parts of a word'),
+      '#description' => $this->t('Find keywords in parts of a word, too. (For example, find results with "database" when searching for "base"). <strong>Caution:</strong> This can make searches much slower on large sites!'),
+      '#default_value' => $this->configuration['partial_matches'],
+    ];
+
+    if ($this->getModuleHandler()->moduleExists('search_api_autocomplete')) {
+      $form['autocomplete'] = [
+        '#type' => 'details',
+        '#title' => $this->t('Autocomplete settings'),
+        '#description' => $this->t('These settings allow you to configure how suggestions are computed when autocompletion is used. If you are seeing many inappropriate suggestions you might want to deactivate the corresponding suggestion type. You can also deactivate one method to speed up the generation of suggestions.'),
+      ];
+      $form['autocomplete']['suggest_suffix'] = [
+        '#type' => 'checkbox',
+        '#title' => $this->t('Suggest word endings'),
+        '#description' => $this->t('Suggest endings for the currently entered word.'),
+        '#default_value' => $this->configuration['autocomplete']['suggest_suffix'],
+      ];
+      $form['autocomplete']['suggest_words'] = [
+        '#type' => 'checkbox',
+        '#title' => $this->t('Suggest additional words'),
+        '#description' => $this->t('Suggest additional words the user might want to search for.'),
+        '#default_value' => $this->configuration['autocomplete']['suggest_words'],
+      ];
+    }
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function viewSettings() {
+    $info = [];
+
+    $info[] = [
+      'label' => $this->t('Database'),
+      'info' => str_replace(':', ' > ', $this->configuration['database']),
+    ];
+    if ($this->configuration['min_chars'] > 1) {
+      $info[] = [
+        'label' => $this->t('Minimum word length'),
+        'info' => $this->configuration['min_chars'],
+      ];
+    }
+    $info[] = [
+      'label' => $this->t('Search on parts of a word'),
+      'info' => !empty($this->configuration['partial_matches']) ? $this->t('enabled') : $this->t('disabled'),
+    ];
+    if (!empty($this->configuration['autocomplete'])) {
+      $this->configuration['autocomplete'] += [
+        'suggest_suffix' => TRUE,
+        'suggest_words' => TRUE,
+      ];
+      $autocomplete_modes = [];
+      if ($this->configuration['autocomplete']['suggest_suffix']) {
+        $autocomplete_modes[] = $this->t('Suggest word endings');
+      }
+      if ($this->configuration['autocomplete']['suggest_words']) {
+        $autocomplete_modes[] = $this->t('Suggest additional words');
+      }
+      $autocomplete_modes = $autocomplete_modes ? implode('; ', $autocomplete_modes) : $this->t('none');
+      $info[] = [
+        'label' => $this->t('Autocomplete suggestions'),
+        'info' => $autocomplete_modes,
+      ];
+    }
+
+    return $info;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getSupportedFeatures() {
+    return [
+      'search_api_autocomplete',
+      'search_api_facets',
+      'search_api_facets_operator_or',
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function postUpdate() {
+    if (empty($this->server->original)) {
+      // When in doubt, opt for the safer route and reindex.
+      return TRUE;
+    }
+    $original_config = $this->server->original->getBackendConfig();
+    $original_config += $this->defaultConfiguration();
+    return $this->configuration['min_chars'] != $original_config['min_chars'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function preDelete() {
+    $schema = $this->database->schema();
+
+    $key_value_store = $this->getKeyValueStore();
+    foreach ($key_value_store->getAll() as $index_id => $db_info) {
+      if ($db_info['server'] != $this->server->id()) {
+        continue;
+      }
+
+      // Delete the regular field tables.
+      foreach ($db_info['field_tables'] as $field) {
+        if ($schema->tableExists($field['table'])) {
+          $schema->dropTable($field['table']);
+        }
+      }
+
+      // Delete the denormalized field tables.
+      if ($schema->tableExists($db_info['index_table'])) {
+        $schema->dropTable($db_info['index_table']);
+      }
+
+      $key_value_store->delete($index_id);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addIndex(IndexInterface $index) {
+    try {
+      // Create the denormalized table now.
+      $index_table = $this->findFreeTable('search_api_db_', $index->id());
+      $this->createFieldTable(NULL, ['table' => $index_table], 'index');
+
+      $db_info = [];
+      $db_info['server'] = $this->server->id();
+      $db_info['field_tables'] = [];
+      $db_info['index_table'] = $index_table;
+      $this->getKeyValueStore()->set($index->id(), $db_info);
+    }
+    // The database operations might throw PDO or other exceptions, so we catch
+    // them all and re-wrap them appropriately.
+    catch (\Exception $e) {
+      throw new SearchApiException($e->getMessage(), $e->getCode(), $e);
+    }
+
+    // If dealing with features or stale data or whatever, we might already have
+    // settings stored for this index. If we have, we should take care to only
+    // change what is needed, so we don't discard indexed data unnecessarily.
+    // The easiest way to do this is by just pretending the index was already
+    // present, but its fields were updated.
+    $this->fieldsUpdated($index);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function updateIndex(IndexInterface $index) {
+    // Process field ID changes so they won't lead to reindexing.
+    $renames = $index->getFieldRenames();
+    if ($renames) {
+      $db_info = $this->getIndexDbInfo($index);
+      // We have to recreate "field_tables" from scratch in case field IDs got
+      // swapped between two (or more) fields.
+      $fields = [];
+      foreach ($db_info['field_tables'] as $field_id => $info) {
+        if (isset($renames[$field_id])) {
+          $field_id = $renames[$field_id];
+        }
+        $fields[$field_id] = $info;
+      }
+      if ($fields != $db_info['field_tables']) {
+        $db_info['field_tables'] = $fields;
+        $this->getKeyValueStore()->set($index->id(), $db_info);
+      }
+    }
+
+    // Check if any fields were updated and trigger a reindex if needed.
+    if ($this->fieldsUpdated($index)) {
+      $index->reindex();
+    }
+  }
+
+  /**
+   * Finds a free table name using a certain prefix and name base.
+   *
+   * Used as a helper method in fieldsUpdated().
+   *
+   * MySQL 5.0 imposes a 64 characters length limit for table names, PostgreSQL
+   * 8.3 only allows 62 bytes. Therefore, always return a name at most 62
+   * bytes long.
+   *
+   * @param string $prefix
+   *   Prefix for the table name. Must only consist of characters valid for SQL
+   *   identifiers.
+   * @param string $name
+   *   Name to base the table name on.
+   *
+   * @return string
+   *   A database table name that isn't in use yet.
+   */
+  protected function findFreeTable($prefix, $name) {
+    // A DB prefix might further reduce the maximum length of the table name.
+    $max_bytes = 62;
+    if ($db_prefix = $this->database->tablePrefix()) {
+      // Use strlen() instead of Unicode::strlen() since we want to measure
+      // bytes, not characters.
+      $max_bytes -= strlen($db_prefix);
+    }
+
+    $base = $table = Unicode::truncateBytes($prefix . Unicode::strtolower(preg_replace('/[^a-z0-9]/i', '_', $name)), $max_bytes);
+    $i = 0;
+    while ($this->database->schema()->tableExists($table)) {
+      $suffix = '_' . ++$i;
+      $table = Unicode::truncateBytes($base, $max_bytes - strlen($suffix)) . $suffix;
+    }
+    return $table;
+  }
+
+  /**
+   * Finds a free column name within a database table.
+   *
+   * Used as a helper method in fieldsUpdated().
+   *
+   * MySQL 5.0 imposes a 64 characters length limit for identifier names,
+   * PostgreSQL 8.3 only allows 62 bytes. Therefore, always return a name at
+   * most 62 bytes long.
+   *
+   * @param string $table
+   *   The name of the table.
+   * @param string $column
+   *   The name to base the column name on.
+   *
+   * @return string
+   *   A column name that isn't in use in the specified table yet.
+   */
+  protected function findFreeColumn($table, $column) {
+    $maxbytes = 62;
+
+    $base = $name = Unicode::truncateBytes(Unicode::strtolower(preg_replace('/[^a-z0-9]/i', '_', $column)), $maxbytes);
+    // If the table does not exist yet, the initial name is not taken.
+    if ($this->database->schema()->tableExists($table)) {
+      $i = 0;
+      while ($this->database->schema()->fieldExists($table, $name)) {
+        $suffix = '_' . ++$i;
+        $name = Unicode::truncateBytes($base, $maxbytes - strlen($suffix)) . $suffix;
+      }
+    }
+    return $name;
+  }
+
+  /**
+   * Creates or modifies a table to add an indexed field.
+   *
+   * Used as a helper method in fieldsUpdated().
+   *
+   * @param \Drupal\search_api\Item\FieldInterface|null $field
+   *   The field to add. Or NULL if only the initial table with an "item_id"
+   *   column should be created.
+   * @param array $db
+   *   Associative array containing the following:
+   *   - table: The table to use for the field.
+   *   - column: (optional) The column to use in that table. Defaults to
+   *     "value". For creating a separate field table, it must be left empty!
+   * @param string $type
+   *   (optional) The type of table being created. Either "index" (for the
+   *   denormalized table for an entire index) or "field" (for field-specific
+   *   tables).
+   *
+   * @todo Write a test to ensure a field named "value" doesn't break this.
+   */
+  protected function createFieldTable(FieldInterface $field = NULL, array $db, $type = 'field') {
+    $new_table = !$this->database->schema()->tableExists($db['table']);
+    if ($new_table) {
+      $table = [
+        'name' => $db['table'],
+        'module' => 'search_api_db',
+        'fields' => [
+          'item_id' => [
+            'type' => 'varchar',
+            'length' => 150,
+            'description' => 'The primary identifier of the item',
+            'not null' => TRUE,
+          ],
+        ],
+      ];
+      // For the denormalized index table, add a primary key right away. For
+      // newly created field tables we first need to add the "value" column.
+      if ($type === 'index') {
+        $table['primary key'] = ['item_id'];
+      }
+      $this->database->schema()->createTable($db['table'], $table);
+      $this->dbmsCompatibility->alterNewTable($db['table'], $type);
+    }
+
+    // Stop here if we want to create a table with just the 'item_id' column.
+    if (!isset($field)) {
+      return;
+    }
+
+    $column = isset($db['column']) ? $db['column'] : 'value';
+    $db_field = $this->sqlType($field->getType());
+    $db_field += [
+      'description' => "The field's value for this item",
+    ];
+    if ($new_table) {
+      $db_field['not null'] = TRUE;
+    }
+    $this->database->schema()->addField($db['table'], $column, $db_field);
+    if ($db_field['type'] === 'varchar') {
+      $index_spec = [[$column, 10]];
+    }
+    else {
+      $index_spec = [$column];
+    }
+    // Create a table specification skeleton to pass to addIndex().
+    $table_spec = [
+      'fields' => [
+        $column => $db_field,
+      ],
+      'indexes' => [
+        $column => $index_spec,
+      ],
+    ];
+
+    // This is a quick fix for a core bug, so we can run the tests with SQLite
+    // until this is fixed.
+    //
+    // In SQLite, indexes and tables can't have the same name, which is
+    // the case for Search API DB. We have following situation:
+    // - a table named search_api_db_default_index_title
+    // - a table named search_api_db_default_index
+    //
+    // The last table has an index on the title column, which results in an
+    // index with the same as the first table, which conflicts in SQLite.
+    //
+    // The core issue addressing this (https://www.drupal.org/node/1008128) was
+    // closed as it fixed the PostgresSQL part. The SQLite fix is added in
+    // https://www.drupal.org/node/2625664
+    // We prevent this by adding an extra underscore (which is also the proposed
+    // solution in the original core issue).
+    //
+    // @todo: Remove when #2625664 lands in Core. See #2625722 for a patch that
+    // implements this.
+    try {
+      $this->database->schema()->addIndex($db['table'], '_' . $column, $index_spec, $table_spec);
+    }
+    catch (\PDOException $e) {
+      $variables['%column'] = $column;
+      $variables['%table'] = $db['table'];
+      $this->logException($e, '%type while trying to add a database index for column %column to table %table: @message in %function (line %line of %file).', $variables, RfcLogLevel::WARNING);
+    }
+    catch (DatabaseException $e) {
+      $variables['%column'] = $column;
+      $variables['%table'] = $db['table'];
+      $this->logException($e, '%type while trying to add a database index for column %column to table %table: @message in %function (line %line of %file).', $variables, RfcLogLevel::WARNING);
+    }
+
+    // Add a covering index for field tables.
+    if ($new_table && $type == 'field') {
+      $this->database->schema()->addPrimaryKey($db['table'], ['item_id', $column]);
+    }
+  }
+
+  /**
+   * Returns the schema definition for a database column for a search data type.
+   *
+   * @param string $type
+   *   An indexed field's search type. One of the default data types.
+   *
+   * @return array
+   *   Column configurations to use for the field's database column.
+   *
+   * @throws \Drupal\search_api\SearchApiException
+   *   Thrown if $type is unknown.
+   */
+  protected function sqlType($type) {
+    switch ($type) {
+      case 'text':
+        return ['type' => 'varchar', 'length' => 30];
+      case 'string':
+      case 'uri':
+        return ['type' => 'varchar', 'length' => 255];
+
+      case 'integer':
+      case 'duration':
+      case 'date':
+        // 'datetime' sucks. Therefore, we just store the timestamp.
+        return ['type' => 'int', 'size' => 'big'];
+
+      case 'decimal':
+        return ['type' => 'float'];
+
+      case 'boolean':
+        return ['type' => 'int', 'size' => 'tiny'];
+
+      default:
+        throw new SearchApiException("Unknown field type '$type'.");
+    }
+  }
+
+  /**
+   * Updates the storage tables when the field configuration changes.
+   *
+   * @param \Drupal\search_api\IndexInterface $index
+   *   The search index whose fields (might) have changed.
+   *
+   * @return bool
+   *   TRUE if the data needs to be reindexed, FALSE otherwise.
+   *
+   * @throws \Drupal\search_api\SearchApiException
+   *   Thrown if any exceptions occur internally – for example, in the database
+   *   layer.
+   */
+  protected function fieldsUpdated(IndexInterface $index) {
+    try {
+      $db_info = $this->getIndexDbInfo($index);
+      $fields = &$db_info['field_tables'];
+      $new_fields = $index->getFields();
+      $new_fields += $this->getSpecialFields($index);
+
+      $reindex = FALSE;
+      $cleared = FALSE;
+      $text_table = NULL;
+      $denormalized_table = $db_info['index_table'];
+
+      foreach ($fields as $field_id => $field) {
+        $was_text_type = $this->getDataTypeHelper()->isTextType($field['type']);
+        if (!isset($text_table) && $was_text_type) {
+          // Stash the shared text table name for the index.
+          $text_table = $field['table'];
+        }
+
+        if (!isset($new_fields[$field_id])) {
+          // The field is no longer in the index, drop the data.
+          $this->removeFieldStorage($field_id, $field, $denormalized_table);
+          unset($fields[$field_id]);
+          continue;
+        }
+        $old_type = $field['type'];
+        $new_type = $new_fields[$field_id]->getType();
+        $fields[$field_id]['type'] = $new_type;
+        $fields[$field_id]['boost'] = $new_fields[$field_id]->getBoost();
+        if ($old_type != $new_type) {
+          $is_text_type = $this->getDataTypeHelper()->isTextType($new_type);
+          if ($was_text_type || $is_text_type) {
+            // A change in fulltext status necessitates completely clearing the
+            // index.
+            $reindex = TRUE;
+            if (!$cleared) {
+              $cleared = TRUE;
+              $this->deleteAllIndexItems($index);
+            }
+            $this->removeFieldStorage($field_id, $field, $denormalized_table);
+            // Keep the table in $new_fields to create the new storage.
+            continue;
+          }
+          elseif ($this->sqlType($old_type) != $this->sqlType($new_type)) {
+            // There is a change in SQL type. We don't have to clear the index,
+            // since types can be converted.
+            $this->database->schema()->changeField($field['table'], 'value', 'value', $this->sqlType($new_type) + ['description' => "The field's value for this item"]);
+            $this->database->schema()->changeField($denormalized_table, $field['column'], $field['column'], $this->sqlType($new_type) + ['description' => "The field's value for this item"]);
+            $reindex = TRUE;
+          }
+          elseif ($old_type == 'date' || $new_type == 'date') {
+            // Even though the SQL type stays the same, we have to reindex since
+            // conversion rules change.
+            $reindex = TRUE;
+          }
+        }
+        elseif ($was_text_type && $field['boost'] != $new_fields[$field_id]->getBoost()) {
+          if (!$reindex) {
+            // If there was a non-zero boost set previously, we can just update
+            // all scores with a single UPDATE query. Otherwise, no way around
+            // re-indexing.
+            if ($field['boost']) {
+              $multiplier = $new_fields[$field_id]->getBoost() / $field['boost'];
+              // Postgres doesn't allow multiplying an integer column with a
+              // float literal, so we have to work around that.
+              $expression = 'score * :mult';
+              $args = [
+                ':mult' => $multiplier,
+              ];
+              if (is_float($multiplier) && $pos = strpos("$multiplier", '.')) {
+                $expression .= ' / :div';
+                $after_point_digits = strlen("$multiplier") - $pos - 1;
+                $args[':div'] = pow(10, min(3, $after_point_digits));
+                $args[':mult'] = (int) round($args[':mult'] * $args[':div']);
+              }
+              $this->database->update($text_table)
+                ->expression('score', $expression, $args)
+                ->condition('field_name', self::getTextFieldName($field_id))
+                ->execute();
+            }
+            else {
+              $reindex = TRUE;
+            }
+          }
+        }
+
+        // Make sure the table and column now exist. (Especially important when
+        // we actually add the index for the first time.)
+        $storage_exists = empty($field['table']) || $this->database->schema()
+          ->fieldExists($field['table'], 'value');
+        $denormalized_storage_exists = $this->database->schema()
+          ->fieldExists($denormalized_table, $field['column']);
+        if (!$was_text_type && !$storage_exists) {
+          $db = [
+            'table' => $field['table'],
+          ];
+          $this->createFieldTable($new_fields[$field_id], $db);
+        }
+        // Ensure that a column is created in the denormalized storage even for
+        // 'text' fields.
+        if (!$denormalized_storage_exists) {
+          $db = [
+            'table' => $denormalized_table,
+            'column' => $field['column'],
+          ];
+          $this->createFieldTable($new_fields[$field_id], $db, 'index');
+        }
+        unset($new_fields[$field_id]);
+      }
+
+      $prefix = 'search_api_db_' . $index->id();
+      // These are new fields that were previously not indexed.
+      foreach ($new_fields as $field_id => $field) {
+        $reindex = TRUE;
+        $fields[$field_id] = [];
+        if ($this->getDataTypeHelper()->isTextType($field->getType())) {
+          if (!isset($text_table)) {
+            // If we have not encountered a text table, assign a name for it.
+            $text_table = $this->findFreeTable($prefix . '_', 'text');
+          }
+          $fields[$field_id]['table'] = $text_table;
+        }
+        else {
+          $fields[$field_id]['table'] = $this->findFreeTable($prefix . '_', $field_id);
+          $this->createFieldTable($field, $fields[$field_id]);
+        }
+
+        // Always add a column in the denormalized table.
+        $fields[$field_id]['column'] = $this->findFreeColumn($denormalized_table, $field_id);
+        $this->createFieldTable($field, ['table' => $denormalized_table, 'column' => $fields[$field_id]['column']], 'index');
+
+        $fields[$field_id]['type'] = $field->getType();
+        $fields[$field_id]['boost'] = $field->getBoost();
+      }
+
+      // If needed, make sure the text table exists.
+      if (isset($text_table) && !$this->database->schema()->tableExists($text_table)) {
+        $table = [
+          'name' => $text_table,
+          'module' => 'search_api_db',
+          'fields' => [
+            'item_id' => [
+              'type' => 'varchar',
+              'length' => 150,
+              'description' => 'The primary identifier of the item',
+              'not null' => TRUE,
+            ],
+            'field_name' => [
+              'description' => "The name of the field in which the token appears, or a base-64 encoded sha-256 hash of the field",
+              'not null' => TRUE,
+              'type' => 'varchar',
+              'length' => 191,
+            ],
+            'word' => [
+              'description' => 'The text of the indexed token',
+              'type' => 'varchar',
+              'length' => 50,
+              'not null' => TRUE,
+              'binary' => TRUE,
+            ],
+            'score' => [
+              'description' => 'The score associated with this token',
+              'type' => 'int',
+              'unsigned' => TRUE,
+              'not null' => TRUE,
+              'default' => 0,
+            ],
+          ],
+          'indexes' => [
+            'word_field' => [['word', 20], 'field_name'],
+          ],
+          // Add a covering index since word is not repeated for each item.
+          'primary key' => ['item_id', 'field_name', 'word'],
+        ];
+        $this->database->schema()->createTable($text_table, $table);
+        $this->dbmsCompatibility->alterNewTable($text_table, 'text');
+      }
+
+      $this->getKeyValueStore()->set($index->id(), $db_info);
+
+      return $reindex;
+    }
+    // The database operations might throw PDO or other exceptions, so we catch
+    // them all and re-wrap them appropriately.
+    catch (\Exception $e) {
+      throw new SearchApiException($e->getMessage(), $e->getCode(), $e);
+    }
+  }
+
+  /**
+   * Drops a field's table and its column from the denormalized table.
+   *
+   * @param string $name
+   *   The field name.
+   * @param array $field
+   *   Backend-internal information about the field.
+   * @param string $index_table
+   *   The table which stores the denormalized data for this field.
+   */
+  protected function removeFieldStorage($name, array $field, $index_table) {
+    if ($this->getDataTypeHelper()->isTextType($field['type'])) {
+      // Remove data from the text table.
+      $this->database->delete($field['table'])
+        ->condition('field_name', self::getTextFieldName($name))
+        ->execute();
+    }
+    elseif ($this->database->schema()->tableExists($field['table'])) {
+      // Remove the field table.
+      $this->database->schema()->dropTable($field['table']);
+    }
+
+    // Remove the field column from the denormalized table.
+    $this->database->schema()->dropField($index_table, $field['column']);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function removeIndex($index) {
+    if (!is_object($index)) {
+      // If the index got deleted, create a dummy to simplify the code. Since we
+      // can't know, we assume the index was read-only, just to be on the safe
+      // side.
+      $index = Index::create([
+        'id' => $index,
+        'read_only' => TRUE,
+      ]);
+    }
+
+    $db_info = $this->getIndexDbInfo($index);
+
+    try {
+      if (!isset($db_info['field_tables']) && !isset($db_info['index_table'])) {
+        return;
+      }
+      // Don't delete the index data of read-only indexes.
+      if (!$index->isReadOnly()) {
+        foreach ($db_info['field_tables'] as $field) {
+          if ($this->database->schema()->tableExists($field['table'])) {
+            $this->database->schema()->dropTable($field['table']);
+          }
+        }
+        if ($this->database->schema()->tableExists($db_info['index_table'])) {
+          $this->database->schema()->dropTable($db_info['index_table']);
+        }
+      }
+
+      $this->getKeyValueStore()->delete($index->id());
+    }
+    // The database operations might throw PDO or other exceptions, so we catch
+    // them all and re-wrap them appropriately.
+    catch (\Exception $e) {
+      throw new SearchApiException($e->getMessage(), $e->getCode(), $e);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function indexItems(IndexInterface $index, array $items) {
+    if (!$this->getIndexDbInfo($index)) {
+      $index_id = $index->id();
+      throw new SearchApiException("No field settings saved for index with ID '$index_id'.");
+    }
+    $indexed = [];
+    foreach ($items as $id => $item) {
+      try {
+        $this->indexItem($index, $item);
+        $indexed[] = $id;
+      }
+      catch (\Exception $e) {
+        // We just log the error, hoping we can index the other items.
+        $this->getLogger()->warning(Html::escape($e->getMessage()));
+      }
+    }
+    return $indexed;
+  }
+
+  /**
+   * Indexes a single item on the specified index.
+   *
+   * Used as a helper method in indexItems().
+   *
+   * @param \Drupal\search_api\IndexInterface $index
+   *   The index for which the item is being indexed.
+   * @param \Drupal\search_api\Item\ItemInterface $item
+   *   The item to index.
+   *
+   * @throws \Exception
+   *   Any encountered database (or other) exceptions are passed on, out of this
+   *   method.
+   */
+  protected function indexItem(IndexInterface $index, ItemInterface $item) {
+    $fields = $this->getFieldInfo($index);
+    $fields_updated = FALSE;
+    $field_errors = [];
+    $db_info = $this->getIndexDbInfo($index);
+    $denormalized_table = $db_info['index_table'];
+    $item_id = $item->getId();
+
+    $transaction = $this->database->startTransaction('search_api_db_indexing');
+
+    try {
+      // Remove the item from the denormalized table.
+      $this->database->delete($denormalized_table)
+        ->condition('item_id', $item_id)
+        ->execute();
+
+      $denormalized_values = [];
+      $text_inserts = [];
+      $item_fields = $item->getFields();
+      $item_fields += $this->getSpecialFields($index, $item);
+      foreach ($item_fields as $field_id => $field) {
+        // Sometimes index changes are not triggering the update hooks
+        // correctly. Therefore, to avoid DB errors, we re-check the tables
+        // here before indexing.
+        if (empty($fields[$field_id]['table']) && !$fields_updated) {
+          unset($db_info['field_tables'][$field_id]);
+          $this->fieldsUpdated($index);
+          $fields_updated = TRUE;
+          $fields = $db_info['field_tables'];
+        }
+        if (empty($fields[$field_id]['table']) && empty($field_errors[$field_id])) {
+          // Log an error, but only once per field. Since a superfluous field is
+          // not too serious, we just index the rest of the item normally.
+          $field_errors[$field_id] = TRUE;
+          $this->getLogger()->warning("Unknown field @field: please check (and re-save) the index's fields settings.", ['@field' => $field_id]);
+          continue;
+        }
+
+        $field_info = $fields[$field_id];
+        $table = $field_info['table'];
+        $column = $field_info['column'];
+
+        $this->database->delete($table)
+          ->condition('item_id', $item_id)
+          ->execute();
+
+        $type = $field->getType();
+        $values = [];
+        foreach ($field->getValues() as $field_value) {
+          $converted_value = $this->convert($field_value, $type, $field->getOriginalType(), $index);
+
+          // Don't add NULL values to the array of values. Also, adding an empty
+          // array is, of course, a waste of time.
+          if (isset($converted_value) && $converted_value !== []) {
+            $values = array_merge($values, is_array($converted_value) ? $converted_value : [$converted_value]);
+          }
+        }
+
+        if (!$values) {
+          // SQLite sometimes has problems letting columns not present in an
+          // INSERT statement default to NULL, so we set NULL values for the
+          // denormalized table explicitly.
+          $denormalized_values[$column] = NULL;
+          continue;
+        }
+
+        // If the field contains more than one value, we remember that the field
+        // can be multi-valued.
+        if (count($values) > 1) {
+          $db_info['field_tables'][$field_id]['multi-valued'] = TRUE;
+        }
+
+        if ($this->getDataTypeHelper()->isTextType($type)) {
+          // Remember the text table the first time we encounter it.
+          if (!isset($text_table)) {
+            $text_table = $table;
+          }
+
+          $unique_tokens = [];
+          $denormalized_value = '';
+          /** @var \Drupal\search_api\Plugin\search_api\data_type\value\TextTokenInterface $token */
+          foreach ($values as $token) {
+            $word = $token->getText();
+            $score = $token->getBoost() * $item->getBoost();
+
+            // In rare cases, tokens with leading or trailing whitespace can
+            // slip through. Since this can lead to errors when such tokens are
+            // part of a primary key (as in this case), we trim such whitespace
+            // here.
+            $word = trim($word);
+
+            // Store the first 30 characters of the string as the denormalized
+            // value.
+            if (Unicode::strlen($denormalized_value) < 30) {
+              $denormalized_value .= $word . ' ';
+            }
+
+            // Skip words that are too short, except for numbers.
+            if (is_numeric($word)) {
+              $word = ltrim($word, '-0');
+            }
+            elseif (Unicode::strlen($word) < $this->configuration['min_chars']) {
+              continue;
+            }
+
+            // Taken from core search to reflect less importance of words later
+            // in the text.
+            // Focus is a decaying value in terms of the amount of unique words
+            // up to this point. From 100 words and more, it decays, to (for
+            // example) 0.5 at 500 words and 0.3 at 1000 words.
+            $score *= min(1, .01 + 3.5 / (2 + count($unique_tokens) * .015));
+
+            // Only insert each canonical base form of a word once.
+            $word_base_form = $this->dbmsCompatibility->preprocessIndexValue($word);
+
+            if (!isset($unique_tokens[$word_base_form])) {
+              $unique_tokens[$word_base_form] = [
+                'value' => $word,
+                'score' => $score,
+              ];
+            }
+            else {
+              $unique_tokens[$word_base_form]['score'] += $score;
+            }
+          }
+          $denormalized_values[$column] = Unicode::substr(trim($denormalized_value), 0, 30);
+          if ($unique_tokens) {
+            $field_name = self::getTextFieldName($field_id);
+            $boost = $field_info['boost'];
+            foreach ($unique_tokens as $token) {
+              $score = round($token['score'] * $boost * self::SCORE_MULTIPLIER);
+              // Take care that the score doesn't exceed the maximum value for
+              // the database column (2^32-1).
+              $score = min((int) $score, 4294967295);
+              $text_inserts[] = [
+                'item_id' => $item_id,
+                'field_name' => $field_name,
+                'word' => $token['value'],
+                'score' => $score,
+              ];
+            }
+          }
+        }
+        else {
+          $denormalized_values[$column] = reset($values);
+
+          // Make sure no duplicate values are inserted (which would lead to a
+          // database exception).
+          // Use the canonical base form of the value for the comparison to
+          // avoid not catching different values that are duplicates under the
+          // database table's collation.
+          $case_insensitive_unique_values = [];
+          foreach ($values as $value) {
+            $value_base_form = $this->dbmsCompatibility->preprocessIndexValue("$value", 'field');
+            // We still insert the value in its original case.
+            $case_insensitive_unique_values[$value_base_form] = $value;
+          }
+          $values = array_values($case_insensitive_unique_values);
+
+          $insert = $this->database->insert($table)
+            ->fields(['item_id', 'value']);
+          foreach ($values as $value) {
+            $insert->values([
+              'item_id' => $item_id,
+              'value' => $value,
+            ]);
+          }
+          $insert->execute();
+        }
+      }
+
+      $this->database->insert($denormalized_table)
+        ->fields(array_merge($denormalized_values, ['item_id' => $item_id]))
+        ->execute();
+      if ($text_inserts && isset($text_table)) {
+        $query = $this->database->insert($text_table)
+          ->fields(['item_id', 'field_name', 'word', 'score']);
+        foreach ($text_inserts as $row) {
+          $query->values($row);
+        }
+        $query->execute();
+      }
+
+      // In case any new fields were detected as multi-valued, we re-save the
+      // index's DB info.
+      $this->getKeyValueStore()->set($index->id(), $db_info);
+    }
+    catch (\Exception $e) {
+      $transaction->rollBack();
+      throw $e;
+    }
+  }
+
+  /**
+   * Trims long field names to fit into the text table's field_name column.
+   *
+   * @param string $name
+   *   The field name.
+   *
+   * @return string
+   *   The field name as stored in the field_name column.
+   */
+  protected static function getTextFieldName($name) {
+    if (strlen($name) > 191) {
+      // Replace long field names with something unique and predictable.
+      return Crypt::hashBase64($name);
+    }
+    else {
+      return $name;
+    }
+  }
+
+  /**
+   * Converts a value between two search types.
+   *
+   * @param mixed $value
+   *   The value to convert.
+   * @param string $type
+   *   The type to convert to. One of the keys from
+   *   search_api_default_field_types().
+   * @param string $original_type
+   *   The value's original type.
+   * @param \Drupal\search_api\IndexInterface $index
+   *   The index for which this conversion takes place.
+   *
+   * @return mixed
+   *   The converted value.
+   *
+   * @throws \Drupal\search_api\SearchApiException
+   *   Thrown if $type is unknown.
+   */
+  protected function convert($value, $type, $original_type, IndexInterface $index) {
+    if (!isset($value)) {
+      // For text fields, we have to return an array even if the value is NULL.
+      return $this->getDataTypeHelper()->isTextType($type) ? [] : NULL;
+    }
+    switch ($type) {
+      case 'text':
+        /** @var \Drupal\search_api\Plugin\search_api\data_type\value\TextValueInterface $value */
+        $tokens = $value->getTokens();
+        if ($tokens === NULL) {
+          $tokens = [];
+          $text = $value->getText();
+          // For dates, splitting the timestamp makes no sense.
+          if ($original_type == 'date') {
+            $text = $this->getDateFormatter()
+              ->format($text, 'custom', 'Y y F M n m j d l D');
+          }
+          foreach (static::splitIntoWords($text) as $word) {
+            if ($word) {
+              if (Unicode::strlen($word) > 50) {
+                $this->getLogger()->warning('An overlong word (more than 50 characters) was encountered while indexing: %word.<br />Since database search servers currently cannot index words of more than 50 characters, the word was truncated for indexing. If this should not be a single word, please make sure the "Tokenizer" processor is enabled and configured correctly for index %index.', ['%word' => $word, '%index' => $index->label()]);
+                $word = Unicode::substr($word, 0, 50);
+              }
+              $tokens[] = new TextToken($word);
+            }
+          }
+        }
+        else {
+          while (TRUE) {
+            foreach ($tokens as $i => $token) {
+              // Check for over-long tokens.
+              $score = $token->getBoost();
+              $word = $token->getText();
+              if (Unicode::strlen($word) > 50) {
+                $new_tokens = [];
+                foreach (static::splitIntoWords($word) as $word) {
+                  if (Unicode::strlen($word) > 50) {
+                    $this->getLogger()->warning('An overlong word (more than 50 characters) was encountered while indexing: %word.<br />Since database search servers currently cannot index words of more than 50 characters, the word was truncated for indexing. If this should not be a single word, please make sure the "Tokenizer" processor is enabled and configured correctly for index %index.', ['%word' => $word, '%index' => $index->label()]);
+                    $word = Unicode::substr($word, 0, 50);
+                  }
+                  $new_tokens[] = new TextToken($word, $score);
+                }
+                array_splice($tokens, $i, 1, $new_tokens);
+                // Restart the loop looking through all the tokens.
+                continue 2;
+              }
+            }
+            break;
+          }
+        }
+        return $tokens;
+
+      case 'string':
+      case 'uri':
+        // For non-dates, PHP can handle this well enough.
+        if ($original_type == 'date') {
+          return date('c', $value);
+        }
+        if (Unicode::strlen($value) > 255) {
+          $value = Unicode::substr($value, 0, 255);
+          $this->getLogger()->warning('An overlong value (more than 255 characters) was encountered while indexing: %value.<br />Database search servers currently cannot index such values correctly – the value was therefore trimmed to the allowed length.', ['%value' => $value]);
+        }
+        return $value;
+
+      case 'integer':
+      case 'duration':
+      case 'decimal':
+        return 0 + $value;
+
+      case 'boolean':
+        return $value ? 1 : 0;
+
+      case 'date':
+        if (is_numeric($value) || !$value) {
+          return 0 + $value;
+        }
+        return strtotime($value);
+
+      default:
+        throw new SearchApiException("Unknown field type '$type'.");
+    }
+  }
+
+  /**
+   * Splits the given string into words.
+   *
+   * Word characters as seen by this method are only alphanumerics.
+   *
+   * @param string $text
+   *   The string to split.
+   *
+   * @return string[]
+   *   All groups of alphanumeric characters contained in the string.
+   */
+  protected static function splitIntoWords($text) {
+    return preg_split('/[^\p{L}\p{N}]+/u', $text, -1, PREG_SPLIT_NO_EMPTY);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function deleteItems(IndexInterface $index, array $item_ids) {
+    try {
+      $db_info = $this->getIndexDbInfo($index);
+
+      if (empty($db_info['field_tables'])) {
+        return;
+      }
+      foreach ($db_info['field_tables'] as $field) {
+        $this->database->delete($field['table'])
+          ->condition('item_id', $item_ids, 'IN')
+          ->execute();
+      }
+      // Delete the denormalized field data.
+      $this->database->delete($db_info['index_table'])
+        ->condition('item_id', $item_ids, 'IN')
+        ->execute();
+    }
+    catch (\Exception $e) {
+      // The database operations might throw PDO or other exceptions, so we
+      // catch them all and re-wrap them appropriately.
+      throw new SearchApiException($e->getMessage(), $e->getCode(), $e);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function deleteAllIndexItems(IndexInterface $index, $datasource_id = NULL) {
+    try {
+      $db_info = $this->getIndexDbInfo($index);
+      $datasource_field = $db_info['field_tables']['search_api_datasource']['column'];
+
+      foreach ($db_info['field_tables'] as $field_id => $field) {
+        if (!$datasource_id) {
+          $this->database->truncate($field['table'])->execute();
+          unset($db_info['field_tables'][$field_id]['multi-valued']);
+        }
+        else {
+          if (!isset($query)) {
+            $query = $this->database->select($db_info['index_table'], 't')
+              ->fields('t', ['item_id'])
+              ->condition($datasource_field, $datasource_id);
+          }
+          $this->database->delete($field['table'])
+            ->condition('item_id', clone $query, 'IN')
+            ->execute();
+        }
+      }
+
+      if (!$datasource_id) {
+        $this->getKeyValueStore()->set($index->id(), $db_info);
+        $this->database->truncate($db_info['index_table'])->execute();
+      }
+      else {
+        $this->database->delete($db_info['index_table'])
+          ->condition($datasource_field, $datasource_id)
+          ->execute();
+      }
+    }
+    catch (\Exception $e) {
+      // The database operations might throw PDO or other exceptions, so we
+      // catch them all and re-wrap them appropriately.
+      throw new SearchApiException($e->getMessage(), $e->getCode(), $e);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function search(QueryInterface $query) {
+    $this->ignored = $this->warnings = [];
+    $index = $query->getIndex();
+    $db_info = $this->getIndexDbInfo($index);
+
+    if (!isset($db_info['field_tables'])) {
+      $index_id = $index->id();
+      throw new SearchApiException("No field settings saved for index with ID '$index_id'.");
+    }
+    $fields = $this->getFieldInfo($index);
+    $fields['search_api_id'] = [
+      'column' => 'item_id',
+    ];
+
+    $db_query = $this->createDbQuery($query, $fields);
+
+    $results = $query->getResults();
+
+    $skip_count = $query->getOption('skip result count');
+    if (!$skip_count) {
+      $count_query = $db_query->countQuery();
+      $results->setResultCount($count_query->execute()->fetchField());
+    }
+
+    if ($skip_count || $results->getResultCount()) {
+      if ($query->getOption('search_api_facets')) {
+        $results->setExtraData('search_api_facets', $this->getFacets($query, clone $db_query));
+      }
+
+      $query_options = $query->getOptions();
+      if (isset($query_options['offset']) || isset($query_options['limit'])) {
+        $offset = isset($query_options['offset']) ? $query_options['offset'] : 0;
+        $limit = isset($query_options['limit']) ? $query_options['limit'] : 1000000;
+        $db_query->range($offset, $limit);
+      }
+
+      $this->setQuerySort($query, $db_query, $fields);
+
+      $result = $db_query->execute();
+
+      foreach ($result as $row) {
+        $item = $this->getFieldsHelper()->createItem($index, $row->item_id);
+        $item->setScore($row->score / self::SCORE_MULTIPLIER);
+        $results->addResultItem($item);
+      }
+      if ($skip_count && !empty($item)) {
+        $results->setResultCount(1);
+      }
+    }
+
+    // Add additional warnings and ignored keys.
+    $metadata = [
+      'warnings' => 'addWarning',
+      'ignored' => 'addIgnoredSearchKey',
+    ];
+    foreach ($metadata as $property => $method) {
+      foreach (array_keys($this->$property) as $value) {
+        $results->$method($value);
+      }
+    }
+  }
+
+  /**
+   * Creates a database query for a search.
+   *
+   * Used as a helper method in search() and getAutocompleteSuggestions().
+   *
+   * @param \Drupal\search_api\Query\QueryInterface $query
+   *   The search query for which to create the database query.
+   * @param array $fields
+   *   The internal field information to use.
+   *
+   * @return \Drupal\Core\Database\Query\SelectInterface
+   *   A database query object which will return the appropriate results (except
+   *   for the range and sorting) for the given search query.
+   *
+   * @throws \Drupal\search_api\SearchApiException
+   *   Thrown if some illegal query setting (unknown field, etc.) was
+   *   encountered.
+   */
+  protected function createDbQuery(QueryInterface $query, array $fields) {
+    $keys = &$query->getKeys();
+    $keys_set = (boolean) $keys;
+    $keys = $this->prepareKeys($keys);
+
+    // Only filter by fulltext keys if there are any real keys present.
+    if ($keys && (!is_array($keys) || count($keys) > 2 || (!isset($keys['#negation']) && count($keys) > 1))) {
+      // Special case: if the outermost $keys array has "#negation" set, we
+      // can't handle it like other negated subkeys. To avoid additional
+      // complexity later, we just wrap $keys so it becomes a subkey.
+      if (!empty($keys['#negation'])) {
+        $keys = [
+          '#conjunction' => 'AND',
+          $keys,
+        ];
+      }
+
+      $fulltext_fields = $this->getQueryFulltextFields($query);
+      if ($fulltext_fields) {
+        $fulltext_field_information = [];
+        foreach ($fulltext_fields as $name) {
+          if (!isset($fields[$name])) {
+            throw new SearchApiException("Unknown field '$name' specified as search target.");
+          }
+          if (!$this->getDataTypeHelper()->isTextType($fields[$name]['type'])) {
+            $types = $this->getDataTypePluginManager()->getInstances();
+            $type = $types[$fields[$name]['type']]->label();
+            throw new SearchApiException("Cannot perform fulltext search on field '$name' of type '$type'.");
+          }
+          $fulltext_field_information[$name] = $fields[$name];
+        }
+
+        $db_query = $this->createKeysQuery($keys, $fulltext_field_information, $fields, $query->getIndex());
+      }
+      else {
+        $this->getLogger()->warning('Search keys are given but no fulltext fields are defined.');
+        $msg = $this->t('Search keys are given but no fulltext fields are defined.');
+        $this->warnings[(string) $msg] = 1;
+      }
+    }
+    elseif ($keys_set) {
+      $msg = $this->t('No valid search keys were present in the query.');
+      $this->warnings[(string) $msg] = 1;
+    }
+
+    if (!isset($db_query)) {
+      $db_info = $this->getIndexDbInfo($query->getIndex());
+      $db_query = $this->database->select($db_info['index_table'], 't');
+      $db_query->addField('t', 'item_id', 'item_id');
+      $db_query->addExpression(':score', 'score', [':score' => self::SCORE_MULTIPLIER]);
+      $db_query->distinct();
+    }
+
+    $condition_group = $query->getConditionGroup();
+    $this->addLanguageConditions($condition_group, $query);
+    if ($condition_group->getConditions()) {
+      $condition = $this->createDbCondition($condition_group, $fields, $db_query, $query->getIndex());
+      if ($condition) {
+        $db_query->condition($condition);
+      }
+    }
+
+    $db_query->addTag('search_api_db_search');
+    $db_query->addMetaData('search_api_query', $query);
+    $db_query->addMetaData('search_api_db_fields', $fields);
+
+    // Allow subclasses and other modules to alter the query (before a count
+    // query is constructed from it).
+    $this->getModuleHandler()->alter('search_api_db_query', $db_query, $query);
+    $this->preQuery($db_query, $query);
+
+    return $db_query;
+  }
+
+  /**
+   * Removes nested expressions and phrase groupings from the search keys.
+   *
+   * Used as a helper method in createDbQuery() and createDbCondition().
+   *
+   * @param array|string|null $keys
+   *   The keys which should be preprocessed.
+   *
+   * @return array|string|null
+   *   The preprocessed keys.
+   */
+  protected function prepareKeys($keys) {
+    if (is_scalar($keys)) {
+      $keys = $this->splitKeys($keys);
+      return is_array($keys) ? $this->eliminateDuplicates($keys) : $keys;
+    }
+    elseif (!$keys) {
+      return NULL;
+    }
+    $keys = $this->eliminateDuplicates($this->splitKeys($keys));
+    $conj = $keys['#conjunction'];
+    $neg = !empty($keys['#negation']);
+    foreach ($keys as $i => &$nested) {
+      if (is_array($nested)) {
+        $nested = $this->prepareKeys($nested);
+        if (is_array($nested) && $neg == !empty($nested['#negation'])) {
+          if ($nested['#conjunction'] == $conj) {
+            unset($nested['#conjunction'], $nested['#negation']);
+            foreach ($nested as $renested) {
+              $keys[] = $renested;
+            }
+            unset($keys[$i]);
+          }
+        }
+      }
+    }
+    $keys = array_filter($keys);
+    if (($count = count($keys)) <= 2) {
+      if ($count < 2 || isset($keys['#negation'])) {
+        $keys = NULL;
+      }
+      else {
+        unset($keys['#conjunction']);
+        $keys = reset($keys);
+      }
+    }
+    return $keys;
+  }
+
+  /**
+   * Splits a keyword expression into separate words.
+   *
+   * Used as a helper method in prepareKeys().
+   *
+   * @param array|string $keys
+   *   The keys to split.
+   *
+   * @return array|string|null
+   *   The keys split into separate words.
+   */
+  protected function splitKeys($keys) {
+    if (is_scalar($keys)) {
+      $processed_keys = $this->dbmsCompatibility->preprocessIndexValue(trim($keys));
+      if (is_numeric($processed_keys)) {
+        return ltrim($processed_keys, '-0');
+      }
+      elseif (Unicode::strlen($processed_keys) < $this->configuration['min_chars']) {
+        $this->ignored[$keys] = 1;
+        return NULL;
+      }
+      $words = static::splitIntoWords($processed_keys);
+      if (count($words) > 1) {
+        $processed_keys = $this->splitKeys($words);
+        if ($processed_keys) {
+          $processed_keys['#conjunction'] = 'AND';
+        }
+        else {
+          $processed_keys = NULL;
+        }
+      }
+      return $processed_keys;
+    }
+    foreach ($keys as $i => $key) {
+      if (Element::child($i)) {
+        $keys[$i] = $this->splitKeys($key);
+      }
+    }
+    return array_filter($keys);
+  }
+
+  /**
+   * Eliminates duplicate keys from a keyword array.
+   *
+   * Used as a helper method in prepareKeys().
+   *
+   * @param array $keys
+   *   The keywords to parse.
+   * @param array $words
+   *   (optional) A cache of all encountered words so far. Used internally for
+   *   recursive invocations.
+   *
+   * @return array
+   *   The processed keywords.
+   */
+  protected function eliminateDuplicates(array $keys, array &$words = []) {
+    foreach ($keys as $i => $word) {
+      if (!Element::child($i)) {
+        continue;
+      }
+      if (is_scalar($word)) {
+        if (isset($words[$word])) {
+          unset($keys[$i]);
+        }
+        else {
+          $words[$word] = TRUE;
+        }
+      }
+      else {
+        $keys[$i] = $this->eliminateDuplicates($word, $words);
+      }
+    }
+    return $keys;
+  }
+
+  /**
+   * Creates a SELECT query for given search keys.
+   *
+   * Used as a helper method in createDbQuery() and createDbCondition().
+   *
+   * @param string|array $keys
+   *   The search keys, formatted like the return value of
+   *   \Drupal\search_api\ParseMode\ParseModeInterface::parseInput(), but
+   *   preprocessed according to internal requirements.
+   * @param array $fields
+   *   The fulltext fields on which to search, with their names as keys mapped
+   *   to internal information about them.
+   * @param array $all_fields
+   *   Internal information about all indexed fields on the index.
+   * @param \Drupal\search_api\IndexInterface $index
+   *   The index we're searching on.
+   *
+   * @return \Drupal\Core\Database\Query\SelectInterface
+   *   A SELECT query returning item_id and score (or only item_id, if
+   *   $keys['#negation'] is set).
+   */
+  protected function createKeysQuery($keys, array $fields, array $all_fields, IndexInterface $index) {
+    if (!is_array($keys)) {
+      $keys = [
+        '#conjunction' => 'AND',
+        $keys,
+      ];
+    }
+
+    $neg = !empty($keys['#negation']);
+    $conj = $keys['#conjunction'];
+    $words = [];
+    $nested = [];
+    $negated = [];
+    $db_query = NULL;
+    $mul_words = FALSE;
+    $neg_nested = $neg && $conj == 'AND';
+    $match_parts = !empty($this->configuration['partial_matches']);
+    $keyword_hits = [];
+
+    foreach ($keys as $i => $key) {
+      if (!Element::child($i)) {
+        continue;
+      }
+      if (is_scalar($key)) {
+        $words[] = $key;
+      }
+      elseif (empty($key['#negation'])) {
+        if ($neg) {
+          // If this query is negated, we also only need item IDs from
+          // subqueries.
+          $key['#negation'] = TRUE;
+        }
+        $nested[] = $key;
+      }
+      else {
+        $negated[] = $key;
+      }
+    }
+    $word_count = count($words);
+    $subs = $word_count + count($nested);
+    $not_nested = ($subs <= 1 && count($fields) == 1) || ($neg && $conj == 'OR' && !$negated);
+
+    if ($words) {
+      // All text fields in the index share a table. Get name from the first.
+      $field = reset($fields);
+      $db_query = $this->database->select($field['table'], 't');
+      $mul_words = ($word_count > 1);
+      if ($neg_nested) {
+        $db_query->fields('t', ['item_id', 'word']);
+      }
+      elseif ($neg) {
+        $db_query->fields('t', ['item_id']);
+      }
+      elseif ($not_nested) {
+        $db_query->fields('t', ['item_id', 'score']);
+      }
+      else {
+        $db_query->fields('t', ['item_id', 'score', 'word']);
+      }
+
+      if (!$match_parts) {
+        $db_query->condition('word', $words, 'IN');
+      }
+      else {
+        $db_or = new Condition('OR');
+        // GROUP BY all existing non-grouped, non-aggregated columns – except
+        // "word", which we remove since it will be useless to us in this case.
+        $columns = &$db_query->getFields();
+        unset($columns['word']);
+        foreach (array_keys($columns) as $column) {
+          $db_query->groupBy($column);
+        }
+
+        foreach ($words as $i => $word) {
+          $db_or->condition('t.word', '%' . $this->database->escapeLike($word) . '%', 'LIKE');
+
+          // Add an expression for each keyword that shows whether the indexed
+          // word matches that particular keyword. That way we don't return a
+          // result multiple times if a single indexed word (partially) matches
+          // multiple keywords. We also remember the column name so we can
+          // afterwards verify that each word matched at least once.
+          $alias = 'w' . $i;
+          $like = '%' . $this->database->escapeLike($word) . '%';
+          $alias = $db_query->addExpression("CASE WHEN t.word LIKE :like_$alias THEN 1 ELSE 0 END", $alias, [":like_$alias" => $like]);
+          $db_query->groupBy($alias);
+          $keyword_hits[] = $alias;
+        }
+        // Also add expressions for any nested queries.
+        for ($i = $word_count; $i < $subs; ++$i) {
+          $alias = 'w' . $i;
+          $alias = $db_query->addExpression('0', $alias);
+          $db_query->groupBy($alias);
+          $keyword_hits[] = $alias;
+        }
+        $db_query->condition($db_or);
+      }
+
+      $db_query->condition('field_name', array_map([__CLASS__, 'getTextFieldName'], array_keys($fields)), 'IN');
+    }
+
+    if ($nested) {
+      $word = '';
+      foreach ($nested as $i => $k) {
+        $query = $this->createKeysQuery($k, $fields, $all_fields, $index);
+        if (!$neg) {
+          if (!$match_parts) {
+            $word .= ' ';
+            $var = ':word' . strlen($word);
+            $query->addExpression($var, 'word', [$var => $word]);
+          }
+          else {
+            $i += $word_count;
+            for ($j = 0; $j < $subs; ++$j) {
+              $alias = isset($keyword_hits[$j]) ? $keyword_hits[$j] : "w$j";
+              $keyword_hits[$j] = $query->addExpression($i == $j ? '1' : '0', $alias);
+            }
+          }
+        }
+        if (!isset($db_query)) {
+          $db_query = $query;
+        }
+        elseif ($not_nested) {
+          $db_query->union($query, 'UNION');
+        }
+        else {
+          $db_query->union($query, 'UNION ALL');
+        }
+      }
+    }
+
+    if (isset($db_query) && !$not_nested) {
+      $db_query = $this->database->select($db_query, 't');
+      $db_query->addField('t', 'item_id', 'item_id');
+      if (!$neg) {
+        $db_query->addExpression('SUM(t.score)', 'score');
+        $db_query->groupBy('t.item_id');
+      }
+      if ($conj == 'AND' && $subs > 1) {
+        $var = ':subs' . ((int) $subs);
+        if (!$db_query->getGroupBy()) {
+          $db_query->groupBy('t.item_id');
+        }
+        if (!$match_parts) {
+          if ($mul_words) {
+            $db_query->having('COUNT(DISTINCT t.word) >= ' . $var, [$var => $subs]);
+          }
+          else {
+            $db_query->having('COUNT(t.word) >= ' . $var, [$var => $subs]);
+          }
+        }
+        else {
+          foreach ($keyword_hits as $alias) {
+            $db_query->having("SUM($alias) >= 1");
+          }
+        }
+      }
+    }
+
+    if ($negated) {
+      if (!isset($db_query) || $conj == 'OR') {
+        if (isset($db_query)) {
+          // We are in a rather bizarre case where the keys are something like
+          // "a OR (NOT b)".
+          $old_query = $db_query;
+        }
+
+        // We use this table because all items should be contained exactly once.
+        $db_info = $this->getIndexDbInfo($index);
+        $db_query = $this->database->select($db_info['index_table'], 't');
+        $db_query->addField('t', 'item_id', 'item_id');
+        if (!$neg) {
+          $db_query->addExpression(':score', 'score', [':score' => self::SCORE_MULTIPLIER]);
+          $db_query->distinct();
+        }
+      }
+
+      if ($conj == 'AND') {
+        foreach ($negated as $k) {
+          $db_query->condition('t.item_id', $this->createKeysQuery($k, $fields, $all_fields, $index), 'NOT IN');
+        }
+      }
+      else {
+        $or = new Condition('OR');
+        foreach ($negated as $k) {
+          $or->condition('t.item_id', $this->createKeysQuery($k, $fields, $all_fields, $index), 'NOT IN');
+        }
+        if (isset($old_query)) {
+          $or->condition('t.item_id', $old_query, 'NOT IN');
+        }
+        $db_query->condition($or);
+      }
+    }
+
+    if ($neg_nested) {
+      $db_query = $this->database->select($db_query, 't')->fields('t', ['item_id']);
+    }
+
+    return $db_query;
+  }
+
+  /**
+   * Adds item language conditions to the condition group, if applicable.
+   *
+   * @param \Drupal\search_api\Query\ConditionGroupInterface $condition_group
+   *   The condition group on which to set conditions.
+   * @param \Drupal\search_api\Query\QueryInterface $query
+   *   The query to inspect for language settings.
+   *
+   * @see \Drupal\search_api\Query\QueryInterface::getLanguages()
+   */
+  protected function addLanguageConditions(ConditionGroupInterface $condition_group, QueryInterface $query) {
+    $languages = $query->getLanguages();
+    if ($languages !== NULL) {
+      $condition_group->addCondition('search_api_language', $languages, 'IN');
+    }
+  }
+
+  /**
+   * Creates a database query condition for a given search filter.
+   *
+   * Used as a helper method in createDbQuery().
+   *
+   * @param \Drupal\search_api\Query\ConditionGroupInterface $conditions
+   *   The conditions for which a condition should be created.
+   * @param array $fields
+   *   Internal information about the index's fields.
+   * @param \Drupal\Core\Database\Query\SelectInterface $db_query
+   *   The database query to which the condition will be added.
+   * @param \Drupal\search_api\IndexInterface $index
+   *   The index we're searching on.
+   *
+   * @return \Drupal\Core\Database\Query\ConditionInterface|null
+   *   The condition to set on the query, or NULL if none is necessary.
+   *
+   * @throws \Drupal\search_api\SearchApiException
+   *   Thrown if an unknown field or operator was used in one of the contained
+   *   conditions.
+   */
+  protected function createDbCondition(ConditionGroupInterface $conditions, array $fields, SelectInterface $db_query, IndexInterface $index) {
+    $conjunction = $conditions->getConjunction();
+    $db_condition = new Condition($conjunction);
+    $db_info = $this->getIndexDbInfo($index);
+
+    // Store the table aliases for the fields in this condition group.
+    $tables = [];
+    $wildcard_count = 0;
+    foreach ($conditions->getConditions() as $condition) {
+      if ($condition instanceof ConditionGroupInterface) {
+        $sub_condition = $this->createDbCondition($condition, $fields, $db_query, $index);
+        if ($sub_condition) {
+          $db_condition->condition($sub_condition);
+        }
+      }
+      else {
+        $field = $condition->getField();
+        $operator = $condition->getOperator();
+        $value = $condition->getValue();
+        $this->validateOperator($operator);
+        $not_equals_operators = ['<>', 'NOT IN', 'NOT BETWEEN'];
+        $not_equals = in_array($operator, $not_equals_operators);
+        $not_between = $operator == 'NOT BETWEEN';
+
+        if (!isset($fields[$field])) {
+          throw new SearchApiException("Unknown field in filter clause: '$field'.");
+        }
+        $field_info = $fields[$field];
+        // For NULL values, we can just use the single-values table, since we
+        // only need to know if there's any value at all for that field.
+        if ($value === NULL || empty($field_info['multi-valued'])) {
+          if (empty($tables[NULL])) {
+            $table = ['table' => $db_info['index_table']];
+            $tables[NULL] = $this->getTableAlias($table, $db_query);
+          }
+          $column = $tables[NULL] . '.' . $field_info['column'];
+          if ($value === NULL) {
+            $method = $not_equals ? 'isNotNull' : 'isNull';
+            $db_condition->$method($column);
+          }
+          elseif ($not_between) {
+            $nested_condition = new Condition('OR');
+            $nested_condition->condition($column, $value[0], '<');
+            $nested_condition->condition($column, $value[1], '>');
+            $nested_condition->isNull($column);
+            $db_condition->condition($nested_condition);
+          }
+          elseif ($not_equals) {
+            // Since SQL never returns TRUE for comparison with NULL values, we
+            // need to include "OR field IS NULL" explicitly for some operators.
+            $nested_condition = new Condition('OR');
+            $nested_condition->condition($column, $value, $operator);
+            $nested_condition->isNull($column);
+            $db_condition->condition($nested_condition);
+          }
+          else {
+            $db_condition->condition($column, $value, $operator);
+          }
+        }
+        elseif ($this->getDataTypeHelper()->isTextType($field_info['type'])) {
+          $keys = $this->prepareKeys($value);
+          if (!isset($keys)) {
+            continue;
+          }
+          $query = $this->createKeysQuery($keys, [$field => $field_info], $fields, $index);
+          // We only want the item IDs, so we use the keys query as a nested
+          // query.
+          $query = $this->database->select($query, 't')
+            ->fields('t', ['item_id']);
+          $db_condition->condition('t.item_id', $query, $not_equals ? 'NOT IN' : 'IN');
+        }
+        elseif ($not_equals) {
+          // The situation is more complicated for negative conditions on
+          // multi-valued fields, since we must make sure that results are
+          // excluded if ANY of the field's values equals the one(s) given in
+          // this condition. Probably the most performant way to do this is to
+          // do a LEFT JOIN with a positive filter on the excluded values in the
+          // ON clause and then make sure we have no value for the field.
+          if ($not_between) {
+            $wildcard1 = ':values_' . ++$wildcard_count;
+            $wildcard2 = ':values_' . ++$wildcard_count;
+            $arguments = array_combine([$wildcard1, $wildcard2], $value);
+            $additional_on = "%alias.value BETWEEN $wildcard1 AND $wildcard2";
+          }
+          else {
+            $wildcard = ':values_' . ++$wildcard_count . '[]';
+            $arguments = [$wildcard => (array) $value];
+            $additional_on = "%alias.value IN ($wildcard)";
+          }
+          $alias = $this->getTableAlias($field_info, $db_query, TRUE, 'leftJoin', $additional_on, $arguments);
+          $db_condition->isNull($alias . '.value');
+        }
+        else {
+          // We need to join the table if it hasn't been joined (for this
+          // condition group) before, or if we have "AND" as the active
+          // conjunction.
+          if ($conjunction == 'AND' || empty($tables[$field])) {
+            $tables[$field] = $this->getTableAlias($field_info, $db_query, TRUE);
+          }
+          $column = $tables[$field] . '.value';
+          $db_condition->condition($column, $value, $operator);
+        }
+      }
+    }
+    return $db_condition->count() ? $db_condition : NULL;
+  }
+
+  /**
+   * Joins a field's table into a database select query.
+   *
+   * @param array $field
+   *   The field information array. The "table" key should contain the table
+   *   name to which a join should be made.
+   * @param \Drupal\Core\Database\Query\SelectInterface $db_query
+   *   The database query used.
+   * @param bool $new_join
+   *   (optional) If TRUE, a join is done even if the table was already joined
+   *   to in the query.
+   * @param string $join
+   *   (optional) The join method to use. Must be a method of the $db_query.
+   *   Normally, "join", "innerJoin", "leftJoin" and "rightJoin" are supported.
+   * @param string|null $additional_on
+   *   (optional) If given, an SQL string with additional conditions for the ON
+   *   clause of the join.
+   * @param array $on_arguments
+   *   (optional) Additional arguments for the ON clause.
+   *
+   * @return string
+   *   The alias for the field's table.
+   */
+  protected function getTableAlias(array $field, SelectInterface $db_query, $new_join = FALSE, $join = 'leftJoin', $additional_on = NULL, array $on_arguments = []) {
+    if (!$new_join) {
+      foreach ($db_query->getTables() as $alias => $info) {
+        $table = $info['table'];
+        if (is_scalar($table) && $table == $field['table']) {
+          return $alias;
+        }
+      }
+    }
+    $condition = 't.item_id = %alias.item_id';
+    if ($additional_on) {
+      $condition .= ' AND ' . $additional_on;
+    }
+    return $db_query->$join($field['table'], 't', $condition, $on_arguments);
+  }
+
+  /**
+   * Preprocesses a search's database query before it is executed.
+   *
+   * This allows subclasses to alter the DB query before a count query (or facet
+   * queries, or other related queries) are constructed from it.
+   *
+   * @param \Drupal\Core\Database\Query\SelectInterface $db_query
+   *   The database query to be executed for the search. Will have "item_id" and
+   *   "score" columns in its result.
+   * @param \Drupal\search_api\Query\QueryInterface $query
+   *   The search query that is being executed.
+   *
+   * @see hook_search_api_db_query_alter()
+   */
+  protected function preQuery(SelectInterface &$db_query, QueryInterface $query) {}
+
+  /**
+   * Adds the approiate "ORDER BY" statements to a search database query.
+   *
+   * @param \Drupal\search_api\Query\QueryInterface $query
+   *   The search query whose sorts should be applied.
+   * @param \Drupal\Core\Database\Query\SelectInterface $db_query
+   *   The database query constructed for the search.
+   * @param string[][] $fields
+   *   An array containing information about the internal server storage of the
+   *   indexed fields.
+   *
+   * @throws \Drupal\search_api\SearchApiException
+   *   Thrown if an illegal sort was specified.
+   */
+  protected function setQuerySort(QueryInterface $query, SelectInterface $db_query, array $fields) {
+    $sort = $query->getSorts();
+    if ($sort) {
+      $db_fields = $db_query->getFields();
+      foreach ($sort as $field_name => $order) {
+        if ($order != QueryInterface::SORT_ASC && $order != QueryInterface::SORT_DESC) {
+          $msg = $this->t('Unknown sort order @order. Assuming "@default".', [
+            '@order' => $order,
+            '@default' => QueryInterface::SORT_ASC,
+          ]);
+          $this->warnings[(string) $msg] = 1;
+          $order = QueryInterface::SORT_ASC;
+        }
+        if ($field_name == 'search_api_relevance') {
+          $db_query->orderBy('score', $order);
+          continue;
+        }
+
+        if (!isset($fields[$field_name])) {
+          throw new SearchApiException("Trying to sort on unknown field '$field_name'.");
+        }
+        $index_table = $this->getIndexDbInfo($query->getIndex())['index_table'];
+        $alias = $this->getTableAlias(['table' => $index_table], $db_query);
+        $db_query->orderBy($alias . '.' . $fields[$field_name]['column'], $order);
+        // PostgreSQL automatically adds a field to the SELECT list when
+        // sorting on it. Therefore, if we have aggregations present we also
+        // have to add the field to the GROUP BY (since Drupal won't do it for
+        // us). However, if no aggregations are present, a GROUP BY would lead
+        // to another error. Therefore, we only add it if there is already a
+        // GROUP BY.
+        if ($db_query->getGroupBy()) {
+          $db_query->groupBy($alias . '.' . $fields[$field_name]['column']);
+        }
+        // For SELECT DISTINCT queries in combination with an ORDER BY clause,
+        // MySQL 5.7 and higher require that the ORDER BY expressions are part
+        // of the field list. Ensure that all fields used for sorting are part
+        // of the select list.
+        if (empty($db_fields[$fields[$field_name]['column']])) {
+          $db_query->addField($alias, $fields[$field_name]['column']);
+        }
+      }
+    }
+    else {
+      $db_query->orderBy('score', 'DESC');
+    }
+  }
+
+  /**
+   * Computes facets for a search query.
+   *
+   * @param \Drupal\search_api\Query\QueryInterface $query
+   *   The search query for which facets should be computed.
+   * @param \Drupal\Core\Database\Query\SelectInterface $db_query
+   *   A database select query which returns all results of that search query.
+   *
+   * @return array
+   *   An array of facets, as specified by the search_api_facets feature.
+   */
+  protected function getFacets(QueryInterface $query, SelectInterface $db_query) {
+    $table = $this->getTemporaryResultsTable($db_query);
+    if (!$table) {
+      return [];
+    }
+
+    $fields = $this->getFieldInfo($query->getIndex());
+    $ret = [];
+    foreach ($query->getOption('search_api_facets') as $key => $facet) {
+      if (empty($fields[$facet['field']])) {
+        $msg = $this->t('Unknown facet field @field.', ['@field' => $facet['field']]);
+        $this->warnings[(string) $msg] = 1;
+        continue;
+      }
+      $field = $fields[$facet['field']];
+
+      if (empty($facet['operator']) || $facet['operator'] != 'or') {
+        // All the AND facets can use the main query.
+        $select = $this->database->select($table, 't');
+      }
+      else {
+        // For OR facets, we need to build a different base query that excludes
+        // the facet filters applied to the facet.
+        $or_query = clone $query;
+        $conditions = &$or_query->getConditionGroup()->getConditions();
+        $tag = 'facet:' . $facet['field'];
+        foreach ($conditions as $i => $condition) {
+          if ($condition instanceof ConditionGroupInterface && $condition->hasTag($tag)) {
+            unset($conditions[$i]);
+          }
+        }
+        $or_db_query = $this->createDbQuery($or_query, $fields);
+        $select = $this->database->select($or_db_query, 't');
+      }
+
+      // If "Include missing facet" is disabled, we use an INNER JOIN and add IS
+      // NOT NULL for shared tables.
+      $is_text_type = $this->getDataTypeHelper()->isTextType($field['type']);
+      $alias = $this->getTableAlias($field, $select, TRUE, $facet['missing'] ? 'leftJoin' : 'innerJoin');
+      $select->addField($alias, $is_text_type ? 'word' : 'value', 'value');
+      if ($is_text_type) {
+        $select->condition($alias . '.field_name', $this->getTextFieldName($facet['field']));
+      }
+      if (!$facet['missing'] && !$is_text_type) {
+        $select->isNotNull($alias . '.value');
+      }
+      $select->addExpression('COUNT(DISTINCT t.item_id)', 'num');
+      $select->groupBy('value');
+      $select->orderBy('num', 'DESC');
+      $select->orderBy('value', 'ASC');
+
+      $limit = $facet['limit'];
+      if ((int) $limit > 0) {
+        $select->range(0, $limit);
+      }
+      if ($facet['min_count'] > 1) {
+        $select->having('COUNT(DISTINCT t.item_id) >= :count', [':count' => $facet['min_count']]);
+      }
+
+      $terms = [];
+      $values = [];
+      $has_missing = FALSE;
+      foreach ($select->execute() as $row) {
+        $terms[] = [
+          'count' => $row->num,
+          'filter' => isset($row->value) ? '"' . $row->value . '"' : '!',
+        ];
+        if (isset($row->value)) {
+          $values[] = $row->value;
+        }
+        else {
+          $has_missing = TRUE;
+        }
+      }
+
+      // If 'Minimum facet count' is set to 0 in the display options for this
+      // facet, we need to retrieve all facets, even ones that aren't matched in
+      // our search result set above. Here we SELECT all DISTINCT facets, and
+      // add in those facets that weren't added above.
+      if ($facet['min_count'] < 1) {
+        $select = $this->database->select($field['table'], 't');
+        $select->addField('t', 'value', 'value');
+        $select->distinct();
+        if ($values) {
+          $select->condition('value', $values, 'NOT IN');
+        }
+        $select->isNotNull('value');
+        foreach ($select->execute() as $row) {
+          $terms[] = [
+            'count' => 0,
+            'filter' => '"' . $row->value . '"',
+          ];
+        }
+        if ($facet['missing'] && !$has_missing) {
+          $terms[] = [
+            'count' => 0,
+            'filter' => '!',
+          ];
+        }
+      }
+
+      $ret[$key] = $terms;
+    }
+    return $ret;
+  }
+
+  /**
+   * Creates a temporary table from a select query.
+   *
+   * Will return the name of a table containing the item IDs of all results, or
+   * FALSE on failure.
+   *
+   * @param \Drupal\Core\Database\Query\SelectInterface $db_query
+   *   The select query whose results should be stored in the temporary table.
+   *
+   * @return string|false
+   *   The name of the temporary table, or FALSE on failure.
+   */
+  protected function getTemporaryResultsTable(SelectInterface $db_query) {
+    // We only need the id field, not the score.
+    $fields = &$db_query->getFields();
+    unset($fields['score']);
+    if (count($fields) != 1 || !isset($fields['item_id'])) {
+      $this->getLogger()->warning('Error while adding facets: only "item_id" field should be used, used are: @fields.', ['@fields' => implode(', ', array_keys($fields))]);
+      return FALSE;
+    }
+    $expressions = &$db_query->getExpressions();
+    $expressions = [];
+
+    // Remove the ORDER BY clause, as it may refer to expressions that are
+    // unset above.
+    $orderBy = &$db_query->getOrderBy();
+    $orderBy = [];
+
+    // If there's a GROUP BY for item_id, we leave that, all others need to be
+    // discarded.
+    $group_by = &$db_query->getGroupBy();
+    $group_by = array_intersect_key($group_by, ['t.item_id' => TRUE]);
+
+    $db_query->distinct();
+    if (!$db_query->preExecute()) {
+      return FALSE;
+    }
+    $args = $db_query->getArguments();
+    try {
+      $result = $this->database->queryTemporary((string) $db_query, $args);
+    }
+    catch (\PDOException $e) {
+      $this->logException($e, '%type while trying to create a temporary table: @message in %function (line %line of %file).');
+      return FALSE;
+    }
+    catch (DatabaseException $e) {
+      $this->logException($e, '%type while trying to create a temporary table: @message in %function (line %line of %file).');
+      return FALSE;
+    }
+    return $result;
+  }
+
+  /**
+   * Retrieves autocompletion suggestions for some user input.
+   *
+   * @param \Drupal\search_api\Query\QueryInterface $query
+   *   A query representing the base search, with all completely entered words
+   *   in the user input so far as the search keys.
+   * @param \Drupal\search_api_autocomplete\SearchInterface $search
+   *   An object containing details about the search the user is on, and
+   *   settings for the autocompletion. See the class documentation for details.
+   *   Especially $search->getOptions() should be checked for settings, like
+   *   whether to try and estimate result counts for returned suggestions.
+   * @param string $incomplete_key
+   *   The start of another fulltext keyword for the search, which should be
+   *   completed. Might be empty, in which case all user input up to now was
+   *   considered completed. Then, additional keywords for the search could be
+   *   suggested.
+   * @param string $user_input
+   *   The complete user input for the fulltext search keywords so far.
+   *
+   * @return \Drupal\search_api_autocomplete\Suggestion\SuggestionInterface[]
+   *   An array of autocomplete suggestions.
+   *
+   * @see \Drupal\search_api_autocomplete\AutocompleteBackendInterface::getAutocompleteSuggestions()
+   */
+  public function getAutocompleteSuggestions(QueryInterface $query, SearchInterface $search, $incomplete_key, $user_input) {
+    $settings = $this->configuration['autocomplete'];
+
+    // If none of the options is checked, the user apparently chose a very
+    // roundabout way of telling us he doesn't want autocompletion.
+    if (!array_filter($settings)) {
+      return [];
+    }
+
+    $index = $query->getIndex();
+    $db_info = $this->getIndexDbInfo($index);
+    if (empty($db_info['field_tables'])) {
+      return [];
+    }
+    $fields = $this->getFieldInfo($index);
+
+    $suggestions = [];
+    $factory = new SuggestionFactory($user_input);
+    $passes = [];
+    $incomplete_like = NULL;
+
+    // Make the input lowercase as the indexed data is (usually) also all
+    // lowercase.
+    $incomplete_key = Unicode::strtolower($incomplete_key);
+    $user_input = Unicode::strtolower($user_input);
+
+    // Decide which methods we want to use.
+    if ($incomplete_key && $settings['suggest_suffix']) {
+      $passes[] = 1;
+      $incomplete_like = $this->database->escapeLike($incomplete_key) . '%';
+    }
+    if ($settings['suggest_words'] && (!$incomplete_key || strlen($incomplete_key) >= $this->configuration['min_chars'])) {
+      $passes[] = 2;
+    }
+
+    if (!$passes) {
+      return [];
+    }
+
+    // We want about half of the suggestions from each enabled method.
+    $limit = $query->getOption('limit', 10);
+    $limit /= count($passes);
+    $limit = ceil($limit);
+
+    // Also collect all keywords already contained in the query so we don't
+    // suggest them.
+    $keys = static::splitIntoWords($user_input);
+    $keys = array_combine($keys, $keys);
+
+    foreach ($passes as $pass) {
+      if ($pass == 2 && $incomplete_key) {
+        $query->keys($user_input);
+      }
+      // To avoid suggesting incomplete words, we have to temporarily disable
+      // the "partial_matches" option. There should be no way we'll save the
+      // server during the createDbQuery() call, so this should be safe.
+      $configuration = $this->configuration;
+      $db_query = NULL;
+      try {
+        $this->configuration['partial_matches'] = FALSE;
+        $db_query = $this->createDbQuery($query, $fields);
+        $this->configuration = $configuration;
+
+        // We need a list of all current results to match the suggestions
+        // against. However, since MySQL doesn't allow using a temporary table
+        // multiple times in one query, we regrettably have to do it this way.
+        $fulltext_fields = $this->getQueryFulltextFields($query);
+        if (count($fulltext_fields) > 1) {
+          $all_results = $db_query->execute()->fetchCol();
+          // Compute the total number of results so we can later sort out
+          // matches that occur too often.
+          $total = count($all_results);
+        }
+        else {
+          $table = $this->getTemporaryResultsTable($db_query);
+          if (!$table) {
+            return [];
+          }
+          $all_results = $this->database->select($table, 't')
+            ->fields('t', ['item_id']);
+          $sql = "SELECT COUNT(item_id) FROM {{$table}}";
+          $total = $this->database->query($sql)->fetchField();
+        }
+      }
+      catch (SearchApiException $e) {
+        // If the exception was in createDbQuery(), we need to reset the
+        // configuration here.
+        $this->configuration = $configuration;
+        $this->logException($e, '%type while trying to create autocomplete suggestions: @message in %function (line %line of %file).');
+        continue;
+      }
+      $max_occurrences = $this->getConfigFactory()
+        ->get('search_api_db.settings')
+        ->get('autocomplete_max_occurrences');
+      $max_occurrences = max(1, floor($total * $max_occurrences));
+
+      if (!$total) {
+        if ($pass == 1) {
+          return [];
+        }
+        continue;
+      }
+
+      /** @var \Drupal\Core\Database\Query\SelectInterface|null $word_query */
+      $word_query = NULL;
+      foreach ($fulltext_fields as $field) {
+        if (!isset($fields[$field]) || !$this->getDataTypeHelper()->isTextType($fields[$field]['type'])) {
+          continue;
+        }
+        $field_query = $this->database->select($fields[$field]['table'], 't');
+        $field_query->fields('t', ['word', 'item_id'])
+          ->condition('field_name', $field)
+          ->condition('item_id', $all_results, 'IN');
+        if ($pass == 1) {
+          $field_query->condition('word', $incomplete_like, 'LIKE')
+            ->condition('word', $keys, 'NOT IN');
+        }
+        if (!isset($word_query)) {
+          $word_query = $field_query;
+        }
+        else {
+          $word_query->union($field_query);
+        }
+      }
+      if (!$word_query) {
+        return [];
+      }
+      $db_query = $this->database->select($word_query, 't');
+      $db_query->addExpression('COUNT(DISTINCT item_id)', 'results');
+      $db_query->fields('t', ['word'])
+        ->groupBy('word')
+        ->having('COUNT(DISTINCT item_id) <= :max', [':max' => $max_occurrences])
+        ->orderBy('results', 'DESC')
+        ->range(0, $limit);
+      $incomp_len = strlen($incomplete_key);
+      foreach ($db_query->execute() as $row) {
+        $suffix = ($pass == 1) ? substr($row->word, $incomp_len) : ' ' . $row->word;
+        $suggestions[] = $factory->createFromSuggestionSuffix($suffix, $row->results);
+      }
+    }
+
+    return $suggestions;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getSpecialFields(IndexInterface $index, ItemInterface $item = NULL) {
+    $fields = parent::getSpecialFields($index, $item);
+    unset($fields['search_api_id']);
+    return $fields;
+  }
+
+  /**
+   * Retrieves the internal field information.
+   *
+   * @param \Drupal\search_api\IndexInterface $index
+   *   The index whose fields should be retrieved.
+   *
+   * @return array[]
+   *   An array of arrays. The outer array is keyed by field name. Each value
+   *   is an associative array with information on the field.
+   */
+  protected function getFieldInfo(IndexInterface $index) {
+    $db_info = $this->getIndexDbInfo($index);
+    return $db_info['field_tables'];
+  }
+
+  /**
+   * Retrieves the database info for the given index.
+   *
+   * @param \Drupal\search_api\IndexInterface $index
+   *   The search index.
+   *
+   * @return array
+   *   The index data from the key-value store.
+   */
+  protected function getIndexDbInfo(IndexInterface $index) {
+    $db_info = $this->getKeyValueStore()->get($index->id(), []);
+    if ($db_info && $db_info['server'] != $this->server->id()) {
+      return [];
+    }
+    return $db_info;
+  }
+
+  /**
+   * Implements the magic __sleep() method.
+   *
+   * Prevents the database connection and logger from being serialized.
+   */
+  public function __sleep() {
+    $properties = array_flip(parent::__sleep());
+    unset($properties['database']);
+    unset($properties['logger']);
+    return array_keys($properties);
+  }
+
+  /**
+   * Implements the magic __wakeup() method.
+   *
+   * Reloads the database connection and logger.
+   */
+  public function __wakeup() {
+    parent::__wakeup();
+
+    if (isset($this->configuration['database'])) {
+      list($key, $target) = explode(':', $this->configuration['database'], 2);
+      $this->database = CoreDatabase::getConnection($target, $key);
+    }
+  }
+
+}

+ 67 - 0
sites/all/modules/contrib/search/search_api/modules/search_api_db/src/Tests/DatabaseTestsTrait.php

@@ -0,0 +1,67 @@
+<?php
+
+namespace Drupal\search_api_db\Tests;
+
+use Drupal\Core\Database\SchemaObjectExistsException;
+
+/**
+ * Provides some common helper methods for database tests.
+ */
+trait DatabaseTestsTrait {
+
+  /**
+   * Asserts that the given table exists and has a primary key.
+   *
+   * @param string $table
+   *   The name of the table.
+   * @param string|null $message
+   *   (optional) The message to print for the assertion, or NULL to use an
+   *   automatically generated one.
+   */
+  protected function assertHasPrimaryKey($table, $message = NULL) {
+    $schema = \Drupal::database()->schema();
+    $this->assertTrue($schema->tableExists($table), "Table $table exists.");
+
+    if (!$message) {
+      $message = "Table $table has a primary key.";
+    }
+    // The database layer doesn't support generic introspection into primary
+    // keys. The simplest way to test whether a primary key exists is therefore
+    // to try to create one and see whether that leads to an exception.
+    try {
+      $schema->addPrimaryKey($table, []);
+      $this->assertTrue(FALSE, $message);
+    }
+    catch (SchemaObjectExistsException $e) {
+      $this->assertTrue(TRUE, $message);
+    }
+    catch (\Exception $e) {
+      // Trying to create a primary key with an empty fields list will probably
+      // still throw an exception, so we catch that as well.
+      $this->assertTrue(FALSE, $message);
+    }
+  }
+
+  /**
+   * Asserts that the given table exists and does not have a primary key.
+   *
+   * @param string $table
+   *   The name of the table.
+   * @param string|null $message
+   *   (optional) The message to print for the assertion, or NULL to use an
+   *   automatically generated one.
+   */
+  protected function assertNotHasPrimaryKey($table, $message = NULL) {
+    $schema = \Drupal::database()->schema();
+    $this->assertTrue($schema->tableExists($table), "Table $table exists.");
+
+    if (!$message) {
+      $message = "Table $table does not have a primary key.";
+    }
+    // The database layer doesn't support generic introspection into primary
+    // keys. The simplest way to make sure a table does not have a primary key
+    // is trying to drop it and checking the return value.
+    $this->assertFalse($schema->dropPrimaryKey($table), $message);
+  }
+
+}

+ 67 - 0
sites/all/modules/contrib/search/search_api/modules/search_api_db/src/Tests/Update/SearchApiDbUpdate8102Test.php

@@ -0,0 +1,67 @@
+<?php
+
+namespace Drupal\search_api_db\Tests\Update;
+
+use Drupal\FunctionalTests\Update\UpdatePathTestBase;
+use Drupal\search_api_db\Tests\DatabaseTestsTrait;
+
+/**
+ * Tests whether search_api_db_update_8102() works correctly.
+ *
+ * @group search_api
+ *
+ * @see https://www.drupal.org/node/2884451
+ */
+class SearchApiDbUpdate8102Test extends UpdatePathTestBase {
+
+  use DatabaseTestsTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    // We need to manually set our entity types as "installed".
+    $entity_type_ids = [
+      'search_api_index',
+      'search_api_server',
+      'search_api_task'
+    ];
+    foreach ($entity_type_ids as $entity_type_id) {
+      $entity_type = \Drupal::getContainer()
+        ->get('entity_type.manager')
+        ->getDefinition($entity_type_id);
+      \Drupal::getContainer()
+        ->get('entity_type.listener')
+        ->onEntityTypeCreate($entity_type);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setDatabaseDumpFiles() {
+    $this->databaseDumpFiles = [
+      DRUPAL_ROOT . '/core/modules/system/tests/fixtures/update/drupal-8.bare.standard.php.gz',
+      __DIR__ . '/../../../tests/fixtures/update/search-api-db-base.php',
+      __DIR__ . '/../../../tests/fixtures/update/search-api-db-update-8102.php',
+    ];
+  }
+
+  /**
+   * Tests whether search_api_db_update_8102() works correctly.
+   *
+   * @see https://www.drupal.org/node/2884451
+   */
+  public function testUpdate8102() {
+    $this->assertNotHasPrimaryKey('search_api_db_index_1');
+    $this->assertHasPrimaryKey('search_api_db_index_2');
+
+    $this->runUpdates();
+
+    $this->assertHasPrimaryKey('search_api_db_index_1');
+    $this->assertHasPrimaryKey('search_api_db_index_2');
+  }
+
+}

+ 59 - 0
sites/all/modules/contrib/search/search_api/modules/search_api_db/tests/fixtures/update/search-api-db-base.php

@@ -0,0 +1,59 @@
+<?php
+
+/**
+ * @file
+ * Contains database additions to drupal-8.bare.standard.php.gz.
+ *
+ * Can be used for setting up a base Search API DB installation.
+ */
+
+use Drupal\Core\Database\Database;
+use Drupal\Core\Serialization\Yaml;
+
+$connection = Database::getConnection();
+
+// Set the schema versions.
+$versions = [
+  'search_api' => 8104,
+  'search_api_db' => 8101,
+];
+foreach ($versions as $name => $version) {
+  $connection->insert('key_value')
+    ->fields([
+      'collection' => 'system.schema',
+      'name' => $name,
+      'value' => serialize($version),
+    ])
+    ->execute();
+}
+
+// Update core.extension.
+$extensions = $connection->select('config')
+  ->fields('config', ['data'])
+  ->condition('collection', '')
+  ->condition('name', 'core.extension')
+  ->execute()
+  ->fetchField();
+$extensions = unserialize($extensions);
+$extensions['module']['search_api'] = 0;
+$extensions['module']['search_api_db'] = 0;
+$connection->update('config')
+  ->fields([
+    'data' => serialize($extensions),
+  ])
+  ->condition('collection', '')
+  ->condition('name', 'core.extension')
+  ->execute();
+
+// Install the default configuration.
+$configs['search_api.settings'] = Yaml::decode(file_get_contents(__DIR__ . '/../../../../../config/install/search_api.settings.yml'));
+$configs['search_api_db.settings'] = Yaml::decode(file_get_contents(__DIR__ . '/../../../config/install/search_api_db.settings.yml'));
+foreach ($configs as $name => $config) {
+  $data = $connection->insert('config')
+    ->fields([
+      'name' => $name,
+      'data' => serialize($config),
+      'collection' => '',
+    ])
+    ->execute();
+}

+ 62 - 0
sites/all/modules/contrib/search/search_api/modules/search_api_db/tests/fixtures/update/search-api-db-update-8102.php

@@ -0,0 +1,62 @@
+<?php
+
+/**
+ * @file
+ * Contains database additions to drupal-8.bare.standard.php.gz.
+ *
+ * Used for testing the search_api_db_update_8102() update.
+ *
+ * @see \Drupal\search_api_db\Tests\Update\SearchApiDbUpdate8102Test
+ */
+
+use Drupal\Core\Database\Database;
+use Drupal\Core\Serialization\Yaml;
+
+$connection = Database::getConnection();
+
+// The update hook needs the server config, though only the "database" config
+// setting is actually relevant.
+$server_configs[] = Yaml::decode(file_get_contents(__DIR__ . '/../../../search_api_db_defaults/config/optional/search_api.server.default_server.yml'));
+
+foreach ($server_configs as $server_config) {
+  $connection->insert('config')
+    ->fields([
+      'collection' => '',
+      'name' => 'search_api.server.' . $server_config['id'],
+      'data' => serialize($server_config),
+    ])
+    ->execute();
+}
+
+foreach ([1, 2] as $i) {
+  $name = "index_$i";
+  $table = "search_api_db_$name";
+  $value = [
+    'server' => 'default_server',
+    'index_table' => $table,
+  ];
+  $connection->insert('key_value')
+    ->fields([
+      'collection' => 'search_api_db.indexes',
+      'name' => $name,
+      'value' => serialize($value),
+    ])
+    ->execute();
+
+  $definition = [
+    'name' => $table,
+    'module' => 'search_api_db',
+    'fields' => [
+      'item_id' => [
+        'type' => 'varchar',
+        'length' => 150,
+        'description' => 'The primary identifier of the item',
+        'not null' => TRUE,
+      ],
+    ],
+  ];
+  if ($i === 2) {
+    $definition['primary key'] = ['item_id'];
+  }
+  $connection->schema()->createTable($table, $definition);
+}

+ 15 - 0
sites/all/modules/contrib/search/search_api/modules/search_api_db/tests/search_api_db_test_autocomplete/config/install/search_api_autocomplete.search.search_api_db_test_autocomplete.yml

@@ -0,0 +1,15 @@
+id: search_api_db_test_autocomplete
+langcode: en
+status: true
+dependencies:
+  config:
+    - search_api.index.database_search_index
+label: Autocomplete test module search
+index_id: database_search_index
+suggester_settings:
+  server:
+    fields: {  }
+search_settings:
+  'search_api_db_test_autocomplete': {  }
+options:
+  show_count: true

+ 15 - 0
sites/all/modules/contrib/search/search_api/modules/search_api_db/tests/search_api_db_test_autocomplete/search_api_db_test_autocomplete.info.yml

@@ -0,0 +1,15 @@
+name: 'Search API Database Autocomplete Test'
+type: module
+description: 'Support module for testing Autocomplete support of the "Database" backend'
+package: Search
+dependencies:
+  - search_api:search_api_test_views
+  - search_api_autocomplete:search_api_autocomplete
+# core: 8.x
+hidden: true
+
+# Information added by Drupal.org packaging script on 2018-02-23
+version: '8.x-1.7'
+core: '8.x'
+project: 'search_api'
+datestamp: 1519387691

+ 70 - 0
sites/all/modules/contrib/search/search_api/modules/search_api_db/tests/search_api_db_test_autocomplete/src/Plugin/search_api_autocomplete/search/TestSearch.php

@@ -0,0 +1,70 @@
+<?php
+
+namespace Drupal\search_api_db_test_autocomplete\Plugin\search_api_autocomplete\search;
+
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Plugin\PluginFormInterface;
+use Drupal\search_api_autocomplete\Search\SearchPluginBase;
+use Drupal\search_api_test\TestPluginTrait;
+
+/**
+ * Defines a test type class.
+ *
+ * @SearchApiAutocompleteSearch(
+ *   id = "search_api_db_test_autocomplete",
+ *   label = @Translation("Autocomplete test module search"),
+ *   description = @Translation("Test autocomplete search"),
+ *   group_label = @Translation("Test search"),
+ *   group_description = @Translation("Searches used for tests"),
+ *   index = "database_search_index",
+ * )
+ */
+class TestSearch extends SearchPluginBase implements PluginFormInterface {
+
+  use TestPluginTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+    $this->logMethodCall(__FUNCTION__, func_get_args());
+    if ($override = $this->getMethodOverride(__FUNCTION__)) {
+      return call_user_func($override, $this, $form, $form_state);
+    }
+    return [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
+    $this->logMethodCall(__FUNCTION__, func_get_args());
+    if ($override = $this->getMethodOverride(__FUNCTION__)) {
+      call_user_func($override, $this, $form, $form_state);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
+    $this->logMethodCall(__FUNCTION__, func_get_args());
+    if ($override = $this->getMethodOverride(__FUNCTION__)) {
+      call_user_func($override, $this, $form, $form_state);
+      return;
+    }
+    $this->setConfiguration($form_state->getValues());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function createQuery($keys, array $data = []) {
+    $this->logMethodCall(__FUNCTION__, func_get_args());
+    if ($override = $this->getMethodOverride(__FUNCTION__)) {
+      return call_user_func($override, $this, $keys, $data);
+    }
+    return $this->search->getIndex()->query()->keys($keys);
+  }
+
+}

+ 40 - 0
sites/all/modules/contrib/search/search_api/modules/search_api_db/tests/src/FunctionalJavascript/IntegrationTest.php

@@ -0,0 +1,40 @@
+<?php
+
+namespace Drupal\Tests\search_api_db\FunctionalJavascript;
+
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Drupal\FunctionalJavascriptTests\JavascriptTestBase;
+
+/**
+ * Tests that using the DB backend via the UI works as expected.
+ *
+ * @group search_api
+ */
+class IntegrationTest extends JavascriptTestBase {
+
+  use StringTranslationTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = [
+    'search_api',
+    'search_api_db',
+  ];
+
+  /**
+   * Tests that adding a server works.
+   */
+  public function testAddingServer() {
+    $admin_user = $this->drupalCreateUser(['administer search_api', 'access administration pages']);
+    $this->drupalLogin($admin_user);
+
+    $this->drupalGet('admin/config/search/search-api/add-server');
+    $this->assertSession()->statusCodeEquals(200);
+
+    $edit = ['name' => ' ~`Test Server', 'id' => '_test'];
+    $this->submitForm($edit, 'Save');
+    $this->assertSession()->addressEquals('admin/config/search/search-api/server/_test');
+  }
+
+}

+ 144 - 0
sites/all/modules/contrib/search/search_api/modules/search_api_db/tests/src/Kernel/AutocompleteTest.php

@@ -0,0 +1,144 @@
+<?php
+
+namespace Drupal\Tests\search_api_db\Kernel;
+
+use Drupal\KernelTests\KernelTestBase;
+use Drupal\search_api\Utility\Utility;
+use Drupal\search_api_autocomplete\Entity\Search;
+use Drupal\search_api_db\Plugin\search_api\backend\Database;
+use Drupal\Tests\search_api\Functional\ExampleContentTrait;
+
+/**
+ * Tests autocomplete functionality of the Database backend.
+ *
+ * @requires module search_api_autocomplete
+ * @coversDefaultClass \Drupal\search_api_db\Plugin\search_api\backend\Database
+ * @group search_api
+ */
+class AutocompleteTest extends KernelTestBase {
+
+  use ExampleContentTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = [
+    'entity_test',
+    'field',
+    'system',
+    'text',
+    'user',
+    'search_api',
+    'search_api_autocomplete',
+    'search_api_db',
+    'search_api_db_test_autocomplete',
+    'search_api_test_db',
+    'search_api_test_example_content',
+  ];
+
+  /**
+   * A search server ID.
+   *
+   * @var string
+   */
+  protected $serverId = 'database_search_server';
+
+  /**
+   * A search index ID.
+   *
+   * @var string
+   */
+  protected $indexId = 'database_search_index';
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+
+    $this->installSchema('search_api', ['search_api_item']);
+    $this->installSchema('system', ['router']);
+    $this->installSchema('user', ['users_data']);
+    $this->installEntitySchema('entity_test_mulrev_changed');
+    $this->installEntitySchema('search_api_task');
+    $this->installConfig('search_api');
+
+    // Do not use a batch for tracking the initial items after creating an
+    // index when running the tests via the GUI. Otherwise, it seems Drupal's
+    // Batch API gets confused and the test fails.
+    if (!Utility::isRunningInCli()) {
+      \Drupal::state()->set('search_api_use_tracking_batch', FALSE);
+    }
+
+    $this->installConfig([
+      'search_api_db',
+      'search_api_test_example_content',
+      'search_api_test_db',
+      'search_api_db_test_autocomplete',
+    ]);
+
+    $this->setUpExampleStructure();
+    $this->insertExampleContent();
+
+    $this->indexItems($this->indexId);
+  }
+
+  /**
+   * Tests whether autocomplete suggestions are correctly created.
+   *
+   * @covers ::getAutocompleteSuggestions
+   */
+  public function testAutocompletion() {
+    /** @var \Drupal\search_api_autocomplete\SearchInterface $autocomplete */
+    $autocomplete = Search::load('search_api_db_test_autocomplete');
+    $index = $autocomplete->getIndex();
+    /** @var \Drupal\search_api_db\Plugin\search_api\backend\Database $backend */
+    $backend = $index->getServerInstance()->getBackend();
+
+    $this->assertInstanceOf(Database::class, $backend);
+
+    $query = $index->query()
+      ->range(0, 10);
+    $suggestions = $backend->getAutocompleteSuggestions($query, $autocomplete, 'fo', 'fo');
+    $expected = [
+      'foo' => 4,
+      'foobar' => 1,
+      'foobaz' => 1,
+      'foobuz' => 1,
+    ];
+    $this->assertSuggestionsEqual($expected, $suggestions);
+
+    $query = $index->query()
+      ->keys('foo')
+      ->range(0, 10);
+    $suggestions = $backend->getAutocompleteSuggestions($query, $autocomplete, 'fo', 'foo fo');
+    $expected = [
+      'foo foobaz' => 1,
+      'foo foobuz' => 1,
+    ];
+    $this->assertSuggestionsEqual($expected, $suggestions);
+  }
+
+  /**
+   * Asserts that the returned suggestions are as expected.
+   *
+   * @param int[] $expected
+   *   Associative array mapping suggestion strings to their counts.
+   * @param \Drupal\search_api_autocomplete\Suggestion\SuggestionInterface[] $suggestions
+   *   The suggestions returned by the backend.
+   */
+  protected function assertSuggestionsEqual(array $expected, array $suggestions) {
+    $terms = [];
+    foreach ($suggestions as $suggestion) {
+      $keys = $suggestion->getSuggestedKeys();
+      if ($keys === NULL) {
+        $keys = $suggestion->getSuggestionPrefix();
+        $keys .= $suggestion->getUserInput();
+        $keys .= $suggestion->getSuggestionSuffix();
+      }
+      $terms[$keys] = $suggestion->getResultsCount();
+    }
+    $this->assertEquals($expected, $terms);
+  }
+
+}

+ 753 - 0
sites/all/modules/contrib/search/search_api/modules/search_api_db/tests/src/Kernel/BackendTest.php

@@ -0,0 +1,753 @@
+<?php
+
+namespace Drupal\Tests\search_api_db\Kernel;
+
+use Drupal\Component\Render\FormattableMarkup;
+use Drupal\Core\Database\Database as CoreDatabase;
+use Drupal\search_api\Entity\Server;
+use Drupal\search_api\IndexInterface;
+use Drupal\search_api\Item\ItemInterface;
+use Drupal\search_api\Plugin\search_api\data_type\value\TextToken;
+use Drupal\search_api\Plugin\search_api\data_type\value\TextValue;
+use Drupal\search_api\Query\QueryInterface;
+use Drupal\search_api\SearchApiException;
+use Drupal\search_api\Utility\Utility;
+use Drupal\search_api_db\DatabaseCompatibility\GenericDatabase;
+use Drupal\search_api_db\Plugin\search_api\backend\Database;
+use Drupal\search_api_db\Tests\DatabaseTestsTrait;
+use Drupal\Tests\search_api\Kernel\BackendTestBase;
+
+/**
+ * Tests index and search capabilities using the Database search backend.
+ *
+ * @see \Drupal\search_api_db\Plugin\search_api\backend\Database
+ *
+ * @group search_api
+ */
+class BackendTest extends BackendTestBase {
+
+  use DatabaseTestsTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = [
+    'search_api_db',
+    'search_api_test_db',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $serverId = 'database_search_server';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $indexId = 'database_search_index';
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp() {
+    parent::setUp();
+
+    // Create a dummy table that will cause a naming conflict with the backend's
+    // default table names, thus testing whether it correctly reacts to such
+    // conflicts.
+    \Drupal::database()->schema()->createTable('search_api_db_database_search_index', [
+      'fields' => [
+        'id' => [
+          'type' => 'int',
+        ],
+      ],
+    ]);
+
+    $this->installConfig(['search_api_test_db']);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function checkBackendSpecificFeatures() {
+    $this->checkMultiValuedInfo();
+    $this->editServerPartial();
+    $this->searchSuccessPartial();
+    $this->editServerMinChars();
+    $this->searchSuccessMinChars();
+    $this->checkUnknownOperator();
+    $this->checkDbQueryAlter();
+    $this->checkFieldIdChanges();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function backendSpecificRegressionTests() {
+    $this->regressionTest2557291();
+    $this->regressionTest2511860();
+    $this->regressionTest2846932();
+    $this->regressionTest2926733();
+    $this->regressionTest2938646();
+  }
+
+  /**
+   * Tests that all tables and all columns have been created.
+   */
+  protected function checkServerBackend() {
+    $db_info = $this->getIndexDbInfo();
+    $normalized_storage_table = $db_info['index_table'];
+    $field_infos = $db_info['field_tables'];
+
+    $expected_fields = [
+      'body',
+      'category',
+      'created',
+      'id',
+      'keywords',
+      'name',
+      'search_api_datasource',
+      'search_api_language',
+      'type',
+      'width',
+    ];
+    $actual_fields = array_keys($field_infos);
+    sort($actual_fields);
+    $this->assertEquals($expected_fields, $actual_fields, 'All expected field tables were created.');
+
+    $this->assertTrue(\Drupal::database()->schema()->tableExists($normalized_storage_table), 'Normalized storage table exists.');
+    $this->assertHasPrimaryKey($normalized_storage_table, 'Normalized storage table has a primary key.');
+    foreach ($field_infos as $field_id => $field_info) {
+      if ($field_id != 'search_api_id') {
+        $this->assertTrue(\Drupal::database()
+          ->schema()
+          ->tableExists($field_info['table']));
+      }
+      else {
+        $this->assertEmpty($field_info['table']);
+      }
+      $this->assertTrue(\Drupal::database()->schema()->fieldExists($normalized_storage_table, $field_info['column']), new FormattableMarkup('Field column %column exists', ['%column' => $field_info['column']]));
+    }
+  }
+
+  /**
+   * Checks whether changes to the index's fields are picked up by the server.
+   */
+  protected function updateIndex() {
+    /** @var \Drupal\search_api\IndexInterface $index */
+    $index = $this->getIndex();
+
+    // Remove a field from the index and check if the change is matched in the
+    // server configuration.
+    $field = $index->getField('keywords');
+    if (!$field) {
+      throw new \Exception();
+    }
+    $index->removeField('keywords');
+    $index->save();
+
+    $index_fields = array_keys($index->getFields());
+    // Include the three "magic" fields we're indexing with the DB backend.
+    $index_fields[] = 'search_api_datasource';
+    $index_fields[] = 'search_api_language';
+
+    $db_info = $this->getIndexDbInfo();
+    $server_fields = array_keys($db_info['field_tables']);
+
+    sort($index_fields);
+    sort($server_fields);
+    $this->assertEquals($index_fields, $server_fields);
+
+    // Add the field back for the next assertions.
+    $index->addField($field)->save();
+  }
+
+  /**
+   * Verifies that the generated table names are correct.
+   */
+  protected function checkTableNames() {
+    $this->assertEquals('search_api_db_database_search_index_1', $this->getIndexDbInfo()['index_table']);
+    $this->assertEquals('search_api_db_database_search_index_text', $this->getIndexDbInfo()['field_tables']['body']['table']);
+  }
+
+  /**
+   * Verifies that the stored information about multi-valued fields is correct.
+   */
+  protected function checkMultiValuedInfo() {
+    $db_info = $this->getIndexDbInfo();
+    $field_info = $db_info['field_tables'];
+
+    $fields = [
+      'name',
+      'body',
+      'type',
+      'keywords',
+      'category',
+      'width',
+      'search_api_datasource',
+      'search_api_language',
+    ];
+    $multi_valued = [
+      'name',
+      'body',
+      'keywords',
+    ];
+    foreach ($fields as $field_id) {
+      $this->assertArrayHasKey($field_id, $field_info, "Field info saved for field $field_id.");
+      if (in_array($field_id, $multi_valued)) {
+        $this->assertFalse(empty($field_info[$field_id]['multi-valued']), "Field $field_id is stored as multi-value.");
+      }
+      else {
+        $this->assertTrue(empty($field_info[$field_id]['multi-valued']), "Field $field_id is not stored as multi-value.");
+      }
+    }
+  }
+
+  /**
+   * Edits the server to enable partial matches.
+   *
+   * @param bool $enable
+   *   (optional) Whether partial matching should be enabled or disabled.
+   */
+  protected function editServerPartial($enable = TRUE) {
+    $server = $this->getServer();
+    $backend_config = $server->getBackendConfig();
+    $backend_config['partial_matches'] = $enable;
+    $server->setBackendConfig($backend_config);
+    $this->assertTrue((bool) $server->save(), 'The server was successfully edited.');
+    $this->resetEntityCache();
+  }
+
+  /**
+   * Tests whether partial searches work.
+   */
+  protected function searchSuccessPartial() {
+    $results = $this->buildSearch('foobaz')->range(0, 1)->execute();
+    $this->assertResults([1], $results, 'Partial search for »foobaz«');
+
+    $results = $this->buildSearch('foo', [], [], FALSE)
+      ->sort('search_api_relevance', QueryInterface::SORT_DESC)
+      ->sort('id')
+      ->execute();
+    $this->assertResults([1, 2, 4, 3, 5], $results, 'Partial search for »foo«');
+
+    $results = $this->buildSearch('foo tes')->execute();
+    $this->assertResults([1, 2, 3, 4], $results, 'Partial search for »foo tes«');
+
+    $results = $this->buildSearch('oob est')->execute();
+    $this->assertResults([1, 2, 3], $results, 'Partial search for »oob est«');
+
+    $results = $this->buildSearch('foo nonexistent')->execute();
+    $this->assertResults([], $results, 'Partial search for »foo nonexistent«');
+
+    $results = $this->buildSearch('bar nonexistent')->execute();
+    $this->assertResults([], $results, 'Partial search for »foo nonexistent«');
+
+    $keys = [
+      '#conjunction' => 'AND',
+      'oob',
+      [
+        '#conjunction' => 'OR',
+        'est',
+        'nonexistent',
+      ],
+    ];
+    $results = $this->buildSearch($keys)->execute();
+    $this->assertResults([1, 2, 3], $results, 'Partial search for complex keys');
+
+    $results = $this->buildSearch('foo', ['category,item_category'], [], FALSE)
+      ->sort('id', QueryInterface::SORT_DESC)
+      ->execute();
+    $this->assertResults([2, 1], $results, 'Partial search for »foo« with additional filter');
+
+    $query = $this->buildSearch();
+    $conditions = $query->createConditionGroup('OR');
+    $conditions->addCondition('name', 'test');
+    $conditions->addCondition('body', 'test');
+    $query->addConditionGroup($conditions);
+    $results = $query->execute();
+    $this->assertResults([1, 2, 3, 4], $results, 'Partial search with multi-field fulltext filter');
+  }
+
+  /**
+   * Edits the server to change the "Minimum word length" setting.
+   */
+  protected function editServerMinChars() {
+    $server = $this->getServer();
+    $backend_config = $server->getBackendConfig();
+    $backend_config['min_chars'] = 4;
+    $backend_config['partial_matches'] = FALSE;
+    $server->setBackendConfig($backend_config);
+    $success = (bool) $server->save();
+    $this->assertTrue($success, 'The server was successfully edited.');
+
+    $this->clearIndex();
+    $this->indexItems($this->indexId);
+
+    $this->resetEntityCache();
+  }
+
+  /**
+   * Tests the results of some test searches with minimum word length of 4.
+   */
+  protected function searchSuccessMinChars() {
+    $results = $this->getIndex()->query()->keys('test')->range(1, 2)->execute();
+    $this->assertEquals(4, $results->getResultCount(), 'Search for »test« returned correct number of results.');
+    $this->assertEquals($this->getItemIds([4, 1]), array_keys($results->getResultItems()), 'Search for »test« returned correct result.');
+    $this->assertEmpty($results->getIgnoredSearchKeys());
+    $this->assertEmpty($results->getWarnings());
+
+    $query = $this->buildSearch();
+    $conditions = $query->createConditionGroup('OR');
+    $conditions->addCondition('name', 'test');
+    $conditions->addCondition('body', 'test');
+    $query->addConditionGroup($conditions);
+    $results = $query->execute();
+    $this->assertResults([1, 2, 3, 4], $results, 'Search with multi-field fulltext filter');
+
+    $results = $this->buildSearch(NULL, ['body,test foobar'])->execute();
+    $this->assertResults([3], $results, 'Search with multi-term fulltext filter');
+
+    $results = $this->getIndex()->query()->keys('test foo')->execute();
+    $this->assertResults([2, 4, 1, 3], $results, 'Search for »test foo«', ['foo']);
+
+    $results = $this->buildSearch('foo', ['type,item'])->execute();
+    $this->assertResults([1, 2, 3], $results, 'Search for »foo«', ['foo'], ['No valid search keys were present in the query.']);
+
+    $keys = [
+      '#conjunction' => 'AND',
+      'test',
+      [
+        '#conjunction' => 'OR',
+        'baz',
+        'foobar',
+      ],
+      [
+        '#conjunction' => 'OR',
+        '#negation' => TRUE,
+        'bar',
+        'fooblob',
+      ],
+    ];
+    $results = $this->buildSearch($keys)->execute();
+    $this->assertResults([3], $results, 'Complex search 1', ['baz', 'bar']);
+
+    $keys = [
+      '#conjunction' => 'AND',
+      'test',
+      [
+        '#conjunction' => 'OR',
+        'baz',
+        'foobar',
+      ],
+      [
+        '#conjunction' => 'OR',
+        '#negation' => TRUE,
+        'bar',
+        'fooblob',
+      ],
+    ];
+    $results = $this->buildSearch($keys)->execute();
+    $this->assertResults([3], $results, 'Complex search 2', ['baz', 'bar']);
+
+    $results = $this->buildSearch(NULL, ['keywords,orange'])->execute();
+    $this->assertResults([1, 2, 5], $results, 'Filter query 1 on multi-valued field');
+
+    $conditions = [
+      'keywords,orange',
+      'keywords,apple',
+    ];
+    $results = $this->buildSearch(NULL, $conditions)->execute();
+    $this->assertResults([2], $results, 'Filter query 2 on multi-valued field');
+
+    $results = $this->buildSearch()->addCondition('keywords', 'orange', '<>')->execute();
+    $this->assertResults([3, 4], $results, 'Negated filter on multi-valued field');
+
+    $results = $this->buildSearch()->addCondition('keywords', NULL)->execute();
+    $this->assertResults([3], $results, 'Query with NULL filter');
+
+    $results = $this->buildSearch()->addCondition('keywords', NULL, '<>')->execute();
+    $this->assertResults([1, 2, 4, 5], $results, 'Query with NOT NULL filter');
+  }
+
+  /**
+   * Checks that an unknown operator throws an exception.
+   */
+  protected function checkUnknownOperator() {
+    try {
+      $this->buildSearch()
+        ->addCondition('id', 1, '!=')
+        ->execute();
+      $this->fail('Unknown operator "!=" did not throw an exception.');
+    }
+    catch (SearchApiException $e) {
+      $this->assertTrue(TRUE, 'Unknown operator "!=" threw an exception.');
+    }
+  }
+
+  /**
+   * Checks whether the module's specific alter hooks work correctly.
+   */
+  protected function checkDbQueryAlter() {
+    $query = $this->buildSearch();
+    $query->setOption('search_api_test_db_search_api_db_query_alter', TRUE);
+    $results = $query->execute();
+    $this->assertResults([], $results, 'Query triggering custom alter hook');
+  }
+
+  /**
+   * Checks that field ID changes are treated correctly (without re-indexing).
+   */
+  protected function checkFieldIdChanges() {
+    $this->getIndex()
+      ->renameField('type', 'foobar')
+      ->save();
+
+    $results = $this->buildSearch(NULL, ['foobar,item'])->execute();
+    $this->assertResults([1, 2, 3], $results, 'Search after renaming a field.');
+    $this->getIndex()->renameField('foobar', 'type')->save();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function checkSecondServer() {
+    /** @var \Drupal\search_api\ServerInterface $second_server */
+    $second_server = Server::create([
+      'id' => 'test2',
+      'backend' => 'search_api_db',
+      'backend_config' => [
+        'database' => 'default:default',
+      ],
+    ]);
+    $second_server->save();
+    $query = $this->buildSearch();
+    try {
+      $second_server->search($query);
+      $this->fail('Could execute a query for an index on a different server.');
+    }
+    catch (SearchApiException $e) {
+      $this->assertTrue(TRUE, 'Executing a query for an index on a different server throws an exception.');
+    }
+    $second_server->delete();
+  }
+
+  /**
+   * Tests the case-sensitivity of fulltext searches.
+   *
+   * @see https://www.drupal.org/node/2557291
+   */
+  protected function regressionTest2557291() {
+    $results = $this->buildSearch('case')->execute();
+    $this->assertResults([1], $results, 'Search for lowercase "case"');
+
+    $results = $this->buildSearch('Case')->execute();
+    $this->assertResults([1, 3], $results, 'Search for capitalized "Case"');
+
+    $results = $this->buildSearch('CASE')->execute();
+    $this->assertResults([], $results, 'Search for non-existent uppercase version of "CASE"');
+
+    $results = $this->buildSearch('föö')->execute();
+    $this->assertResults([1], $results, 'Search for keywords with umlauts');
+
+    $results = $this->buildSearch('smile' . json_decode('"\u1F601"'))->execute();
+    $this->assertResults([1], $results, 'Search for keywords with umlauts');
+
+    $results = $this->buildSearch()->addCondition('keywords', 'grape', '<>')->execute();
+    $this->assertResults([1, 3], $results, 'Negated filter on multi-valued field');
+  }
+
+  /**
+   * Tests searching for multiple two-letter words.
+   *
+   * @see https://www.drupal.org/node/2511860
+   */
+  protected function regressionTest2511860() {
+    $query = $this->buildSearch();
+    $query->addCondition('body', 'ab xy');
+    $results = $query->execute();
+    $this->assertEquals(5, $results->getResultCount(), 'Fulltext filters on short words do not change the result.');
+
+    $query = $this->buildSearch();
+    $query->addCondition('body', 'ab ab');
+    $results = $query->execute();
+    $this->assertEquals(5, $results->getResultCount(), 'Fulltext filters on duplicate short words do not change the result.');
+  }
+
+  /**
+   * Tests changing a field boost to a floating point value.
+   *
+   * @see https://www.drupal.org/node/2846932
+   */
+  protected function regressionTest2846932() {
+    $index = $this->getIndex();
+    $index->getField('body')->setBoost(0.8);
+    $index->save();
+  }
+
+  /**
+   * Tests indexing of text tokens with leading/trailing whitespace.
+   *
+   * @see https://www.drupal.org/node/2926733
+   */
+  protected function regressionTest2926733() {
+    $index = $this->getIndex();
+    $item_id = $this->getItemIds([1])[0];
+    $fields_helper = \Drupal::getContainer()
+      ->get('search_api.fields_helper');
+    $item = $fields_helper->createItem($index, $item_id);
+    $field = clone $index->getField('body');
+    $value = new TextValue('test');
+    $tokens = [];
+    foreach (['test', ' test', '  test', 'test  ', ' test '] as $token) {
+      $tokens[] = new TextToken($token);
+    }
+    $value->setTokens($tokens);
+    $field->setValues([$value]);
+    $item->setFields([
+      'body' => $field,
+    ]);
+    $item->setFieldsExtracted(TRUE);
+    $index->getServerInstance()->indexItems($index, [$item_id => $item]);
+
+    // Make sure to re-index the proper version of the item to avoid confusing
+    // the other tests.
+    list($datasource_id, $raw_id) = Utility::splitCombinedId($item_id);
+    $index->trackItemsUpdated($datasource_id, [$raw_id]);
+    $this->indexItems($index->id());
+  }
+
+  /**
+   * Tests indexing of items with boost.
+   *
+   * @see https://www.drupal.org/node/2938646
+   */
+  protected function regressionTest2938646() {
+    $db_info = $this->getIndexDbInfo();
+    $text_table = $db_info['field_tables']['body']['table'];
+    $item_id = $this->getItemIds([1])[0];
+    $select = \Drupal::database()->select($text_table, 't');
+    $select
+      ->fields('t', ['score'])
+      ->condition('item_id', $item_id)
+      ->condition('word', 'test');
+    $select2 = clone $select;
+
+    // Check old score.
+    $old_score = $select
+      ->execute()
+      ->fetchField();
+    $this->assertNotSame(FALSE, $old_score);
+    $this->assertGreaterThan(0, $old_score);
+
+    // Re-index item with higher boost.
+    $index = $this->getIndex();
+    $item = $this->container->get('search_api.fields_helper')
+      ->createItem($index, $item_id);
+    $item->setBoost(2);
+    $indexed_ids = $this->indexItemDirectly($index, $item);
+    $this->assertEquals([$item_id], $indexed_ids);
+
+    // Verify the field scores changed accordingly.
+    $new_score = $select2
+      ->execute()
+      ->fetchField();
+    $this->assertNotSame(FALSE, $new_score);
+    $this->assertEquals(2 * $old_score, $new_score);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function checkIndexWithoutFields() {
+    $index = parent::checkIndexWithoutFields();
+
+    $expected = [
+      'search_api_datasource',
+      'search_api_language',
+    ];
+    $db_info = $this->getIndexDbInfo($index->id());
+    $info_fields = array_keys($db_info['field_tables']);
+    sort($info_fields);
+    $this->assertEquals($expected, $info_fields);
+
+    return $index;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function checkModuleUninstall() {
+    $db_info = $this->getIndexDbInfo();
+    $normalized_storage_table = $db_info['index_table'];
+    $field_tables = $db_info['field_tables'];
+
+    // See whether clearing the server works.
+    // Regression test for #2156151.
+    $server = $this->getServer();
+    $index = $this->getIndex();
+    $server->deleteAllIndexItems($index);
+    $query = $this->buildSearch();
+    $results = $query->execute();
+    $this->assertEquals(0, $results->getResultCount(), 'Clearing the server worked correctly.');
+    $schema = \Drupal::database()->schema();
+    $table_exists = $schema->tableExists($normalized_storage_table);
+    $this->assertTrue($table_exists, 'The index tables were left in place.');
+
+    // See whether disabling the index correctly removes all of its tables.
+    $index->disable()->save();
+    $db_info = $this->getIndexDbInfo();
+    $this->assertNull($db_info, 'The index was successfully removed from the server.');
+    $table_exists = $schema->tableExists($normalized_storage_table);
+    $this->assertFalse($table_exists, 'The index tables were deleted.');
+    foreach ($field_tables as $field_table) {
+      $table_exists = $schema->tableExists($field_table['table']);
+      $this->assertFalse($table_exists, "Field table {$field_table['table']} was successfully deleted.");
+    }
+    $index->enable()->save();
+
+    // Remove first the index and then the server.
+    $index->setServer();
+    $index->save();
+
+    $db_info = $this->getIndexDbInfo();
+    $this->assertNull($db_info, 'The index was successfully removed from the server.');
+    $table_exists = $schema->tableExists($normalized_storage_table);
+    $this->assertFalse($table_exists, 'The index tables were deleted.');
+    foreach ($field_tables as $field_table) {
+      $table_exists = $schema->tableExists($field_table['table']);
+      $this->assertFalse($table_exists, "Field table {$field_table['table']} was successfully deleted.");
+    }
+
+    // Re-add the index to see if the associated tables are also properly
+    // removed when the server is deleted.
+    $index->setServer($server);
+    $index->save();
+    $server->delete();
+
+    $db_info = $this->getIndexDbInfo();
+    $this->assertNull($db_info, 'The index was successfully removed from the server.');
+    $table_exists = $schema->tableExists($normalized_storage_table);
+    $this->assertFalse($table_exists, 'The index tables were deleted.');
+    foreach ($field_tables as $field_table) {
+      $table_exists = $schema->tableExists($field_table['table']);
+      $this->assertFalse($table_exists, "Field table {$field_table['table']} was successfully deleted.");
+    }
+
+    // Uninstall the module.
+    \Drupal::service('module_installer')->uninstall(['search_api_db'], FALSE);
+    $this->assertFalse(\Drupal::moduleHandler()->moduleExists('search_api_db'), 'The Database Search module was successfully uninstalled.');
+
+    $tables = $schema->findTables('search_api_db_%');
+    $expected = [
+      'search_api_db_database_search_index' => 'search_api_db_database_search_index',
+    ];
+    $this->assertEquals($expected, $tables, 'All the tables of the the Database Search module have been removed.');
+  }
+
+  /**
+   * Retrieves the database information for the test index.
+   *
+   * @param string|null $index_id
+   *   (optional) The ID of the index whose database information should be
+   *   retrieved.
+   *
+   * @return array
+   *   The database information stored by the backend for the test index.
+   */
+  protected function getIndexDbInfo($index_id = NULL) {
+    $index_id = $index_id ?: $this->indexId;
+    return \Drupal::keyValue(Database::INDEXES_KEY_VALUE_STORE_ID)
+      ->get($index_id);
+  }
+
+  /**
+   * Indexes an item directly.
+   *
+   * @param \Drupal\search_api\IndexInterface $index
+   *   The search index to index the item on.
+   * @param \Drupal\search_api\Item\ItemInterface $item
+   *   The item.
+   *
+   * @return string[]
+   *   The successfully indexed IDs.
+   *
+   * @throws \Drupal\search_api\SearchApiException
+   *   Thrown if indexing failed.
+   */
+  protected function indexItemDirectly(IndexInterface $index, ItemInterface $item) {
+    $items = [$item->getId() => $item];
+
+    // Minimalistic version of code copied from
+    // \Drupal\search_api\Entity\Index::indexSpecificItems().
+    $index->alterIndexedItems($items);
+    \Drupal::moduleHandler()->alter('search_api_index_items', $index, $items);
+    foreach ($items as $item) {
+      // This will cache the extracted fields so processors, etc., can retrieve
+      // them directly.
+      $item->getFields();
+    }
+    $index->preprocessIndexItems($items);
+
+    $indexed_ids = [];
+    if ($items) {
+      $indexed_ids = $index->getServerInstance()->indexItems($index, $items);
+    }
+    return $indexed_ids;
+  }
+
+  /**
+   * Tests whether a server on a non-default database is handled correctly.
+   */
+  public function testNonDefaultDatabase() {
+    // Clone the primary credentials to a replica connection.
+    // Note this will result in two independent connection objects that happen
+    // to point to the same place.
+    // @see \Drupal\KernelTests\Core\Database\ConnectionTest::testConnectionRouting()
+    $connection_info = CoreDatabase::getConnectionInfo('default');
+    CoreDatabase::addConnectionInfo('default', 'replica', $connection_info['default']);
+
+    $db1 = CoreDatabase::getConnection('default', 'default');
+    $db2 = CoreDatabase::getConnection('replica', 'default');
+
+    // Safety checks copied from the Core test, if these fail something is wrong
+    // with Core.
+    $this->assertNotNull($db1, 'default connection is a real connection object.');
+    $this->assertNotNull($db2, 'replica connection is a real connection object.');
+    $this->assertNotSame($db1, $db2, 'Each target refers to a different connection.');
+
+    // Create backends based on each of the two targets and verify they use the
+    // right connections.
+    $config = [
+      'database' => 'default:default',
+    ];
+    $backend1 = Database::create($this->container, $config, '', []);
+    $config['database'] = 'default:replica';
+    $backend2 = Database::create($this->container, $config, '', []);
+
+    $this->assertSame($db1, $backend1->getDatabase());
+    $this->assertSame($db2, $backend2->getDatabase());
+
+    // Make sure they also use different DBMS compatibility handlers, which also
+    // use the correct database connections.
+    $dbms_comp1 = $backend1->getDbmsCompatibilityHandler();
+    $dbms_comp2 = $backend2->getDbmsCompatibilityHandler();
+    $this->assertNotSame($dbms_comp1, $dbms_comp2);
+    $this->assertSame($db1, $dbms_comp1->getDatabase());
+    $this->assertSame($db2, $dbms_comp2->getDatabase());
+
+    // Finally, make sure the DBMS compatibility handlers also have the correct
+    // classes (meaning we used the correct one and didn't just fall back to the
+    // generic database).
+    $service = $this->container->get('search_api_db.database_compatibility');
+    $database_type = $db1->databaseType();
+    $service_id = "$database_type.search_api_db.database_compatibility";
+    $service2 = $this->container->get($service_id);
+    $this->assertSame($service2, $service);
+    $class = get_class($service);
+    $this->assertNotEquals(GenericDatabase::class, $class);
+    $this->assertSame($dbms_comp1, $service);
+    $this->assertEquals($class, get_class($dbms_comp2));
+  }
+
+}

+ 18 - 0
sites/all/modules/contrib/search/search_api/modules/search_api_views_taxonomy/search_api_views_taxonomy.info.yml

@@ -0,0 +1,18 @@
+type: module
+name: 'Search API Taxonomy Term Handlers'
+description: 'Deprecated. You should uninstall this module.'
+package: Search
+# core: 8.x
+hidden: true
+configure: search_api.overview
+dependencies:
+  - drupal:system (>=8.1)
+  - search_api:search_api
+  - drupal:taxonomy
+  - drupal:views
+
+# Information added by Drupal.org packaging script on 2018-02-23
+version: '8.x-1.7'
+core: '8.x'
+project: 'search_api'
+datestamp: 1519387691

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

@@ -0,0 +1,24 @@
+<?php
+
+/**
+ * @file
+ * Install, update and uninstall functions for the Search API Taxonomy Term Handlers module.
+ */
+
+use Drupal\Core\Url;
+
+/**
+ * Implements hook_requirements().
+ */
+function search_api_views_taxonomy_requirements() {
+  $requirements['search_api_views_taxonomy'] = [
+    'title' => t('Search API Taxonomy Term Handlers'),
+    'value' => t('Out-dated'),
+    'description' => t('All functionality of the "Search API Taxonomy Term Handlers" module has been moved into the Search API module. You should <a href=":uninstall">uninstall this module</a>.', [
+      ':uninstall' => Url::fromRoute('system.modules_uninstall')->toString(),
+    ]),
+    'severity' => REQUIREMENT_WARNING,
+  ];
+
+  return $requirements;
+}

+ 254 - 0
sites/all/modules/contrib/search/search_api/phpcs.xml

@@ -0,0 +1,254 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ruleset name="search_api">
+  <description>Default PHP CodeSniffer configuration for the Search API module.</description>
+  <file>.</file>
+  <arg name="extensions" value="inc,install,module,php,profile,test,theme"/>
+
+  <!-- Only include specific sniffs that pass. This ensures that, if new sniffs are added, HEAD does not fail.-->
+  <!-- Drupal sniffs -->
+  <rule ref="Generic.Arrays.DisallowLongArraySyntax"/>
+  <rule ref="Drupal.Classes.ClassCreateInstance"/>
+  <rule ref="Drupal.Classes.ClassDeclaration"/>
+  <rule ref="Drupal.Classes.FullyQualifiedNamespace"/>
+  <rule ref="Drupal.Classes.InterfaceName"/>
+  <rule ref="Drupal.Classes.UnusedUseStatement"/>
+  <rule ref="Drupal.Classes.UseLeadingBackslash"/>
+  <rule ref="Drupal.CSS.ClassDefinitionNameSpacing"/>
+  <rule ref="Drupal.CSS.ColourDefinition"/>
+  <rule ref="Drupal.Commenting.ClassComment"/>
+  <rule ref="Drupal.Commenting.DataTypeNamespace" />
+  <rule ref="Drupal.Commenting.DocComment"/>
+  <rule ref="Drupal.Commenting.DocCommentStar"/>
+  <rule ref="Drupal.Commenting.FileComment"/>
+  <rule ref="Drupal.Commenting.FunctionComment"/>
+  <rule ref="Drupal.Commenting.InlineComment">
+    <!-- This is impractical when commenting code out. -->
+    <exclude name="Drupal.Commenting.InlineComment.InvalidEndChar" />
+    <!-- We (rarely) use comments as "headings" for multiple functions. -->
+    <exclude name="Drupal.Commenting.InlineComment.SpacingAfter" />
+    <!--
+      This disallows indentation in comments, even though it can sometimes be
+      helpful for structured explanations.
+
+      @see \Drupal\search_api\Plugin\search_api\processor\ContentAccess::addNodeAccess()
+    -->
+    <exclude name="Drupal.Commenting.InlineComment.SpacingBefore" />
+  </rule>
+  <rule ref="Drupal.Commenting.VariableComment">
+    <!-- This finds false positives when @code is used. -->
+    <exclude name="Drupal.Commenting.VariableComment.VarOrder"/>
+  </rule>
+  <rule ref="Drupal.Commenting.PostStatementComment"/>
+  <rule ref="Drupal.ControlStructures.ElseIf"/>
+  <rule ref="Drupal.ControlStructures.ControlSignature"/>
+  <rule ref="Drupal.ControlStructures.InlineControlStructure"/>
+  <rule ref="Drupal.Files.EndFileNewline"/>
+  <rule ref="Drupal.Files.FileEncoding"/>
+  <rule ref="Drupal.Files.TxtFileLineLength"/>
+  <rule ref="Drupal.Formatting.MultiLineAssignment"/>
+  <rule ref="Drupal.Formatting.SpaceInlineIf"/>
+  <rule ref="Drupal.Formatting.SpaceUnaryOperator"/>
+  <rule ref="Drupal.Functions.DiscouragedFunctions"/>
+  <rule ref="Drupal.Functions.FunctionDeclaration"/>
+  <rule ref="Drupal.InfoFiles.AutoAddedKeys"/>
+  <rule ref="Drupal.InfoFiles.ClassFiles"/>
+  <rule ref="Drupal.InfoFiles.DuplicateEntry"/>
+  <rule ref="Drupal.InfoFiles.Required"/>
+  <rule ref="Drupal.Methods.MethodDeclaration"/>
+  <rule ref="Drupal.NamingConventions.ValidVariableName">
+    <!-- This interferes with the stored entity properties. -->
+    <exclude name="Drupal.NamingConventions.ValidVariableName.LowerCamelName"/>
+  </rule>
+  <rule ref="Drupal.Scope.MethodScope"/>
+  <rule ref="Drupal.Semantics.EmptyInstall"/>
+  <rule ref="Drupal.Semantics.FunctionAlias"/>
+  <rule ref="Drupal.Semantics.FunctionT"/>
+  <rule ref="Drupal.Semantics.FunctionWatchdog"/>
+  <rule ref="Drupal.Semantics.InstallHooks"/>
+  <rule ref="Drupal.Semantics.LStringTranslatable"/>
+  <rule ref="Drupal.Semantics.PregSecurity"/>
+  <rule ref="Drupal.Semantics.TInHookMenu"/>
+  <rule ref="Drupal.Semantics.TInHookSchema"/>
+  <rule ref="Drupal.WhiteSpace.CloseBracketSpacing"/>
+  <rule ref="Drupal.WhiteSpace.Comma"/>
+  <rule ref="Drupal.WhiteSpace.EmptyLines"/>
+  <rule ref="Drupal.WhiteSpace.Namespace"/>
+  <rule ref="Drupal.WhiteSpace.ObjectOperatorIndent"/>
+  <rule ref="Drupal.WhiteSpace.ObjectOperatorSpacing"/>
+  <rule ref="Drupal.WhiteSpace.OpenBracketSpacing"/>
+  <rule ref="Drupal.WhiteSpace.OpenTagNewline"/>
+  <rule ref="Drupal.WhiteSpace.OperatorSpacing"/>
+  <rule ref="Drupal.WhiteSpace.ScopeClosingBrace"/>
+  <rule ref="Drupal.WhiteSpace.ScopeIndent"/>
+
+  <!-- Drupal Practice sniffs -->
+  <rule ref="DrupalPractice.Commenting.ExpectedException"/>
+
+  <!-- Generic sniffs -->
+  <rule ref="Generic.Files.ByteOrderMark"/>
+  <rule ref="Generic.Files.LineEndings"/>
+  <rule ref="Generic.Formatting.SpaceAfterCast"/>
+  <rule ref="Generic.Functions.FunctionCallArgumentSpacing"/>
+  <rule ref="Generic.Functions.OpeningFunctionBraceKernighanRitchie">
+    <properties>
+      <property name="checkClosures" value="true"/>
+    </properties>
+  </rule>
+  <rule ref="Generic.NamingConventions.ConstructorName"/>
+  <rule ref="Generic.NamingConventions.UpperCaseConstantName"/>
+  <rule ref="Generic.PHP.DeprecatedFunctions"/>
+  <rule ref="Generic.PHP.DisallowShortOpenTag"/>
+  <rule ref="Generic.PHP.LowerCaseKeyword"/>
+  <rule ref="Generic.PHP.UpperCaseConstant"/>
+  <rule ref="Generic.WhiteSpace.DisallowTabIndent"/>
+
+  <!-- MySource sniffs -->
+  <rule ref="MySource.Debug.DebugCode"/>
+
+  <!-- PEAR sniffs -->
+  <rule ref="PEAR.Files.IncludingFile"/>
+  <!-- Disable some error messages that we do not want. -->
+  <rule ref="PEAR.Files.IncludingFile.UseIncludeOnce">
+    <severity>0</severity>
+  </rule>
+  <rule ref="PEAR.Files.IncludingFile.UseInclude">
+    <severity>0</severity>
+  </rule>
+  <rule ref="PEAR.Files.IncludingFile.UseRequireOnce">
+    <severity>0</severity>
+  </rule>
+  <rule ref="PEAR.Files.IncludingFile.UseRequire">
+    <severity>0</severity>
+  </rule>
+  <rule ref="PEAR.Functions.ValidDefaultValue"/>
+
+  <!-- PEAR sniffs -->
+  <rule ref="PEAR.Functions.FunctionCallSignature"/>
+  <!-- The sniffs inside PEAR.Functions.FunctionCallSignature silenced below are
+    also silenced in Drupal CS' ruleset.xml. The code below is a 1-on-1 copy
+    from that file. -->
+  <!-- Disable some error messages that we already cover. -->
+  <rule ref="PEAR.Functions.FunctionCallSignature.SpaceAfterOpenBracket">
+    <severity>0</severity>
+  </rule>
+  <rule ref="PEAR.Functions.FunctionCallSignature.SpaceBeforeCloseBracket">
+    <severity>0</severity>
+  </rule>
+  <!-- Disable some error messages that we do not want. -->
+  <rule ref="PEAR.Functions.FunctionCallSignature.Indent">
+    <severity>0</severity>
+  </rule>
+  <rule ref="PEAR.Functions.FunctionCallSignature.ContentAfterOpenBracket">
+    <severity>0</severity>
+  </rule>
+  <rule ref="PEAR.Functions.FunctionCallSignature.CloseBracketLine">
+    <severity>0</severity>
+  </rule>
+  <rule ref="PEAR.Functions.FunctionCallSignature.EmptyLine">
+    <severity>0</severity>
+  </rule>
+
+  <!-- PSR-2 sniffs -->
+  <rule ref="PSR2.Classes.PropertyDeclaration"/>
+  <rule ref="PSR2.Namespaces.NamespaceDeclaration"/>
+  <rule ref="PSR2.Namespaces.UseDeclaration"/>
+
+  <!-- Squiz sniffs -->
+  <rule ref="Squiz.Arrays.ArrayBracketSpacing"/>
+  <rule ref="Squiz.Arrays.ArrayDeclaration">
+    <exclude name="Squiz.Arrays.ArrayDeclaration.NoKeySpecified"/>
+    <exclude name="Squiz.Arrays.ArrayDeclaration.KeySpecified"/>
+  </rule>
+  <!-- Disable some error messages that we do not want. -->
+  <rule ref="Squiz.Arrays.ArrayDeclaration.CloseBraceNotAligned">
+    <severity>0</severity>
+  </rule>
+  <rule ref="Squiz.Arrays.ArrayDeclaration.DoubleArrowNotAligned">
+    <severity>0</severity>
+  </rule>
+  <rule ref="Squiz.Arrays.ArrayDeclaration.FirstValueNoNewline">
+    <severity>0</severity>
+  </rule>
+  <rule ref="Squiz.Arrays.ArrayDeclaration.KeyNotAligned">
+    <severity>0</severity>
+  </rule>
+  <rule ref="Squiz.Arrays.ArrayDeclaration.MultiLineNotAllowed">
+    <severity>0</severity>
+  </rule>
+  <rule ref="Squiz.Arrays.ArrayDeclaration.NoComma">
+    <severity>0</severity>
+  </rule>
+  <rule ref="Squiz.Arrays.ArrayDeclaration.NoCommaAfterLast">
+    <severity>0</severity>
+  </rule>
+  <rule ref="Squiz.Arrays.ArrayDeclaration.NotLowerCase">
+    <severity>0</severity>
+  </rule>
+  <rule ref="Squiz.Arrays.ArrayDeclaration.SingleLineNotAllowed">
+    <severity>0</severity>
+  </rule>
+  <rule ref="Squiz.Arrays.ArrayDeclaration.ValueNotAligned">
+    <severity>0</severity>
+  </rule>
+  <rule ref="Squiz.Arrays.ArrayDeclaration.ValueNoNewline">
+    <severity>0</severity>
+  </rule>
+  <rule ref="Squiz.ControlStructures.ForEachLoopDeclaration"/>
+  <!-- Disable some error messages that we already cover. -->
+  <rule ref="Squiz.ControlStructures.ForEachLoopDeclaration.AsNotLower">
+    <severity>0</severity>
+  </rule>
+  <rule ref="Squiz.ControlStructures.ForEachLoopDeclaration.SpaceAfterOpen">
+    <severity>0</severity>
+  </rule>
+  <rule ref="Squiz.ControlStructures.ForEachLoopDeclaration.SpaceBeforeClose">
+    <severity>0</severity>
+  </rule>
+  <rule ref="Squiz.ControlStructures.ForLoopDeclaration"/>
+  <!-- Disable some error messages that we already cover. -->
+  <rule ref="Squiz.ControlStructures.ForLoopDeclaration.SpacingAfterOpen">
+    <severity>0</severity>
+  </rule>
+  <rule ref="Squiz.ControlStructures.ForLoopDeclaration.SpacingBeforeClose">
+    <severity>0</severity>
+  </rule>
+  <rule ref="Squiz.Functions.MultiLineFunctionDeclaration"/>
+  <rule ref="Squiz.Functions.MultiLineFunctionDeclaration.BraceOnSameLine">
+    <severity>0</severity>
+  </rule>
+  <rule ref="Squiz.Functions.MultiLineFunctionDeclaration.ContentAfterBrace">
+    <severity>0</severity>
+  </rule>
+  <!-- Standard yet to be finalized on this (https://www.drupal.org/node/1539712). -->
+  <rule ref="Squiz.Functions.MultiLineFunctionDeclaration.FirstParamSpacing">
+    <severity>0</severity>
+  </rule>
+  <rule ref="Squiz.Functions.MultiLineFunctionDeclaration.Indent">
+    <severity>0</severity>
+  </rule>
+  <rule ref="Squiz.Functions.MultiLineFunctionDeclaration.CloseBracketLine">
+    <severity>0</severity>
+  </rule>
+  <rule ref="Squiz.Functions.FunctionDeclarationArgumentSpacing">
+    <properties>
+      <property name="equalsSpacing" value="1"/>
+    </properties>
+  </rule>
+  <rule ref="Squiz.Functions.FunctionDeclarationArgumentSpacing.NoSpaceBeforeArg">
+    <severity>0</severity>
+  </rule>
+  <rule ref="Squiz.PHP.LowercasePHPFunctions"/>
+  <rule ref="Squiz.Strings.ConcatenationSpacing">
+    <properties>
+      <property name="spacing" value="1"/>
+      <property name="ignoreNewlines" value="true"/>
+    </properties>
+  </rule>
+  <rule ref="Squiz.WhiteSpace.LanguageConstructSpacing" />
+  <rule ref="Squiz.WhiteSpace.SemicolonSpacing"/>
+  <rule ref="Squiz.WhiteSpace.SuperfluousWhitespace"/>
+
+  <!-- Zend sniffs -->
+  <rule ref="Zend.Files.ClosingTag"/>
+
+</ruleset>

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

@@ -0,0 +1,354 @@
+<?php
+
+/**
+ * @file
+ * Hooks provided by the Search API module.
+ */
+
+/**
+ * @addtogroup hooks
+ * @{
+ */
+
+/**
+ * Alter the available Search API backends.
+ *
+ * Modules may implement this hook to alter the information that defines Search
+ * API backends. All properties that are available in
+ * \Drupal\search_api\Annotation\SearchApiBackend can be altered here, with the
+ * addition of the "class" and "provider" keys.
+ *
+ * @param array $backend_info
+ *   The Search API backend info array, keyed by backend ID.
+ *
+ * @see \Drupal\search_api\Backend\BackendPluginBase
+ */
+function hook_search_api_backend_info_alter(array &$backend_info) {
+  foreach ($backend_info as $id => $info) {
+    $backend_info[$id]['class'] = '\Drupal\my_module\MyBackendDecorator';
+    $backend_info[$id]['example_original_class'] = $info['class'];
+  }
+}
+
+/**
+ * Alter the features a given search server supports.
+ *
+ * @param string[] $features
+ *   The features supported by the server's backend.
+ * @param \Drupal\search_api\ServerInterface $server
+ *   The search server in question.
+ *
+ * @see \Drupal\search_api\Backend\BackendSpecificInterface::getSupportedFeatures()
+ */
+function hook_search_api_server_features_alter(array &$features, \Drupal\search_api\ServerInterface $server) {
+  if ($server->getBackend() instanceof \Drupal\search_api_solr\Plugin\search_api\backend\SearchApiSolrBackend) {
+    $features[] = 'my_custom_feature';
+  }
+}
+
+/**
+ * Alter the available datasources.
+ *
+ * Modules may implement this hook to alter the information that defines
+ * datasources. All properties that are available in
+ * \Drupal\search_api\Annotation\SearchApiDatasource can be altered here, with
+ * the addition of the "class" and "provider" keys.
+ *
+ * @param array $infos
+ *   The datasource info array, keyed by datasource IDs.
+ *
+ * @see \Drupal\search_api\Datasource\DatasourcePluginBase
+ */
+function hook_search_api_datasource_info_alter(array &$infos) {
+  // I'm a traditionalist, I want them called "nodes"!
+  $infos['entity:node']['label'] = t('Node');
+}
+
+/**
+ * Alter the available processors.
+ *
+ * Modules may implement this hook to alter the information that defines
+ * processors. All properties that are available in
+ * \Drupal\search_api\Annotation\SearchApiProcessor can be altered here, with
+ * the addition of the "class" and "provider" keys.
+ *
+ * @param array $processors
+ *   The processor information to be altered, keyed by processor IDs.
+ *
+ * @see \Drupal\search_api\Processor\ProcessorPluginBase
+ */
+function hook_search_api_processor_info_alter(array &$processors) {
+  if (!empty($processors['example_processor'])) {
+    $processors['example_processor']['class'] = '\Drupal\my_module\MuchBetterExampleProcessor';
+  }
+}
+
+/**
+ * Alter the available data types.
+ *
+ * @param array $data_type_definitions
+ *   The definitions of the data type plugins.
+ *
+ * @see \Drupal\search_api\DataType\DataTypePluginBase
+ */
+function hook_search_api_data_type_info_alter(array &$data_type_definitions) {
+  if (isset($data_type_definitions['text'])) {
+    $data_type_definitions['text']['label'] = t('Parsed text');
+  }
+}
+
+/**
+ * Alter the available parse modes.
+ *
+ * @param array $parse_mode_definitions
+ *   The definitions of the data type plugins.
+ *
+ * @see \Drupal\search_api\ParseMode\ParseModePluginBase
+ */
+function hook_search_api_parse_mode_info_alter(array &$parse_mode_definitions) {
+  if (isset($parse_mode_definitions['direct'])) {
+    $parse_mode_definitions['direct']['label'] = t('Solr syntax');
+  }
+}
+
+/**
+ * Alter the tracker info.
+ *
+ * @param array $tracker_info
+ *   The Search API tracker info array, keyed by tracker ID.
+ *
+ * @see \Drupal\search_api\Tracker\TrackerPluginBase
+ */
+function hook_search_api_tracker_info_alter(array &$tracker_info) {
+  if (isset($tracker_info['default'])) {
+    $tracker_info['default']['example_original_class'] = $tracker_info['default']['class'];
+    $tracker_info['default']['class'] = '\Drupal\my_module\Plugin\search_api\tracker\MyCustomImplementationTracker';
+  }
+}
+
+/**
+ * Alter the list of known search displays.
+ *
+ * @param array $displays
+ *   The Search API display info array, keyed by display ID.
+ *
+ * @see \Drupal\search_api\Display\DisplayPluginBase
+ */
+function hook_search_api_displays_alter(array &$displays) {
+  if (isset($displays['some_key'])) {
+    $displays['some_key']['label'] = t('New label for existing Display');
+  }
+}
+
+/**
+ * Alter the mapping of Drupal data types to Search API data types.
+ *
+ * @param array $mapping
+ *   An array mapping all known (and supported) Drupal data types to their
+ *   corresponding Search API data types. A value of FALSE means that fields of
+ *   that type should be ignored by the Search API.
+ *
+ * @see \Drupal\search_api\Utility\DataTypeHelperInterface::getFieldTypeMapping()
+ */
+function hook_search_api_field_type_mapping_alter(array &$mapping) {
+  $mapping['duration_iso8601'] = FALSE;
+  $mapping['my_new_type'] = 'string';
+}
+
+/**
+ * Alter the mapping of Search API data types to their default Views handlers.
+ *
+ * Field handlers are not determined by these simplified (Search API) types, but
+ * by their actual property data types. For altering that mapping, see
+ * hook_search_api_views_field_handler_mapping_alter().
+ *
+ * @param array $mapping
+ *   An associative array with data types as the keys and Views table data
+ *   definition items as the values. In addition to all normally defined Search
+ *   API data types, keys can also be "options" for any field with an options
+ *   list, "entity" for general entity-typed fields or "entity:ENTITY_TYPE"
+ *   (with "ENTITY_TYPE" being the machine name of an entity type) for entities
+ *   of that type.
+ */
+function hook_search_api_views_handler_mapping_alter(array &$mapping) {
+  $mapping['entity:my_entity_type'] = [
+    'argument' => [
+      'id' => 'my_entity_type',
+    ],
+    'filter' => [
+      'id' => 'my_entity_type',
+    ],
+    'sort' => [
+      'id' => 'my_entity_type',
+    ],
+  ];
+  $mapping['date']['filter']['id'] = 'my_date_filter';
+}
+
+/**
+ * Alter the mapping of property types to their default Views field handlers.
+ *
+ * This is used in the Search API Views integration to create Search
+ * API-specific field handlers for all properties of datasources and some entity
+ * types.
+ *
+ * In addition to the definition returned here, for Field API fields, the
+ * "field_name" will be set to the field's machine name.
+ *
+ * @param array $mapping
+ *   An associative array with property data types as the keys and Views field
+ *   handler definitions as the values (that is, just the inner "field" portion
+ *   of Views data definition items). In some cases the value might also be NULL
+ *   instead, to indicate that properties of this type shouldn't have field
+ *   handlers. The data types in the keys might also contain asterisks (*) as
+ *   wildcard characters. Data types with wildcards will be matched only if no
+ *   specific type exists, and longer type patterns will be tried before shorter
+ *   ones. The "*" mapping therefore is the default if no other match could be
+ *   found.
+ */
+function hook_search_api_views_field_handler_mapping_alter(array &$mapping) {
+  $mapping['field_item:string_long'] = [
+    'id' => 'example_field',
+  ];
+  $mapping['example_property_type'] = [
+    'id' => 'example_field',
+    'some_option' => 'foo',
+  ];
+}
+
+/**
+ * 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 processors, 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 \Drupal\search_api\IndexInterface $index
+ *   The search index on which items will be indexed.
+ * @param \Drupal\search_api\Item\ItemInterface[] $items
+ *   The items that will be indexed.
+ */
+function hook_search_api_index_items_alter(\Drupal\search_api\IndexInterface $index, array &$items) {
+  foreach ($items as $item_id => $item) {
+    list(, $raw_id) = \Drupal\search_api\Utility\Utility::splitCombinedId($item->getId());
+    if ($raw_id % 5 == 0) {
+      unset($items[$item_id]);
+    }
+  }
+  $arguments = [
+    '%index' => $index->label(),
+    '@ids' => implode(', ', array_keys($items)),
+  ];
+  drupal_set_message(t('Indexing items on index %index with the following IDs: @ids', $arguments));
+}
+
+/**
+ * React after items were indexed.
+ *
+ * @param \Drupal\search_api\IndexInterface $index
+ *   The used index.
+ * @param array $item_ids
+ *   An array containing the successfully indexed items' IDs.
+ */
+function hook_search_api_items_indexed(\Drupal\search_api\IndexInterface $index, array $item_ids) {
+  if ($index->isValidDatasource('entity:node')) {
+    // Note that this is just an example, and would only work if there are only
+    // nodes indexed in that index (and even then the printed IDs would probably
+    // not be as expected).
+    drupal_set_message(t('Nodes indexed: @ids.', implode(', ', $item_ids)));
+  }
+}
+
+/**
+ * Alter a search query before it gets executed.
+ *
+ * The hook is invoked after all enabled processors have preprocessed the query.
+ *
+ * @param \Drupal\search_api\Query\QueryInterface $query
+ *   The query that will be executed.
+ */
+function hook_search_api_query_alter(\Drupal\search_api\Query\QueryInterface &$query) {
+  // Do not run for queries with a certain tag.
+  if ($query->hasTag('example_tag')) {
+    return;
+  }
+  // Otherwise, exclude the node with ID 10 from the search results.
+  $fields = $query->getIndex()->getFields();
+  foreach ($query->getIndex()->getDatasources() as $datasource_id => $datasource) {
+    if ($datasource->getEntityTypeId() == 'node') {
+      if (isset($fields['nid'])) {
+        $query->addCondition('nid', 10, '<>');
+      }
+    }
+  }
+}
+
+/**
+ * Alter a search query with a specific tag before it gets executed.
+ *
+ * The hook is invoked after all enabled processors have preprocessed the query.
+ *
+ * @param \Drupal\search_api\Query\QueryInterface $query
+ *   The query that will be executed.
+ */
+function hook_search_api_query_TAG_alter(\Drupal\search_api\Query\QueryInterface &$query) {
+  // Exclude the node with ID 10 from the search results.
+  $fields = $query->getIndex()->getFields();
+  foreach ($query->getIndex()->getDatasources() as $datasource_id => $datasource) {
+    if ($datasource->getEntityTypeId() == 'node') {
+      if (isset($fields['nid'])) {
+        $query->addCondition('nid', 10, '<>');
+      }
+    }
+  }
+}
+
+/**
+ * Alter a search query's result set.
+ *
+ * The hook is invoked after all enabled processors have postprocessed the
+ * results.
+ *
+ * @param \Drupal\search_api\Query\ResultSetInterface $results
+ *   The search results to alter.
+ */
+function hook_search_api_results_alter(\Drupal\search_api\Query\ResultSetInterface &$results) {
+  $results->setExtraData('example_hook_invoked', microtime(TRUE));
+}
+
+/**
+ * Alter the result set of a search query with a specific tag.
+ *
+ * The hook is invoked after all enabled processors have postprocessed the
+ * results.
+ *
+ * @param \Drupal\search_api\Query\ResultSetInterface $results
+ *   The search results to alter.
+ */
+function hook_search_api_results_TAG_alter(\Drupal\search_api\Query\ResultSetInterface &$results) {
+  $results->setExtraData('example_hook_invoked', microtime(TRUE));
+}
+
+/**
+ * React when a search index was scheduled for reindexing.
+ *
+ * @param \Drupal\search_api\IndexInterface $index
+ *   The index scheduled for reindexing.
+ * @param bool $clear
+ *   Boolean indicating whether the index was also cleared.
+ */
+function hook_search_api_index_reindex(\Drupal\search_api\IndexInterface $index, $clear = FALSE) {
+  \Drupal\Core\Database\Database::getConnection()->insert('example_search_index_reindexed')
+    ->fields([
+      'index' => $index->id(),
+      'clear' => $clear,
+      'update_time' => \Drupal::time()->getRequestTime(),
+    ])
+    ->execute();
+}
+
+/**
+ * @} End of "addtogroup hooks".
+ */

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

@@ -0,0 +1,449 @@
+<?php
+
+/**
+ * @file
+ * Drush commands for Search API.
+ */
+
+use Drupal\search_api\ConsoleException;
+use Drupal\search_api\Utility\CommandHelper;
+
+/**
+ * Implements hook_drush_command().
+ */
+function search_api_drush_command() {
+  $items = [];
+
+  $index['index_id'] = dt('The machine name of an index');
+  $server['server_id'] = dt('The machine name of a server');
+
+  $items['search-api-list'] = [
+    'description' => 'List all search indexes.',
+    'examples' => [
+      'drush search-api-list' => dt('List all search indexes.'),
+      'drush sapi-l' => dt('Alias to list all search indexes.'),
+    ],
+    'aliases' => ['sapi-l'],
+  ];
+
+  $items['search-api-enable'] = [
+    'description' => 'Enable one or more disabled search indexes.',
+    'examples' => [
+      'drush search-api-enable node_index' => dt('Enable the search index with the ID @name.', ['@name' => 'node_index']),
+      'drush sapi-en node_index' => dt('Alias to enable the search index with the ID @name.', ['@name' => 'node_index']),
+    ],
+    'arguments' => $index,
+    'aliases' => ['sapi-en'],
+  ];
+
+  $items['search-api-enable-all'] = [
+    'description' => 'Enable all disabled search indexes.',
+    'examples' => [
+      'drush search-api-enable-all' => dt('Enable all disabled indexes.'),
+      'drush sapi-ena' => dt('Alias to enable all disabled indexes.'),
+    ],
+    'arguments' => [],
+    'aliases' => ['sapi-ena'],
+  ];
+
+  $items['search-api-disable'] = [
+    'description' => 'Disable one or more enabled search indexes.',
+    'examples' => [
+      'drush search-api-disable node_index' => dt('Disable the search index with the ID @name.', ['@name' => 'node_index']),
+      'drush sapi-dis node_index' => dt('Alias to disable the search index with the ID @name.', ['@name' => 'node_index']),
+    ],
+    'arguments' => $index,
+    'aliases' => ['sapi-dis'],
+  ];
+
+  $items['search-api-disable-all'] = [
+    'description' => 'Disable all enabled search indexes.',
+    'examples' => [
+      'drush search-api-disable-all' => dt('Disable all enabled indexes.'),
+      'drush sapi-disa' => dt('Alias to disable all enabled indexes.'),
+    ],
+    'arguments' => [],
+    'aliases' => ['sapi-disa'],
+  ];
+
+  $items['search-api-status'] = [
+    'description' => 'Show the status of one or all search indexes.',
+    'examples' => [
+      'drush search-api-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 node_index' => dt('Show the status of the search index with the ID @name.', ['@name' => 'node_index']),
+    ],
+    'arguments' => $index,
+    'aliases' => ['sapi-s'],
+  ];
+
+  $items['search-api-index'] = [
+    'description' => 'Index items for one or all enabled search indexes.',
+    'examples' => [
+      'drush search-api-index' => dt('Index all items for all enabled indexes.'),
+      'drush sapi-i' => dt('Alias to index all items for all enabled indexes.'),
+      'drush sapi-i node_index' => dt('Index all items for the index with the ID @name.', ['@name' => 'node_index']),
+      'drush sapi-i node_index 100' => dt('Index a maximum number of @limit items for the index with the ID @name.', ['@limit' => 100, '@name' => 'node_index']),
+      'drush sapi-i node_index 100 10' => dt('Index a maximum number of @limit items (@batch_size items per batch run) for the index with the ID @name.', ['@limit' => 100, '@batch_size' => 10, '@name' => 'node_index']),
+    ],
+    'options' => [
+      'limit' => dt('The number of items to index. 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 "!batch_size_label" setting of the index.', ['!batch_size_label' => dt('Cron batch size')]),
+    ],
+    'arguments' => $index,
+    'aliases' => ['sapi-i'],
+  ];
+
+  $items['search-api-reset-tracker'] = [
+    'description' => 'Force reindexing of one or all search indexes, without deleting existing index data.',
+    'examples' => [
+      'drush search-api-reindex' => dt('Schedule all search indexes for reindexing.'),
+      'drush sapi-r' => dt('Alias to schedule all search indexes for reindexing .'),
+      'drush sapi-r node_index' => dt('Schedule the search index with the ID @name for reindexing.', ['@name' => 'node_index']),
+    ],
+    'options' => [
+      'entity-types' => [
+        'description' => dt('List of entity type ids to reset tracker for.'),
+        'example_value' => 'user,node',
+      ],
+    ],
+    'arguments' => $index,
+    'aliases' => [
+      'search-api-mark-all',
+      'search-api-reindex',
+      'sapi-r',
+    ],
+  ];
+
+  $items['search-api-clear'] = [
+    'description' => 'Clear one or all search indexes and mark them for reindexing.',
+    'examples' => [
+      'drush search-api-clear' => dt('Clear all search indexes.'),
+      'drush sapi-c' => dt('Alias to clear all search indexes.'),
+      'drush sapi-c node_index' => dt('Clear the search index with the ID @name.', ['@name' => 'node_index']),
+    ],
+    'arguments' => $index,
+    'aliases' => ['sapi-c'],
+  ];
+
+  $items['search-api-search'] = [
+    'description' => 'Search for a keyword or phrase in a given index.',
+    'examples' => [
+      'drush search-api-search node_index title' => dt('Search for "title" inside the "node_index" index.'),
+      'drush sapi-search node_index title' => dt('Alias to search for "title" inside the "node_index" index.'),
+    ],
+    'arguments' => $index + [
+      'keyword' => dt('The keyword to look for.'),
+    ],
+    'aliases' => ['sapi-search'],
+  ];
+
+  $items['search-api-server-list'] = [
+    'description' => 'List all search servers.',
+    'examples' => [
+      'drush search-api-server-list' => dt('List all search servers.'),
+      'drush sapi-sl' => dt('Alias to list all search servers.'),
+    ],
+    'aliases' => ['sapi-sl'],
+  ];
+
+  $items['search-api-server-enable'] = [
+    'description' => 'Enable a search server.',
+    'examples' => [
+      'drush search-api-server-e my_solr_server' => dt('Enable the @server search server.', ['@server' => 'my_solr_server']),
+      'drush sapi-se my_solr_server' => dt('Alias to enable the @server search server.', ['@server' => 'my_solr_server']),
+    ],
+    'arguments' => $server,
+    'aliases' => ['sapi-se'],
+  ];
+
+  $items['search-api-server-disable'] = [
+    'description' => 'Disable a search server.',
+    'examples' => [
+      'drush search-api-server-disable' => dt('Disable the @server search server.', ['@server' => 'my_solr_server']),
+      'drush sapi-sd' => dt('Alias to disable the @server search server.', ['@server' => 'my_solr_server']),
+    ],
+    'arguments' => $server,
+    'aliases' => ['sapi-sd'],
+  ];
+
+  $items['search-api-server-clear'] = [
+    'description' => 'Clear all search indexes on the search server and mark them for reindexing.',
+    'examples' => [
+      'drush search-api-server-clear' => dt('Clear all search indexes on the search server @server.', ['@server' => 'my_solr_server']),
+      'drush sapi-sc' => dt('Alias to clear all search indexes on the search server @server.', ['@server' => 'my_solr_server']),
+    ],
+    'arguments' => $server,
+    'aliases' => ['sapi-sc'],
+  ];
+
+  $items['search-api-set-index-server'] = [
+    'description' => 'Set the search server used by a given index.',
+    'examples' => [
+      'drush search-api-set-index-server default_node_index my_solr_server' => dt('Set the @index index to used the @server server.', ['@index' => 'default_node_index', '@server' => 'my_solr_server']),
+      'drush sapi-sis default_node_index my_solr_server' => dt('Alias to set the @index index to used the @server server.', ['@index' => 'default_node_index', '@server' => 'my_solr_server']),
+    ],
+    'arguments' => $index + $server,
+    'aliases' => ['sapi-sis'],
+  ];
+
+  return $items;
+}
+
+/**
+ * Prints a list of all search indexes.
+ */
+function drush_search_api_list() {
+  $command_helper = _search_api_drush_command_helper();
+  $rows[] = [
+    dt('ID'),
+    dt('Name'),
+    dt('Server'),
+    dt('Type'),
+    dt('Status'),
+    dt('Limit'),
+  ];
+  $rows += $command_helper->indexListCommand();
+  foreach ($rows as &$row) {
+    $row['types'] = is_array($row['types']) ? implode(', ', $row['types']) : $row['types'];
+    $row['typeNames'] = is_array($row['types']) ? implode(', ', $row['typeNames']) : $row['types'];
+  }
+  drush_print_table($rows);
+}
+
+/**
+ * Enables one or more search indexes.
+ *
+ * @param string|null $index_id
+ *   The ID of a search index to enable. Or NULL (only used internally) to
+ *   enable all disabled indexes.
+ */
+function drush_search_api_enable($index_id = NULL) {
+  $command_helper = _search_api_drush_command_helper();
+  try {
+    $command_helper->enableIndexCommand([$index_id]);
+  }
+  catch (ConsoleException $exception) {
+    drush_set_error($exception->getMessage());
+  }
+}
+
+/**
+ * Enables all search indexes.
+ */
+function drush_search_api_enable_all() {
+  $command_helper = _search_api_drush_command_helper();
+  try {
+    $command_helper->enableIndexCommand();
+  }
+  catch (ConsoleException $exception) {
+    drush_set_error($exception->getMessage());
+  }
+}
+
+/**
+ * Disables one or more search indexes.
+ *
+ * @param string|null $index_id
+ *   The ID of a search index to disable. Or NULL (only used internally) to
+ *   disable all enabled indexes.
+ */
+function drush_search_api_disable($index_id = NULL) {
+  $command_helper = _search_api_drush_command_helper();
+  try {
+    $command_helper->disableIndexCommand([$index_id]);
+  }
+  catch (ConsoleException $exception) {
+    drush_set_error($exception->getMessage());
+  }
+}
+
+/**
+ * Disables all search indexes.
+ */
+function drush_search_api_disable_all() {
+  $command_helper = _search_api_drush_command_helper();
+  try {
+    $command_helper->disableIndexCommand();
+  }
+  catch (ConsoleException $exception) {
+    drush_set_error($exception->getMessage());
+  }
+}
+
+/**
+ * Displays the status of one or all search indexes.
+ *
+ * @param string|null $index_id
+ *   (optional) The ID of the search index whose status should be displayed, or
+ *   NULL to display the status of all search indexes.
+ */
+function drush_search_api_status($index_id = NULL) {
+  $command_helper = _search_api_drush_command_helper();
+  $rows[] = [
+    dt('ID'),
+    dt('Name'),
+    dt('% Complete'),
+    dt('Indexed'),
+    dt('Total'),
+  ];
+  $rows += $command_helper->indexStatusCommand([$index_id]);
+
+  drush_print_table($rows);
+}
+
+/**
+ * Indexes items.
+ *
+ * @param string|null $index_id
+ *   (optional) The index ID for which items should be indexed, or NULL to index
+ *   items on all indexes.
+ */
+function drush_search_api_index($index_id = NULL) {
+  $command_helper = _search_api_drush_command_helper();
+  $limit = drush_get_option('limit');
+  $batch_size = drush_get_option('batch-size');
+
+  $batch_set = $command_helper->indexItemsToIndexCommand([$index_id], $limit, $batch_size);
+  if ($batch_set) {
+    drush_backend_batch_process();
+  }
+}
+
+/**
+ * Schedules a search index for reindexing.
+ *
+ * @param string|null $index_id
+ *   (optional) The index ID for which items should be reindexed, or NULL to
+ *   reindex all search indexes.
+ */
+function drush_search_api_reset_tracker($index_id = NULL) {
+  $command_helper = _search_api_drush_command_helper();
+  $entity_types = drush_get_option_list('entity-types');
+  $command_helper->resetTrackerCommand([$index_id], $entity_types);
+}
+
+/**
+ * Clears a search index.
+ *
+ * @param string|null $index_id
+ *   (optional) The ID of the search index which should be cleared, or NULL to
+ *   clear all search indexes.
+ */
+function drush_search_api_clear($index_id = NULL) {
+  $command_helper = _search_api_drush_command_helper();
+  $command_helper->clearIndexCommand([$index_id]);
+}
+
+/**
+ * Executes a simple keyword search and displays the results in a table.
+ *
+ * @param string $index_id
+ *   The ID of the index being searched.
+ * @param string $keyword
+ *   The search keyword.
+ */
+function drush_search_api_search($index_id, $keyword) {
+  $command_helper = _search_api_drush_command_helper();
+  $rows = $command_helper->searchIndexCommand($index_id, $keyword);
+  drush_print_table($rows);
+}
+
+/**
+ * Lists all available search servers.
+ */
+function drush_search_api_server_list() {
+  $command_helper = _search_api_drush_command_helper();
+  $rows[] = [
+    dt('ID'),
+    dt('Name'),
+    dt('Status'),
+  ];
+
+  try {
+    $rows += $command_helper->serverListCommand();
+  }
+  catch (ConsoleException $exception) {
+    drush_print($exception->getMessage());
+  }
+
+  drush_print_table($rows);
+}
+
+/**
+ * Enables a search server.
+ *
+ * @param string $server_id
+ *   The ID of the server to enable.
+ */
+function drush_search_api_server_enable($server_id = NULL) {
+  $command_helper = _search_api_drush_command_helper();
+  try {
+    $command_helper->enableServerCommand($server_id);
+  }
+  catch (ConsoleException $exception) {
+    drush_print($exception->getMessage());
+  }
+}
+
+/**
+ * Disables a search server.
+ *
+ * @param string $server_id
+ *   The ID of the server to disable.
+ */
+function drush_search_api_server_disable($server_id = NULL) {
+  $command_helper = _search_api_drush_command_helper();
+  try {
+    $command_helper->disableServerCommand($server_id);
+  }
+  catch (ConsoleException $exception) {
+    drush_print($exception->getMessage());
+  }
+}
+
+/**
+ * Clears all search indexes on the server and marks them for reindexing.
+ *
+ * @param string $server_id
+ *   The ID of the server to clear all search indexes.
+ */
+function drush_search_api_server_clear($server_id = NULL) {
+  $command_helper = _search_api_drush_command_helper();
+  try {
+    $command_helper->clearServerCommand($server_id);
+  }
+  catch (ConsoleException $exception) {
+    drush_print($exception->getMessage());
+  }
+}
+
+/**
+ * Sets the server for a given index.
+ *
+ * @param string $index_id
+ *   The ID of the index whose server should be changed.
+ * @param string $server_id
+ *   The ID of the new server for the index.
+ */
+function drush_search_api_set_index_server($index_id = NULL, $server_id = NULL) {
+  $command_helper = _search_api_drush_command_helper();
+  try {
+    $command_helper->setIndexServerCommand($index_id, $server_id);
+  }
+  catch (ConsoleException $exception) {
+    drush_print($exception->getMessage());
+  }
+}
+
+/**
+ * Returns an instance of the command helper.
+ *
+ * @return \Drupal\search_api\Utility\CommandHelper
+ *   An instance of the command helper class.
+ */
+function _search_api_drush_command_helper() {
+  $command_helper = new CommandHelper(\Drupal::entityTypeManager(), \Drupal::moduleHandler(), 'dt');
+  $command_helper->setLogger(\Drupal::logger('search_api'));
+  return $command_helper;
+}

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

@@ -0,0 +1,14 @@
+type: module
+name: 'Search API'
+description: 'Provides a generic framework for modules offering search capabilities.'
+package: Search
+# core: 8.x
+configure: search_api.overview
+dependencies:
+  - system (>=8.4)
+
+# Information added by Drupal.org packaging script on 2018-02-23
+version: '8.x-1.7'
+core: '8.x'
+project: 'search_api'
+datestamp: 1519387691

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

@@ -0,0 +1,281 @@
+<?php
+
+/**
+ * @file
+ * Install, update and uninstall functions for the Search API module.
+ */
+
+use Drupal\Component\Render\FormattableMarkup;
+use Drupal\Core\Link;
+use Drupal\search_api\Entity\Server;
+use Drupal\Core\Url;
+
+/**
+ * Implements hook_schema().
+ */
+function search_api_schema() {
+  $schema['search_api_item'] = [
+    'description' => 'Stores the items which should be indexed for each index, and their state.',
+    'fields' => [
+      'index_id' => [
+        'description' => 'The ID of the index this item belongs to',
+        'type' => 'varchar',
+        'length' => 50,
+        'not null' => TRUE,
+      ],
+      'datasource' => [
+        'description' => 'The plugin ID of the datasource this item belongs to',
+        'type' => 'varchar',
+        'length' => 50,
+        'not null' => TRUE,
+      ],
+      'item_id' => [
+        'description' => 'The unique identifier of this item',
+        'type' => 'varchar',
+        'length' => 150,
+        'not null' => TRUE,
+      ],
+      'changed' => [
+        'description' => 'A timestamp indicating when the item was last changed',
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+      ],
+      'status' => [
+        'description' => 'Boolean indicating the reindexation status, "1" when we need to reindex, "0" otherwise',
+        'type' => 'int',
+        'not null' => TRUE,
+      ],
+    ],
+    'indexes' => [
+      'indexing' => ['index_id', 'status', 'changed', 'item_id'],
+    ],
+    'primary key' => ['index_id', 'item_id'],
+  ];
+
+  return $schema;
+}
+
+/**
+ * Implements hook_uninstall().
+ */
+function search_api_uninstall() {
+  \Drupal::state()->delete('search_api_use_tracking_batch');
+  foreach (\Drupal::configFactory()->listAll('search_api.index.') as $index_id) {
+    \Drupal::state()->delete("search_api.index.$index_id.has_reindexed");
+  }
+}
+
+/**
+ * Implements hook_requirements().
+ */
+function search_api_requirements($phase) {
+  if ($phase == 'runtime') {
+    $requirements = [];
+    $message = _search_api_search_module_warning();
+    if ($message) {
+      $requirements += [
+        'search_api_core_search' => [
+          'title' => t('Search API'),
+          'value' => $message,
+          'severity' => REQUIREMENT_WARNING,
+        ],
+      ];
+    }
+
+    /** @var \Drupal\search_api\ServerInterface[] $servers */
+    $servers = Server::loadMultiple();
+    $unavailable_servers = [];
+    foreach ($servers as $server) {
+      if ($server->status() && !$server->isAvailable()) {
+        $unavailable_servers[] = $server->label();
+      }
+    }
+    if (!empty($unavailable_servers)) {
+      $requirements += [
+        'search_api_server_unavailable' => [
+          'title' => t('Search API'),
+          'value' => \Drupal::translation()->formatPlural(
+            count($unavailable_servers),
+            'The search server "@servers" is currently not available',
+            'The following search servers are not available: @servers',
+            ['@servers' => implode(', ', $unavailable_servers)]
+          ),
+          'severity' => REQUIREMENT_ERROR
+        ]
+      ];
+    }
+
+    $pending_tasks = \Drupal::getContainer()
+      ->get('search_api.task_manager')
+      ->getTasksCount();
+    if ($pending_tasks) {
+      $args['@link'] = '';
+      $url = Url::fromRoute('search_api.execute_tasks');
+      if ($url->access()) {
+        $link = new Link(t('Execute now'), $url);
+        $link = $link->toString();
+        $args['@link'] = $link;
+        $args['@link'] = new FormattableMarkup(' (@link)', $args);
+      }
+
+      $requirements['search_api_pending_tasks'] = [
+        'title' => t('Search API'),
+        'value' => \Drupal::translation()->formatPlural(
+          $pending_tasks,
+          'There is @count pending Search API task. @link',
+          'There are @count pending Search API tasks. @link',
+          $args
+        ),
+        'severity' => REQUIREMENT_WARNING,
+      ];
+    }
+
+    return $requirements;
+  }
+  return [];
+}
+
+/**
+ * Adapts index config schema to remove an unnecessary layer for plugins.
+ */
+function search_api_update_8101() {
+  // This update function updates search indexes for the change from
+  // https://www.drupal.org/node/2656052.
+  $config_factory = \Drupal::configFactory();
+  $plugin_types = [
+    'processor',
+    'datasource',
+    'tracker',
+  ];
+
+  foreach ($config_factory->listAll('search_api.index.') as $index_id) {
+    $index = $config_factory->getEditable($index_id);
+    $changed = FALSE;
+
+    foreach ($plugin_types as $plugin_type) {
+      $property = $plugin_type . '_settings';
+      $plugins = $index->get($property);
+      foreach ($plugins as $id => $config) {
+        if (isset($config['plugin_id']) && isset($config['settings'])) {
+          $changed = TRUE;
+          $plugins[$id] = $config['settings'];
+        }
+      }
+      $index->set($property, $plugins);
+    }
+
+    if ($changed) {
+      // Mark the resulting configuration as trusted data. This avoids issues
+      // with future schema changes.
+      $index->save(TRUE);
+    }
+  }
+
+  return t('Index config schema updated.');
+}
+
+/**
+ * Removes unsupported cache plugins from Search API views.
+ */
+function search_api_update_8102() {
+  $config_factory = \Drupal::configFactory();
+  $changed = [];
+
+  foreach ($config_factory->listAll('views.view.') as $view_config_name) {
+    $view = $config_factory->getEditable($view_config_name);
+    $displays = $view->get('display');
+
+    if ($displays['default']['display_options']['query']['type'] === 'search_api_query') {
+      $change = FALSE;
+      foreach ($displays as $id => $display) {
+        if (!empty($display['display_options']['cache']['type']) && in_array($display['display_options']['cache']['type'], ['tag', 'time'])
+        ) {
+          $displays[$id]['display_options']['cache']['type'] = 'none';
+          $change = TRUE;
+        }
+      }
+
+      if ($change) {
+        $view->set('display', $displays);
+        // Mark the resulting configuration as trusted data. This avoids issues
+        // with future schema changes.
+        $view->save(TRUE);
+        $changed[] = $view->get('id');
+      }
+    }
+  }
+
+  if (!empty($changed)) {
+    return \Drupal::translation()->translate('Removed incompatible cache options for the following Search API-based views: @ids', ['@ids' => implode(', ', array_unique($changed))]);
+  }
+
+  return NULL;
+}
+
+/**
+ * Switches from the old "Node status" to the new "Entity status" processor.
+ */
+function search_api_update_8103() {
+  // This update function updates search indexes for the change from
+  // https://www.drupal.org/node/2491175.
+  $config_factory = \Drupal::configFactory();
+
+  foreach ($config_factory->listAll('search_api.index.') as $index_id) {
+    $index = $config_factory->getEditable($index_id);
+    $processors = $index->get('processor_settings');
+
+    if (isset($processors['node_status'])) {
+      $processors['entity_status'] = $processors['node_status'];
+      unset($processors['node_status']);
+      $index->set('processor_settings', $processors);
+      // Mark the resulting configuration as trusted data. This avoids issues
+      // with future schema changes.
+      $index->save(TRUE);
+    }
+  }
+
+  // Clear the processor plugin cache so that if anything else indirectly tries
+  // to update Search API-related configuration, the plugin helper gets the most
+  // up-to-date plugin definitions.
+  \Drupal::getContainer()
+    ->get('plugin.manager.search_api.processor')
+    ->clearCachedDefinitions();
+
+  return t('Switched from old "Node status" to new "Entity status" processor.');
+}
+
+/**
+ * Update Views to use the time-based cache plugin for Search API.
+ */
+function search_api_update_8104() {
+  $config_factory = \Drupal::configFactory();
+  $changed = [];
+
+  foreach ($config_factory->listAll('views.view.') as $view_config_name) {
+    $view = $config_factory->getEditable($view_config_name);
+    $displays = $view->get('display');
+
+    $updated = FALSE;
+    foreach ($displays as $id => $display) {
+      if (!empty($display['display_options']['cache']['type']) && $display['display_options']['cache']['type'] === 'search_api') {
+        $displays[$id]['display_options']['cache']['type'] = 'search_api_time';
+        $updated = TRUE;
+      }
+    }
+
+    if ($updated) {
+      $view->set('display', $displays);
+      // Mark the resulting configuration as trusted data. This avoids issues
+      // with future schema changes.
+      $view->save(TRUE);
+      $changed[] = $view->get('id');
+    }
+  }
+
+  if (!empty($changed)) {
+    return \Drupal::translation()->translate('The following views have been updated to use the time-based cache plugin: @ids', ['@ids' => implode(', ', array_unique($changed))]);
+  }
+
+  return NULL;
+}

+ 13 - 0
sites/all/modules/contrib/search/search_api/search_api.libraries.yml

@@ -0,0 +1,13 @@
+drupal.search_api.admin_css:
+  version: VERSION
+  css:
+    theme:
+      css/search_api.admin.css: {}
+drupal.search_api.processors:
+  version: VERSION
+  js:
+    js/search_api.processors.js: {}
+  dependencies:
+    - core/drupal
+    - core/jquery
+    - core/jquery.once

+ 15 - 0
sites/all/modules/contrib/search/search_api/search_api.links.action.yml

@@ -0,0 +1,15 @@
+entity.search_api_server.add_form:
+  route_name: entity.search_api_server.add_form
+  title: 'Add server'
+  appears_on:
+    - search_api.overview
+entity.search_api_index.add_form:
+  route_name: entity.search_api_index.add_form
+  title: 'Add index'
+  appears_on:
+    - search_api.overview
+search_api.execute_tasks:
+  route_name: search_api.execute_tasks
+  title: 'Execute pending tasks'
+  appears_on:
+    - search_api.overview

+ 5 - 0
sites/all/modules/contrib/search/search_api/search_api.links.menu.yml

@@ -0,0 +1,5 @@
+search_api.overview:
+  title: Search API
+  description: 'Create and configure search indexes and servers.'
+  route_name: search_api.overview
+  parent: system.admin_config_search

+ 27 - 0
sites/all/modules/contrib/search/search_api/search_api.links.task.yml

@@ -0,0 +1,27 @@
+entity.search_api_server.canonical:
+  route_name: entity.search_api_server.canonical
+  base_route: entity.search_api_server.canonical
+  title: 'View'
+entity.search_api_server.edit_form:
+  route_name: entity.search_api_server.edit_form
+  base_route: entity.search_api_server.canonical
+  title: 'Edit'
+entity.search_api_index.canonical:
+  route_name: entity.search_api_index.canonical
+  base_route: entity.search_api_index.canonical
+  title: 'View'
+entity.search_api_index.edit_form:
+  route_name: entity.search_api_index.edit_form
+  base_route: entity.search_api_index.canonical
+  title: 'Edit'
+  weight: 5
+entity.search_api_index.fields:
+  title: 'Fields'
+  route_name: entity.search_api_index.fields
+  base_route: entity.search_api_index.canonical
+  weight: 10
+entity.search_api_index.processors:
+  route_name: entity.search_api_index.processors
+  base_route: entity.search_api_index.canonical
+  title: 'Processors'
+  weight: 20

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

@@ -0,0 +1,671 @@
+<?php
+
+/**
+ * @file
+ * Provides a rich framework for creating searches.
+ */
+
+use Drupal\comment\Entity\Comment;
+use Drupal\Core\Config\ConfigImporter;
+use Drupal\Core\Entity\ContentEntityInterface;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Url;
+use Drupal\node\NodeInterface;
+use Drupal\search_api\Entity\Index;
+use Drupal\search_api\IndexInterface;
+use Drupal\search_api\Plugin\search_api\datasource\ContentEntity;
+use Drupal\search_api\Plugin\search_api\datasource\ContentEntityTaskManager;
+use Drupal\search_api\Plugin\search_api\datasource\EntityDatasourceInterface;
+use Drupal\search_api\Plugin\views\argument\SearchApiTerm as TermArgument;
+use Drupal\search_api\Plugin\views\filter\SearchApiTerm as TermFilter;
+use Drupal\search_api\Plugin\views\query\SearchApiQuery;
+use Drupal\search_api\SearchApiException;
+use Drupal\search_api\Task\IndexTaskManager;
+use Drupal\views\ViewEntityInterface;
+
+/**
+ * Implements hook_help().
+ */
+function search_api_help($route_name) {
+  switch ($route_name) {
+    case 'search_api.overview':
+      $message = t('Below is a list of indexes grouped by the server they are associated with. A server is the definition of the actual indexing, querying and storage engine (for example, an Apache Solr server, the database, …). An index defines the indexed content (for example, all content and all comments on "Article" posts).');
+
+      $search_module_warning = _search_api_search_module_warning();
+      if ($search_module_warning) {
+        $message = "<p>$message</p><p>$search_module_warning</p>";
+      }
+      return $message;
+  }
+  return NULL;
+}
+
+/**
+ * Implements hook_cron().
+ *
+ * This will first execute pending tasks (if there are any). 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 cron_worker_runtime config setting
+ * (defaulting to 15 seconds), but will at least index one batch of items on
+ * each index.
+ */
+function search_api_cron() {
+  // Execute pending server tasks.
+  \Drupal::getContainer()->get('search_api.server_task_manager')->execute();
+
+  // Load all enabled, not read-only indexes.
+  $conditions = [
+    'status' => TRUE,
+  ];
+  $index_storage = \Drupal::entityTypeManager()->getStorage('search_api_index');
+  /** @var \Drupal\search_api\IndexInterface[] $indexes */
+  $indexes = $index_storage->loadByProperties($conditions);
+  if (!$indexes) {
+    return;
+  }
+
+  // Add items to the tracking system for all indexes for which this hasn't
+  // happened yet.
+  $task_manager = \Drupal::getContainer()->get('search_api.task_manager');
+  foreach ($indexes as $index_id => $index) {
+    $conditions = [
+      'type' => IndexTaskManager::TRACK_ITEMS_TASK_TYPE,
+      'index_id' => $index_id,
+    ];
+    $task_manager->executeSingleTask($conditions);
+
+    // Filter out read-only indexes here, since we want to have tracking but not
+    // index items for them.
+    if ($index->isReadOnly()) {
+      unset($indexes[$index_id]);
+    }
+  }
+
+  // Now index items.
+  // Remember servers which threw an exception.
+  $ignored_servers = [];
+
+  // Continue indexing, one batch from each index, until the time is up, but at
+  // least index one batch per index.
+  $settings = \Drupal::config('search_api.settings');
+  $default_cron_limit = $settings->get('default_cron_limit');
+  $end = time() + $settings->get('cron_worker_runtime');
+  $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->getServerId()])) {
+        continue;
+      }
+
+      $limit = $index->getOption('cron_limit', $default_cron_limit);
+      $num = 0;
+      if ($limit) {
+        try {
+          $num = $index->indexItems($limit);
+          if ($num) {
+            $variables = [
+              '@num' => $num,
+              '%name' => $index->label(),
+            ];
+            \Drupal::service('logger.channel.search_api')->info('Indexed @num items for index %name.', $variables);
+          }
+        }
+        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->getServerId()] = TRUE;
+          $vars['%index'] = $index->label();
+          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_hook_info().
+ */
+function search_api_hook_info() {
+  $hooks = [
+    'search_api_backend_info_alter',
+    'search_api_server_features_alter',
+    'search_api_datasource_info_alter',
+    'search_api_processor_info_alter',
+    'search_api_data_type_info_alter',
+    'search_api_parse_mode_info_alter',
+    'search_api_tracker_info_alter',
+    'search_api_displays_alter',
+    'search_api_field_type_mapping_alter',
+    'search_api_views_handler_mapping_alter',
+    'search_api_views_field_handler_mapping_alter',
+    'search_api_index_items_alter',
+    'search_api_items_indexed',
+    'search_api_query_alter',
+    // Unfortunately, it's not possible to add hook infos for hooks with
+    // "wildcards".
+    // 'search_api_query_TAG_alter',
+    'search_api_results_alter',
+    // 'search_api_results_TAG_alter',
+    'search_api_index_reindex',
+  ];
+  $info = [
+    'group' => 'search_api',
+  ];
+  return array_fill_keys($hooks, $info);
+}
+
+/**
+ * Implements hook_config_import_steps_alter().
+ */
+function search_api_config_import_steps_alter(&$sync_steps, ConfigImporter $config_importer) {
+  $new = $config_importer->getUnprocessedConfiguration('create');
+  $changed = $config_importer->getUnprocessedConfiguration('update');
+  $new_or_changed = array_merge($new, $changed);
+  $prefix = \Drupal::entityTypeManager()->getDefinition('search_api_index')->getConfigPrefix() . '.';
+  $prefix_length = strlen($prefix);
+  foreach ($new_or_changed as $config_id) {
+    if (substr($config_id, 0, $prefix_length) === $prefix) {
+      $sync_steps[] = ['Drupal\search_api\Task\IndexTaskManager', 'processIndexTasks'];
+    }
+  }
+}
+
+/**
+ * Implements hook_entity_insert().
+ *
+ * Adds entries for all languages of the new entity to the tracking table for
+ * each index that tracks entities of this type.
+ *
+ * By setting the $entity->search_api_skip_tracking property to a true-like
+ * value before this hook is invoked, you can prevent this behavior and make the
+ * Search API ignore this new entity.
+ *
+ * Note that this function implements tracking only on behalf of the "Content
+ * Entity" datasource defined in this module, not for entity-based datasources
+ * in general. Datasources defined by other modules still have to implement
+ * their own mechanism for tracking new/updated/deleted entities.
+ *
+ * @see \Drupal\search_api\Plugin\search_api\datasource\ContentEntity
+ */
+function search_api_entity_insert(EntityInterface $entity) {
+  // Check if the entity is a content entity.
+  if (!($entity instanceof ContentEntityInterface) || $entity->search_api_skip_tracking) {
+    return;
+  }
+  $indexes = ContentEntity::getIndexesForEntity($entity);
+  if (!$indexes) {
+    return;
+  }
+
+  // Compute the item IDs for all languages of the entity.
+  $item_ids = [];
+  $entity_id = $entity->id();
+  foreach (array_keys($entity->getTranslationLanguages()) as $langcode) {
+    $item_ids[] = $entity_id . ':' . $langcode;
+  }
+  $datasource_id = 'entity:' . $entity->getEntityTypeId();
+  foreach ($indexes as $index) {
+    $filtered_item_ids = ContentEntity::filterValidItemIds($index, $datasource_id, $item_ids);
+    $index->trackItemsInserted($datasource_id, $filtered_item_ids);
+  }
+}
+
+/**
+ * Implements hook_entity_update().
+ *
+ * Updates the corresponding tracking table entries for each index that tracks
+ * this entity.
+ *
+ * Also takes care of new or deleted translations.
+ *
+ * By setting the $entity->search_api_skip_tracking property to a true-like
+ * value before this hook is invoked, you can prevent this behavior and make the
+ * Search API ignore this update.
+ *
+ * Note that this function implements tracking only on behalf of the "Content
+ * Entity" datasource defined in this module, not for entity-based datasources
+ * in general. Datasources defined by other modules still have to implement
+ * their own mechanism for tracking new/updated/deleted entities.
+ *
+ * @see \Drupal\search_api\Plugin\search_api\datasource\ContentEntity
+ */
+function search_api_entity_update(EntityInterface $entity) {
+  // Check if the entity is a content entity.
+  if (!($entity instanceof ContentEntityInterface) || $entity->search_api_skip_tracking) {
+    return;
+  }
+  $indexes = ContentEntity::getIndexesForEntity($entity);
+  if (!$indexes) {
+    return;
+  }
+
+  // Compare old and new languages for the entity to identify inserted,
+  // updated and deleted translations (and, therefore, search items).
+  $entity_id = $entity->id();
+  $inserted_item_ids = [];
+  $updated_item_ids = $entity->getTranslationLanguages();
+  $deleted_item_ids = [];
+  $old_translations = $entity->original->getTranslationLanguages();
+  foreach ($old_translations as $langcode => $language) {
+    if (!isset($updated_item_ids[$langcode])) {
+      $deleted_item_ids[] = $langcode;
+    }
+  }
+  foreach ($updated_item_ids as $langcode => $language) {
+    if (!isset($old_translations[$langcode])) {
+      unset($updated_item_ids[$langcode]);
+      $inserted_item_ids[] = $langcode;
+    }
+  }
+
+  $datasource_id = 'entity:' . $entity->getEntityTypeId();
+  $combine_id = function ($langcode) use ($entity_id) {
+    return $entity_id . ':' . $langcode;
+  };
+  $inserted_item_ids = array_map($combine_id, $inserted_item_ids);
+  $updated_item_ids = array_map($combine_id, array_keys($updated_item_ids));
+  $deleted_item_ids = array_map($combine_id, $deleted_item_ids);
+  foreach ($indexes as $index) {
+    if ($inserted_item_ids) {
+      $filtered_item_ids = ContentEntity::filterValidItemIds($index, $datasource_id, $inserted_item_ids);
+      $index->trackItemsInserted($datasource_id, $filtered_item_ids);
+    }
+    if ($updated_item_ids) {
+      $index->trackItemsUpdated($datasource_id, $updated_item_ids);
+    }
+    if ($deleted_item_ids) {
+      $index->trackItemsDeleted($datasource_id, $deleted_item_ids);
+    }
+  }
+}
+
+/**
+ * Implements hook_entity_delete().
+ *
+ * Deletes all entries for this entity from the tracking table for each index
+ * that tracks this entity type.
+ *
+ * By setting the $entity->search_api_skip_tracking property to a true-like
+ * value before this hook is invoked, you can prevent this behavior and make the
+ * Search API ignore this deletion. (Note that this might lead to stale data in
+ * the tracking table or on the server, since the item will not removed from
+ * there (if it has been added before).)
+ *
+ * Note that this function implements tracking only on behalf of the "Content
+ * Entity" datasource defined in this module, not for entity-based datasources
+ * in general. Datasources defined by other modules still have to implement
+ * their own mechanism for tracking new/updated/deleted entities.
+ *
+ * @see \Drupal\search_api\Plugin\search_api\datasource\ContentEntity
+ */
+function search_api_entity_delete(EntityInterface $entity) {
+  // Check if the entity is a content entity.
+  if (!($entity instanceof ContentEntityInterface) || $entity->search_api_skip_tracking) {
+    return;
+  }
+  $indexes = ContentEntity::getIndexesForEntity($entity);
+  if (!$indexes) {
+    return;
+  }
+
+  // Remove the search items for all the entity's translations.
+  $item_ids = [];
+  $entity_id = $entity->id();
+  foreach (array_keys($entity->getTranslationLanguages()) as $langcode) {
+    $item_ids[] = $entity_id . ':' . $langcode;
+  }
+  $datasource_id = 'entity:' . $entity->getEntityTypeId();
+  foreach ($indexes as $index) {
+    $index->trackItemsDeleted($datasource_id, $item_ids);
+  }
+}
+
+/**
+ * Implements hook_node_access_records_alter().
+ *
+ * Marks the node and its comments changed for indexes that use the "Content
+ * access" processor.
+ */
+function search_api_node_access_records_alter(array &$grants, NodeInterface $node) {
+  /** @var \Drupal\search_api\IndexInterface $index */
+  foreach (Index::loadMultiple() as $index) {
+    if (!$index->hasValidTracker() || !$index->status()) {
+      continue;
+    }
+    if (!$index->isValidProcessor('content_access')) {
+      continue;
+    }
+
+    foreach ($index->getDatasources() as $datasource_id => $datasource) {
+      switch ($datasource->getEntityTypeId()) {
+        case 'node':
+          // Don't index the node if search_api_skip_tracking is set on it.
+          if ($node->search_api_skip_tracking) {
+            continue 2;
+          }
+          $item_id = $datasource->getItemId($node->getTypedData());
+          if ($item_id !== NULL) {
+            $index->trackItemsUpdated($datasource_id, [$item_id]);
+          }
+          break;
+
+        case 'comment':
+          if (!isset($comments)) {
+            $entity_query = \Drupal::entityQuery('comment');
+            $entity_query->condition('entity_id', (int) $node->id());
+            $entity_query->condition('entity_type', 'node');
+            $comment_ids = $entity_query->execute();
+            /** @var \Drupal\comment\CommentInterface[] $comments */
+            $comments = Comment::loadMultiple($comment_ids);
+          }
+          $item_ids = [];
+          foreach ($comments as $comment) {
+            $item_id = $datasource->getItemId($comment->getTypedData());
+            if ($item_id !== NULL) {
+              $item_ids[] = $item_id;
+            }
+          }
+          if ($item_ids) {
+            $index->trackItemsUpdated($datasource_id, $item_ids);
+          }
+          break;
+      }
+    }
+  }
+}
+
+/**
+ * Implements hook_theme().
+ */
+function search_api_theme() {
+  return [
+    'search_api_admin_fields_table' => [
+      'render element' => 'element',
+      'function' => 'theme_search_api_admin_fields_table',
+      'file' => 'search_api.theme.inc',
+    ],
+    'search_api_admin_data_type_table' => [
+      'variables' => [
+        'data_types' => [],
+        'fallback_mapping' => []
+      ],
+      'function' => 'theme_search_api_admin_data_type_table',
+      'file' => 'search_api.theme.inc',
+    ],
+    'search_api_form_item_list' => [
+      'render element' => 'element',
+      'function' => 'theme_search_api_form_item_list',
+      'file' => 'search_api.theme.inc',
+    ],
+    'search_api_server' => [
+      'variables' => ['server' => NULL],
+      'function' => 'theme_search_api_server',
+      'file' => 'search_api.theme.inc',
+    ],
+    'search_api_index' => [
+      'variables' => [
+        'index' => NULL,
+        'server_count' => NULL,
+        'server_count_error' => NULL,
+      ],
+      'function' => 'theme_search_api_index',
+      'file' => 'search_api.theme.inc',
+    ],
+  ];
+}
+
+/**
+ * Implements hook_ENTITY_TYPE_update() for type "search_api_index".
+ *
+ * Implemented on behalf of the "entity" datasource plugin.
+ *
+ * @see \Drupal\search_api\Plugin\search_api\datasource\ContentEntity
+ */
+function search_api_search_api_index_update(IndexInterface $index) {
+  if (!$index->status() || empty($index->original)) {
+    return;
+  }
+  /** @var \Drupal\search_api\IndexInterface $original */
+  $original = $index->original;
+  if (!$original->status()) {
+    return;
+  }
+
+  foreach ($index->getDatasources() as $datasource_id => $datasource) {
+    if ($datasource->getBaseId() != 'entity'
+      || !($datasource instanceof EntityDatasourceInterface)
+      || !$original->isValidDatasource($datasource_id)) {
+      continue;
+    }
+    $old_datasource = $original->getDatasource($datasource_id);
+    $old_config = $old_datasource->getConfiguration();
+    $new_config = $datasource->getConfiguration();
+
+    if ($old_config != $new_config) {
+      // Bundles and languages share the same structure, so changes can be
+      // processed in a unified way.
+      $tasks = [];
+      $insert_task = ContentEntityTaskManager::INSERT_ITEMS_TASK_TYPE;
+      $delete_task = ContentEntityTaskManager::DELETE_ITEMS_TASK_TYPE;
+      $settings = [];
+      $entity_type = \Drupal::entityTypeManager()
+        ->getDefinition($datasource->getEntityTypeId());
+      if ($entity_type->hasKey('bundle')) {
+        $settings['bundles'] = $datasource->getBundles();
+      }
+      if ($entity_type->isTranslatable()) {
+        $settings['languages'] = \Drupal::languageManager()->getLanguages();
+      }
+
+      // Determine which bundles/languages have been newly selected or
+      // deselected and then assign them to the appropriate actions depending
+      // on the current "default" setting.
+      foreach ($settings as $setting => $all) {
+        $old_selected = array_flip($old_config[$setting]['selected']);
+        $new_selected = array_flip($new_config[$setting]['selected']);
+
+        // First, check if the "default" setting changed and invert the checked
+        // items for the old config, so the following comparison makes sense.
+        if ($old_config[$setting]['default'] != $new_config[$setting]['default']) {
+          $old_selected = array_diff_key($all, $old_selected);
+        }
+
+        $newly_selected = array_keys(array_diff_key($new_selected, $old_selected));
+        $newly_unselected = array_keys(array_diff_key($old_selected, $new_selected));
+        if ($new_config[$setting]['default']) {
+          $tasks[$insert_task][$setting] = $newly_unselected;
+          $tasks[$delete_task][$setting] = $newly_selected;
+        }
+        else {
+          $tasks[$insert_task][$setting] = $newly_selected;
+          $tasks[$delete_task][$setting] = $newly_unselected;
+        }
+      }
+
+      // This will keep only those tasks where at least one of "bundles" or
+      // "languages" is non-empty.
+      $tasks = array_filter($tasks, 'array_filter');
+      $task_manager = \Drupal::getContainer()
+        ->get('search_api.task_manager');
+      foreach ($tasks as $task => $data) {
+        $data += [
+          'datasource' => $datasource_id,
+          'page' => 0,
+        ];
+        $task_manager->addTask($task, NULL, $index, $data);
+      }
+
+      // If we added any new tasks, set a batch for them. (If we aren't in a
+      // form submission, this will just be ignored.)
+      if ($tasks) {
+        $task_manager->setTasksBatch([
+          'index_id' => $index->id(),
+          'type' => array_keys($tasks),
+        ]);
+      }
+    }
+  }
+}
+
+/**
+ * Implements hook_views_plugins_argument_alter().
+ */
+function search_api_views_plugins_argument_alter(array &$plugins) {
+  // We have to include the term argument handler like this, since adding it
+  // directly (i.e., with an annotation) would cause fatal errors on sites
+  // without the Taxonomy module.
+  if (\Drupal::moduleHandler()->moduleExists('taxonomy')) {
+    $plugins['search_api_term'] = [
+      'plugin_type' => 'argument',
+      'id' => 'search_api_term',
+      'class' => TermArgument::class,
+      'provider' => 'search_api',
+    ];
+  }
+}
+
+/**
+ * Implements hook_views_plugins_filter_alter().
+ */
+function search_api_views_plugins_filter_alter(array &$plugins) {
+  // We have to include the term filter handler like this, since adding it
+  // directly (i.e., with an annotation) would cause fatal errors on sites
+  // without the Taxonomy module.
+  if (\Drupal::moduleHandler()->moduleExists('taxonomy')) {
+    $plugins['search_api_term'] = [
+      'plugin_type' => 'filter',
+      'id' => 'search_api_term',
+      'class' => TermFilter::class,
+      'provider' => 'search_api',
+    ];
+  }
+}
+
+/**
+ * Implements hook_ENTITY_TYPE_insert() for type "view".
+ */
+function search_api_view_insert(ViewEntityInterface $view) {
+  _search_api_view_crud_event($view);
+
+  // Disable Views' default caching mechanisms on Search API views.
+  $displays = $view->get('display');
+  if ($displays['default']['display_options']['query']['type'] === 'search_api_query') {
+    $change = FALSE;
+    foreach ($displays as $id => $display) {
+      if (isset($display['display_options']['cache']['type']) && in_array($display['display_options']['cache']['type'], ['tag', 'time'])) {
+        $displays[$id]['display_options']['cache']['type'] = 'none';
+        $change = TRUE;
+      }
+    }
+
+    if ($change) {
+      drupal_set_message(\Drupal::translation()->translate('The selected caching mechanism does not work with views on Search API indexes. Please either use one of the Search API-specific caching options or "None". Caching was turned off for this view.'), 'warning');
+      $view->set('display', $displays);
+      $view->save();
+    }
+  }
+}
+
+/**
+ * Implements hook_ENTITY_TYPE_update() for type "view".
+ */
+function search_api_view_update(ViewEntityInterface $view) {
+  _search_api_view_crud_event($view);
+}
+
+/**
+ * Implements hook_ENTITY_TYPE_delete() for type "view".
+ */
+function search_api_view_delete(ViewEntityInterface $view) {
+  _search_api_view_crud_event($view);
+}
+
+/**
+ * Reacts to a view CRUD event.
+ *
+ * @param \Drupal\views\ViewEntityInterface $view
+ *   The view that was created, changed or deleted.
+ */
+function _search_api_view_crud_event(ViewEntityInterface $view) {
+  // Whenever a view is created, updated (displays might have been added or
+  // removed) or deleted, we need to clear our cached display definitions.
+  if (SearchApiQuery::getIndexFromTable($view->get('base_table'))) {
+    \Drupal::getContainer()
+      ->get('plugin.manager.search_api.display')
+      ->clearCachedDefinitions();
+  }
+}
+
+/**
+ * Retrieves the allowed values for a list field instance.
+ *
+ * @param string $entity_type
+ *   The entity type to which the field is attached.
+ * @param string $bundle
+ *   The bundle to which the field is attached.
+ * @param string $field_name
+ *   The field's field name.
+ *
+ * @return array|null
+ *   An array of allowed values in the form key => label, or NULL.
+ *
+ * @see _search_api_views_handler_adjustments()
+ */
+function _search_api_views_get_allowed_values($entity_type, $bundle, $field_name) {
+  $field_manager = \Drupal::getContainer()->get('entity_field.manager');
+  $field_definitions = $field_manager->getFieldDefinitions($entity_type, $bundle);
+  if (!empty($field_definitions[$field_name])) {
+    /** @var \Drupal\Core\Field\FieldDefinitionInterface $field_definition */
+    $field_definition = $field_definitions[$field_name];
+    $options = $field_definition->getSetting('allowed_values');
+    if ($options) {
+      return $options;
+    }
+  }
+  return NULL;
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter() for form "views_ui_edit_display_form".
+ */
+function search_api_form_views_ui_edit_display_form_alter(&$form, FormStateInterface $form_state) {
+  // Disable Views' default caching mechanisms on Search API views.
+  $displays = $form_state->getStorage()['view']->get('display');
+  if ($displays['default']['display_options']['query']['type'] === 'search_api_query') {
+    unset($form['options']['cache']['type']['#options']['tag']);
+    unset($form['options']['cache']['type']['#options']['time']);
+  }
+}
+
+/**
+ * Returns a warning message if the Core Search module is enabled.
+ *
+ * @return string|null
+ *   A warning message if needed, NULL otherwise.
+ *
+ * @see search_api_install()
+ * @see search_api_requirements()
+ */
+function _search_api_search_module_warning() {
+  if (\Drupal::moduleHandler()->moduleExists('search')) {
+    $args = [
+      ':url' => Url::fromRoute('system.modules_uninstall')->toString(),
+      ':documentation' => 'https://www.drupal.org/node/2010146#core-search',
+    ];
+    return t('The default Drupal core Search module is still enabled. If you are using Search API, you probably want to <a href=":url">uninstall</a> the Search module for performance reasons. For more information see <a href=":documentation">the Search API handbook</a>.', $args);
+  }
+  return NULL;
+}

+ 3 - 0
sites/all/modules/contrib/search/search_api/search_api.permissions.yml

@@ -0,0 +1,3 @@
+'administer search_api':
+  title: 'Administer Search API'
+  description: 'Create and configure Search API servers and indexes.'

+ 34 - 0
sites/all/modules/contrib/search/search_api/search_api.plugin_type.yml

@@ -0,0 +1,34 @@
+search_api_backend:
+  label: Search API backend
+  plugin_manager_service_id: plugin.manager.search_api.backend
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
+
+search_api_datasource:
+  label: Search API datasource
+  plugin_manager_service_id: plugin.manager.search_api.datasource
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
+
+search_api_data_type:
+  label: Search API data type
+  plugin_manager_service_id: plugin.manager.search_api.data_type
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
+
+search_api_display:
+  label: Search API display
+  plugin_manager_service_id: plugin.manager.search_api.display
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
+
+search_api_processor:
+  label: Search API processor
+  plugin_manager_service_id: plugin.manager.search_api.processor
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
+
+search_api_parse_mode:
+  label: Search API parse mode
+  plugin_manager_service_id: plugin.manager.search_api.parse_mode
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator
+
+search_api_tracker:
+  label: Search API tracker
+  plugin_manager_service_id: plugin.manager.search_api.tracker
+  plugin_definition_decorator_class: \Drupal\plugin\PluginDefinition\ArrayPluginDefinitionDecorator

+ 242 - 0
sites/all/modules/contrib/search/search_api/search_api.routing.yml

@@ -0,0 +1,242 @@
+search_api.overview:
+  path: '/admin/config/search/search-api'
+  defaults:
+    _title: 'Search API'
+    _entity_list: 'search_api_index'
+  requirements:
+    _permission: 'administer search_api'
+
+search_api.execute_tasks:
+  path: '/admin/config/search/search-api/execute-tasks'
+  defaults:
+    _controller: '\Drupal\search_api\Controller\TaskController::executeTasks'
+    _title: 'Execute pending tasks'
+  requirements:
+    _permission: 'administer search_api'
+    _search_api_tasks: 'TRUE'
+
+entity.search_api_server.add_form:
+  path: '/admin/config/search/search-api/add-server'
+  defaults:
+    _entity_form: 'search_api_server.default'
+  requirements:
+    _entity_create_access: 'search_api_server'
+
+entity.search_api_server.canonical:
+  path: '/admin/config/search/search-api/server/{search_api_server}'
+  defaults:
+    _controller: '\Drupal\search_api\Controller\ServerController::page'
+    _title_callback: '\Drupal\search_api\Controller\ServerController::pageTitle'
+    _title: "View"
+  requirements:
+    _entity_access: 'search_api_server.view'
+  options:
+    parameters:
+      search_api_server:
+        with_config_overrides: TRUE
+
+entity.search_api_server.edit_form:
+  path: '/admin/config/search/search-api/server/{search_api_server}/edit'
+  defaults:
+    _entity_form: 'search_api_server.edit'
+  requirements:
+    _entity_access: 'search_api_server.edit'
+
+entity.search_api_server.delete_form:
+  path: '/admin/config/search/search-api/server/{search_api_server}/delete'
+  defaults:
+    _entity_form: 'search_api_server.delete'
+  requirements:
+    _entity_access: 'search_api_server.delete'
+  options:
+    parameters:
+      search_api_server:
+        with_config_overrides: TRUE
+
+entity.search_api_server.enable:
+  path: '/admin/config/search/search-api/server/{search_api_server}/enable'
+  defaults:
+    _controller: 'Drupal\search_api\Controller\ServerController::serverBypassEnable'
+  requirements:
+    _entity_access: 'search_api_server.enable'
+    _csrf_token: 'TRUE'
+
+entity.search_api_server.disable:
+  path: '/admin/config/search/search-api/server/{search_api_server}/disable'
+  defaults:
+    _entity_form: 'search_api_server.disable'
+  requirements:
+    _entity_access: 'search_api_server.disable'
+
+entity.search_api_server.clear:
+  path: '/admin/config/search/search-api/server/{search_api_server}/clear'
+  defaults:
+    _entity_form: 'search_api_server.clear'
+  requirements:
+    _entity_access: 'search_api_server.clear'
+  options:
+    parameters:
+      search_api_server:
+        with_config_overrides: TRUE
+
+entity.search_api_index.add_form:
+  path: '/admin/config/search/search-api/add-index'
+  defaults:
+    _entity_form: 'search_api_index.default'
+  requirements:
+    _entity_create_access: 'search_api_index'
+
+entity.search_api_index.canonical:
+  path: '/admin/config/search/search-api/index/{search_api_index}'
+  defaults:
+    _controller: '\Drupal\search_api\Controller\IndexController::page'
+    _title_callback: '\Drupal\search_api\Controller\IndexController::pageTitle'
+  requirements:
+    _entity_access: 'search_api_index.view'
+  options:
+    parameters:
+      search_api_index:
+        with_config_overrides: TRUE
+
+entity.search_api_index.edit_form:
+  path: '/admin/config/search/search-api/index/{search_api_index}/edit'
+  defaults:
+    _entity_form: 'search_api_index.edit'
+  requirements:
+    _entity_access: 'search_api_index.edit'
+
+entity.search_api_index.delete_form:
+  path: '/admin/config/search/search-api/index/{search_api_index}/delete'
+  defaults:
+    _entity_form: 'search_api_index.delete'
+  requirements:
+    _entity_access: 'search_api_index.delete'
+  options:
+    parameters:
+      search_api_index:
+        with_config_overrides: TRUE
+
+entity.search_api_index.enable:
+  path: '/admin/config/search/search-api/index/{search_api_index}/enable'
+  defaults:
+    _controller: 'Drupal\search_api\Controller\IndexController::indexBypassEnable'
+  requirements:
+    _entity_access: 'search_api_index.enable'
+    _csrf_token: 'TRUE'
+
+entity.search_api_index.disable:
+  path: '/admin/config/search/search-api/index/{search_api_index}/disable'
+  defaults:
+    _entity_form: 'search_api_index.disable'
+  requirements:
+    _entity_access: 'search_api_index.disable'
+
+entity.search_api_index.fields:
+  path: '/admin/config/search/search-api/index/{search_api_index}/fields'
+  options:
+    parameters:
+      search_api_index:
+        tempstore: TRUE
+        type: 'entity:search_api_index'
+  defaults:
+    _entity_form: 'search_api_index.fields'
+  requirements:
+    _entity_access: 'search_api_index.fields'
+
+entity.search_api_index.add_fields:
+  path: '/admin/config/search/search-api/index/{search_api_index}/fields/add/nojs'
+  options:
+    parameters:
+      search_api_index:
+        tempstore: TRUE
+        type: 'entity:search_api_index'
+  defaults:
+    _entity_form: 'search_api_index.add_fields'
+  requirements:
+    _entity_access: 'search_api_index.fields'
+
+entity.search_api_index.add_fields_ajax:
+  path: '/admin/config/search/search-api/index/{search_api_index}/fields/add/ajax'
+  options:
+    parameters:
+      search_api_index:
+        tempstore: TRUE
+        type: 'entity:search_api_index'
+  defaults:
+    _entity_form: 'search_api_index.add_fields'
+  requirements:
+    _entity_access: 'search_api_index.fields'
+
+entity.search_api_index.field_config:
+  path: '/admin/config/search/search-api/index/{search_api_index}/fields/edit/{field_id}'
+  options:
+    parameters:
+      search_api_index:
+        tempstore: TRUE
+        type: 'entity:search_api_index'
+  defaults:
+    _title: 'Edit field'
+    _entity_form: 'search_api_index.field_config'
+  requirements:
+    _entity_access: 'search_api_index.fields'
+
+entity.search_api_index.remove_field:
+  path: '/admin/config/search/search-api/index/{search_api_index}/fields/remove/{field_id}'
+  options:
+    parameters:
+      search_api_index:
+        tempstore: TRUE
+        type: 'entity:search_api_index'
+  defaults:
+    _controller: 'Drupal\search_api\Controller\IndexController::removeField'
+  requirements:
+    _entity_access: 'search_api_index.fields'
+    _csrf_token: 'TRUE'
+
+entity.search_api_index.break_lock_form:
+  path: '/admin/config/search/search-api/index/{search_api_index}/fields/break-lock'
+  defaults:
+    _entity_form: 'search_api_index.break_lock'
+    _title: 'Break lock'
+  requirements:
+    _entity_access: 'search_api_index.break-lock'
+
+entity.search_api_index.processors:
+  path: '/admin/config/search/search-api/index/{search_api_index}/processors'
+  defaults:
+    _entity_form: 'search_api_index.processors'
+  requirements:
+    _entity_access: 'search_api_index.processors'
+
+entity.search_api_index.reindex:
+  path: '/admin/config/search/search-api/index/{search_api_index}/reindex'
+  defaults:
+    _entity_form: 'search_api_index.reindex'
+  requirements:
+    _entity_access: 'search_api_index.reindex'
+  options:
+    parameters:
+      search_api_index:
+        with_config_overrides: TRUE
+
+entity.search_api_index.clear:
+  path: '/admin/config/search/search-api/index/{search_api_index}/clear'
+  defaults:
+    _entity_form: 'search_api_index.clear'
+  requirements:
+    _entity_access: 'search_api_index.clear'
+  options:
+    parameters:
+      search_api_index:
+        with_config_overrides: TRUE
+
+entity.search_api_index.rebuild_tracker:
+  path: '/admin/config/search/search-api/index/{search_api_index}/rebuild-tracker'
+  defaults:
+    _entity_form: 'search_api_index.rebuild_tracker'
+  requirements:
+    _entity_access: 'search_api_index.rebuild_tracker'
+  options:
+    parameters:
+      search_api_index:
+        with_config_overrides: TRUE

+ 94 - 0
sites/all/modules/contrib/search/search_api/search_api.services.yml

@@ -0,0 +1,94 @@
+services:
+  access_check.search_api_tasks:
+    class: Drupal\search_api\Controller\ExecuteTasksAccessCheck
+    arguments: ['@search_api.task_manager']
+    tags:
+      - { name: access_check, applies_to: _search_api_tasks }
+
+  logger.channel.search_api:
+    parent: logger.channel_base
+    arguments: ['search_api']
+
+  paramconverter.search_api:
+    class: Drupal\search_api\ParamConverter\SearchApiConverter
+    arguments: ['@entity.manager', '@user.shared_tempstore', '@current_user']
+    tags:
+      - { name: paramconverter, priority: 10 }
+    lazy: true
+
+  plugin.manager.search_api.backend:
+    class: Drupal\search_api\Backend\BackendPluginManager
+    parent: default_plugin_manager
+
+  plugin.manager.search_api.data_type:
+    class: Drupal\search_api\DataType\DataTypePluginManager
+    parent: default_plugin_manager
+
+  plugin.manager.search_api.datasource:
+    class: Drupal\search_api\Datasource\DatasourcePluginManager
+    parent: default_plugin_manager
+
+  plugin.manager.search_api.display:
+    class: Drupal\search_api\Display\DisplayPluginManager
+    parent: default_plugin_manager
+
+  plugin.manager.search_api.parse_mode:
+    class: Drupal\search_api\ParseMode\ParseModePluginManager
+    parent: default_plugin_manager
+
+  plugin.manager.search_api.processor:
+    class: Drupal\search_api\Processor\ProcessorPluginManager
+    arguments: ['@container.namespaces', '@cache.discovery', '@module_handler', '@string_translation']
+
+  plugin.manager.search_api.tracker:
+    class: Drupal\search_api\Tracker\TrackerPluginManager
+    parent: default_plugin_manager
+
+  search_api.datasource_task_manager:
+    class: Drupal\search_api\Plugin\search_api\datasource\ContentEntityTaskManager
+    arguments: ['@search_api.task_manager', '@entity_type.manager']
+    tags:
+      - { name: event_subscriber }
+
+  search_api.data_type_helper:
+    class: \Drupal\search_api\Utility\DataTypeHelper
+    arguments: ['@module_handler', '@plugin.manager.search_api.data_type']
+
+  search_api.fields_helper:
+    class: \Drupal\search_api\Utility\FieldsHelper
+    arguments: ['@entity_type.manager', '@entity_field.manager', '@entity_type.bundle.info', '@search_api.data_type_helper']
+
+  search_api.index_task_manager:
+    class: Drupal\search_api\Task\IndexTaskManager
+    arguments: ['@search_api.task_manager', '@entity_type.manager']
+    tags:
+      - { name: event_subscriber }
+
+  search_api.plugin_helper:
+    class: Drupal\search_api\Utility\PluginHelper
+    arguments: ['@plugin.manager.search_api.datasource', '@plugin.manager.search_api.processor', '@plugin.manager.search_api.tracker']
+
+  search_api.post_request_indexing:
+    class: Drupal\search_api\Utility\PostRequestIndexing
+    arguments: ['@entity_type.manager']
+    tags:
+      - { name: event_subscriber }
+
+  search_api.query_helper:
+    class: Drupal\search_api\Utility\QueryHelper
+    arguments: ['@request_stack', '@module_handler', '@plugin.manager.search_api.parse_mode']
+
+  search_api.server_task_manager:
+    class: Drupal\search_api\Task\ServerTaskManager
+    arguments: ['@search_api.task_manager', '@entity_type.manager']
+    tags:
+      - { name: event_subscriber }
+
+  search_api.task_manager:
+    class: Drupal\search_api\Task\TaskManager
+    arguments: ['@entity_type.manager', '@event_dispatcher', '@string_translation']
+
+  search_api.vbo_view_data_provider:
+    class: Drupal\search_api\Contrib\ViewsBulkOperationsEventSubscriber
+    tags:
+      - { name: event_subscriber }

+ 506 - 0
sites/all/modules/contrib/search/search_api/search_api.theme.inc

@@ -0,0 +1,506 @@
+<?php
+
+/**
+ * @file
+ * Defines theme functions for the Search API module.
+ */
+
+use Drupal\Component\Render\FormattableMarkup;
+use Drupal\Component\Utility\Html;
+use Drupal\Core\Render\Element;
+use Drupal\Core\Url;
+use Drupal\search_api\Query\QueryInterface;
+use Drupal\search_api\SearchApiException;
+use Drupal\search_api\Utility\Utility;
+
+/**
+ * Returns HTML for a fields form table.
+ *
+ * @param array $variables
+ *   An associative array containing:
+ *   - element: A render element representing the form.
+ *
+ * @return string
+ *   The rendered HTML for a fields form table.
+ *
+ * @ingroup themeable
+ */
+function theme_search_api_admin_fields_table(array $variables) {
+  $form = $variables['element'];
+  $rows = [];
+  if (!empty($form['fields'])) {
+    foreach (Element::children($form['fields']) as $name) {
+      $row = [];
+      foreach (Element::children($form['fields'][$name]) as $field) {
+        if ($cell = render($form['fields'][$name][$field])) {
+          $row[] = $cell;
+        }
+      }
+      $row = [
+        'data' => $row,
+        'data-field-row-id' => $name,
+      ];
+      if (!empty($form['fields'][$name]['description']['#value'])) {
+        $row['title'] = strip_tags($form['fields'][$name]['description']['#value']);
+      }
+      $rows[] = $row;
+    }
+  }
+
+  $note = isset($form['note']) ? $form['note'] : '';
+  unset($form['note'], $form['submit']);
+  $output = '';
+  foreach (Element::children($form) as $key) {
+    if (!empty($form[$key])) {
+      $output .= render($form[$key]);
+    }
+  }
+
+  $build = [
+    '#theme' => 'table',
+    '#header' => $form['#header'],
+    '#rows' => $rows,
+    '#empty' => t('No fields have been added for this datasource.'),
+  ];
+
+  $output .= render($build);
+  $output .= render($note);
+
+  return $output;
+}
+
+/**
+ * Returns HTML for the data type overview table.
+ *
+ * @param array $variables
+ *   An associative array containing:
+ *   - data_types: An associative array of data types, keyed by data type ID and
+ *     containing associative arrays with information about the data type:
+ *     - label: The (translated) human-readable label for the data type.
+ *     - description: The (translated) description of the data type.
+ *     - fallback: The type ID of the fallback type.
+ *   - fallback_mapping: array of fallback data types for unsupported data
+ *     types.
+ *
+ * @return string
+ *   The rendered HTML for a fields form table.
+ *
+ * @ingroup themeable
+ */
+function theme_search_api_admin_data_type_table(array $variables) {
+  $data_types = $variables['data_types'];
+  $fallback_mapping = $variables['fallback_mapping'];
+  $header = [
+    t('Data Type'),
+    t('Description'),
+    t('Supported'),
+  ];
+  // Only show the column with fallback types if there is actually an
+  // unsupported type listed.
+  if ($fallback_mapping) {
+    $header[] = t('Fallback data type');
+  }
+
+  $rows = [];
+  $yes = t('Yes');
+  $yes_img = 'core/misc/icons/73b355/check.svg';
+  $no = t('No');
+  $no_img = 'core/misc/icons/e32700/error.svg';
+  foreach ($data_types as $data_type_id => $data_type) {
+    $has_fallback = isset($fallback_mapping[$data_type_id]);
+    $supported_label = $has_fallback ? $no : $yes;
+    $supported_icon = [
+      '#theme' => 'image',
+      '#uri' => $has_fallback ? $no_img : $yes_img,
+      '#width' => 18,
+      '#height' => 18,
+      '#alt' => $supported_label,
+      '#title' => $supported_label,
+    ];
+
+    $row = [
+      $data_type['label'],
+      $data_type['description'],
+      ['data' => $supported_icon],
+    ];
+
+    if ($fallback_mapping) {
+      $row[] = $has_fallback ? $data_types[$data_type['fallback']]['label'] : '';
+    }
+
+    $rows[] = $row;
+  }
+
+  $build = [
+    '#theme' => 'table',
+    '#header' => $header,
+    '#rows' => $rows,
+  ];
+
+  return render($build);
+}
+
+/**
+ * Returns HTML for a list of form items.
+ *
+ * Wrapper around the "item_list" theme which uses the child elements as the
+ * list items instead of the "#items" key.
+ *
+ * @param array $variables
+ *   An associative array containing a single value, "element", containing the
+ *   element to be rendered.
+ *
+ * @return string
+ *   The rendered HTML for the list.
+ *
+ * @ingroup themeable
+ */
+function theme_search_api_form_item_list(array $variables) {
+  $element = $variables['element'];
+
+  $build = [
+    '#theme' => 'item_list',
+  ];
+  if (!empty($element['#title'])) {
+    $build['#title'] = $element['#title'];
+  }
+  foreach (Element::children($element) as $key) {
+    $build['#items'][$key] = $element[$key];
+  }
+
+  return render($build);
+}
+
+/**
+ * Returns HTML for a search server.
+ *
+ * @param array $variables
+ *   An associative array containing:
+ *   - server: The server that should be displayed.
+ *
+ * @return string
+ *   The rendered HTML for a search server.
+ *
+ * @ingroup themeable
+ */
+function theme_search_api_server(array $variables) {
+  // Get the search server.
+  /** @var \Drupal\search_api\ServerInterface $server */
+  $server = $variables['server'];
+
+  $output = '';
+
+  if (($description = $server->getDescription())) {
+    // Sanitize the description and append to the output.
+    $output .= '<p class="description">' . nl2br(Html::escape($description)) . '</p>';
+  }
+
+  // Initialize the $rows variable which will hold the different parts of server
+  // information.
+  $rows = [];
+  // Create a row template with references so we don't have to deal with the
+  // complicated structure for each individual row.
+  $row = [
+    'data' => [
+      ['header' => TRUE],
+      '',
+    ],
+    'class' => [''],
+  ];
+  // Get the individual parts of the row by reference.
+  $label = &$row['data'][0]['data'];
+  $info = &$row['data'][1];
+  $classes = &$row['class'];
+
+  // Check if the server is enabled.
+  if ($server->status()) {
+    $classes[] = 'ok';
+    $info = t('enabled (<a href=":url">disable</a>)', [':url' => $server->toUrl('disable')->toString()]);
+  }
+  else {
+    $classes[] = 'warning';
+    $info = t('disabled (<a href=":url">enable</a>)', [':url' => $server->toUrl('enable')->toString()]);
+  }
+  // Append the row and reset variables.
+  $label = t('Status');
+  $classes[] = 'search-api-server-summary--status';
+  $rows[] = Utility::deepCopy($row);
+  $classes = [];
+
+  // Check if the backend used by the server is valid and get its label.
+  if ($server->hasValidBackend()) {
+    $backend = $server->getBackend();
+    $info = Html::escape($backend->label());
+  }
+  else {
+    $classes[] = 'error';
+    $info = t('Invalid or missing backend plugin: %backend_id', ['%backend_id' => $server->getBackendId()]);
+  }
+
+  // Append the row and reset variables.
+  $label = t('Backend class');
+  $classes[] = 'search-api-server-summary--backend';
+  $rows[] = Utility::deepCopy($row);
+  $classes = [];
+
+  // Build the indexes links container.
+  $indexes = [
+    '#theme' => 'links',
+    '#attributes' => ['class' => ['inline']],
+    '#links' => [],
+  ];
+  // Add links for all indexes attached to this server.
+  foreach ($server->getIndexes() as $index) {
+    $indexes['#links'][] = [
+      'title' => $index->label(),
+      'url' => $index->toUrl('canonical'),
+    ];
+  }
+  // Check if the indexes variable contains links.
+  if ($indexes['#links']) {
+    $label = t('Search indexes');
+    $info = render($indexes);
+    $classes[] = 'search-api-server-summary--indexes';
+    $rows[] = Utility::deepCopy($row);
+    $classes = [];
+  }
+
+  // Add backend-specific additional information.
+  foreach ($server->viewSettings() as $information) {
+    // Convert the extra information and append the information to the row.
+    $label = $information['label'];
+    $info = $information['info'];
+    if (!empty($information['status'])) {
+      $classes[] = $information['status'];
+    }
+    $rows[] = Utility::deepCopy($row);
+    $classes = [];
+  }
+
+  // Append the server info table to the output.
+  $server_info_table = [
+    '#theme' => 'table',
+    '#rows' => $rows,
+    '#attributes' => [
+      'class' => [
+        'search-api-server-summary',
+      ],
+    ],
+  ];
+  $output .= render($server_info_table);
+
+  return $output;
+}
+
+/**
+ * Implements hook_preprocess_search_api_index().
+ */
+function search_api_preprocess_search_api_index(array &$variables) {
+  /** @var \Drupal\search_api\IndexInterface $index */
+  $index = $variables['index'];
+
+  if ($index->status()) {
+    try {
+      $variables['server_count'] = $index->query()
+        ->setProcessingLevel(QueryInterface::PROCESSING_NONE)
+        ->addTag('server_index_status')
+        ->range(0, 0)
+        ->execute()
+        ->getResultCount();
+    }
+    catch (SearchApiException $e) {
+      $variables['server_count_error'] = $e->getMessage();
+    }
+  }
+}
+
+/**
+ * Returns HTML for a search index.
+ *
+ * @param array $variables
+ *   An associative array containing:
+ *   - index: The search index to display.
+ *   - server_count: The count of items on the server for this index.
+ *   - server_count_error: If set, the error that occurred while trying to get
+ *     the server items count.
+ *
+ * @return string
+ *   The rendered HTML for a search index.
+ *
+ * @ingroup themeable
+ */
+function theme_search_api_index(array $variables) {
+  // Get the index.
+  /** @var \Drupal\search_api\IndexInterface $index */
+  $index = $variables['index'];
+  $server = $index->hasValidServer() ? $index->getServerInstance() : NULL;
+  $tracker = $index->hasValidTracker() ? $index->getTrackerInstance() : NULL;
+
+  $output = '';
+
+  if (($description = $index->getDescription())) {
+    // Sanitize the description and append to the output.
+    $output .= '<p class="description">' . nl2br(Html::escape($description)) . '</p>';
+  }
+
+  // Initialize the $rows variable which will hold the different parts of server
+  // information.
+  $rows = [];
+  // Create a row template with references so we don't have to deal with the
+  // complicated structure for each individual row.
+  $row = [
+    'data' => [
+      ['header' => TRUE],
+      '',
+    ],
+    'class' => [],
+  ];
+  // Get the individual parts of the row by reference.
+  $label = &$row['data'][0]['data'];
+  $info = &$row['data'][1];
+  $classes = &$row['class'];
+
+  // Check if the index is enabled.
+  if ($index->status()) {
+    $classes[] = 'ok';
+    $info = t('enabled (<a href=":url">disable</a>)', [':url' => $index->toUrl('disable')->toString()]);
+  }
+  // Check if a server is available and enabled.
+  elseif ($server && $server->status()) {
+    $classes[] = 'warning';
+    $info = t('disabled (<a href=":url">enable</a>)', [':url' => $index->toUrl('enable')->toString()]);
+  }
+  else {
+    $classes[] = 'warning';
+    $info = t('disabled');
+  }
+  // Append the row and reset variables.
+  $label = t('Status');
+  $classes[] = 'search-api-index-summary--status';
+  $rows[] = Utility::deepCopy($row);
+  $classes = [];
+
+  foreach ($index->getDatasourceIds() as $datasource_id) {
+    // Check if the datasource is valid.
+    if ($index->isValidDatasource($datasource_id)) {
+      $info = $index->getDatasource($datasource_id)->label();
+      if ($tracker) {
+        $args = [
+          '@indexed' => $tracker->getIndexedItemsCount($datasource_id),
+          '@total' => $tracker->getTotalItemsCount($datasource_id),
+        ];
+        $indexed = t('@indexed/@total indexed', $args);
+        $args = [
+          '@datasource' => $info,
+          '@indexed' => $indexed,
+        ];
+        $info = new FormattableMarkup('@datasource <small>(@indexed)</small>', $args);
+      }
+    }
+    else {
+      $classes[] = 'error';
+      $info = t('Invalid or missing datasource plugin: %datasource_id', ['%datasource_id' => $datasource_id]);
+    }
+    // Append the row and reset variables.
+    $label = t('Datasource');
+    $classes[] = 'search-api-index-summary--datasource';
+    $rows[] = Utility::deepCopy($row);
+    $classes = [];
+  }
+
+  // Check if the tracker is valid.
+  if ($tracker) {
+    $info = $tracker->label();
+  }
+  else {
+    $classes[] = 'error';
+    $info = t('Invalid or missing tracker plugin: %tracker_id', ['%tracker_id' => $index->getTrackerId()]);
+  }
+  // Append the row and reset variables.
+  $label = t('Tracker');
+  $classes[] = 'search-api-index-summary--tracker';
+  $rows[] = Utility::deepCopy($row);
+  $classes = [];
+
+  // Check if a server is available.
+  $classes[] = 'search-api-index-summary--server';
+  if ($server) {
+    $label = t('Server');
+    $info = $server->toLink(NULL, 'canonical')->toString();
+    $rows[] = Utility::deepCopy($row);
+  }
+  elseif ($index->getServerId()) {
+    $classes[] = 'error';
+    $label = t('Server');
+    $info = t('Unknown server set for index: %server_id', ['%server_id' => $index->getServerId()]);
+    $rows[] = Utility::deepCopy($row);
+  }
+  $classes = [];
+
+  // Check if the index is enabled.
+  if ($index->status()) {
+    $label = t('Server index status');
+    if (isset($variables['server_count'])) {
+      $vars = [':url' => Url::fromUri('https://drupal.org/node/2009804#server-index-status')->toString()];
+      // Build the server index status info.
+      $info = \Drupal::translation()->formatPlural($variables['server_count'], '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);
+    }
+    else {
+      $args = ['@message' => $variables['server_count_error']];
+      $info = t('Error while checking server index status: @message', $args);
+      $classes[] = 'error';
+    }
+    $classes[] = 'search-api-index-summary--server-index-status';
+    $rows[] = Utility::deepCopy($row);
+    $classes = [];
+
+    $cron_limit = $index->getOption('cron_limit', \Drupal::config('search_api.settings')->get('default_cron_limit'));
+    // Check if the cron limit is higher than zero.
+    if ($cron_limit != 0) {
+      $classes[] = 'ok';
+      if ($cron_limit > 0) {
+        $info = \Drupal::translation()->formatPlural($cron_limit, 'During cron runs, 1 item will be indexed per batch.', 'During cron runs, @count items will be indexed per batch.');
+      }
+      else {
+        $info = t('All items will be indexed at once during cron runs.');
+      }
+    }
+    else {
+      $classes[] = 'warning';
+      $info = t('No items will be indexed during cron runs.');
+    }
+    // Append the row and reset variables.
+    $label = t('Cron batch size');
+    $classes[] = 'search-api-index-summary--cron-batch-size';
+    $rows[] = Utility::deepCopy($row);
+    $classes = [];
+
+    // Add the indexing progress bar.
+    if ($tracker) {
+      $indexed_count = $tracker->getIndexedItemsCount();
+      $total_count = $tracker->getTotalItemsCount();
+
+      $index_progress = [
+        '#theme' => 'progress_bar',
+        '#percent' => $total_count ? (int) (100 * $indexed_count / $total_count) : 100,
+        '#message' => t('@indexed/@total indexed', ['@indexed' => $indexed_count, '@total' => $total_count]),
+      ];
+      $output .= '<h3>' . t('Index status') . '</h3>';
+      $output .= '<div class="search-api-index-status">' . render($index_progress) . '</div>';
+    }
+  }
+
+  // Append the index info table to the output.
+  $index_info_table = [
+    '#theme' => 'table',
+    '#rows' => $rows,
+    '#attributes' => [
+      'class' => [
+        'search-api-index-summary',
+      ],
+    ],
+  ];
+  $output .= render($index_info_table);
+
+  return $output;
+}

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

@@ -0,0 +1,828 @@
+<?php
+
+/**
+ * @file
+ * Views hook implementations for the Search API module.
+ */
+
+use Drupal\Core\Entity\FieldableEntityInterface;
+use Drupal\Core\Field\TypedData\FieldItemDataDefinition;
+use Drupal\Core\TypedData\DataDefinitionInterface;
+use Drupal\Core\TypedData\DataReferenceDefinitionInterface;
+use Drupal\search_api\Datasource\DatasourceInterface;
+use Drupal\search_api\Entity\Index;
+use Drupal\search_api\Item\FieldInterface;
+use Drupal\search_api\SearchApiException;
+use Drupal\search_api\Utility\Utility;
+
+/**
+ * Implements hook_views_data().
+ *
+ * For each search index, we provide the following tables:
+ * - One base table, with key "search_api_index_INDEX", which contains field,
+ *   filter, argument and sort handlers for all indexed fields. (Field handlers,
+ *   too, to allow things like click-sorting.)
+ * - Tables for each datasource, by default with key
+ *   "search_api_datasource_INDEX_DATASOURCE", with field and (where applicable)
+ *   relationship handlers for each property of the datasource. Those will be
+ *   joined to the index base table by default.
+ *
+ * Also, for each entity type encountered in any table, a table with
+ * field/relationship handlers for all of that entity type's properties is
+ * created. Those tables will use the key "search_api_entity_ENTITY".
+ */
+function search_api_views_data() {
+  $data = [];
+
+  /** @var \Drupal\search_api\IndexInterface $index */
+  foreach (Index::loadMultiple() as $index) {
+    try {
+      // Fill in base data.
+      $key = 'search_api_index_' . $index->id();
+      $table = &$data[$key];
+      $index_label = $index->label();
+      $table['table']['group'] = t('Index @name', ['@name' => $index_label]);
+      $table['table']['base'] = [
+        'field' => 'search_api_id',
+        'index' => $index->id(),
+        'title' => t('Index @name', ['@name' => $index_label]),
+        'help' => t('Use the @name search index for filtering and retrieving data.', ['@name' => $index_label]),
+        'query_id' => 'search_api_query',
+      ];
+
+      // Add suitable handlers for all indexed fields.
+      foreach ($index->getFields(TRUE) as $field_id => $field) {
+        $field_alias = _search_api_views_find_field_alias($field_id, $table);
+        $field_definition = _search_api_views_get_handlers($field);
+        // The field handler has to be extra, since it is a) determined by the
+        // field's underlying property and b) needs a different "real field"
+        // set.
+        if ($field->getPropertyPath()) {
+          $field_handler = _search_api_views_get_field_handler_for_property($field->getDataDefinition(), $field->getPropertyPath());
+          if ($field_handler) {
+            $field_definition['field'] = $field_handler;
+            $field_definition['field']['real field'] = $field->getCombinedPropertyPath();
+            $field_definition['field']['search_api field'] = $field_id;
+          }
+        }
+        if ($field_definition) {
+          $field_label = $field->getLabel();
+          $field_definition += [
+            'title' => $field_label,
+            'help' => $field->getDescription() ?: t('(No description available)'),
+          ];
+          if ($datasource = $field->getDatasource()) {
+            $field_definition['group'] = t('@datasource datasource', ['@datasource' => $datasource->label()]);
+          }
+          if ($field_id != $field_alias) {
+            $field_definition['real field'] = $field_id;
+          }
+          if (isset($field_definition['field'])) {
+            $field_definition['field']['title'] = t('@field (indexed field)', ['@field' => $field_label]);
+          }
+          $table[$field_alias] = $field_definition;
+        }
+      }
+
+      // Add special fields.
+      _search_api_views_data_special_fields($table);
+
+      // Add relationships for field data of all datasources.
+      $datasource_tables_prefix = 'search_api_datasource_' . $index->id() . '_';
+      foreach ($index->getDatasources() as $datasource_id => $datasource) {
+        $table_key = _search_api_views_find_field_alias($datasource_tables_prefix . $datasource_id, $data);
+        $data[$table_key] = _search_api_views_datasource_table($datasource, $data);
+        // Automatically join this table for views of this index.
+        $data[$table_key]['table']['join'][$key] = [
+          'join_id' => 'search_api',
+        ];
+      }
+    }
+    catch (\Exception $e) {
+      $args = [
+        '%index' => $index->label(),
+      ];
+      watchdog_exception('search_api', $e, '%type while computing Views data for index %index: @message in %function (line %line of %file).', $args);
+    }
+  }
+
+  return array_filter($data);
+}
+
+/**
+ * Implements hook_views_plugins_cache_alter().
+ */
+function search_api_views_plugins_cache_alter(array &$plugins) {
+  // Collect all base tables provided by this module.
+  $bases = [];
+  /** @var \Drupal\search_api\IndexInterface $index */
+  foreach (Index::loadMultiple() as $index) {
+    $bases[] = 'search_api_index_' . $index->id();
+  }
+  $plugins['search_api']['base'] = $bases;
+}
+
+/**
+ * Implements hook_views_plugins_row_alter().
+ */
+function search_api_views_plugins_row_alter(array &$plugins) {
+  // Collect all base tables provided by this module.
+  $bases = [];
+  /** @var \Drupal\search_api\IndexInterface $index */
+  foreach (Index::loadMultiple() as $index) {
+    $bases[] = 'search_api_index_' . $index->id();
+  }
+  $plugins['search_api']['base'] = $bases;
+}
+
+/**
+ * Finds an unused field alias for a field in a Views table definition.
+ *
+ * @param string $field_id
+ *   The original ID of the Search API field.
+ * @param array $table
+ *   The Views table definition.
+ *
+ * @return string
+ *   The field alias to use.
+ */
+function _search_api_views_find_field_alias($field_id, array &$table) {
+  $base = $field_alias = preg_replace('/[^a-zA-Z0-9]+/S', '_', $field_id);
+  $i = 0;
+  while (isset($table[$field_alias])) {
+    $field_alias = $base . '_' . ++$i;
+  }
+  return $field_alias;
+}
+
+/**
+ * Returns the Views handlers to use for a given field.
+ *
+ * @param \Drupal\search_api\Item\FieldInterface $field
+ *   The field to add to the definition.
+ *
+ * @return array
+ *   The Views definition to add for the given field.
+ */
+function _search_api_views_get_handlers(FieldInterface $field) {
+  $mapping = _search_api_views_handler_mapping();
+
+  try {
+    $types = [];
+
+    $definition = $field->getDataDefinition();
+    if ($definition->getSetting('target_type')) {
+      $types[] = 'entity:' . $definition->getSetting('target_type');
+      $types[] = 'entity';
+    }
+
+    if ($definition->getSetting('allowed_values')) {
+      $types[] = 'options';
+    }
+
+    $types[] = $field->getType();
+    /** @var \Drupal\search_api\DataType\DataTypeInterface $data_type */
+    $data_type = \Drupal::service('plugin.manager.search_api.data_type')->createInstance($field->getType());
+    if (!$data_type->isDefault()) {
+      $types[] = $data_type->getFallbackType();
+    }
+
+    foreach ($types as $type) {
+      if (isset($mapping[$type])) {
+        _search_api_views_handler_adjustments($type, $field, $mapping[$type]);
+        return $mapping[$type];
+      }
+    }
+  }
+  catch (SearchApiException $e) {
+    $vars['%index'] = $field->getIndex()->label();
+    $vars['%field'] = $field->getPrefixedLabel();
+    watchdog_exception('search_api', $e, '%type while adding Views handlers for field %field on index %index: @message in %function (line %line of %file).', $vars);
+  }
+
+  return [];
+}
+
+/**
+ * Determines the mapping of Search API data types to their Views handlers.
+ *
+ * @return array
+ *   An associative array with data types as the keys and Views field data
+ *   definitions as the values. In addition to all normally defined data types,
+ *   keys can also be "options" for any field with an options list, "entity" for
+ *   general entity-typed fields or "entity:ENTITY_TYPE" (with "ENTITY_TYPE"
+ *   being the machine name of an entity type) for entities of that type.
+ *
+ * @see search_api_views_handler_mapping_alter()
+ */
+function _search_api_views_handler_mapping() {
+  $mapping = &drupal_static(__FUNCTION__);
+
+  if (!isset($mapping)) {
+    $mapping = [
+      'boolean' => [
+        'argument' => [
+          'id' => 'search_api',
+        ],
+        'filter' => [
+          'id' => 'search_api_boolean',
+        ],
+        'sort' => [
+          'id' => 'search_api',
+        ],
+      ],
+      'date' => [
+        'argument' => [
+          'id' => 'search_api_date',
+        ],
+        'filter' => [
+          'id' => 'search_api_date',
+        ],
+        'sort' => [
+          'id' => 'search_api',
+        ],
+      ],
+      'decimal' => [
+        'argument' => [
+          'id' => 'search_api',
+          'filter' => 'floatval',
+        ],
+        'filter' => [
+          'id' => 'search_api_numeric',
+        ],
+        'sort' => [
+          'id' => 'search_api',
+        ],
+      ],
+      'integer' => [
+        'argument' => [
+          'id' => 'search_api',
+          'filter' => 'intval',
+        ],
+        'filter' => [
+          'id' => 'search_api_numeric',
+        ],
+        'sort' => [
+          'id' => 'search_api',
+        ],
+      ],
+      'string' => [
+        'argument' => [
+          'id' => 'search_api',
+        ],
+        'filter' => [
+          'id' => 'search_api_string',
+        ],
+        'sort' => [
+          'id' => 'search_api',
+        ],
+      ],
+      'text' => [
+        'argument' => [
+          'id' => 'search_api',
+        ],
+        'filter' => [
+          'id' => 'search_api_text',
+        ],
+        'sort' => [
+          'id' => 'search_api',
+        ],
+      ],
+      'options' => [
+        'argument' => [
+          'id' => 'search_api',
+        ],
+        'filter' => [
+          'id' => 'search_api_options',
+        ],
+        'sort' => [
+          'id' => 'search_api',
+        ],
+      ],
+      'entity:taxonomy_term' => [
+        'argument' => [
+          'id' => 'search_api_term',
+        ],
+        'filter' => [
+          'id' => 'search_api_term',
+        ],
+        'sort' => [
+          'id' => 'search_api',
+        ],
+      ],
+      'entity:user' => [
+        'argument' => [
+          'id' => 'search_api',
+          'filter' => 'intval',
+        ],
+        'filter' => [
+          'id' => 'search_api_user',
+        ],
+        'sort' => [
+          'id' => 'search_api',
+        ],
+      ],
+      'entity:node_type' => [
+        'argument' => [
+          'id' => 'search_api',
+        ],
+        'filter' => [
+          'id' => 'search_api_options',
+          'options callback' => 'node_type_get_names',
+        ],
+        'sort' => [
+          'id' => 'search_api',
+        ],
+      ],
+    ];
+
+    $alter_id = 'search_api_views_handler_mapping';
+    \Drupal::moduleHandler()->alter($alter_id, $mapping);
+  }
+
+  return $mapping;
+}
+
+/**
+ * Makes necessary, field-specific adjustments to Views handler definitions.
+ *
+ * @param string $type
+ *   The type of field, as defined in _search_api_views_handler_mapping().
+ * @param \Drupal\search_api\Item\FieldInterface $field
+ *   The field whose handler definitions are being created.
+ * @param array $definitions
+ *   The handler definitions for the field, as a reference.
+ */
+function _search_api_views_handler_adjustments($type, FieldInterface $field, array &$definitions) {
+  // By default, all fields can be empty (or at least have to be treated that
+  // way by the Search API).
+  if (!isset($definitions['filter']['allow empty'])) {
+    $definitions['filter']['allow empty'] = TRUE;
+  }
+
+  // For taxonomy term references, set the referenced vocabulary.
+  $data_definition = $field->getDataDefinition();
+  if ($type == 'entity:taxonomy_term') {
+    if (isset($data_definition->getSettings()['handler_settings']['target_bundles'])) {
+      $target_bundles = $data_definition->getSettings()['handler_settings']['target_bundles'];
+      if (count($target_bundles) == 1) {
+        $definitions['filter']['vocabulary'] = reset($target_bundles);
+      }
+    }
+  }
+  elseif ($type == 'options') {
+    if ($data_definition instanceof FieldItemDataDefinition) {
+      // If this is a normal Field API field, dynamically retrieve the options
+      // list at query time.
+      $field_definition = $data_definition->getFieldDefinition();
+      $bundle = $field_definition->getTargetBundle();
+      $field_name = $field_definition->getName();
+      $entity_type = $field_definition->getTargetEntityTypeId();
+      $definitions['filter']['options callback'] = '_search_api_views_get_allowed_values';
+      $definitions['filter']['options arguments'] = [$entity_type, $bundle, $field_name];
+    }
+    else {
+      // Otherwise, include the options list verbatim in the Views data, unless
+      // it's too big (or doesn't look valid).
+      $options = $data_definition->getSetting('allowed_values');
+      if (is_array($options) && count($options) <= 50) {
+        // Since the Views InOperator filter plugin doesn't allow just including
+        // the options in the definition, we use this workaround.
+        $definitions['filter']['options callback'] = 'array_filter';
+        $definitions['filter']['options arguments'] = [$options];
+      }
+    }
+  }
+}
+
+/**
+ * Adds definitions for our special fields to a Views data table definition.
+ *
+ * @param array $table
+ *   The existing Views data table definition.
+ */
+function _search_api_views_data_special_fields(array &$table) {
+  $id_field = _search_api_views_find_field_alias('search_api_id', $table);
+  $table[$id_field]['title'] = t('Item ID');
+  $table[$id_field]['help'] = t("The item's internal (Search API-specific) ID");
+  $table[$id_field]['field']['id'] = 'standard';
+  $table[$id_field]['sort']['id'] = 'search_api';
+  if ($id_field != 'search_api_id') {
+    $table[$id_field]['real field'] = 'search_api_id';
+  }
+
+  $datasource_field = _search_api_views_find_field_alias('search_api_datasource', $table);
+  $table[$datasource_field]['title'] = t('Datasource');
+  $table[$datasource_field]['help'] = t("The data source ID");
+  $table[$datasource_field]['argument']['id'] = 'search_api';
+  $table[$datasource_field]['argument']['disable_break_phrase'] = TRUE;
+  $table[$datasource_field]['field']['id'] = 'standard';
+  $table[$datasource_field]['filter']['id'] = 'search_api_datasource';
+  $table[$datasource_field]['sort']['id'] = 'search_api';
+  if ($datasource_field != 'search_api_datasource') {
+    $table[$datasource_field]['real field'] = 'search_api_datasource';
+  }
+
+  $language_field = _search_api_views_find_field_alias('search_api_language', $table);
+  $table[$language_field]['title'] = t('Item language');
+  $table[$language_field]['help'] = t("The item's language");
+  $table[$language_field]['field']['id'] = 'language';
+  $table[$language_field]['filter']['id'] = 'search_api_language';
+  $table[$language_field]['filter']['allow empty'] = FALSE;
+  $table[$language_field]['sort']['id'] = 'search_api';
+  if ($language_field != 'search_api_language') {
+    $table[$language_field]['real field'] = 'search_api_language';
+  }
+
+  $relevance_field = _search_api_views_find_field_alias('search_api_relevance', $table);
+  $table[$relevance_field]['group'] = t('Search');
+  $table[$relevance_field]['title'] = t('Relevance');
+  $table[$relevance_field]['help'] = t('The relevance of this search result with respect to the query');
+  $table[$relevance_field]['field']['type'] = 'decimal';
+  $table[$relevance_field]['field']['id'] = 'numeric';
+  $table[$relevance_field]['field']['search_api field'] = 'search_api_relevance';
+  $table[$relevance_field]['sort']['id'] = 'search_api';
+  if ($relevance_field != 'search_api_relevance') {
+    $table[$relevance_field]['real field'] = 'search_api_relevance';
+  }
+
+  $excerpt_field = _search_api_views_find_field_alias('search_api_excerpt', $table);
+  $table[$excerpt_field]['group'] = t('Search');
+  $table[$excerpt_field]['title'] = t('Excerpt');
+  $table[$excerpt_field]['help'] = t('The search result excerpted to show found search terms');
+  $table[$excerpt_field]['field']['id'] = 'search_api';
+  $table[$excerpt_field]['field']['filter_type'] = 'xss';
+  if ($excerpt_field != 'search_api_excerpt') {
+    $table[$excerpt_field]['real field'] = 'search_api_excerpt';
+  }
+
+  $fulltext_field = _search_api_views_find_field_alias('search_api_fulltext', $table);
+  $table[$fulltext_field]['group'] = t('Search');
+  $table[$fulltext_field]['title'] = t('Fulltext search');
+  $table[$fulltext_field]['help'] = t('Search several or all fulltext fields at once.');
+  $table[$fulltext_field]['filter']['id'] = 'search_api_fulltext';
+  $table[$fulltext_field]['argument']['id'] = 'search_api_fulltext';
+  if ($fulltext_field != 'search_api_fulltext') {
+    $table[$fulltext_field]['real field'] = 'search_api_fulltext';
+  }
+
+  $mlt_field = _search_api_views_find_field_alias('search_api_more_like_this', $table);
+  $table[$mlt_field]['group'] = t('Search');
+  $table[$mlt_field]['title'] = t('More like this');
+  $table[$mlt_field]['help'] = t('Find similar content.');
+  $table[$mlt_field]['argument']['id'] = 'search_api_more_like_this';
+  if ($mlt_field != 'search_api_more_like_this') {
+    $table[$mlt_field]['real field'] = 'search_api_more_like_this';
+  }
+
+  // @todo Add an "All taxonomy terms" contextual filter (if applicable).
+}
+
+/**
+ * Creates a Views table definition for one datasource of an index.
+ *
+ * @param \Drupal\search_api\Datasource\DatasourceInterface $datasource
+ *   The datasource for which to create a table definition.
+ * @param array $data
+ *   The existing Views data definitions. Passed by reference so additionally
+ *   needed tables can be inserted.
+ *
+ * @return array
+ *   A Views table definition for the given datasource.
+ */
+function _search_api_views_datasource_table(DatasourceInterface $datasource, array &$data) {
+  $datasource_id = $datasource->getPluginId();
+  $table = [
+    'table' => [
+      'group' => t('@datasource datasource', ['@datasource' => $datasource->label()]),
+      'index' => $datasource->getIndex()->id(),
+      'datasource' => $datasource_id,
+    ],
+  ];
+  $entity_type_id = $datasource->getEntityTypeId();
+  if ($entity_type_id) {
+    $table['table']['entity type'] = $entity_type_id;
+    $table['table']['entity revision'] = FALSE;
+  }
+
+  _search_api_views_add_handlers_for_properties($datasource->getPropertyDefinitions(), $table, $data);
+
+  // Prefix the "real field" of each entry with the datasource ID.
+  foreach ($table as $key => $definition) {
+    if ($key == 'table') {
+      continue;
+    }
+
+    $real_field = isset($definition['real field']) ? $definition['real field'] : $key;
+    $table[$key]['real field'] = Utility::createCombinedId($datasource_id, $real_field);
+
+    // Relationships sometimes have different real fields set, since they might
+    // also include the nested property that contains the actual reference. So,
+    // if a "real field" is set for that, we need to adapt it as well.
+    if (isset($definition['relationship']['real field'])) {
+      $real_field = $definition['relationship']['real field'];
+      $table[$key]['relationship']['real field'] = Utility::createCombinedId($datasource_id, $real_field);
+    }
+  }
+
+  return $table;
+}
+
+/**
+ * Creates a Views table definition for an entity type.
+ *
+ * @param string $entity_type_id
+ *   The ID of the entity type.
+ * @param array $data
+ *   The existing Views data definitions, passed by reference.
+ *
+ * @return array
+ *   A Views table definition for the given entity type. Or an empty array if
+ *   the entity type could not be found.
+ */
+function _search_api_views_entity_type_table($entity_type_id, array &$data) {
+  $entity_type = \Drupal::entityTypeManager()->getDefinition($entity_type_id);
+  if (!$entity_type || !$entity_type->entityClassImplements(FieldableEntityInterface::class)) {
+    return [];
+  }
+
+  $table = [
+    'table' => [
+      'group' => t('@entity_type relationship', ['@entity_type' => $entity_type->getLabel()]),
+      'entity type' => $entity_type_id,
+      'entity revision' => FALSE,
+    ],
+  ];
+
+  $entity_field_manager = \Drupal::getContainer()->get('entity_field.manager');
+  $bundle_info = \Drupal::getContainer()->get('entity_type.bundle.info');
+  $properties = $entity_field_manager->getBaseFieldDefinitions($entity_type_id);
+  foreach (array_keys($bundle_info->getBundleInfo($entity_type_id)) as $bundle_id) {
+    $additional = $entity_field_manager->getFieldDefinitions($entity_type_id, $bundle_id);
+    $properties += $additional;
+  }
+  _search_api_views_add_handlers_for_properties($properties, $table, $data);
+
+  return $table;
+}
+
+/**
+ * Adds field and relationship handlers for the given properties.
+ *
+ * @param \Drupal\Core\TypedData\DataDefinitionInterface[] $properties
+ *   The properties for which handlers should be added.
+ * @param array $table
+ *   The existing Views data table definition, passed by reference.
+ * @param array $data
+ *   The existing Views data definitions, passed by reference.
+ */
+function _search_api_views_add_handlers_for_properties(array $properties, array &$table, array &$data) {
+  $entity_reference_types = array_flip([
+    'field_item:entity_reference',
+    'field_item:image',
+    'field_item:file',
+  ]);
+
+  foreach ($properties as $property_path => $property) {
+    $key = _search_api_views_find_field_alias($property_path, $table);
+    $original_property = $property;
+    $property = \Drupal::getContainer()
+      ->get('search_api.fields_helper')
+      ->getInnerProperty($property);
+
+    // Add a field handler, if applicable.
+    $definition = _search_api_views_get_field_handler_for_property($property, $property_path);
+    if ($definition) {
+      $table[$key]['field'] = $definition;
+    }
+
+    // For entity-typed properties, also add a relationship to the entity type
+    // table.
+    if ($property instanceof FieldItemDataDefinition && isset($entity_reference_types[$property->getDataType()])) {
+      $entity_type_id = $property->getSetting('target_type');
+      if ($entity_type_id) {
+        $entity_type_table_key = 'search_api_entity_' . $entity_type_id;
+        if (!isset($data[$entity_type_table_key])) {
+          // Initialize the table definition before calling
+          // _search_api_views_entity_type_table() to avoid an infinite
+          // recursion.
+          $data[$entity_type_table_key] = [];
+          $data[$entity_type_table_key] = _search_api_views_entity_type_table($entity_type_id, $data);
+        }
+        // Add the relationship only if we have a non-empty table definition.
+        if ($data[$entity_type_table_key]) {
+          // Get the entity type to determine the label for the relationship.
+          $entity_type = \Drupal::entityTypeManager()
+            ->getDefinition($entity_type_id);
+          $entity_type_label = $entity_type ? $entity_type->getLabel() : $entity_type_id;
+          $args = [
+            '@label' => $entity_type_label,
+            '@field_name' => $original_property->getLabel(),
+          ];
+          // Look through the child properties to find the data reference
+          // property that should be the "real field" for the relationship.
+          // (For Core entity references, this will usually be ":entity".)
+          $suffix = '';
+          foreach ($property->getPropertyDefinitions() as $name => $nested_property) {
+            if ($nested_property instanceof DataReferenceDefinitionInterface) {
+              $suffix = ":$name";
+              break;
+            }
+          }
+          $table[$key]['relationship'] = [
+            'title' => t('@label referenced from @field_name', $args),
+            'label' => t('@field_name: @label', $args),
+            'help' => $property->getDescription() ?: t('(No description available)'),
+            'id' => 'search_api',
+            'base' => $entity_type_table_key,
+            'entity type' => $entity_type_id,
+            'entity revision' => FALSE,
+            'real field' => $property_path . $suffix,
+          ];
+        }
+      }
+    }
+
+    if (!empty($table[$key]) && empty($table[$key]['title'])) {
+      $table[$key]['title'] = $original_property->getLabel();
+      $table[$key]['help'] = $original_property->getDescription() ?: t('(No description available)');
+      if ($key != $property_path) {
+        $table[$key]['real field'] = $property_path;
+      }
+    }
+  }
+}
+
+/**
+ * Computes a handler definition for the given property.
+ *
+ * @param \Drupal\Core\TypedData\DataDefinitionInterface $property
+ *   The property definition.
+ * @param string|null $property_path
+ *   (optional) The property path of the property. If set, it will be used for
+ *   Field API fields to set the "field_name" property of the definition.
+ *
+ * @return array|null
+ *   Either a Views field handler definition for this property, or NULL if the
+ *   property shouldn't have one.
+ *
+ * @see hook_search_api_views_field_handler_mapping_alter()
+ */
+function _search_api_views_get_field_handler_for_property(DataDefinitionInterface $property, $property_path = NULL) {
+  $mappings = _search_api_views_get_field_handler_mapping();
+
+  // First, look for an exact match.
+  $data_type = $property->getDataType();
+  if (array_key_exists($data_type, $mappings['simple'])) {
+    $definition = $mappings['simple'][$data_type];
+  }
+  else {
+    // Then check all the patterns defined by regular expressions, defaulting to
+    // the "default" definition.
+    $definition = $mappings['default'];
+    foreach (array_keys($mappings['regex']) as $regex) {
+      if (preg_match($regex, $data_type)) {
+        $definition = $mappings['regex'][$regex];
+      }
+    }
+  }
+
+  // Field items have a special handler, but need a fallback handler set to be
+  // able to optionally circumvent entity field rendering. That's why we just
+  // set the "field_item:…" types to their fallback handlers in
+  // _search_api_views_get_field_handler_mapping(), along with non-field item
+  // types, and here manually update entity field properties to have the correct
+  // definition, with "search_api_field" handler, correct fallback handler and
+  // "field_name" and "entity_type" correctly set.
+  if (isset($definition) && $property instanceof FieldItemDataDefinition) {
+    list(, $field_name) = Utility::splitPropertyPath($property_path, TRUE);
+    if (!isset($definition['fallback_handler'])) {
+      $definition['fallback_handler'] = $definition['id'];
+      $definition['id'] = 'search_api_field';
+    }
+    $definition['field_name'] = $field_name;
+    $definition['entity_type'] = $property
+      ->getFieldDefinition()
+      ->getTargetEntityTypeId();
+  }
+
+  return $definition;
+}
+
+/**
+ * Retrieves the field handler mapping used by the Search API Views integration.
+ *
+ * @return array
+ *   An associative array with three keys:
+ *   - simple: An associative array mapping property data types to their field
+ *     handler definitions.
+ *   - regex: An array associative array mapping regular expressions for
+ *     property data types to their field handler definitions, ordered by
+ *     descending string length of the regular expression.
+ *   - default: The default definition for data types that match no other field.
+ */
+function _search_api_views_get_field_handler_mapping() {
+  $mappings = &drupal_static(__FUNCTION__);
+
+  if (!isset($mappings)) {
+    // First create a plain mapping and pass it to the alter hook.
+    $plain_mapping = [];
+
+    $plain_mapping['*'] = [
+      'id' => 'search_api',
+    ];
+
+    $text_mapping = [
+      'id' => 'search_api',
+      'filter_type' => 'xss',
+    ];
+    $plain_mapping['field_item:text_long'] = $text_mapping;
+    $plain_mapping['field_item:text_with_summary'] = $text_mapping;
+    $plain_mapping['search_api_html'] = $text_mapping;
+    unset($text_mapping['filter_type']);
+    $plain_mapping['search_api_text'] = $text_mapping;
+
+    $numeric_mapping = [
+      'id' => 'search_api_numeric',
+    ];
+    $plain_mapping['field_item:integer'] = $numeric_mapping;
+    $plain_mapping['field_item:list_integer'] = $numeric_mapping;
+    $plain_mapping['integer'] = $numeric_mapping;
+    $plain_mapping['timespan'] = $numeric_mapping;
+
+    $float_mapping = [
+      'id' => 'search_api_numeric',
+      'float' => TRUE,
+    ];
+    $plain_mapping['field_item:decimal'] = $float_mapping;
+    $plain_mapping['field_item:float'] = $float_mapping;
+    $plain_mapping['field_item:list_float'] = $float_mapping;
+    $plain_mapping['decimal'] = $float_mapping;
+    $plain_mapping['float'] = $float_mapping;
+
+    $date_mapping = [
+      'id' => 'search_api_date',
+    ];
+    $plain_mapping['field_item:created'] = $date_mapping;
+    $plain_mapping['field_item:changed'] = $date_mapping;
+    $plain_mapping['datetime_iso8601'] = $date_mapping;
+    $plain_mapping['timestamp'] = $date_mapping;
+
+    $bool_mapping = [
+      'id' => 'search_api_boolean',
+    ];
+    $plain_mapping['boolean'] = $bool_mapping;
+    $plain_mapping['field_item:boolean'] = $bool_mapping;
+
+    $ref_mapping = [
+      'id' => 'search_api_entity',
+    ];
+    $plain_mapping['field_item:entity_reference'] = $ref_mapping;
+    $plain_mapping['field_item:comment'] = $ref_mapping;
+
+    // Finally, set a default handler for unknown field items.
+    $plain_mapping['field_item:*'] = [
+      'id' => 'search_api',
+    ];
+
+    // Let other modules change or expand this mapping.
+    $alter_id = 'search_api_views_field_handler_mapping';
+    \Drupal::moduleHandler()->alter($alter_id, $plain_mapping);
+
+    // Then create a new, more practical structure, with the mappings grouped by
+    // mapping type.
+    $mappings = [
+      'simple' => [],
+      'regex' => [],
+      'default' => NULL,
+    ];
+    foreach ($plain_mapping as $type => $definition) {
+      if ($type == '*') {
+        $mappings['default'] = $definition;
+      }
+      elseif (strpos($type, '*') === FALSE) {
+        $mappings['simple'][$type] = $definition;
+      }
+      else {
+        // Transform the type into a PCRE regular expression, taking care to
+        // quote everything except for the wildcards.
+        $parts = explode('*', $type);
+        // Passing the second parameter to preg_quote() is a bit tricky with
+        // array_map(), we need to construct an array of slashes.
+        $slashes = array_fill(0, count($parts), '/');
+        $parts = array_map('preg_quote', $parts, $slashes);
+        // Use the "S" modifier for closer analysis of the pattern, since it
+        // might be executed a lot.
+        $regex = '/^' . implode('.*', $parts) . '$/S';
+        $mappings['regex'][$regex] = $definition;
+      }
+    }
+    // Finally, order the regular expressions descending by their lengths.
+    $compare = function ($a, $b) {
+      return strlen($b) - strlen($a);
+    };
+    uksort($mappings['regex'], $compare);
+  }
+
+  return $mappings;
+}

+ 44 - 0
sites/all/modules/contrib/search/search_api/src/Annotation/SearchApiBackend.php

@@ -0,0 +1,44 @@
+<?php
+
+namespace Drupal\search_api\Annotation;
+
+use Drupal\Component\Annotation\Plugin;
+
+/**
+ * Defines a Search API backend annotation object.
+ *
+ * @see \Drupal\search_api\Backend\BackendPluginManager
+ * @see \Drupal\search_api\Backend\BackendInterface
+ * @see \Drupal\search_api\Backend\BackendPluginBase
+ * @see plugin_api
+ *
+ * @Annotation
+ */
+class SearchApiBackend extends Plugin {
+
+  /**
+   * The backend plugin ID.
+   *
+   * @var string
+   */
+  public $id;
+
+  /**
+   * The human-readable name of the backend plugin.
+   *
+   * @ingroup plugin_translatable
+   *
+   * @var \Drupal\Core\Annotation\Translation
+   */
+  public $label;
+
+  /**
+   * The backend description.
+   *
+   * @ingroup plugin_translatable
+   *
+   * @var \Drupal\Core\Annotation\Translation
+   */
+  public $description;
+
+}

+ 60 - 0
sites/all/modules/contrib/search/search_api/src/Annotation/SearchApiDataType.php

@@ -0,0 +1,60 @@
+<?php
+
+namespace Drupal\search_api\Annotation;
+
+use Drupal\Component\Annotation\Plugin;
+
+/**
+ * Defines a Search API data type annotation object.
+ *
+ * @see \Drupal\search_api\DataType\DataTypePluginManager
+ * @see \Drupal\search_api\DataType\DataTypeInterface
+ * @see \Drupal\search_api\DataType\DataTypePluginBase
+ * @see plugin_api
+ *
+ * @Annotation
+ */
+class SearchApiDataType extends Plugin {
+
+  /**
+   * The data type plugin ID.
+   *
+   * @var string
+   */
+  public $id;
+
+  /**
+   * The human-readable name of the data type plugin.
+   *
+   * @ingroup plugin_translatable
+   *
+   * @var \Drupal\Core\Annotation\Translation
+   */
+  public $label;
+
+  /**
+   * The description of the data type.
+   *
+   * @ingroup plugin_translatable
+   *
+   * @var \Drupal\Core\Annotation\Translation
+   */
+  public $description;
+
+  /**
+   * Whether this is one of the default data types provided by the Search API.
+   *
+   * @var bool
+   */
+  public $default = FALSE;
+
+  /**
+   * The ID of the fallback data type for this data type.
+   *
+   * Needs to be one of the default data types defined in the Search API itself.
+   *
+   * @var string
+   */
+  public $fallback_type = 'string';
+
+}

+ 44 - 0
sites/all/modules/contrib/search/search_api/src/Annotation/SearchApiDatasource.php

@@ -0,0 +1,44 @@
+<?php
+
+namespace Drupal\search_api\Annotation;
+
+use Drupal\Component\Annotation\Plugin;
+
+/**
+ * Defines a Search API datasource annotation object.
+ *
+ * @see \Drupal\search_api\Datasource\DatasourcePluginManager
+ * @see \Drupal\search_api\Datasource\DatasourceInterface
+ * @see \Drupal\search_api\Datasource\DatasourcePluginBase
+ * @see plugin_api
+ *
+ * @Annotation
+ */
+class SearchApiDatasource extends Plugin {
+
+  /**
+   * The datasource plugin ID.
+   *
+   * @var string
+   */
+  public $id;
+
+  /**
+   * The human-readable name of the datasource plugin.
+   *
+   * @var \Drupal\Core\Annotation\Translation
+   *
+   * @ingroup plugin_translatable
+   */
+  public $label;
+
+  /**
+   * The description of the datasource.
+   *
+   * @var \Drupal\Core\Annotation\Translation
+   *
+   * @ingroup plugin_translatable
+   */
+  public $description;
+
+}

+ 58 - 0
sites/all/modules/contrib/search/search_api/src/Annotation/SearchApiDisplay.php

@@ -0,0 +1,58 @@
+<?php
+
+namespace Drupal\search_api\Annotation;
+
+use Drupal\Component\Annotation\Plugin;
+
+/**
+ * Defines a Search API display annotation object.
+ *
+ * @see \Drupal\search_api\Display\DisplayPluginManager
+ * @see \Drupal\search_api\Display\DisplayInterface
+ * @see \Drupal\search_api\Display\DisplayPluginBase
+ * @see plugin_api
+ *
+ * @Annotation
+ */
+class SearchApiDisplay extends Plugin {
+
+  /**
+   * The display plugin ID.
+   *
+   * @var string
+   */
+  public $id;
+
+  /**
+   * The human-readable name of the display plugin.
+   *
+   * @ingroup plugin_translatable
+   *
+   * @var \Drupal\Core\Annotation\Translation
+   */
+  public $label;
+
+  /**
+   * The human-readable description for the display plugin.
+   *
+   * @ingroup plugin_translatable
+   *
+   * @var \Drupal\Core\Annotation\Translation
+   */
+  public $description;
+
+  /**
+   * The ID of the display's index.
+   *
+   * @var string
+   */
+  public $index;
+
+  /**
+   * The path to the search display, if any.
+   *
+   * @var string|null
+   */
+  public $path;
+
+}

+ 44 - 0
sites/all/modules/contrib/search/search_api/src/Annotation/SearchApiParseMode.php

@@ -0,0 +1,44 @@
+<?php
+
+namespace Drupal\search_api\Annotation;
+
+use Drupal\Component\Annotation\Plugin;
+
+/**
+ * Defines a Search API parse mode annotation object.
+ *
+ * @see \Drupal\search_api\ParseMode\ParseModePluginManager
+ * @see \Drupal\search_api\ParseMode\ParseModeInterface
+ * @see \Drupal\search_api\ParseMode\ParseModePluginBase
+ * @see plugin_api
+ *
+ * @Annotation
+ */
+class SearchApiParseMode extends Plugin {
+
+  /**
+   * The parse mode plugin ID.
+   *
+   * @var string
+   */
+  public $id;
+
+  /**
+   * The human-readable name of the parse mode.
+   *
+   * @ingroup plugin_translatable
+   *
+   * @var \Drupal\Core\Annotation\Translation
+   */
+  public $label;
+
+  /**
+   * The description of the parse mode.
+   *
+   * @ingroup plugin_translatable
+   *
+   * @var \Drupal\Core\Annotation\Translation
+   */
+  public $description;
+
+}

+ 56 - 0
sites/all/modules/contrib/search/search_api/src/Annotation/SearchApiProcessor.php

@@ -0,0 +1,56 @@
+<?php
+
+namespace Drupal\search_api\Annotation;
+
+use Drupal\Component\Annotation\Plugin;
+
+/**
+ * Defines a Search API processor annotation object.
+ *
+ * @see \Drupal\search_api\Processor\ProcessorPluginManager
+ * @see \Drupal\search_api\Processor\ProcessorInterface
+ * @see \Drupal\search_api\Processor\ProcessorPluginBase
+ * @see plugin_api
+ *
+ * @Annotation
+ */
+class SearchApiProcessor extends Plugin {
+
+  /**
+   * The processor plugin ID.
+   *
+   * @var string
+   */
+  public $id;
+
+  /**
+   * The human-readable name of the processor plugin.
+   *
+   * @ingroup plugin_translatable
+   *
+   * @var \Drupal\Core\Annotation\Translation
+   */
+  public $label;
+
+  /**
+   * The description of the processor.
+   *
+   * @ingroup plugin_translatable
+   *
+   * @var \Drupal\Core\Annotation\Translation
+   */
+  public $description;
+
+  /**
+   * The stages this processor will run in, along with their default weights.
+   *
+   * This is represented as an associative array, mapping one or more of the
+   * stage identifiers to the default weight for that stage. For the available
+   * stages, see
+   * \Drupal\search_api\Processor\ProcessorPluginManager::getProcessingStages().
+   *
+   * @var int[]
+   */
+  public $stages;
+
+}

+ 44 - 0
sites/all/modules/contrib/search/search_api/src/Annotation/SearchApiTracker.php

@@ -0,0 +1,44 @@
+<?php
+
+namespace Drupal\search_api\Annotation;
+
+use Drupal\Component\Annotation\Plugin;
+
+/**
+ * Defines a Search API tracker annotation object.
+ *
+ * @see \Drupal\search_api\Tracker\TrackerPluginManager
+ * @see \Drupal\search_api\Tracker\TrackerInterface
+ * @see \Drupal\search_api\Tracker\TrackerPluginBase
+ * @see plugin_api
+ *
+ * @Annotation
+ */
+class SearchApiTracker extends Plugin {
+
+  /**
+   * The tracker plugin ID.
+   *
+   * @var string
+   */
+  public $id;
+
+  /**
+   * The human-readable name of the tracker plugin.
+   *
+   * @ingroup plugin_translatable
+   *
+   * @var \Drupal\Core\Annotation\Translation
+   */
+  public $label;
+
+  /**
+   * The description of the tracker.
+   *
+   * @ingroup plugin_translatable
+   *
+   * @var \Drupal\Core\Annotation\Translation
+   */
+  public $description;
+
+}

+ 86 - 0
sites/all/modules/contrib/search/search_api/src/Backend/BackendInterface.php

@@ -0,0 +1,86 @@
+<?php
+
+namespace Drupal\search_api\Backend;
+
+use Drupal\search_api\Plugin\ConfigurablePluginInterface;
+use Drupal\search_api\ServerInterface;
+
+/**
+ * Defines an interface for search backend plugins.
+ *
+ * Consists of general plugin methods and the backend-specific methods defined
+ * in \Drupal\search_api\Backend\BackendSpecificInterface, as well as special
+ * CRUD "hook" methods that cannot be present on the server entity (which also
+ * implements \Drupal\search_api\Backend\BackendSpecificInterface).
+ *
+ * @see \Drupal\search_api\Annotation\SearchApiBackend
+ * @see \Drupal\search_api\Backend\BackendPluginManager
+ * @see \Drupal\search_api\Backend\BackendPluginBase
+ * @see plugin_api
+ */
+interface BackendInterface extends ConfigurablePluginInterface, BackendSpecificInterface {
+
+  /**
+   * Retrieves the server entity for this backend.
+   *
+   * @return \Drupal\search_api\ServerInterface
+   *   The server entity.
+   */
+  public function getServer();
+
+  /**
+   * Sets the server entity for this backend.
+   *
+   * @param \Drupal\search_api\ServerInterface $server
+   *   The server entity.
+   *
+   * @return $this
+   */
+  public function setServer(ServerInterface $server);
+
+  /**
+   * Reacts to the server's creation.
+   *
+   * Called once, when the server is first created. Allows the backend class to
+   * set up its necessary infrastructure.
+   */
+  public function postInsert();
+
+  /**
+   * Notifies the backend that its configuration is about to be updated.
+   *
+   * The server's $original property can be used to inspect the old
+   * configuration values.
+   *
+   * Take care, though, that the server at this point might be override-free and
+   * thus contain property values (and apparent changes) which will not actually
+   * go into effect. If this might influence the code in this method, you have
+   * to manually check for overrides to ensure no incorrect action is taken.
+   *
+   * @see \Drupal\search_api\Utility\Utility::getConfigOverrides()
+   */
+  public function preUpdate();
+
+  /**
+   * Notifies the backend that its configuration was updated.
+   *
+   * The server's $original property can be used to inspect the old
+   * configuration values.
+   *
+   * @return bool
+   *   TRUE, if the update requires reindexing of all content on the server.
+   */
+  public function postUpdate();
+
+  /**
+   * Notifies the backend that the server is about to be deleted.
+   *
+   * 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();
+
+}

+ 327 - 0
sites/all/modules/contrib/search/search_api/src/Backend/BackendPluginBase.php

@@ -0,0 +1,327 @@
+<?php
+
+namespace Drupal\search_api\Backend;
+
+use Drupal\search_api\Entity\Server;
+use Drupal\search_api\Item\ItemInterface;
+use Drupal\search_api\LoggerTrait;
+use Drupal\search_api\Query\QueryInterface;
+use Drupal\search_api\SearchApiException;
+use Drupal\search_api\IndexInterface;
+use Drupal\search_api\Plugin\ConfigurablePluginBase;
+use Drupal\search_api\ServerInterface;
+use Drupal\search_api\Utility\FieldsHelper;
+
+/**
+ * Defines a base class for backend plugins.
+ *
+ * Plugins extending this class need to define a plugin definition array through
+ * annotation. These definition arrays may be altered through
+ * hook_search_api_backend_info_alter(). The definition includes the following
+ * keys:
+ * - id: The unique, system-wide identifier of the backend class.
+ * - label: The human-readable name of the backend class, translated.
+ * - description: A human-readable description for the backend class,
+ *   translated.
+ *
+ * A complete plugin definition should be written as in this example:
+ *
+ * @code
+ * @SearchApiBackend(
+ *   id = "my_backend",
+ *   label = @Translation("My backend"),
+ *   description = @Translation("Searches with SuperSearch™.")
+ * )
+ * @endcode
+ *
+ * @see \Drupal\search_api\Annotation\SearchApiBackend
+ * @see \Drupal\search_api\Backend\BackendPluginManager
+ * @see \Drupal\search_api\Backend\BackendInterface
+ * @see plugin_api
+ */
+abstract class BackendPluginBase extends ConfigurablePluginBase implements BackendInterface {
+
+  use LoggerTrait;
+
+  /**
+   * The server this backend is configured for.
+   *
+   * @var \Drupal\search_api\ServerInterface
+   */
+  protected $server;
+
+  /**
+   * The backend's server's ID.
+   *
+   * Used for serialization.
+   *
+   * @var string
+   */
+  protected $serverId;
+
+  /**
+   * The fields helper.
+   *
+   * @var \Drupal\search_api\Utility\FieldsHelper|null
+   */
+  protected $fieldsHelper;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(array $configuration, $plugin_id, array $plugin_definition) {
+    if (!empty($configuration['#server']) && $configuration['#server'] instanceof ServerInterface) {
+      $this->setServer($configuration['#server']);
+      unset($configuration['#server']);
+    }
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+  }
+
+  /**
+   * Retrieves the fields helper.
+   *
+   * @return \Drupal\search_api\Utility\FieldsHelper
+   *   The fields helper.
+   */
+  public function getFieldsHelper() {
+    return $this->fieldsHelper ?: \Drupal::service('search_api.fields_helper');
+  }
+
+  /**
+   * Sets the fields helper.
+   *
+   * @param \Drupal\search_api\Utility\FieldsHelper $fields_helper
+   *   The new fields helper.
+   *
+   * @return $this
+   */
+  public function setFieldsHelper(FieldsHelper $fields_helper) {
+    $this->fieldsHelper = $fields_helper;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getServer() {
+    return $this->server;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setServer(ServerInterface $server) {
+    $this->server = $server;
+    return $this;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function viewSettings() {
+    return [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isAvailable() {
+    return TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getSupportedFeatures() {
+    return [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function supportsDataType($type) {
+    return FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function postInsert() {}
+
+  /**
+   * {@inheritdoc}
+   */
+  public function preUpdate() {}
+
+  /**
+   * {@inheritdoc}
+   */
+  public function postUpdate() {
+    return FALSE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function preDelete() {
+    try {
+      $this->getServer()->deleteAllItems();
+    }
+    catch (SearchApiException $e) {
+      $vars = [
+        '%server' => $this->getServer()->label(),
+      ];
+      $this->logException($e, '%type while deleting items from server %server: @message in %function (line %line of %file).', $vars);
+      drupal_set_message($this->t('Deleting some of the items on the server failed. Check the logs for details. The server was still removed.'), 'error');
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getBackendDefinedFields(IndexInterface $index) {
+    return [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function addIndex(IndexInterface $index) {}
+
+  /**
+   * {@inheritdoc}
+   */
+  public function updateIndex(IndexInterface $index) {}
+
+  /**
+   * {@inheritdoc}
+   */
+  public function removeIndex($index) {
+    // Only delete the index's data if the index isn't read-only. (If only the
+    // ID is given, we assume the index was read-only, to be on the safe side.)
+    if ($index instanceof IndexInterface && !$index->isReadOnly()) {
+      $this->deleteAllIndexItems($index);
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDiscouragedProcessors() {
+    return [];
+  }
+
+  /**
+   * Creates dummy field objects for the "magic" fields present for every index.
+   *
+   * @param \Drupal\search_api\IndexInterface $index
+   *   The index for which to create the fields. (Needed since field objects
+   *   always need an index set.)
+   * @param \Drupal\search_api\Item\ItemInterface|null $item
+   *   (optional) If given, an item whose data should be used for the fields'
+   *   values.
+   *
+   * @return \Drupal\search_api\Item\FieldInterface[]
+   *   An array of field objects for all "magic" fields, keyed by field IDs.
+   */
+  protected function getSpecialFields(IndexInterface $index, ItemInterface $item = NULL) {
+    $field_info = [
+      'type' => 'string',
+      'original type' => 'string',
+    ];
+    $fields['search_api_id'] = $this->getFieldsHelper()
+      ->createField($index, 'search_api_id', $field_info);
+    $fields['search_api_datasource'] = $this->getFieldsHelper()
+      ->createField($index, 'search_api_datasource', $field_info);
+    $fields['search_api_language'] = $this->getFieldsHelper()
+      ->createField($index, 'search_api_language', $field_info);
+
+    if ($item) {
+      $fields['search_api_id']->setValues([$item->getId()]);
+      $fields['search_api_datasource']->setValues([$item->getDatasourceId()]);
+      $fields['search_api_language']->setValues([$item->getLanguage()]);
+    }
+
+    return $fields;
+  }
+
+  /**
+   * Verifies that the given condition operator is valid for this backend.
+   *
+   * @param string $operator
+   *   The operator in question.
+   *
+   * @throws \Drupal\search_api\SearchApiException
+   *   Thrown if the operator is not known.
+   *
+   * @see \Drupal\search_api\Query\ConditionSetInterface::addCondition()
+   */
+  protected function validateOperator($operator) {
+    switch ($operator) {
+      case '=':
+      case '<>':
+      case '<':
+      case '<=':
+      case '>=':
+      case '>':
+      case 'IN':
+      case 'NOT IN':
+      case 'BETWEEN':
+      case 'NOT BETWEEN':
+        return;
+    }
+    throw new SearchApiException("Unknown operator '$operator' used in search query condition");
+  }
+
+  /**
+   * Implements the magic __sleep() method.
+   *
+   * Prevents the server entity from being serialized.
+   */
+  public function __sleep() {
+    if ($this->server) {
+      $this->serverId = $this->server->id();
+    }
+    $properties = array_flip(parent::__sleep());
+    unset($properties['server']);
+    return array_keys($properties);
+  }
+
+  /**
+   * Implements the magic __wakeup() method.
+   *
+   * Reloads the server entity.
+   */
+  public function __wakeup() {
+    parent::__wakeup();
+
+    if ($this->serverId) {
+      $this->server = Server::load($this->serverId);
+      $this->serverId = NULL;
+    }
+  }
+
+  /**
+   * Retrieves the effective fulltext fields from the query.
+   *
+   * Automatically translates a NULL value in the query object to all fulltext
+   * fields in the search index.
+   *
+   * If a specific backend supports any "virtual" fulltext fields not listed in
+   * the index, it should override this method to add them, if appropriate.
+   *
+   * @param \Drupal\search_api\Query\QueryInterface $query
+   *   The search query.
+   *
+   * @return string[]
+   *   The fulltext fields in which to search for the search keys.
+   *
+   * @see \Drupal\search_api\Query\QueryInterface::getFulltextFields()
+   */
+  protected function getQueryFulltextFields(QueryInterface $query) {
+    $fulltext_fields = $query->getFulltextFields();
+    $index_fields = $query->getIndex()->getFulltextFields();
+    return $fulltext_fields === NULL ? $index_fields : array_intersect($fulltext_fields, $index_fields);
+  }
+
+}

+ 36 - 0
sites/all/modules/contrib/search/search_api/src/Backend/BackendPluginManager.php

@@ -0,0 +1,36 @@
+<?php
+
+namespace Drupal\search_api\Backend;
+
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Plugin\DefaultPluginManager;
+
+/**
+ * Manages search backend plugins.
+ *
+ * @see \Drupal\search_api\Annotation\SearchApiBackend
+ * @see \Drupal\search_api\Backend\BackendInterface
+ * @see \Drupal\search_api\Backend\BackendPluginBase
+ * @see plugin_api
+ */
+class BackendPluginManager extends DefaultPluginManager {
+
+  /**
+   * Constructs a BackendPluginManager object.
+   *
+   * @param \Traversable $namespaces
+   *   An object that implements \Traversable which contains the root paths
+   *   keyed by the corresponding namespace to look for plugin implementations.
+   * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
+   *   The cache backend instance to use.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler.
+   */
+  public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
+    parent::__construct('Plugin/search_api/backend', $namespaces, $module_handler, 'Drupal\search_api\Backend\BackendInterface', 'Drupal\search_api\Annotation\SearchApiBackend');
+    $this->setCacheBackend($cache_backend, 'search_api_backends');
+    $this->alterInfo('search_api_backend_info');
+  }
+
+}

+ 216 - 0
sites/all/modules/contrib/search/search_api/src/Backend/BackendSpecificInterface.php

@@ -0,0 +1,216 @@
+<?php
+
+namespace Drupal\search_api\Backend;
+
+use Drupal\search_api\IndexInterface;
+use Drupal\search_api\Query\QueryInterface;
+
+/**
+ * Defines methods common to search servers and backend plugins.
+ *
+ * This is separate from \Drupal\search_api\Backend\BackendInterface since the
+ * CRUD reaction methods in the server entity differ from those for the backend
+ * plugin.
+ */
+interface BackendSpecificInterface {
+
+  /**
+   * Returns additional, backend-specific information about this server.
+   *
+   * This information will be then added to the server's "View" tab in some way.
+   * In the default theme implementation the data is 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".
+   */
+  public function viewSettings();
+
+  /**
+   * Returns a boolean with the availability of the backend.
+   *
+   * This can implement a specific call to test if the backend is available for
+   * reading. For SOLR or elasticsearch this would be a "ping" to the server to
+   * test if it's online. If this is a db-backend that is running on a separate
+   * server, this can also be a ping. When it's a db-backend that runs in the
+   * same database as the drupal installation; just returning TRUE is enough.
+   *
+   * @return bool
+   *   The availability of the backend.
+   */
+  public function isAvailable();
+
+  /**
+   * Returns all features that this backend supports.
+   *
+   * Features are optional extensions to Search API functionality and usually
+   * defined and used by third-party modules.
+   *
+   * There are currently two features defined directly in the Search API module:
+   * - search_api_mlt, by the
+   *   \Drupal\search_api\Plugin\views\argument\SearchApiMoreLikeThis class.
+   * - search_api_random_sort, by the
+   *   \Drupal\search_api\Plugin\views\query\SearchApiQuery class.
+   *
+   * @return string[]
+   *   The identifiers of all features this backend supports.
+   *
+   * @see hook_search_api_server_features_alter()
+   */
+  public function getSupportedFeatures();
+
+  /**
+   * Determines whether the backend supports a given add-on data type.
+   *
+   * @param string $type
+   *   The identifier of the add-on data type.
+   *
+   * @return bool
+   *   TRUE if the backend supports that data type.
+   */
+  public function supportsDataType($type);
+
+  /**
+   * Limits the processors displayed in the UI for indexes on this server.
+   *
+   * Returns an array of processor IDs that should not be enabled for this
+   * backend. It is a bad idea, for example, to have the "Tokenizer" processor
+   * enabled when using a Solr backend.
+   *
+   * @return string[]
+   *   A list of processor IDs.
+   */
+  public function getDiscouragedProcessors();
+
+  /**
+   * Provides information on additional fields made available by the backend.
+   *
+   * If a backend indexes additional data with items and wants to make this
+   * available as fixed fields on the index (for example, to be used with
+   * Views), it can implement this method to facilitate this.
+   *
+   * Fields returned here are expected to work correctly with this server when
+   * used in query conditions, sorts or similar places.
+   *
+   * @param \Drupal\search_api\IndexInterface $index
+   *   The index for which fields are being determined.
+   *
+   * @return \Drupal\search_api\Item\FieldInterface[]
+   *   An array of additional fields that are available for this index, keyed by
+   *   their field IDs. The field IDs should always start with "search_api_"
+   *   (avoiding the special field IDs defined by
+   *   \Drupal\search_api\Query\QueryInterface::sort()) to avoid conflicts with
+   *   user-defined fields.
+   */
+  public function getBackendDefinedFields(IndexInterface $index);
+
+  /**
+   * 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 \Drupal\search_api\IndexInterface $index
+   *   The index to add.
+   *
+   * @throws \Drupal\search_api\SearchApiException
+   *   Thrown if an error occurred while adding the index.
+   */
+  public function addIndex(IndexInterface $index);
+
+  /**
+   * Notifies the server that an index attached to it has been changed.
+   *
+   * If any user action is necessary as a result of this, the method should
+   * use drupal_set_message() to notify the user.
+   *
+   * @param \Drupal\search_api\IndexInterface $index
+   *   The updated index.
+   *
+   * @throws \Drupal\search_api\SearchApiException
+   *   Thrown if an error occurred while reacting to the change.
+   */
+  public function updateIndex(IndexInterface $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->getServerId().
+   *
+   * If the index wasn't added to the server previously, the method call should
+   * be ignored.
+   *
+   * Implementations of this method should also check whether
+   * $index->isReadOnly() and don't delete any indexed data if it is.
+   *
+   * @param \Drupal\search_api\IndexInterface|string $index
+   *   Either an object representing the index to remove, or its ID (if the
+   *   index was completely deleted).
+   *
+   * @throws \Drupal\search_api\SearchApiException
+   *   Thrown if an error occurred while removing the index.
+   */
+  public function removeIndex($index);
+
+  /**
+   * Indexes the specified items.
+   *
+   * @param \Drupal\search_api\IndexInterface $index
+   *   The search index for which items should be indexed.
+   * @param \Drupal\search_api\Item\ItemInterface[] $items
+   *   An array of items to be indexed, keyed by their item IDs.
+   *
+   * @return string[]
+   *   The IDs of all items that were successfully indexed.
+   *
+   * @throws \Drupal\search_api\SearchApiException
+   *   Thrown if indexing was prevented by a fundamental configuration error.
+   */
+  public function indexItems(IndexInterface $index, array $items);
+
+  /**
+   * Deletes the specified items from the index.
+   *
+   * @param \Drupal\search_api\IndexInterface $index
+   *   The index from which items should be deleted.
+   * @param string[] $item_ids
+   *   The IDs of the deleted items.
+   *
+   * @throws \Drupal\search_api\SearchApiException
+   *   Thrown if an error occurred while trying to delete the items.
+   */
+  public function deleteItems(IndexInterface $index, array $item_ids);
+
+  /**
+   * Deletes all the items from the index.
+   *
+   * @param \Drupal\search_api\IndexInterface $index
+   *   The index for which items should be deleted.
+   * @param string|null $datasource_id
+   *   (optional) If given, only delete items from the datasource with the
+   *   given ID.
+   *
+   * @throws \Drupal\search_api\SearchApiException
+   *   Thrown if an error occurred while trying to delete indexed items.
+   */
+  public function deleteAllIndexItems(IndexInterface $index, $datasource_id = NULL);
+
+  /**
+   * Executes a search on this server.
+   *
+   * @param \Drupal\search_api\Query\QueryInterface $query
+   *   The query to execute.
+   *
+   * @throws \Drupal\search_api\SearchApiException
+   *   Thrown if an error prevented the search from completing.
+   */
+  public function search(QueryInterface $query);
+
+}

+ 458 - 0
sites/all/modules/contrib/search/search_api/src/Commands/SearchApiCommands.php

@@ -0,0 +1,458 @@
+<?php
+
+namespace Drupal\search_api\Commands;
+
+use Consolidation\OutputFormatters\StructuredData\RowsOfFields;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\search_api\Contrib\RowsOfMultiValueFields;
+use Drupal\search_api\Utility\CommandHelper;
+use Drush\Commands\DrushCommands;
+use Psr\Log\LoggerInterface;
+
+/**
+ * Defines Drush commands for the Search API.
+ */
+class SearchApiCommands extends DrushCommands {
+
+  /**
+   * The command helper.
+   *
+   * @var \Drupal\search_api\Utility\CommandHelper
+   */
+  protected $commandHelper;
+
+  /**
+   * Constructs a SearchApiCommands object.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
+   *   The entity type manager.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler
+   *   The module handler.
+   */
+  public function __construct(EntityTypeManagerInterface $entityTypeManager, ModuleHandlerInterface $moduleHandler) {
+    $this->commandHelper = new CommandHelper($entityTypeManager, $moduleHandler, 'dt');
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setLogger(LoggerInterface $logger) {
+    parent::setLogger($logger);
+    $this->commandHelper->setLogger($logger);
+  }
+
+  /**
+   * Lists all search indexes.
+   *
+   * @command search-api:list
+   *
+   * @usage drush search-api:list
+   *   List all search indexes.
+   *
+   * @field-labels
+   *   id: ID
+   *   name: Name
+   *   server: Server ID
+   *   serverName: Server name
+   *   types: Type IDs
+   *   typeNames: Type names
+   *   status: Status
+   *   limit: Limit
+   *
+   * @default-fields id,name,serverName,typeNames,status,limit
+   *
+   * @aliases sapi-l,search-api-list
+   *
+   * @return \Consolidation\OutputFormatters\StructuredData\RowsOfFields
+   *   The table rows.
+   *
+   * @throws \Drupal\search_api\SearchApiException
+   *   Thrown if an index has a server which couldn't be loaded.
+   */
+  public function listCommand() {
+    $rows = $this->commandHelper->indexListCommand();
+
+    return new RowsOfMultiValueFields($rows);
+  }
+
+  /**
+   * Enables one disabled search index.
+   *
+   * @param string $indexId
+   *   A search index ID.
+   *
+   * @command search-api:enable
+   *
+   * @usage drush search-api:enable node_index
+   *   Enable the search index with the ID node_index.
+   *
+   * @aliases sapi-en,search-api-enable
+   *
+   * @throws \Drupal\search_api\ConsoleException
+   *   Thrown if no indexes could be loaded.
+   */
+  public function enable($indexId) {
+    $this->commandHelper->enableIndexCommand([$indexId]);
+  }
+
+  /**
+   * Enables all disabled search indexes.
+   *
+   * @command search-api:enable-all
+   *
+   * @usage drush search-api:enable-all
+   *   Enable all disabled indexes.
+   * @usage drush sapi-ena
+   *   Alias to enable all disabled indexes.
+   *
+   * @aliases sapi-ena,search-api-enable-all
+   *
+   * @throws \Drupal\search_api\ConsoleException
+   *   Thrown if no indexes could be loaded.
+   */
+  public function enableAll() {
+    $this->commandHelper->enableIndexCommand();
+  }
+
+  /**
+   * Disables one or more enabled search indexes.
+   *
+   * @param string $indexId
+   *   A search index ID.
+   *
+   * @command search-api:disable
+   *
+   * @usage drush search-api:disable node_index
+   *   Disable the search index with the ID node_index.
+   * @usage drush sapi-dis node_index
+   *   Alias to disable the search index with the ID node_index.
+   *
+   * @aliases sapi-dis,search-api-disable
+   *
+   * @throws \Exception
+   *   If no indexes are defined or no index has been passed.
+   */
+  public function disable($indexId) {
+    $this->commandHelper->disableIndexCommand([$indexId]);
+  }
+
+  /**
+   * Disables all enabled search indexes.
+   *
+   * @command search-api:disable-all
+   *
+   * @usage drush search-api:disable-all
+   *   Disable all enabled indexes.
+   * @usage drush sapi-disa
+   *   Alias to disable all enabled indexes.
+   *
+   * @aliases sapi-disa,search-api-disable-all
+   *
+   * @throws \Drupal\search_api\ConsoleException
+   *   Thrown if no indexes could be loaded.
+   */
+  public function disableAll() {
+    $this->commandHelper->disableIndexCommand();
+  }
+
+  /**
+   * Shows the status of one or all search indexes.
+   *
+   * @param string|null $indexId
+   *   (optional) A search index ID, or NULL to show the status of all indexes.
+   *
+   * @command search-api:status
+   *
+   * @usage drush search-api:status
+   *   Show the status of all search indexes.
+   * @usage drush sapi-s
+   *   Alias to show the status of all search indexes.
+   * @usage drush sapi-s node_index
+   *   Show the status of the search index with the ID node_index.
+   *
+   * @field-labels
+   *   id: ID
+   *   name: Name
+   *   complete: % Complete
+   *   indexed: Indexed
+   *   total: Total
+   *
+   * @aliases sapi-s,search-api-status
+   *
+   * @return \Consolidation\OutputFormatters\StructuredData\RowsOfFields
+   *   The table rows.
+   *
+   * @throws \Drupal\search_api\SearchApiException
+   *   Thrown if one of the affected indexes had an invalid tracker set.
+   */
+  public function status($indexId = NULL) {
+    $rows = $this->commandHelper->indexStatusCommand([$indexId]);
+    return new RowsOfFields($rows);
+  }
+
+  /**
+   * Indexes items for one or all enabled search indexes.
+   *
+   * @param string $indexId
+   *   (optional) A search index ID, or NULL to index items for all enabled
+   *   indexes.
+   * @param array $options
+   *   (optional) An array of options.
+   *
+   * @command search-api:index
+   *
+   * @option limit
+   *   The maximum number of items to index. Set to 0 to index all items.
+   *   Defaults to 0 (index all).
+   * @option batch-size
+   *   The maximum number of items to index per batch run. Set to 0 to index all
+   *   items at once. Defaults to the "Cron batch size" setting of the index.
+   *
+   * @usage drush search-api:index
+   *   Index all items for all enabled indexes.
+   * @usage drush sapi-i
+   *   Alias to index all items for all enabled indexes.
+   * @usage drush sapi-i node_index
+   *   Index all items for the index with the ID node_index.
+   * @usage drush sapi-i node_index 100
+   *   Index a maximum number of 100 items for the index with the ID node_index.
+   * @usage drush sapi-i node_index 100 10
+   *   Index a maximum number of 100 items (10 items per batch run) for the
+   *   index with the ID node_index.
+   *
+   * @aliases sapi-i,search-api-index
+   *
+   * @throws \Exception
+   *   If a batch process could not be created.
+   */
+  public function index($indexId = NULL, array $options = ['limit' => NULL, 'batch-size' => NULL]) {
+    $limit = $options['limit'];
+    $batch_size = $options['batch-size'];
+    $process_batch = $this->commandHelper->indexItemsToIndexCommand([$indexId], $limit, $batch_size);
+
+    if ($process_batch === TRUE) {
+      drush_backend_batch_process();
+    }
+  }
+
+  /**
+   * Marks one or all indexes for reindexing without deleting existing data.
+   *
+   * @param string $indexId
+   *   The machine name of an index. Optional. If missed, will schedule all
+   *   search indexes for reindexing.
+   * @param array $options
+   *   An array of options.
+   *
+   * @command search-api:reset-tracker
+   *
+   * @option entity-types List of entity type ids to reset tracker for.
+   *
+   * @usage drush search-api:reset-tracker
+   *   Schedule all search indexes for reindexing.
+   * @usage drush sapi-r
+   *   Alias to schedule all search indexes for reindexing .
+   * @usage drush sapi-r node_index
+   *   Schedule the search index with the ID node_index for reindexing.
+   *
+   * @aliases search-api-mark-all,search-api-reindex,sapi-r,search-api-reset-tracker
+   *
+   * @throws \Drupal\search_api\SearchApiException
+   *   Thrown if one of the affected indexes had an invalid tracker set, or some
+   *   other internal error occurred.
+   */
+  public function resetTracker($indexId = NULL, array $options = ['entity-types' => []]) {
+    $this->commandHelper->resetTrackerCommand([$indexId], $options['entity-types']);
+  }
+
+  /**
+   * Clears one or all search indexes and marks them for reindexing.
+   *
+   * @param string $indexId
+   *   The machine name of an index. Optional. If missed all search indexes will
+   *   be cleared.
+   *
+   * @command search-api:clear
+   *
+   * @usage drush search-api:clear
+   *   Clear all search indexes.
+   * @usage drush sapi-c
+   *   Alias to clear all search indexes.
+   * @usage drush sapi-c node_index
+   *   Clear the search index with the ID node_index.
+   *
+   * @aliases sapi-c,search-api-clear
+   *
+   * @throws \Drupal\search_api\SearchApiException
+   *   Thrown if one of the affected indexes had an invalid tracker set, or some
+   *   other internal error occurred.
+   */
+  public function clear($indexId = NULL) {
+    $this->commandHelper->clearIndexCommand([$indexId]);
+  }
+
+  /**
+   * Searches for a keyword or phrase in a given index.
+   *
+   * @param string $indexId
+   *   The machine name of an index.
+   * @param string $keyword
+   *   The keyword to look for.
+   *
+   * @command search-api:search
+   *
+   * @usage drush search-api:search node_index title
+   *   Search for "title" inside the "node_index" index.
+   * @usage drush sapi-search node_index title
+   *   Alias to search for "title" inside the "node_index" index.
+   *
+   * @field-labels
+   *   id: ID
+   *   label: Label
+   *
+   * @aliases sapi-search,search-api-search
+   *
+   * @return \Consolidation\OutputFormatters\StructuredData\RowsOfFields
+   *   The table rows.
+   *
+   * @throws \Drupal\search_api\ConsoleException
+   *   Thrown if searching failed for any reason.
+   * @throws \Drupal\search_api\SearchApiException
+   *   Thrown if no search query could be created for the given index, for
+   *   example because it is disabled or its server could not be loaded.
+   */
+  public function search($indexId, $keyword) {
+    $rows = $this->commandHelper->searchIndexCommand($indexId, $keyword);
+
+    return new RowsOfFields($rows);
+  }
+
+  /**
+   * Lists all search servers.
+   *
+   * @command search-api:server-list
+   *
+   * @usage drush search-api:server-list
+   *   List all search servers.
+   * @usage drush sapi-sl
+   *   Alias to list all search servers.
+   *
+   * @field-labels
+   *   id: ID
+   *   name: Name
+   *   status: Status
+   *
+   * @aliases sapi-sl,search-api-server-list
+   *
+   * @return \Consolidation\OutputFormatters\StructuredData\RowsOfFields
+   *   The table rows.
+   *
+   * @throws \Drupal\search_api\ConsoleException
+   *   Thrown if no servers could be loaded.
+   */
+  public function serverList() {
+    $rows = $this->commandHelper->serverListCommand();
+
+    return new RowsOfFields($rows);
+  }
+
+  /**
+   * Enables a search server.
+   *
+   * @param string $serverId
+   *   The machine name of a server.
+   *
+   * @command search-api:server-enable
+   *
+   * @usage drush search-api:server-enable my_solr_server
+   *   Enable the my_solr_server search server.
+   * @usage drush sapi-se my_solr_server
+   *   Alias to enable the my_solr_server search server.
+   *
+   * @aliases sapi-se,search-api-server-enable
+   *
+   * @throws \Drupal\search_api\ConsoleException
+   *   Thrown if the server couldn't be loaded.
+   * @throws \Drupal\Core\Entity\EntityStorageException
+   *   Thrown if an internal error occurred when saving the server.
+   */
+  public function serverEnable($serverId) {
+    $this->commandHelper->enableServerCommand($serverId);
+  }
+
+  /**
+   * Disables a search server.
+   *
+   * @param string $serverId
+   *   The machine name of a server.
+   *
+   * @command search-api:server-disable
+   *
+   * @usage drush search-api:server-disable
+   *   Disable the my_solr_server search server.
+   * @usage drush sapi-sd
+   *   Alias to disable the my_solr_server search server.
+   *
+   * @aliases sapi-sd,search-api-server-disable
+   *
+   * @throws \Drupal\search_api\ConsoleException
+   *   Thrown if the server couldn't be loaded.
+   * @throws \Drupal\Core\Entity\EntityStorageException
+   *   Thrown if an internal error occurred when saving the server.
+   */
+  public function serverDisable($serverId) {
+    $this->commandHelper->disableServerCommand($serverId);
+  }
+
+  /**
+   * Clears all search indexes on the given search server.
+   *
+   * @param string $serverId
+   *   The machine name of a server.
+   *
+   * @command search-api:server-clear
+   *
+   * @usage drush search-api:server-clear my_solr_server
+   *   Clear all search indexes on the search server my_solr_server.
+   * @usage drush sapi-sc my_solr_server
+   *   Alias to clear all search indexes on the search server my_solr_server.
+   *
+   * @aliases sapi-sc,search-api-server-clear
+   *
+   * @throws \Drupal\search_api\ConsoleException
+   *   Thrown if the server couldn't be loaded.
+   * @throws \Drupal\search_api\SearchApiException
+   *   Thrown if one of the affected indexes had an invalid tracker set, or some
+   *   other internal error occurred.
+   */
+  public function serverClear($serverId) {
+    $this->commandHelper->clearServerCommand($serverId);
+  }
+
+  /**
+   * Sets the search server used by a given index.
+   *
+   * @param string $indexId
+   *   The machine name of an index.
+   * @param string $serverId
+   *   The machine name of a server.
+   *
+   * @command search-api:set-index-server
+   *
+   * @usage drush search-api:set-index-server default_node_index my_solr_server
+   *   Set the default_node_index index to used the my_solr_server server.
+   * @usage drush sapi-sis default_node_index my_solr_server
+   *   Alias to set the default_node_index index to used the my_solr_server
+   *   server.
+   *
+   * @aliases sapi-sis,search-api-set-index-server
+   *
+   * @throws \Exception
+   *   If no index or no server were passed or passed values are invalid.
+   */
+  public function setIndexServer($indexId, $serverId) {
+    $this->commandHelper->setIndexServerCommand($indexId, $serverId);
+  }
+
+}

+ 8 - 0
sites/all/modules/contrib/search/search_api/src/ConsoleException.php

@@ -0,0 +1,8 @@
+<?php
+
+namespace Drupal\search_api;
+
+/**
+ * Represents an exception that occurred in the console code of Search API.
+ */
+class ConsoleException extends SearchApiException {}

+ 43 - 0
sites/all/modules/contrib/search/search_api/src/Contrib/RowsOfMultiValueFields.php

@@ -0,0 +1,43 @@
+<?php
+
+namespace Drupal\search_api\Contrib;
+
+use Consolidation\OutputFormatters\Options\FormatterOptions;
+use Consolidation\OutputFormatters\StructuredData\RowsOfFields;
+use Consolidation\OutputFormatters\StructuredData\RenderCellInterface;
+
+/**
+ * Outputs multi-valued data as comma-separated values.
+ *
+ * This is used in the Drush integration.
+ */
+class RowsOfMultiValueFields extends RowsOfFields implements RenderCellInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function renderCell($key, $cellData, FormatterOptions $options, $rowData) {
+    if (is_array($cellData)) {
+      return static::arrayToString($cellData);
+    }
+    return $cellData;
+  }
+
+  /**
+   * Converts an array of string data into a comma separated string.
+   *
+   * @param array $array
+   *   A multidimensional array of string data.
+   *
+   * @return string
+   *   A comma separated string.
+   */
+  protected static function arrayToString(array $array) {
+    $elements = [];
+    foreach ($array as $element) {
+      $elements[] = is_array($element) ? '"' . self::arrayToString($element) . '"' : $element;
+    }
+    return implode(',', $elements);
+  }
+
+}

+ 49 - 0
sites/all/modules/contrib/search/search_api/src/Contrib/ViewsBulkOperationsEventSubscriber.php

@@ -0,0 +1,49 @@
+<?php
+
+namespace Drupal\search_api\Contrib;
+
+use Drupal\search_api\Plugin\views\query\SearchApiQuery;
+use Drupal\views_bulk_operations\ViewsBulkOperationsEvent;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+
+/**
+ * Provides an event subscriber that interfaces with Views Bulk Operations.
+ *
+ * This will provide VBO integration for search views by enabling VBO to
+ * retrieve the entities contained in search view result rows.
+ *
+ * @see \Drupal\views_bulk_operations\EventSubscriber\ViewsBulkOperationsEventSubscriber
+ */
+class ViewsBulkOperationsEventSubscriber implements EventSubscriberInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getSubscribedEvents() {
+    $events = [];
+    if (class_exists(ViewsBulkOperationsEvent::class)) {
+      $events[ViewsBulkOperationsEvent::NAME][] = 'provideViewData';
+    }
+    return $events;
+  }
+
+  /**
+   * Responds to view data request events.
+   *
+   * @var \Drupal\views_bulk_operations\ViewsBulkOperationsEvent $event
+   *   The event to respond to.
+   */
+  public function provideViewData(ViewsBulkOperationsEvent $event) {
+    $base_table = $event->getView()->storage->get('base_table');
+    $index = SearchApiQuery::getIndexFromTable($base_table);
+
+    if ($index) {
+      $event->setEntityTypeIds($index->getEntityTypes());
+
+      $event->setEntityGetter([
+        'callable' => [SearchApiQuery::class, 'getEntityFromRow'],
+      ]);
+    }
+  }
+
+}

+ 46 - 0
sites/all/modules/contrib/search/search_api/src/Controller/ExecuteTasksAccessCheck.php

@@ -0,0 +1,46 @@
+<?php
+
+namespace Drupal\search_api\Controller;
+
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Routing\Access\AccessInterface;
+use Drupal\search_api\Task\TaskManagerInterface;
+
+/**
+ * Provides an access check for the "Execute pending tasks" route.
+ */
+class ExecuteTasksAccessCheck implements AccessInterface {
+
+  /**
+   * The tasks manager service.
+   *
+   * @var \Drupal\search_api\Task\TaskManagerInterface
+   */
+  protected $tasksManager;
+
+  /**
+   * Creates an ExecuteTasksAccessCheck object.
+   *
+   * @param \Drupal\search_api\Task\TaskManagerInterface $tasksManager
+   *   The tasks manager service.
+   */
+  public function __construct(TaskManagerInterface $tasksManager) {
+    $this->tasksManager = $tasksManager;
+  }
+
+  /**
+   * Checks access.
+   *
+   * @return \Drupal\Core\Access\AccessResultInterface
+   *   The access result.
+   */
+  public function access() {
+    // @todo Once #2722237 is fixed, see whether this can't just use the
+    //   "search_api_task_list" cache tag instead.
+    if ($this->tasksManager->getTasksCount()) {
+      return AccessResult::allowed()->setCacheMaxAge(0);
+    }
+    return AccessResult::forbidden()->setCacheMaxAge(0);
+  }
+
+}

+ 183 - 0
sites/all/modules/contrib/search/search_api/src/Controller/IndexController.php

@@ -0,0 +1,183 @@
+<?php
+
+namespace Drupal\search_api\Controller;
+
+use Drupal\Component\Render\FormattableMarkup;
+use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Ajax\RemoveCommand;
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\EventSubscriber\AjaxResponseSubscriber;
+use Drupal\search_api\IndexInterface;
+use Drupal\search_api\SearchApiException;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpFoundation\RequestStack;
+use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+
+/**
+ * Provides route responses for search indexes.
+ */
+class IndexController extends ControllerBase {
+
+  /**
+   * The request stack.
+   *
+   * @var \Symfony\Component\HttpFoundation\RequestStack|null
+   */
+  protected $requestStack;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    /** @var static $controller */
+    $controller = parent::create($container);
+
+    $controller->setRequestStack($container->get('request_stack'));
+
+    return $controller;
+  }
+
+  /**
+   * Retrieves the request stack.
+   *
+   * @return \Symfony\Component\HttpFoundation\RequestStack
+   *   The request stack.
+   */
+  public function getRequestStack() {
+    return $this->requestStack ?: \Drupal::service('request_stack');
+  }
+
+  /**
+   * Retrieves the current request.
+   *
+   * @return \Symfony\Component\HttpFoundation\Request|null
+   *   The current request.
+   */
+  public function getRequest() {
+    return $this->getRequestStack()->getCurrentRequest();
+  }
+
+  /**
+   * Sets the request stack.
+   *
+   * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
+   *   The new request stack.
+   *
+   * @return $this
+   */
+  public function setRequestStack(RequestStack $request_stack) {
+    $this->requestStack = $request_stack;
+    return $this;
+  }
+
+  /**
+   * Displays information about a search index.
+   *
+   * @param \Drupal\search_api\IndexInterface $search_api_index
+   *   The index to display.
+   *
+   * @return array
+   *   An array suitable for drupal_render().
+   */
+  public function page(IndexInterface $search_api_index) {
+    // Build the search index information.
+    $render = [
+      'view' => [
+        '#theme' => 'search_api_index',
+        '#index' => $search_api_index,
+      ],
+    ];
+    // Check if the index is enabled and can be written to.
+    if ($search_api_index->status() && !$search_api_index->isReadOnly()) {
+      // Attach the index status form.
+      $render['form'] = $this->formBuilder()->getForm('Drupal\search_api\Form\IndexStatusForm', $search_api_index);
+    }
+    return $render;
+  }
+
+  /**
+   * Returns the page title for an index's "View" tab.
+   *
+   * @param \Drupal\search_api\IndexInterface $search_api_index
+   *   The index that is displayed.
+   *
+   * @return string
+   *   The page title.
+   */
+  public function pageTitle(IndexInterface $search_api_index) {
+    return new FormattableMarkup('@title', ['@title' => $search_api_index->label()]);
+  }
+
+  /**
+   * Enables a search index without a confirmation form.
+   *
+   * @param \Drupal\search_api\IndexInterface $search_api_index
+   *   The index to be enabled.
+   *
+   * @return \Symfony\Component\HttpFoundation\Response
+   *   The response to send to the browser.
+   */
+  public function indexBypassEnable(IndexInterface $search_api_index) {
+    // Enable the index.
+    $search_api_index->setStatus(TRUE)->save();
+
+    // \Drupal\search_api\Entity\Index::preSave() doesn't allow an index to be
+    // enabled if its server is not set or disabled.
+    if ($search_api_index->status()) {
+      // Notify the user about the status change.
+      drupal_set_message($this->t('The search index %name has been enabled.', ['%name' => $search_api_index->label()]));
+    }
+    else {
+      // Notify the user that the status change did not succeed.
+      drupal_set_message($this->t('The search index %name could not be enabled. Check if its server is set and enabled.', ['%name' => $search_api_index->label()]));
+    }
+
+    // Redirect to the index's "View" page.
+    $url = $search_api_index->toUrl('canonical');
+    return $this->redirect($url->getRouteName(), $url->getRouteParameters());
+  }
+
+  /**
+   * Removes a field from a search index.
+   *
+   * @param \Drupal\search_api\IndexInterface $search_api_index
+   *   The search index.
+   * @param string $field_id
+   *   The ID of the field to remove.
+   *
+   * @return \Symfony\Component\HttpFoundation\Response
+   *   The response to send to the browser.
+   *
+   * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
+   *   Thrown when the field was not found.
+   */
+  public function removeField(IndexInterface $search_api_index, $field_id) {
+    $fields = $search_api_index->getFields();
+    $success = FALSE;
+    if (isset($fields[$field_id])) {
+      try {
+        $search_api_index->removeField($field_id);
+        $search_api_index->save();
+        $success = TRUE;
+      }
+      catch (SearchApiException $e) {
+        $args['%field'] = $fields[$field_id]->getLabel();
+        drupal_set_message($this->t('The field %field is locked and cannot be removed.', $args), 'error');
+      }
+    }
+    else {
+      throw new NotFoundHttpException();
+    }
+
+    // If this is an AJAX request, just remove the row in question.
+    if ($success && $this->getRequest()->request->get(AjaxResponseSubscriber::AJAX_REQUEST_PARAMETER)) {
+      $response = new AjaxResponse();
+      $response->addCommand(new RemoveCommand("tr[data-field-row-id='$field_id']"));
+      return $response;
+    }
+    // Redirect to the index's "Fields" page.
+    $url = $search_api_index->toUrl('fields');
+    return $this->redirect($url->getRouteName(), $url->getRouteParameters());
+  }
+
+}

+ 75 - 0
sites/all/modules/contrib/search/search_api/src/Controller/ServerController.php

@@ -0,0 +1,75 @@
+<?php
+
+namespace Drupal\search_api\Controller;
+
+use Drupal\Component\Render\FormattableMarkup;
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\search_api\ServerInterface;
+
+/**
+ * Provides block routines for search server-specific routes.
+ */
+class ServerController extends ControllerBase {
+
+  /**
+   * Displays information about a search server.
+   *
+   * @param \Drupal\search_api\ServerInterface $search_api_server
+   *   The server to display.
+   *
+   * @return array
+   *   An array suitable for drupal_render().
+   */
+  public function page(ServerInterface $search_api_server) {
+    // Build the search server information.
+    $render = [
+      'view' => [
+        '#theme' => 'search_api_server',
+        '#server' => $search_api_server,
+      ],
+      '#attached' => [
+        'library' => ['search_api/drupal.search_api.admin_css'],
+      ],
+    ];
+    // Check if the server is enabled.
+    if ($search_api_server->status()) {
+      // Attach the server status form.
+      $render['form'] = $this->formBuilder()->getForm('Drupal\search_api\Form\ServerStatusForm', $search_api_server);
+    }
+    return $render;
+  }
+
+  /**
+   * Returns the page title for a server's "View" tab.
+   *
+   * @param \Drupal\search_api\ServerInterface $search_api_server
+   *   The server that is displayed.
+   *
+   * @return string
+   *   The page title.
+   */
+  public function pageTitle(ServerInterface $search_api_server) {
+    return new FormattableMarkup('@title', ['@title' => $search_api_server->label()]);
+  }
+
+  /**
+   * Enables a search server without a confirmation form.
+   *
+   * @param \Drupal\search_api\ServerInterface $search_api_server
+   *   The server to be enabled.
+   *
+   * @return \Symfony\Component\HttpFoundation\Response
+   *   The response to send to the browser.
+   */
+  public function serverBypassEnable(ServerInterface $search_api_server) {
+    $search_api_server->setStatus(TRUE)->save();
+
+    // Notify the user about the status change.
+    drupal_set_message($this->t('The search server %name has been enabled.', ['%name' => $search_api_server->label()]));
+
+    // Redirect to the server's "View" page.
+    $url = $search_api_server->toUrl('canonical');
+    return $this->redirect($url->getRouteName(), $url->getRouteParameters());
+  }
+
+}

+ 65 - 0
sites/all/modules/contrib/search/search_api/src/Controller/TaskController.php

@@ -0,0 +1,65 @@
+<?php
+
+namespace Drupal\search_api\Controller;
+
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\Url;
+use Drupal\search_api\Task\TaskManagerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Returns responses for task-related routes.
+ */
+class TaskController extends ControllerBase {
+
+  /**
+   * The server task manager.
+   *
+   * @var \Drupal\search_api\Task\TaskManagerInterface|null
+   */
+  protected $taskManager;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    /** @var static $controller */
+    $controller = parent::create($container);
+
+    $controller->setTaskManager($container->get('search_api.task_manager'));
+
+    return $controller;
+  }
+
+  /**
+   * Retrieves the task manager.
+   *
+   * @return \Drupal\search_api\Task\TaskManagerInterface
+   *   The task manager.
+   */
+  public function getTaskManager() {
+    return $this->taskManager ?: \Drupal::service('search_api._task_manager');
+  }
+
+  /**
+   * Sets the task manager.
+   *
+   * @param \Drupal\search_api\Task\TaskManagerInterface $task_manager
+   *   The new task manager.
+   *
+   * @return $this
+   */
+  public function setTaskManager(TaskManagerInterface $task_manager) {
+    $this->taskManager = $task_manager;
+    return $this;
+  }
+
+  /**
+   * Executes all pending tasks.
+   */
+  public function executeTasks() {
+    $this->getTaskManager()->setTasksBatch();
+    return batch_process(Url::fromRoute('search_api.overview'));
+  }
+
+}

+ 64 - 0
sites/all/modules/contrib/search/search_api/src/DataType/DataTypeInterface.php

@@ -0,0 +1,64 @@
+<?php
+
+namespace Drupal\search_api\DataType;
+
+use Drupal\Component\Plugin\DerivativeInspectionInterface;
+use Drupal\Component\Plugin\PluginInspectionInterface;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+
+/**
+ * Defines an interface for data type plugins.
+ *
+ * @see \Drupal\search_api\Annotation\SearchApiDataType
+ * @see \Drupal\search_api\DataType\DataTypePluginManager
+ * @see \Drupal\search_api\DataType\DataTypePluginBase
+ * @see plugin_api
+ */
+interface DataTypeInterface extends PluginInspectionInterface, DerivativeInspectionInterface, ContainerFactoryPluginInterface {
+
+  /**
+   * Returns the label of the data type.
+   *
+   * @return string
+   *   The administration label.
+   */
+  public function label();
+
+  /**
+   * Returns the description of the data type.
+   */
+  public function getDescription();
+
+  /**
+   * Converts a field value to match the data type (if needed).
+   *
+   * @param mixed $value
+   *   The value to convert.
+   *
+   * @return mixed
+   *   The converted value.
+   */
+  public function getValue($value);
+
+  /**
+   * Returns the fallback default data type for this data type.
+   *
+   * @return string
+   *   The fallback default data type.
+   */
+  public function getFallbackType();
+
+  /**
+   * Determines whether this data type is a default data type.
+   *
+   * Default data types are provided by the Search API module itself and have to
+   * be supported by all backends. They therefore are the only ones that can be
+   * used as a fallback for other data types, and don't need to have a fallback
+   * type themselves.
+   *
+   * @return bool
+   *   TRUE if the data type is a default type, FALSE otherwise.
+   */
+  public function isDefault();
+
+}

+ 89 - 0
sites/all/modules/contrib/search/search_api/src/DataType/DataTypePluginBase.php

@@ -0,0 +1,89 @@
+<?php
+
+namespace Drupal\search_api\DataType;
+
+use Drupal\Core\Plugin\PluginBase;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Defines a base class from which other data type classes may extend.
+ *
+ * Plugins extending this class need to define a plugin definition array through
+ * annotation. These definition arrays may be altered through
+ * hook_search_api_data_type_info_alter(). The definition includes the following
+ * keys:
+ * - id: The unique, system-wide identifier of the data type class.
+ * - label: The human-readable name of the data type class, translated.
+ * - description: A human-readable description for the data type class,
+ *   translated.
+ * - fallback_type: (optional) The fallback data type for this data type. Needs
+ *   to be one of the default data types defined in the Search API itself.
+ *   Defaults to "string".
+ *
+ * A complete plugin definition should be written as in this example:
+ *
+ * @code
+ * @SearchApiDataType(
+ *   id = "my_data_type",
+ *   label = @Translation("My data type"),
+ *   description = @Translation("Some information about my data type"),
+ *   fallback_type = "string"
+ * )
+ * @endcode
+ *
+ * Search API comes with a couple of default data types. These have an extra
+ * "default" property in the annotation. It is not allowed for custom data type
+ * plugins to set this property.
+ *
+ * @see \Drupal\search_api\Annotation\SearchApiDataType
+ * @see \Drupal\search_api\DataType\DataTypePluginManager
+ * @see \Drupal\search_api\DataType\DataTypeInterface
+ * @see plugin_api
+ */
+abstract class DataTypePluginBase extends PluginBase implements DataTypeInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static($configuration, $plugin_id, $plugin_definition);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getValue($value) {
+    return $value;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFallbackType() {
+    return !empty($this->pluginDefinition['fallback_type']) ? $this->pluginDefinition['fallback_type'] : 'string';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isDefault() {
+    return !empty($this->pluginDefinition['default']);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function label() {
+    $plugin_definition = $this->getPluginDefinition();
+    return $plugin_definition['label'];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getDescription() {
+    $plugin_definition = $this->getPluginDefinition();
+    return $plugin_definition['description'];
+  }
+
+}

+ 119 - 0
sites/all/modules/contrib/search/search_api/src/DataType/DataTypePluginManager.php

@@ -0,0 +1,119 @@
+<?php
+
+namespace Drupal\search_api\DataType;
+
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Plugin\DefaultPluginManager;
+
+/**
+ * Manages data type plugins.
+ *
+ * @see \Drupal\search_api\Annotation\SearchApiDataType
+ * @see \Drupal\search_api\DataType\DataTypeInterface
+ * @see \Drupal\search_api\DataType\DataTypePluginBase
+ * @see plugin_api
+ */
+class DataTypePluginManager extends DefaultPluginManager {
+
+  /**
+   * Static cache for the data type definitions.
+   *
+   * @var \Drupal\search_api\DataType\DataTypeInterface[]
+   *
+   * @see \Drupal\search_api\DataType\DataTypePluginManager::createInstance()
+   * @see \Drupal\search_api\DataType\DataTypePluginManager::getInstances()
+   */
+  protected $dataTypes;
+
+  /**
+   * Whether all plugin instances have already been created.
+   *
+   * @var bool
+   *
+   * @see \Drupal\search_api\DataType\DataTypePluginManager::getInstances()
+   */
+  protected $allCreated = FALSE;
+
+  /**
+   * Constructs a DataTypePluginManager object.
+   *
+   * @param \Traversable $namespaces
+   *   An object that implements \Traversable which contains the root paths
+   *   keyed by the corresponding namespace to look for plugin implementations.
+   * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
+   *   Cache backend instance to use.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler.
+   */
+  public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
+    parent::__construct('Plugin/search_api/data_type', $namespaces, $module_handler, 'Drupal\search_api\DataType\DataTypeInterface', 'Drupal\search_api\Annotation\SearchApiDataType');
+
+    $this->setCacheBackend($cache_backend, 'search_api_data_type');
+    $this->alterInfo('search_api_data_type_info');
+  }
+
+  /**
+   * Creates or retrieves a data type plugin.
+   *
+   * @param string $plugin_id
+   *   The ID of the plugin being instantiated.
+   * @param array $configuration
+   *   (optional) An array of configuration relevant to the plugin instance.
+   *   Ignored for data type plugins.
+   *
+   * @return \Drupal\search_api\DataType\DataTypeInterface
+   *   The requested data type plugin.
+   *
+   * @throws \Drupal\Component\Plugin\Exception\PluginException
+   *   If the instance cannot be created, such as if the ID is invalid.
+   */
+  public function createInstance($plugin_id, array $configuration = []) {
+    if (empty($this->dataTypes[$plugin_id])) {
+      $this->dataTypes[$plugin_id] = parent::createInstance($plugin_id, $configuration);
+    }
+    return $this->dataTypes[$plugin_id];
+  }
+
+  /**
+   * Returns all known data types.
+   *
+   * @return \Drupal\search_api\DataType\DataTypeInterface[]
+   *   An array of data type plugins, keyed by type identifier.
+   */
+  public function getInstances() {
+    if (!$this->allCreated) {
+      $this->allCreated = TRUE;
+      if (!isset($this->dataTypes)) {
+        $this->dataTypes = [];
+      }
+
+      foreach ($this->getDefinitions() as $plugin_id => $definition) {
+        if (class_exists($definition['class']) && empty($this->dataTypes[$plugin_id])) {
+          $data_type = $this->createInstance($plugin_id);
+          $this->dataTypes[$plugin_id] = $data_type;
+        }
+      }
+    }
+
+    return $this->dataTypes;
+  }
+
+  /**
+   * Returns all field data types known by the Search API as an options list.
+   *
+   * @return string[]
+   *   An associative array with all recognized types as keys, mapped to their
+   *   translated display names.
+   *
+   * @see \Drupal\search_api\DataType\DataTypePluginManager::getInstances()
+   */
+  public function getInstancesOptions() {
+    $types = [];
+    foreach ($this->getInstances() as $id => $info) {
+      $types[$id] = $info->label();
+    }
+    return $types;
+  }
+
+}

+ 241 - 0
sites/all/modules/contrib/search/search_api/src/Datasource/DatasourceInterface.php

@@ -0,0 +1,241 @@
+<?php
+
+namespace Drupal\search_api\Datasource;
+
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\TypedData\ComplexDataInterface;
+use Drupal\search_api\Plugin\IndexPluginInterface;
+
+/**
+ * Describes a source for search items.
+ *
+ * A datasource is used to abstract the type of data that can be indexed and
+ * searched with the Search API. Content entities are supported by default (with
+ * the \Drupal\search_api\Plugin\search_api\datasource\ContentEntity
+ * datasource), but others can be added by other modules. Datasources provide
+ * all kinds of metadata for search items of their type, as well as loading and
+ * viewing functionality.
+ *
+ * Modules providing new datasources are also responsible for calling the
+ * appropriate track*() methods on all indexes that use that datasource when an
+ * item of that type is inserted, updated or deleted.
+ *
+ * Note that the two load methods in this interface do not receive the normal
+ * combined item IDs (that also include the datasource ID), but only the raw,
+ * datasource-specific IDs.
+ *
+ * @see \Drupal\search_api\Annotation\SearchApiDatasource
+ * @see \Drupal\search_api\Datasource\DatasourcePluginManager
+ * @see \Drupal\search_api\Datasource\DatasourcePluginBase
+ * @see plugin_api
+ */
+interface DatasourceInterface extends IndexPluginInterface {
+
+  /**
+   * Retrieves the properties exposed by the underlying complex data type.
+   *
+   * Property names have to start with a letter or an underscore, followed by
+   * any number of letters, numbers and underscores.
+   *
+   * @return \Drupal\Core\TypedData\DataDefinitionInterface[]
+   *   An associative array of property data types, keyed by the property name.
+   */
+  public function getPropertyDefinitions();
+
+  /**
+   * Loads an item.
+   *
+   * @param mixed $id
+   *   The datasource-specific ID of the item.
+   *
+   * @return \Drupal\Core\TypedData\ComplexDataInterface|null
+   *   The loaded item if it could be found, NULL otherwise.
+   */
+  public function load($id);
+
+  /**
+   * Loads multiple items.
+   *
+   * @param array $ids
+   *   An array of datasource-specific item IDs.
+   *
+   * @return \Drupal\Core\TypedData\ComplexDataInterface[]
+   *   An associative array of loaded items, keyed by their
+   *   (datasource-specific) IDs.
+   */
+  public function loadMultiple(array $ids);
+
+  /**
+   * Retrieves the unique ID of an object from this datasource.
+   *
+   * @param \Drupal\Core\TypedData\ComplexDataInterface $item
+   *   An object from this datasource.
+   *
+   * @return string|null
+   *   The datasource-internal, unique ID of the item. Or NULL if the given item
+   *   is no valid item of this datasource.
+   */
+  public function getItemId(ComplexDataInterface $item);
+
+  /**
+   * Retrieves a human-readable label for an item.
+   *
+   * @param \Drupal\Core\TypedData\ComplexDataInterface $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.
+   */
+  public function getItemLabel(ComplexDataInterface $item);
+
+  /**
+   * Retrieves the item's bundle.
+   *
+   * @param \Drupal\Core\TypedData\ComplexDataInterface $item
+   *   An item of this datasource's type.
+   *
+   * @return string
+   *   The bundle identifier of the item. Might be just the datasource
+   *   identifier or a similar pseudo-bundle if the datasource does not contain
+   *   any bundles.
+   *
+   * @see getBundles()
+   */
+  public function getItemBundle(ComplexDataInterface $item);
+
+  /**
+   * Retrieves the item's language.
+   *
+   * @param \Drupal\Core\TypedData\ComplexDataInterface $item
+   *   An item of this datasource's type.
+   *
+   * @return string
+   *   The language code of this item.
+   */
+  public function getItemLanguage(ComplexDataInterface $item);
+
+  /**
+   * Retrieves a URL at which the item can be viewed on the web.
+   *
+   * @param \Drupal\Core\TypedData\ComplexDataInterface $item
+   *   An item of this datasource's type.
+   *
+   * @return \Drupal\Core\Url|null
+   *   Either an object representing the URL of the given item, or NULL if the
+   *   item has no URL of its own.
+   */
+  public function getItemUrl(ComplexDataInterface $item);
+
+  /**
+   * Checks whether a user has permission to view the given item.
+   *
+   * @param \Drupal\Core\TypedData\ComplexDataInterface $item
+   *   An item of this datasource's type.
+   * @param \Drupal\Core\Session\AccountInterface|null $account
+   *   (optional) The user session for which to check access, or NULL to check
+   *   access for the current user.
+   *
+   * @return bool
+   *   TRUE if access is granted, FALSE otherwise.
+   */
+  public function checkItemAccess(ComplexDataInterface $item, AccountInterface $account = NULL);
+
+  /**
+   * Returns the available view modes for this datasource.
+   *
+   * @param string|null $bundle
+   *   (optional) The bundle for which to return the available view modes. Or
+   *   NULL to return all view modes for this datasource, across all bundles.
+   *
+   * @return string[]
+   *   An associative array of view mode labels, keyed by the view mode ID. Can
+   *   be empty if it isn't possible to view items of this datasource.
+   */
+  public function getViewModes($bundle = NULL);
+
+  /**
+   * Retrieves the bundles associated to this datasource.
+   *
+   * @return string[]
+   *   An associative array mapping the datasource's bundles' IDs to their
+   *   labels. If the datasource doesn't contain any bundles, a single
+   *   pseudo-bundle should be returned, usually equal to the datasource
+   *   identifier (and label).
+   */
+  public function getBundles();
+
+  /**
+   * Returns the render array for the provided item and view mode.
+   *
+   * @param \Drupal\Core\TypedData\ComplexDataInterface $item
+   *   The item to render.
+   * @param string $view_mode
+   *   (optional) The view mode that should be used to render the item.
+   * @param string|null $langcode
+   *   (optional) For which language the item should be rendered. Defaults to
+   *   the language the item has been loaded in.
+   *
+   * @return array
+   *   A render array for displaying the item.
+   */
+  public function viewItem(ComplexDataInterface $item, $view_mode, $langcode = NULL);
+
+  /**
+   * Returns the render array for the provided items and view mode.
+   *
+   * @param \Drupal\Core\TypedData\ComplexDataInterface[] $items
+   *   The items to render.
+   * @param string $view_mode
+   *   (optional) The view mode that should be used to render the items.
+   * @param string|null $langcode
+   *   (optional) For which language the items should be rendered. Defaults to
+   *   the language each item has been loaded in.
+   *
+   * @return array
+   *   A render array for displaying the items.
+   */
+  public function viewMultipleItems(array $items, $view_mode, $langcode = NULL);
+
+  /**
+   * Retrieves the entity type ID of items from this datasource, if any.
+   *
+   * @return string|null
+   *   If items from this datasource are all entities of a single entity type,
+   *   that type's ID; NULL otherwise.
+   */
+  public function getEntityTypeId();
+
+  /**
+   * Returns a list of IDs of items from this datasource.
+   *
+   * Returns all items IDs by default. However, to avoid issues for large data
+   * sets, plugins should also implement a paging mechanism (the details of
+   * which are up to the datasource to decide) which guarantees that all item
+   * IDs can be retrieved by repeatedly calling this method with increasing
+   * values for $page (starting with 0) until NULL is returned.
+   *
+   * @param int|null $page
+   *   The zero-based page of IDs to retrieve, for the paging mechanism
+   *   implemented by this datasource; or NULL to retrieve all items at once.
+   *
+   * @return string[]|null
+   *   An array with datasource-specific item IDs (that is, raw item IDs not
+   *   prefixed with the datasource ID); or NULL if there are no more items for
+   *   this and all following pages.
+   */
+  public function getItemIds($page = NULL);
+
+  /**
+   * Retrieves any dependencies of the given fields.
+   *
+   * @param string[] $fields
+   *   An array of property paths on this datasource, keyed by field IDs.
+   *
+   * @return string[][][]
+   *   An associative array containing the dependencies of the given fields. The
+   *   array is keyed by field ID and dependency type, the values are arrays
+   *   with dependency names.
+   */
+  public function getFieldDependencies(array $fields);
+
+}

+ 158 - 0
sites/all/modules/contrib/search/search_api/src/Datasource/DatasourcePluginBase.php

@@ -0,0 +1,158 @@
+<?php
+
+namespace Drupal\search_api\Datasource;
+
+use Drupal\Core\Language\Language;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\TypedData\ComplexDataInterface;
+use Drupal\Core\TypedData\TranslatableInterface;
+use Drupal\search_api\Plugin\IndexPluginBase;
+
+/**
+ * Defines a base class from which other datasources may extend.
+ *
+ * Plugins extending this class need to define a plugin definition array through
+ * annotation. These definition arrays may be altered through
+ * hook_search_api_datasource_info_alter(). The definition includes the
+ * following keys:
+ * - id: The unique, system-wide identifier of the datasource.
+ * - label: The human-readable name of the datasource, translated.
+ * - description: A human-readable description for the datasource, translated.
+ *
+ * A complete plugin definition should be written as in this example:
+ *
+ * @code
+ * @SearchApiDatasource(
+ *   id = "my_datasource",
+ *   label = @Translation("My datasource"),
+ *   description = @Translation("Exposes my custom items as a datasource."),
+ * )
+ * @endcode
+ *
+ * @see \Drupal\search_api\Annotation\SearchApiDatasource
+ * @see \Drupal\search_api\Datasource\DatasourcePluginManager
+ * @see \Drupal\search_api\Datasource\DatasourceInterface
+ * @see plugin_api
+ */
+abstract class DatasourcePluginBase extends IndexPluginBase implements DatasourceInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getPropertyDefinitions() {
+    return [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function load($id) {
+    $items = $this->loadMultiple([$id]);
+    return $items ? reset($items) : NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function loadMultiple(array $ids) {
+    return [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getItemLabel(ComplexDataInterface $item) {
+    return NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getItemBundle(ComplexDataInterface $item) {
+    return $this->getPluginId();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getItemLanguage(ComplexDataInterface $item) {
+    if ($item instanceof TranslatableInterface) {
+      return $item->language()->getId();
+    }
+    $item = $item->getValue();
+    if ($item instanceof TranslatableInterface) {
+      return $item->language()->getId();
+    }
+    return Language::LANGCODE_NOT_SPECIFIED;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getItemUrl(ComplexDataInterface $item) {
+    return NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function checkItemAccess(ComplexDataInterface $item, AccountInterface $account = NULL) {
+    return TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getViewModes($bundle = NULL) {
+    return [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getBundles() {
+    return [
+      $this->getPluginId() => $this->label(),
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function viewItem(ComplexDataInterface $item, $view_mode, $langcode = NULL) {
+    return [];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function viewMultipleItems(array $items, $view_mode, $langcode = NULL) {
+    $build = [];
+    foreach ($items as $key => $item) {
+      $build[$key] = $this->viewItem($item, $view_mode, $langcode);
+    }
+    return $build;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getEntityTypeId() {
+    return NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getItemIds($page = NULL) {
+    return NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFieldDependencies(array $fields) {
+    return [];
+  }
+
+}

+ 36 - 0
sites/all/modules/contrib/search/search_api/src/Datasource/DatasourcePluginManager.php

@@ -0,0 +1,36 @@
+<?php
+
+namespace Drupal\search_api\Datasource;
+
+use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Extension\ModuleHandlerInterface;
+use Drupal\Core\Plugin\DefaultPluginManager;
+
+/**
+ * Manages datasource plugins.
+ *
+ * @see \Drupal\search_api\Annotation\SearchApiDatasource
+ * @see \Drupal\search_api\Datasource\DatasourceInterface
+ * @see \Drupal\search_api\Datasource\DatasourcePluginBase
+ * @see plugin_api
+ */
+class DatasourcePluginManager extends DefaultPluginManager {
+
+  /**
+   * Constructs a DatasourcePluginManager object.
+   *
+   * @param \Traversable $namespaces
+   *   An object that implements \Traversable which contains the root paths
+   *   keyed by the corresponding namespace to look for plugin implementations.
+   * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
+   *   Cache backend instance to use.
+   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+   *   The module handler.
+   */
+  public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
+    parent::__construct('Plugin/search_api/datasource', $namespaces, $module_handler, 'Drupal\search_api\Datasource\DatasourceInterface', 'Drupal\search_api\Annotation\SearchApiDatasource');
+    $this->setCacheBackend($cache_backend, 'search_api_datasources');
+    $this->alterInfo('search_api_datasource_info');
+  }
+
+}

+ 68 - 0
sites/all/modules/contrib/search/search_api/src/Display/DisplayDeriverBase.php

@@ -0,0 +1,68 @@
+<?php
+
+namespace Drupal\search_api\Display;
+
+use Drupal\Component\Plugin\Derivative\DeriverBase;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
+use Drupal\Core\StringTranslation\StringTranslationTrait;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * A base class for display derivers.
+ */
+abstract class DisplayDeriverBase extends DeriverBase implements ContainerDeriverInterface {
+
+  use StringTranslationTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $derivatives = NULL;
+
+  /**
+   * The entity manager.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+   */
+  protected $entityTypeManager;
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, $base_plugin_id) {
+    $deriver = new static();
+
+    $entity_type_manager = $container->get('entity_type.manager');
+    $deriver->setEntityTypeManager($entity_type_manager);
+
+    $translation = $container->get('string_translation');
+    $deriver->setStringTranslation($translation);
+
+    return $deriver;
+  }
+
+  /**
+   * Retrieves the entity manager.
+   *
+   * @return \Drupal\Core\Entity\EntityTypeManagerInterface
+   *   The entity manager.
+   */
+  public function getEntityTypeManager() {
+    return $this->entityTypeManager;
+  }
+
+  /**
+   * Sets the entity manager.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+   *   The entity manager.
+   *
+   * @return $this
+   */
+  public function setEntityTypeManager(EntityTypeManagerInterface $entity_type_manager) {
+    $this->entityTypeManager = $entity_type_manager;
+    return $this;
+  }
+
+}

Някои файлове не бяха показани, защото твърде много файлове са промени