diff --git a/sites/all/modules/contrib/search/search_api/CHANGELOG.txt b/sites/all/modules/contrib/search/search_api/CHANGELOG.txt
new file mode 100644
index 000000000..85c35b3ea
--- /dev/null
+++ b/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.
diff --git a/sites/all/modules/contrib/search/search_api/LICENSE.txt b/sites/all/modules/contrib/search/search_api/LICENSE.txt
new file mode 100644
index 000000000..d159169d1
--- /dev/null
+++ b/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.
+
+
+ Copyright (C)
+
+ 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.
+
+ , 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.
diff --git a/sites/all/modules/contrib/search/search_api/README.txt b/sites/all/modules/contrib/search/search_api/README.txt
new file mode 100644
index 000000000..44b2e3d64
--- /dev/null
+++ b/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
diff --git a/sites/all/modules/contrib/search/search_api/composer.json b/sites/all/modules/contrib/search/search_api/composer.json
new file mode 100644
index 000000000..43bb2ef42
--- /dev/null
+++ b/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"
+ }
+ }
+ }
+}
diff --git a/sites/all/modules/contrib/search/search_api/config/install/search_api.settings.yml b/sites/all/modules/contrib/search/search_api/config/install/search_api.settings.yml
new file mode 100644
index 000000000..3d5fe5e8c
--- /dev/null
+++ b/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
diff --git a/sites/all/modules/contrib/search/search_api/config/optional/tour.tour.search-api-index-fields.yml b/sites/all/modules/contrib/search/search_api/config/optional/tour.tour.search-api-index-fields.yml
new file mode 100644
index 000000000..e98680a04
--- /dev/null
+++ b/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 "Data types" box 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
diff --git a/sites/all/modules/contrib/search/search_api/config/optional/tour.tour.search-api-index-form.yml b/sites/all/modules/contrib/search/search_api/config/optional/tour.tour.search-api-index-form.yml
new file mode 100644
index 000000000..8669147aa
--- /dev/null
+++ b/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
diff --git a/sites/all/modules/contrib/search/search_api/config/optional/tour.tour.search-api-index-processors.yml b/sites/all/modules/contrib/search/search_api/config/optional/tour.tour.search-api-index-processors.yml
new file mode 100644
index 000000000..008b5db54
--- /dev/null
+++ b/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
diff --git a/sites/all/modules/contrib/search/search_api/config/optional/tour.tour.search-api-index.yml b/sites/all/modules/contrib/search/search_api/config/optional/tour.tour.search-api-index.yml
new file mode 100644
index 000000000..bc207ddad
--- /dev/null
+++ b/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", see the module's documentation.
+ 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
diff --git a/sites/all/modules/contrib/search/search_api/config/optional/tour.tour.search-api-server-form.yml b/sites/all/modules/contrib/search/search_api/config/optional/tour.tour.search-api-server-form.yml
new file mode 100644
index 000000000..840aeca44
--- /dev/null
+++ b/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 "Solr", which requires to be set up separately.
+ weight: 4
+ attributes:
+ data-id: edit-backend
diff --git a/sites/all/modules/contrib/search/search_api/config/optional/tour.tour.search-api-server.yml b/sites/all/modules/contrib/search/search_api/config/optional/tour.tour.search-api-server.yml
new file mode 100644
index 000000000..4c9db8c1b
--- /dev/null
+++ b/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
diff --git a/sites/all/modules/contrib/search/search_api/config/schema/search_api.datasource.schema.yml b/sites/all/modules/contrib/search/search_api/config/schema/search_api.datasource.schema.yml
new file mode 100644
index 000000000..895f5b3b8
--- /dev/null
+++ b/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'
diff --git a/sites/all/modules/contrib/search/search_api/config/schema/search_api.index.schema.yml b/sites/all/modules/contrib/search/search_api/config/schema/search_api.index.schema.yml
new file mode 100644
index 000000000..60e879afd
--- /dev/null
+++ b/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'
diff --git a/sites/all/modules/contrib/search/search_api/config/schema/search_api.processor.schema.yml b/sites/all/modules/contrib/search/search_api/config/schema/search_api.processor.schema.yml
new file mode 100644
index 000000000..1a0018c1a
--- /dev/null
+++ b/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'
diff --git a/sites/all/modules/contrib/search/search_api/config/schema/search_api.schema.yml b/sites/all/modules/contrib/search/search_api/config/schema/search_api.schema.yml
new file mode 100644
index 000000000..bfbdf2246
--- /dev/null
+++ b/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'
diff --git a/sites/all/modules/contrib/search/search_api/config/schema/search_api.server.schema.yml b/sites/all/modules/contrib/search/search_api/config/schema/search_api.server.schema.yml
new file mode 100644
index 000000000..62dd39cf5
--- /dev/null
+++ b/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
diff --git a/sites/all/modules/contrib/search/search_api/config/schema/search_api.tracker.schema.yml b/sites/all/modules/contrib/search/search_api/config/schema/search_api.tracker.schema.yml
new file mode 100644
index 000000000..3af3715dc
--- /dev/null
+++ b/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'
diff --git a/sites/all/modules/contrib/search/search_api/config/schema/search_api.views.schema.yml b/sites/all/modules/contrib/search/search_api/config/schema/search_api.views.schema.yml
new file mode 100644
index 000000000..2ae3943c5
--- /dev/null
+++ b/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.
diff --git a/sites/all/modules/contrib/search/search_api/css/search_api.admin.css b/sites/all/modules/contrib/search/search_api/css/search_api.admin.css
new file mode 100644
index 000000000..4c8f52aaa
--- /dev/null
+++ b/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;
+}
diff --git a/sites/all/modules/contrib/search/search_api/drush.services.yml b/sites/all/modules/contrib/search/search_api/drush.services.yml
new file mode 100644
index 000000000..f619dfffd
--- /dev/null
+++ b/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 }
diff --git a/sites/all/modules/contrib/search/search_api/js/search_api.processors.js b/sites/all/modules/contrib/search/search_api/js/search_api.processors.js
new file mode 100644
index 000000000..c3e601c49
--- /dev/null
+++ b/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);
diff --git a/sites/all/modules/contrib/search/search_api/modules/search_api_db/README.txt b/sites/all/modules/contrib/search/search_api/modules/search_api_db/README.txt
new file mode 100644
index 000000000..32cb1ecfa
--- /dev/null
+++ b/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.
diff --git a/sites/all/modules/contrib/search/search_api/modules/search_api_db/config/install/search_api_db.settings.yml b/sites/all/modules/contrib/search/search_api/modules/search_api_db/config/install/search_api_db.settings.yml
new file mode 100644
index 000000000..d2f16c17d
--- /dev/null
+++ b/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
diff --git a/sites/all/modules/contrib/search/search_api/modules/search_api_db/config/schema/search_api_db.backend.schema.yml b/sites/all/modules/contrib/search/search_api/modules/search_api_db/config/schema/search_api_db.backend.schema.yml
new file mode 100644
index 000000000..b64c0a19a
--- /dev/null
+++ b/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
diff --git a/sites/all/modules/contrib/search/search_api/modules/search_api_db/config/schema/search_api_db.schema.yml b/sites/all/modules/contrib/search/search_api/modules/search_api_db/config/schema/search_api_db.schema.yml
new file mode 100644
index 000000000..ecb3aeb56
--- /dev/null
+++ b/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'
diff --git a/sites/all/modules/contrib/search/search_api/modules/search_api_db/search_api_db.api.php b/sites/all/modules/contrib/search/search_api/modules/search_api_db/search_api_db.api.php
new file mode 100644
index 000000000..9d39bdf81
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/modules/search_api_db/search_api_db.api.php
@@ -0,0 +1,38 @@
+getOption('custom_sql_conditions')) {
+ foreach ($custom as $condition) {
+ $db_query->condition($condition['field'], $condition['value'], $condition['operator']);
+ }
+ }
+}
+
+/**
+ * @} End of "addtogroup hooks".
+ */
diff --git a/sites/all/modules/contrib/search/search_api/modules/search_api_db/search_api_db.info.yml b/sites/all/modules/contrib/search/search_api/modules/search_api_db/search_api_db.info.yml
new file mode 100644
index 000000000..e290ca6d0
--- /dev/null
+++ b/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
diff --git a/sites/all/modules/contrib/search/search_api/modules/search_api_db/search_api_db.install b/sites/all/modules/contrib/search/search_api/modules/search_api_db/search_api_db.install
new file mode 100644
index 000000000..786d4c06b
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/modules/search_api_db/search_api_db.install
@@ -0,0 +1,78 @@
+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.');
+}
diff --git a/sites/all/modules/contrib/search/search_api/modules/search_api_db/search_api_db.module b/sites/all/modules/contrib/search/search_api/modules/search_api_db/search_api_db.module
new file mode 100644
index 000000000..adc9f5ab6
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/modules/search_api_db/search_api_db.module
@@ -0,0 +1,17 @@
+ [
+ 'group' => 'search_api',
+ ],
+ ];
+}
diff --git a/sites/all/modules/contrib/search/search_api/modules/search_api_db/search_api_db.services.yml b/sites/all/modules/contrib/search/search_api/modules/search_api_db/search_api_db.services.yml
new file mode 100644
index 000000000..4fe69b623
--- /dev/null
+++ b/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']
diff --git a/sites/all/modules/contrib/search/search_api/modules/search_api_db/search_api_db_defaults/README.txt b/sites/all/modules/contrib/search/search_api/modules/search_api_db/search_api_db_defaults/README.txt
new file mode 100644
index 000000000..5acc9d503
--- /dev/null
+++ b/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)
diff --git a/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 b/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
new file mode 100644
index 000000000..9215b0f60
--- /dev/null
+++ b/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
diff --git a/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 b/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
new file mode 100644
index 000000000..eca0c257f
--- /dev/null
+++ b/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
diff --git a/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 b/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
new file mode 100644
index 000000000..79e8ab464
--- /dev/null
+++ b/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
diff --git a/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 b/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
new file mode 100644
index 000000000..eba08beba
--- /dev/null
+++ b/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
diff --git a/sites/all/modules/contrib/search/search_api/modules/search_api_db/search_api_db_defaults/config/optional/search_api.index.default_index.yml b/sites/all/modules/contrib/search/search_api/modules/search_api_db/search_api_db_defaults/config/optional/search_api.index.default_index.yml
new file mode 100644
index 000000000..05395b1e3
--- /dev/null
+++ b/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
diff --git a/sites/all/modules/contrib/search/search_api/modules/search_api_db/search_api_db_defaults/config/optional/search_api.server.default_server.yml b/sites/all/modules/contrib/search/search_api/modules/search_api_db/search_api_db_defaults/config/optional/search_api.server.default_server.yml
new file mode 100644
index 000000000..5566720d3
--- /dev/null
+++ b/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
diff --git a/sites/all/modules/contrib/search/search_api/modules/search_api_db/search_api_db_defaults/config/optional/views.view.search_content.yml b/sites/all/modules/contrib/search/search_api/modules/search_api_db/search_api_db_defaults/config/optional/views.view.search_content.yml
new file mode 100644
index 000000000..d2bfb978a
--- /dev/null
+++ b/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: { }
diff --git a/sites/all/modules/contrib/search/search_api/modules/search_api_db/search_api_db_defaults/search_api_db_defaults.info.yml b/sites/all/modules/contrib/search/search_api/modules/search_api_db/search_api_db_defaults/search_api_db_defaults.info.yml
new file mode 100644
index 000000000..ca68b7720
--- /dev/null
+++ b/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
diff --git a/sites/all/modules/contrib/search/search_api/modules/search_api_db/search_api_db_defaults/search_api_db_defaults.install b/sites/all/modules/contrib/search/search_api/modules/search_api_db/search_api_db_defaults/search_api_db_defaults.install
new file mode 100644
index 000000000..249d37009
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/modules/search_api_db/search_api_db_defaults/search_api_db_defaults.install
@@ -0,0 +1,85 @@
+ ['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();
+}
diff --git a/sites/all/modules/contrib/search/search_api/modules/search_api_db/search_api_db_defaults/tests/src/Functional/IntegrationTest.php b/sites/all/modules/contrib/search/search_api/modules/search_api_db/search_api_db_defaults/tests/src/Functional/IntegrationTest.php
new file mode 100644
index 000000000..61dcad7c6
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/modules/search_api_db/search_api_db_defaults/tests/src/Functional/IntegrationTest.php
@@ -0,0 +1,181 @@
+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);
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/modules/search_api_db/src/DatabaseCompatibility/CaseSensitiveDatabase.php b/sites/all/modules/contrib/search/search_api/modules/search_api_db/src/DatabaseCompatibility/CaseSensitiveDatabase.php
new file mode 100644
index 000000000..5152c87e2
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/modules/search_api_db/src/DatabaseCompatibility/CaseSensitiveDatabase.php
@@ -0,0 +1,17 @@
+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));
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/modules/search_api_db/src/DatabaseCompatibility/MySql.php b/sites/all/modules/contrib/search/search_api/modules/search_api_db/src/DatabaseCompatibility/MySql.php
new file mode 100644
index 000000000..08112f206
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/modules/search_api_db/src/DatabaseCompatibility/MySql.php
@@ -0,0 +1,39 @@
+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);
+ }
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/modules/search_api_db/src/Plugin/search_api/backend/Database.php b/sites/all/modules/contrib/search/search_api/modules/search_api_db/src/Plugin/search_api/backend/Database.php
new file mode 100644
index 000000000..88f22e76e
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/modules/search_api_db/src/Plugin/search_api/backend/Database.php
@@ -0,0 +1,2738 @@
+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"). Caution: 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.
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.
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.
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);
+ }
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/modules/search_api_db/src/Tests/DatabaseTestsTrait.php b/sites/all/modules/contrib/search/search_api/modules/search_api_db/src/Tests/DatabaseTestsTrait.php
new file mode 100644
index 000000000..daa1eca54
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/modules/search_api_db/src/Tests/DatabaseTestsTrait.php
@@ -0,0 +1,67 @@
+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);
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/modules/search_api_db/src/Tests/Update/SearchApiDbUpdate8102Test.php b/sites/all/modules/contrib/search/search_api/modules/search_api_db/src/Tests/Update/SearchApiDbUpdate8102Test.php
new file mode 100644
index 000000000..bb0b1e4ef
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/modules/search_api_db/src/Tests/Update/SearchApiDbUpdate8102Test.php
@@ -0,0 +1,67 @@
+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');
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/modules/search_api_db/tests/fixtures/update/search-api-db-base.php b/sites/all/modules/contrib/search/search_api/modules/search_api_db/tests/fixtures/update/search-api-db-base.php
new file mode 100644
index 000000000..655c9a635
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/modules/search_api_db/tests/fixtures/update/search-api-db-base.php
@@ -0,0 +1,59 @@
+ 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();
+}
diff --git a/sites/all/modules/contrib/search/search_api/modules/search_api_db/tests/fixtures/update/search-api-db-update-8102.php b/sites/all/modules/contrib/search/search_api/modules/search_api_db/tests/fixtures/update/search-api-db-update-8102.php
new file mode 100644
index 000000000..0e42bde4e
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/modules/search_api_db/tests/fixtures/update/search-api-db-update-8102.php
@@ -0,0 +1,62 @@
+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);
+}
diff --git a/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 b/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
new file mode 100644
index 000000000..13fbf413b
--- /dev/null
+++ b/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
diff --git a/sites/all/modules/contrib/search/search_api/modules/search_api_db/tests/search_api_db_test_autocomplete/search_api_db_test_autocomplete.info.yml b/sites/all/modules/contrib/search/search_api/modules/search_api_db/tests/search_api_db_test_autocomplete/search_api_db_test_autocomplete.info.yml
new file mode 100644
index 000000000..d1793e789
--- /dev/null
+++ b/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
diff --git a/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 b/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
new file mode 100644
index 000000000..106bf436d
--- /dev/null
+++ b/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 @@
+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);
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/modules/search_api_db/tests/src/FunctionalJavascript/IntegrationTest.php b/sites/all/modules/contrib/search/search_api/modules/search_api_db/tests/src/FunctionalJavascript/IntegrationTest.php
new file mode 100644
index 000000000..3c629a383
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/modules/search_api_db/tests/src/FunctionalJavascript/IntegrationTest.php
@@ -0,0 +1,40 @@
+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');
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/modules/search_api_db/tests/src/Kernel/AutocompleteTest.php b/sites/all/modules/contrib/search/search_api/modules/search_api_db/tests/src/Kernel/AutocompleteTest.php
new file mode 100644
index 000000000..9f15316ac
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/modules/search_api_db/tests/src/Kernel/AutocompleteTest.php
@@ -0,0 +1,144 @@
+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);
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/modules/search_api_db/tests/src/Kernel/BackendTest.php b/sites/all/modules/contrib/search/search_api/modules/search_api_db/tests/src/Kernel/BackendTest.php
new file mode 100644
index 000000000..d6ac55080
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/modules/search_api_db/tests/src/Kernel/BackendTest.php
@@ -0,0 +1,753 @@
+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));
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/modules/search_api_views_taxonomy/search_api_views_taxonomy.info.yml b/sites/all/modules/contrib/search/search_api/modules/search_api_views_taxonomy/search_api_views_taxonomy.info.yml
new file mode 100644
index 000000000..813a86989
--- /dev/null
+++ b/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
diff --git a/sites/all/modules/contrib/search/search_api/modules/search_api_views_taxonomy/search_api_views_taxonomy.install b/sites/all/modules/contrib/search/search_api/modules/search_api_views_taxonomy/search_api_views_taxonomy.install
new file mode 100644
index 000000000..229a91972
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/modules/search_api_views_taxonomy/search_api_views_taxonomy.install
@@ -0,0 +1,24 @@
+ 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 uninstall this module.', [
+ ':uninstall' => Url::fromRoute('system.modules_uninstall')->toString(),
+ ]),
+ 'severity' => REQUIREMENT_WARNING,
+ ];
+
+ return $requirements;
+}
diff --git a/sites/all/modules/contrib/search/search_api/phpcs.xml b/sites/all/modules/contrib/search/search_api/phpcs.xml
new file mode 100644
index 000000000..6449d5959
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/phpcs.xml
@@ -0,0 +1,254 @@
+
+
+ Default PHP CodeSniffer configuration for the Search API module.
+ .
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+
+
+
+
+
+
+ 0
+
+
+ 0
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+
+
+ 0
+
+
+ 0
+
+
+
+ 0
+
+
+ 0
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+
+
+
+
+
+ 0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/sites/all/modules/contrib/search/search_api/search_api.api.php b/sites/all/modules/contrib/search/search_api/search_api.api.php
new file mode 100644
index 000000000..7860c9d3e
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/search_api.api.php
@@ -0,0 +1,354 @@
+ $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".
+ */
diff --git a/sites/all/modules/contrib/search/search_api/search_api.drush.inc b/sites/all/modules/contrib/search/search_api/search_api.drush.inc
new file mode 100644
index 000000000..43f900ad9
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/search_api.drush.inc
@@ -0,0 +1,449 @@
+ '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;
+}
diff --git a/sites/all/modules/contrib/search/search_api/search_api.info.yml b/sites/all/modules/contrib/search/search_api/search_api.info.yml
new file mode 100644
index 000000000..f7693e5d1
--- /dev/null
+++ b/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
diff --git a/sites/all/modules/contrib/search/search_api/search_api.install b/sites/all/modules/contrib/search/search_api/search_api.install
new file mode 100644
index 000000000..dca72e83c
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/search_api.install
@@ -0,0 +1,281 @@
+ '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;
+}
diff --git a/sites/all/modules/contrib/search/search_api/search_api.libraries.yml b/sites/all/modules/contrib/search/search_api/search_api.libraries.yml
new file mode 100644
index 000000000..ac1b451b9
--- /dev/null
+++ b/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
diff --git a/sites/all/modules/contrib/search/search_api/search_api.links.action.yml b/sites/all/modules/contrib/search/search_api/search_api.links.action.yml
new file mode 100644
index 000000000..5b99f87fa
--- /dev/null
+++ b/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
diff --git a/sites/all/modules/contrib/search/search_api/search_api.links.menu.yml b/sites/all/modules/contrib/search/search_api/search_api.links.menu.yml
new file mode 100644
index 000000000..897449dc8
--- /dev/null
+++ b/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
diff --git a/sites/all/modules/contrib/search/search_api/search_api.links.task.yml b/sites/all/modules/contrib/search/search_api/search_api.links.task.yml
new file mode 100644
index 000000000..8eb808c88
--- /dev/null
+++ b/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
diff --git a/sites/all/modules/contrib/search/search_api/search_api.module b/sites/all/modules/contrib/search/search_api/search_api.module
new file mode 100644
index 000000000..d2bd0e98f
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/search_api.module
@@ -0,0 +1,671 @@
+$message
$search_module_warning
";
+ }
+ 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 uninstall the Search module for performance reasons. For more information see the Search API handbook.', $args);
+ }
+ return NULL;
+}
diff --git a/sites/all/modules/contrib/search/search_api/search_api.permissions.yml b/sites/all/modules/contrib/search/search_api/search_api.permissions.yml
new file mode 100644
index 000000000..b515ad814
--- /dev/null
+++ b/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.'
diff --git a/sites/all/modules/contrib/search/search_api/search_api.plugin_type.yml b/sites/all/modules/contrib/search/search_api/search_api.plugin_type.yml
new file mode 100644
index 000000000..ffa129416
--- /dev/null
+++ b/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
diff --git a/sites/all/modules/contrib/search/search_api/search_api.routing.yml b/sites/all/modules/contrib/search/search_api/search_api.routing.yml
new file mode 100644
index 000000000..eb6bf603a
--- /dev/null
+++ b/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
diff --git a/sites/all/modules/contrib/search/search_api/search_api.services.yml b/sites/all/modules/contrib/search/search_api/search_api.services.yml
new file mode 100644
index 000000000..92f58db5d
--- /dev/null
+++ b/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 }
diff --git a/sites/all/modules/contrib/search/search_api/search_api.theme.inc b/sites/all/modules/contrib/search/search_api/search_api.theme.inc
new file mode 100644
index 000000000..6e04006f0
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/search_api.theme.inc
@@ -0,0 +1,506 @@
+ $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 .= '' . nl2br(Html::escape($description)) . '
';
+ }
+
+ // 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 (disable)', [':url' => $server->toUrl('disable')->toString()]);
+ }
+ else {
+ $classes[] = 'warning';
+ $info = t('disabled (enable)', [':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 .= '' . nl2br(Html::escape($description)) . '
';
+ }
+
+ // 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 (disable)', [':url' => $index->toUrl('disable')->toString()]);
+ }
+ // Check if a server is available and enabled.
+ elseif ($server && $server->status()) {
+ $classes[] = 'warning';
+ $info = t('disabled (enable)', [':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 (@indexed)', $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. (More information)', 'There are @count items indexed on the server for this index. (More information)', $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 .= '' . t('Index status') . '
';
+ $output .= '' . render($index_progress) . '
';
+ }
+ }
+
+ // 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;
+}
diff --git a/sites/all/modules/contrib/search/search_api/search_api.views.inc b/sites/all/modules/contrib/search/search_api/search_api.views.inc
new file mode 100644
index 000000000..8a80996cb
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/search_api.views.inc
@@ -0,0 +1,828 @@
+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;
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Annotation/SearchApiBackend.php b/sites/all/modules/contrib/search/search_api/src/Annotation/SearchApiBackend.php
new file mode 100644
index 000000000..7e1d2d762
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Annotation/SearchApiBackend.php
@@ -0,0 +1,44 @@
+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);
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Backend/BackendPluginManager.php b/sites/all/modules/contrib/search/search_api/src/Backend/BackendPluginManager.php
new file mode 100644
index 000000000..c498cc485
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Backend/BackendPluginManager.php
@@ -0,0 +1,36 @@
+setCacheBackend($cache_backend, 'search_api_backends');
+ $this->alterInfo('search_api_backend_info');
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Backend/BackendSpecificInterface.php b/sites/all/modules/contrib/search/search_api/src/Backend/BackendSpecificInterface.php
new file mode 100644
index 000000000..93e300f94
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Backend/BackendSpecificInterface.php
@@ -0,0 +1,216 @@
+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);
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Commands/SearchApiCommands.php b/sites/all/modules/contrib/search/search_api/src/Commands/SearchApiCommands.php
new file mode 100644
index 000000000..f7fdcba88
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Commands/SearchApiCommands.php
@@ -0,0 +1,458 @@
+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);
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/ConsoleException.php b/sites/all/modules/contrib/search/search_api/src/ConsoleException.php
new file mode 100644
index 000000000..7d1b1fef9
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/ConsoleException.php
@@ -0,0 +1,8 @@
+getView()->storage->get('base_table');
+ $index = SearchApiQuery::getIndexFromTable($base_table);
+
+ if ($index) {
+ $event->setEntityTypeIds($index->getEntityTypes());
+
+ $event->setEntityGetter([
+ 'callable' => [SearchApiQuery::class, 'getEntityFromRow'],
+ ]);
+ }
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Controller/ExecuteTasksAccessCheck.php b/sites/all/modules/contrib/search/search_api/src/Controller/ExecuteTasksAccessCheck.php
new file mode 100644
index 000000000..9f2f39f3c
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Controller/ExecuteTasksAccessCheck.php
@@ -0,0 +1,46 @@
+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);
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Controller/IndexController.php b/sites/all/modules/contrib/search/search_api/src/Controller/IndexController.php
new file mode 100644
index 000000000..91cfb5228
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Controller/IndexController.php
@@ -0,0 +1,183 @@
+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());
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Controller/ServerController.php b/sites/all/modules/contrib/search/search_api/src/Controller/ServerController.php
new file mode 100644
index 000000000..8cc5ed22c
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Controller/ServerController.php
@@ -0,0 +1,75 @@
+ [
+ '#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());
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Controller/TaskController.php b/sites/all/modules/contrib/search/search_api/src/Controller/TaskController.php
new file mode 100644
index 000000000..06c502068
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Controller/TaskController.php
@@ -0,0 +1,65 @@
+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'));
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/DataType/DataTypeInterface.php b/sites/all/modules/contrib/search/search_api/src/DataType/DataTypeInterface.php
new file mode 100644
index 000000000..ddbd41440
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/DataType/DataTypeInterface.php
@@ -0,0 +1,64 @@
+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'];
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/DataType/DataTypePluginManager.php b/sites/all/modules/contrib/search/search_api/src/DataType/DataTypePluginManager.php
new file mode 100644
index 000000000..770dbb9e1
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/DataType/DataTypePluginManager.php
@@ -0,0 +1,119 @@
+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;
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Datasource/DatasourceInterface.php b/sites/all/modules/contrib/search/search_api/src/Datasource/DatasourceInterface.php
new file mode 100644
index 000000000..5cd4ed1a0
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Datasource/DatasourceInterface.php
@@ -0,0 +1,241 @@
+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 [];
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Datasource/DatasourcePluginManager.php b/sites/all/modules/contrib/search/search_api/src/Datasource/DatasourcePluginManager.php
new file mode 100644
index 000000000..be9e684cd
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Datasource/DatasourcePluginManager.php
@@ -0,0 +1,36 @@
+setCacheBackend($cache_backend, 'search_api_datasources');
+ $this->alterInfo('search_api_datasource_info');
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Display/DisplayDeriverBase.php b/sites/all/modules/contrib/search/search_api/src/Display/DisplayDeriverBase.php
new file mode 100644
index 000000000..05dbefb31
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Display/DisplayDeriverBase.php
@@ -0,0 +1,68 @@
+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;
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Display/DisplayInterface.php b/sites/all/modules/contrib/search/search_api/src/Display/DisplayInterface.php
new file mode 100644
index 000000000..99af31db1
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Display/DisplayInterface.php
@@ -0,0 +1,73 @@
+setCurrentPath($container->get('path.current'));
+ $display->setEntityTypeManager($container->get('entity_type.manager'));
+
+ return $display;
+ }
+
+ /**
+ * Retrieves the current path service.
+ *
+ * @return \Drupal\Core\Path\CurrentPathStack
+ * The current path service.
+ */
+ public function getCurrentPath() {
+ return $this->currentPath ?: \Drupal::service('path.current');
+ }
+
+ /**
+ * Sets the current path service.
+ *
+ * @param \Drupal\Core\Path\CurrentPathStack $current_path
+ * The new current path service.
+ *
+ * @return $this
+ */
+ public function setCurrentPath(CurrentPathStack $current_path) {
+ $this->currentPath = $current_path;
+ return $this;
+ }
+
+ /**
+ * Retrieves the entity type manager.
+ *
+ * @return \Drupal\Core\Entity\EntityTypeManagerInterface
+ * The entity type manager.
+ */
+ public function getEntityTypeManager() {
+ return $this->entityTypeManager ?: \Drupal::service('entity_type.manager');
+ }
+
+ /**
+ * Sets the entity type manager.
+ *
+ * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+ * The new entity type manager.
+ *
+ * @return $this
+ */
+ public function setEntityTypeManager(EntityTypeManagerInterface $entity_type_manager) {
+ $this->entityTypeManager = $entity_type_manager;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function label() {
+ $plugin_definition = $this->getPluginDefinition();
+ return $plugin_definition['label'];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDescription() {
+ $plugin_definition = $this->getPluginDefinition();
+ return $plugin_definition['description'];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getIndex() {
+ $plugin_definition = $this->getPluginDefinition();
+ return $this->getEntityTypeManager()
+ ->getStorage('search_api_index')
+ ->load($plugin_definition['index']);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getUrl() {
+ if ($path = $this->getPath()) {
+ return Url::fromUserInput($path);
+ }
+ return NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getPath() {
+ $plugin_definition = $this->getPluginDefinition();
+ if (!empty($plugin_definition['path'])) {
+ return $plugin_definition['path'];
+ }
+ return NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isRenderedInCurrentRequest() {
+ if ($path = $this->getPath()) {
+ $current_path = $this->getCurrentPath()->getPath();
+ return $current_path == $path;
+ }
+ return FALSE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function calculateDependencies() {
+ $dependencies = [];
+
+ // By default, add dependencies to the module providing this display and to
+ // the index it is based on.
+ $definition = $this->getPluginDefinition();
+ $dependencies['module'][] = $definition['provider'];
+
+ $index = $this->getIndex();
+ $dependencies[$index->getConfigDependencyKey()][] = $index->getConfigDependencyName();
+
+ return $dependencies;
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Display/DisplayPluginManager.php b/sites/all/modules/contrib/search/search_api/src/Display/DisplayPluginManager.php
new file mode 100644
index 000000000..abac905ae
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Display/DisplayPluginManager.php
@@ -0,0 +1,64 @@
+setCacheBackend($cache_backend, 'search_api_displays');
+ $this->alterInfo('search_api_displays');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getInstances() {
+ if ($this->displays === NULL) {
+ $this->displays = [];
+
+ foreach ($this->getDefinitions() as $name => $display_definition) {
+ if (class_exists($display_definition['class']) && empty($this->displays[$name])) {
+ $display = $this->createInstance($name);
+ $this->displays[$name] = $display;
+ }
+ }
+ }
+
+ return $this->displays;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function clearCachedDefinitions() {
+ parent::clearCachedDefinitions();
+
+ $this->discovery = NULL;
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Display/DisplayPluginManagerInterface.php b/sites/all/modules/contrib/search/search_api/src/Display/DisplayPluginManagerInterface.php
new file mode 100644
index 000000000..a1d4c49d1
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Display/DisplayPluginManagerInterface.php
@@ -0,0 +1,20 @@
+ [
+ * // Settings …
+ * ],
+ * …
+ * ]
+ * @endcode
+ *
+ * @var array
+ */
+ protected $datasource_settings = [];
+
+ /**
+ * The instantiated datasource plugins.
+ *
+ * In the ::preSave method we're saving the contents of these back into the
+ * $datasource_settings array. When adding, removing or changing configuration
+ * we should therefore always manipulate this property instead of the stored
+ * one.
+ *
+ * @var \Drupal\search_api\Datasource\DatasourceInterface[]|null
+ *
+ * @see getDatasources()
+ */
+ protected $datasourceInstances;
+
+ /**
+ * The tracker settings.
+ *
+ * The array has the following structure:
+ *
+ * @code
+ * [
+ * 'TRACKER_ID' => [
+ * // Settings …
+ * ],
+ * ]
+ * @endcode
+ *
+ * There is always just a single entry in the array.
+ *
+ * @var array
+ */
+ protected $tracker_settings = NULL;
+
+ /**
+ * The tracker plugin instance.
+ *
+ * In the ::preSave method we're saving the contents of these back into the
+ * $tracker_settings array. When adding, removing or changing configuration
+ * we should therefore always manipulate this property instead of the stored
+ * one.
+ *
+ * @var \Drupal\search_api\Tracker\TrackerInterface|null
+ *
+ * @see getTrackerInstance()
+ */
+ protected $trackerInstance;
+
+ /**
+ * The ID of the server on which data should be indexed.
+ *
+ * @var string|null
+ */
+ protected $server;
+
+ /**
+ * The server entity belonging to this index.
+ *
+ * @var \Drupal\search_api\ServerInterface
+ *
+ * @see getServerInstance()
+ */
+ protected $serverInstance;
+
+ /**
+ * The array of processor settings.
+ *
+ * The array has the following structure:
+ *
+ * @code
+ * [
+ * 'PROCESSOR_ID' => [
+ * 'weights' => [],
+ * // Other settings …
+ * ],
+ * …
+ * ]
+ * @endcode
+ *
+ * @var array
+ */
+ protected $processor_settings = [];
+
+ /**
+ * Instances of the processor plugins.
+ *
+ * In the ::preSave method we're saving the contents of these back into the
+ * $tracker_settings array. When adding, removing or changing configuration
+ * we should therefore always manipulate this property instead of the stored
+ * one.
+ *
+ * @var \Drupal\search_api\Processor\ProcessorInterface[]|null
+ */
+ protected $processorInstances;
+
+ /**
+ * Static cache of retrieved property definitions, grouped by datasource.
+ *
+ * @var \Drupal\Core\TypedData\DataDefinitionInterface[][]
+ *
+ * @see \Drupal\search_api\Entity\Index::getPropertyDefinitions()
+ */
+ protected $properties = [];
+
+ /**
+ * The number of currently active "batch tracking" modes.
+ *
+ * @var int
+ */
+ protected $batchTracking = 0;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function id() {
+ return $this->id;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDescription() {
+ return $this->description;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isReadOnly() {
+ return $this->read_only;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getOption($name, $default = NULL) {
+ return isset($this->options[$name]) ? $this->options[$name] : $default;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getOptions() {
+ return $this->options;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setOption($name, $option) {
+ $this->options[$name] = $option;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setOptions(array $options) {
+ $this->options = $options;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDatasources() {
+ if ($this->datasourceInstances === NULL) {
+ $this->datasourceInstances = \Drupal::getContainer()
+ ->get('search_api.plugin_helper')
+ ->createDatasourcePlugins($this, array_keys($this->datasource_settings));
+ }
+
+ return $this->datasourceInstances;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDatasourceIds() {
+ return array_keys($this->getDatasources());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isValidDatasource($datasource_id) {
+ $datasources = $this->getDatasources();
+ return !empty($datasources[$datasource_id]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDatasource($datasource_id) {
+ $datasources = $this->getDatasources();
+
+ if (empty($datasources[$datasource_id])) {
+ $index_label = $this->label();
+ throw new SearchApiException("The datasource with ID '$datasource_id' could not be retrieved for index '$index_label'.");
+ }
+
+ return $datasources[$datasource_id];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addDatasource(DatasourceInterface $datasource) {
+ // Make sure the datasourceInstances are loaded before trying to add a plugin
+ // to them.
+ if ($this->datasourceInstances === NULL) {
+ $this->getDatasources();
+ }
+ $this->datasourceInstances[$datasource->getPluginId()] = $datasource;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function removeDatasource($datasource_id) {
+ // Make sure the datasourceInstances are loaded before trying to remove a
+ // plugin from them.
+ if ($this->datasourceInstances === NULL) {
+ $this->getDatasources();
+ }
+ unset($this->datasourceInstances[$datasource_id]);
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setDatasources(array $datasources = NULL) {
+ $this->datasourceInstances = $datasources;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getEntityTypes($return_bool = FALSE) {
+ $types = [];
+ foreach ($this->getDatasources() as $datasource_id => $datasource) {
+ if ($type = $datasource->getEntityTypeId()) {
+ $types[$datasource_id] = $type;
+ }
+ }
+ return $types;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hasValidTracker() {
+ return (bool) \Drupal::getContainer()
+ ->get('plugin.manager.search_api.tracker')
+ ->getDefinition($this->getTrackerId(), FALSE);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getTrackerId() {
+ if ($this->trackerInstance) {
+ return $this->trackerInstance->getPluginId();
+ }
+ if (empty($this->tracker_settings)) {
+ return \Drupal::config('search_api.settings')->get('default_tracker');
+ }
+ reset($this->tracker_settings);
+ return key($this->tracker_settings);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getTrackerInstance() {
+ if (!$this->trackerInstance) {
+ $tracker_id = $this->getTrackerId();
+
+ $configuration = [];
+ if (!empty($this->tracker_settings[$tracker_id])) {
+ $configuration = $this->tracker_settings[$tracker_id];
+ }
+
+ $this->trackerInstance = \Drupal::getContainer()
+ ->get('search_api.plugin_helper')
+ ->createTrackerPlugin($this, $tracker_id, $configuration);
+ }
+
+ return $this->trackerInstance;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setTracker(TrackerInterface $tracker) {
+ $this->trackerInstance = $tracker;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hasValidServer() {
+ return $this->serverInstance
+ || ($this->server !== NULL && Server::load($this->server));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isServerEnabled() {
+ return $this->hasValidServer() && $this->getServerInstance()->status();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getServerId() {
+ return $this->server;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getServerInstance() {
+ if (!$this->serverInstance && $this->server) {
+ $this->serverInstance = Server::load($this->server);
+ if (!$this->serverInstance) {
+ $index_label = $this->label();
+ throw new SearchApiException("The server with ID '$this->server' could not be retrieved for index '$index_label'.");
+ }
+ }
+
+ return $this->serverInstance;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setServer(ServerInterface $server = NULL) {
+ $this->serverInstance = $server;
+ $this->server = $server ? $server->id() : NULL;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getProcessors() {
+ if ($this->processorInstances !== NULL) {
+ return $this->processorInstances;
+ }
+
+ // Filter the processors to only include those that are enabled (or locked).
+ // We should only reach this point in the code once, at the first call after
+ // the index is loaded.
+ $this->processorInstances = [];
+ $processors = \Drupal::getContainer()
+ ->get('search_api.plugin_helper')
+ ->createProcessorPlugins($this);
+ foreach ($processors as $processor_id => $processor) {
+ if (isset($this->processor_settings[$processor_id]) || $processor->isLocked()) {
+ $this->processorInstances[$processor_id] = $processor;
+ }
+ }
+
+ return $this->processorInstances;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getProcessorsByStage($stage, array $overrides = []) {
+ // Get a list of all processors which support this stage, along with their
+ // weights.
+ $processors = $this->getProcessors();
+ $processor_weights = [];
+ foreach ($processors as $name => $processor) {
+ if ($processor->supportsStage($stage)) {
+ $processor_weights[$name] = $processor->getWeight($stage);
+ }
+ }
+
+ // Apply any overrides that were passed by the caller.
+ $plugin_helper = \Drupal::getContainer()->get('search_api.plugin_helper');
+ foreach ($overrides as $name => $config) {
+ $processor = $plugin_helper->createProcessorPlugin($this, $name, $config);
+ if ($processor->supportsStage($stage)) {
+ $processors[$name] = $processor;
+ $processor_weights[$name] = $processor->getWeight($stage);
+ }
+ else {
+ // In rare cases, the override might change whether or not the processor
+ // supports the given stage. So, to make sure, unset the weight in case
+ // it was set before.
+ unset($processor_weights[$name]);
+ }
+ }
+
+ // Sort requested processors by weight.
+ asort($processor_weights);
+
+ $return_processors = [];
+ foreach ($processor_weights as $name => $weight) {
+ $return_processors[$name] = $processors[$name];
+ }
+
+ return $return_processors;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isValidProcessor($processor_id) {
+ $processors = $this->getProcessors();
+ return !empty($processors[$processor_id]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getProcessor($processor_id) {
+ $processors = $this->getProcessors();
+
+ if (empty($processors[$processor_id])) {
+ $index_label = $this->label();
+ throw new SearchApiException("The processor with ID '$processor_id' could not be retrieved for index '$index_label'.");
+ }
+
+ return $processors[$processor_id];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addProcessor(ProcessorInterface $processor) {
+ // Make sure the processorInstances are loaded before trying to add a plugin
+ // to them.
+ if ($this->processorInstances === NULL) {
+ $this->getProcessors();
+ }
+ $this->processorInstances[$processor->getPluginId()] = $processor;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function removeProcessor($processor_id) {
+ // Make sure the processorInstances are loaded before trying to remove a
+ // plugin from them.
+ if ($this->processorInstances === NULL) {
+ $this->getProcessors();
+ }
+ unset($this->processorInstances[$processor_id]);
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setProcessors(array $processors) {
+ $this->processorInstances = $processors;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function alterIndexedItems(array &$items) {
+ foreach ($this->getProcessorsByStage(ProcessorInterface::STAGE_ALTER_ITEMS) as $processor) {
+ $processor->alterIndexedItems($items);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function preprocessIndexItems(array $items) {
+ foreach ($this->getProcessorsByStage(ProcessorInterface::STAGE_PREPROCESS_INDEX) as $processor) {
+ $processor->preprocessIndexItems($items);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function preprocessSearchQuery(QueryInterface $query) {
+ foreach ($this->getProcessorsByStage(ProcessorInterface::STAGE_PREPROCESS_QUERY) as $processor) {
+ $processor->preprocessSearchQuery($query);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function postprocessSearchResults(ResultSetInterface $results) {
+ foreach ($this->getProcessorsByStage(ProcessorInterface::STAGE_POSTPROCESS_QUERY) as $processor) {
+ $processor->postprocessSearchResults($results);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addField(FieldInterface $field) {
+ $field_id = $field->getFieldIdentifier();
+ $reserved = \Drupal::getContainer()
+ ->get('search_api.fields_helper')
+ ->isFieldIdReserved($field_id);
+ if ($reserved) {
+ throw new SearchApiException("'$field_id' is a reserved value and cannot be used as the machine name of a normal field.");
+ }
+
+ // This will automatically call getFields(), thus initializing
+ // $this->fieldInstances, if that hasn't been done yet.
+ $old_field = $this->getField($field_id);
+ if ($old_field && $old_field != $field) {
+ throw new SearchApiException("Cannot add field with machine name '$field_id': machine name is already taken.");
+ }
+
+ $this->fieldInstances[$field_id] = $field;
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function renameField($old_field_id, $new_field_id) {
+ if (!isset($this->getFields()[$old_field_id])) {
+ throw new SearchApiException("Could not rename field with machine name '$old_field_id': no such field.");
+ }
+ $reserved = \Drupal::getContainer()
+ ->get('search_api.fields_helper')
+ ->isFieldIdReserved($new_field_id);
+ if ($reserved) {
+ throw new SearchApiException("'$new_field_id' is a reserved value and cannot be used as the machine name of a normal field.");
+ }
+ if (isset($this->getFields()[$new_field_id])) {
+ throw new SearchApiException("'$new_field_id' already exists and can't be used as a new field id.");
+ }
+
+ $this->fieldInstances[$new_field_id] = $this->fieldInstances[$old_field_id];
+ unset($this->fieldInstances[$old_field_id]);
+ $this->fieldInstances[$new_field_id]->setFieldIdentifier($new_field_id);
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function removeField($field_id) {
+ $field = $this->getField($field_id);
+ if (!$field) {
+ return $this;
+ }
+ if ($field->isIndexedLocked()) {
+ throw new SearchApiException("Cannot remove field with machine name '$field_id': field is locked.");
+ }
+
+ unset($this->fieldInstances[$field_id]);
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setFields(array $fields) {
+ $this->fieldInstances = $fields;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFields($include_server_defined = FALSE) {
+ if (!isset($this->fieldInstances)) {
+ $this->fieldInstances = [];
+ foreach ($this->field_settings as $key => $field_info) {
+ $this->fieldInstances[$key] = \Drupal::getContainer()
+ ->get('search_api.fields_helper')
+ ->createField($this, $key, $field_info);
+ }
+ }
+
+ $fields = $this->fieldInstances;
+ if ($include_server_defined && $this->hasValidServer()) {
+ $fields += $this->getServerInstance()->getBackendDefinedFields($this);
+ }
+ return $fields;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getField($field_id) {
+ $fields = $this->getFields();
+ return isset($fields[$field_id]) ? $fields[$field_id] : NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFieldsByDatasource($datasource_id) {
+ $datasource_fields = [];
+ foreach ($this->getFields() as $field_id => $field) {
+ if ($field->getDatasourceId() === $datasource_id) {
+ $datasource_fields[$field_id] = $field;
+ }
+ }
+
+ return $datasource_fields;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFulltextFields() {
+ $fulltext_fields = [];
+ foreach ($this->getFields() as $key => $field) {
+ if (\Drupal::getContainer()
+ ->get('search_api.data_type_helper')
+ ->isTextType($field->getType())) {
+ $fulltext_fields[] = $key;
+ }
+ }
+ return $fulltext_fields;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFieldRenames() {
+ $renames = [];
+ foreach ($this->getFields() as $field_id => $field) {
+ if ($field->getOriginalFieldIdentifier() != $field_id) {
+ $renames[$field->getOriginalFieldIdentifier()] = $field_id;
+ }
+ }
+ return $renames;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function discardFieldChanges() {
+ $this->fieldInstances = NULL;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getPropertyDefinitions($datasource_id) {
+ if (!isset($this->properties[$datasource_id])) {
+ if (isset($datasource_id)) {
+ $datasource = $this->getDatasource($datasource_id);
+ $properties = $datasource->getPropertyDefinitions();
+ }
+ else {
+ $datasource = NULL;
+ $properties = [];
+ }
+
+ foreach ($this->getProcessorsByStage(ProcessorInterface::STAGE_ADD_PROPERTIES) as $processor) {
+ $properties += $processor->getPropertyDefinitions($datasource);
+ }
+
+ $this->properties[$datasource_id] = $properties;
+ }
+
+ return $this->properties[$datasource_id];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function loadItem($item_id) {
+ $items = $this->loadItemsMultiple([$item_id]);
+ return $items ? reset($items) : NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function loadItemsMultiple(array $item_ids) {
+ // Group the requested items by datasource. This will also later be used to
+ // determine whether all items were loaded successfully.
+ $items_by_datasource = [];
+ foreach ($item_ids as $item_id) {
+ list($datasource_id, $raw_id) = Utility::splitCombinedId($item_id);
+ $items_by_datasource[$datasource_id][$raw_id] = $item_id;
+ }
+
+ // Load the items from the datasources and keep track of which were
+ // successfully retrieved.
+ $items = [];
+ foreach ($items_by_datasource as $datasource_id => $raw_ids) {
+ try {
+ $datasource = $this->getDatasource($datasource_id);
+ $datasource_items = $datasource->loadMultiple(array_keys($raw_ids));
+ foreach ($datasource_items as $raw_id => $item) {
+ $id = $raw_ids[$raw_id];
+ $items[$id] = $item;
+ // Remember that we successfully loaded this item.
+ unset($items_by_datasource[$datasource_id][$raw_id]);
+ }
+ }
+ catch (SearchApiException $e) {
+ $this->logException($e);
+ // If the complete datasource could not be loaded, don't report all its
+ // individual requested items as missing.
+ unset($items_by_datasource[$datasource_id]);
+ }
+ }
+
+ // Check whether there are requested items that couldn't be loaded.
+ $items_by_datasource = array_filter($items_by_datasource);
+ if ($items_by_datasource) {
+ // Extract the second-level values of the two-dimensional array (that is,
+ // the combined item IDs) and log a warning reporting their absence.
+ $missing_ids = array_reduce(array_map('array_values', $items_by_datasource), 'array_merge', []);
+ $args['%index'] = $this->label();
+ $args['@items'] = '"' . implode('", "', $missing_ids) . '"';
+ $this->getLogger()->warning('Could not load the following items on index %index: @items.', $args);
+ // Also remove those items from tracking so we don't keep trying to load
+ // them.
+ foreach ($items_by_datasource as $datasource_id => $raw_ids) {
+ $this->trackItemsDeleted($datasource_id, array_keys($raw_ids));
+ }
+ }
+
+ // Return the loaded items.
+ return $items;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function indexItems($limit = '-1', $datasource_id = NULL) {
+ if ($this->hasValidTracker() && !$this->isReadOnly()) {
+ $tracker = $this->getTrackerInstance();
+ $next_set = $tracker->getRemainingItems($limit, $datasource_id);
+ $items = $this->loadItemsMultiple($next_set);
+ if ($items) {
+ try {
+ return count($this->indexSpecificItems($items));
+ }
+ catch (SearchApiException $e) {
+ $variables['%index'] = $this->label();
+ $this->logException($e, '%type while trying to index items on index %index: @message in %function (line %line of %file)', $variables);
+ }
+ }
+ }
+ return 0;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function indexSpecificItems(array $search_objects) {
+ if (!$search_objects || $this->read_only) {
+ return [];
+ }
+ if (!$this->status) {
+ $index_label = $this->label();
+ throw new SearchApiException("Couldn't index values on index '$index_label' (index is disabled)");
+ }
+
+ /** @var \Drupal\search_api\Item\ItemInterface[] $items */
+ $items = [];
+ foreach ($search_objects as $item_id => $object) {
+ $items[$item_id] = \Drupal::getContainer()
+ ->get('search_api.fields_helper')
+ ->createItemFromObject($this, $object, $item_id);
+ }
+
+ // Remember the items that were initially passed, to be able to determine
+ // the items rejected by alter hooks and processors afterwards.
+ $rejected_ids = array_keys($items);
+ $rejected_ids = array_combine($rejected_ids, $rejected_ids);
+
+ // Preprocess the indexed items.
+ $this->alterIndexedItems($items);
+ \Drupal::moduleHandler()->alter('search_api_index_items', $this, $items);
+ foreach ($items as $item) {
+ // This will cache the extracted fields so processors, etc., can retrieve
+ // them directly.
+ $item->getFields();
+ }
+ $this->preprocessIndexItems($items);
+
+ // Remove all items still in $items from $rejected_ids. Thus, only the
+ // rejected items' IDs are still contained in $ret, to later be returned
+ // along with the successfully indexed ones.
+ foreach ($items as $item_id => $item) {
+ unset($rejected_ids[$item_id]);
+ }
+
+ // Items that are rejected should also be deleted from the server.
+ if ($rejected_ids) {
+ $this->getServerInstance()->deleteItems($this, $rejected_ids);
+ }
+
+ $indexed_ids = [];
+ if ($items) {
+ $indexed_ids = $this->getServerInstance()->indexItems($this, $items);
+ }
+
+ // Return the IDs of all items that were either successfully indexed or
+ // rejected before being handed to the server.
+ $processed_ids = array_merge(array_values($rejected_ids), array_values($indexed_ids));
+
+ if ($processed_ids) {
+ if ($this->hasValidTracker()) {
+ $this->getTrackerInstance()->trackItemsIndexed($processed_ids);
+ }
+ // Since we've indexed items now, triggering reindexing would have some
+ // effect again. Therefore, we reset the flag.
+ $this->setHasReindexed(FALSE);
+ \Drupal::moduleHandler()->invokeAll('search_api_items_indexed', [$this, $processed_ids]);
+
+ // Clear search api list caches.
+ Cache::invalidateTags(['search_api_list:' . $this->id]);
+ }
+
+ return $processed_ids;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isBatchTracking() {
+ return (bool) $this->batchTracking;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function startBatchTracking() {
+ $this->batchTracking++;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function stopBatchTracking() {
+ if (!$this->batchTracking) {
+ throw new SearchApiException('Trying to leave "batch tracking" mode on index "' . $this->label() . '" which was not entered first.');
+ }
+ $this->batchTracking--;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function trackItemsInserted($datasource_id, array $ids) {
+ $this->trackItemsInsertedOrUpdated($datasource_id, $ids, __FUNCTION__);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function trackItemsUpdated($datasource_id, array $ids) {
+ $this->trackItemsInsertedOrUpdated($datasource_id, $ids, __FUNCTION__);
+ }
+
+ /**
+ * Tracks insertion or updating of items.
+ *
+ * Used as a helper method in trackItemsInserted() and trackItemsUpdated() to
+ * avoid code duplication.
+ *
+ * @param string $datasource_id
+ * The ID of the datasource to which the items belong.
+ * @param array $ids
+ * An array of datasource-specific item IDs.
+ * @param string $tracker_method
+ * The method to call on the tracker. Must be either "trackItemsInserted" or
+ * "trackItemsUpdated".
+ */
+ protected function trackItemsInsertedOrUpdated($datasource_id, array $ids, $tracker_method) {
+ if ($this->hasValidTracker() && $this->status()) {
+ $item_ids = [];
+ foreach ($ids as $id) {
+ $item_ids[] = Utility::createCombinedId($datasource_id, $id);
+ }
+ $this->getTrackerInstance()->$tracker_method($item_ids);
+ if (!$this->isReadOnly() && $this->getOption('index_directly') && !$this->batchTracking) {
+ \Drupal::getContainer()->get('search_api.post_request_indexing')
+ ->registerIndexingOperation($this->id(), $item_ids);
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function trackItemsDeleted($datasource_id, array $ids) {
+ if (!$this->status()) {
+ return;
+ }
+
+ $item_ids = [];
+ foreach ($ids as $id) {
+ $item_ids[] = Utility::createCombinedId($datasource_id, $id);
+ }
+ if ($this->hasValidTracker()) {
+ $this->getTrackerInstance()->trackItemsDeleted($item_ids);
+ }
+ if (!$this->isReadOnly() && $this->hasValidServer()) {
+ $this->getServerInstance()->deleteItems($this, $item_ids);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function reindex() {
+ if ($this->status() && !$this->isReindexing()) {
+ $this->setHasReindexed();
+ $this->getTrackerInstance()->trackAllItemsUpdated();
+ \Drupal::moduleHandler()->invokeAll('search_api_index_reindex', [$this, FALSE]);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function clear() {
+ if (!$this->status()) {
+ return;
+ }
+
+ // Only invoke the hook if we actually did something.
+ $invoke_hook = FALSE;
+ if (!$this->isReindexing()) {
+ $invoke_hook = TRUE;
+ $this->setHasReindexed();
+ $this->getTrackerInstance()->trackAllItemsUpdated();
+ }
+ if (!$this->isReadOnly()) {
+ $invoke_hook = TRUE;
+ $this->getServerInstance()->deleteAllIndexItems($this);
+ }
+ if ($invoke_hook) {
+ \Drupal::moduleHandler()
+ ->invokeAll('search_api_index_reindex', [$this, !$this->isReadOnly()]);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function rebuildTracker() {
+ if (!$this->status()) {
+ return;
+ }
+
+ $index_task_manager = \Drupal::getContainer()
+ ->get('search_api.index_task_manager');
+ $index_task_manager->stopTracking($this);
+ $index_task_manager->startTracking($this);
+ $this->setHasReindexed();
+ \Drupal::moduleHandler()
+ ->invokeAll('search_api_index_reindex', [$this, FALSE]);
+ $index_task_manager->addItemsBatch($this);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isReindexing() {
+ $key = "search_api.index.{$this->id()}.has_reindexed";
+ return \Drupal::state()->get($key, FALSE);
+ }
+
+ /**
+ * Sets whether this index has all items marked for re-indexing.
+ *
+ * @param bool $has_reindexed
+ * (optional) TRUE if the index has all items marked for re-indexing, FALSE
+ * otherwise.
+ *
+ * @return $this
+ */
+ protected function setHasReindexed($has_reindexed = TRUE) {
+ if ($this->isReindexing() !== $has_reindexed) {
+ $key = "search_api.index.{$this->id()}.has_reindexed";
+ \Drupal::state()->set($key, $has_reindexed);
+ }
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function query(array $options = []) {
+ if (!$this->status()) {
+ throw new SearchApiException('Cannot search on a disabled index.');
+ }
+ return \Drupal::getContainer()
+ ->get('search_api.query_helper')
+ ->createQuery($this, $options);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function postCreate(EntityStorageInterface $storage) {
+ parent::postCreate($storage);
+
+ // Merge in default options.
+ $config = \Drupal::config('search_api.settings');
+ $this->options += [
+ 'cron_limit' => $config->get('default_cron_limit'),
+ 'index_directly' => TRUE,
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function preSave(EntityStorageInterface $storage) {
+ // If we are in the process of syncing, we shouldn't change any entity
+ // properties (or other configuration).
+ if ($this->isSyncing()) {
+ parent::preSave($storage);
+ return;
+ }
+
+ // Retrieve active config overrides for this index.
+ $overrides = Utility::getConfigOverrides($this);
+
+ // Prevent enabling of indexes when the server is disabled. Take into
+ // account that both the index's "status" and "server" properties might be
+ // overridden.
+ if ($this->status() && !isset($overrides['status'])) {
+ // NULL would be a valid override, so we can't use isset() here.
+ if (!array_key_exists('server', $overrides)) {
+ if (!$this->isServerEnabled()) {
+ $this->disable();
+ }
+ }
+ else {
+ $server_id = $overrides['server'];
+ $server = $server_id !== NULL ? Server::load($server_id) : NULL;
+ if (!$server || !$server->status()) {
+ $this->disable();
+ }
+ }
+ }
+
+ // Merge in default options.
+ $config = \Drupal::config('search_api.settings');
+ $this->options += [
+ 'cron_limit' => $config->get('default_cron_limit'),
+ 'index_directly' => TRUE,
+ ];
+
+ // Reset the static cache for getPropertyDefinitions() to make sure we don't
+ // remove any fields just because of caching problems.
+ $this->properties = [];
+ foreach ($this->getFields() as $field_id => $field) {
+ // Remove all "locked" and "hidden" flags from all fields of the index. If
+ // they are still valid, they should be re-added by the processors.
+ $field->setIndexedLocked(FALSE);
+ $field->setTypeLocked(FALSE);
+ $field->setHidden(FALSE);
+
+ // Also check whether the underlying property actually (still) exists.
+ $datasource_id = $field->getDatasourceId();
+ $property = NULL;
+ if ($datasource_id === NULL || $this->isValidDatasource($datasource_id)) {
+ $properties = $this->getPropertyDefinitions($datasource_id);
+ $property = \Drupal::getContainer()
+ ->get('search_api.fields_helper')
+ ->retrieveNestedProperty($properties, $field->getPropertyPath());
+ }
+ if (!$property) {
+ $this->removeField($field_id);
+ }
+ }
+
+ // Check whether all enabled processors actually still support this index.
+ // (Since we can't remove processors which are present in overrides anyways,
+ // we don't need to take overrides into account here.)
+ foreach ($this->getProcessors() as $processor_id => $processor) {
+ if (!$processor->supportsIndex($this)) {
+ $this->removeProcessor($processor_id);
+ }
+ }
+
+ // Call the preIndexSave() method of all applicable processors.
+ $processor_overrides = !empty($overrides['processor_settings']) ? $overrides['processor_settings'] : [];
+ foreach ($this->getProcessorsByStage(ProcessorInterface::STAGE_PRE_INDEX_SAVE, $processor_overrides) as $processor) {
+ $processor->preIndexSave();
+ }
+
+ // Write the field and plugin settings to the persistent *_settings
+ // properties.
+ $this->writeChangesToSettings();
+
+ // Since we change dependency-relevant data in this method, we can only call
+ // the parent method at the end (or we'd need to re-calculate the
+ // dependencies).
+ parent::preSave($storage);
+ }
+
+ /**
+ * Prepares for changes to this index to be persisted.
+ *
+ * To this end, the settings for all loaded field and plugin objects are
+ * written back to the corresponding *_settings properties.
+ *
+ * @return $this
+ */
+ protected function writeChangesToSettings() {
+ // Calculate field dependencies and save field settings containing them.
+ $fields = $this->getFields();
+ $field_dependencies = $this->getFieldDependencies();
+ $field_dependencies += array_fill_keys(array_keys($fields), []);
+ $this->field_settings = [];
+ foreach ($fields as $field_id => $field) {
+ $field->setDependencies($field_dependencies[$field_id]);
+ $this->field_settings[$field_id] = $field->getSettings();
+ }
+
+ // Write the enabled processors to the settings property.
+ $processors = $this->getProcessors();
+ $this->processor_settings = [];
+ foreach ($processors as $processor_id => $processor) {
+ $this->processor_settings[$processor_id] = $processor->getConfiguration();
+ }
+
+ // Write the tracker configuration to the settings property.
+ $tracker = $this->getTrackerInstance();
+ $tracker_id = $tracker->getPluginId();
+ $this->tracker_settings = [
+ $tracker_id => $tracker->getConfiguration(),
+ ];
+
+ // Write the enabled datasources to the settings array.
+ $this->datasource_settings = [];
+ foreach ($this->getDatasources() as $plugin_id => $datasource) {
+ $this->datasource_settings[$plugin_id] = $datasource->getConfiguration();
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function postSave(EntityStorageInterface $storage, $update = TRUE) {
+ parent::postSave($storage, $update);
+
+ // New indexes don't have any items indexed.
+ if (!$update) {
+ $this->setHasReindexed();
+ }
+
+ try {
+ // Fake an original for inserts to make code cleaner.
+ /** @var \Drupal\search_api\IndexInterface $original */
+ $original = $update ? $this->original : static::create(['status' => FALSE]);
+ $index_task_manager = \Drupal::getContainer()
+ ->get('search_api.index_task_manager');
+
+ if ($this->status() && $original->status()) {
+ // React on possible changes that would require re-indexing, etc.
+ $this->reactToServerSwitch($original);
+ $this->reactToDatasourceSwitch($original);
+ $this->reactToTrackerSwitch($original);
+ $this->reactToProcessorChanges($original);
+ }
+ elseif (!$this->status() && $original->status()) {
+ if ($this->hasValidTracker()) {
+ $index_task_manager->stopTracking($original);
+ }
+ if ($original->isServerEnabled()) {
+ $original->getServerInstance()->removeIndex($original);
+ }
+ }
+ elseif ($this->status() && !$original->status()) {
+ $this->getServerInstance()->addIndex($this);
+ if ($this->hasValidTracker()) {
+ $index_task_manager->startTracking($this);
+ }
+ }
+
+ if (!$index_task_manager->isTrackingComplete($this)) {
+ // Give tests and site admins the possibility to disable the use of a
+ // batch for tracking items. Also, do not use a batch if running in the
+ // CLI.
+ $use_batch = \Drupal::state()->get('search_api_use_tracking_batch', TRUE);
+ if (!$use_batch || Utility::isRunningInCli()) {
+ $index_task_manager->addItemsAll($this);
+ }
+ else {
+ $index_task_manager->addItemsBatch($this);
+ }
+ }
+
+ if (\Drupal::moduleHandler()->moduleExists('views')) {
+ Views::viewsData()->clear();
+ // Remove this line when https://www.drupal.org/node/2370365 gets fixed.
+ Cache::invalidateTags(['extension:views']);
+ \Drupal::cache('discovery')->delete('views:wizard');
+ }
+
+ Cache::invalidateTags($this->getCacheTags());
+
+ $this->properties = [];
+ }
+ catch (SearchApiException $e) {
+ $this->logException($e);
+ }
+ }
+
+ /**
+ * Checks whether the index switched server and reacts accordingly.
+ *
+ * Used as a helper method in postSave(). Should only be called when the index
+ * was enabled before the change and remained so.
+ *
+ * @param \Drupal\search_api\IndexInterface $original
+ * The previous version of the index.
+ */
+ protected function reactToServerSwitch(IndexInterface $original) {
+ // Asserts that the index was enabled before saving and will still be
+ // enabled afterwards. Otherwise, this method should not be called.
+ assert($this->status() && $original->status(), '::reactToServerSwitch should only be called when the index is enabled');
+
+ if ($this->getServerId() != $original->getServerId()) {
+ if ($original->hasValidServer()) {
+ $original->getServerInstance()->removeIndex($this);
+ }
+ if ($this->hasValidServer()) {
+ $this->getServerInstance()->addIndex($this);
+ }
+ // When the server changes we also need to trigger a reindex.
+ $this->reindex();
+ }
+ elseif ($this->hasValidServer()) {
+ // Tell the server the index configuration got updated.
+ $this->getServerInstance()->updateIndex($this);
+ }
+ }
+
+ /**
+ * Checks whether the index's datasources changed and reacts accordingly.
+ *
+ * Used as a helper method in postSave(). Should only be called when the index
+ * was enabled before the change and remained so.
+ *
+ * @param \Drupal\search_api\IndexInterface $original
+ * The previous version of the index.
+ */
+ protected function reactToDatasourceSwitch(IndexInterface $original) {
+ // Asserts that the index was enabled before saving and will still be
+ // enabled afterwards. Otherwise, this method should not be called.
+ assert($this->status() && $original->status(), '::reactToDatasourceSwitch should only be called when the index is enabled');
+
+ $new_datasource_ids = $this->getDatasourceIds();
+ $original_datasource_ids = $original->getDatasourceIds();
+ if ($new_datasource_ids != $original_datasource_ids) {
+ $added = array_diff($new_datasource_ids, $original_datasource_ids);
+ $removed = array_diff($original_datasource_ids, $new_datasource_ids);
+ $index_task_manager = \Drupal::getContainer()->get('search_api.index_task_manager');
+ $index_task_manager->stopTracking($this, $removed);
+ if ($this->hasValidServer()) {
+ /** @var \Drupal\search_api\ServerInterface $server */
+ $server = $this->getServerInstance();
+ foreach ($removed as $datasource_id) {
+ $server->deleteAllIndexItems($this, $datasource_id);
+ }
+ }
+ $index_task_manager->startTracking($this, $added);
+ }
+ }
+
+ /**
+ * Checks whether the index switched tracker plugin and reacts accordingly.
+ *
+ * Used as a helper method in postSave(). Should only be called when the index
+ * was enabled before the change and remained so.
+ *
+ * @param \Drupal\search_api\IndexInterface $original
+ * The previous version of the index.
+ */
+ protected function reactToTrackerSwitch(IndexInterface $original) {
+ // Asserts that the index was enabled before saving and will still be
+ // enabled afterwards. Otherwise, this method should not be called.
+ assert($this->status() && $original->status(), '::reactToTrackerSwitch should only be called when the index is enabled');
+
+ if ($this->getTrackerId() != $original->getTrackerId()) {
+ $index_task_manager = \Drupal::getContainer()
+ ->get('search_api.index_task_manager');
+ if ($original->hasValidTracker()) {
+ $index_task_manager->stopTracking($original);
+ }
+ if ($this->hasValidTracker()) {
+ $index_task_manager->startTracking($this);
+ }
+ }
+ }
+
+ /**
+ * Reacts to changes in processor configuration.
+ *
+ * @param \Drupal\search_api\IndexInterface $original
+ * The previous version of the index.
+ */
+ protected function reactToProcessorChanges(IndexInterface $original) {
+ $old_processors = $original->getProcessors();
+ $new_processors = $this->getProcessors();
+
+ $requires_reindex = FALSE;
+
+ // Loop over all new settings and check if the processors were already set
+ // in the original entity.
+ foreach ($new_processors as $key => $processor) {
+ // The processor is new, because it wasn't configured in the original
+ // entity.
+ if (!isset($old_processors[$key])) {
+ if ($processor->requiresReindexing(NULL, $processor->getConfiguration())) {
+ $requires_reindex = TRUE;
+ break;
+ }
+ }
+ }
+
+ if (!$requires_reindex) {
+ // Loop over all original settings and check if one of them has been
+ // removed or changed.
+ foreach ($old_processors as $key => $old_processor) {
+ $new_processor = isset($new_processors[$key]) ? $new_processors[$key] : NULL;
+ $old_config = $old_processor->getConfiguration();
+ $new_config = $new_processor ? $new_processor->getConfiguration() : NULL;
+ if (!$new_processor || $old_config != $new_config) {
+ if ($old_processor->requiresReindexing($old_config, $new_config)) {
+ $requires_reindex = TRUE;
+ break;
+ }
+ }
+ }
+ }
+
+ if ($requires_reindex) {
+ $this->reindex();
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function preDelete(EntityStorageInterface $storage, array $entities) {
+ parent::preDelete($storage, $entities);
+
+ $index_task_manager = \Drupal::getContainer()
+ ->get('search_api.index_task_manager');
+ /** @var \Drupal\search_api\IndexInterface[] $entities */
+ foreach ($entities as $index) {
+ if ($index->status()) {
+ $index_task_manager->stopTracking($index);
+ if ($index->hasValidServer()) {
+ $index->getServerInstance()->removeIndex($index);
+ }
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function postDelete(EntityStorageInterface $storage, array $entities) {
+ parent::postDelete($storage, $entities);
+
+ if (\Drupal::moduleHandler()->moduleExists('views')) {
+ Views::viewsData()->clear();
+ // Remove this line when https://www.drupal.org/node/2370365 gets fixed.
+ Cache::invalidateTags(['extension:views']);
+ \Drupal::cache('discovery')->delete('views:wizard');
+ }
+
+ /** @var \Drupal\user\SharedTempStore $temp_store */
+ $temp_store = \Drupal::service('user.shared_tempstore')->get('search_api_index');
+ foreach ($entities as $entity) {
+ try {
+ $temp_store->delete($entity->id());
+ }
+ catch (TempStoreException $e) {
+ // Can't really be helped, I guess. But is also very unlikely to happen.
+ // Ignore it.
+ }
+ }
+ }
+
+ // @todo Override static load() etc. methods? Measure performance difference.
+
+ /**
+ * {@inheritdoc}
+ */
+ public function calculateDependencies() {
+ $dependencies = $this->getDependencyData();
+ // Keep only "enforced" dependencies, then add those computed by
+ // getDependencyData().
+ $this->dependencies = array_intersect_key($this->dependencies, ['enforced' => TRUE]);
+ $this->dependencies += array_map('array_keys', $dependencies);
+ return $this;
+ }
+
+ /**
+ * Retrieves data about this index's dependencies.
+ *
+ * The return value is structured as follows:
+ *
+ * @code
+ * [
+ * 'config' => [
+ * 'CONFIG_DEPENDENCY_KEY' => [
+ * 'always' => [
+ * 'processors' => [
+ * 'PROCESSOR_ID' => $processor,
+ * ],
+ * 'datasources' => [
+ * 'DATASOURCE_ID_1' => $datasource_1,
+ * 'DATASOURCE_ID_2' => $datasource_2,
+ * ],
+ * ],
+ * 'optional' => [
+ * 'index' => [
+ * 'INDEX_ID' => $index,
+ * ],
+ * 'tracker' => [
+ * 'TRACKER_ID' => $tracker,
+ * ],
+ * ],
+ * ],
+ * ],
+ * ]
+ * @endcode
+ *
+ * Enforced dependencies are not included in this method's return value.
+ *
+ * @return object[][][][][]
+ * An associative array containing the index's dependencies. The array is
+ * first keyed by the config dependency type ("module", "config", etc.) and
+ * then by the names of the config dependencies of that type which the index
+ * has. The values are associative arrays with up to two keys, "always" and
+ * "optional", specifying whether the dependency is a hard one by the plugin
+ * (or index) in question or potentially depending on the configuration. The
+ * values on this level are arrays with keys "index", "tracker",
+ * "datasources" and/or "processors" and values arrays of IDs mapped to
+ * their entities/plugins.
+ */
+ protected function getDependencyData() {
+ $dependency_data = [];
+
+ // Since calculateDependencies() will work directly on the $dependencies
+ // property, we first save its original state and then restore it
+ // afterwards.
+ $original_dependencies = $this->dependencies;
+ parent::calculateDependencies();
+ unset($this->dependencies['enforced']);
+ foreach ($this->dependencies as $dependency_type => $list) {
+ foreach ($list as $name) {
+ $dependency_data[$dependency_type][$name]['always']['index'][$this->id] = $this;
+ }
+ }
+ $this->dependencies = $original_dependencies;
+
+ // Include the field dependencies.
+ $type_dependencies = [];
+ foreach ($this->getFields() as $field_id => $field) {
+ foreach ($field->getDependencies() as $dependency_type => $names) {
+ foreach ($names as $name) {
+ $dependency_data[$dependency_type][$name]['always']['fields'][$field_id] = $field;
+ }
+ }
+
+ // Also take dependencies of the field's data type plugin into account.
+ // (Since data type plugins cannot have configuration, this will always be
+ // the same for a certain type, so we only have to compute this once per
+ // type.)
+ $type = $field->getType();
+ if (!isset($type_dependencies[$type])) {
+ $type_dependencies[$type] = [];
+ $data_type = $field->getDataTypePlugin();
+ if ($data_type && !$data_type->isDefault()) {
+ $definition = $data_type->getPluginDefinition();
+ $type_dependencies[$type]['module'][] = $definition['provider'];
+ // Plugins can declare additional dependencies in their definition.
+ if (!empty($definition['config_dependencies'])) {
+ $type_dependencies[$type] = NestedArray::mergeDeep(
+ $type_dependencies[$type],
+ $definition['config_dependencies']
+ );
+ }
+ // If a plugin is dependent, calculate its dependencies.
+ if ($data_type instanceof DependentPluginInterface) {
+ $type_dependencies[$type] = NestedArray::mergeDeep(
+ $type_dependencies[$type],
+ $data_type->calculateDependencies()
+ );
+ }
+ }
+ }
+ foreach ($type_dependencies[$type] as $dependency_type => $list) {
+ foreach ($list as $name) {
+ $dependency_data[$dependency_type][$name]['optional']['fields'][$field_id] = $field;
+ }
+ }
+ }
+
+ // The server needs special treatment, since it is a dependency of the index
+ // itself, and not one of its plugins.
+ if ($this->hasValidServer()) {
+ $name = $this->getServerInstance()->getConfigDependencyName();
+ $dependency_data['config'][$name]['optional']['index'][$this->id] = $this;
+ }
+
+ // All other plugins can be treated uniformly.
+ $plugins = $this->getAllPlugins();
+
+ foreach ($plugins as $plugin_type => $type_plugins) {
+ foreach ($type_plugins as $plugin_id => $plugin) {
+ // Largely copied from
+ // \Drupal\Core\Plugin\PluginDependencyTrait::calculatePluginDependencies().
+ $definition = $plugin->getPluginDefinition();
+
+ // First, always depend on the module providing the plugin.
+ $dependency_data['module'][$definition['provider']]['always'][$plugin_type][$plugin_id] = $plugin;
+
+ // Plugins can declare additional dependencies in their definition.
+ if (isset($definition['config_dependencies'])) {
+ foreach ($definition['config_dependencies'] as $dependency_type => $list) {
+ foreach ($list as $name) {
+ $dependency_data[$dependency_type][$name]['always'][$plugin_type][$plugin_id] = $plugin;
+ }
+ }
+ }
+
+ // Finally, add the dynamically-calculated dependencies of the plugin.
+ foreach ($plugin->calculateDependencies() as $dependency_type => $list) {
+ foreach ($list as $name) {
+ $dependency_data[$dependency_type][$name]['optional'][$plugin_type][$plugin_id] = $plugin;
+ }
+ }
+ }
+ }
+
+ return $dependency_data;
+ }
+
+ /**
+ * Retrieves information about the dependencies of the indexed fields.
+ *
+ * @return string[][][]
+ * An associative array containing the dependencies of the indexed fields.
+ * The array is keyed by field ID and dependency type, the values are arrays
+ * with dependency names.
+ */
+ protected function getFieldDependencies() {
+ $field_dependencies = [];
+
+ foreach ($this->getDatasources() as $datasource_id => $datasource) {
+ $fields = [];
+ foreach ($this->getFieldsByDatasource($datasource_id) as $field_id => $field) {
+ $fields[$field_id] = $field->getPropertyPath();
+ }
+ $field_dependencies += $datasource->getFieldDependencies($fields);
+ }
+
+ return $field_dependencies;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function onDependencyRemoval(array $dependencies) {
+ $changed = parent::onDependencyRemoval($dependencies);
+
+ $all_plugins = $this->getAllPlugins();
+ $dependency_data = $this->getDependencyData();
+ // Make sure our dependency data has the exact same keys as $dependencies,
+ // to simplify the subsequent code.
+ $dependencies = array_filter($dependencies);
+ $dependency_data = array_intersect_key($dependency_data, $dependencies);
+ $dependency_data += array_fill_keys(array_keys($dependencies), []);
+ $call_on_removal = [];
+
+ foreach ($dependencies as $dependency_type => $dependency_objects) {
+ // Annoyingly, modules and theme dependencies come not keyed by dependency
+ // name here, while entities do. Flip the array for modules and themes to
+ // make the code simpler.
+ if (in_array($dependency_type, ['module', 'theme'])) {
+ $dependency_objects = array_flip($dependency_objects);
+ }
+ $dependency_data[$dependency_type] = array_intersect_key($dependency_data[$dependency_type], $dependency_objects);
+ foreach ($dependency_data[$dependency_type] as $name => $dependency_sources) {
+ // We first remove all the "hard" dependencies.
+ if (!empty($dependency_sources['always'])) {
+ foreach ($dependency_sources['always'] as $plugin_type => $plugins) {
+ // We can hardly remove the index itself.
+ if ($plugin_type == 'index') {
+ continue;
+ }
+
+ // This will definitely lead to a change.
+ $changed = TRUE;
+
+ if ($plugin_type == 'fields') {
+ // Remove a field from the index that is being removed from the
+ // system.
+ /** @var \Drupal\search_api\Item\FieldInterface $field */
+ foreach ($plugins as $field_id => $field) {
+ // In case the field is locked, unlock it before removing.
+ if ($field->isIndexedLocked()) {
+ $field->setIndexedLocked(FALSE);
+ }
+ $this->removeField($field_id);
+ }
+ }
+ else {
+ // For all other types, just remove the plugin from our list.
+ $all_plugins[$plugin_type] = array_diff_key($all_plugins[$plugin_type], $plugins);
+ }
+ }
+ }
+
+ // Then, collect all the optional ones.
+ if (!empty($dependency_sources['optional'])) {
+ // However this plays out, it will lead to a change.
+ $changed = TRUE;
+
+ foreach ($dependency_sources['optional'] as $plugin_type => $plugins) {
+ // Deal with the index right away, since that dependency can only be
+ // the server.
+ if ($plugin_type == 'index') {
+ $this->setServer(NULL);
+ continue;
+ }
+
+ // Fields can only have optional dependencies caused by their data
+ // type plugin. Reset to the fallback type.
+ if ($plugin_type == 'fields') {
+ foreach ($plugins as $field) {
+ $field->setType($field->getDataTypePlugin()->getFallbackType());
+ }
+ continue;
+ }
+
+ // Only include those plugins that have not already been removed.
+ $plugins = array_intersect_key($plugins, $all_plugins[$plugin_type]);
+
+ foreach ($plugins as $plugin_id => $plugin) {
+ $call_on_removal[$plugin_type][$plugin_id][$dependency_type][$name] = $dependency_objects[$name];
+ }
+ }
+ }
+ }
+ }
+
+ // Now for all plugins with optional dependencies (stored in
+ // $call_on_removal, mapped to their removed dependencies) call their
+ // onDependencyRemoval() methods.
+ $updated_config = [];
+ foreach ($call_on_removal as $plugin_type => $plugins) {
+ foreach ($plugins as $plugin_id => $plugin_dependencies) {
+ $removal_successful = $all_plugins[$plugin_type][$plugin_id]->onDependencyRemoval($plugin_dependencies);
+ // If the plugin was successfully changed to remove the dependency,
+ // remember the new configuration to later set it. Otherwise, remove the
+ // plugin from the index so the dependency still gets removed.
+ if ($removal_successful) {
+ $updated_config[$plugin_type][$plugin_id] = $all_plugins[$plugin_type][$plugin_id]->getConfiguration();
+ }
+ else {
+ unset($all_plugins[$plugin_type][$plugin_id]);
+ }
+ }
+ }
+
+ // The handling of how we translate plugin changes back to the index varies
+ // according to plugin type, unfortunately.
+ // First, remove plugins that need to be removed.
+ $this->processor_settings = array_intersect_key($this->processor_settings, $all_plugins['processors']);
+ $this->processorInstances = array_intersect_key($this->processorInstances, $all_plugins['processors']);
+
+ $this->datasource_settings = array_intersect_key($this->datasource_settings, $all_plugins['datasources']);
+ $this->datasourceInstances = array_intersect_key($this->datasourceInstances, $all_plugins['datasources']);
+
+ // There always needs to be a tracker so reset it back to the default
+ // tracker.
+ if (empty($all_plugins['tracker'])) {
+ $default_tracker_id = \Drupal::config('search_api.settings')
+ ->get('default_tracker');
+
+ $this->tracker_settings = [
+ $default_tracker_id => [],
+ ];
+ // Reset $trackerInstance so it will get newly loaded from our reset
+ // settings when required.
+ $this->trackerInstance = NULL;
+ }
+
+ // There also always needs to be a datasource, but here we have no easy way
+ // out – if we had to remove all datasources, the operation fails. Return
+ // FALSE to indicate this, which will cause the index to be deleted.
+ if (!$this->datasource_settings) {
+ return FALSE;
+ }
+
+ // Then, update configuration as necessary.
+ foreach ($updated_config as $plugin_type => $plugin_configs) {
+ foreach ($plugin_configs as $plugin_id => $plugin_config) {
+ switch ($plugin_type) {
+ case 'processors':
+ $this->processor_settings[$plugin_id] = $plugin_config;
+ break;
+
+ case 'datasources':
+ $this->datasource_settings[$plugin_id] = $plugin_config;
+ break;
+
+ case 'tracker':
+ $this->tracker_settings[$plugin_id] = $plugin_config;
+ break;
+
+ }
+ }
+ }
+
+ return $changed;
+ }
+
+ /**
+ * Retrieves all the plugins contained in this index.
+ *
+ * @return \Drupal\search_api\Plugin\IndexPluginInterface[][]
+ * All plugins contained in this index, keyed by their property on the index
+ * and their plugin ID.
+ */
+ protected function getAllPlugins() {
+ $plugins = [];
+
+ if ($this->hasValidTracker()) {
+ $plugins['tracker'][$this->getTrackerId()] = $this->getTrackerInstance();
+ }
+ $plugins['processors'] = $this->getProcessors();
+ $plugins['datasources'] = $this->getDatasources();
+
+ return $plugins;
+ }
+
+ /**
+ * Implements the magic __sleep() method.
+ *
+ * Prevents the instantiated plugins and fields from being serialized.
+ */
+ public function __sleep() {
+ // First, write our changes to the persistent *_settings properties so they
+ // won't be discarded. Make sure we have a container to do this. This is
+ // important to correctly display test failures.
+ if (\Drupal::hasContainer()) {
+ $this->writeChangesToSettings();
+ }
+
+ // Then, return a list of all properties that don't contain objects.
+ $properties = get_object_vars($this);
+ unset($properties['datasourceInstances']);
+ unset($properties['trackerInstance']);
+ unset($properties['serverInstance']);
+ unset($properties['processorInstances']);
+ unset($properties['fieldInstances']);
+ unset($properties['properties']);
+ return array_keys($properties);
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Entity/SearchApiConfigEntityStorage.php b/sites/all/modules/contrib/search/search_api/src/Entity/SearchApiConfigEntityStorage.php
new file mode 100644
index 000000000..1505b084d
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Entity/SearchApiConfigEntityStorage.php
@@ -0,0 +1,70 @@
+resetCache([$entity->id()]);
+
+ // The entity is no longer new.
+ $entity->enforceIsNew(FALSE);
+
+ $overridden_entity = $this->load($entity->id());
+ if (isset($entity->original)) {
+ $overridden_entity->original = $entity->original;
+
+ // In the case of indexes, we also need to clone the fields to allow the
+ // correct detection of renamed field. Conversely, we need to set the new,
+ // rename-free fields on the passed index ($entity) so a subsequent save
+ // won't false detect field renames.
+ if ($entity instanceof IndexInterface) {
+ /** @var \Drupal\search_api\IndexInterface $overridden_entity */
+ $old_fields = $entity->original->getFields();
+ $new_fields = $entity->getFields();
+ $saved_fields = $overridden_entity->getFields();
+ foreach ($entity->getFieldRenames() as $old_id => $new_id) {
+ if (!empty($old_fields[$old_id]) && !empty($saved_fields[$new_id])) {
+ $field = clone $new_fields[$new_id];
+ $field->setIndex($overridden_entity);
+ $saved_fields[$new_id] = $field;
+ }
+ }
+ $overridden_entity->setFields($saved_fields);
+ }
+ }
+
+ // Allow code to run after saving.
+ $overridden_entity->postSave($this, $update);
+ $this->invokeHook($update ? 'update' : 'insert', $overridden_entity);
+
+ if ($entity instanceof IndexInterface) {
+ // Reset the field instances so saved renames won't be reported anymore.
+ $entity->discardFieldChanges();
+ $overridden_entity->discardFieldChanges();
+ }
+
+ // After saving, this is now the "original entity", and subsequent saves
+ // will be updates instead of inserts, and updates must always be able to
+ // correctly identify the original entity.
+ $entity->setOriginalId($entity->id());
+ $overridden_entity->setOriginalId($entity->id());
+
+ unset($entity->original);
+ unset($overridden_entity->original);
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Entity/Server.php b/sites/all/modules/contrib/search/search_api/src/Entity/Server.php
new file mode 100644
index 000000000..4515e1741
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Entity/Server.php
@@ -0,0 +1,623 @@
+description;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hasValidBackend() {
+ $backend_plugin_definition = \Drupal::service('plugin.manager.search_api.backend')->getDefinition($this->getBackendId(), FALSE);
+ return !empty($backend_plugin_definition);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getBackendId() {
+ return $this->backend;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getBackend() {
+ if (!$this->backendPlugin) {
+ $backend_plugin_manager = \Drupal::service('plugin.manager.search_api.backend');
+ $config = $this->backend_config;
+ $config['#server'] = $this;
+ if (!($this->backendPlugin = $backend_plugin_manager->createInstance($this->getBackendId(), $config))) {
+ $backend_id = $this->getBackendId();
+ $label = $this->label();
+ throw new SearchApiException("The backend with ID '$backend_id' could not be retrieved for server '$label'.");
+ }
+ }
+ return $this->backendPlugin;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getBackendConfig() {
+ return $this->backend_config;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setBackendConfig(array $backend_config) {
+ $this->backend_config = $backend_config;
+ $this->getBackend()->setConfiguration($backend_config);
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getIndexes(array $properties = []) {
+ $storage = \Drupal::entityTypeManager()->getStorage('search_api_index');
+ return $storage->loadByProperties(['server' => $this->id()] + $properties);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function viewSettings() {
+ return $this->hasValidBackend() ? $this->getBackend()->viewSettings() : [];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isAvailable() {
+ return $this->hasValidBackend() ? $this->getBackend()->isAvailable() : FALSE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function supportsFeature($feature) {
+ return in_array($feature, $this->getSupportedFeatures());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSupportedFeatures() {
+ if (!isset($this->features)) {
+ $this->features = [];
+ if ($this->hasValidBackend()) {
+ $this->features = $this->getBackend()->getSupportedFeatures();
+ }
+ \Drupal::moduleHandler()
+ ->alter('search_api_server_features', $this->features, $this);
+ }
+
+ return $this->features;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function supportsDataType($type) {
+ if ($this->hasValidBackend()) {
+ return $this->getBackend()->supportsDataType($type);
+ }
+ return FALSE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDiscouragedProcessors() {
+ if ($this->hasValidBackend()) {
+ return $this->getBackend()->getDiscouragedProcessors();
+ }
+ return [];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getBackendDefinedFields(IndexInterface $index) {
+ if ($this->hasValidBackend()) {
+ return $this->getBackend()->getBackendDefinedFields($index);
+ }
+ return [];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addIndex(IndexInterface $index) {
+ $server_task_manager = \Drupal::getContainer()->get('search_api.server_task_manager');
+ // When freshly adding an index to a server, it doesn't make any sense to
+ // execute possible other tasks for that server/index combination.
+ // (removeIndex() is implicit when adding an index which was already added.)
+ $server_task_manager->delete($this, $index);
+
+ try {
+ if ($server_task_manager->execute($this)) {
+ $this->getBackend()->addIndex($index);
+ return;
+ }
+ }
+ catch (SearchApiException $e) {
+ $vars = [
+ '%server' => $this->label(),
+ '%index' => $index->label(),
+ ];
+ $this->logException($e, '%type while adding index %index to server %server: @message in %function (line %line of %file).', $vars);
+ }
+
+ $task_manager = \Drupal::getContainer()
+ ->get('search_api.task_manager');
+ $task_manager->addTask(__FUNCTION__, $this, $index);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function updateIndex(IndexInterface $index) {
+ $server_task_manager = \Drupal::getContainer()->get('search_api.server_task_manager');
+ try {
+ if ($server_task_manager->execute($this)) {
+ $this->getBackend()->updateIndex($index);
+ return;
+ }
+ }
+ catch (SearchApiException $e) {
+ $vars = [
+ '%server' => $this->label(),
+ '%index' => $index->label(),
+ ];
+ $this->logException($e, '%type while updating the fields of index %index on server %server: @message in %function (line %line of %file).', $vars);
+ }
+
+ $task_manager = \Drupal::getContainer()
+ ->get('search_api.task_manager');
+ $task_manager->addTask(__FUNCTION__, $this, $index, isset($index->original) ? $index->original : NULL);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function removeIndex($index) {
+ $server_task_manager = \Drupal::getContainer()->get('search_api.server_task_manager');
+ // When removing an index from a server, it doesn't make any sense anymore
+ // to delete items from it, or react to other changes.
+ $server_task_manager->delete($this, $index);
+
+ try {
+ if ($server_task_manager->execute($this)) {
+ $this->getBackend()->removeIndex($index);
+ return;
+ }
+ }
+ catch (SearchApiException $e) {
+ $vars = [
+ '%server' => $this->label(),
+ '%index' => is_object($index) ? $index->label() : $index,
+ ];
+ $this->logException($e, '%type while removing index %index from server %server: @message in %function (line %line of %file).', $vars);
+ }
+
+ $task_manager = \Drupal::getContainer()
+ ->get('search_api.task_manager');
+ $data = NULL;
+ if (!is_object($index)) {
+ $data = $index;
+ $index = NULL;
+ }
+ $task_manager->addTask(__FUNCTION__, $this, $index, $data);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function indexItems(IndexInterface $index, array $items) {
+ $server_task_manager = \Drupal::getContainer()->get('search_api.server_task_manager');
+ if ($server_task_manager->execute($this)) {
+ return $this->getBackend()->indexItems($index, $items);
+ }
+ $index_label = $index->label();
+ throw new SearchApiException("Could not index items on index '$index_label' because pending server tasks could not be executed.");
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function deleteItems(IndexInterface $index, array $item_ids) {
+ if ($index->isReadOnly()) {
+ $vars = [
+ '%index' => $index->label(),
+ ];
+ $this->getLogger()->warning('Trying to delete items from index %index which is marked as read-only.', $vars);
+ return;
+ }
+
+ $server_task_manager = \Drupal::getContainer()->get('search_api.server_task_manager');
+ try {
+ if ($server_task_manager->execute($this)) {
+ $this->getBackend()->deleteItems($index, $item_ids);
+ // Clear search api list caches.
+ Cache::invalidateTags(['search_api_list:' . $index->id()]);
+ return;
+ }
+ }
+ catch (SearchApiException $e) {
+ $vars = [
+ '%server' => $this->label(),
+ ];
+ $this->logException($e, '%type while deleting items from server %server: @message in %function (line %line of %file).', $vars);
+ }
+
+ $task_manager = \Drupal::getContainer()
+ ->get('search_api.task_manager');
+ $task_manager->addTask(__FUNCTION__, $this, $index, $item_ids);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function deleteAllIndexItems(IndexInterface $index, $datasource_id = NULL) {
+ if ($index->isReadOnly()) {
+ $vars = [
+ '%index' => $index->label(),
+ ];
+ $this->getLogger()->warning('Trying to delete items from index %index which is marked as read-only.', $vars);
+ return;
+ }
+
+ $server_task_manager = \Drupal::getContainer()->get('search_api.server_task_manager');
+
+ if (!$datasource_id) {
+ // If we're deleting all items of the index, there's no point in keeping
+ // any other "delete items" tasks.
+ $types = [
+ 'deleteItems',
+ 'deleteAllIndexItems',
+ ];
+ $server_task_manager->delete($this, $index, $types);
+ }
+
+ try {
+ if ($server_task_manager->execute($this)) {
+ $this->getBackend()->deleteAllIndexItems($index, $datasource_id);
+ // Clear search api list caches.
+ Cache::invalidateTags(['search_api_list:' . $index->id()]);
+ return;
+ }
+ }
+ catch (SearchApiException $e) {
+ $vars = [
+ '%server' => $this->label(),
+ '%index' => $index->label(),
+ ];
+ $this->logException($e, '%type while deleting items of index %index from server %server: @message in %function (line %line of %file).', $vars);
+ }
+
+ $task_manager = \Drupal::getContainer()
+ ->get('search_api.task_manager');
+ $task_manager->addTask(__FUNCTION__, $this, $index, $datasource_id);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function deleteAllItems() {
+ $failed = [];
+ $properties['status'] = TRUE;
+ $properties['read_only'] = FALSE;
+ foreach ($this->getIndexes($properties) as $index) {
+ try {
+ $this->getBackend()->deleteAllIndexItems($index);
+ Cache::invalidateTags(['search_api_list:' . $index->id()]);
+ }
+ catch (SearchApiException $e) {
+ $args = [
+ '%index' => $index->label(),
+ ];
+ $this->logException($e, '%type while deleting all items from index %index: @message in %function (line %line of %file).', $args);
+ $failed[] = $index->label();
+ }
+ }
+ if (!empty($e)) {
+ $server_name = $this->label();
+ $failed = implode(', ', $failed);
+ throw new SearchApiException("Deleting all items from server '$server_name' failed for the following (write-enabled) indexes: $failed.", 0, $e);
+ }
+
+ $types = [
+ 'deleteItems',
+ 'deleteAllIndexItems',
+ ];
+ \Drupal::getContainer()
+ ->get('search_api.server_task_manager')
+ ->delete($this, NULL, $types);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function search(QueryInterface $query) {
+ $this->getBackend()->search($query);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function preSave(EntityStorageInterface $storage) {
+ parent::preSave($storage);
+
+ // The rest of the code only applies to updates.
+ if (!isset($this->original)) {
+ return;
+ }
+ // Retrieve active config overrides for this server.
+ $overrides = Utility::getConfigOverrides($this);
+
+ // If there are overrides for the backend or its configuration, attempt to
+ // apply them for the preUpdate() call.
+ if (isset($overrides['backend']) || isset($overrides['backend_config'])) {
+ $backend_config = $this->getBackendConfig();
+ if (isset($overrides['backend_config'])) {
+ $backend_config = $overrides['backend_config'];
+ }
+ $backend_id = $this->getBackendId();
+ if (isset($overrides['backend'])) {
+ $backend_id = $overrides['backend'];
+ }
+ $backend_plugin_manager = \Drupal::service('plugin.manager.search_api.backend');
+ $backend_config['#server'] = $this;
+ if (!($backend = $backend_plugin_manager->createInstance($backend_id, $backend_config))) {
+ $label = $this->label();
+ throw new SearchApiException("The backend with ID '$backend_id' could not be retrieved for server '$label'.");
+ }
+ }
+ else {
+ $backend = $this->getBackend();
+ }
+
+ $backend->preUpdate();
+
+ // If the server is being disabled, also disable all its indexes.
+ if (!$this->isSyncing()
+ && !isset($overrides['status'])
+ && !$this->status()
+ && $this->original->status()) {
+ foreach ($this->getIndexes(['status' => TRUE]) as $index) {
+ /** @var \Drupal\search_api\IndexInterface $index */
+ $index->setStatus(FALSE)->save();
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function postSave(EntityStorageInterface $storage, $update = TRUE) {
+ if ($this->hasValidBackend()) {
+ if ($update) {
+ $reindexing_necessary = $this->getBackend()->postUpdate();
+ if ($reindexing_necessary) {
+ foreach ($this->getIndexes() as $index) {
+ $index->reindex();
+ }
+ }
+ }
+ else {
+ $this->getBackend()->postInsert();
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function preDelete(EntityStorageInterface $storage, array $entities) {
+ // @todo This will, via Index::onDependencyRemoval(), remove all indexes
+ // from this server, triggering the server's removeIndex() method. This
+ // is, at best, wasted performance and could at worst lead to a bug if
+ // removeIndex() saves the server. We should try what happens when this is
+ // the case, whether there really is a bug, and try to fix it somehow –
+ // maybe clever detection of this case in removeIndex() or
+ // Index::postSave(). $server->isUninstalling() might help?
+ parent::preDelete($storage, $entities);
+
+ // Iterate through the servers, executing the backends' preDelete() methods
+ // and removing all their pending server tasks.
+ foreach ($entities as $server) {
+ /** @var \Drupal\search_api\ServerInterface $server */
+ if ($server->hasValidBackend()) {
+ $server->getBackend()->preDelete();
+ }
+ \Drupal::getContainer()->get('search_api.server_task_manager')->delete($server);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function toArray() {
+ // @todo It's a bug that we have to do this. Backend configuration should
+ // always be set via the server's setBackendConfiguration() method,
+ // otherwise the two can diverge causing this and other problems. The
+ // alternative would be to call $server->setBackendConfiguration() in the
+ // backend's setConfiguration() method and use a second $propagate
+ // parameter to avoid an infinite loop. Similar things go for the index's
+ // various plugins. Maybe using plugin bags is the solution here?
+ $properties = parent::toArray();
+ if ($this->hasValidBackend()) {
+ $properties['backend_config'] = $this->getBackend()->getConfiguration();
+ }
+ return $properties;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function calculateDependencies() {
+ parent::calculateDependencies();
+
+ // Add the backend's dependencies.
+ if ($this->hasValidBackend()) {
+ $this->calculatePluginDependencies($this->getBackend());
+ }
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function onDependencyRemoval(array $dependencies) {
+ $changed = parent::onDependencyRemoval($dependencies);
+
+ if ($this->hasValidBackend()) {
+ $removed_backend_dependencies = [];
+ $backend = $this->getBackend();
+ foreach ($backend->calculateDependencies() as $dependency_type => $list) {
+ if (isset($dependencies[$dependency_type])) {
+ $removed_backend_dependencies[$dependency_type] = array_intersect_key($dependencies[$dependency_type], array_flip($list));
+ }
+ }
+ $removed_backend_dependencies = array_filter($removed_backend_dependencies);
+ if ($removed_backend_dependencies) {
+ if ($backend->onDependencyRemoval($removed_backend_dependencies)) {
+ $this->backend_config = $backend->getConfiguration();
+ $changed = TRUE;
+ }
+ }
+ }
+
+ return $changed;
+ }
+
+ /**
+ * Implements the magic __clone() method.
+ *
+ * Prevents the backend plugin instance from being cloned.
+ */
+ public function __clone() {
+ $this->backendPlugin = NULL;
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Entity/Task.php b/sites/all/modules/contrib/search/search_api/src/Entity/Task.php
new file mode 100644
index 000000000..09edc21d9
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Entity/Task.php
@@ -0,0 +1,184 @@
+entityTypeManager ?: \Drupal::entityTypeManager();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setEntityTypeManager(EntityTypeManagerInterface $entityTypeManager) {
+ $this->entityTypeManager = $entityTypeManager;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getType() {
+ return $this->get('type')[0]->value;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getServerId() {
+ $field = $this->get('server_id')[0];
+ return $field ? $field->value : NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getServer() {
+ $server_id = $this->getServerId();
+ if ($server_id && !isset($this->serverInstance)) {
+ $this->serverInstance = $this->getEntityTypeManager()
+ ->getStorage('search_api_server')
+ ->load($server_id);
+ if (!$this->serverInstance) {
+ $args['%server'] = $server_id;
+ throw new SearchApiException("Could not load server with ID '$server_id'.");
+ }
+ }
+
+ return $this->serverInstance;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getIndexId() {
+ $field = $this->get('index_id')[0];
+ return $field ? $field->value : NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getIndex() {
+ $index_id = $this->getIndexId();
+ if ($index_id && !isset($this->indexInstance)) {
+ $this->indexInstance = $this->getEntityTypeManager()
+ ->getStorage('search_api_index')
+ ->load($index_id);
+ if (!$this->indexInstance) {
+ throw new SearchApiException("Could not load index with ID '$index_id'.");
+ }
+ }
+
+ return $this->indexInstance;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getData() {
+ if (!isset($this->unserializedData)) {
+ $data = $this->get('data')[0];
+ if ($data) {
+ $this->unserializedData = unserialize($data->value);
+ if (!empty($this->unserializedData['#entity_type'])) {
+ $this->unserializedData = \Drupal::entityTypeManager()
+ ->getStorage($this->unserializedData['#entity_type'])
+ ->create($this->unserializedData['#values']);
+ }
+ }
+ }
+
+ return $this->unserializedData;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
+ /** @var \Drupal\Core\Field\BaseFieldDefinition[] $fields */
+ $fields = parent::baseFieldDefinitions($entity_type);
+
+ $fields['type'] = BaseFieldDefinition::create('string')
+ ->setLabel(t('Task type'))
+ ->setSetting('max_length', 50)
+ ->setRequired(TRUE)
+ ->setReadOnly(TRUE);
+
+ $fields['server_id'] = BaseFieldDefinition::create('string')
+ ->setLabel(t('Server ID'))
+ ->setSetting('max_length', 50)
+ ->setReadOnly(TRUE);
+
+ $fields['index_id'] = BaseFieldDefinition::create('string')
+ ->setLabel(t('Index ID'))
+ ->setSetting('max_length', 50)
+ ->setReadOnly(TRUE);
+
+ $fields['data'] = BaseFieldDefinition::create('string_long')
+ ->setLabel(t('Task data'))
+ ->setReadOnly(TRUE);
+
+ return $fields;
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/FacetsQueryTypeMappingInterface.php b/sites/all/modules/contrib/search/search_api/src/FacetsQueryTypeMappingInterface.php
new file mode 100644
index 000000000..ee6729430
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/FacetsQueryTypeMappingInterface.php
@@ -0,0 +1,23 @@
+entityTypeManager = $entity_type_manager;
+ $this->renderer = $renderer;
+ $this->dateFormatter = $date_formatter;
+ $this->requestStack = $request_stack;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ $entity_type_manager = $container->get('entity_type.manager');
+ $renderer = $container->get('renderer');
+ $date_formatter = $container->get('date.formatter');
+ $request_stack = $container->get('request_stack');
+
+ return new static($entity_type_manager, $renderer, $date_formatter, $request_stack);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(array $form, FormStateInterface $form_state) {
+ $field = $this->getField();
+
+ if (!$field) {
+ $args['@id'] = $this->getRequest()->attributes->get('field_id');
+ $form['message'] = [
+ '#markup' => $this->t('Unknown field with ID "@id".', $args),
+ ];
+ return $form;
+ }
+
+ $args['%field'] = $field->getLabel();
+ $form['#title'] = $this->t('Edit field %field', $args);
+
+ if ($this->getRequest()->query->get('modal_redirect')) {
+ $form['title']['#markup'] = new FormattableMarkup('@title
', ['@title' => $form['#title']]);
+ Html::setIsAjax(TRUE);
+ }
+
+ $this->formIdAttribute = Html::getUniqueId($this->getFormId());
+ $form['#id'] = $this->formIdAttribute;
+
+ $form['messages'] = [
+ '#type' => 'status_messages',
+ ];
+
+ $property = $field->getDataDefinition();
+ if (!($property instanceof ConfigurablePropertyInterface)) {
+ $args['%field'] = $field->getLabel();
+ $form['message'] = [
+ '#markup' => $this->t('Field %field is not configurable.', $args),
+ ];
+ return $form;
+ }
+
+ return parent::buildForm($form, $form_state);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function form(array $form, FormStateInterface $form_state) {
+ $form = parent::form($form, $form_state);
+
+ $field = $this->getField();
+ /** @var \Drupal\search_api\Processor\ConfigurablePropertyInterface $property */
+ $property = $field->getDataDefinition();
+
+ $form = $property->buildConfigurationForm($field, $form, $form_state);
+
+ $this->checkEntityEditable($form, $this->entity);
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function actions(array $form, FormStateInterface $form_state) {
+ $actions = parent::actions($form, $form_state);
+ unset($actions['delete']);
+
+ if ($this->getRequest()->query->get('modal_redirect')) {
+ $actions['submit']['#ajax']['wrapper'] = $this->formIdAttribute;
+ }
+ else {
+ $actions['cancel'] = [
+ '#type' => 'link',
+ '#title' => $this->t('Cancel'),
+ '#url' => $this->entity->toUrl('fields'),
+ ];
+ }
+
+ return $actions;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validateForm(array &$form, FormStateInterface $form_state) {
+ $field = $this->getField();
+ /** @var \Drupal\search_api\Processor\ConfigurablePropertyInterface $property */
+ $property = $field->getDataDefinition();
+ $property->validateConfigurationForm($field, $form, $form_state);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ $field = $this->getField();
+ /** @var \Drupal\search_api\Processor\ConfigurablePropertyInterface $property */
+ $property = $field->getDataDefinition();
+ $property->submitConfigurationForm($field, $form, $form_state);
+
+ drupal_set_message($this->t('The field configuration was successfully saved.'));
+ if ($this->getRequest()->query->get('modal_redirect')) {
+ $url = $this->entity->toUrl('add-fields-ajax')
+ ->setOption('query', [
+ MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax',
+ ]);
+ $form_state->setRedirectUrl($url);
+ }
+ else {
+ $form_state->setRedirectUrl($this->entity->toUrl('fields'));
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function copyFormValuesToEntity(EntityInterface $entity, array $form, FormStateInterface $form_state) {
+ // Our form structure doesn't emulate the entity structure, so copying those
+ // values wouldn't make any sense.
+ }
+
+ /**
+ * Retrieves the field that is being edited.
+ *
+ * @return \Drupal\search_api\Item\FieldInterface|null
+ * The field, if it exists.
+ */
+ protected function getField() {
+ if (!isset($this->field)) {
+ $field_id = $this->getRequest()->attributes->get('field_id');
+ $this->field = $this->entity->getField($field_id);
+ }
+
+ return $this->field;
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Form/IndexAddFieldsForm.php b/sites/all/modules/contrib/search/search_api/src/Form/IndexAddFieldsForm.php
new file mode 100644
index 000000000..8966d6d1f
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Form/IndexAddFieldsForm.php
@@ -0,0 +1,486 @@
+entityTypeManager = $entity_type_manager;
+ $this->fieldsHelper = $fields_helper;
+ $this->dataTypeHelper = $data_type_helper;
+ $this->renderer = $renderer;
+ $this->dateFormatter = $date_formatter;
+ $this->parameters = $parameters;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ $entity_type_manager = $container->get('entity_type.manager');
+ $fields_helper = $container->get('search_api.fields_helper');
+ $data_type_helper = $container->get('search_api.data_type_helper');
+ $renderer = $container->get('renderer');
+ $date_formatter = $container->get('date.formatter');
+ $request_stack = $container->get('request_stack');
+ $parameters = $request_stack->getCurrentRequest()->query->all();
+
+ return new static($entity_type_manager, $fields_helper, $data_type_helper, $renderer, $date_formatter, $parameters);
+ }
+
+ /**
+ * Retrieves a single page request parameter.
+ *
+ * @param string $name
+ * The name of the parameter.
+ * @param string|null $default
+ * The value to return if the parameter isn't present.
+ *
+ * @return string|null
+ * The parameter value.
+ */
+ public function getParameter($name, $default = NULL) {
+ return isset($this->parameters[$name]) ? $this->parameters[$name] : $default;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(array $form, FormStateInterface $form_state) {
+ $index = $this->entity;
+
+ // Do not allow the form to be cached. See
+ // \Drupal\views_ui\ViewEditForm::form().
+ $form_state->disableCache();
+
+ $this->checkEntityEditable($form, $index);
+
+ $args['%index'] = $index->label();
+ $form['#title'] = $this->t('Add fields to index %index', $args);
+
+ $this->formIdAttribute = Html::getUniqueId($this->getFormId());
+ $form['#id'] = $this->formIdAttribute;
+
+ $form['messages'] = [
+ '#type' => 'status_messages',
+ ];
+
+ $datasources = [
+ '' => NULL,
+ ];
+ $datasources += $this->entity->getDatasources();
+ foreach ($datasources as $datasource_id => $datasource) {
+ $item = $this->getDatasourceListItem($datasource);
+ if ($item) {
+ $form['datasources']['datasource_' . $datasource_id] = $item;
+ }
+ }
+
+ $form['actions'] = $this->actionsElement($form, $form_state);
+
+ // Log any unmapped types that were encountered.
+ if ($this->unmappedFields) {
+ $unmapped_fields = [];
+ foreach ($this->unmappedFields as $type => $fields) {
+ foreach ($fields as $field) {
+ $unmapped_fields[] = new FormattableMarkup('@field (type "@type")', [
+ '@field' => $field,
+ '@type' => $type,
+ ]);
+ }
+ }
+ $form['unmapped_types'] = [
+ '#type' => 'details',
+ '#title' => $this->t('Skipped fields'),
+ 'fields' => [
+ '#theme' => 'item_list',
+ '#items' => $unmapped_fields,
+ '#prefix' => $this->t('The following fields cannot be indexed since there is no type mapping for them:'),
+ '#suffix' => $this->t("If you think one of these fields should be available for indexing, please report this in the module's issue queue. (Make sure to first search for an existing issue for this field.) Please note that entity-valued fields generally can be indexed by either indexing their parent reference field, or their child entity ID field.", [':url' => Url::fromUri('https://www.drupal.org/project/issues/search_api')->toString()]),
+ ],
+ ];
+ }
+
+ return $form;
+ }
+
+ /**
+ * Creates a list item for one datasource.
+ *
+ * @param \Drupal\search_api\Datasource\DatasourceInterface|null $datasource
+ * The datasource, or NULL for general properties.
+ *
+ * @return array
+ * A render array representing the given datasource and, possibly, its
+ * attached properties.
+ */
+ protected function getDatasourceListItem(DatasourceInterface $datasource = NULL) {
+ $datasource_id = $datasource ? $datasource->getPluginId() : NULL;
+ $datasource_id_param = $datasource_id ?: '';
+ $properties = $this->entity->getPropertyDefinitions($datasource_id);
+ if ($properties) {
+ $active_property_path = '';
+ $active_datasource = $this->getParameter('datasource');
+ if ($active_datasource !== NULL && $active_datasource == $datasource_id_param) {
+ $active_property_path = $this->getParameter('property_path', '');
+ }
+
+ $base_url = $this->entity->toUrl('add-fields');
+ $base_url->setOption('query', ['datasource' => $datasource_id_param]);
+
+ $item = $this->getPropertiesList($properties, $active_property_path, $base_url, $datasource_id);
+ $item['#title'] = $datasource ? $datasource->label() : $this->t('General');
+ return $item;
+ }
+
+ return NULL;
+ }
+
+ /**
+ * Creates an items list for the given properties.
+ *
+ * @param \Drupal\Core\TypedData\DataDefinitionInterface[] $properties
+ * The property definitions, keyed by their property names.
+ * @param string $active_property_path
+ * The relative property path to the active property.
+ * @param \Drupal\Core\Url $base_url
+ * The base URL to which property path parameters should be added for
+ * the navigation links.
+ * @param string|null $datasource_id
+ * The datasource ID of the listed properties, or NULL for
+ * datasource-independent properties.
+ * @param string $parent_path
+ * (optional) The common property path prefix of the given properties.
+ * @param string $label_prefix
+ * (optional) The prefix to use for the labels of created fields.
+ *
+ * @return array
+ * A render array representing the given properties and, possibly, nested
+ * properties.
+ */
+ protected function getPropertiesList(array $properties, $active_property_path, Url $base_url, $datasource_id, $parent_path = '', $label_prefix = '') {
+ $list = [];
+
+ $active_item = '';
+ if ($active_property_path) {
+ list($active_item, $active_property_path) = explode(':', $active_property_path, 2) + [1 => ''];
+ }
+
+ $type_mapping = $this->dataTypeHelper->getFieldTypeMapping();
+
+ $query_base = $base_url->getOption('query');
+ foreach ($properties as $key => $property) {
+ if ($property instanceof ProcessorPropertyInterface && $property->isHidden()) {
+ continue;
+ }
+ $this_path = $parent_path ? $parent_path . ':' : '';
+ $this_path .= $key;
+
+ $label = $property->getLabel();
+ $property = $this->fieldsHelper->getInnerProperty($property);
+
+ $can_be_indexed = TRUE;
+ $nested_properties = [];
+ $parent_child_type = NULL;
+ if ($property instanceof ComplexDataDefinitionInterface) {
+ $can_be_indexed = FALSE;
+ $nested_properties = $this->fieldsHelper->getNestedProperties($property);
+ $main_property = $property->getMainPropertyName();
+ if ($main_property && isset($nested_properties[$main_property])) {
+ $parent_child_type = $property->getDataType() . '.';
+ $property = $nested_properties[$main_property];
+ $parent_child_type .= $property->getDataType();
+ unset($nested_properties[$main_property]);
+ $can_be_indexed = TRUE;
+ }
+
+ // Don't add the additional "entity" property for entity reference
+ // fields which don't target a content entity type.
+ if (isset($nested_properties['entity'])) {
+ $entity_property = $nested_properties['entity'];
+ if ($entity_property instanceof DataReferenceDefinitionInterface) {
+ $target = $entity_property->getTargetDefinition();
+ if ($target instanceof EntityDataDefinitionInterface) {
+ if (!$this->fieldsHelper->isContentEntityType($target->getEntityTypeId())) {
+ unset($nested_properties['entity']);
+ }
+ }
+ }
+ }
+ }
+
+ // Don't allow indexing of properties with unmapped types. Also, prefer
+ // a "parent.child" type mapping (taking into account the parent property
+ // for, for example, text fields).
+ $type = $property->getDataType();
+ if ($parent_child_type && !empty($type_mapping[$parent_child_type])) {
+ $type = $parent_child_type;
+ }
+ elseif (empty($type_mapping[$type])) {
+ // Remember the type only if it was not explicitly mapped to FALSE.
+ if (!isset($type_mapping[$type])) {
+ $this->unmappedFields[$type][] = $label_prefix . $label;
+ }
+ $can_be_indexed = FALSE;
+ }
+
+ // If the property can neither be expanded nor indexed, just skip it.
+ if (!($nested_properties || $can_be_indexed)) {
+ continue;
+ }
+
+ $item = [
+ '#type' => 'container',
+ '#attributes' => [
+ 'class' => ['container-inline'],
+ ],
+ ];
+
+ $nested_list = [];
+ if ($nested_properties) {
+ if ($key == $active_item) {
+ $link_url = clone $base_url;
+ $query_base['property_path'] = $parent_path;
+ $link_url->setOption('query', $query_base);
+ $item['expand_link'] = [
+ '#type' => 'link',
+ '#title' => '(-) ',
+ '#url' => $link_url,
+ '#ajax' => [
+ 'wrapper' => $this->formIdAttribute,
+ ],
+ ];
+
+ $nested_list = $this->getPropertiesList($nested_properties, $active_property_path, $base_url, $datasource_id, $this_path, $label_prefix . $label . ' » ');
+ }
+ else {
+ $link_url = clone $base_url;
+ $query_base['property_path'] = $this_path;
+ $link_url->setOption('query', $query_base);
+ $item['expand_link'] = [
+ '#type' => 'link',
+ '#title' => '(+) ',
+ '#url' => $link_url,
+ '#ajax' => [
+ 'wrapper' => $this->formIdAttribute,
+ ],
+ ];
+ }
+ }
+
+ $label_markup = Html::escape($label);
+ $escaped_path = Html::escape($this_path);
+ $label_markup = "$label_markup ($escaped_path)";
+ $item['label']['#markup'] = $label_markup . ' ';
+
+ if ($can_be_indexed) {
+ $item['add'] = [
+ '#type' => 'submit',
+ '#name' => Utility::createCombinedId($datasource_id, $this_path),
+ '#value' => $this->t('Add'),
+ '#submit' => ['::addField', '::save'],
+ '#property' => $property,
+ '#prefixed_label' => $label_prefix . $label,
+ '#data_type' => $type_mapping[$type],
+ '#ajax' => [
+ 'wrapper' => $this->formIdAttribute,
+ ],
+ ];
+ }
+
+ if ($nested_list) {
+ $item['properties'] = $nested_list;
+ }
+
+ $list[$key] = $item;
+ }
+
+ if ($list) {
+ uasort($list, [static::class, 'compareFieldLabels']);
+ $list['#theme'] = 'search_api_form_item_list';
+ }
+
+ return $list;
+ }
+
+ /**
+ * Compares labels of property render arrays.
+ *
+ * Used as an uasort() callback in
+ * \Drupal\search_api\Form\IndexAddFieldsForm::getPropertiesList().
+ *
+ * @param array $a
+ * First property render array.
+ * @param array $b
+ * Second property render array.
+ *
+ * @return int
+ * -1, 0 or 1 if the first array should be considered, respectively, less
+ * than, equal to or greater than the second.
+ */
+ public static function compareFieldLabels(array $a, array $b) {
+ $a_title = (string) $a['label']['#markup'];
+ $b_title = (string) $b['label']['#markup'];
+
+ return strnatcasecmp($a_title, $b_title);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function actions(array $form, FormStateInterface $form_state) {
+ return [
+ 'done' => [
+ '#type' => 'link',
+ '#title' => $this->t('Done'),
+ '#url' => $this->entity->toUrl('fields'),
+ '#attributes' => [
+ 'class' => ['button'],
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * Form submission handler for adding a new field to the index.
+ *
+ * @param array $form
+ * An associative array containing the structure of the form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ */
+ public function addField(array $form, FormStateInterface $form_state) {
+ $button = $form_state->getTriggeringElement();
+ if (!$button) {
+ return;
+ }
+
+ /** @var \Drupal\Core\TypedData\DataDefinitionInterface $property */
+ $property = $button['#property'];
+
+ list($datasource_id, $property_path) = Utility::splitCombinedId($button['#name']);
+ $field = $this->fieldsHelper->createFieldFromProperty($this->entity, $property, $datasource_id, $property_path, NULL, $button['#data_type']);
+ $field->setLabel($button['#prefixed_label']);
+ $this->entity->addField($field);
+
+ if ($property instanceof ConfigurablePropertyInterface) {
+ $parameters = [
+ 'search_api_index' => $this->entity->id(),
+ 'field_id' => $field->getFieldIdentifier(),
+ ];
+ $options = [];
+ $route = $this->getRequest()->attributes->get('_route');
+ if ($route === 'entity.search_api_index.add_fields_ajax') {
+ $options['query'] = [
+ MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax',
+ 'modal_redirect' => 1,
+ ];
+ }
+ $form_state->setRedirect('entity.search_api_index.field_config', $parameters, $options);
+ }
+
+ $args['%label'] = $field->getLabel();
+ drupal_set_message($this->t('Field %label was added to the index.', $args));
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Form/IndexBreakLockForm.php b/sites/all/modules/contrib/search/search_api/src/Form/IndexBreakLockForm.php
new file mode 100644
index 000000000..70892d454
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Form/IndexBreakLockForm.php
@@ -0,0 +1,126 @@
+tempStore = $temp_store_factory->get('search_api_index');
+ $this->entityTypeManager = $entity_type_manager;
+ $this->renderer = $renderer;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ $temp_store_factory = $container->get('user.shared_tempstore');
+ $entity_type_manager = $container->get('entity_type.manager');
+ $renderer = $container->get('renderer');
+
+ return new static($temp_store_factory, $entity_type_manager, $renderer);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFormId() {
+ return 'search_api_index_break_lock_confirm';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getQuestion() {
+ return $this->t('Do you want to break the lock on search index %name?', ['%name' => $this->entity->id()]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDescription() {
+ $locked = $this->tempStore->getMetadata($this->entity->id());
+ $account = $this->entityTypeManager->getStorage('user')->load($locked->owner);
+ $username = [
+ '#theme' => 'username',
+ '#account' => $account,
+ ];
+ return $this->t('By breaking this lock, any unsaved changes made by @user will be lost.', ['@user' => $this->renderer->render($username)]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCancelUrl() {
+ return $this->entity->toUrl('fields');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getConfirmText() {
+ return $this->t('Break lock');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(array $form, FormStateInterface $form_state) {
+ if (!$this->tempStore->getMetadata($this->entity->id())) {
+ $form['message']['#markup'] = $this->t('There is no lock on search index %name to break.', ['%name' => $this->entity->id()]);
+ return $form;
+ }
+ return parent::buildForm($form, $form_state);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ $this->tempStore->delete($this->entity->id());
+ $form_state->setRedirectUrl($this->entity->toUrl('fields'));
+ drupal_set_message($this->t('The lock has been broken. You may now edit this search index.'));
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Form/IndexClearConfirmForm.php b/sites/all/modules/contrib/search/search_api/src/Form/IndexClearConfirmForm.php
new file mode 100644
index 000000000..528ba3497
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Form/IndexClearConfirmForm.php
@@ -0,0 +1,61 @@
+t('Are you sure you want to clear the indexed data for the search index %name?', ['%name' => $this->entity->label()]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDescription() {
+ return $this->t('All indexed data for this index will be deleted from the search server. Searches on this index will not return any results until items are reindexed. This action cannot be undone.');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCancelUrl() {
+ return new Url('entity.search_api_index.canonical', ['search_api_index' => $this->entity->id()]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ /** @var \Drupal\search_api\IndexInterface $index */
+ $index = $this->getEntity();
+
+ try {
+ $index->clear();
+ drupal_set_message($this->t('All items were successfully deleted from search index %name.', ['%name' => $index->label()]));
+ }
+ catch (SearchApiException $e) {
+ drupal_set_message($this->t('Failed to clear the search index %name.', ['%name' => $index->label()]), 'error');
+ $message = '%type while trying to clear the index %name: @message in %function (line %line of %file)';
+ $variables = [
+ '%name' => $index->label(),
+ ];
+ $variables += Error::decodeException($e);
+ $this->getLogger('search_api')->error($message, $variables);
+ }
+
+ $form_state->setRedirect('entity.search_api_index.canonical', ['search_api_index' => $index->id()]);
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Form/IndexDeleteConfirmForm.php b/sites/all/modules/contrib/search/search_api/src/Form/IndexDeleteConfirmForm.php
new file mode 100644
index 000000000..7709bdbb8
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Form/IndexDeleteConfirmForm.php
@@ -0,0 +1,44 @@
+t('Are you sure you want to delete the search index %name?', ['%name' => $this->entity->label()]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCancelUrl() {
+ return new Url('entity.search_api_index.canonical', ['search_api_index' => $this->entity->id()]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getConfirmText() {
+ return $this->t('Delete');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ $this->entity->delete();
+ drupal_set_message($this->t('The search index %name has been removed.', ['%name' => $this->entity->label()]));
+ $form_state->setRedirect('search_api.overview');
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Form/IndexDisableConfirmForm.php b/sites/all/modules/contrib/search/search_api/src/Form/IndexDisableConfirmForm.php
new file mode 100644
index 000000000..d9aa56dc4
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Form/IndexDisableConfirmForm.php
@@ -0,0 +1,54 @@
+t('Are you sure you want to disable the search index %name?', ['%name' => $this->entity->label()]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDescription() {
+ return $this->t('Searches on this index will stop working. Indexed data will be deleted from the server and will need to be reindexed when the index is enabled again.');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCancelUrl() {
+ return new Url('entity.search_api_index.canonical', ['search_api_index' => $this->entity->id()]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getConfirmText() {
+ return $this->t('Disable');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ /** @var \Drupal\search_api\IndexInterface $entity */
+ $entity = $this->entity;
+
+ $entity->setStatus(FALSE)->save();
+ drupal_set_message($this->t('The search index %name has been disabled.', ['%name' => $this->entity->label()]));
+ $form_state->setRedirect('search_api.overview');
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Form/IndexFieldsForm.php b/sites/all/modules/contrib/search/search_api/src/Form/IndexFieldsForm.php
new file mode 100644
index 000000000..eba7a48be
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Form/IndexFieldsForm.php
@@ -0,0 +1,498 @@
+tempStore = $temp_store_factory->get('search_api_index');
+ $this->entityTypeManager = $entity_type_manager;
+ $this->dataTypePluginManager = $data_type_plugin_manager;
+ $this->renderer = $renderer;
+ $this->dateFormatter = $date_formatter;
+ $this->dataTypeHelper = $data_type_helper;
+ $this->fieldsHelper = $fields_helper;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ $temp_store_factory = $container->get('user.shared_tempstore');
+ $entity_type_manager = $container->get('entity_type.manager');
+ $data_type_plugin_manager = $container->get('plugin.manager.search_api.data_type');
+ $renderer = $container->get('renderer');
+ $date_formatter = $container->get('date.formatter');
+ $data_type_helper = $container->get('search_api.data_type_helper');
+ $fields_helper = $container->get('search_api.fields_helper');
+
+ return new static($temp_store_factory, $entity_type_manager, $data_type_plugin_manager, $renderer, $date_formatter, $data_type_helper, $fields_helper);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(array $form, FormStateInterface $form_state) {
+ $form['#attached']['library'][] = 'core/drupal.dialog.ajax';
+
+ $index = $this->entity;
+
+ // Do not allow the form to be cached. See
+ // \Drupal\views_ui\ViewEditForm::form().
+ $form_state->disableCache();
+
+ $this->checkEntityEditable($form, $index, TRUE);
+
+ // Set an appropriate page title.
+ $form['#title'] = $this->t('Manage fields for search index %label', ['%label' => $index->label()]);
+ $form['#tree'] = TRUE;
+
+ $form['add-field'] = [
+ '#type' => 'link',
+ '#title' => $this->t('Add fields'),
+ '#url' => $this->entity->toUrl('add-fields'),
+ '#attributes' => [
+ 'class' => [
+ 'use-ajax',
+ 'button',
+ 'button-action',
+ 'button--primary',
+ 'button--small',
+ ],
+ 'data-dialog-type' => 'modal',
+ 'data-dialog-options' => Json::encode([
+ 'width' => 700,
+ ]),
+ ],
+ ];
+
+ $form['description']['#markup'] = $this->t('The data type of a field determines how it can be used for searching and filtering. The boost is used to give additional weight to certain fields, for example titles or tags.
For information about the data types available for indexing, see the data types table at the bottom of the page.
', ['@url' => '#search-api-data-types-table']);
+
+ if ($fields = $index->getFieldsByDatasource(NULL)) {
+ $form['_general'] = $this->buildFieldsTable($fields);
+ $form['_general']['#title'] = $this->t('General');
+ }
+
+ foreach ($index->getDatasources() as $datasource_id => $datasource) {
+ $fields = $index->getFieldsByDatasource($datasource_id);
+ $form[$datasource_id] = $this->buildFieldsTable($fields);
+ $form[$datasource_id]['#title'] = $datasource->label();
+ }
+
+ // Build the data type table.
+ $instances = $this->dataTypePluginManager->getInstances();
+ $fallback_mapping = $this->dataTypeHelper
+ ->getDataTypeFallbackMapping($index);
+
+ $data_types = [];
+ foreach ($instances as $name => $type) {
+ $data_types[$name] = [
+ 'label' => $type->label(),
+ 'description' => $type->getDescription(),
+ 'fallback' => $type->getFallbackType(),
+ ];
+ }
+
+ $form['data_type_explanation'] = [
+ '#type' => 'details',
+ '#id' => 'search-api-data-types-table',
+ '#title' => $this->t('Data types'),
+ '#description' => $this->t("The data types which can be used for indexing fields in this index. Whether a type is supported depends on the backend of the index's server. If a type is not supported, the fallback type that will be used instead is shown, too."),
+ '#theme' => 'search_api_admin_data_type_table',
+ '#data_types' => $data_types,
+ '#fallback_mapping' => $fallback_mapping,
+ ];
+
+ $form['actions'] = $this->actionsElement($form, $form_state);
+
+ return $form;
+ }
+
+ /**
+ * Builds the form fields for a set of fields.
+ *
+ * @param \Drupal\search_api\Item\FieldInterface[] $fields
+ * List of fields to display.
+ *
+ * @return array
+ * The build structure.
+ */
+ protected function buildFieldsTable(array $fields) {
+ $types = $this->dataTypePluginManager->getInstancesOptions();
+ $fallback_types = $this->dataTypeHelper
+ ->getDataTypeFallbackMapping($this->entity);
+
+ // If one of the unsupported types is actually used by the index, show a
+ // warning.
+ if ($fallback_types) {
+ foreach ($fields as $field) {
+ if (isset($fallback_types[$field->getType()])) {
+ drupal_set_message($this->t("Some of the used data types aren't supported by the server's backend. See the data types table to find out which types are supported.", [':url' => '#search-api-data-types-table']), 'warning');
+ break;
+ }
+ }
+ }
+
+ $fulltext_types = [
+ [
+ 'value' => 'text',
+ ],
+ ];
+ // Add all data types with fallback "text" to fulltext types as well.
+ foreach ($this->dataTypePluginManager->getInstances() as $type_id => $type) {
+ if ($type->getFallbackType() == 'text') {
+ $fulltext_types[] = [
+ 'value' => $type_id,
+ ];
+ }
+ }
+
+ $boost_values = [
+ '0.0',
+ '0.1',
+ '0.2',
+ '0.3',
+ '0.5',
+ '0.8',
+ '1.0',
+ '2.0',
+ '3.0',
+ '5.0',
+ '8.0',
+ '13.0',
+ '21.0',
+ ];
+ $boosts = array_combine($boost_values, $boost_values);
+
+ $build = [
+ '#type' => 'details',
+ '#open' => TRUE,
+ '#theme' => 'search_api_admin_fields_table',
+ '#parents' => [],
+ '#header' => [
+ $this->t('Label'),
+ $this->t('Machine name'),
+ [
+ 'data' => $this->t('Property path'),
+ 'class' => [RESPONSIVE_PRIORITY_LOW],
+ ],
+ $this->t('Type'),
+ $this->t('Boost'),
+ [
+ 'data' => $this->t('Operations'),
+ 'colspan' => 2,
+ ],
+ ],
+ ];
+
+ foreach ($fields as $key => $field) {
+ $build['fields'][$key]['#access'] = !$field->isHidden();
+
+ $build['fields'][$key]['title'] = [
+ '#type' => 'textfield',
+ '#default_value' => $field->getLabel() ? $field->getLabel() : $key,
+ '#required' => TRUE,
+ '#size' => 40,
+ ];
+ $build['fields'][$key]['id'] = [
+ '#type' => 'textfield',
+ '#default_value' => $key,
+ '#required' => TRUE,
+ '#size' => 35,
+ ];
+ $build['fields'][$key]['property_path'] = [
+ '#markup' => Html::escape($field->getPropertyPath()),
+ ];
+
+ if ($field->getDescription()) {
+ $build['fields'][$key]['description'] = [
+ '#type' => 'value',
+ '#value' => $field->getDescription(),
+ ];
+ }
+
+ $build['fields'][$key]['type'] = [
+ '#type' => 'select',
+ '#options' => $types,
+ '#default_value' => $field->getType(),
+ ];
+ if ($field->isTypeLocked()) {
+ $build['fields'][$key]['type']['#disabled'] = TRUE;
+ }
+
+ $build['fields'][$key]['boost'] = [
+ '#type' => 'select',
+ '#options' => $boosts,
+ '#default_value' => sprintf('%.1f', $field->getBoost()),
+ '#states' => [
+ 'visible' => [
+ ':input[name="fields[' . $key . '][type]"]' => $fulltext_types,
+ ],
+ ],
+ ];
+
+ $route_parameters = [
+ 'search_api_index' => $this->entity->id(),
+ 'field_id' => $key,
+ ];
+ // Provide some invisible markup as default, if a link is missing, so we
+ // don't break the table structure. (theme_search_api_admin_fields_table()
+ // does not add empty cells.)
+ $build['fields'][$key]['edit']['#markup'] = '';
+ try {
+ if ($field->getDataDefinition() instanceof ConfigurablePropertyInterface) {
+ $build['fields'][$key]['edit'] = [
+ '#type' => 'link',
+ '#title' => $this->t('Edit'),
+ '#url' => Url::fromRoute('entity.search_api_index.field_config', $route_parameters),
+ '#attributes' => [
+ 'class' => [
+ 'use-ajax',
+ ],
+ 'data-dialog-type' => 'modal',
+ 'data-dialog-options' => Json::encode([
+ 'width' => 700,
+ ]),
+ ],
+ ];
+ }
+ }
+ catch (SearchApiException $e) {
+ // Could not retrieve data definition. Since this almost certainly means
+ // that the property isn't configurable, we can just ignore it here.
+ }
+ $build['fields'][$key]['remove']['#markup'] = '';
+ if (!$field->isIndexedLocked()) {
+ $build['fields'][$key]['remove'] = [
+ '#type' => 'link',
+ '#title' => $this->t('Remove'),
+ '#url' => Url::fromRoute('entity.search_api_index.remove_field', $route_parameters),
+ '#attributes' => [
+ 'class' => ['use-ajax'],
+ ],
+ ];
+ }
+ }
+
+ return $build;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function actions(array $form, FormStateInterface $form_state) {
+ $actions = [
+ 'submit' => [
+ '#type' => 'submit',
+ '#value' => $this->t('Save changes'),
+ '#button_type' => 'primary',
+ '#submit' => ['::submitForm', '::save'],
+ ],
+ ];
+ if ($this->entity instanceof UnsavedConfigurationInterface && $this->entity->hasChanges()) {
+ $actions['cancel'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Cancel'),
+ '#button_type' => 'danger',
+ '#submit' => ['::cancel'],
+ ];
+ }
+ return $actions;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validateForm(array &$form, FormStateInterface $form_state) {
+ $field_values = $form_state->getValues()['fields'];
+ $new_ids = [];
+
+ foreach ($field_values as $field_id => $field) {
+ $new_id = $field['id'];
+ $new_ids[$new_id][] = $field_id;
+
+ // Check for reserved and other illegal field IDs.
+ if ($this->fieldsHelper->isFieldIdReserved($new_id)) {
+ $args = [
+ '%field_id' => $new_id,
+ ];
+ $error = $this->t('%field_id is a reserved value and cannot be used as the machine name of a normal field.', $args);
+ $form_state->setErrorByName('fields][' . $field_id . '][id', $error);
+ }
+ elseif (preg_match('/^_+$/', $new_id)) {
+ $error = $this->t('Field IDs have to contain non-underscore characters.');
+ $form_state->setErrorByName('fields][' . $field_id . '][id', $error);
+ }
+ elseif (preg_match('/[^a-z0-9_]/', $new_id)) {
+ $error = $this->t('Field IDs must contain only lowercase letters, numbers and underscores.');
+ $form_state->setErrorByName('fields][' . $field_id . '][id', $error);
+ }
+ }
+
+ // Identify duplicates.
+ $has_duplicates = function (array $old_ids) {
+ return count($old_ids) > 1;
+ };
+ foreach (array_filter($new_ids, $has_duplicates) as $new_id => $old_ids) {
+ $args['%field_id'] = $new_id;
+ $error = $this->t('Field ID %field_id is used multiple times. Field IDs must be unique.', $args);
+ foreach ($old_ids as $field_id) {
+ $form_state->setErrorByName('fields][' . $field_id . '][id', $error);
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ $index = $this->entity;
+
+ // Store the fields configuration.
+ $fields = $index->getFields();
+ $field_values = $form_state->getValue('fields', []);
+ $new_fields = [];
+ foreach ($field_values as $field_id => $new_settings) {
+ if (!isset($fields[$field_id])) {
+ $args['%field_id'] = $field_id;
+ drupal_set_message($this->t('The field with ID %field_id does not exist anymore.', $args), 'warning');
+ continue;
+ }
+ $field = $fields[$field_id];
+ $field->setLabel($new_settings['title']);
+ $field->setType($new_settings['type']);
+ $field->setBoost($new_settings['boost']);
+ $field->setFieldIdentifier($new_settings['id']);
+
+ $new_fields[$new_settings['id']] = $field;
+ }
+
+ $index->setFields($new_fields);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function save(array $form, FormStateInterface $form_state) {
+ $index = $this->entity;
+ if ($index instanceof UnsavedConfigurationInterface) {
+ $index->savePermanent();
+ }
+ else {
+ $index->save();
+ }
+
+ drupal_set_message($this->t('The changes were successfully saved.'));
+ if ($this->entity->isReindexing()) {
+ drupal_set_message($this->t('All content was scheduled for reindexing so the new settings can take effect.'));
+ }
+
+ return SAVED_UPDATED;
+ }
+
+ /**
+ * Cancels the editing of the index's fields.
+ *
+ * @param array $form
+ * An associative array containing the structure of the form.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current state of the form.
+ */
+ public function cancel(array &$form, FormStateInterface $form_state) {
+ if ($this->entity instanceof UnsavedConfigurationInterface && $this->entity->hasChanges()) {
+ $this->entity->discardChanges();
+ }
+
+ $form_state->setRedirectUrl($this->entity->toUrl('canonical'));
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Form/IndexForm.php b/sites/all/modules/contrib/search/search_api/src/Form/IndexForm.php
new file mode 100644
index 000000000..cb88aba2b
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Form/IndexForm.php
@@ -0,0 +1,622 @@
+entityTypeManager = $entity_type_manager;
+ $this->pluginHelper = $plugin_helper;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ $entity_type_manager = $container->get('entity_type.manager');
+ $plugin_helper = $container->get('search_api.plugin_helper');
+ return new static($entity_type_manager, $plugin_helper);
+ }
+
+ /**
+ * Retrieves all available servers as an options list.
+ *
+ * @return string[]
+ * An associative array mapping server IDs to their labels.
+ */
+ protected function getServerOptions() {
+ $options = [];
+ /** @var \Drupal\search_api\ServerInterface[] $servers */
+ $servers = $this->entityTypeManager
+ ->getStorage('search_api_server')
+ ->loadMultiple();
+ foreach ($servers as $server_id => $server) {
+ // @todo Special formatting for disabled servers.
+ $options[$server_id] = Html::escape($server->label());
+ }
+ return $options;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function form(array $form, FormStateInterface $form_state) {
+ // If the form is being rebuilt, rebuild the entity with the current form
+ // values.
+ if ($form_state->isRebuilding()) {
+ // When the form is being built for an AJAX response the ID is not present
+ // in $form_state. To ensure our entity is always valid, we're adding the
+ // ID back.
+ if (!$this->entity->isNew()) {
+ $form_state->setValue('id', $this->entity->id());
+ }
+ $this->entity = $this->buildEntity($form, $form_state);
+ }
+
+ $form = parent::form($form, $form_state);
+
+ /** @var \Drupal\search_api\IndexInterface $index */
+ $index = $this->getEntity();
+ if ($index->isNew()) {
+ $form['#title'] = $this->t('Add search index');
+ }
+ else {
+ $arguments = ['%label' => $index->label()];
+ $form['#title'] = $this->t('Edit search index %label', $arguments);
+ }
+
+ $this->buildEntityForm($form, $form_state, $index);
+
+ return $form;
+ }
+
+ /**
+ * Builds the form for the basic index properties.
+ *
+ * @param array $form
+ * The current form array.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current form state.
+ * @param \Drupal\search_api\IndexInterface $index
+ * The index that is being created or edited.
+ */
+ public function buildEntityForm(array &$form, FormStateInterface $form_state, IndexInterface $index) {
+ $form['#tree'] = TRUE;
+ $form['name'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Index name'),
+ '#description' => $this->t('Enter the displayed name for the index.'),
+ '#default_value' => $index->label(),
+ '#required' => TRUE,
+ ];
+ $form['id'] = [
+ '#type' => 'machine_name',
+ '#default_value' => $index->isNew() ? NULL : $index->id(),
+ '#maxlength' => 50,
+ '#required' => TRUE,
+ '#machine_name' => [
+ 'exists' => '\Drupal\search_api\Entity\Index::load',
+ 'source' => ['name'],
+ ],
+ '#disabled' => !$index->isNew(),
+ ];
+
+ $form['#attached']['library'][] = 'search_api/drupal.search_api.admin_css';
+
+ $form['datasources'] = [
+ '#type' => 'checkboxes',
+ '#title' => $this->t('Data sources'),
+ '#description' => $this->t('Select one or more data sources of items that will be stored in this index.'),
+ '#default_value' => $index->getDatasourceIds(),
+ '#multiple' => TRUE,
+ '#required' => TRUE,
+ '#attributes' => ['class' => ['search-api-checkboxes-list']],
+ '#ajax' => [
+ 'trigger_as' => ['name' => 'datasources_configure'],
+ 'callback' => '::buildAjaxDatasourceConfigForm',
+ 'wrapper' => 'search-api-datasources-config-form',
+ 'method' => 'replace',
+ 'effect' => 'fade',
+ ],
+ ];
+ $datasource_options = [];
+ foreach ($this->pluginHelper->createDatasourcePlugins($index) as $datasource_id => $datasource) {
+ $datasource_options[$datasource_id] = $datasource->label();
+ $form['datasources'][$datasource_id]['#description'] = $datasource->getDescription();
+ }
+ asort($datasource_options, SORT_NATURAL | SORT_FLAG_CASE);
+ $form['datasources']['#options'] = $datasource_options;
+
+ $form['datasource_configs'] = [
+ '#type' => 'container',
+ '#attributes' => [
+ 'id' => 'search-api-datasources-config-form',
+ ],
+ '#tree' => TRUE,
+ ];
+
+ $form['datasource_configure_button'] = [
+ '#type' => 'submit',
+ '#name' => 'datasources_configure',
+ '#value' => $this->t('Configure'),
+ '#limit_validation_errors' => [['datasources']],
+ '#submit' => ['::submitAjaxDatasourceConfigForm'],
+ '#ajax' => [
+ 'callback' => '::buildAjaxDatasourceConfigForm',
+ 'wrapper' => 'search-api-datasources-config-form',
+ ],
+ '#attributes' => ['class' => ['js-hide']],
+ ];
+
+ $this->buildDatasourcesConfigForm($form, $form_state, $index);
+
+ $form['tracker'] = [
+ '#type' => 'radios',
+ '#title' => $this->t('Tracker'),
+ '#description' => $this->t('Select the type of tracker which should be used for keeping track of item changes.'),
+ '#default_value' => $index->getTrackerId(),
+ '#required' => TRUE,
+ '#ajax' => [
+ 'trigger_as' => ['name' => 'tracker_configure'],
+ 'callback' => '::buildAjaxTrackerConfigForm',
+ 'wrapper' => 'search-api-tracker-config-form',
+ 'method' => 'replace',
+ 'effect' => 'fade',
+ ],
+ ];
+ $tracker_options = [];
+ foreach ($this->pluginHelper->createTrackerPlugins($index) as $tracker_id => $tracker) {
+ $tracker_options[$tracker_id] = $tracker->label();
+ $form['tracker'][$tracker_id]['#description'] = $tracker->getDescription();
+ }
+ asort($tracker_options, SORT_NATURAL | SORT_FLAG_CASE);
+ $form['tracker']['#options'] = $tracker_options;
+ $form['tracker']['#access'] = !$index->hasValidTracker() || count($tracker_options) > 1;
+
+ $form['tracker_config'] = [
+ '#type' => 'container',
+ '#attributes' => [
+ 'id' => 'search-api-tracker-config-form',
+ ],
+ '#tree' => TRUE,
+ ];
+
+ $form['tracker_configure_button'] = [
+ '#type' => 'submit',
+ '#name' => 'tracker_configure',
+ '#value' => $this->t('Configure'),
+ '#limit_validation_errors' => [['tracker']],
+ '#submit' => ['::submitAjaxTrackerConfigForm'],
+ '#ajax' => [
+ 'callback' => '::buildAjaxTrackerConfigForm',
+ 'wrapper' => 'search-api-tracker-config-form',
+ ],
+ '#attributes' => ['class' => ['js-hide']],
+ '#access' => count($tracker_options) > 1,
+ ];
+
+ $this->buildTrackerConfigForm($form, $form_state, $index);
+
+ $form['server'] = [
+ '#type' => 'radios',
+ '#title' => $this->t('Server'),
+ '#description' => $this->t('Select the server this index should use. Indexes cannot be enabled without a connection to a valid, enabled server.'),
+ '#options' => ['' => '' . $this->t('- No server -') . ''] + $this->getServerOptions(),
+ '#default_value' => $index->hasValidServer() ? $index->getServerId() : '',
+ ];
+
+ $form['status'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Enabled'),
+ '#description' => $this->t('Only enabled indexes can be used for indexing and searching. This setting will only take effect if the selected server is also enabled.'),
+ '#default_value' => $index->status(),
+ // Can't enable an index lying on a disabled server or no server at all.
+ '#disabled' => !$index->status() && (!$index->hasValidServer() || !$index->getServerInstance()->status()),
+ // @todo This doesn't seem to work and should also hide for disabled
+ // servers. If that works, we can probably remove the last sentence of
+ // the description.
+ '#states' => [
+ 'invisible' => [
+ ':input[name="server"]' => ['value' => ''],
+ ],
+ ],
+ ];
+
+ $form['description'] = [
+ '#type' => 'textarea',
+ '#title' => $this->t('Description'),
+ '#description' => $this->t('Enter a description for the index.'),
+ '#default_value' => $index->getDescription(),
+ ];
+
+ $form['options'] = [
+ '#tree' => TRUE,
+ '#type' => 'details',
+ '#title' => $this->t('Index options'),
+ '#collapsed' => TRUE,
+ ];
+
+ // We display the "read-only" flag along with the other options, even though
+ // it is a property directly on the index object. We use "#parents" to move
+ // it to the correct place in the form values.
+ $form['options']['read_only'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Read only'),
+ '#description' => $this->t('Do not write to this index or track the status of items in this index.'),
+ '#default_value' => $index->isReadOnly(),
+ '#parents' => ['read_only'],
+ ];
+ $form['options']['index_directly'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Index items immediately'),
+ '#description' => $this->t('Immediately index new or updated items instead of waiting for the next cron run. This might have serious performance drawbacks and is generally not advised for larger sites.'),
+ '#default_value' => $index->getOption('index_directly'),
+ ];
+ $form['options']['cron_limit'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Cron batch size'),
+ '#description' => $this->t('Set how many items will be indexed at once when indexing items during a cron run. "0" means that no items will be indexed by cron for this index, "-1" means that cron should index all items at once.'),
+ '#default_value' => $index->getOption('cron_limit'),
+ '#size' => 4,
+ ];
+ }
+
+ /**
+ * Builds the configuration forms for all selected datasources.
+ *
+ * @param array $form
+ * The current form array.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current form state.
+ * @param \Drupal\search_api\IndexInterface $index
+ * The index being created or edited.
+ */
+ public function buildDatasourcesConfigForm(array &$form, FormStateInterface $form_state, IndexInterface $index) {
+ $selected_datasources = $form_state->getValue('datasources');
+ if ($selected_datasources === NULL) {
+ // Initial form build, use the saved datasources (or none for new
+ // indexes).
+ $datasources = $index->getDatasources();
+ }
+ else {
+ // The form is being rebuilt – use the datasources selected by the user
+ // instead of the ones saved in the config.
+ $datasources = $this->pluginHelper->createDatasourcePlugins($index, $selected_datasources);
+ }
+ $form_state->set('datasources', array_keys($datasources));
+
+ $show_message = FALSE;
+ foreach ($datasources as $datasource_id => $datasource) {
+ if ($datasource instanceof PluginFormInterface) {
+ // Get the "sub-form state" and appropriate form part to send to
+ // buildConfigurationForm().
+ $datasource_form = [];
+ if (!empty($form['datasource_configs'][$datasource_id])) {
+ $datasource_form = $form['datasource_configs'][$datasource_id];
+ }
+ $datasource_form_state = SubformState::createForSubform($datasource_form, $form, $form_state);
+ $form['datasource_configs'][$datasource_id] = $datasource->buildConfigurationForm($datasource_form, $datasource_form_state);
+
+ $show_message = TRUE;
+ $form['datasource_configs'][$datasource_id]['#type'] = 'details';
+ $form['datasource_configs'][$datasource_id]['#title'] = $this->t('Configure the %datasource datasource', ['%datasource' => $datasource->label()]);
+ $form['datasource_configs'][$datasource_id]['#open'] = $index->isNew();
+ }
+ }
+
+ // If the user changed the datasources and there is at least one datasource
+ // config form, show a message telling the user to configure it.
+ if ($selected_datasources && $show_message) {
+ drupal_set_message($this->t('Please configure the used datasources.'), 'warning');
+ }
+ }
+
+ /**
+ * Builds the tracker configuration form.
+ *
+ * @param array $form
+ * The current form array.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current form state.
+ * @param \Drupal\search_api\IndexInterface $index
+ * The index being created or edited.
+ */
+ public function buildTrackerConfigForm(array &$form, FormStateInterface $form_state, IndexInterface $index) {
+ $selected_tracker = $form_state->getValue('tracker');
+ if ($selected_tracker === NULL || $selected_tracker == $index->getTrackerId()) {
+ // Initial form build, use the saved tracker (or none for new indexes).
+ if ($index->hasValidTracker()) {
+ $tracker = $index->getTrackerInstance();
+ }
+ // Only notify the user of a missing tracker plugin if we're editing an
+ // existing index.
+ elseif (!$index->isNew()) {
+ drupal_set_message($this->t('The tracker plugin is missing or invalid.'), 'error');
+ }
+ }
+ else {
+ // Probably an AJAX rebuild of the form – use the tracker selected by
+ // the user.
+ $tracker = $this->pluginHelper->createTrackerPlugin($index, $selected_tracker);
+ }
+
+ if (empty($tracker)) {
+ return;
+ }
+
+ $form_state->set('tracker', $tracker->getPluginId());
+
+ if ($tracker instanceof PluginFormInterface) {
+ // Get the "sub-form state" and appropriate form part to send to
+ // buildConfigurationForm().
+ $tracker_form = !empty($form['tracker_config']) ? $form['tracker_config'] : [];
+ $tracker_form_state = SubformState::createForSubform($tracker_form, $form, $form_state);
+ $form['tracker_config'] = $tracker->buildConfigurationForm($tracker_form, $tracker_form_state);
+
+ $form['tracker_config']['#type'] = 'details';
+ $form['tracker_config']['#title'] = $this->t('Configure the %plugin tracker', ['%plugin' => $tracker->label()]);
+ $form['tracker_config']['#description'] = Html::escape($tracker->getDescription());
+ $form['tracker_config']['#open'] = $index->isNew();
+
+ // If the user changed the tracker and the new one has a config form, show
+ // a message telling the user to configure it.
+ if ($selected_tracker && $selected_tracker != $tracker->getPluginId()) {
+ drupal_set_message($this->t('Please configure the used tracker.'), 'warning');
+ }
+ }
+ }
+
+ /**
+ * Form submission handler for buildEntityForm().
+ *
+ * Takes care of changes in the selected datasources.
+ *
+ * @param array $form
+ * The current form array.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current form state.
+ */
+ public function submitAjaxDatasourceConfigForm(array $form, FormStateInterface $form_state) {
+ $form_state->setValue('id', NULL);
+ $form_state->setRebuild();
+ }
+
+ /**
+ * Handles changes to the selected datasources.
+ *
+ * @param array $form
+ * The current form array.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current form state.
+ *
+ * @return array
+ * The part of the form to return as AJAX.
+ */
+ public function buildAjaxDatasourceConfigForm(array $form, FormStateInterface $form_state) {
+ return $form['datasource_configs'];
+ }
+
+ /**
+ * Form submission handler for buildEntityForm().
+ *
+ * Takes care of changes in the selected tracker plugin.
+ *
+ * @param array $form
+ * The current form array.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current form state.
+ */
+ public function submitAjaxTrackerConfigForm(array $form, FormStateInterface $form_state) {
+ $form_state->setValue('id', NULL);
+ $form_state->setRebuild();
+ }
+
+ /**
+ * Handles switching the selected tracker plugin.
+ *
+ * @param array $form
+ * The current form array.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current form state.
+ *
+ * @return array
+ * The part of the form to return as AJAX.
+ */
+ public function buildAjaxTrackerConfigForm(array $form, FormStateInterface $form_state) {
+ return $form['tracker_config'];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validateForm(array &$form, FormStateInterface $form_state) {
+ parent::validateForm($form, $form_state);
+
+ /** @var \Drupal\search_api\IndexInterface $index */
+ $index = $this->getEntity();
+
+ $storage = $this->entityTypeManager->getStorage('search_api_index');
+ if (!$index->isNew()) {
+ $this->originalEntity = $storage->loadUnchanged($index->id());
+ }
+ if (empty($this->originalEntity)) {
+ $this->originalEntity = $storage->create(['status' => FALSE]);
+ }
+
+ // Store the array of datasource plugin IDs with integer keys.
+ $datasource_ids = array_values(array_filter($form_state->getValue('datasources', [])));
+ $form_state->setValue('datasources', $datasource_ids);
+
+ // Call validateConfigurationForm() for each enabled datasource with a form.
+ $datasources = $this->pluginHelper->createDatasourcePlugins($index, $datasource_ids);
+ $previous_datasources = $form_state->get('datasources');
+ foreach ($datasources as $datasource_id => $datasource) {
+ if ($datasource instanceof PluginFormInterface) {
+ if (!in_array($datasource_id, $previous_datasources)) {
+ $form_state->setRebuild();
+ continue;
+ }
+ $datasource_form = &$form['datasource_configs'][$datasource_id];
+ $datasource_form_state = SubformState::createForSubform($datasource_form, $form, $form_state);
+ $datasource->validateConfigurationForm($datasource_form, $datasource_form_state);
+ }
+ }
+
+ // Call validateConfigurationForm() for the (possibly new) tracker, if it
+ // has not changed and if it has a form.
+ $tracker_id = $form_state->getValue('tracker', NULL);
+ if ($tracker_id == $form_state->get('tracker')) {
+ $tracker = $this->pluginHelper->createTrackerPlugin($index, $tracker_id);
+ if ($tracker instanceof PluginFormInterface) {
+ $tracker_form_state = SubformState::createForSubform($form['tracker_config'], $form, $form_state);
+ $tracker->validateConfigurationForm($form['tracker_config'], $tracker_form_state);
+ }
+ }
+ else {
+ $tracker = $this->pluginHelper->createTrackerPlugin($this->originalEntity, $tracker_id);
+ if ($tracker instanceof PluginFormInterface) {
+ $form_state->setRebuild();
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function actions(array $form, FormStateInterface $form_state) {
+ $actions = parent::actions($form, $form_state);
+
+ if ($this->getEntity()->isNew()) {
+ $actions['save_edit'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Save and add fields'),
+ '#submit' => $actions['submit']['#submit'],
+ '#button_type' => 'primary',
+ // Work around for submit callbacks after save() not being called due to
+ // batch operations.
+ '#redirect_to_url' => 'add-fields',
+ ];
+ }
+
+ return $actions;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ parent::submitForm($form, $form_state);
+
+ /** @var \Drupal\search_api\IndexInterface $index */
+ $index = $this->getEntity();
+ $index->setOptions($form_state->getValue('options', []) + $this->originalEntity->getOptions());
+
+ $datasource_ids = $form_state->getValue('datasources', []);
+ $datasources = $this->pluginHelper->createDatasourcePlugins($index, $datasource_ids);
+ foreach ($datasources as $datasource_id => $datasource) {
+ if ($datasource instanceof PluginFormInterface) {
+ $datasource_form_state = SubformState::createForSubform($form['datasource_configs'][$datasource_id], $form, $form_state);
+ $datasource->submitConfigurationForm($form['datasource_configs'][$datasource_id], $datasource_form_state);
+ }
+ }
+ $index->setDatasources($datasources);
+
+ // Call submitConfigurationForm() for the (possibly new) tracker, if it
+ // has not changed and if it has a form.
+ $tracker_id = $form_state->getValue('tracker', NULL);
+ $tracker = $this->pluginHelper->createTrackerPlugin($index, $tracker_id);
+ if ($tracker_id == $form_state->get('tracker')) {
+ if ($tracker instanceof PluginFormInterface) {
+ $tracker_form_state = SubformState::createForSubform($form['tracker_config'], $form, $form_state);
+ $tracker->submitConfigurationForm($form['tracker_config'], $tracker_form_state);
+ }
+ }
+ /** @var \Drupal\search_api\Tracker\TrackerInterface $tracker */
+ $index->setTracker($tracker);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function save(array $form, FormStateInterface $form_state) {
+ // @todo Redirect to a confirm form if changing server or tracker, since
+ // that isn't such a light operation (equaling a "clear", basically).
+ // Only save the index if the form doesn't need to be rebuilt.
+ if (!$form_state->isRebuilding()) {
+ try {
+ /** @var \Drupal\search_api\IndexInterface $index */
+ $index = $this->getEntity();
+ $index->save();
+ drupal_set_message($this->t('The index was successfully saved.'));
+ $button = $form_state->getTriggeringElement();
+ if (!empty($button['#redirect_to_url'])) {
+ $form_state->setRedirectUrl($index->toUrl($button['#redirect_to_url']));
+ }
+ else {
+ $form_state->setRedirect('entity.search_api_index.canonical', ['search_api_index' => $index->id()]);
+ }
+ }
+ catch (SearchApiException $e) {
+ $form_state->setRebuild();
+
+ $message = '%type: @message in %function (line %line of %file).';
+ $variables = Error::decodeException($e);
+ $this->getLogger('search_api')->error($message, $variables);
+
+ drupal_set_message($this->t('The index could not be saved.'), 'error');
+ }
+ }
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Form/IndexProcessorsForm.php b/sites/all/modules/contrib/search/search_api/src/Form/IndexProcessorsForm.php
new file mode 100644
index 000000000..d2fec7a80
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Form/IndexProcessorsForm.php
@@ -0,0 +1,379 @@
+entityTypeManager = $entity_type_manager;
+ $this->processorPluginManager = $processor_plugin_manager;
+ $this->logger = $logger;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ $entity_type_manager = $container->get('entity_type.manager');
+ $processor_plugin_manager = $container->get('plugin.manager.search_api.processor');
+ $logger = $container->get('logger.channel.search_api');
+
+ return new static($entity_type_manager, $processor_plugin_manager, $logger);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getBaseFormId() {
+ return NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function form(array $form, FormStateInterface $form_state) {
+ $form['#attached']['library'][] = 'search_api/drupal.search_api.admin_css';
+
+ // Retrieve lists of all processors, and the stages and weights they have.
+ if (!$form_state->has('processors')) {
+ $all_processors = $this->getAllProcessors();
+ $sort_processors = function (ProcessorInterface $a, ProcessorInterface $b) {
+ return strnatcasecmp($a->label(), $b->label());
+ };
+ uasort($all_processors, $sort_processors);
+ $form_state->set('processors', $all_processors);
+ }
+ else {
+ $all_processors = $form_state->get('processors');
+ }
+
+ $stages = $this->processorPluginManager->getProcessingStages();
+ /** @var \Drupal\search_api\Processor\ProcessorInterface[][] $processors_by_stage */
+ $processors_by_stage = [];
+ foreach ($all_processors as $processor_id => $processor) {
+ foreach ($stages as $stage => $definition) {
+ if ($processor->supportsStage($stage)) {
+ $processors_by_stage[$stage][$processor_id] = $processor;
+ }
+ }
+ }
+
+ $enabled_processors = $this->entity->getProcessors();
+
+ $backend_discouraged_processors = [];
+ if ($this->entity->getServerInstance()) {
+ $backend_discouraged_processors = $this->entity->getServerInstance()
+ ->getDiscouragedProcessors();
+
+ foreach ($backend_discouraged_processors as $processor_id) {
+ if (!isset($enabled_processors[$processor_id])) {
+ // Remove processors from the overview.
+ unset($all_processors[$processor_id]);
+
+ // Remove processors from the stages.
+ foreach ($processors_by_stage as $stage => $processors) {
+ unset($processors_by_stage[$stage][$processor_id]);
+ }
+ }
+ }
+ }
+
+ $form['#tree'] = TRUE;
+ $form['#attached']['library'][] = 'search_api/drupal.search_api.processors';
+ $form['#title'] = $this->t('Manage processors for search index %label', ['%label' => $this->entity->label()]);
+ $form['description']['#markup'] = '' . $this->t('Configure processors which will pre- and post-process data at index and search time.') . '
';
+
+ // Add the list of processors with checkboxes to enable/disable them.
+ $form['status'] = [
+ '#type' => 'fieldset',
+ '#title' => $this->t('Enabled'),
+ '#attributes' => [
+ 'class' => [
+ 'search-api-status-wrapper',
+ ],
+ ],
+ ];
+ foreach ($all_processors as $processor_id => $processor) {
+ $clean_css_id = Html::cleanCssIdentifier($processor_id);
+ $form['status'][$processor_id] = [
+ '#type' => 'checkbox',
+ '#title' => $processor->label(),
+ '#default_value' => $processor->isLocked() || !empty($enabled_processors[$processor_id]),
+ '#description' => $processor->getDescription(),
+ '#attributes' => [
+ 'class' => [
+ 'search-api-processor-status-' . $clean_css_id,
+ ],
+ 'data-id' => $clean_css_id,
+ ],
+ '#disabled' => $processor->isLocked(),
+ '#access' => !$processor->isHidden(),
+ ];
+ if (in_array($processor_id, $backend_discouraged_processors)) {
+ $form['status'][$processor_id]['#description'] .= '
' . $this->t('It is recommended not to use this processor with the selected server.') . '';
+ }
+ }
+
+ $form['weights'] = [
+ '#type' => 'fieldset',
+ '#title' => $this->t('Processor order'),
+ ];
+ // Order enabled processors per stage.
+ foreach ($stages as $stage => $description) {
+ $form['weights'][$stage] = [
+ '#type' => 'fieldset',
+ '#title' => $description['label'],
+ '#attributes' => [
+ 'class' => [
+ 'search-api-stage-wrapper',
+ 'search-api-stage-wrapper-' . Html::cleanCssIdentifier($stage),
+ ],
+ ],
+ ];
+ $form['weights'][$stage]['order'] = [
+ '#type' => 'table',
+ ];
+ $form['weights'][$stage]['order']['#tabledrag'][] = [
+ 'action' => 'order',
+ 'relationship' => 'sibling',
+ 'group' => 'search-api-processor-weight-' . Html::cleanCssIdentifier($stage),
+ ];
+ }
+ foreach ($processors_by_stage as $stage => $processors) {
+ // Sort the processors by weight for this stage.
+ $processor_weights = [];
+ foreach ($processors as $processor_id => $processor) {
+ $processor_weights[$processor_id] = $processor->getWeight($stage);
+ }
+ asort($processor_weights);
+
+ foreach ($processor_weights as $processor_id => $weight) {
+ $processor = $processors[$processor_id];
+ if ($processor->isHidden()) {
+ $form['processors'][$processor_id]['weights'][$stage] = [
+ '#type' => 'value',
+ '#value' => $weight,
+ ];
+ continue;
+ }
+ $form['weights'][$stage]['order'][$processor_id]['#attributes']['class'][] = 'draggable';
+ $form['weights'][$stage]['order'][$processor_id]['#attributes']['class'][] = 'search-api-processor-weight--' . Html::cleanCssIdentifier($processor_id);
+ $form['weights'][$stage]['order'][$processor_id]['#weight'] = $weight;
+ $form['weights'][$stage]['order'][$processor_id]['label']['#plain_text'] = $processor->label();
+ $form['weights'][$stage]['order'][$processor_id]['weight'] = [
+ '#type' => 'weight',
+ '#title' => $this->t('Weight for processor %title', ['%title' => $processor->label()]),
+ '#title_display' => 'invisible',
+ '#delta' => 50,
+ '#default_value' => $weight,
+ '#parents' => ['processors', $processor_id, 'weights', $stage],
+ '#attributes' => [
+ 'class' => [
+ 'search-api-processor-weight-' . Html::cleanCssIdentifier($stage),
+ ],
+ ],
+ ];
+ }
+ }
+
+ // Add vertical tabs containing the settings for the processors. Tabs for
+ // disabled processors are hidden with JS magic, but need to be included in
+ // case the processor is enabled.
+ $form['processor_settings'] = [
+ '#title' => $this->t('Processor settings'),
+ '#type' => 'vertical_tabs',
+ ];
+
+ foreach ($all_processors as $processor_id => $processor) {
+ if ($processor instanceof PluginFormInterface) {
+ $form['settings'][$processor_id] = [
+ '#type' => 'details',
+ '#title' => $processor->label(),
+ '#group' => 'processor_settings',
+ '#parents' => ['processors', $processor_id, 'settings'],
+ '#attributes' => [
+ 'class' => [
+ 'search-api-processor-settings-' . Html::cleanCssIdentifier($processor_id),
+ ],
+ ],
+ ];
+ $processor_form_state = SubformState::createForSubform($form['settings'][$processor_id], $form, $form_state);
+ $form['settings'][$processor_id] += $processor->buildConfigurationForm($form['settings'][$processor_id], $processor_form_state);
+ }
+ else {
+ unset($form['settings'][$processor_id]);
+ }
+ }
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validateForm(array &$form, FormStateInterface $form_state) {
+ parent::validateForm($form, $form_state);
+
+ $values = $form_state->getValues();
+ $processors = $this->getAllProcessors();
+
+ // Iterate over all processors that have a form and are enabled.
+ foreach (array_keys(array_filter($values['status'])) as $processor_id) {
+ $processor = $processors[$processor_id];
+ if ($processor instanceof PluginFormInterface) {
+ $processor_form_state = SubformState::createForSubform($form['settings'][$processor_id], $form, $form_state);
+ $processor->validateConfigurationForm($form['settings'][$processor_id], $processor_form_state);
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ $values = $form_state->getValues();
+ $old_processors = $this->entity->getProcessors();
+
+ // Store processor settings.
+ $processors = $this->getAllProcessors();
+ foreach ($processors as $processor_id => $processor) {
+ if (empty($values['status'][$processor_id])) {
+ if (isset($old_processors[$processor_id])) {
+ $this->entity->removeProcessor($processor_id);
+ $form_state->set('processors_changed', TRUE);
+ }
+ continue;
+ }
+ $old_configuration = $processor->getConfiguration();
+ if ($processor instanceof PluginFormInterface) {
+ $processor_form_state = SubformState::createForSubform($form['settings'][$processor_id], $form, $form_state);
+ $processor->submitConfigurationForm($form['settings'][$processor_id], $processor_form_state);
+ }
+ if (!empty($values['processors'][$processor_id]['weights'])) {
+ foreach ($values['processors'][$processor_id]['weights'] as $stage => $weight) {
+ $processor->setWeight($stage, (int) $weight);
+ }
+ }
+ if (!isset($old_processors[$processor_id])) {
+ $this->entity->addProcessor($processor);
+ $form_state->set('processors_changed', TRUE);
+ }
+ elseif ($old_configuration != $processor->getConfiguration()) {
+ $form_state->set('processors_changed', TRUE);
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function save(array $form, FormStateInterface $form_state) {
+ if ($form_state->get('processors_changed')) {
+ $save_status = parent::save($form, $form_state);
+ drupal_set_message($this->t('The indexing workflow was successfully edited.'));
+ if ($this->entity->isReindexing()) {
+ drupal_set_message($this->t('All content was scheduled for reindexing so the new settings can take effect.'));
+ }
+ }
+ else {
+ drupal_set_message($this->t('No values were changed.'));
+ $save_status = SAVED_UPDATED;
+ }
+
+ return $save_status;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function actions(array $form, FormStateInterface $form_state) {
+ $actions = parent::actions($form, $form_state);
+
+ // We don't have a "delete" action here.
+ unset($actions['delete']);
+
+ return $actions;
+ }
+
+ /**
+ * Retrieves all available processors.
+ */
+ protected function getAllProcessors() {
+ $processors = $this->entity->getProcessors();
+ $settings['#index'] = $this->entity;
+
+ foreach ($this->processorPluginManager->getDefinitions() as $name => $processor_definition) {
+ if (isset($processors[$name])) {
+ continue;
+ }
+ elseif (class_exists($processor_definition['class'])) {
+ if (call_user_func([$processor_definition['class'], 'supportsIndex'], $this->entity)) {
+ /** @var \Drupal\search_api\Processor\ProcessorInterface $processor */
+ $processor = $this->processorPluginManager->createInstance($name, $settings);
+ $processors[$name] = $processor;
+ }
+ }
+ else {
+ $this->logger->warning('Processor %id specifies a non-existing class %class.', [
+ '%id' => $name,
+ '%class' => $processor_definition['class'],
+ ]);
+ }
+ }
+
+ return $processors;
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Form/IndexRebuildTrackerConfirmForm.php b/sites/all/modules/contrib/search/search_api/src/Form/IndexRebuildTrackerConfirmForm.php
new file mode 100644
index 000000000..6a60ec0ab
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Form/IndexRebuildTrackerConfirmForm.php
@@ -0,0 +1,46 @@
+t('Are you sure you want to rebuild the tracking data for the search index %name?', ['%name' => $this->entity->label()]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDescription() {
+ return $this->t("The complete information about existing and indexed items for this index will be deleted and will have to be rebuilt.
This should usually not be necessary, but can help if some existing items aren't contained in the index's tracking data for whatever reason (in other words, when the total number of items to be indexed is less than it should be).
This action cannot be undone.
");
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCancelUrl() {
+ return new Url('entity.search_api_index.canonical', ['search_api_index' => $this->entity->id()]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ /** @var \Drupal\search_api\IndexInterface $index */
+ $index = $this->getEntity();
+ $index->rebuildTracker();
+ drupal_set_message($this->t('The tracking information for search index %name will be rebuilt.', ['%name' => $index->label()]));
+ $form_state->setRedirect('entity.search_api_index.canonical', ['search_api_index' => $index->id()]);
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Form/IndexReindexConfirmForm.php b/sites/all/modules/contrib/search/search_api/src/Form/IndexReindexConfirmForm.php
new file mode 100644
index 000000000..d4003f71b
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Form/IndexReindexConfirmForm.php
@@ -0,0 +1,61 @@
+t('Are you sure you want to reindex the search index %name?', ['%name' => $this->entity->label()]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDescription() {
+ return $this->t('Indexed data will remain on the search server until all items have been reindexed. Searches on this index will continue to yield results. This action cannot be undone.');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCancelUrl() {
+ return new Url('entity.search_api_index.canonical', ['search_api_index' => $this->entity->id()]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ /** @var \Drupal\search_api\IndexInterface $index */
+ $index = $this->getEntity();
+
+ try {
+ $index->reindex();
+ drupal_set_message($this->t('The search index %name was successfully reindexed.', ['%name' => $index->label()]));
+ }
+ catch (SearchApiException $e) {
+ drupal_set_message($this->t('Failed to reindex items for the search index %name.', ['%name' => $index->label()]), 'error');
+ $message = '%type while trying to reindex items on index %name: @message in %function (line %line of %file)';
+ $variables = [
+ '%name' => $index->label(),
+ ];
+ $variables += Error::decodeException($e);
+ $this->getLogger('search_api')->error($message, $variables);
+ }
+
+ $form_state->setRedirect('entity.search_api_index.canonical', ['search_api_index' => $index->id()]);
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Form/IndexStatusForm.php b/sites/all/modules/contrib/search/search_api/src/Form/IndexStatusForm.php
new file mode 100644
index 000000000..f738cfe4d
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Form/IndexStatusForm.php
@@ -0,0 +1,246 @@
+indexTaskManager ?: \Drupal::service('search_api.index_task_manager');
+ }
+
+ /**
+ * Sets the index task manager.
+ *
+ * @param \Drupal\search_api\Task\IndexTaskManagerInterface $index_task_manager
+ * The new index task manager.
+ *
+ * @return $this
+ */
+ public function setIndexTaskManager(IndexTaskManagerInterface $index_task_manager) {
+ $this->indexTaskManager = $index_task_manager;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFormId() {
+ return 'search_api_index_status';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(array $form, FormStateInterface $form_state, IndexInterface $index = NULL) {
+ if (!isset($index)) {
+ return [];
+ }
+
+ $form['#index'] = $index;
+
+ if ($index->status() && $index->hasValidTracker()) {
+ $form['#attached']['library'][] = 'search_api/drupal.search_api.admin_css';
+
+ if (!$this->getIndexTaskManager()->isTrackingComplete($index)) {
+ $form['tracking'] = [
+ '#type' => 'details',
+ '#title' => $this->t('Track items for index'),
+ '#description' => $this->t('Not all items have been tracked for this index. This means the displayed index status is incomplete and not all items will currently be indexed.'),
+ '#open' => TRUE,
+ '#attributes' => [
+ 'class' => ['container-inline'],
+ ],
+ ];
+ $form['tracking']['index_now'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Track now'),
+ '#name' => 'track_now',
+ ];
+ }
+
+ // Add the "Index now" form.
+ $form['index'] = [
+ '#type' => 'details',
+ '#title' => $this->t('Start indexing now'),
+ '#open' => TRUE,
+ '#attributes' => [
+ 'class' => ['container-inline'],
+ ],
+ ];
+ $has_remaining_items = ($index->getTrackerInstance()->getRemainingItemsCount() > 0);
+ $all_value = $this->t('all', [], ['context' => 'items to index']);
+ $limit = [
+ '#type' => 'textfield',
+ '#default_value' => $all_value,
+ '#size' => 4,
+ '#attributes' => [
+ 'class' => ['search-api-limit'],
+ ],
+ '#disabled' => !$has_remaining_items,
+ ];
+ $batch_size = [
+ '#type' => 'textfield',
+ '#default_value' => $index->getOption('cron_limit', $this->config('search_api.settings')->get('default_cron_limit')),
+ '#size' => 4,
+ '#attributes' => [
+ 'class' => ['search-api-batch-size'],
+ ],
+ '#disabled' => !$has_remaining_items,
+ ];
+ // Here it gets complicated. We want to build a sentence from the form
+ // input elements, but to translate that we have to make the two form
+ // elements (for limit and batch size) pseudo-variables in the $this->t()
+ // call.
+ // Since we can't pass them directly, we split the translated sentence
+ // (which still has the two tokens), figure out their order and then put
+ // the pieces together again using the form elements' #prefix and #suffix
+ // properties.
+ $sentence = preg_split('/@(limit|batch_size)/', $this->t('Index @limit items in batches of @batch_size items'), -1, PREG_SPLIT_DELIM_CAPTURE);
+ // Check if the sentence contains the expected amount of parts.
+ if (count($sentence) === 5) {
+ $first = $sentence[1];
+ $form['index'][$first] = ${$first};
+ $form['index'][$first]['#prefix'] = $sentence[0];
+ $form['index'][$first]['#suffix'] = $sentence[2];
+ $second = $sentence[3];
+ $form['index'][$second] = ${$second};
+ $form['index'][$second]['#suffix'] = "{$sentence[4]} ";
+ }
+ else {
+ // Sentence is broken. Use fallback method instead.
+ $limit['#title'] = $this->t('Number of items to index');
+ $form['index']['limit'] = $limit;
+ $batch_size['#title'] = $this->t('Number of items per batch run');
+ $form['index']['batch_size'] = $batch_size;
+ }
+ // Add the value "all" so it can be used by the validation.
+ $form['index']['all'] = [
+ '#type' => 'value',
+ '#value' => $all_value,
+ ];
+ $form['index']['index_now'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Index now'),
+ '#disabled' => !$has_remaining_items,
+ '#name' => 'index_now',
+ ];
+
+ // Add actions for reindexing and for clearing the index.
+ $form['actions']['#type'] = 'actions';
+ $form['actions']['reindex'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Queue all items for reindexing'),
+ '#name' => 'reindex',
+ '#button_type' => 'danger',
+ ];
+ $form['actions']['clear'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Clear all indexed data'),
+ '#name' => 'clear',
+ '#button_type' => 'danger',
+ ];
+ $form['actions']['rebuild_tracker'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Rebuild tracking information'),
+ '#name' => 'rebuild_tracker',
+ '#button_type' => 'danger',
+ ];
+ }
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validateForm(array &$form, FormStateInterface $form_state) {
+ parent::validateForm($form, $form_state);
+
+ // Only the "Index now" action needs any validation.
+ if ($form_state->getTriggeringElement()['#name'] === 'index_now') {
+ $values = $form_state->getValues();
+ // Get the translated "all" value and lowercase it for comparison.
+ $all_value = Unicode::strtolower($values['all']);
+
+ foreach (['limit', 'batch_size'] as $field) {
+ // Trim and lowercase the value so we correctly identify "all" values,
+ // even if not matching exactly.
+ $value = Unicode::strtolower(trim($values[$field]));
+
+ if ($value === $all_value) {
+ $value = -1;
+ }
+ elseif (!$value || !is_numeric($value) || ((int) $value) != $value) {
+ $form_state->setErrorByName($field, $this->t('Enter a non-zero integer. Use "-1" or "@all" for "all items".', ['@all' => $values['all']]));
+ }
+ else {
+ $value = (int) $value;
+ }
+
+ $form_state->setValue($field, $value);
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ /** @var \Drupal\search_api\IndexInterface $index */
+ $index = $form['#index'];
+
+ switch ($form_state->getTriggeringElement()['#name']) {
+ case 'index_now':
+ $values = $form_state->getValues();
+ try {
+ IndexBatchHelper::setStringTranslation($this->getStringTranslation());
+ IndexBatchHelper::create($index, $values['batch_size'], $values['limit']);
+ }
+ catch (SearchApiException $e) {
+ drupal_set_message($this->t('Failed to create a batch, please check the batch size and limit.'), 'warning');
+ }
+ break;
+
+ case 'reindex':
+ $form_state->setRedirect('entity.search_api_index.reindex', ['search_api_index' => $index->id()]);
+ break;
+
+ case 'clear':
+ $form_state->setRedirect('entity.search_api_index.clear', ['search_api_index' => $index->id()]);
+ break;
+
+ case 'rebuild_tracker':
+ $form_state->setRedirect('entity.search_api_index.rebuild_tracker', ['search_api_index' => $index->id()]);
+ break;
+
+ case 'track_now':
+ $this->getIndexTaskManager()->addItemsBatch($index);
+ break;
+ }
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Form/ServerClearConfirmForm.php b/sites/all/modules/contrib/search/search_api/src/Form/ServerClearConfirmForm.php
new file mode 100644
index 000000000..315bd2eb8
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Form/ServerClearConfirmForm.php
@@ -0,0 +1,83 @@
+t('Are you sure you want to clear all indexed data from the search server %name?', ['%name' => $this->entity->label()]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDescription() {
+ return $this->t("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. This action cannot be undone.");
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCancelUrl() {
+ return new Url('entity.search_api_server.canonical', ['search_api_server' => $this->entity->id()]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ /** @var \Drupal\search_api\ServerInterface $server */
+ $server = $this->getEntity();
+
+ try {
+ $server->deleteAllItems();
+ drupal_set_message($this->t('All indexed data was successfully deleted from the server.'));
+ }
+ catch (SearchApiException $e) {
+ drupal_set_message($this->t('Indexed data could not be cleared for some indexes. Check the logs for details.'), 'error');
+ }
+
+ $failed_reindexing = [];
+ $properties = [
+ 'status' => TRUE,
+ 'read_only' => FALSE,
+ ];
+ foreach ($server->getIndexes($properties) as $index) {
+ try {
+ $index->reindex();
+ }
+ catch (SearchApiException $e) {
+ $message = '%type while clearing index %index: @message in %function (line %line of %file).';
+ $variables = [
+ '%index' => $index->label(),
+ ];
+ $variables += Error::decodeException($e);
+ $this->getLogger('search_api')->error($message, $variables);
+
+ $failed_reindexing[] = $index->label();
+ }
+ }
+
+ if ($failed_reindexing) {
+ $args = [
+ '@indexes' => implode(', ', $failed_reindexing),
+ ];
+ drupal_set_message($this->t('Failed to mark the following indexes for reindexing: @indexes. Check the logs for details.', $args), 'warning');
+ }
+
+ $form_state->setRedirect('entity.search_api_server.canonical', ['search_api_server' => $server->id()]);
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Form/ServerDeleteConfirmForm.php b/sites/all/modules/contrib/search/search_api/src/Form/ServerDeleteConfirmForm.php
new file mode 100644
index 000000000..bdd8a8ae6
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Form/ServerDeleteConfirmForm.php
@@ -0,0 +1,51 @@
+t('Are you sure you want to delete the search server %name?', ['%name' => $this->entity->label()]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDescription() {
+ return $this->t('Deleting a server will disable all its indexes and their searches.');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCancelUrl() {
+ return new Url('entity.search_api_server.canonical', ['search_api_server' => $this->entity->id()]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getConfirmText() {
+ return $this->t('Delete');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ $this->entity->delete();
+ drupal_set_message($this->t('The search server %name has been deleted.', ['%name' => $this->entity->label()]));
+ $form_state->setRedirect('search_api.overview');
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Form/ServerDisableConfirmForm.php b/sites/all/modules/contrib/search/search_api/src/Form/ServerDisableConfirmForm.php
new file mode 100644
index 000000000..03df68d3b
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Form/ServerDisableConfirmForm.php
@@ -0,0 +1,54 @@
+t('Are you sure you want to disable the search server %name?', ['%name' => $this->entity->label()]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDescription() {
+ return $this->t('Disabling a server will also disable all attached indexes, clearing their tracking tables and indexed data. When re-enabling the server and its indexes, all data will have to be re-indexed. This action cannot be undone.');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCancelUrl() {
+ return new Url('entity.search_api_server.canonical', ['search_api_server' => $this->entity->id()]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getConfirmText() {
+ return $this->t('Disable');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ /** @var \Drupal\search_api\ServerInterface $server */
+ $server = $this->entity;
+ $server->setStatus(FALSE)->save();
+
+ drupal_set_message($this->t('The search server %name has been disabled.', ['%name' => $this->entity->label()]));
+ $form_state->setRedirect('search_api.overview');
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Form/ServerForm.php b/sites/all/modules/contrib/search/search_api/src/Form/ServerForm.php
new file mode 100644
index 000000000..0bbcc7d03
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Form/ServerForm.php
@@ -0,0 +1,300 @@
+backendPluginManager = $backend_plugin_manager;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static($container->get('plugin.manager.search_api.backend'));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function form(array $form, FormStateInterface $form_state) {
+ // If the form is being rebuilt, rebuild the entity with the current form
+ // values.
+ if ($form_state->isRebuilding()) {
+ $this->entity = $this->buildEntity($form, $form_state);
+ }
+
+ $form = parent::form($form, $form_state);
+
+ /** @var \Drupal\search_api\ServerInterface $server */
+ $server = $this->getEntity();
+
+ // Set the page title according to whether we are creating or editing the
+ // server.
+ if ($server->isNew()) {
+ $form['#title'] = $this->t('Add search server');
+ }
+ else {
+ $form['#title'] = $this->t('Edit search server %label', ['%label' => $server->label()]);
+ }
+
+ $this->buildEntityForm($form, $form_state, $server);
+ // Skip adding the backend config form if we cleared the server form due to
+ // an error.
+ if ($form) {
+ $this->buildBackendConfigForm($form, $form_state, $server);
+ }
+
+ return $form;
+ }
+
+ /**
+ * Builds the form for the basic server properties.
+ *
+ * @param array $form
+ * The current form array.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current form state.
+ * @param \Drupal\search_api\ServerInterface $server
+ * The server that is being created or edited.
+ */
+ public function buildEntityForm(array &$form, FormStateInterface $form_state, ServerInterface $server) {
+ $form['#attached']['library'][] = 'search_api/drupal.search_api.admin_css';
+
+ $form['name'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Server name'),
+ '#description' => $this->t('Enter the displayed name for the server.'),
+ '#default_value' => $server->label(),
+ '#required' => TRUE,
+ ];
+ $form['id'] = [
+ '#type' => 'machine_name',
+ '#default_value' => $server->isNew() ? NULL : $server->id(),
+ '#maxlength' => 50,
+ '#required' => TRUE,
+ '#machine_name' => [
+ 'exists' => '\Drupal\search_api\Entity\Server::load',
+ 'source' => ['name'],
+ ],
+ '#disabled' => !$server->isNew(),
+ ];
+ $form['status'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Enabled'),
+ '#description' => $this->t('Only enabled servers can index items or execute searches.'),
+ '#default_value' => $server->status(),
+ ];
+ $form['description'] = [
+ '#type' => 'textarea',
+ '#title' => $this->t('Description'),
+ '#description' => $this->t('Enter a description for the server.'),
+ '#default_value' => $server->getDescription(),
+ ];
+
+ $backends = $this->backendPluginManager->getDefinitions();
+ $backend_options = [];
+ $descriptions = [];
+ foreach ($backends as $backend_id => $definition) {
+ $config = $backend_id === $server->getBackendId() ? $server->getBackendConfig() : [];
+ $config['#server'] = $server;
+ $backend = $this->backendPluginManager
+ ->createInstance($backend_id, $config);
+ $backend_options[$backend_id] = $backend->label();
+ $descriptions[$backend_id]['#description'] = $backend->getDescription();
+ }
+ asort($backend_options, SORT_NATURAL | SORT_FLAG_CASE);
+ if ($backend_options) {
+ if (count($backend_options) == 1) {
+ $server->set('backend', key($backend_options));
+ }
+ $form['backend'] = [
+ '#type' => 'radios',
+ '#title' => $this->t('Backend'),
+ '#description' => $this->t('Choose a backend to use for this server.'),
+ '#options' => $backend_options,
+ '#default_value' => $server->getBackendId(),
+ '#required' => TRUE,
+ '#disabled' => !$server->isNew(),
+ '#ajax' => [
+ 'callback' => [get_class($this), 'buildAjaxBackendConfigForm'],
+ 'wrapper' => 'search-api-backend-config-form',
+ 'method' => 'replace',
+ 'effect' => 'fade',
+ ],
+ ];
+ $form['backend'] += $descriptions;
+ }
+ else {
+ drupal_set_message($this->t('There are no backend plugins available for the Search API. Please install a module that provides a backend plugin to proceed.', [':url' => Url::fromUri('https://www.drupal.org/node/1254698')->toString()]), 'error');
+ $form = [];
+ }
+ }
+
+ /**
+ * Builds the backend-specific configuration form.
+ *
+ * @param array $form
+ * The current form array.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current form state.
+ * @param \Drupal\search_api\ServerInterface $server
+ * The server that is being created or edited.
+ */
+ public function buildBackendConfigForm(array &$form, FormStateInterface $form_state, ServerInterface $server) {
+ $form['backend_config'] = [];
+ if ($server->hasValidBackend()) {
+ $backend = $server->getBackend();
+ $form_state->set('backend', $backend->getPluginId());
+ if ($backend instanceof PluginFormInterface) {
+ if ($form_state->isRebuilding()) {
+ drupal_set_message($this->t('Please configure the selected backend.'), 'warning');
+ }
+ // Attach the backend plugin configuration form.
+ $backend_form_state = SubformState::createForSubform($form['backend_config'], $form, $form_state);
+ $form['backend_config'] = $backend->buildConfigurationForm($form['backend_config'], $backend_form_state);
+
+ // Modify the backend plugin configuration container element.
+ $form['backend_config']['#type'] = 'details';
+ $form['backend_config']['#title'] = $this->t('Configure %plugin backend', ['%plugin' => $backend->label()]);
+ $form['backend_config']['#open'] = TRUE;
+ }
+ }
+ // Only notify the user of a missing backend plugin if we're editing an
+ // existing server.
+ elseif (!$server->isNew()) {
+ drupal_set_message($this->t('The backend plugin is missing or invalid.'), 'error');
+ return;
+ }
+ $form['backend_config'] += [
+ '#type' => 'container',
+ ];
+ $form['backend_config']['#attributes']['id'] = 'search-api-backend-config-form';
+ $form['backend_config']['#tree'] = TRUE;
+ }
+
+ /**
+ * Handles switching the selected backend plugin.
+ *
+ * @param array $form
+ * The current form array.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current form state.
+ *
+ * @return array
+ * The part of the form to return as AJAX.
+ */
+ public static function buildAjaxBackendConfigForm(array $form, FormStateInterface $form_state) {
+ // The work is already done in form(), where we rebuild the entity according
+ // to the current form values and then create the backend configuration form
+ // based on that. So we just need to return the relevant part of the form
+ // here.
+ return $form['backend_config'];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validateForm(array &$form, FormStateInterface $form_state) {
+ parent::validateForm($form, $form_state);
+
+ /** @var \Drupal\search_api\ServerInterface $server */
+ $server = $this->getEntity();
+
+ // Check if the backend plugin changed.
+ $backend_id = $server->getBackendId();
+ if ($backend_id != $form_state->get('backend')) {
+ // This can only happen during initial server creation, since we don't
+ // allow switching the backend afterwards. The user has selected a
+ // different backend, so any values entered for the other backend should
+ // be discarded.
+ $input = &$form_state->getUserInput();
+ $input['backend_config'] = [];
+ $new_backend = $this->backendPluginManager->createInstance($form_state->getValues()['backend']);
+ if ($new_backend instanceof PluginFormInterface) {
+ $form_state->setRebuild();
+ }
+ }
+ // Check before loading the backend plugin so we don't throw an exception.
+ elseif ($server->hasValidBackend()) {
+ $backend = $server->getBackend();
+ if ($backend instanceof PluginFormInterface) {
+ $backend_form_state = SubformState::createForSubform($form['backend_config'], $form, $form_state);
+ $backend->validateConfigurationForm($form['backend_config'], $backend_form_state);
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ parent::submitForm($form, $form_state);
+
+ /** @var \Drupal\search_api\ServerInterface $server */
+ $server = $this->getEntity();
+ // Check before loading the backend plugin so we don't throw an exception.
+ if ($server->hasValidBackend()) {
+ $backend = $server->getBackend();
+ if ($backend instanceof PluginFormInterface) {
+ $backend_form_state = SubformState::createForSubform($form['backend_config'], $form, $form_state);
+ $backend->submitConfigurationForm($form['backend_config'], $backend_form_state);
+ }
+ }
+
+ return $server;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function save(array $form, FormStateInterface $form_state) {
+ // Only save the server if the form doesn't need to be rebuilt.
+ if (!$form_state->isRebuilding()) {
+ try {
+ $server = $this->getEntity();
+ $server->save();
+ drupal_set_message($this->t('The server was successfully saved.'));
+ $form_state->setRedirect('entity.search_api_server.canonical', ['search_api_server' => $server->id()]);
+ }
+ catch (SearchApiException $e) {
+ $form_state->setRebuild();
+
+ $message = '%type: @message in %function (line %line of %file).';
+ $variables = Error::decodeException($e);
+ $this->getLogger('search_api')->error($message, $variables);
+
+ drupal_set_message($this->t('The server could not be saved.'), 'error');
+ }
+ }
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Form/ServerStatusForm.php b/sites/all/modules/contrib/search/search_api/src/Form/ServerStatusForm.php
new file mode 100644
index 000000000..895a37cbb
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Form/ServerStatusForm.php
@@ -0,0 +1,123 @@
+setServerTaskManager($container->get('search_api.server_task_manager'));
+
+ return $form;
+ }
+
+ /**
+ * Retrieves the server task manager.
+ *
+ * @return \Drupal\search_api\Task\ServerTaskManagerInterface
+ * The server task manager.
+ */
+ public function getServerTaskManager() {
+ return $this->serverTaskManager ?: \Drupal::service('search_api.server_task_manager');
+ }
+
+ /**
+ * Sets the server task manager.
+ *
+ * @param \Drupal\search_api\Task\ServerTaskManagerInterface $server_task_manager
+ * The new server task manager.
+ *
+ * @return $this
+ */
+ public function setServerTaskManager(ServerTaskManagerInterface $server_task_manager) {
+ $this->serverTaskManager = $server_task_manager;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFormId() {
+ return 'search_api_server_status';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(array $form, FormStateInterface $form_state, ServerInterface $server = NULL) {
+ $form['#server'] = $server;
+
+ $pending_tasks = $this->getServerTaskManager()->getCount($server);
+ if ($pending_tasks) {
+ $status = $this->formatPlural(
+ $pending_tasks,
+ 'There is currently @count task pending for this server.',
+ 'There are currently @count tasks pending for this server.'
+ );
+ $form['tasks'] = [
+ '#type' => 'fieldset',
+ '#title' => $this->t('Pending server tasks'),
+ ];
+ $form['tasks']['help'] = [
+ '#type' => 'item',
+ '#title' => $status,
+ '#description' => $this->t('Pending tasks are created when operations on the server, such as deleting one or more items, cannot be executed because the server is currently unavailable (which will usually also create an entry in the Drupal logs). They are automatically tried again before any other operation is executed and the operation is aborted if the tasks could still not be executed, or if there are too many pending tasks to be executed in a single page request. In the latter case, you can use this form to manually execute all tasks and thus unblock the server again.'),
+ ];
+ $form['tasks']['execute'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Execute tasks now'),
+ '#disabled' => !$server->isAvailable(),
+ '#submit' => ['::executeTasks'],
+ ];
+ }
+
+ $form['actions']['#type'] = 'actions';
+ $form['actions']['clear'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Delete all indexed data on this server'),
+ '#button_type' => 'danger',
+ ];
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function executeTasks(array &$form, FormStateInterface $form_state) {
+ /** @var \Drupal\search_api\ServerInterface $server */
+ $server = $form['#server'];
+ $this->getServerTaskManager()->setExecuteBatch($server);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ // Redirect to the "Clear server" confirmation form.
+ /** @var \Drupal\search_api\ServerInterface $server */
+ $server = $form['#server'];
+ $form_state->setRedirect('entity.search_api_server.clear', ['search_api_server' => $server->id()]);
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Form/UnsavedConfigurationFormTrait.php b/sites/all/modules/contrib/search/search_api/src/Form/UnsavedConfigurationFormTrait.php
new file mode 100644
index 000000000..6b90121c2
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Form/UnsavedConfigurationFormTrait.php
@@ -0,0 +1,106 @@
+renderer;
+ }
+
+ /**
+ * Retrieves the date formatter.
+ *
+ * @return \Drupal\Core\Datetime\DateFormatterInterface
+ * The date formatter.
+ */
+ public function getDateFormatter() {
+ return $this->dateFormatter;
+ }
+
+ /**
+ * Checks whether the given entity contains unsaved changes.
+ *
+ * If this is the case and the changes were made by a different user, the form
+ * is disabled and a message displayed.
+ *
+ * Optionally, if there are unsaved changes by the current user, a different
+ * message can be displayed.
+ *
+ * @param array $form
+ * The form structure, passed by reference.
+ * @param object $entity
+ * The entity in question.
+ * @param bool $reportChanged
+ * (optional) If TRUE, also show a message for unsaved changes by the
+ * current user.
+ */
+ protected function checkEntityEditable(array &$form, $entity, $reportChanged = FALSE) {
+ if ($entity instanceof UnsavedConfigurationInterface && $entity->hasChanges()) {
+ if ($entity->isLocked()) {
+ $form['#disabled'] = TRUE;
+ $username = [
+ '#theme' => 'username',
+ '#account' => $entity->getLockOwner(),
+ ];
+ $lockMessageSubstitutions = [
+ '@user' => $this->renderer->render($username),
+ '@age' => $this->dateFormatter->formatTimeDiffSince($entity->getLastUpdated()),
+ ':url' => $entity->toUrl('break-lock-form')->toString(),
+ ];
+ $form['locked'] = [
+ '#type' => 'container',
+ '#attributes' => [
+ 'class' => [
+ 'index-locked',
+ 'messages',
+ 'messages--warning',
+ ],
+ ],
+ '#children' => $this->t('This index is being edited by user @user, and is therefore locked from editing by others. This lock is @age old. Click here to break this lock.', $lockMessageSubstitutions),
+ '#weight' => -10,
+ ];
+ }
+ elseif ($reportChanged) {
+ $form['changed'] = [
+ '#type' => 'container',
+ '#attributes' => [
+ 'class' => [
+ 'index-changed',
+ 'messages',
+ 'messages--warning',
+ ],
+ ],
+ '#children' => $this->t('You have unsaved changes.'),
+ '#weight' => -10,
+ ];
+ }
+ }
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/IndexBatchHelper.php b/sites/all/modules/contrib/search/search_api/src/IndexBatchHelper.php
new file mode 100644
index 000000000..5ea5083d5
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/IndexBatchHelper.php
@@ -0,0 +1,210 @@
+translate($string, $args, $options);
+ }
+
+ /**
+ * Formats a string containing a count of items.
+ *
+ * @see \Drupal\Core\StringTranslation\TranslationInterface::formatPlural()
+ */
+ protected static function formatPlural($count, $singular, $plural, array $args = [], array $options = []) {
+ return static::getStringTranslation()->formatPlural($count, $singular, $plural, $args, $options);
+ }
+
+ /**
+ * Creates an indexing batch for a given search index.
+ *
+ * @param \Drupal\search_api\IndexInterface $index
+ * The search index for which items should be indexed.
+ * @param int|null $batch_size
+ * (optional) Number of items to index per batch. Defaults to the cron limit
+ * set for the index.
+ * @param int $limit
+ * (optional) Maximum number of items to index. Defaults to indexing all
+ * remaining items.
+ *
+ * @throws \Drupal\search_api\SearchApiException
+ * Thrown if the batch could not be created.
+ */
+ public static function create(IndexInterface $index, $batch_size = NULL, $limit = -1) {
+ // Check if the size should be determined by the index cron limit option.
+ if ($batch_size === NULL) {
+ // Use the size set by the index.
+ $batch_size = $index->getOption('cron_limit', \Drupal::config('search_api.settings')->get('default_cron_limit'));
+ }
+ // Check if indexing items is allowed.
+ if ($index->status() && !$index->isReadOnly() && $batch_size !== 0 && $limit !== 0) {
+ // Define the search index batch definition.
+ $batch_definition = [
+ 'operations' => [
+ [[__CLASS__, 'process'], [$index, $batch_size, $limit]],
+ ],
+ 'finished' => [__CLASS__, 'finish'],
+ 'progress_message' => static::t('Completed about @percentage% of the indexing operation (@current of @total).'),
+ ];
+ // Schedule the batch.
+ batch_set($batch_definition);
+ }
+ else {
+ $index_label = $index->label();
+ throw new SearchApiException("Failed to create a batch with batch size '$batch_size' and limit '$limit' for index '$index_label'.");
+ }
+ }
+
+ /**
+ * Processes an index batch operation.
+ *
+ * @param \Drupal\search_api\IndexInterface $index
+ * The index on which items should be indexed.
+ * @param int $batch_size
+ * The maximum number of items to index per batch pass.
+ * @param int $limit
+ * The maximum number of items to index in total, or -1 to index all items.
+ * @param array|\ArrayAccess $context
+ * The context of the current batch, as defined in the @link batch Batch
+ * operations @endlink documentation.
+ */
+ public static function process(IndexInterface $index, $batch_size, $limit, &$context) {
+ // Check if the sandbox should be initialized.
+ if (!isset($context['sandbox']['limit'])) {
+ // Initialize the sandbox with data which is shared among the batch runs.
+ $context['sandbox']['limit'] = $limit;
+ $context['sandbox']['batch_size'] = $batch_size;
+ }
+ // Check if the results should be initialized.
+ if (!isset($context['results']['indexed'])) {
+ // Initialize the results with data which is shared among the batch runs.
+ $context['results']['indexed'] = 0;
+ $context['results']['not indexed'] = 0;
+ }
+ // Get the remaining item count. When no valid tracker is available then
+ // the value will be set to zero which will cause the batch process to
+ // stop.
+ $remaining_item_count = ($index->hasValidTracker() ? $index->getTrackerInstance()->getRemainingItemsCount() : 0);
+
+ // Check if an explicit limit needs to be used.
+ if ($context['sandbox']['limit'] > -1) {
+ // Calculate the remaining amount of items that can be indexed. Note that
+ // a minimum is taking between the allowed number of items and the
+ // remaining item count to prevent incorrect reporting of not indexed
+ // items.
+ $actual_limit = min($context['sandbox']['limit'] - $context['results']['indexed'], $remaining_item_count);
+ }
+ else {
+ // Use the remaining item count as actual limit.
+ $actual_limit = $remaining_item_count;
+ }
+
+ // Store original count of items to be indexed to show progress properly.
+ if (empty($context['sandbox']['original_item_count'])) {
+ $context['sandbox']['original_item_count'] = $actual_limit;
+ }
+
+ // Determine the number of items to index for this run.
+ $to_index = min($actual_limit, $context['sandbox']['batch_size']);
+ // Catch any exception that may occur during indexing.
+ try {
+ // Index items limited by the given count.
+ $indexed = $index->indexItems($to_index);
+ // Increment the indexed result and progress.
+ $context['results']['indexed'] += $indexed;
+ // Display progress message.
+ if ($indexed > 0) {
+ $context['message'] = static::formatPlural($context['results']['indexed'], 'Successfully indexed 1 item.', 'Successfully indexed @count items.');
+ }
+ // Everything has been indexed?
+ if ($indexed === 0 || $context['results']['indexed'] >= $context['sandbox']['original_item_count']) {
+ $context['finished'] = 1;
+ $context['results']['not indexed'] = $context['sandbox']['original_item_count'] - $context['results']['indexed'];
+ }
+ else {
+ $context['finished'] = ($context['results']['indexed'] / $context['sandbox']['original_item_count']);
+ }
+ }
+ catch (\Exception $e) {
+ // Log exception to watchdog and abort the batch job.
+ watchdog_exception('search_api', $e);
+ $context['message'] = static::t('An error occurred during indexing: @message', ['@message' => $e->getMessage()]);
+ $context['finished'] = 1;
+ $context['results']['not indexed'] = $context['sandbox']['original_item_count'] - $context['results']['indexed'];
+ }
+ }
+
+ /**
+ * Finishes an index batch.
+ */
+ public static function finish($success, $results, $operations) {
+ // Check if the batch job was successful.
+ if ($success) {
+ // Display the number of items indexed.
+ if (!empty($results['indexed'])) {
+ // Build the indexed message.
+ $indexed_message = static::formatPlural($results['indexed'], 'Successfully indexed 1 item.', 'Successfully indexed @count items.');
+ // Notify user about indexed items.
+ drupal_set_message($indexed_message);
+ // Display the number of items not indexed.
+ if (!empty($results['not indexed'])) {
+ // Build the not indexed message.
+ $not_indexed_message = static::formatPlural($results['not indexed'], '1 item could not be indexed. Check the logs for details.', '@count items could not be indexed. Check the logs for details.');
+ // Notify user about not indexed items.
+ drupal_set_message($not_indexed_message, 'warning');
+ }
+ }
+ else {
+ // Notify user about failure to index items.
+ drupal_set_message(static::t("Couldn't index items. Check the logs for details."), 'error');
+ }
+ }
+ else {
+ // Notify user about batch job failure.
+ drupal_set_message(static::t('An error occurred while trying to index items. Check the logs for details.'), 'error');
+ }
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/IndexInterface.php b/sites/all/modules/contrib/search/search_api/src/IndexInterface.php
new file mode 100644
index 000000000..277f76c1b
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/IndexInterface.php
@@ -0,0 +1,733 @@
+serverStorage = $server_storage;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
+ return new static(
+ $entity_type,
+ $container->get('entity_type.manager')->getStorage($entity_type->id()),
+ $container->get('entity_type.manager')->getStorage('search_api_server')
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDefaultOperations(EntityInterface $entity) {
+ $operations = parent::getDefaultOperations($entity);
+
+ if ($entity instanceof IndexInterface) {
+ $route_parameters['search_api_index'] = $entity->id();
+ $operations['fields'] = [
+ 'title' => $this->t('Fields'),
+ 'weight' => 20,
+ 'url' => new Url('entity.search_api_index.fields', $route_parameters),
+ ];
+ $operations['processors'] = [
+ 'title' => $this->t('Processors'),
+ 'weight' => 30,
+ 'url' => new Url('entity.search_api_index.processors', $route_parameters),
+ ];
+ }
+
+ return $operations;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildHeader() {
+ return [
+ 'type' => $this->t('Type'),
+ 'title' => $this->t('Name'),
+ 'status' => [
+ 'data' => $this->t('Status'),
+ 'class' => ['checkbox'],
+ ],
+ ] + parent::buildHeader();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildRow(EntityInterface $entity) {
+ /** @var \Drupal\Core\Config\Entity\ConfigEntityInterface $entity */
+ $row = parent::buildRow($entity);
+
+ $status = $entity->status();
+ $status_server = TRUE;
+ $status_label = $status ? $this->t('Enabled') : $this->t('Disabled');
+
+ if ($entity instanceof ServerInterface && $entity->status() && !$entity->isAvailable()) {
+ $status = FALSE;
+ $status_server = FALSE;
+ $status_label = $this->t('Unavailable');
+ }
+
+ $status_icon = [
+ '#theme' => 'image',
+ '#uri' => $status ? 'core/misc/icons/73b355/check.svg' : 'core/misc/icons/e32700/error.svg',
+ '#width' => 18,
+ '#height' => 18,
+ '#alt' => $status_label,
+ '#title' => $status_label,
+ ];
+
+ $row = [
+ 'data' => [
+ 'type' => [
+ 'data' => $entity instanceof ServerInterface ? $this->t('Server') : $this->t('Index'),
+ 'class' => ['search-api-type'],
+ ],
+ 'title' => [
+ 'data' => [
+ '#type' => 'link',
+ '#title' => $entity->label(),
+ '#suffix' => '' . $entity->get('description') . '
',
+ ] + $entity->toUrl('canonical')->toRenderArray(),
+ 'class' => ['search-api-title'],
+ ],
+ 'status' => [
+ 'data' => $status_icon,
+ 'class' => ['checkbox'],
+ ],
+ 'operations' => $row['operations'],
+ ],
+ 'title' => $this->t('ID: @name', ['@name' => $entity->id()]),
+ 'class' => [
+ Html::cleanCssIdentifier($entity->getEntityTypeId() . '-' . $entity->id()),
+ $status ? 'search-api-list-enabled' : 'search-api-list-disabled',
+ $entity instanceof ServerInterface ? 'search-api-list-server' : 'search-api-list-index',
+ ],
+ ];
+
+ if (!$status_server) {
+ $row['class'][] = 'color-error';
+ }
+
+ return $row;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function render() {
+ $entity_groups = $this->loadGroups();
+ $list['#type'] = 'container';
+ $list['#attached']['library'][] = 'search_api/drupal.search_api.admin_css';
+
+ $list['servers'] = [
+ '#type' => 'table',
+ '#header' => $this->buildHeader(),
+ '#rows' => [],
+ '#empty' => $entity_groups['lone_indexes'] ? '' : $this->t('There are no servers or indexes defined. For a quick start, we suggest you install the Database Search Defaults module.'),
+ '#attributes' => [
+ 'id' => 'search-api-entity-list',
+ 'class' => ['search-api-entity-list'],
+ ],
+ ];
+ foreach ($entity_groups['servers'] as $server_groups) {
+ /** @var \Drupal\Core\Config\Entity\ConfigEntityInterface $entity */
+ foreach ($server_groups as $entity) {
+ $list['servers']['#rows'][$entity->getEntityTypeId() . '.' . $entity->id()] = $this->buildRow($entity);
+ }
+ }
+
+ // Output the list of indexes without a server separately.
+ if (!empty($entity_groups['lone_indexes'])) {
+ $list['lone_indexes']['heading']['#markup'] = '' . $this->t('Indexes not currently associated with any server') . '
';
+ $list['lone_indexes']['table'] = [
+ '#type' => 'table',
+ '#header' => $this->buildHeader(),
+ '#rows' => [],
+ ];
+
+ foreach ($entity_groups['lone_indexes'] as $entity) {
+ $list['lone_indexes']['table']['#rows'][$entity->id()] = $this->buildRow($entity);
+ }
+ }
+
+ return $list;
+ }
+
+ /**
+ * Loads search servers and indexes, grouped by servers.
+ *
+ * @return \Drupal\Core\Config\Entity\ConfigEntityInterface[][]
+ * An associative array with two keys:
+ * - servers: All available search servers, each followed by all search
+ * indexes attached to it.
+ * - lone_indexes: All search indexes that aren't attached to any server.
+ */
+ public function loadGroups() {
+ $indexes = $this->storage->loadMultiple();
+ /** @var \Drupal\search_api\ServerInterface[] $servers */
+ $servers = $this->serverStorage->loadMultiple();
+
+ $this->sortByStatusThenAlphabetically($indexes);
+ $this->sortByStatusThenAlphabetically($servers);
+
+ $server_groups = [];
+ foreach ($servers as $server) {
+ $server_group = [
+ 'server.' . $server->id() => $server,
+ ];
+
+ foreach ($server->getIndexes() as $index) {
+ $server_group['index.' . $index->id()] = $index;
+ // Remove this index from $index so it will finally only contain those
+ // indexes not belonging to any server.
+ unset($indexes[$index->id()]);
+ }
+
+ $server_groups['server.' . $server->id()] = $server_group;
+ }
+
+ return [
+ 'servers' => $server_groups,
+ 'lone_indexes' => $indexes,
+ ];
+ }
+
+ /**
+ * Sorts an array of entities by status and then alphabetically.
+ *
+ * Will preserve the key/value association of the array.
+ *
+ * @param \Drupal\Core\Config\Entity\ConfigEntityInterface[] $entities
+ * An array of config entities.
+ */
+ protected function sortByStatusThenAlphabetically(array &$entities) {
+ uasort($entities, function (ConfigEntityInterface $a, ConfigEntityInterface $b) {
+ if ($a->status() == $b->status()) {
+ return strnatcasecmp($a->label(), $b->label());
+ }
+ else {
+ return $a->status() ? -1 : 1;
+ }
+ });
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Item/Field.php b/sites/all/modules/contrib/search/search_api/src/Item/Field.php
new file mode 100644
index 000000000..7371d603e
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Item/Field.php
@@ -0,0 +1,683 @@
+index = $index;
+ $this->fieldIdentifier = $this->originalFieldIdentifier = $field_identifier;
+ }
+
+ /**
+ * Retrieves the data type manager.
+ *
+ * @return \Drupal\search_api\DataType\DataTypePluginManager
+ * The data type manager.
+ */
+ public function getDataTypeManager() {
+ return $this->dataTypeManager ?: \Drupal::service('plugin.manager.search_api.data_type');
+ }
+
+ /**
+ * Sets the data type manager.
+ *
+ * @param \Drupal\search_api\DataType\DataTypePluginManager $data_type_manager
+ * The new data type manager.
+ *
+ * @return $this
+ */
+ public function setDataTypeManager(DataTypePluginManager $data_type_manager) {
+ $this->dataTypeManager = $data_type_manager;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getIndex() {
+ return $this->index;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setIndex(IndexInterface $index) {
+ if ($this->index->id() != $index->id()) {
+ throw new \InvalidArgumentException('Attempted to change the index of a field object.');
+ }
+ $this->index = $index;
+ $this->datasource = NULL;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFieldIdentifier() {
+ return $this->fieldIdentifier;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getOriginalFieldIdentifier() {
+ return $this->originalFieldIdentifier;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setFieldIdentifier($field_id) {
+ $this->fieldIdentifier = $field_id;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function wasRenamed() {
+ return $this->fieldIdentifier != $this->originalFieldIdentifier;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSettings() {
+ $settings = [
+ 'label' => $this->getLabel(),
+ 'datasource_id' => $this->getDatasourceId(),
+ 'property_path' => $this->getPropertyPath(),
+ 'type' => $this->getType(),
+ ];
+ if ($this->getDatasourceId() === NULL) {
+ unset($settings['datasource_id']);
+ }
+ if ($this->getBoost() != 1.0) {
+ $settings['boost'] = $this->getBoost();
+ }
+ if ($this->isIndexedLocked()) {
+ $settings['indexed_locked'] = TRUE;
+ }
+ if ($this->isTypeLocked()) {
+ $settings['type_locked'] = TRUE;
+ }
+ if ($this->isHidden()) {
+ $settings['hidden'] = TRUE;
+ }
+ if ($this->getConfiguration()) {
+ $settings['configuration'] = $this->getConfiguration();
+ }
+ if ($this->getDependencies()) {
+ $settings['dependencies'] = $this->getDependencies();
+ }
+ return $settings;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDatasourceId() {
+ return $this->datasourceId;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDatasource() {
+ if (!isset($this->datasource) && isset($this->datasourceId)) {
+ $this->datasource = $this->index->getDatasource($this->datasourceId);
+ }
+ return $this->datasource;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setDatasourceId($datasource_id) {
+ if ($this->datasourceId != $datasource_id) {
+ $this->datasource = NULL;
+ }
+ $this->datasourceId = $datasource_id;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getPropertyPath() {
+ return $this->propertyPath;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setPropertyPath($property_path) {
+ $this->propertyPath = $property_path;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCombinedPropertyPath() {
+ return Utility::createCombinedId($this->getDatasourceId(), $this->getPropertyPath());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getLabel() {
+ return $this->label;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setLabel($label) {
+ $this->label = $label;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDescription() {
+ if (!isset($this->description)) {
+ try {
+ $property = $this->getDataDefinition();
+ if ($property instanceof ConfigurablePropertyInterface) {
+ $this->description = $property->getFieldDescription($this);
+ }
+ else {
+ $this->description = $property->getDescription();
+ }
+ $this->description = $this->description ?: FALSE;
+ }
+ catch (SearchApiException $e) {
+ $this->logException($e);
+ }
+ }
+ return $this->description ?: NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setDescription($description) {
+ // Set FALSE instead of NULL so caching will work properly.
+ $this->description = $description ?: FALSE;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getPrefixedLabel() {
+ if (!isset($this->labelPrefix)) {
+ $this->labelPrefix = '';
+ if (isset($this->datasourceId)) {
+ $this->labelPrefix = $this->datasourceId;
+ try {
+ $this->labelPrefix = $this->getDatasource()->label();
+ }
+ catch (SearchApiException $e) {
+ $this->logException($e);
+ }
+ $this->labelPrefix .= ' » ';
+ }
+ }
+ return $this->labelPrefix . $this->getLabel();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setLabelPrefix($label_prefix) {
+ $this->labelPrefix = $label_prefix;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isHidden() {
+ return (bool) $this->hidden;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setHidden($hidden = TRUE) {
+ $this->hidden = $hidden;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDataDefinition() {
+ if (!isset($this->dataDefinition)) {
+ $definitions = $this->index->getPropertyDefinitions($this->getDatasourceId());
+ $definition = \Drupal::getContainer()
+ ->get('search_api.fields_helper')
+ ->retrieveNestedProperty($definitions, $this->getPropertyPath());
+ if (!$definition) {
+ $field_label = $this->getLabel();
+ $index_label = $this->getIndex()->label();
+ throw new SearchApiException("Could not retrieve data definition for field '$field_label' on index '$index_label'.");
+ }
+ $this->dataDefinition = $definition;
+ }
+ return $this->dataDefinition;
+ }
+
+ /**
+ * Sets the field's data definition.
+ *
+ * This should mainly be used only when creating a new field object. Calling
+ * this on an existing field object might not work as expected.
+ *
+ * @param \Drupal\Core\TypedData\DataDefinitionInterface $data_definition
+ * The field's new data definition.
+ *
+ * @return $this
+ */
+ public function setDataDefinition(DataDefinitionInterface $data_definition) {
+ $this->dataDefinition = $data_definition;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getType() {
+ return $this->type;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDataTypePlugin() {
+ $data_type_manager = $this->getDataTypeManager();
+ if ($data_type_manager->hasDefinition($this->getType())) {
+ return $data_type_manager->createInstance($this->getType());
+ }
+ return NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setType($type) {
+ if ($type != $this->type && $this->isTypeLocked()) {
+ $field_label = $this->getLabel();
+ $index_label = $this->getIndex()->label();
+ throw new SearchApiException("Trying to change the type of field '$field_label' on index '$index_label', which is locked.");
+ }
+ $this->type = $type;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getValues() {
+ return $this->values;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setValues(array $values) {
+ $this->values = array_values($values);
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addValue($value) {
+ // The data type has to be able to alter the given value before it is
+ // included.
+ $data_type_plugin = $this->getDataTypePlugin();
+ if ($data_type_plugin) {
+ $value = $data_type_plugin->getValue($value);
+ }
+
+ $this->values[] = $value;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getOriginalType() {
+ if (!isset($this->originalType)) {
+ $this->originalType = 'string';
+ try {
+ $this->originalType = $this->getDataDefinition()->getDataType();
+ }
+ catch (SearchApiException $e) {
+ $this->logException($e);
+ }
+ }
+ return $this->originalType;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setOriginalType($original_type) {
+ $this->originalType = $original_type;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getBoost() {
+ return isset($this->boost) ? $this->boost : 1.0;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setBoost($boost) {
+ $this->boost = (float) $boost;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isIndexedLocked() {
+ return (bool) $this->indexedLocked;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setIndexedLocked($indexed_locked = TRUE) {
+ $this->indexedLocked = $indexed_locked;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isTypeLocked() {
+ return (bool) $this->typeLocked;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setTypeLocked($type_locked = TRUE) {
+ $this->typeLocked = $type_locked;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getConfiguration() {
+ return $this->configuration;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setConfiguration(array $configuration) {
+ $this->configuration = $configuration;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDependencies() {
+ return $this->dependencies;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setDependencies(array $dependencies) {
+ $this->dependencies = $dependencies;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getIterator() {
+ return new \ArrayIterator($this->values);
+ }
+
+ /**
+ * Implements the magic __toString() method to simplify debugging.
+ */
+ public function __toString() {
+ $label = $this->getLabel();
+ $field_id = $this->getFieldIdentifier();
+ $type = $this->getType();
+ $out = "$label [$field_id]: indexed as type $type";
+ $is_text_type = \Drupal::getContainer()
+ ->get('search_api.data_type_helper')
+ ->isTextType($type);
+ if ($is_text_type) {
+ $out .= ' (boost ' . $this->getBoost() . ')';
+ }
+ if ($this->getValues()) {
+ $out .= "\nValues:";
+ foreach ($this->getValues() as $value) {
+ $value = str_replace("\n", "\n ", "$value");
+ $out .= "\n- " . $value;
+ }
+ }
+ return $out;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __sleep() {
+ $this->indexId = $this->index->id();
+ $properties = get_object_vars($this);
+ // Don't serialize objects in properties or the field values.
+ unset($properties['index']);
+ unset($properties['datasource']);
+ unset($properties['dataDefinition']);
+ unset($properties['dataTypeManager']);
+ return array_keys($properties);
+ }
+
+ /**
+ * Implements the magic __wakeup() method to control object unserialization.
+ */
+ public function __wakeup() {
+ // Make sure we have a container to do this. This is important to correctly
+ // display test failures.
+ if ($this->indexId && \Drupal::hasContainer()) {
+ $this->index = \Drupal::entityTypeManager()
+ ->getStorage('search_api_index')
+ ->load($this->indexId);
+ $this->indexId = NULL;
+ }
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Item/FieldInterface.php b/sites/all/modules/contrib/search/search_api/src/Item/FieldInterface.php
new file mode 100644
index 000000000..5c993e8e3
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Item/FieldInterface.php
@@ -0,0 +1,425 @@
+index = $index;
+ $this->itemId = $id;
+ if ($datasource) {
+ $this->datasource = $datasource;
+ $this->datasourceId = $datasource->getPluginId();
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDatasourceId() {
+ if (!isset($this->datasourceId)) {
+ list($this->datasourceId) = Utility::splitCombinedId($this->itemId);
+ }
+ return $this->datasourceId;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDatasource() {
+ if (!isset($this->datasource)) {
+ $this->datasource = $this->index->getDatasource($this->getDatasourceId());
+ }
+ return $this->datasource;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getIndex() {
+ return $this->index;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getLanguage() {
+ if (!isset($this->language)) {
+ $this->language = $this->getDatasource()->getItemLanguage($this->getOriginalObject());
+ }
+ return $this->language;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setLanguage($language) {
+ $this->language = $language;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getId() {
+ return $this->itemId;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getOriginalObject($load = TRUE) {
+ if (!isset($this->originalObject) && $load) {
+ $this->originalObject = $this->index->loadItem($this->itemId);
+ if (!$this->originalObject) {
+ throw new SearchApiException('Failed to load original object ' . $this->itemId);
+ }
+ }
+ return $this->originalObject;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setOriginalObject(ComplexDataInterface $original_object) {
+ $this->originalObject = $original_object;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getField($field_id, $extract = TRUE) {
+ if (isset($this->fields[$field_id])) {
+ return $this->fields[$field_id];
+ }
+ $fields = $this->getFields($extract);
+ return isset($fields[$field_id]) ? $fields[$field_id] : NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFields($extract = TRUE) {
+ if ($extract && !$this->fieldsExtracted) {
+ $data_type_fallback_mapping = \Drupal::getContainer()
+ ->get('search_api.data_type_helper')
+ ->getDataTypeFallbackMapping($this->index);
+ foreach ([NULL, $this->getDatasourceId()] as $datasource_id) {
+ $fields_by_property_path = [];
+ $processors_with_fields = [];
+ $properties = $this->index->getPropertyDefinitions($datasource_id);
+ foreach ($this->index->getFieldsByDatasource($datasource_id) as $field_id => $field) {
+ // Don't overwrite fields that were previously set.
+ if (empty($this->fields[$field_id])) {
+ $this->fields[$field_id] = clone $field;
+
+ $field_data_type = $this->fields[$field_id]->getType();
+ // If the field data type is in the fallback mapping list, then use
+ // the fallback type as field type.
+ if (isset($data_type_fallback_mapping[$field_data_type])) {
+ $this->fields[$field_id]->setType($data_type_fallback_mapping[$field_data_type]);
+ }
+
+ // For determining whether the field is provided via a processor, we
+ // need to check using the first part of its property path (in other
+ // words, the property that's directly on the result item, not
+ // nested), since only direct properties of the item can be added by
+ // the processor.
+ $property = NULL;
+ $property_name = Utility::splitPropertyPath($field->getPropertyPath(), FALSE)[0];
+ if (isset($properties[$property_name])) {
+ $property = $properties[$property_name];
+ }
+ if ($property instanceof ProcessorPropertyInterface) {
+ $processors_with_fields[$property->getProcessorId()] = TRUE;
+ }
+ elseif ($datasource_id) {
+ $fields_by_property_path[$field->getPropertyPath()][] = $this->fields[$field_id];
+ }
+ }
+ }
+ try {
+ if ($fields_by_property_path) {
+ \Drupal::getContainer()
+ ->get('search_api.fields_helper')
+ ->extractFields($this->getOriginalObject(), $fields_by_property_path, $this->getLanguage());
+ }
+ if ($processors_with_fields) {
+ $processors = $this->index->getProcessorsByStage(ProcessorInterface::STAGE_ADD_PROPERTIES);
+ foreach ($processors as $processor_id => $processor) {
+ if (isset($processors_with_fields[$processor_id])) {
+ $processor->addFieldValues($this);
+ }
+ }
+ }
+ }
+ catch (SearchApiException $e) {
+ // If we couldn't load the object, just log an error and fail
+ // silently to set the values.
+ $this->logException($e);
+ }
+ }
+ $this->fieldsExtracted = TRUE;
+ }
+ return $this->fields;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setField($field_id, FieldInterface $field = NULL) {
+ if ($field) {
+ if ($field->getFieldIdentifier() !== $field_id) {
+ throw new \InvalidArgumentException('The field identifier passed must be consistent with the identifier set on the field object.');
+ }
+ // Make sure that the field has the same index object set as we. This
+ // might otherwise cause impossibly hard-to-detect bugs.
+ $field->setIndex($this->index);
+ $this->fields[$field_id] = $field;
+ }
+ else {
+ unset($this->fields[$field_id]);
+ }
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setFields(array $fields) {
+ // Make sure that all fields have the same index object set as we. This
+ // might otherwise cause impossibly hard-to-detect bugs.
+ /** @var \Drupal\search_api\Item\FieldInterface $field */
+ foreach ($fields as $field) {
+ $field->setIndex($this->index);
+ }
+ $this->fields = $fields;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isFieldsExtracted() {
+ return $this->fieldsExtracted;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setFieldsExtracted($fields_extracted) {
+ $this->fieldsExtracted = $fields_extracted;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getScore() {
+ return $this->score;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setScore($score) {
+ $this->score = $score;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getBoost() {
+ return $this->boost;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setBoost($boost) {
+ $this->boost = $boost;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getExcerpt() {
+ return $this->excerpt;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setExcerpt($excerpt) {
+ $this->excerpt = $excerpt;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hasExtraData($key) {
+ return array_key_exists($key, $this->extraData);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getExtraData($key, $default = NULL) {
+ return array_key_exists($key, $this->extraData) ? $this->extraData[$key] : $default;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getAllExtraData() {
+ return $this->extraData;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setExtraData($key, $data = NULL) {
+ if (isset($data)) {
+ $this->extraData[$key] = $data;
+ }
+ else {
+ unset($this->extraData[$key]);
+ }
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function checkAccess(AccountInterface $account = NULL) {
+ try {
+ return $this->getDatasource()
+ ->checkItemAccess($this->getOriginalObject(), $account);
+ }
+ catch (SearchApiException $e) {
+ return FALSE;
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getIterator() {
+ return new \ArrayIterator($this->getFields());
+ }
+
+ /**
+ * Implements the magic __clone() method to implement a deep clone.
+ */
+ public function __clone() {
+ // The fields definitely need to be cloned. For the extra data its hard (or,
+ // rather, impossible) to tell, but we opt for cloning objects there, too,
+ // to be on the (hopefully) safer side. (Ideas for later: introduce an
+ // interface that tells us to not clone the data object; or check whether
+ // its an entity; or introduce some other system to override this default.)
+ foreach ($this->fields as $field_id => $field) {
+ $this->fields[$field_id] = clone $field;
+ }
+ foreach ($this->extraData as $key => $data) {
+ if (is_object($data)) {
+ $this->extraData[$key] = clone $data;
+ }
+ }
+ }
+
+ /**
+ * Implements the magic __toString() method to simplify debugging.
+ */
+ public function __toString() {
+ $out = 'Item ' . $this->getId();
+ if ($this->getScore() != 1) {
+ $out .= "\nScore: " . $this->getScore();
+ }
+ if ($this->getBoost() != 1) {
+ $out .= "\nBoost: " . $this->getBoost();
+ }
+ if ($this->getExcerpt()) {
+ $excerpt = str_replace("\n", "\n ", $this->getExcerpt());
+ $out .= "\nExcerpt: $excerpt";
+ }
+ if ($this->getFields()) {
+ $out .= "\nFields:";
+ foreach ($this->getFields() as $field) {
+ $field = str_replace("\n", "\n ", "$field");
+ $out .= "\n- " . $field;
+ }
+ }
+ if ($this->getAllExtraData()) {
+ $data = str_replace("\n", "\n ", print_r($this->getAllExtraData(), TRUE));
+ $out .= "\nExtra data: " . $data;
+ }
+ return $out;
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Item/ItemInterface.php b/sites/all/modules/contrib/search/search_api/src/Item/ItemInterface.php
new file mode 100644
index 000000000..7091fdacd
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Item/ItemInterface.php
@@ -0,0 +1,299 @@
+logger ?: \Drupal::service('logger.channel.search_api');
+ }
+
+ /**
+ * Sets the logger.
+ *
+ * @param \Psr\Log\LoggerInterface $logger
+ * The new logger.
+ *
+ * @return $this
+ */
+ public function setLogger(LoggerInterface $logger) {
+ $this->logger = $logger;
+ return $this;
+ }
+
+ /**
+ * Logs an exception.
+ *
+ * @param \Exception $exception
+ * The exception that is going to be logged.
+ * @param string|null $message
+ * (optional) The message to display in the log, which can use variables
+ * retrieved from the exception (like "%type" or "@message"). Or NULL to use
+ * the default message.
+ * @param array $variables
+ * (optional) Array of variables to replace in the message when it is
+ * displayed, or NULL if the message should not be translated. The normal
+ * patterns for translation variables can be used.
+ * @param int $severity
+ * (optional) The severity of the message, as per RFC 3164.
+ * @param string|null $link
+ * (optional) A link to associate with the message, if any.
+ *
+ * @see watchdog_exception()
+ * @see \Drupal\Core\Utility\Error::decodeException()
+ */
+ protected function logException(\Exception $exception, $message = NULL, array $variables = [], $severity = RfcLogLevel::ERROR, $link = NULL) {
+ // Use a default value if $message is not set.
+ if (empty($message)) {
+ $message = '%type: @message in %function (line %line of %file).';
+ }
+
+ if ($link) {
+ $variables['link'] = $link;
+ }
+
+ $variables += Error::decodeException($exception);
+
+ $this->getLogger()->log($severity, $message, $variables);
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/ParamConverter/SearchApiConverter.php b/sites/all/modules/contrib/search/search_api/src/ParamConverter/SearchApiConverter.php
new file mode 100644
index 000000000..a68da13c0
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/ParamConverter/SearchApiConverter.php
@@ -0,0 +1,97 @@
+tempStoreFactory = $temp_store_factory;
+ $this->currentUser = $user;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function convert($value, $definition, $name, array $defaults) {
+ /** @var \Drupal\search_api\IndexInterface $entity */
+ $storage = $this->entityManager->getStorage('search_api_index');
+ if (!($storage instanceof ConfigEntityStorageInterface)) {
+ return NULL;
+ }
+ if (!($entity = $storage->loadOverrideFree($value))) {
+ return NULL;
+ }
+
+ // Get the temp store for this variable if it needs one. Attempt to load the
+ // index from the temp store, update the currently logged-in user's ID and
+ // store the lock metadata.
+ $store = $this->tempStoreFactory->get('search_api_index');
+ $current_user_id = $this->currentUser->id() ?: session_id();
+ /** @var \Drupal\search_api\IndexInterface|\Drupal\search_api\UnsavedIndexConfiguration $index */
+ if ($index = $store->get($value)) {
+ $index = new UnsavedIndexConfiguration($index, $store, $current_user_id);
+ $index->setLockInformation($store->getMetadata($value));
+ $index->setEntityTypeManager($this->entityManager);
+ }
+ // Otherwise, create a new temporary copy of the search index.
+ else {
+ $index = new UnsavedIndexConfiguration($entity, $store, $current_user_id);
+ }
+
+ return $index;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function applies($definition, $name, Route $route) {
+ if (parent::applies($definition, $name, $route)) {
+ return !empty($definition['tempstore']) && $definition['type'] === 'entity:search_api_index';
+ }
+ return FALSE;
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/ParseMode/ParseModeInterface.php b/sites/all/modules/contrib/search/search_api/src/ParseMode/ParseModeInterface.php
new file mode 100644
index 000000000..718cfee7a
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/ParseMode/ParseModeInterface.php
@@ -0,0 +1,74 @@
+getPluginDefinition();
+ return $plugin_definition['label'];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDescription() {
+ $plugin_definition = $this->getPluginDefinition();
+ return $plugin_definition['description'];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getConjunction() {
+ return $this->conjunction;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setConjunction($conjunction) {
+ $this->conjunction = $conjunction;
+ return $this;
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/ParseMode/ParseModePluginManager.php b/sites/all/modules/contrib/search/search_api/src/ParseMode/ParseModePluginManager.php
new file mode 100644
index 000000000..d913d9a25
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/ParseMode/ParseModePluginManager.php
@@ -0,0 +1,72 @@
+setCacheBackend($cache_backend, 'search_api_parse_mode');
+ $this->alterInfo('search_api_parse_mode_info');
+ }
+
+ /**
+ * Returns all known parse modes.
+ *
+ * @return \Drupal\search_api\ParseMode\ParseModeInterface[]
+ * An array of parse mode plugins, keyed by type identifier.
+ */
+ public function getInstances() {
+ $parse_modes = [];
+
+ foreach ($this->getDefinitions() as $plugin_id => $definition) {
+ if (class_exists($definition['class'])) {
+ $parse_modes[$plugin_id] = $this->createInstance($plugin_id);
+ }
+ }
+
+ return $parse_modes;
+ }
+
+ /**
+ * Returns all parse modes known by the Search API as an options list.
+ *
+ * @return string[]
+ * An associative array with all parse mode's IDs as keys, mapped to their
+ * translated labels.
+ *
+ * @see \Drupal\search_api\ParseMode\ParseModePluginManager::getInstances()
+ */
+ public function getInstancesOptions() {
+ $parse_modes = [];
+ foreach ($this->getInstances() as $id => $info) {
+ $parse_modes[$id] = $info->label();
+ }
+ return $parse_modes;
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Plugin/ConfigurablePluginBase.php b/sites/all/modules/contrib/search/search_api/src/Plugin/ConfigurablePluginBase.php
new file mode 100644
index 000000000..12d776341
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Plugin/ConfigurablePluginBase.php
@@ -0,0 +1,96 @@
+defaultConfiguration();
+ parent::__construct($configuration, $plugin_id, $plugin_definition);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+ $plugin = new static($configuration, $plugin_id, $plugin_definition);
+
+ /** @var \Drupal\Core\StringTranslation\TranslationInterface $translation */
+ $translation = $container->get('string_translation');
+ $plugin->setStringTranslation($translation);
+
+ return $plugin;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function label() {
+ $plugin_definition = $this->getPluginDefinition();
+ return $plugin_definition['label'];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDescription() {
+ $plugin_definition = $this->getPluginDefinition();
+ return isset($plugin_definition['description']) ? $plugin_definition['description'] : '';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defaultConfiguration() {
+ return [];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getConfiguration() {
+ return $this->configuration;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setConfiguration(array $configuration) {
+ $this->configuration = $configuration + $this->defaultConfiguration();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function calculateDependencies() {
+ return [];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function onDependencyRemoval(array $dependencies) {
+ // By default, we're not reacting to anything and so we should leave
+ // everything as it was.
+ return FALSE;
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Plugin/ConfigurablePluginInterface.php b/sites/all/modules/contrib/search/search_api/src/Plugin/ConfigurablePluginInterface.php
new file mode 100644
index 000000000..71f5bd30a
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Plugin/ConfigurablePluginInterface.php
@@ -0,0 +1,57 @@
+setIndex($configuration['#index']);
+ unset($configuration['#index']);
+ }
+ parent::__construct($configuration, $plugin_id, $plugin_definition);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getIndex() {
+ return $this->index;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setIndex(IndexInterface $index) {
+ $this->index = $index;
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Plugin/IndexPluginInterface.php b/sites/all/modules/contrib/search/search_api/src/Plugin/IndexPluginInterface.php
new file mode 100644
index 000000000..b42db65c4
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Plugin/IndexPluginInterface.php
@@ -0,0 +1,28 @@
+setConfiguration($form_state->getValues());
+ }
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/data_type/BooleanDataType.php b/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/data_type/BooleanDataType.php
new file mode 100644
index 000000000..2be11146a
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/data_type/BooleanDataType.php
@@ -0,0 +1,19 @@
+text = $text;
+ $this->boost = $boost;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getText() {
+ return $this->text;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setText($text) {
+ $this->text = $text;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getBoost() {
+ return $this->boost;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setBoost($boost) {
+ $this->boost = $boost;
+ return $this;
+ }
+
+ /**
+ * Implements the magic __toString() method.
+ */
+ public function __toString() {
+ return $this->getText();
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/data_type/value/TextTokenInterface.php b/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/data_type/value/TextTokenInterface.php
new file mode 100644
index 000000000..7b6b2dae0
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/data_type/value/TextTokenInterface.php
@@ -0,0 +1,46 @@
+text = $this->originalText = $text;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getText() {
+ return $this->text;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function toText() {
+ $tokens = $this->getTokens();
+ if ($tokens !== NULL) {
+ $to_string = function (TextTokenInterface $token) {
+ return $token->getText();
+ };
+ return implode(' ', array_map($to_string, $tokens));
+ }
+ return $this->getText();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setText($text) {
+ $this->text = $text;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getOriginalText() {
+ return $this->originalText;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setOriginalText($originalText) {
+ $this->originalText = $originalText;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getTokens() {
+ return $this->tokens;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setTokens(array $tokens = NULL) {
+ $this->tokens = $tokens;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getProperties() {
+ return $this->properties;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getProperty($name, $default = NULL) {
+ if (array_key_exists($name, $this->properties)) {
+ return $this->properties[$name];
+ }
+ return $default;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setProperties(array $properties) {
+ $this->properties = $properties;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setProperty($name, $value = TRUE) {
+ $this->properties[$name] = $value;
+ return $this;
+ }
+
+ /**
+ * Implements the magic __toString() method.
+ */
+ public function __toString() {
+ return $this->toText();
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/data_type/value/TextValueInterface.php b/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/data_type/value/TextValueInterface.php
new file mode 100644
index 000000000..2b4b601b2
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/data_type/value/TextValueInterface.php
@@ -0,0 +1,125 @@
+setIndex($configuration['#index']);
+ unset($configuration['#index']);
+ }
+
+ // Since defaultConfiguration() depends on the plugin definition, we need to
+ // override the constructor and set the definition property before calling
+ // that method.
+ $this->pluginDefinition = $plugin_definition;
+ $this->pluginId = $plugin_id;
+ $this->configuration = $configuration + $this->defaultConfiguration();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+ /** @var static $datasource */
+ $datasource = parent::create($container, $configuration, $plugin_id, $plugin_definition);
+
+ $datasource->setEntityTypeManager($container->get('entity_type.manager'));
+ $datasource->setEntityFieldManager($container->get('entity_field.manager'));
+ $datasource->setEntityDisplayRepository($container->get('entity_display.repository'));
+ $datasource->setEntityTypeBundleInfo($container->get('entity_type.bundle.info'));
+ $datasource->setTypedDataManager($container->get('typed_data_manager'));
+ $datasource->setConfigFactory($container->get('config.factory'));
+ $datasource->setLanguageManager($container->get('language_manager'));
+ $datasource->setFieldsHelper($container->get('search_api.fields_helper'));
+
+ return $datasource;
+ }
+
+ /**
+ * Retrieves the entity type manager.
+ *
+ * @return \Drupal\Core\Entity\EntityTypeManagerInterface
+ * The entity type manager.
+ */
+ public function getEntityTypeManager() {
+ return $this->entityTypeManager ?: \Drupal::entityTypeManager();
+ }
+
+ /**
+ * Retrieves the entity storage.
+ *
+ * @return \Drupal\Core\Entity\EntityStorageInterface
+ * The entity storage.
+ */
+ protected function getEntityStorage() {
+ return $this->getEntityTypeManager()->getStorage($this->getEntityTypeId());
+ }
+
+ /**
+ * Returns the definition of this datasource's entity type.
+ *
+ * @return \Drupal\Core\Entity\EntityTypeInterface
+ * The entity type definition.
+ */
+ protected function getEntityType() {
+ return $this->getEntityTypeManager()
+ ->getDefinition($this->getEntityTypeId());
+ }
+
+ /**
+ * Sets the entity type manager.
+ *
+ * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+ * The new entity type manager.
+ *
+ * @return $this
+ */
+ public function setEntityTypeManager(EntityTypeManagerInterface $entity_type_manager) {
+ $this->entityTypeManager = $entity_type_manager;
+ return $this;
+ }
+
+ /**
+ * Retrieves the entity field manager.
+ *
+ * @return \Drupal\Core\Entity\EntityFieldManagerInterface
+ * The entity field manager.
+ */
+ public function getEntityFieldManager() {
+ return $this->entityFieldManager ?: \Drupal::service('entity_field.manager');
+ }
+
+ /**
+ * Sets the entity field manager.
+ *
+ * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
+ * The new entity field manager.
+ *
+ * @return $this
+ */
+ public function setEntityFieldManager(EntityFieldManagerInterface $entity_field_manager) {
+ $this->entityFieldManager = $entity_field_manager;
+ return $this;
+ }
+
+ /**
+ * Retrieves the entity display repository.
+ *
+ * @return \Drupal\Core\Entity\EntityDisplayRepositoryInterface
+ * The entity entity display repository.
+ */
+ public function getEntityDisplayRepository() {
+ return $this->entityDisplayRepository ?: \Drupal::service('entity_display.repository');
+ }
+
+ /**
+ * Sets the entity display repository.
+ *
+ * @param \Drupal\Core\Entity\EntityDisplayRepositoryInterface $entity_display_repository
+ * The new entity display repository.
+ *
+ * @return $this
+ */
+ public function setEntityDisplayRepository(EntityDisplayRepositoryInterface $entity_display_repository) {
+ $this->entityDisplayRepository = $entity_display_repository;
+ return $this;
+ }
+
+ /**
+ * Retrieves the entity display repository.
+ *
+ * @return \Drupal\Core\Entity\EntityTypeBundleInfoInterface
+ * The entity entity display repository.
+ */
+ public function getEntityTypeBundleInfo() {
+ return $this->entityTypeBundleInfo ?: \Drupal::service('entity_type.bundle.info');
+ }
+
+ /**
+ * Sets the entity type bundle info.
+ *
+ * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
+ * The new entity type bundle info.
+ *
+ * @return $this
+ */
+ public function setEntityTypeBundleInfo(EntityTypeBundleInfoInterface $entity_type_bundle_info) {
+ $this->entityTypeBundleInfo = $entity_type_bundle_info;
+ return $this;
+ }
+
+ /**
+ * Retrieves the typed data manager.
+ *
+ * @return \Drupal\Core\TypedData\TypedDataManagerInterface
+ * The typed data manager.
+ */
+ public function getTypedDataManager() {
+ return $this->typedDataManager ?: \Drupal::typedDataManager();
+ }
+
+ /**
+ * Sets the typed data manager.
+ *
+ * @param \Drupal\Core\TypedData\TypedDataManagerInterface $typed_data_manager
+ * The new typed data manager.
+ *
+ * @return $this
+ */
+ public function setTypedDataManager(TypedDataManagerInterface $typed_data_manager) {
+ $this->typedDataManager = $typed_data_manager;
+ return $this;
+ }
+
+ /**
+ * Retrieves the config factory.
+ *
+ * @return \Drupal\Core\Config\ConfigFactoryInterface
+ * The config factory.
+ */
+ public function getConfigFactory() {
+ return $this->configFactory ?: \Drupal::configFactory();
+ }
+
+ /**
+ * Retrieves the config value for a certain key in the Search API settings.
+ *
+ * @param string $key
+ * The key whose value should be retrieved.
+ *
+ * @return mixed
+ * The config value for the given key.
+ */
+ protected function getConfigValue($key) {
+ return $this->getConfigFactory()->get('search_api.settings')->get($key);
+ }
+
+ /**
+ * Sets the config factory.
+ *
+ * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+ * The new config factory.
+ *
+ * @return $this
+ */
+ public function setConfigFactory(ConfigFactoryInterface $config_factory) {
+ $this->configFactory = $config_factory;
+ return $this;
+ }
+
+ /**
+ * Retrieves the language manager.
+ *
+ * @return \Drupal\Core\Language\LanguageManagerInterface
+ * The language manager.
+ */
+ public function getLanguageManager() {
+ return $this->languageManager ?: \Drupal::languageManager();
+ }
+
+ /**
+ * Sets the language manager.
+ *
+ * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
+ * The new language manager.
+ */
+ public function setLanguageManager(LanguageManagerInterface $language_manager) {
+ $this->languageManager = $language_manager;
+ }
+
+ /**
+ * Retrieves the fields helper.
+ *
+ * @return \Drupal\search_api\Utility\FieldsHelperInterface
+ * The fields helper.
+ */
+ public function getFieldsHelper() {
+ return $this->fieldsHelper ?: \Drupal::service('search_api.fields_helper');
+ }
+
+ /**
+ * Sets the fields helper.
+ *
+ * @param \Drupal\search_api\Utility\FieldsHelperInterface $fields_helper
+ * The new fields helper.
+ *
+ * @return $this
+ */
+ public function setFieldsHelper(FieldsHelperInterface $fields_helper) {
+ $this->fieldsHelper = $fields_helper;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getPropertyDefinitions() {
+ $type = $this->getEntityTypeId();
+ $properties = $this->getEntityFieldManager()->getBaseFieldDefinitions($type);
+ if ($bundles = array_keys($this->getBundles())) {
+ foreach ($bundles as $bundle_id) {
+ $properties += $this->getEntityFieldManager()->getFieldDefinitions($type, $bundle_id);
+ }
+ }
+ // Exclude properties with custom storage, since we can't extract them
+ // currently, due to a shortcoming of Core's Typed Data API. See #2695527.
+ // Computed properties should mostly be OK, though, even though they still
+ // count as having "custom storage". The "Path" field from the Core module
+ // does not work, though, so we explicitly exclude it here to avoid
+ // confusion.
+ foreach ($properties as $key => $property) {
+ if (!$property->isComputed() || $key === 'path') {
+ if ($property->getFieldStorageDefinition()->hasCustomStorage()) {
+ unset($properties[$key]);
+ }
+ }
+ }
+ return $properties;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function loadMultiple(array $ids) {
+ $allowed_languages = $this->getLanguages();
+ // Always allow items with undefined language. (Can be the case when
+ // entities are created programmatically.)
+ $allowed_languages[LanguageInterface::LANGCODE_NOT_SPECIFIED] = TRUE;
+ $allowed_languages[LanguageInterface::LANGCODE_NOT_APPLICABLE] = TRUE;
+
+ $entity_ids = [];
+ foreach ($ids as $item_id) {
+ $pos = strrpos($item_id, ':');
+ // This can only happen if someone passes an invalid ID, since we always
+ // include a language code. Still, no harm in guarding against bad input.
+ if ($pos === FALSE) {
+ continue;
+ }
+ $entity_id = substr($item_id, 0, $pos);
+ $langcode = substr($item_id, $pos + 1);
+ if (isset($allowed_languages[$langcode])) {
+ $entity_ids[$entity_id][$item_id] = $langcode;
+ }
+ }
+
+ /** @var \Drupal\Core\Entity\ContentEntityInterface[] $entities */
+ $entities = $this->getEntityStorage()->loadMultiple(array_keys($entity_ids));
+ $items = [];
+ $allowed_bundles = $this->getBundles();
+ foreach ($entity_ids as $entity_id => $langcodes) {
+ if (empty($entities[$entity_id]) || !isset($allowed_bundles[$entities[$entity_id]->bundle()])) {
+ continue;
+ }
+ foreach ($langcodes as $item_id => $langcode) {
+ if ($entities[$entity_id]->hasTranslation($langcode)) {
+ $items[$item_id] = $entities[$entity_id]->getTranslation($langcode)->getTypedData();
+ }
+ }
+ }
+
+ return $items;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defaultConfiguration() {
+ $default_configuration = [];
+
+ if ($this->hasBundles()) {
+ $default_configuration['bundles'] = [
+ 'default' => TRUE,
+ 'selected' => [],
+ ];
+ }
+
+ if ($this->isTranslatable()) {
+ $default_configuration['languages'] = [
+ 'default' => TRUE,
+ 'selected' => [],
+ ];
+ }
+
+ return $default_configuration;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+ if ($this->hasBundles() && ($bundles = $this->getEntityBundleOptions())) {
+ $form['bundles'] = [
+ '#type' => 'details',
+ '#title' => $this->t('Bundles'),
+ '#open' => TRUE,
+ ];
+ $form['bundles']['default'] = [
+ '#type' => 'radios',
+ '#title' => $this->t('Which bundles should be indexed?'),
+ '#options' => [
+ 0 => $this->t('Only those selected'),
+ 1 => $this->t('All except those selected'),
+ ],
+ '#default_value' => (int) $this->configuration['bundles']['default'],
+ ];
+ $form['bundles']['selected'] = [
+ '#type' => 'checkboxes',
+ '#title' => $this->t('Bundles'),
+ '#options' => $bundles,
+ '#default_value' => $this->configuration['bundles']['selected'],
+ '#size' => min(4, count($bundles)),
+ '#multiple' => TRUE,
+ ];
+ }
+
+ if ($this->isTranslatable()) {
+ $form['languages'] = [
+ '#type' => 'details',
+ '#title' => $this->t('Languages'),
+ '#open' => TRUE,
+ ];
+ $form['languages']['default'] = [
+ '#type' => 'radios',
+ '#title' => $this->t('Which languages should be indexed?'),
+ '#options' => [
+ 0 => $this->t('Only those selected'),
+ 1 => $this->t('All except those selected'),
+ ],
+ '#default_value' => (int) $this->configuration['languages']['default'],
+ ];
+ $form['languages']['selected'] = [
+ '#type' => 'checkboxes',
+ '#title' => $this->t('Languages'),
+ '#options' => $this->getTranslationOptions(),
+ '#default_value' => $this->configuration['languages']['selected'],
+ '#multiple' => TRUE,
+ ];
+ }
+
+ return $form;
+ }
+
+ /**
+ * Retrieves the available bundles of this entity type as an options list.
+ *
+ * @return array
+ * An associative array of bundle labels, keyed by the bundle name.
+ */
+ protected function getEntityBundleOptions() {
+ $options = [];
+ if (($bundles = $this->getEntityBundles())) {
+ foreach ($bundles as $bundle => $bundle_info) {
+ $options[$bundle] = Html::escape($bundle_info['label']);
+ }
+ }
+ return $options;
+ }
+
+ /**
+ * Retrieves the available languages of this entity type as an options list.
+ *
+ * @return array
+ * An associative array of language labels, keyed by the language name.
+ */
+ protected function getTranslationOptions() {
+ $options = [];
+ foreach ($this->getLanguageManager()->getLanguages() as $language) {
+ $options[$language->getId()] = $language->getName();
+ }
+
+ return $options;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
+ // Filter out empty checkboxes.
+ foreach (['bundles', 'languages'] as $key) {
+ if ($form_state->hasValue($key)) {
+ $parents = [$key, 'selected'];
+ $value = $form_state->getValue($parents, []);
+ $value = array_keys(array_filter($value));
+ $form_state->setValue($parents, $value);
+ }
+ }
+
+ $this->setConfiguration($form_state->getValues());
+ }
+
+ /**
+ * Retrieves the entity from a search item.
+ *
+ * @param \Drupal\Core\TypedData\ComplexDataInterface $item
+ * An item of this datasource's type.
+ *
+ * @return \Drupal\Core\Entity\EntityInterface|null
+ * The entity object represented by that item, or NULL if none could be
+ * found.
+ */
+ protected function getEntity(ComplexDataInterface $item) {
+ $value = $item->getValue();
+ return $value instanceof EntityInterface ? $value : NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getItemId(ComplexDataInterface $item) {
+ if ($entity = $this->getEntity($item)) {
+ $enabled_bundles = $this->getBundles();
+ if (isset($enabled_bundles[$entity->bundle()])) {
+ return $entity->id() . ':' . $entity->language()->getId();
+ }
+ }
+ return NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getItemLabel(ComplexDataInterface $item) {
+ if ($entity = $this->getEntity($item)) {
+ return $entity->label();
+ }
+ return NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getItemBundle(ComplexDataInterface $item) {
+ if ($entity = $this->getEntity($item)) {
+ return $entity->bundle();
+ }
+ return NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getItemUrl(ComplexDataInterface $item) {
+ if ($entity = $this->getEntity($item)) {
+ if ($entity->hasLinkTemplate('canonical')) {
+ return $entity->toUrl('canonical');
+ }
+ }
+ return NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function checkItemAccess(ComplexDataInterface $item, AccountInterface $account = NULL) {
+ if ($entity = $this->getEntity($item)) {
+ return $this->getEntityTypeManager()
+ ->getAccessControlHandler($this->getEntityTypeId())
+ ->access($entity, 'view', $account);
+ }
+ return FALSE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getItemIds($page = NULL) {
+ return $this->getPartialItemIds($page);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getEntityTypeId() {
+ $plugin_definition = $this->getPluginDefinition();
+ return $plugin_definition['entity_type'];
+ }
+
+ /**
+ * Determines whether the entity type supports bundles.
+ *
+ * @return bool
+ * TRUE if the entity type supports bundles, FALSE otherwise.
+ */
+ protected function hasBundles() {
+ return $this->getEntityType()->hasKey('bundle');
+ }
+
+ /**
+ * Determines whether the entity type supports translations.
+ *
+ * @return bool
+ * TRUE if the entity is translatable, FALSE otherwise.
+ */
+ protected function isTranslatable() {
+ return $this->getEntityType()->isTranslatable();
+ }
+
+ /**
+ * Retrieves all bundles of this datasource's entity type.
+ *
+ * @return array
+ * An associative array of bundle infos, keyed by the bundle names.
+ */
+ protected function getEntityBundles() {
+ return $this->hasBundles() ? $this->getEntityTypeBundleInfo()->getBundleInfo($this->getEntityTypeId()) : [];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getPartialItemIds($page = NULL, array $bundles = NULL, array $languages = NULL) {
+ // These would be pretty pointless calls, but for the sake of completeness
+ // we should check for them and return early. (Otherwise makes the rest of
+ // the code more complicated.)
+ if (($bundles === [] && !$languages) || ($languages === [] && !$bundles)) {
+ return NULL;
+ }
+
+ $select = $this->getEntityTypeManager()
+ ->getStorage($this->getEntityTypeId())
+ ->getQuery();
+
+ // We want to determine all entities of either one of the given bundles OR
+ // one of the given languages. That means we can't just filter for $bundles
+ // if $languages is given. Instead, we have to filter for all bundles we
+ // might want to include and later sort out those for which we want only the
+ // translations in $languages and those (matching $bundles) where we want
+ // all (enabled) translations.
+ if ($this->hasBundles()) {
+ $bundle_property = $this->getEntityType()->getKey('bundle');
+ if ($bundles && !$languages) {
+ $select->condition($bundle_property, $bundles, 'IN');
+ }
+ else {
+ $enabled_bundles = array_keys($this->getBundles());
+ // Since this is also called for removed bundles/languages,
+ // $enabled_bundles might not include $bundles.
+ if ($bundles) {
+ $enabled_bundles = array_unique(array_merge($bundles, $enabled_bundles));
+ }
+ if (count($enabled_bundles) < count($this->getEntityBundles())) {
+ $select->condition($bundle_property, $enabled_bundles, 'IN');
+ }
+ }
+ }
+
+ if (isset($page)) {
+ $page_size = $this->getConfigValue('tracking_page_size');
+ assert($page_size, 'Tracking page size is not set.');
+ $select->range($page * $page_size, $page_size);
+ // For paging to reliably work, a sort should be present.
+ $entity_id = $this->getEntityType()->getKey('id');
+ $select->sort($entity_id);
+ }
+
+ $entity_ids = $select->execute();
+
+ if (!$entity_ids) {
+ return NULL;
+ }
+
+ // For all loaded entities, compute all their item IDs (one for each
+ // translation we want to include). For those matching the given bundles (if
+ // any), we want to include translations for all enabled languages. For all
+ // other entities, we just want to include the translations for the
+ // languages passed to the method (if any).
+ $item_ids = [];
+ $enabled_languages = array_keys($this->getLanguages());
+ // As above for bundles, $enabled_languages might not include $languages.
+ if ($languages) {
+ $enabled_languages = array_unique(array_merge($languages, $enabled_languages));
+ }
+ // Also, we want to always include entities with unknown language.
+ $enabled_languages[] = LanguageInterface::LANGCODE_NOT_SPECIFIED;
+ $enabled_languages[] = LanguageInterface::LANGCODE_NOT_APPLICABLE;
+
+ /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
+ foreach ($this->getEntityStorage()->loadMultiple($entity_ids) as $entity_id => $entity) {
+ $translations = array_keys($entity->getTranslationLanguages());
+ $translations = array_intersect($translations, $enabled_languages);
+ // If only languages were specified, keep only those translations matching
+ // them. If bundles were also specified, keep all (enabled) translations
+ // for those entities that match those bundles.
+ if ($languages !== NULL
+ && (!$bundles || !in_array($entity->bundle(), $bundles))) {
+ $translations = array_intersect($translations, $languages);
+ }
+ foreach ($translations as $langcode) {
+ $item_ids[] = "$entity_id:$langcode";
+ }
+ }
+
+ if (Utility::isRunningInCli()) {
+ // When running in the CLI, this might be executed for all entities from
+ // within a single process. To avoid running out of memory, reset the
+ // static cache after each batch.
+ $this->getEntityStorage()->resetCache($entity_ids);
+ }
+
+ return $item_ids;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getBundles() {
+ if (!$this->hasBundles()) {
+ // For entity types that have no bundle, return a default pseudo-bundle.
+ return [$this->getEntityTypeId() => $this->label()];
+ }
+
+ $configuration = $this->getConfiguration();
+
+ // If "default" is TRUE (that is, "All except those selected"),remove all
+ // the selected bundles from the available ones to compute the indexed
+ // bundles. Otherwise, return all the selected bundles.
+ $bundles = [];
+ $entity_bundles = $this->getEntityBundles();
+ $selected_bundles = array_flip($configuration['bundles']['selected']);
+ $function = $configuration['bundles']['default'] ? 'array_diff_key' : 'array_intersect_key';
+ $entity_bundles = $function($entity_bundles, $selected_bundles);
+ foreach ($entity_bundles as $bundle_id => $bundle_info) {
+ $bundles[$bundle_id] = isset($bundle_info['label']) ? $bundle_info['label'] : $bundle_id;
+ }
+ return $bundles ?: [$this->getEntityTypeId() => $this->label()];
+ }
+
+ /**
+ * Retrieves the enabled languages.
+ *
+ * @return \Drupal\Core\Language\LanguageInterface[]
+ * All languages that are enabled for this datasource, keyed by language
+ * code.
+ */
+ protected function getLanguages() {
+ $all_languages = $this->getLanguageManager()->getLanguages();
+
+ if ($this->isTranslatable()) {
+ $selected_languages = array_flip($this->configuration['languages']['selected']);
+ if ($this->configuration['languages']['default']) {
+ return array_diff_key($all_languages, $selected_languages);
+ }
+ else {
+ return array_intersect_key($all_languages, $selected_languages);
+ }
+ }
+
+ return $all_languages;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getViewModes($bundle = NULL) {
+ if (isset($bundle) && $this->hasBundles()) {
+ return $this->getEntityDisplayRepository()->getViewModeOptionsByBundle($this->getEntityTypeId(), $bundle);
+ }
+ else {
+ return $this->getEntityDisplayRepository()->getViewModeOptions($this->getEntityTypeId());
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function viewItem(ComplexDataInterface $item, $view_mode, $langcode = NULL) {
+ try {
+ if ($entity = $this->getEntity($item)) {
+ $langcode = $langcode ?: $entity->language()->getId();
+ return $this->getEntityTypeManager()->getViewBuilder($this->getEntityTypeId())->view($entity, $view_mode, $langcode);
+ }
+ }
+ catch (\Exception $e) {
+ // The most common reason for this would be a
+ // \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException in
+ // getViewBuilder(), because the entity type definition doesn't specify a
+ // view_builder class.
+ }
+ return [];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function viewMultipleItems(array $items, $view_mode, $langcode = NULL) {
+ try {
+ $view_builder = $this->getEntityTypeManager()->getViewBuilder($this->getEntityTypeId());
+ // Langcode passed, use that for viewing.
+ if (isset($langcode)) {
+ $entities = [];
+ foreach ($items as $i => $item) {
+ if ($entity = $this->getEntity($item)) {
+ $entities[$i] = $entity;
+ }
+ }
+ if ($entities) {
+ return $view_builder->viewMultiple($entities, $view_mode, $langcode);
+ }
+ return [];
+ }
+ // Otherwise, separate the items by language, keeping the keys.
+ $items_by_language = [];
+ foreach ($items as $i => $item) {
+ if ($item instanceof EntityInterface) {
+ $items_by_language[$item->language()->getId()][$i] = $item;
+ }
+ }
+ // Then build the items for each language. We initialize $build beforehand
+ // and use array_replace() to add to it so the order stays the same.
+ $build = array_fill_keys(array_keys($items), []);
+ foreach ($items_by_language as $langcode => $language_items) {
+ $build = array_replace($build, $view_builder->viewMultiple($language_items, $view_mode, $langcode));
+ }
+ return $build;
+ }
+ catch (\Exception $e) {
+ // The most common reason for this would be a
+ // \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException in
+ // getViewBuilder(), because the entity type definition doesn't specify a
+ // view_builder class.
+ return [];
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function calculateDependencies() {
+ $this->dependencies = parent::calculateDependencies();
+
+ $this->addDependency('module', $this->getEntityType()->getProvider());
+
+ return $this->dependencies;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFieldDependencies(array $fields) {
+ $dependencies = [];
+ $properties = $this->getPropertyDefinitions();
+
+ foreach ($fields as $field_id => $property_path) {
+ $dependencies[$field_id] = $this->getPropertyPathDependencies($property_path, $properties);
+ }
+
+ return $dependencies;
+ }
+
+ /**
+ * Computes all dependencies of the given property path.
+ *
+ * @param string $property_path
+ * The property path of the property.
+ * @param \Drupal\Core\TypedData\DataDefinitionInterface[] $properties
+ * The properties which form the basis for the property path.
+ *
+ * @return string[][]
+ * An associative array with the dependencies for the given property path,
+ * mapping dependency types to arrays of dependency names.
+ */
+ protected function getPropertyPathDependencies($property_path, array $properties) {
+ $dependencies = [];
+
+ list($key, $nested_path) = Utility::splitPropertyPath($property_path, FALSE);
+ if (!isset($properties[$key])) {
+ return $dependencies;
+ }
+
+ $property = $properties[$key];
+ if ($property instanceof FieldConfigInterface) {
+ $storage = $property->getFieldStorageDefinition();
+ if ($storage instanceof FieldStorageConfigInterface) {
+ $name = $storage->getConfigDependencyName();
+ $dependencies[$storage->getConfigDependencyKey()][$name] = $name;
+ }
+ }
+
+ $property = $this->getFieldsHelper()->getInnerProperty($property);
+
+ if ($property instanceof EntityDataDefinitionInterface) {
+ $entity_type_definition = $this->getEntityTypeManager()
+ ->getDefinition($property->getEntityTypeId());
+ if ($entity_type_definition) {
+ $module = $entity_type_definition->getProvider();
+ $dependencies['module'][$module] = $module;
+ }
+ }
+
+ if (isset($nested_path) && $property instanceof ComplexDataDefinitionInterface) {
+ $nested = $this->getFieldsHelper()->getNestedProperties($property);
+ $nested_dependencies = $this->getPropertyPathDependencies($nested_path, $nested);
+ foreach ($nested_dependencies as $type => $names) {
+ $dependencies += [$type => []];
+ $dependencies[$type] += $names;
+ }
+ }
+
+ return array_map('array_values', $dependencies);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function getIndexesForEntity(ContentEntityInterface $entity) {
+ $datasource_id = 'entity:' . $entity->getEntityTypeId();
+ $entity_bundle = $entity->bundle();
+ $has_bundles = $entity->getEntityType()->hasKey('bundle');
+
+ // Needed for PhpStorm. See https://youtrack.jetbrains.com/issue/WI-23395.
+ /** @var \Drupal\search_api\IndexInterface[] $indexes */
+ $indexes = Index::loadMultiple();
+
+ foreach ($indexes as $index_id => $index) {
+ // Filter our indexes that don't contain the datasource in question.
+ if (!$index->isValidDatasource($datasource_id)) {
+ unset($indexes[$index_id]);
+ }
+ elseif ($has_bundles) {
+ // If the entity type supports bundles, we also have to filter out
+ // indexes that exclude the entity's bundle.
+ $config = $index->getDatasource($datasource_id)->getConfiguration();
+ $default = !empty($config['bundles']['default']);
+ $bundle_set = in_array($entity_bundle, $config['bundles']['selected']);
+ if ($default == $bundle_set) {
+ unset($indexes[$index_id]);
+ }
+ }
+ }
+
+ return $indexes;
+ }
+
+ /**
+ * Filters a set of datasource-specific item IDs.
+ *
+ * Returns only those item IDs that are valid for the given datasource and
+ * index. This method only checks the item language, though – whether an
+ * entity with that ID actually exists, or whether it has a bundle included
+ * for that datasource, is not verified.
+ *
+ * @param \Drupal\search_api\IndexInterface $index
+ * The index for which to validate.
+ * @param string $datasource_id
+ * The ID of the datasource on the index for which to validate.
+ * @param string[] $item_ids
+ * The item IDs to be validated.
+ *
+ * @return string[]
+ * All given item IDs that are valid for that index and datasource.
+ */
+ public static function filterValidItemIds(IndexInterface $index, $datasource_id, array $item_ids) {
+ if (!$index->isValidDatasource($datasource_id)) {
+ return $item_ids;
+ }
+ $config = $index->getDatasource($datasource_id)->getConfiguration();
+ // If the entity type doesn't allow translations, we just accept all IDs.
+ // (If the entity type were translatable, the config key would have been set
+ // with the default configuration.)
+ if (!isset($config['languages']['selected'])) {
+ return $item_ids;
+ }
+ $default = !empty($config['languages']['default']);
+ $selected = $config['languages']['selected'];
+ $always_valid = [
+ LanguageInterface::LANGCODE_NOT_SPECIFIED,
+ LanguageInterface::LANGCODE_NOT_APPLICABLE,
+ ];
+ $valid_ids = [];
+ foreach ($item_ids as $item_id) {
+ $pos = strrpos($item_id, ':');
+ // Item IDs without colons are always invalid.
+ if ($pos === FALSE) {
+ continue;
+ }
+ $langcode = substr($item_id, $pos + 1);
+ if ($default != in_array($langcode, $selected)
+ || in_array($langcode, $always_valid)) {
+ $valid_ids[] = $item_id;
+ }
+ }
+ return $valid_ids;
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/datasource/ContentEntityDeriver.php b/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/datasource/ContentEntityDeriver.php
new file mode 100644
index 000000000..7f6318fb6
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/datasource/ContentEntityDeriver.php
@@ -0,0 +1,97 @@
+get('entity_type.manager');
+ $deriver->setEntityTypeManager($entity_type_manager);
+
+ /** @var \Drupal\Core\StringTranslation\TranslationInterface $translation */
+ $translation = $container->get('string_translation');
+ $deriver->setStringTranslation($translation);
+
+ return $deriver;
+ }
+
+ /**
+ * Retrieves the entity type manager.
+ *
+ * @return \Drupal\Core\Entity\EntityTypeManagerInterface
+ * The entity type manager.
+ */
+ public function getEntityTypeManager() {
+ return $this->entityTypeManager ?: \Drupal::entityTypeManager();
+ }
+
+ /**
+ * Sets the entity type manager.
+ *
+ * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+ * The entity type manager.
+ *
+ * @return $this
+ */
+ public function setEntityTypeManager(EntityTypeManagerInterface $entity_type_manager) {
+ $this->entityTypeManager = $entity_type_manager;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDerivativeDefinitions($base_plugin_definition) {
+ if (!isset($this->derivatives)) {
+ $plugin_derivatives = [];
+ foreach ($this->getEntityTypeManager()->getDefinitions() as $entity_type => $entity_type_definition) {
+ // We only support content entity types at the moment, since config
+ // entities don't implement \Drupal\Core\TypedData\ComplexDataInterface.
+ if ($entity_type_definition instanceof ContentEntityType) {
+ $plugin_derivatives[$entity_type] = [
+ 'entity_type' => $entity_type,
+ 'label' => $entity_type_definition->getLabel(),
+ 'description' => $this->t('Provides %entity_type entities for indexing and searching.', ['%entity_type' => $entity_type_definition->getLabel()]),
+ ] + $base_plugin_definition;
+ }
+ }
+
+ $this->derivatives = $plugin_derivatives;
+ }
+
+ return $this->derivatives;
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/datasource/ContentEntityTaskManager.php b/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/datasource/ContentEntityTaskManager.php
new file mode 100644
index 000000000..2faea4333
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/datasource/ContentEntityTaskManager.php
@@ -0,0 +1,113 @@
+taskManager = $task_manager;
+ $this->entityTypeManager = $entity_type_manager;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function getSubscribedEvents() {
+ $events['search_api.task.' . static::INSERT_ITEMS_TASK_TYPE][] = ['processEvent'];
+ $events['search_api.task.' . static::DELETE_ITEMS_TASK_TYPE][] = ['processEvent'];
+
+ return $events;
+ }
+
+ /**
+ * Processes a datasource tracking event.
+ *
+ * @param \Drupal\search_api\Task\TaskEvent $event
+ * The task event.
+ * @param string $event_name
+ * The name of the event.
+ */
+ public function processEvent(TaskEvent $event, $event_name) {
+ $event->stopPropagation();
+
+ // The complete event name prefix in front of the method name is 45
+ // characters long: "search_api.task.search_api.entity_datasource.".
+ $method = substr($event_name, 45);
+
+ $task = $event->getTask();
+ $index = $task->getIndex();
+ $data = $task->getData();
+
+ if (!$index->hasValidTracker()) {
+ $args['%index'] = $index->label();
+ $message = new FormattableMarkup('Index %index does not have a valid tracker set.', $args);
+ $event->setException(new SearchApiException($message));
+ return;
+ }
+
+ $datasource_id = $data['datasource'];
+ $reschedule = FALSE;
+ if ($index->isValidDatasource($datasource_id)) {
+ $datasource = $index->getDatasource($datasource_id);
+ if ($datasource instanceof EntityDatasourceInterface) {
+ $raw_ids = $datasource->getPartialItemIds($data['page'], $data['bundles'], $data['languages']);
+ if ($raw_ids !== NULL) {
+ $reschedule = TRUE;
+ if ($raw_ids) {
+ $index->startBatchTracking();
+ $index->$method($datasource_id, $raw_ids);
+ $index->stopBatchTracking();
+ }
+ }
+ }
+ }
+
+ if ($reschedule) {
+ ++$data['page'];
+ $this->taskManager->addTask($task->getType(), NULL, $index, $data);
+ }
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/datasource/EntityDatasourceInterface.php b/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/datasource/EntityDatasourceInterface.php
new file mode 100644
index 000000000..db92c8954
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/datasource/EntityDatasourceInterface.php
@@ -0,0 +1,46 @@
+pluginDefinition['view_id'] . '-' . $this->pluginDefinition['view_display'];
+ $blocks = $this->getEntityTypeManager()
+ ->getStorage('block')
+ ->loadByProperties(['plugin' => $plugin_id]);
+ foreach ($blocks as $block) {
+ if ($block->access('view')) {
+ return TRUE;
+ }
+ }
+
+ return FALSE;
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/display/ViewsDisplayBase.php b/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/display/ViewsDisplayBase.php
new file mode 100644
index 000000000..d8ef7bdb6
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/display/ViewsDisplayBase.php
@@ -0,0 +1,108 @@
+setCurrentRouteMatch($container->get('current_route_match'));
+
+ return $display;
+ }
+
+ /**
+ * Retrieves the current route match service.
+ *
+ * @return \Drupal\Core\Routing\RouteMatchInterface
+ * The current route match service.
+ */
+ public function getCurrentRouteMatch() {
+ return $this->currentRouteMatch ?: \Drupal::routeMatch();
+ }
+
+ /**
+ * Sets the current route match service.
+ *
+ * @param \Drupal\Core\Routing\RouteMatchInterface $current_route_match
+ * The new current route match service.
+ *
+ * @return $this
+ */
+ public function setCurrentRouteMatch(RouteMatchInterface $current_route_match) {
+ $this->currentPath = $current_route_match;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getPath() {
+ $path = parent::getPath();
+
+ // Recreating a link when a contextual filter is used in the display's path
+ // is not possible. So instead we return NULL, which forces most
+ // implementations to use the current request's path instead.
+ if (strpos($path, '%') !== FALSE) {
+ return NULL;
+ }
+
+ return $path;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isRenderedInCurrentRequest() {
+ $plugin_definition = $this->getPluginDefinition();
+ $current_route = $this->getCurrentRouteMatch();
+ $view_id = $current_route->getParameter('view_id');
+ $display_id = $current_route->getParameter('display_id');
+ return $view_id === $plugin_definition['view_id'] &&
+ $display_id === $plugin_definition['view_display'];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function calculateDependencies() {
+ $dependencies = parent::calculateDependencies();
+
+ $view = $this->getView();
+ $dependencies[$view->getConfigDependencyKey()][] = $view->getConfigDependencyName();
+
+ return $dependencies;
+ }
+
+ /**
+ * Retrieves the view this search display is based on.
+ *
+ * @returns \Drupal\views\ViewEntityInterface
+ */
+ protected function getView() {
+ $plugin_definition = $this->getPluginDefinition();
+ return $this->getEntityTypeManager()
+ ->getStorage('view')
+ ->load($plugin_definition['view_id']);
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/display/ViewsDisplayDeriver.php b/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/display/ViewsDisplayDeriver.php
new file mode 100644
index 000000000..8ed5701a5
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/display/ViewsDisplayDeriver.php
@@ -0,0 +1,122 @@
+derivatives)) {
+ $this->derivatives = [];
+
+ try {
+ /** @var \Drupal\Core\Entity\EntityStorageInterface $views_storage */
+ $views_storage = $this->entityTypeManager->getStorage('view');
+ $all_views = $views_storage->loadMultiple();
+ }
+ catch (PluginNotFoundException $e) {
+ return $this->derivatives;
+ }
+
+ /** @var \Drupal\views\Entity\View $view */
+ foreach ($all_views as $view) {
+ $this->derivatives += $this->getDisplaysForView($base_plugin_definition, $view, $this->derivatives);
+ }
+ }
+
+ return $this->derivatives;
+ }
+
+ /**
+ * Creates derived plugin definitions for a view.
+ *
+ * @param array $base_plugin_definition
+ * The plugin definition for this plugin.
+ * @param \Drupal\views\ViewEntityInterface $view
+ * The view to create plugin definitions for.
+ * @param array $plugin_derivatives
+ * An array of already existing derived plugin definitions.
+ *
+ * @return array
+ * Returns an array of plugin definitions, keyed by derivative ID.
+ */
+ protected function getDisplaysForView(array $base_plugin_definition, ViewEntityInterface $view, array $plugin_derivatives) {
+ $type = $base_plugin_definition['views_display_type'];
+
+ $index = SearchApiQuery::getIndexFromTable($view->get('base_table'));
+ if (!$index instanceof IndexInterface) {
+ return [];
+ }
+
+ $displays = $view->get('display');
+ foreach ($displays as $name => $display_info) {
+ if ($display_info['display_plugin'] == $type) {
+ // Create a machine name by getting the view ID and appending the name
+ // of the display to it (block1, rest_export, foobar).
+ $base_machine_name = $view->id() . '__' . $name;
+ $machine_name = $base_machine_name;
+
+ // Make sure the machine name is unique. (Will almost always be
+ // the case, unless a view or page ID contains two consecutive
+ // underscores.)
+ $i = 0;
+ while (isset($plugin_derivatives[$machine_name])) {
+ $machine_name = $base_machine_name . '_' . ++$i;
+ }
+
+ $label_arguments = [
+ '%view_name' => $view->label(),
+ '%display_title' => $display_info['display_title'],
+ ];
+ $label = $this->t('View %view_name, display %display_title', $label_arguments);
+
+ $executable = $view->getExecutable();
+ $executable->setDisplay($name);
+ $display = $executable->getDisplay();
+
+ // Create the actual derivative plugin definition.
+ $args = [
+ '%view_name' => $view->label(),
+ '%display_title' => $display_info['display_title'],
+ ];
+ if ($view->get('description')) {
+ $args['%view_description'] = $view->get('description');
+ $description = $this->t('%view_description – Represents the display %display_title of view %view_name.', $args);
+ }
+ else {
+ $description = $this->t('Represents the display %display_title of view %view_name.', $args);
+ }
+ $plugin_derivatives[$machine_name] = [
+ 'label' => $label,
+ 'description' => $description,
+ 'view_id' => $view->id(),
+ 'view_display' => $name,
+ 'index' => $index->id(),
+ ] + $base_plugin_definition;
+
+ // Add the path information to the definition.
+ if ($display->hasPath()) {
+ $plugin_derivatives[$machine_name]['path'] = '/' . $display->getPath();
+ }
+ }
+ }
+
+ return $plugin_derivatives;
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/display/ViewsEmbed.php b/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/display/ViewsEmbed.php
new file mode 100644
index 000000000..717654b3c
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/display/ViewsEmbed.php
@@ -0,0 +1,14 @@
+ $this->getConjunction(),
+ $keys,
+ ];
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/parse_mode/Terms.php b/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/parse_mode/Terms.php
new file mode 100644
index 000000000..e5ea66e9b
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/parse_mode/Terms.php
@@ -0,0 +1,60 @@
+ $v) {
+ if (!$v) {
+ continue;
+ }
+ if ($quoted) {
+ if (substr($v, -1) == '"') {
+ $v = substr($v, 0, -1);
+ $str .= ' ' . $v;
+ $ret[$k] = $str;
+ $quoted = FALSE;
+ }
+ else {
+ $str .= ' ' . $v;
+ unset($ret[$k]);
+ }
+ }
+ elseif ($v[0] == '"') {
+ $len = strlen($v);
+ if ($len > 1 && $v[$len - 1] == '"') {
+ $ret[$k] = substr($v, 1, -1);
+ }
+ else {
+ $str = substr($v, 1);
+ $quoted = TRUE;
+ unset($ret[$k]);
+ }
+ }
+ }
+ if ($quoted) {
+ $ret[] = $str;
+ }
+ $ret['#conjunction'] = $this->getConjunction();
+ return array_filter($ret);
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/processor/AddHierarchy.php b/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/processor/AddHierarchy.php
new file mode 100644
index 000000000..abd3e2e93
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/processor/AddHierarchy.php
@@ -0,0 +1,317 @@
+setEntityTypeManager($container->get('entity_type.manager'));
+
+ return $processor;
+ }
+
+ /**
+ * Retrieves the entity type manager service.
+ *
+ * @return \Drupal\Core\Entity\EntityTypeManagerInterface
+ * The entity type manager service.
+ */
+ public function getEntityTypeManager() {
+ return $this->entityTypeManager ?: \Drupal::entityTypeManager();
+ }
+
+ /**
+ * Sets the entity type manager service.
+ *
+ * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+ * The entity type manager service.
+ *
+ * @return $this
+ */
+ public function setEntityTypeManager(EntityTypeManagerInterface $entity_type_manager) {
+ $this->entityTypeManager = $entity_type_manager;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function supportsIndex(IndexInterface $index) {
+ $processor = new static(['#index' => $index], 'hierarchy', []);
+ return (bool) $processor->getHierarchyFields();
+ }
+
+ /**
+ * Finds all (potentially) hierarchical fields for this processor's index.
+ *
+ * Fields are returned if:
+ * - they point to an entity type; and
+ * - that entity type contains a property referencing the same type of entity
+ * (so that a hierarchy could be built from that nested property).
+ *
+ * @return string[][]
+ * An array containing all fields of the index for which hierarchical data
+ * might be retrievable. The keys are those field's IDs, the values are
+ * associative arrays containing the nested properties of those fields from
+ * which a hierarchy might be constructed, with the property paths as the
+ * keys and labels as the values.
+ */
+ protected function getHierarchyFields() {
+ if (!isset(static::$indexHierarchyFields[$this->index->id()])) {
+ $field_options = [];
+
+ foreach ($this->index->getFields() as $field_id => $field) {
+ $definition = $field->getDataDefinition();
+ if ($definition instanceof ComplexDataDefinitionInterface) {
+ $properties = $this->getFieldsHelper()
+ ->getNestedProperties($definition);
+ // The property might be an entity data definition itself.
+ $properties[''] = $definition;
+ foreach ($properties as $property) {
+ $property_label = $property->getLabel();
+ $property = $this->getFieldsHelper()->getInnerProperty($property);
+ if ($property instanceof EntityDataDefinitionInterface) {
+ $options = self::findHierarchicalProperties($property, $property_label);
+ if ($options) {
+ $field_options += [$field_id => []];
+ $field_options[$field_id] += $options;
+ }
+ }
+ }
+ }
+ }
+
+ static::$indexHierarchyFields[$this->index->id()] = $field_options;
+ }
+
+ return static::$indexHierarchyFields[$this->index->id()];
+ }
+
+ /**
+ * Finds all hierarchical properties nested on an entity-typed property.
+ *
+ * @param \Drupal\Core\Entity\TypedData\EntityDataDefinitionInterface $property
+ * The property to be searched for hierarchical nested properties.
+ * @param string $property_label
+ * The property's label.
+ *
+ * @return string[]
+ * An options list of hierarchical properties, keyed by the parent
+ * property's entity type ID and the nested properties identifier,
+ * concatenated with a dash (-).
+ */
+ protected function findHierarchicalProperties(EntityDataDefinitionInterface $property, $property_label) {
+ $entity_type_id = $property->getEntityTypeId();
+ $options = [];
+
+ // Check properties for potential hierarchy. Check two levels down, since
+ // Core's entity references all have an additional "entity" sub-property for
+ // accessing the actual entity reference, which we'd otherwise miss.
+ foreach ($this->getFieldsHelper()->getNestedProperties($property) as $name_2 => $property_2) {
+ $property_2_label = $property_2->getLabel();
+ $property_2 = $this->getFieldsHelper()->getInnerProperty($property_2);
+ if ($property_2 instanceof EntityDataDefinitionInterface) {
+ if ($property_2->getEntityTypeId() == $entity_type_id) {
+ $options["$entity_type_id-$name_2"] = Html::escape($property_label . ' » ' . $property_2_label);
+ }
+ }
+ elseif ($property_2 instanceof ComplexDataDefinitionInterface) {
+ foreach ($property_2->getPropertyDefinitions() as $property_3) {
+ $property_3 = $this->getFieldsHelper()->getInnerProperty($property_3);
+ if ($property_3 instanceof EntityDataDefinitionInterface) {
+ if ($property_3->getEntityTypeId() == $entity_type_id) {
+ $options["$entity_type_id-$name_2"] = Html::escape($property_label . ' » ' . $property_2_label);
+ break;
+ }
+ }
+ }
+ }
+ }
+ return $options;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defaultConfiguration() {
+ return [
+ 'fields' => [],
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildConfigurationForm(array $form, FormStateInterface $formState) {
+ $form['#description'] = $this->t('Select the fields to which hierarchical data should be added.');
+
+ foreach ($this->getHierarchyFields() as $field_id => $options) {
+ $enabled = !empty($this->configuration['fields'][$field_id]);
+ $form['fields'][$field_id]['status'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->index->getField($field_id)->getLabel(),
+ '#default_value' => $enabled,
+ ];
+ reset($options);
+ $form['fields'][$field_id]['property'] = [
+ '#type' => 'radios',
+ '#title' => $this->t('Hierarchy property to use'),
+ '#description' => $this->t("This field has several nested properties which look like they might contain hierarchy data for the field. Please pick the one that should be used."),
+ '#options' => $options,
+ '#default_value' => $enabled ? $this->configuration['fields'][$field_id] : key($options),
+ '#access' => count($options) > 1,
+ '#states' => [
+ 'visible' => [
+ // @todo This shouldn't be dependent on the form array structure.
+ // Use the '#process' trick instead.
+ ":input[name=\"processors[hierarchy][settings][fields][$field_id][status]\"]" => [
+ 'checked' => TRUE,
+ ],
+ ],
+ ],
+ ];
+ }
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validateConfigurationForm(array &$form, FormStateInterface $formState) {
+ $fields = [];
+ foreach ($formState->getValue('fields', []) as $field_id => $values) {
+ if (!empty($values['status'])) {
+ if (empty($values['property'])) {
+ $formState->setError($form['fields'][$field_id]['property'], $this->t('You need to select a nested property to use for the hierarchy data.'));
+ }
+ else {
+ $fields[$field_id] = $values['property'];
+ }
+ }
+ }
+ $formState->setValue('fields', $fields);
+ if (!$fields) {
+ $formState->setError($form['fields'], $this->t('You need to select at least one field for which to add hierarchy data.'));
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function preprocessIndexItems(array $items) {
+ /** @var \Drupal\search_api\Item\ItemInterface $item */
+ foreach ($items as $item) {
+ foreach ($this->configuration['fields'] as $field_id => $property_specifier) {
+ $field = $item->getField($field_id);
+ if (!$field) {
+ continue;
+ }
+ list ($entity_type_id, $property) = explode('-', $property_specifier);
+ foreach ($field->getValues() as $entity_id) {
+ $this->addHierarchyValues($entity_type_id, $entity_id, $property, $field);
+ }
+ }
+ }
+ }
+
+ /**
+ * Adds all ancestors' IDs of the given entity to the given field.
+ *
+ * @param string $entityTypeId
+ * The entity type ID.
+ * @param mixed $entityId
+ * The ID of the entity for which ancestors should be found.
+ * @param string $property
+ * The name of the property on the entity type which contains the references
+ * to the parent entities.
+ * @param \Drupal\search_api\Item\FieldInterface $field
+ * The field to which values should be added.
+ */
+ protected function addHierarchyValues($entityTypeId, $entityId, $property, FieldInterface $field) {
+ if ("$entityTypeId-$property" == 'taxonomy_term-parent') {
+ /** @var \Drupal\taxonomy\TermStorageInterface $entity_storage */
+ $entity_storage = $this->getEntityTypeManager()
+ ->getStorage('taxonomy_term');
+ $parents = [];
+ foreach ($entity_storage->loadParents($entityId) as $term) {
+ $parents[] = $term->id();
+ }
+ }
+ else {
+ $entity = $this->getEntityTypeManager()
+ ->getStorage($entityTypeId)
+ ->load($entityId);
+ $parents = [];
+ if ($entity instanceof ContentEntityInterface) {
+ try {
+ foreach ($entity->get($property) as $data) {
+ $values = static::getFieldsHelper()->extractFieldValues($data);
+ $parents = array_merge($parents, $values);
+ }
+ }
+ catch (\InvalidArgumentException $e) {
+ // Might happen, for example, if the property only exists on a certain
+ // bundle, and this entity has the wrong one.
+ }
+ }
+ }
+
+ foreach ($parents as $parent) {
+ if (!in_array($parent, $field->getValues())) {
+ $field->addValue($parent);
+ $this->addHierarchyValues($entityTypeId, $parent, $property, $field);
+ }
+ }
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/processor/AddURL.php b/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/processor/AddURL.php
new file mode 100644
index 000000000..c39bb2ef1
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/processor/AddURL.php
@@ -0,0 +1,60 @@
+ $this->t('URI'),
+ 'description' => $this->t('A URI where the item can be accessed'),
+ 'type' => 'string',
+ 'processor_id' => $this->getPluginId(),
+ ];
+ $properties['search_api_url'] = new ProcessorProperty($definition);
+ }
+
+ return $properties;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addFieldValues(ItemInterface $item) {
+ $url = $item->getDatasource()->getItemUrl($item->getOriginalObject());
+ if ($url) {
+ $url = $url->toString();
+ $fields = $this->getFieldsHelper()
+ ->filterForPropertyPath($item->getFields(), NULL, 'search_api_url');
+ foreach ($fields as $field) {
+ $field->addValue($url);
+ }
+ }
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/processor/AggregatedFields.php b/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/processor/AggregatedFields.php
new file mode 100644
index 000000000..595c6fbea
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/processor/AggregatedFields.php
@@ -0,0 +1,124 @@
+ $this->t('Aggregated field'),
+ 'description' => $this->t('An aggregation of multiple other fields.'),
+ 'type' => 'string',
+ 'processor_id' => $this->getPluginId(),
+ // Most aggregation types are single-valued, but "Union" isn't, and we
+ // can't know which will be picked, so err on the side of caution here.
+ 'is_list' => TRUE,
+ ];
+ $properties['aggregated_field'] = new AggregatedFieldProperty($definition);
+ }
+
+ return $properties;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addFieldValues(ItemInterface $item) {
+ $fields = $this->index->getFields();
+ $aggregated_fields = $this->getFieldsHelper()
+ ->filterForPropertyPath($fields, NULL, 'aggregated_field');
+ $required_properties_by_datasource = [
+ NULL => [],
+ $item->getDatasourceId() => [],
+ ];
+ foreach ($aggregated_fields as $field) {
+ foreach ($field->getConfiguration()['fields'] as $combined_id) {
+ list($datasource_id, $property_path) = Utility::splitCombinedId($combined_id);
+ $required_properties_by_datasource[$datasource_id][$property_path] = $combined_id;
+ }
+ }
+
+ $property_values = $this->getFieldsHelper()
+ ->extractItemValues([$item], $required_properties_by_datasource)[0];
+
+ $aggregated_fields = $this->getFieldsHelper()
+ ->filterForPropertyPath($item->getFields(), NULL, 'aggregated_field');
+ foreach ($aggregated_fields as $aggregated_field) {
+ $values = [];
+ $configuration = $aggregated_field->getConfiguration();
+ foreach ($configuration['fields'] as $combined_id) {
+ if (!empty($property_values[$combined_id])) {
+ $values = array_merge($values, $property_values[$combined_id]);
+ }
+ }
+
+ switch ($configuration['type']) {
+ case 'concat':
+ $values = [implode("\n\n", $values)];
+ break;
+
+ case 'sum':
+ $values = [array_sum($values)];
+ break;
+
+ case 'count':
+ $values = [count($values)];
+ break;
+
+ case 'max':
+ $values = [max($values)];
+ break;
+
+ case 'min':
+ $values = [min($values)];
+ break;
+
+ case 'first':
+ if ($values) {
+ $values = [reset($values)];
+ }
+ break;
+ case 'last':
+ if ($values) {
+ $values = [end($values)];
+ }
+ break;
+ }
+
+ // Do not use setValues(), since that doesn't preprocess the values
+ // according to their data type.
+ foreach ($values as $value) {
+ $aggregated_field->addValue($value);
+ }
+ }
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/processor/ContentAccess.php b/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/processor/ContentAccess.php
new file mode 100644
index 000000000..18e7f5519
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/processor/ContentAccess.php
@@ -0,0 +1,380 @@
+setLogger($container->get('logger.channel.search_api'));
+ $processor->setDatabase($container->get('database'));
+ $processor->setCurrentUser($container->get('current_user'));
+
+ return $processor;
+ }
+
+ /**
+ * Retrieves the database connection.
+ *
+ * @return \Drupal\Core\Database\Connection
+ * The database connection.
+ */
+ public function getDatabase() {
+ return $this->database ?: \Drupal::database();
+ }
+
+ /**
+ * Sets the database connection.
+ *
+ * @param \Drupal\Core\Database\Connection $database
+ * The new database connection.
+ *
+ * @return $this
+ */
+ public function setDatabase(Connection $database) {
+ $this->database = $database;
+ return $this;
+ }
+
+ /**
+ * Retrieves the current user.
+ *
+ * @return \Drupal\Core\Session\AccountProxyInterface
+ * The current user.
+ */
+ public function getCurrentUser() {
+ return $this->currentUser ?: \Drupal::currentUser();
+ }
+
+ /**
+ * Sets the current user.
+ *
+ * @param \Drupal\Core\Session\AccountProxyInterface $current_user
+ * The current user.
+ *
+ * @return $this
+ */
+ public function setCurrentUser(AccountProxyInterface $current_user) {
+ $this->currentUser = $current_user;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function supportsIndex(IndexInterface $index) {
+ foreach ($index->getDatasources() as $datasource) {
+ if (in_array($datasource->getEntityTypeId(), ['node', 'comment'])) {
+ return TRUE;
+ }
+ }
+ return FALSE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getPropertyDefinitions(DatasourceInterface $datasource = NULL) {
+ $properties = [];
+
+ if (!$datasource) {
+ $definition = [
+ 'label' => $this->t('Node access information'),
+ 'description' => $this->t('Data needed to apply node access.'),
+ 'type' => 'string',
+ 'processor_id' => $this->getPluginId(),
+ 'hidden' => TRUE,
+ 'is_list' => TRUE,
+ ];
+ $properties['search_api_node_grants'] = new ProcessorProperty($definition);
+ }
+
+ return $properties;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addFieldValues(ItemInterface $item) {
+ static $anonymous_user;
+
+ if (!isset($anonymous_user)) {
+ // Load the anonymous user.
+ $anonymous_user = new AnonymousUserSession();
+ }
+
+ // Only run for node and comment items.
+ $entity_type_id = $item->getDatasource()->getEntityTypeId();
+ if (!in_array($entity_type_id, ['node', 'comment'])) {
+ return;
+ }
+
+ // Get the node object.
+ $node = $this->getNode($item->getOriginalObject());
+ if (!$node) {
+ // Apparently we were active for a wrong item.
+ return;
+ }
+
+ $fields = $item->getFields();
+ $fields = $this->getFieldsHelper()
+ ->filterForPropertyPath($fields, NULL, 'search_api_node_grants');
+ foreach ($fields as $field) {
+ // Collect grant information for the node.
+ if (!$node->access('view', $anonymous_user)) {
+ // If anonymous user has no permission we collect all grants with
+ // their realms in the item.
+ $sql = 'SELECT * FROM {node_access} WHERE (nid = 0 OR nid = :nid) AND grant_view = 1';
+ $args = [':nid' => $node->id()];
+ foreach ($this->getDatabase()->query($sql, $args) as $grant) {
+ $field->addValue("node_access_{$grant->realm}:{$grant->gid}");
+ }
+ }
+ else {
+ // Add the generic pseudo view grant if we are not using node access
+ // or the node is viewable by anonymous users.
+ $field->addValue('node_access__all');
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function preIndexSave() {
+ foreach ($this->index->getDatasources() as $datasource_id => $datasource) {
+ $entity_type = $datasource->getEntityTypeId();
+ if (in_array($entity_type, ['node', 'comment'])) {
+ $this->ensureField($datasource_id, 'status', 'boolean');
+ if ($entity_type == 'node') {
+ $this->ensureField($datasource_id, 'uid', 'integer');
+ }
+ }
+ }
+
+ $field = $this->ensureField(NULL, 'search_api_node_grants', 'string');
+ $field->setHidden();
+ }
+
+ /**
+ * Retrieves the node related to an indexed search object.
+ *
+ * Will be either the node itself, or the node the comment is attached to.
+ *
+ * @param \Drupal\Core\TypedData\ComplexDataInterface $item
+ * A search object that is being indexed.
+ *
+ * @return \Drupal\node\NodeInterface|null
+ * The node related to that search object.
+ */
+ protected function getNode(ComplexDataInterface $item) {
+ $item = $item->getValue();
+ if ($item instanceof CommentInterface) {
+ $item = $item->getCommentedEntity();
+ }
+ if ($item instanceof NodeInterface) {
+ return $item;
+ }
+
+ return NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function preprocessSearchQuery(QueryInterface $query) {
+ if (!$query->getOption('search_api_bypass_access')) {
+ $account = $query->getOption('search_api_access_account', $this->getCurrentUser());
+ if (is_numeric($account)) {
+ $account = User::load($account);
+ }
+ if ($account instanceof AccountInterface) {
+ try {
+ $this->addNodeAccess($query, $account);
+ }
+ catch (SearchApiException $e) {
+ $this->logException($e);
+ }
+ }
+ else {
+ $account = $query->getOption('search_api_access_account', $this->getCurrentUser());
+ if ($account instanceof AccountInterface) {
+ $account = $account->id();
+ }
+ if (!is_scalar($account)) {
+ $account = var_export($account, TRUE);
+ }
+ $this->getLogger()->warning('An illegal user UID was given for node access: @uid.', ['@uid' => $account]);
+ }
+ }
+ }
+
+ /**
+ * Adds a node access filter to a search query, if applicable.
+ *
+ * @param \Drupal\search_api\Query\QueryInterface $query
+ * The query to which a node access filter should be added, if applicable.
+ * @param \Drupal\Core\Session\AccountInterface $account
+ * The user for whom the search is executed.
+ *
+ * @throws \Drupal\search_api\SearchApiException
+ * Thrown if not all necessary fields are indexed on the index.
+ */
+ protected function addNodeAccess(QueryInterface $query, AccountInterface $account) {
+ // Don't do anything if the user can access all content.
+ if ($account->hasPermission('bypass node access')) {
+ return;
+ }
+
+ // Gather the affected datasources, grouped by entity type, as well as the
+ // unaffected ones.
+ $affected_datasources = [];
+ $unaffected_datasources = [];
+ foreach ($this->index->getDatasources() as $datasource_id => $datasource) {
+ $entity_type = $datasource->getEntityTypeId();
+ if (in_array($entity_type, ['node', 'comment'])) {
+ $affected_datasources[$entity_type][] = $datasource_id;
+ }
+ else {
+ $unaffected_datasources[] = $datasource_id;
+ }
+ }
+
+ // The filter structure we want looks like this:
+ // [belongs to other datasource]
+ // OR
+ // (
+ // [is enabled (or was created by the user, if applicable)]
+ // AND
+ // [grants view access to one of the user's gid/realm combinations]
+ // )
+ // If there are no "other" datasources, we don't need the nested OR,
+ // however, and can add the inner conditions directly to the query.
+ if ($unaffected_datasources) {
+ $outer_conditions = $query->createConditionGroup('OR', ['content_access']);
+ $query->addConditionGroup($outer_conditions);
+ foreach ($unaffected_datasources as $datasource_id) {
+ $outer_conditions->addCondition('search_api_datasource', $datasource_id);
+ }
+ $access_conditions = $query->createConditionGroup('AND');
+ $outer_conditions->addConditionGroup($access_conditions);
+ }
+ else {
+ $access_conditions = $query;
+ }
+
+ if (!$account->hasPermission('access content')) {
+ unset($affected_datasources['node']);
+ }
+ if (!$account->hasPermission('access comments')) {
+ unset($affected_datasources['comment']);
+ }
+
+ // If the user does not have the permission to see any content at all, deny
+ // access to all items from affected datasources.
+ if (!$affected_datasources) {
+ // If there were "other" datasources, the existing filter will already
+ // remove all results of node or comment datasources. Otherwise, we should
+ // not return any results at all.
+ if (!$unaffected_datasources) {
+ $query->abort($this->t('You have no access to any results in this search.'));
+ }
+ return;
+ }
+
+ // Collect all the required fields that need to be part of the index.
+ $unpublished_own = $account->hasPermission('view own unpublished content');
+
+ $enabled_conditions = $query->createConditionGroup('OR', ['content_access_enabled']);
+ foreach ($affected_datasources as $entity_type => $datasources) {
+ foreach ($datasources as $datasource_id) {
+ // If this is a comment datasource, or users cannot view their own
+ // unpublished nodes, a simple filter on "status" is enough. Otherwise,
+ // it's a bit more complicated.
+ $status_field = $this->findField($datasource_id, 'status', 'boolean');
+ if ($status_field) {
+ $enabled_conditions->addCondition($status_field->getFieldIdentifier(), TRUE);
+ }
+ if ($entity_type == 'node' && $unpublished_own) {
+ $author_field = $this->findField($datasource_id, 'uid', 'integer');
+ if ($author_field) {
+ $enabled_conditions->addCondition($author_field->getFieldIdentifier(), $account->id());
+ }
+ }
+ }
+ }
+ $access_conditions->addConditionGroup($enabled_conditions);
+
+ // Filter by the user's node access grants.
+ $node_grants_field = $this->findField(NULL, 'search_api_node_grants', 'string');
+ if (!$node_grants_field) {
+ return;
+ }
+ $node_grants_field_id = $node_grants_field->getFieldIdentifier();
+ $grants_conditions = $query->createConditionGroup('OR', ['content_access_grants']);
+ $grants = node_access_grants('view', $account);
+ foreach ($grants as $realm => $gids) {
+ foreach ($gids as $gid) {
+ $grants_conditions->addCondition($node_grants_field_id, "node_access_$realm:$gid");
+ }
+ }
+ // Also add items that are accessible for everyone by checking the "access
+ // all" pseudo grant.
+ $grants_conditions->addCondition($node_grants_field_id, 'node_access__all');
+ $access_conditions->addConditionGroup($grants_conditions);
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/processor/EntityStatus.php b/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/processor/EntityStatus.php
new file mode 100644
index 000000000..98627c1b5
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/processor/EntityStatus.php
@@ -0,0 +1,70 @@
+getDatasources() as $datasource) {
+ $entity_type_id = $datasource->getEntityTypeId();
+ if (!$entity_type_id) {
+ continue;
+ }
+ // We support users and any entities that implement
+ // \Drupal\Core\Entity\EntityPublishedInterface.
+ if ($entity_type_id === 'user') {
+ return TRUE;
+ }
+ $entity_type = \Drupal::entityTypeManager()
+ ->getDefinition($entity_type_id);
+ if ($entity_type && $entity_type->entityClassImplements($interface)) {
+ return TRUE;
+ }
+ }
+ return FALSE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function alterIndexedItems(array &$items) {
+ // Annoyingly, this doc comment is needed for PHPStorm. See
+ // http://youtrack.jetbrains.com/issue/WI-23586
+ /** @var \Drupal\search_api\Item\ItemInterface $item */
+ foreach ($items as $item_id => $item) {
+ $object = $item->getOriginalObject()->getValue();
+ $enabled = TRUE;
+ if ($object instanceof EntityPublishedInterface) {
+ $enabled = $object->isPublished();
+ }
+ elseif ($object instanceof UserInterface) {
+ $enabled = $object->isActive();
+ }
+ if (!$enabled) {
+ unset($items[$item_id]);
+ }
+ }
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/processor/Highlight.php b/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/processor/Highlight.php
new file mode 100644
index 000000000..195b7d574
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/processor/Highlight.php
@@ -0,0 +1,631 @@
+dataTypeHelper ?: \Drupal::service('search_api.data_type_helper');
+ }
+
+ /**
+ * Sets the data type helper.
+ *
+ * @param \Drupal\search_api\Utility\DataTypeHelperInterface $data_type_helper
+ * The new data type helper.
+ *
+ * @return $this
+ */
+ public function setDataTypeHelper(DataTypeHelperInterface $data_type_helper) {
+ $this->dataTypeHelper = $data_type_helper;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function preIndexSave() {
+ parent::preIndexSave();
+
+ if (empty($this->configuration['exclude_fields'])) {
+ return;
+ }
+
+ $renames = $this->index->getFieldRenames();
+
+ $selected_fields = array_flip($this->configuration['exclude_fields']);
+ $renames = array_intersect_key($renames, $selected_fields);
+ if ($renames) {
+ $new_fields = array_keys(array_diff_key($selected_fields, $renames));
+ $new_fields = array_merge($new_fields, array_values($renames));
+ $this->configuration['exclude_fields'] = $new_fields;
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defaultConfiguration() {
+ return [
+ 'prefix' => '',
+ 'suffix' => '',
+ 'excerpt' => TRUE,
+ 'excerpt_length' => 256,
+ 'highlight' => 'always',
+ 'highlight_partial' => FALSE,
+ 'exclude_fields' => [],
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+ $parent_name = 'processors[highlight][settings]';
+ if (!empty($form['#parents'])) {
+ $parents = $form['#parents'];
+ $parent_name = $root = array_shift($parents);
+ if ($parents) {
+ $parent_name = $root . '[' . implode('][', $parents) . ']';
+ }
+ }
+
+ $form['highlight'] = [
+ '#type' => 'select',
+ '#title' => $this->t('Highlight returned field data'),
+ '#description' => $this->t('Select whether returned fields should be highlighted.'),
+ '#options' => [
+ 'always' => $this->t('Always'),
+ 'server' => $this->t('If the server returns fields'),
+ 'never' => $this->t('Never'),
+ ],
+ '#default_value' => $this->configuration['highlight'],
+ ];
+ $form['highlight_partial'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Highlight partial matches'),
+ '#description' => $this->t('When enabled, matches in parts of words will be highlighted as well.'),
+ '#default_value' => $this->configuration['highlight_partial'],
+ ];
+ $form['excerpt'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Create excerpt'),
+ '#description' => $this->t('When enabled, an excerpt will be created for searches with keywords, containing all occurrences of keywords in a fulltext field.'),
+ '#default_value' => $this->configuration['excerpt'],
+ ];
+ $form['excerpt_length'] = [
+ '#type' => 'number',
+ '#title' => $this->t('Excerpt length'),
+ '#description' => $this->t('The requested length of the excerpt, in characters'),
+ '#default_value' => $this->configuration['excerpt_length'],
+ '#min' => 50,
+ '#states' => [
+ 'visible' => [
+ ":input[name=\"{$parent_name}[excerpt]\"]" => [
+ 'checked' => TRUE,
+ ],
+ ],
+ ],
+ ];
+ // Exclude certain fulltext fields.
+ $fields = $this->index->getFields();
+ $fulltext_fields = [];
+ foreach ($this->index->getFulltextFields() as $field_id) {
+ $fulltext_fields[$field_id] = $fields[$field_id]->getLabel() . ' (' . $field_id . ')';
+ }
+ $form['exclude_fields'] = [
+ '#type' => 'checkboxes',
+ '#title' => $this->t('Exclude fields from excerpt'),
+ '#description' => $this->t('Exclude certain fulltext fields from being included in the excerpt.'),
+ '#options' => $fulltext_fields,
+ '#default_value' => $this->configuration['exclude_fields'],
+ '#attributes' => ['class' => ['search-api-checkboxes-list']],
+ '#states' => [
+ 'visible' => [
+ ":input[name=\"{$parent_name}[excerpt]\"]" => [
+ 'checked' => TRUE,
+ ],
+ ],
+ ],
+ ];
+ $form['prefix'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Highlighting prefix'),
+ '#description' => $this->t('Text/HTML that will be prepended to all occurrences of search keywords in highlighted text'),
+ '#default_value' => $this->configuration['prefix'],
+ ];
+ $form['suffix'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Highlighting suffix'),
+ '#description' => $this->t('Text/HTML that will be appended to all occurrences of search keywords in highlighted text'),
+ '#default_value' => $this->configuration['suffix'],
+ ];
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
+ // Sanitize the storage for the "exclude_fields" setting.
+ $excluded = &$form_state->getValue('exclude_fields');
+ $excluded = array_keys(array_filter($excluded));
+ $this->setConfiguration($form_state->getValues());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function postprocessSearchResults(ResultSetInterface $results) {
+ $query = $results->getQuery();
+ if (!$results->getResultCount()
+ || $query->getProcessingLevel() != QueryInterface::PROCESSING_FULL
+ || !($keys = $this->getKeywords($query))) {
+ return;
+ }
+
+ $excerpt_fulltext_fields = $this->index->getFulltextFields();
+ if (!empty($this->configuration['exclude_fields'])) {
+ $excerpt_fulltext_fields = array_combine($excerpt_fulltext_fields, $excerpt_fulltext_fields);
+ foreach ($this->configuration['exclude_fields'] as $field) {
+ unset($excerpt_fulltext_fields[$field]);
+ }
+ }
+
+ $result_items = $results->getResultItems();
+ if ($this->configuration['excerpt']) {
+ $this->addExcerpts($result_items, $excerpt_fulltext_fields, $keys);
+ }
+ if ($this->configuration['highlight'] != 'never') {
+ $highlighted_fields = $this->highlightFields($result_items, $keys);
+ foreach ($highlighted_fields as $item_id => $item_fields) {
+ $item = $result_items[$item_id];
+ // Maybe the backend or some other processor has already set highlighted
+ // field values.
+ $item_fields += $item->getExtraData('highlighted_fields', []);
+ $item->setExtraData('highlighted_fields', $item_fields);
+ }
+ }
+ }
+
+ /**
+ * Adds excerpts to all results, if possible.
+ *
+ * @param \Drupal\search_api\Item\ItemInterface[] $results
+ * The result items to which excerpts should be added.
+ * @param string[] $fulltext_fields
+ * The fulltext fields from which the excerpt should be created.
+ * @param array $keys
+ * The search keys to use for highlighting.
+ */
+ protected function addExcerpts(array $results, array $fulltext_fields, array $keys) {
+ $items = $this->getFulltextFields($results, $fulltext_fields);
+ foreach ($items as $item_id => $item) {
+ $text = [];
+ foreach ($item as $values) {
+ $text = array_merge($text, $values);
+ }
+ // @todo This is pretty poor handling for the borders between different
+ // values/fields. Better would be to pass an array and have proper
+ // handling of this in createExcerpt(), ensuring that no snippet goes
+ // across multiple values/fields.
+ $results[$item_id]->setExcerpt($this->createExcerpt(implode($this->getEllipses()[1], $text), $keys));
+ }
+ }
+
+ /**
+ * Retrieves highlighted field values for the given result items.
+ *
+ * @param \Drupal\search_api\Item\ItemInterface[] $results
+ * The result items whose fields should be highlighted.
+ * @param array $keys
+ * The search keys to use for highlighting.
+ *
+ * @return string[][][]
+ * An array keyed by item IDs, containing arrays that map field IDs to the
+ * highlighted versions of the values for that field.
+ */
+ protected function highlightFields(array $results, array $keys) {
+ $item_fields = $this->getFulltextFields($results, NULL, $this->configuration['highlight'] == 'always');
+ $highlighted_fields = [];
+ foreach ($item_fields as $item_id => $fields) {
+ foreach ($fields as $field_id => $values) {
+ $change = FALSE;
+ foreach ($values as $i => $value) {
+ $values[$i] = $this->highlightField($value, $keys);
+ if ($values[$i] !== $value) {
+ $change = TRUE;
+ }
+ }
+ if ($change) {
+ $highlighted_fields[$item_id][$field_id] = $values;
+ }
+ }
+ }
+ return $highlighted_fields;
+ }
+
+ /**
+ * Retrieves the fulltext fields of the given result items.
+ *
+ * @param \Drupal\search_api\Item\ItemInterface[] $result_items
+ * The results for which fulltext data should be extracted, keyed by item
+ * ID.
+ * @param string[]|null $fulltext_fields
+ * (optional) The fulltext fields to highlight, or NULL to highlight all
+ * fulltext fields.
+ * @param bool $load
+ * (optional) If FALSE, only field values already present will be returned.
+ * Otherwise, fields will be loaded if necessary.
+ *
+ * @return mixed[][][]
+ * Field values extracted from the result items' fulltext fields, keyed by
+ * item ID, field ID and then numeric indices.
+ */
+ protected function getFulltextFields(array $result_items, array $fulltext_fields = NULL, $load = TRUE) {
+ // All the index's fulltext fields, grouped by datasource.
+ $fields_by_datasource = [];
+ foreach ($this->index->getFields() as $field_id => $field) {
+ if (isset($fulltext_fields) && !in_array($field_id, $fulltext_fields)) {
+ continue;
+ }
+ if ($this->getDataTypeHelper()->isTextType($field->getType())) {
+ $fields_by_datasource[$field->getDatasourceId()][$field->getPropertyPath()] = $field_id;
+ }
+ }
+
+ return $this->getFieldsHelper()
+ ->extractItemValues($result_items, $fields_by_datasource, $load);
+ }
+
+ /**
+ * Extracts the positive keywords used in a search query.
+ *
+ * @param \Drupal\search_api\Query\QueryInterface $query
+ * The query from which to extract the keywords.
+ *
+ * @return string[]
+ * An array of all unique positive keywords used in the query.
+ */
+ protected function getKeywords(QueryInterface $query) {
+ $keys = $query->getOriginalKeys();
+ if (!$keys) {
+ return [];
+ }
+ if (is_array($keys)) {
+ return $this->flattenKeysArray($keys);
+ }
+
+ $keywords_in = preg_split(self::$split, $keys);
+ // Assure there are no duplicates. (This is actually faster than
+ // array_unique() by a factor of 3 to 4.)
+ // Remove quotes from keywords.
+ $keywords = [];
+ foreach (array_filter($keywords_in) as $keyword) {
+ if ($keyword = trim($keyword, "'\"")) {
+ $keywords[$keyword] = $keyword;
+ }
+ }
+ return $keywords;
+ }
+
+ /**
+ * Extracts the positive keywords from a keys array.
+ *
+ * @param array $keys
+ * A search keys array, as specified by
+ * \Drupal\search_api\ParseMode\ParseModeInterface::parseInput().
+ *
+ * @return string[]
+ * An array of all unique positive keywords contained in the keys array.
+ */
+ protected function flattenKeysArray(array $keys) {
+ if (!empty($keys['#negation'])) {
+ return [];
+ }
+
+ $keywords = [];
+ foreach ($keys as $i => $key) {
+ if (!Element::child($i)) {
+ continue;
+ }
+ if (is_array($key)) {
+ $keywords += $this->flattenKeysArray($key);
+ }
+ else {
+ $keywords[$key] = $key;
+ }
+ }
+
+ return $keywords;
+ }
+
+ /**
+ * Returns snippets from a piece of text, with certain keywords highlighted.
+ *
+ * Largely copied from search_excerpt().
+ *
+ * @param string $text
+ * The text to extract fragments from.
+ * @param array $keys
+ * The search keywords entered by the user.
+ *
+ * @return string|null
+ * A string containing HTML for the excerpt. Or NULL if no excerpt could be
+ * created.
+ */
+ protected function createExcerpt($text, array $keys) {
+ // Prepare text by stripping HTML tags and decoding HTML entities.
+ $text = strip_tags(str_replace(['<', '>'], [' <', '> '], $text));
+ $text = Html::decodeEntities($text);
+ $text = preg_replace('/\s+/', ' ', $text);
+ $text = trim($text, ' ');
+ $text_length = strlen($text);
+
+ // Try to reach the requested excerpt length with about two fragments (each
+ // with a keyword and some context).
+ $ranges = [];
+ $length = 0;
+ $look_start = [];
+ $remaining_keys = $keys;
+
+ // Get the set excerpt length from the configuration. If the length is too
+ // small, only use one fragment.
+ $excerpt_length = $this->configuration['excerpt_length'];
+ $context_length = round($excerpt_length / 4) - 3;
+ if ($context_length < 32) {
+ $context_length = round($excerpt_length / 2) - 1;
+ }
+
+ while ($length < $excerpt_length && !empty($remaining_keys)) {
+ $found_keys = [];
+ foreach ($remaining_keys as $key) {
+ if ($length >= $excerpt_length) {
+ break;
+ }
+
+ // Remember where we last found $key, in case we are coming through a
+ // second time.
+ if (!isset($look_start[$key])) {
+ $look_start[$key] = 0;
+ }
+
+ // See if we can find $key after where we found it the last time. Since
+ // we are requiring a match on a word boundary, make sure $text starts
+ // and ends with a space.
+ $matches = [];
+
+ if (!$this->configuration['highlight_partial']) {
+ $found_position = FALSE;
+ $regex = '/' . self::$boundary . preg_quote($key, '/') . self::$boundary . '/iu';
+ if (preg_match($regex, ' ' . $text . ' ', $matches, PREG_OFFSET_CAPTURE, $look_start[$key])) {
+ $found_position = $matches[0][1];
+ }
+ }
+ elseif (function_exists('mb_stripos')) {
+ $found_position = mb_stripos($text, $key, $look_start[$key], 'UTF-8');
+ }
+ else {
+ $found_position = stripos($text, $key, $look_start[$key]);
+ }
+ if ($found_position !== FALSE) {
+ $look_start[$key] = $found_position + 1;
+ // Keep track of which keys we found this time, in case we need to
+ // pass through again to find more text.
+ $found_keys[] = $key;
+
+ // Locate a space before and after this match, leaving some context on
+ // each end.
+ if ($found_position > $context_length) {
+ $before = strpos($text, ' ', $found_position - $context_length);
+ if ($before !== FALSE) {
+ ++$before;
+ }
+ }
+ else {
+ $before = 0;
+ }
+ if ($before !== FALSE && $before <= $found_position) {
+ if ($text_length > $found_position + $context_length) {
+ $after = strrpos(substr($text, 0, $found_position + $context_length), ' ', $found_position);
+ }
+ else {
+ $after = $text_length;
+ }
+ if ($after !== FALSE && $after > $found_position) {
+ if ($before < $after) {
+ // Save this range.
+ $ranges[$before] = $after;
+ $length += $after - $before;
+ }
+ }
+ }
+ }
+ }
+ // Next time through this loop, only look for keys we found this time,
+ // if any.
+ $remaining_keys = $found_keys;
+ }
+
+ if (!$ranges) {
+ // We didn't find any keyword matches, return NULL.
+ return NULL;
+ }
+
+ // Sort the text ranges by starting position.
+ ksort($ranges);
+
+ // Collapse overlapping text ranges into one. The sorting makes it O(n).
+ $new_ranges = [];
+ $max_end = 0;
+ $working_from = $working_to = NULL;
+ foreach ($ranges as $this_from => $this_to) {
+ $max_end = max($max_end, $this_to);
+ if ($working_from === NULL) {
+ // This is the first time through this loop: initialize.
+ $working_from = $this_from;
+ $working_to = $this_to;
+ continue;
+ }
+ if ($this_from <= $working_to) {
+ // The ranges overlap: combine them.
+ $working_to = max($working_to, $this_to);
+ }
+ else {
+ // The ranges do not overlap: save the working range and start a new
+ // one.
+ $new_ranges[$working_from] = $working_to;
+ $working_from = $this_from;
+ $working_to = $this_to;
+ }
+ }
+ // Save the remaining working range.
+ $new_ranges[$working_from] = $working_to;
+
+ // Fetch text within the combined ranges we found.
+ $out = [];
+ foreach ($new_ranges as $from => $to) {
+ $out[] = Html::escape(substr($text, $from, $to - $from));
+ }
+ if (!$out) {
+ return NULL;
+ }
+
+ $ellipses = $this->getEllipses();
+ $excerpt = $ellipses[0] . implode($ellipses[1], $out) . $ellipses[2];
+
+ // Since we stripped the tags at the beginning, highlighting doesn't need to
+ // handle HTML anymore.
+ return $this->highlightField($excerpt, $keys, FALSE);
+ }
+
+ /**
+ * Marks occurrences of the search keywords in a text field.
+ *
+ * @param string $text
+ * The text of the field.
+ * @param array $keys
+ * The search keywords entered by the user.
+ * @param bool $html
+ * (optional) Whether the text can contain HTML tags or not. In the former
+ * case, text inside tags (that is, tag names and attributes) won't be
+ * highlighted.
+ *
+ * @return string
+ * The given text with all occurrences of search keywords highlighted.
+ */
+ protected function highlightField($text, array $keys, $html = TRUE) {
+ if ($html) {
+ $texts = preg_split('#((?:?[[:alpha:]](?:[^>"\']*|"[^"]*"|\'[^\']\')*>)+)#i', $text, -1, PREG_SPLIT_DELIM_CAPTURE);
+ for ($i = 0; $i < count($texts); $i += 2) {
+ $texts[$i] = $this->highlightField($texts[$i], $keys, FALSE);
+ }
+ return implode('', $texts);
+ }
+ $keys = implode('|', array_map('preg_quote', $keys, array_fill(0, count($keys), '/')));
+ // If "Highlight partial matches" is disabled, we only want to highlight
+ // matches that are complete words. Otherwise, we want all of them.
+ $boundary = !$this->configuration['highlight_partial'] ? self::$boundary : '';
+ $regex = '/' . $boundary . '(?:' . $keys . ')' . $boundary . '/iu';
+ $replace = $this->configuration['prefix'] . '\0' . $this->configuration['suffix'];
+ $text = preg_replace($regex, $replace, ' ' . $text . ' ');
+ return trim($text);
+ }
+
+ /**
+ * Retrieves the translated separators for excerpts.
+ *
+ * Defaults to Unicode ellipses (…) on all positions.
+ *
+ * @return string[]
+ * A numeric array containing three elements: the separator to put at the
+ * front of the excerpt (if that is not the front of the string), the
+ * separator to put in between different portions of the text, and the
+ * separator to append at the end of the excerpt if it doesn't end with the
+ * end of the text.
+ */
+ protected function getEllipses() {
+ // Combine the text chunks with "…" separators. The "…" needs to be
+ // translated. Let translators have the … separator text as one chunk.
+ $ellipses = explode('@excerpt', $this->t('… @excerpt … @excerpt …'));
+ return $ellipses;
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/processor/HtmlFilter.php b/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/processor/HtmlFilter.php
new file mode 100644
index 000000000..1ca77ae2d
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/processor/HtmlFilter.php
@@ -0,0 +1,306 @@
+dataTypeHelper ?: \Drupal::service('search_api.data_type_helper');
+ }
+
+ /**
+ * Sets the data type helper.
+ *
+ * @param \Drupal\search_api\Utility\DataTypeHelperInterface $data_type_helper
+ * The new data type helper.
+ *
+ * @return $this
+ */
+ public function setDataTypeHelper(DataTypeHelperInterface $data_type_helper) {
+ $this->dataTypeHelper = $data_type_helper;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defaultConfiguration() {
+ $configuration = parent::defaultConfiguration();
+
+ $configuration += [
+ 'title' => TRUE,
+ 'alt' => TRUE,
+ 'tags' => [
+ 'h1' => 5,
+ 'h2' => 3,
+ 'h3' => 2,
+ 'strong' => 2,
+ 'b' => 2,
+ 'em' => 1.5,
+ 'u' => 1.5,
+ ],
+ ];
+
+ return $configuration;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+ $form = parent::buildConfigurationForm($form, $form_state);
+
+ $form['title'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Index title attribute'),
+ '#description' => $this->t('If set, the contents of title attributes will be indexed.'),
+ '#default_value' => $this->configuration['title'],
+ ];
+
+ $form['alt'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Index alt attribute'),
+ '#description' => $this->t('If set, the alternative text of images will be indexed.'),
+ '#default_value' => $this->configuration['alt'],
+ ];
+
+ $dumper = new Dumper();
+ $tags = $dumper->dump($this->configuration['tags'], 2);
+ $tags = str_replace('\r\n', "\n", $tags);
+ $tags = str_replace('"', '', $tags);
+
+ $t_args[':url'] = Url::fromUri('https://en.wikipedia.org/wiki/YAML')->toString();
+ $form['tags'] = [
+ '#type' => 'textarea',
+ '#title' => $this->t('Tag boosts'),
+ '#description' => $this->t('Specify special boost values for certain HTML elements, in YAML file format. The boost values of nested elements are multiplied, elements not mentioned will have the default boost value of 1. Assign a boost of 0 to ignore the text content of that HTML element.', $t_args),
+ '#default_value' => $tags,
+ ];
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
+ parent::validateConfigurationForm($form, $form_state);
+
+ $tags = trim($form_state->getValue('tags'));
+ if (!$tags) {
+ $form_state->setValue('tags', []);
+ return;
+ }
+ $errors = [];
+ try {
+ $parser = new Parser();
+ $tags = $parser->parse($tags);
+ if (!is_array($tags)) {
+ $errors[] = $this->t('Tags is not a valid YAML map. See @link for information on how to write correctly formed YAML.', ['@link' => 'http://yaml.org']);
+ $tags = [];
+ }
+ }
+ catch (ParseException $exception) {
+ $errors[] = $this->t('Tags is not a valid YAML map. See @link for information on how to write correctly formed YAML.', ['@link' => 'http://yaml.org']);
+ $tags = [];
+ }
+ foreach ($tags as $key => $value) {
+ $tag = "<$key>";
+ if (is_array($value)) {
+ $errors[] = $this->t("Boost value for tag @tag can't be an array.", ['@tag' => $tag]);
+ }
+ elseif (!is_numeric($value)) {
+ $errors[] = $this->t('Boost value for tag @tag must be numeric.', ['@tag' => $tag]);
+ }
+ elseif ($value < 0) {
+ $errors[] = $this->t('Boost value for tag @tag must be non-negative.', ['@tag' => $tag]);
+ }
+ elseif ($value == 1) {
+ unset($tags[$key]);
+ }
+ else {
+ $tags[$key] = (float) $value;
+ }
+ }
+ $form_state->setValue('tags', $tags);
+ if ($errors) {
+ $message = array_shift($errors);
+ foreach ($errors as $error) {
+ $args = [
+ '@message1' => $message,
+ '@message2' => $error,
+ ];
+ $message = new FormattableMarkup('@message1
@message2', $args);
+ }
+ $form_state->setError($form['tags'], $message);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function processField(FieldInterface $field) {
+ parent::processField($field);
+
+ foreach ($field->getValues() as $value) {
+ if ($value instanceof TextValueInterface) {
+ $value->setProperty('strip_html');
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function processFieldValue(&$value, $type) {
+ // Remove invisible content.
+ $text = preg_replace('@<(applet|audio|canvas|command|embed|iframe|map|menu|noembed|noframes|noscript|script|style|svg|video)[^>]*>.*\1>@siU', ' ', $value);
+ // Let removed tags still delimit words.
+ $is_text_type = $this->getDataTypeHelper()->isTextType($type);
+ if ($is_text_type) {
+ $text = str_replace(['<', '>'], [' <', '> '], $text);
+ if ($this->configuration['title']) {
+ $text = preg_replace('/(<[-a-z_]+[^>]*["\s])title\s*=\s*("([^"]+)"|\'([^\']+)\')([^>]*>)/i', '$1 $5 $3$4 ', $text);
+ }
+ if ($this->configuration['alt']) {
+ $text = preg_replace('/<[-a-z_]+[^>]*["\s]alt\s*=\s*("([^"]+)"|\'([^\']+)\')[^>]*>/i', '
$2$3 ', $text);
+ }
+ }
+ if ($this->configuration['tags'] && $is_text_type) {
+ $text = strip_tags($text, '<' . implode('><', array_keys($this->configuration['tags'])) . '>');
+ $value = $this->parseHtml($text);
+ }
+ else {
+ $text = strip_tags($text);
+ $value = $this->normalizeText(trim($text));
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function process(&$value) {
+ $value = str_replace(['<', '>'], [' <', '> '], $value);
+ $value = strip_tags($value);
+ $value = $this->normalizeText($value);
+ }
+
+ /**
+ * Tokenizes an HTML string according to the HTML elements.
+ *
+ * Assigns boost values to the elements' contents accordingly.
+ *
+ * @param string $text
+ * The HTML string to parse, passed by reference. After the method call, the
+ * variable will contain the portion of the string after the current
+ * element, or an empty string (if there is no current element).
+ * @param string|null $active_tag
+ * (optional) The currently active tag, for which a closing tag has to be
+ * found. Internal use only.
+ * @param float $boost
+ * (optional) The currently active boost value. Internal use only.
+ *
+ * @return \Drupal\search_api\Plugin\search_api\data_type\value\TextTokenInterface[]
+ * Tokenized text with appropriate scores.
+ */
+ protected function parseHtml(&$text, $active_tag = NULL, $boost = 1.0) {
+ $ret = [];
+ while (($pos = strpos($text, '<')) !== FALSE) {
+ $text_before = substr($text, 0, $pos);
+ $text_after = substr($text, $pos + 1);
+ // Attempt some small error tolerance when literal "<" characters aren't
+ // escaped properly (and are free-standing).
+ if (!preg_match('#^(/?)([-:_a-zA-Z0-9]+)#', $text_after, $m)) {
+ $text = $text_before . '<' . $text_after;
+ continue;
+ }
+ if ($boost && $pos > 0) {
+ $value = $this->normalizeText($text_before);
+ if ($value !== '') {
+ $ret[] = Utility::createTextToken($value, $boost);
+ }
+ }
+ $text = $text_after;
+ $pos = strpos($text, '>');
+ $empty_tag = $text[$pos - 1] == '/';
+ $text = substr($text, $pos + 1);
+ if ($m[1]) {
+ // Closing tag.
+ if ($active_tag && $m[2] == $active_tag) {
+ return $ret;
+ }
+ }
+ elseif (!$empty_tag) {
+ // Opening tag => recursive call.
+ $inner_boost = $boost * (isset($this->configuration['tags'][$m[2]]) ? $this->configuration['tags'][$m[2]] : 1);
+ $ret = array_merge($ret, $this->parseHtml($text, $m[2], $inner_boost));
+ }
+ }
+ if ($text) {
+ $value = $this->normalizeText($text);
+ if ($value !== '') {
+ $ret[] = Utility::createTextToken($value, $boost);
+ }
+ $text = '';
+ }
+ return $ret;
+ }
+
+ /**
+ * Removes superfluous whitespace and unescapes HTML entities.
+ *
+ * @param string $value
+ * The text to process.
+ *
+ * @return string
+ * The text without unnecessary whitespace and HTML entities transformed
+ * back to plain text.
+ */
+ protected function normalizeText($value) {
+ $value = Html::decodeEntities($value);
+ $value = trim($value);
+ $value = preg_replace('/\s+/', ' ', $value);
+ return $value;
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/processor/IgnoreCase.php b/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/processor/IgnoreCase.php
new file mode 100644
index 000000000..013d3d086
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/processor/IgnoreCase.php
@@ -0,0 +1,49 @@
+getValues() as $value) {
+ if ($value instanceof TextValueInterface) {
+ $value->setProperty('lowercase');
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function process(&$value) {
+ // We don't touch integers, NULL values or the like.
+ if (is_string($value)) {
+ $value = Unicode::strtolower($value);
+ }
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/processor/IgnoreCharacters.php b/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/processor/IgnoreCharacters.php
new file mode 100644
index 000000000..8cc4e1c7c
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/processor/IgnoreCharacters.php
@@ -0,0 +1,180 @@
+ "['¿¡!?,.:;]",
+ 'strip' => [
+ 'character_sets' => [
+ 'Pc' => 'Pc',
+ 'Pd' => 'Pd',
+ 'Pe' => 'Pe',
+ 'Pf' => 'Pf',
+ 'Pi' => 'Pi',
+ 'Po' => 'Po',
+ 'Ps' => 'Ps',
+ ],
+ ],
+ ];
+
+ return $configuration;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+ $form = parent::buildConfigurationForm($form, $form_state);
+
+ $form['ignorable'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Strip by regular expression'),
+ '#description' => $this->t('Specify characters which should be removed from fulltext fields and search strings, as a PCRE regular expression.', [':url' => Url::fromUri('http://php.net/manual/reference.pcre.pattern.syntax.php')->toString()]),
+ '#default_value' => $this->configuration['ignorable'],
+ '#maxlength' => 1000,
+ ];
+
+ $character_sets = $this->getCharacterSets();
+ $form['strip'] = [
+ '#type' => 'details',
+ '#title' => $this->t('Strip by character property'),
+ '#description' => $this->t('Specify Unicode character properties of characters to be ignored.', [':url' => Url::fromUri('http://www.fileformat.info/info/unicode/category/index.htm')->toString()]),
+ '#open' => FALSE,
+ '#maxlength' => 300,
+
+ ];
+ $form['strip']['character_sets'] = [
+ '#type' => 'checkboxes',
+ '#title' => $this->t('Ignored character properties'),
+ '#options' => $character_sets,
+ '#default_value' => $this->configuration['strip']['character_sets'],
+ '#multiple' => TRUE,
+ ];
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
+ parent::validateConfigurationForm($form, $form_state);
+
+ $ignorable = str_replace('/', '\/', $form_state->getValues()['ignorable']);
+ if ($ignorable !== '' && @preg_match('/(' . $ignorable . ')+/u', '') === FALSE) {
+ $el = $form['ignorable'];
+ $form_state->setError($el, $el['#title'] . ': ' . $this->t('The entered text is no valid regular expression.'));
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function process(&$value) {
+ if ($this->configuration['ignorable']) {
+ if (!isset($this->ignorable)) {
+ $this->ignorable = str_replace('/', '\/', $this->configuration['ignorable']);
+ }
+ $value = preg_replace('/' . $this->ignorable . '+/u', '', $value);
+ }
+
+ // Loop over the character sets and strip the characters from the text.
+ foreach ($this->configuration['strip']['character_sets'] as $character_set) {
+ $regex = $this->getFormatRegularExpression($character_set);
+ if ($regex) {
+ $value = preg_replace('/[' . $regex . ']+/u', '', $value);
+ }
+ }
+ }
+
+ /**
+ * Retrieves an options list for available Unicode character properties.
+ *
+ * @return string[]
+ * An options list with all available Unicode character properties.
+ */
+ protected function getCharacterSets() {
+ return [
+ 'Pc' => $this->t('Punctuation, Connector Characters'),
+ 'Pd' => $this->t('Punctuation, Dash Characters'),
+ 'Pe' => $this->t('Punctuation, Close Characters'),
+ 'Pf' => $this->t('Punctuation, Final quote Characters'),
+ 'Pi' => $this->t('Punctuation, Initial quote Characters'),
+ 'Po' => $this->t('Punctuation, Other Characters'),
+ 'Ps' => $this->t('Punctuation, Open Characters'),
+
+ 'Cc' => $this->t('Other, Control Characters'),
+ 'Cf' => $this->t('Other, Format Characters'),
+ 'Co' => $this->t('Other, Private Use Characters'),
+
+ 'Mc' => $this->t('Mark, Spacing Combining Characters'),
+ 'Me' => $this->t('Mark, Enclosing Characters'),
+ 'Mn' => $this->t('Mark, Nonspacing Characters'),
+
+ 'Sc' => $this->t('Symbol, Currency Characters'),
+ 'Sk' => $this->t('Symbol, Modifier Characters'),
+ 'Sm' => $this->t('Symbol, Math Characters'),
+ 'So' => $this->t('Symbol, Other Characters'),
+
+ 'Zl' => $this->t('Separator, Line Characters'),
+ 'Zp' => $this->t('Separator, Paragraph Characters'),
+ 'Zs' => $this->t('Separator, Space Characters'),
+ ];
+ }
+
+ /**
+ * Retrieves a regular expression for a certain Unicode character property.
+ *
+ * @param string $property
+ * The abbreviation of the character property for which to get the regular
+ * expression.
+ *
+ * @return string|null
+ * The regular expression for the property, or NULL if it could not be
+ * found.
+ */
+ protected function getFormatRegularExpression($property) {
+ $class = 'Drupal\search_api\Plugin\search_api\processor\Resources\\' . $property;
+ if (class_exists($class) && in_array('Drupal\search_api\Plugin\search_api\processor\Resources\UnicodeCharacterPropertyInterface', class_implements($class))) {
+ return $class::getRegularExpression();
+ }
+ return NULL;
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/processor/Property/AggregatedFieldProperty.php b/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/processor/Property/AggregatedFieldProperty.php
new file mode 100644
index 000000000..8b5be3063
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/processor/Property/AggregatedFieldProperty.php
@@ -0,0 +1,224 @@
+ 'union',
+ 'fields' => [],
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildConfigurationForm(FieldInterface $field, array $form, FormStateInterface $form_state) {
+ $index = $field->getIndex();
+ $configuration = $field->getConfiguration();
+
+ $form['#attached']['library'][] = 'search_api/drupal.search_api.admin_css';
+ $form['#tree'] = TRUE;
+
+ $form['type'] = [
+ '#type' => 'radios',
+ '#title' => $this->t('Aggregation type'),
+ '#options' => $this->getTypes(),
+ '#default_value' => $configuration['type'],
+ '#required' => TRUE,
+ ];
+
+ foreach ($this->getTypes('description') as $type => $description) {
+ $form['type'][$type]['#description'] = $description;
+ }
+
+ $form['fields'] = [
+ '#type' => 'checkboxes',
+ '#title' => $this->t('Contained fields'),
+ '#options' => [],
+ '#attributes' => ['class' => ['search-api-checkboxes-list']],
+ '#default_value' => $configuration['fields'],
+ '#required' => TRUE,
+ ];
+ $datasource_labels = $this->getDatasourceLabelPrefixes($index);
+ $properties = $this->getAvailableProperties($index);
+ $field_options = [];
+ foreach ($properties as $combined_id => $property) {
+ list($datasource_id, $name) = Utility::splitCombinedId($combined_id);
+ // Do not include the "aggregated field" property.
+ if (!$datasource_id && $name == 'aggregated_field') {
+ continue;
+ }
+ $label = $datasource_labels[$datasource_id] . $property->getLabel();
+ $field_options[$combined_id] = Html::escape($label);
+ if ($property instanceof ConfigurablePropertyInterface) {
+ $description = $property->getFieldDescription($field);
+ }
+ else {
+ $description = $property->getDescription();
+ }
+ $form['fields'][$combined_id] = [
+ '#attributes' => ['title' => $this->t('Machine name: @name', ['@name' => $name])],
+ '#description' => $description,
+ ];
+ }
+ // Set the field options in a way that sorts them first by whether they are
+ // selected (to quickly see which one are included) and second by their
+ // labels.
+ asort($field_options, SORT_NATURAL);
+ $selected = array_flip($configuration['fields']);
+ $form['fields']['#options'] = array_intersect_key($field_options, $selected);
+ $form['fields']['#options'] += array_diff_key($field_options, $selected);
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitConfigurationForm(FieldInterface $field, array &$form, FormStateInterface $form_state) {
+ $values = [
+ 'type' => $form_state->getValue('type'),
+ 'fields' => array_keys(array_filter($form_state->getValue('fields'))),
+ ];
+ $field->setConfiguration($values);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFieldDescription(FieldInterface $field) {
+ $index = $field->getIndex();
+ $available_properties = $this->getAvailableProperties($index);
+ $datasource_label_prefixes = $this->getDatasourceLabelPrefixes($index);
+ $configuration = $field->getConfiguration();
+
+ $fields = [];
+ foreach ($configuration['fields'] as $combined_id) {
+ list($datasource_id, $property_path) = Utility::splitCombinedId($combined_id);
+ $label = $property_path;
+ if (isset($available_properties[$combined_id])) {
+ $label = $available_properties[$combined_id]->getLabel();
+ }
+ $fields[] = $datasource_label_prefixes[$datasource_id] . $label;
+ }
+ $type = $this->getTypes()[$configuration['type']];
+
+ $arguments = ['@type' => $type, '@fields' => implode(', ', $fields)];
+
+ return $this->t('A @type aggregation of the following fields: @fields.', $arguments);
+ }
+
+ /**
+ * Retrieves information about available aggregation types.
+ *
+ * @param string $info
+ * (optional) One of "label" or "description", to indicate what values
+ * should be returned for the types.
+ *
+ * @return array
+ * An array of the identifiers of the available types mapped to, depending
+ * on $info, their labels, their data types or their descriptions.
+ */
+ protected function getTypes($info = 'label') {
+ switch ($info) {
+ case 'label':
+ return [
+ 'union' => $this->t('Union'),
+ 'concat' => $this->t('Concatenation'),
+ 'sum' => $this->t('Sum'),
+ 'count' => $this->t('Count'),
+ 'max' => $this->t('Maximum'),
+ 'min' => $this->t('Minimum'),
+ 'first' => $this->t('First'),
+ 'last' => $this->t('Last'),
+ ];
+
+ case 'description':
+ return [
+ 'union' => $this->t('The Union aggregation does an union operation of all the values of the field. 2 fields with 2 values each become 1 field with 4 values.'),
+ 'concat' => $this->t('The Concatenation aggregation concatenates the text data of all contained fields.'),
+ 'sum' => $this->t('The Sum aggregation adds the values of all contained fields numerically.'),
+ 'count' => $this->t('The Count aggregation takes the total number of contained field values as the aggregated field value.'),
+ 'max' => $this->t('The Maximum aggregation computes the numerically largest contained field value.'),
+ 'min' => $this->t('The Minimum aggregation computes the numerically smallest contained field value.'),
+ 'first' => $this->t('The First aggregation will simply keep the first encountered field value.'),
+ 'last' => $this->t('The Last aggregation will keep the last encountered field value.'),
+ ];
+
+ }
+ return [];
+ }
+
+ /**
+ * Retrieves label prefixes for an index's datasources.
+ *
+ * @param \Drupal\search_api\IndexInterface $index
+ * The search index.
+ *
+ * @return string[]
+ * An associative array mapping datasource IDs (and an empty string for
+ * datasource-independent properties) to their label prefixes.
+ */
+ protected function getDatasourceLabelPrefixes(IndexInterface $index) {
+ $prefixes = [
+ NULL => $this->t('General') . ' » ',
+ ];
+
+ foreach ($index->getDatasources() as $datasource_id => $datasource) {
+ $prefixes[$datasource_id] = $datasource->label() . ' » ';
+ }
+
+ return $prefixes;
+ }
+
+ /**
+ * Retrieve all properties available on the index.
+ *
+ * The properties will be keyed by combined ID, which is a combination of the
+ * datasource ID and the property path. This is used internally in this class
+ * to easily identify any property on the index.
+ *
+ * @param \Drupal\search_api\IndexInterface $index
+ * The search index.
+ *
+ * @return \Drupal\Core\TypedData\DataDefinitionInterface[]
+ * All the properties available on the index, keyed by combined ID.
+ *
+ * @see \Drupal\search_api\Utility::createCombinedId()
+ */
+ protected function getAvailableProperties(IndexInterface $index) {
+ $properties = [];
+
+ $datasource_ids = $index->getDatasourceIds();
+ $datasource_ids[] = NULL;
+ foreach ($datasource_ids as $datasource_id) {
+ foreach ($index->getPropertyDefinitions($datasource_id) as $property_path => $property) {
+ $properties[Utility::createCombinedId($datasource_id, $property_path)] = $property;
+ }
+ }
+
+ return $properties;
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/processor/Property/RenderedItemProperty.php b/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/processor/Property/RenderedItemProperty.php
new file mode 100644
index 000000000..f6b5f1101
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/processor/Property/RenderedItemProperty.php
@@ -0,0 +1,89 @@
+ [AccountInterface::ANONYMOUS_ROLE],
+ 'view_mode' => [],
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildConfigurationForm(FieldInterface $field, array $form, FormStateInterface $form_state) {
+ $configuration = $field->getConfiguration();
+ $index = $field->getIndex();
+ $form['#tree'] = TRUE;
+
+ $roles = user_role_names();
+ $form['roles'] = [
+ '#type' => 'select',
+ '#title' => $this->t('User roles'),
+ '#description' => $this->t('Your item will be rendered as seen by a user with the selected roles. We recommend to just use "@anonymous" here to prevent data leaking out to unauthorized roles.', ['@anonymous' => $roles[AccountInterface::ANONYMOUS_ROLE]]),
+ '#options' => $roles,
+ '#multiple' => TRUE,
+ '#default_value' => $configuration['roles'],
+ '#required' => TRUE,
+ ];
+
+ $form['view_mode'] = [
+ '#type' => 'item',
+ '#description' => $this->t('You can choose the view modes to use for rendering the items of different datasources and bundles. We recommend using a dedicated view mode (for example, the "Search index" view mode available by default for content) to make sure that only relevant data (especially no field labels) will be included in the index.'),
+ ];
+
+ $options_present = FALSE;
+ foreach ($index->getDatasources() as $datasource_id => $datasource) {
+ $bundles = $datasource->getBundles();
+ foreach ($bundles as $bundle_id => $bundle_label) {
+ $view_modes = $datasource->getViewModes($bundle_id);
+ $view_modes[''] = $this->t("Don't include the rendered item.");
+ if (count($view_modes) > 1) {
+ $form['view_mode'][$datasource_id][$bundle_id] = [
+ '#type' => 'select',
+ '#title' => $this->t('View mode for %datasource » %bundle', ['%datasource' => $datasource->label(), '%bundle' => $bundle_label]),
+ '#options' => $view_modes,
+ ];
+ if (isset($configuration['view_mode'][$datasource_id][$bundle_id])) {
+ $form['view_mode'][$datasource_id][$bundle_id]['#default_value'] = $configuration['view_mode'][$datasource_id][$bundle_id];
+ }
+ $options_present = TRUE;
+ }
+ else {
+ $form['view_mode'][$datasource_id][$bundle_id] = [
+ '#type' => 'value',
+ '#value' => $view_modes ? key($view_modes) : FALSE,
+ ];
+ }
+ }
+ }
+ // If there are no datasources/bundles with more than one view mode, don't
+ // display the description either.
+ if (!$options_present) {
+ unset($form['view_mode']['#type']);
+ unset($form['view_mode']['#description']);
+ }
+
+ return $form;
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/processor/RenderedItem.php b/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/processor/RenderedItem.php
new file mode 100644
index 000000000..58fad9662
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/processor/RenderedItem.php
@@ -0,0 +1,361 @@
+setCurrentUser($container->get('current_user'));
+ $plugin->setRenderer($container->get('renderer'));
+ $plugin->setLogger($container->get('logger.channel.search_api'));
+ $plugin->setThemeManager($container->get('theme.manager'));
+ $plugin->setThemeInitializer($container->get('theme.initialization'));
+ $plugin->setConfigFactory($container->get('config.factory'));
+
+ return $plugin;
+ }
+
+ /**
+ * Retrieves the current user.
+ *
+ * @return \Drupal\Core\Session\AccountProxyInterface
+ * The current user.
+ */
+ public function getCurrentUser() {
+ return $this->currentUser ?: \Drupal::currentUser();
+ }
+
+ /**
+ * Sets the current user.
+ *
+ * @param \Drupal\Core\Session\AccountProxyInterface $current_user
+ * The current user.
+ *
+ * @return $this
+ */
+ public function setCurrentUser(AccountProxyInterface $current_user) {
+ $this->currentUser = $current_user;
+ return $this;
+ }
+
+ /**
+ * Retrieves the renderer.
+ *
+ * @return \Drupal\Core\Render\RendererInterface
+ * The renderer.
+ */
+ public function getRenderer() {
+ return $this->renderer ?: \Drupal::service('renderer');
+ }
+
+ /**
+ * Sets the renderer.
+ *
+ * @param \Drupal\Core\Render\RendererInterface $renderer
+ * The new renderer.
+ *
+ * @return $this
+ */
+ public function setRenderer(RendererInterface $renderer) {
+ $this->renderer = $renderer;
+ return $this;
+ }
+
+ /**
+ * Retrieves the theme manager.
+ *
+ * @return \Drupal\Core\Theme\ThemeManagerInterface
+ * The theme manager.
+ */
+ protected function getThemeManager() {
+ return $this->themeManager ?: \Drupal::theme();
+ }
+
+ /**
+ * Sets the theme manager.
+ *
+ * @param \Drupal\Core\Theme\ThemeManagerInterface $theme_manager
+ * The theme manager.
+ *
+ * @return $this
+ */
+ protected function setThemeManager(ThemeManagerInterface $theme_manager) {
+ $this->themeManager = $theme_manager;
+ return $this;
+ }
+
+ /**
+ * Retrieves the theme initialization service.
+ *
+ * @return \Drupal\Core\Theme\ThemeInitializationInterface
+ * The theme initialization service.
+ */
+ protected function getThemeInitializer() {
+ return $this->themeInitialization ?: \Drupal::service('theme.initialization');
+ }
+
+ /**
+ * Sets the theme initialization service.
+ *
+ * @param \Drupal\Core\Theme\ThemeInitializationInterface $theme_initialization
+ * The theme initialization service.
+ *
+ * @return $this
+ */
+ protected function setThemeInitializer(ThemeInitializationInterface $theme_initialization) {
+ $this->themeInitialization = $theme_initialization;
+ return $this;
+ }
+
+ /**
+ * Retrieves the config factory service.
+ *
+ * @return \Drupal\Core\Config\ConfigFactoryInterface
+ * The config factory.
+ */
+ protected function getConfigFactory() {
+ return $this->configFactory ?: \Drupal::configFactory();
+ }
+
+ /**
+ * Sets the config factory service.
+ *
+ * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+ * The config factory.
+ *
+ * @return $this
+ */
+ protected function setConfigFactory(ConfigFactoryInterface $config_factory) {
+ $this->configFactory = $config_factory;
+ return $this;
+ }
+
+ // @todo Add a supportsIndex() implementation that checks whether there is
+ // actually any datasource present which supports viewing.
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getPropertyDefinitions(DatasourceInterface $datasource = NULL) {
+ $properties = [];
+
+ if (!$datasource) {
+ $definition = [
+ 'label' => $this->t('Rendered HTML output'),
+ 'description' => $this->t('The complete HTML which would be displayed when viewing the item'),
+ 'type' => 'search_api_html',
+ 'processor_id' => $this->getPluginId(),
+ ];
+ $properties['rendered_item'] = new RenderedItemProperty($definition);
+ }
+
+ return $properties;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addFieldValues(ItemInterface $item) {
+ $original_user = $this->currentUser->getAccount();
+
+ // Switch to the default theme in case the admin theme is enabled.
+ $active_theme = $this->getThemeManager()->getActiveTheme();
+ $default_theme = $this->getConfigFactory()
+ ->get('system.theme')
+ ->get('default');
+ $default_theme = $this->getThemeInitializer()
+ ->getActiveThemeByName($default_theme);
+ $this->getThemeManager()->setActiveTheme($default_theme);
+
+ // Count of items that don't have a view mode.
+ $unset_view_modes = 0;
+
+ $fields = $this->getFieldsHelper()
+ ->filterForPropertyPath($item->getFields(), NULL, 'rendered_item');
+ foreach ($fields as $field) {
+ $configuration = $field->getConfiguration();
+
+ // Change the current user to our dummy implementation to ensure we are
+ // using the configured roles.
+ $this->currentUser->setAccount(new UserSession(['roles' => $configuration['roles']]));
+
+ $datasource_id = $item->getDatasourceId();
+ $datasource = $item->getDatasource();
+ $bundle = $datasource->getItemBundle($item->getOriginalObject());
+ // When no view mode has been set for the bundle, or it has been set to
+ // "Don't include the rendered item", skip this item.
+ if (empty($configuration['view_mode'][$datasource_id][$bundle])) {
+ // If it was really not set, also notify the user through the log.
+ if (!isset($configuration['view_mode'][$datasource_id][$bundle])) {
+ ++$unset_view_modes;
+ }
+ continue;
+ }
+ else {
+ $view_mode = (string) $configuration['view_mode'][$datasource_id][$bundle];
+ }
+
+ $build = $datasource->viewItem($item->getOriginalObject(), $view_mode);
+ $value = (string) $this->getRenderer()->renderPlain($build);
+ if ($value) {
+ $field->addValue($value);
+ }
+ }
+
+ // Restore the original user.
+ $this->currentUser->setAccount($original_user);
+ // Restore the original theme.
+ $this->getThemeManager()->setActiveTheme($active_theme);
+
+ if ($unset_view_modes > 0) {
+ $context = [
+ '%index' => $this->index->label(),
+ '%processor' => $this->label(),
+ '@count' => $unset_view_modes,
+ ];
+ $this->getLogger()->warning('Warning: While indexing items on search index %index, @count item(s) did not have a view mode configured for one or more "Rendered item" fields.', $context);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function calculateDependencies() {
+ $this->dependencies = parent::calculateDependencies();
+
+ $fields = $this->getFieldsHelper()
+ ->filterForPropertyPath($this->index->getFields(), NULL, 'rendered_item');
+ foreach ($fields as $field) {
+ $view_modes = $field->getConfiguration()['view_mode'];
+ foreach ($this->index->getDatasources() as $datasource_id => $datasource) {
+ if (($entity_type_id = $datasource->getEntityTypeId()) && !empty($view_modes[$datasource_id])) {
+ foreach ($view_modes[$datasource_id] as $view_mode) {
+ if ($view_mode) {
+ /** @var \Drupal\Core\Entity\EntityViewModeInterface $view_mode_entity */
+ $view_mode_entity = EntityViewMode::load($entity_type_id . '.' . $view_mode);
+ if ($view_mode_entity) {
+ $this->addDependency($view_mode_entity->getConfigDependencyKey(), $view_mode_entity->getConfigDependencyName());
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return $this->dependencies;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function onDependencyRemoval(array $dependencies) {
+ // All dependencies of this processor are entity view modes, so we go
+ // through all of the index's fields using our property and remove the
+ // settings for all datasources or bundles which were set to one of the
+ // removed view modes. This will always result in the removal of all those
+ // dependencies.
+ // The code is highly similar to calculateDependencies(), only that we
+ // remove the setting (if necessary) instead of adding a dependency.
+ $fields = $this->getFieldsHelper()
+ ->filterForPropertyPath($this->index->getFields(), NULL, 'rendered_item');
+ foreach ($fields as $field) {
+ $field_config = $field->getConfiguration();
+ $view_modes = $field_config['view_mode'];
+ foreach ($this->index->getDatasources() as $datasource_id => $datasource) {
+ if (!empty($view_modes[$datasource_id]) && ($entity_type_id = $datasource->getEntityTypeId())) {
+ foreach ($view_modes[$datasource_id] as $bundle => $view_mode_id) {
+ if ($view_mode_id) {
+ /** @var \Drupal\Core\Entity\EntityViewModeInterface $view_mode */
+ $view_mode = EntityViewMode::load($entity_type_id . '.' . $view_mode_id);
+ if ($view_mode) {
+ $dependency_key = $view_mode->getConfigDependencyKey();
+ $dependency_name = $view_mode->getConfigDependencyName();
+ if (!empty($dependencies[$dependency_key][$dependency_name])) {
+ unset($view_modes[$datasource_id][$bundle]);
+ }
+ }
+ }
+ }
+ }
+ }
+ $field_config['view_mode'] = $view_modes;
+ $field->setConfiguration($field_config);
+ }
+
+ return TRUE;
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/processor/Resources/Cc.php b/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/processor/Resources/Cc.php
new file mode 100644
index 000000000..a5f58b191
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/processor/Resources/Cc.php
@@ -0,0 +1,27 @@
+word = $word;
+ $this->exceptions = $custom_exceptions + [
+ 'skis' => 'ski',
+ 'skies' => 'sky',
+ 'dying' => 'die',
+ 'lying' => 'lie',
+ 'tying' => 'tie',
+ 'idly' => 'idl',
+ 'gently' => 'gentl',
+ 'ugly' => 'ugli',
+ 'early' => 'earli',
+ 'only' => 'onli',
+ 'singly' => 'singl',
+ 'sky' => 'sky',
+ 'news' => 'news',
+ 'howe' => 'howe',
+ 'atlas' => 'atlas',
+ 'cosmos' => 'cosmos',
+ 'bias' => 'bias',
+ 'andes' => 'andes',
+ ];
+
+ // Set initial y, or y after a vowel, to Y.
+ $inc = 0;
+ while ($inc <= $this->length()) {
+ if (substr($this->word, $inc, 1) === 'y' && ($inc == 0 || $this->isVowel($inc - 1))) {
+ $this->word = substr_replace($this->word, 'Y', $inc, 1);
+
+ }
+ $inc++;
+ }
+ // Establish the regions R1 and R2. See function R().
+ $this->r1 = $this->R(1);
+ $this->r2 = $this->R(2);
+ }
+
+ /**
+ * Computes the stem of the word.
+ *
+ * @return string
+ * The word's stem.
+ */
+ public function stem() {
+ // Ignore exceptions & words that are two letters or less.
+ if ($this->exceptions() || $this->length() <= 2) {
+ return strtolower($this->word);
+ }
+ else {
+ $this->step0();
+ $this->step1a();
+ $this->step1b();
+ $this->step1c();
+ $this->step2();
+ $this->step3();
+ $this->step4();
+ $this->step5();
+ }
+ return strtolower($this->word);
+ }
+
+ /**
+ * Determines whether the word is contained in our list of exceptions.
+ *
+ * If so, the $word property is changed to the stem listed in the exceptions.
+ *
+ * @return bool
+ * TRUE if the word was an exception, FALSE otherwise.
+ */
+ protected function exceptions() {
+ if (isset($this->exceptions[$this->word])) {
+ $this->word = $this->exceptions[$this->word];
+ return TRUE;
+ }
+ return FALSE;
+ }
+
+ /**
+ * Searches for the longest among the "s" suffixes and removes it.
+ *
+ * Implements step 0 of the Porter2 algorithm.
+ */
+ protected function step0() {
+ $found = FALSE;
+ $checks = ["'s'", "'s", "'"];
+ foreach ($checks as $check) {
+ if (!$found && $this->hasEnding($check)) {
+ $this->removeEnding($check);
+ $found = TRUE;
+ }
+ }
+ }
+
+ /**
+ * Handles various suffixes, of which the longest is replaced.
+ *
+ * Implements step 1a of the Porter2 algorithm.
+ */
+ protected function step1a() {
+ $found = FALSE;
+ if ($this->hasEnding('sses')) {
+ $this->removeEnding('sses');
+ $this->addEnding('ss');
+ $found = TRUE;
+ }
+ $checks = ['ied', 'ies'];
+ foreach ($checks as $check) {
+ if (!$found && $this->hasEnding($check)) {
+ $length = $this->length();
+ $this->removeEnding($check);
+ if ($length > 4) {
+ $this->addEnding('i');
+ }
+ else {
+ $this->addEnding('ie');
+ }
+ $found = TRUE;
+ }
+ }
+ if ($this->hasEnding('us') || $this->hasEnding('ss')) {
+ $found = TRUE;
+ }
+ // Delete if preceding word part has a vowel not immediately before the s.
+ if (!$found && $this->hasEnding('s') && $this->containsVowel(substr($this->word, 0, -2))) {
+ $this->removeEnding('s');
+ }
+ }
+
+ /**
+ * Handles various suffixes, of which the longest is replaced.
+ *
+ * Implements step 1b of the Porter2 algorithm.
+ */
+ protected function step1b() {
+ $exceptions = [
+ 'inning',
+ 'outing',
+ 'canning',
+ 'herring',
+ 'earring',
+ 'proceed',
+ 'exceed',
+ 'succeed',
+ ];
+ if (in_array($this->word, $exceptions)) {
+ return;
+ }
+ $checks = ['eedly', 'eed'];
+ foreach ($checks as $check) {
+ if ($this->hasEnding($check)) {
+ if ($this->r1 !== $this->length()) {
+ $this->removeEnding($check);
+ $this->addEnding('ee');
+ }
+ return;
+ }
+ }
+ $checks = ['ingly', 'edly', 'ing', 'ed'];
+ $second_endings = ['at', 'bl', 'iz'];
+ foreach ($checks as $check) {
+ // If the ending is present and the previous part contains a vowel.
+ if ($this->hasEnding($check) && $this->containsVowel(substr($this->word, 0, -strlen($check)))) {
+ $this->removeEnding($check);
+ foreach ($second_endings as $ending) {
+ if ($this->hasEnding($ending)) {
+ $this->addEnding('e');
+ return;
+ }
+ }
+ // If the word ends with a double, remove the last letter.
+ $found = $this->removeDoubles();
+ // If the word is short, add e (so hop -> hope).
+ if (!$found && ($this->isShort())) {
+ $this->addEnding('e');
+ }
+ return;
+ }
+ }
+ }
+
+ /**
+ * Replaces suffix y or Y with i if after non-vowel not @ word begin.
+ *
+ * Implements step 1c of the Porter2 algorithm.
+ */
+ protected function step1c() {
+ if (($this->hasEnding('y') || $this->hasEnding('Y')) && $this->length() > 2 && !($this->isVowel($this->length() - 2))) {
+ $this->removeEnding('y');
+ $this->addEnding('i');
+ }
+ }
+
+ /**
+ * Implements step 2 of the Porter2 algorithm.
+ */
+ protected function step2() {
+ $checks = [
+ "ization" => "ize",
+ "iveness" => "ive",
+ "fulness" => "ful",
+ "ational" => "ate",
+ "ousness" => "ous",
+ "biliti" => "ble",
+ "tional" => "tion",
+ "lessli" => "less",
+ "fulli" => "ful",
+ "entli" => "ent",
+ "ation" => "ate",
+ "aliti" => "al",
+ "iviti" => "ive",
+ "ousli" => "ous",
+ "alism" => "al",
+ "abli" => "able",
+ "anci" => "ance",
+ "alli" => "al",
+ "izer" => "ize",
+ "enci" => "ence",
+ "ator" => "ate",
+ "bli" => "ble",
+ "ogi" => "og",
+ ];
+ foreach ($checks as $find => $replace) {
+ if ($this->hasEnding($find)) {
+ if ($this->inR1($find)) {
+ $this->removeEnding($find);
+ $this->addEnding($replace);
+ }
+ return;
+ }
+ }
+ if ($this->hasEnding('li')) {
+ if ($this->length() > 4 && $this->validLi($this->charAt(-3))) {
+ $this->removeEnding('li');
+ }
+ }
+ }
+
+ /**
+ * Implements step 3 of the Porter2 algorithm.
+ */
+ protected function step3() {
+ $checks = [
+ 'ational' => 'ate',
+ 'tional' => 'tion',
+ 'alize' => 'al',
+ 'icate' => 'ic',
+ 'iciti' => 'ic',
+ 'ical' => 'ic',
+ 'ness' => '',
+ 'ful' => '',
+ ];
+ foreach ($checks as $find => $replace) {
+ if ($this->hasEnding($find)) {
+ if ($this->inR1($find)) {
+ $this->removeEnding($find);
+ $this->addEnding($replace);
+ }
+ return;
+ }
+ }
+ if ($this->hasEnding('ative')) {
+ if ($this->inR2('ative')) {
+ $this->removeEnding('ative');
+ }
+ }
+ }
+
+ /**
+ * Implements step 4 of the Porter2 algorithm.
+ */
+ protected function step4() {
+ $checks = [
+ 'ement',
+ 'ment',
+ 'ance',
+ 'ence',
+ 'able',
+ 'ible',
+ 'ant',
+ 'ent',
+ 'ion',
+ 'ism',
+ 'ate',
+ 'iti',
+ 'ous',
+ 'ive',
+ 'ize',
+ 'al',
+ 'er',
+ 'ic',
+ ];
+ foreach ($checks as $check) {
+ // Among the suffixes, if found and in R2, delete.
+ if ($this->hasEnding($check)) {
+ if ($this->inR2($check)) {
+ if ($check !== 'ion' || in_array($this->charAt(-4), ['s', 't'])) {
+ $this->removeEnding($check);
+ }
+ }
+ return;
+ }
+ }
+ }
+
+ /**
+ * Implements step 5 of the Porter2 algorithm.
+ */
+ protected function step5() {
+ if ($this->hasEnding('e')) {
+ // Delete if in R2, or in R1 and not preceded by a short syllable.
+ if ($this->inR2('e') || ($this->inR1('e') && !$this->isShortSyllable($this->length() - 3))) {
+ $this->removeEnding('e');
+ }
+ return;
+ }
+ if ($this->hasEnding('l')) {
+ // Delete if in R2 and preceded by l.
+ if ($this->inR2('l') && $this->charAt(-2) == 'l') {
+ $this->removeEnding('l');
+ }
+ }
+ }
+
+ /**
+ * Removes certain double consonants from the word's end.
+ *
+ * @return bool
+ * TRUE if a match was found and removed, FALSE otherwise.
+ */
+ protected function removeDoubles() {
+ $found = FALSE;
+ $doubles = ['bb', 'dd', 'ff', 'gg', 'mm', 'nn', 'pp', 'rr', 'tt'];
+ foreach ($doubles as $double) {
+ if (substr($this->word, -2) == $double) {
+ $this->word = substr($this->word, 0, -1);
+ $found = TRUE;
+ break;
+ }
+ }
+ return $found;
+ }
+
+ /**
+ * Checks whether a character is a vowel.
+ *
+ * @param int $position
+ * The character's position.
+ * @param string|null $word
+ * (optional) The word in which to check. Defaults to $this->word.
+ * @param string[] $additional
+ * (optional) Additional characters that should count as vowels.
+ *
+ * @return bool
+ * TRUE if the character is a vowel, FALSE otherwise.
+ */
+ protected function isVowel($position, $word = NULL, array $additional = []) {
+ if ($word === NULL) {
+ $word = $this->word;
+ }
+ $vowels = array_merge(['a', 'e', 'i', 'o', 'u', 'y'], $additional);
+ return in_array($this->charAt($position, $word), $vowels);
+ }
+
+ /**
+ * Retrieves the character at the given position.
+ *
+ * @param int $position
+ * The 0-based index of the character. If a negative number is given, the
+ * position is counted from the end of the string.
+ * @param string|null $word
+ * (optional) The word from which to retrieve the character. Defaults to
+ * $this->word.
+ *
+ * @return string
+ * The character at the given position, or an empty string if the given
+ * position was illegal.
+ */
+ protected function charAt($position, $word = NULL) {
+ if ($word === NULL) {
+ $word = $this->word;
+ }
+ $length = strlen($word);
+ if (abs($position) >= $length) {
+ return '';
+ }
+ if ($position < 0) {
+ $position += $length;
+ }
+ return $word[$position];
+ }
+
+ /**
+ * Determines whether the word ends in a "vowel-consonant" suffix.
+ *
+ * Unless the word is only two characters long, it also checks that the
+ * third-last character is neither "w", "x" nor "Y".
+ *
+ * @param int|null $position
+ * (optional) If given, do not check the end of the word, but the character
+ * at the given position, and the next one.
+ *
+ * @return bool
+ * TRUE if the word has the described suffix, FALSE otherwise.
+ */
+ protected function isShortSyllable($position = NULL) {
+ if ($position === NULL) {
+ $position = $this->length() - 2;
+ }
+ // A vowel at the beginning of the word followed by a non-vowel.
+ if ($position === 0) {
+ return $this->isVowel(0) && !$this->isVowel(1);
+ }
+ // Vowel followed by non-vowel other than w, x, Y and preceded by
+ // non-vowel.
+ $additional = ['w', 'x', 'Y'];
+ return !$this->isVowel($position - 1) && $this->isVowel($position) && !$this->isVowel($position + 1, NULL, $additional);
+ }
+
+ /**
+ * Determines whether the word is short.
+ *
+ * A word is called short if it ends in a short syllable and if R1 is null.
+ *
+ * @return bool
+ * TRUE if the word is short, FALSE otherwise.
+ */
+ protected function isShort() {
+ return $this->isShortSyllable() && $this->r1 == $this->length();
+ }
+
+ /**
+ * Determines the start of a certain "R" region.
+ *
+ * R is a region after the first non-vowel following a vowel, or end of word.
+ *
+ * @param int $type
+ * (optional) 1 or 2. If 2, then calculate the R after the R1.
+ *
+ * @return int
+ * The R position.
+ */
+ protected function R($type = 1) {
+ $inc = 1;
+ if ($type === 2) {
+ $inc = $this->r1;
+ }
+ elseif ($this->length() > 5) {
+ $prefix_5 = substr($this->word, 0, 5);
+ if ($prefix_5 === 'gener' || $prefix_5 === 'arsen') {
+ return 5;
+ }
+ if ($this->length() > 6 && substr($this->word, 0, 6) === 'commun') {
+ return 6;
+ }
+ }
+
+ while ($inc <= $this->length()) {
+ if (!$this->isVowel($inc) && $this->isVowel($inc - 1)) {
+ $position = $inc;
+ break;
+ }
+ $inc++;
+ }
+ if (!isset($position)) {
+ $position = $this->length();
+ }
+ else {
+ // We add one, as this is the position AFTER the first non-vowel.
+ $position++;
+ }
+ return $position;
+ }
+
+ /**
+ * Checks whether the given string is contained in R1.
+ *
+ * @param string $string
+ * The string.
+ *
+ * @return bool
+ * TRUE if the string is in R1, FALSE otherwise.
+ */
+ protected function inR1($string) {
+ $r1 = substr($this->word, $this->r1);
+ return strpos($r1, $string) !== FALSE;
+ }
+
+ /**
+ * Checks whether the given string is contained in R2.
+ *
+ * @param string $string
+ * The string.
+ *
+ * @return bool
+ * TRUE if the string is in R2, FALSE otherwise.
+ */
+ protected function inR2($string) {
+ $r2 = substr($this->word, $this->r2);
+ return strpos($r2, $string) !== FALSE;
+ }
+
+ /**
+ * Determines the string length of the current word.
+ *
+ * @return int
+ * The string length of the current word.
+ */
+ protected function length() {
+ return strlen($this->word);
+ }
+
+ /**
+ * Checks whether the word ends with the given string.
+ *
+ * @param string $string
+ * The string.
+ *
+ * @return bool
+ * TRUE if the word ends with the given string, FALSE otherwise.
+ */
+ protected function hasEnding($string) {
+ $length = strlen($string);
+ if ($length > $this->length()) {
+ return FALSE;
+ }
+ return (substr_compare($this->word, $string, -1 * $length, $length) === 0);
+ }
+
+ /**
+ * Appends a given string to the current word.
+ *
+ * @param string $string
+ * The ending to append.
+ */
+ protected function addEnding($string) {
+ $this->word = $this->word . $string;
+ }
+
+ /**
+ * Removes a given string from the end of the current word.
+ *
+ * Does not check whether the ending is actually there.
+ *
+ * @param string $string
+ * The ending to remove.
+ */
+ protected function removeEnding($string) {
+ $this->word = substr($this->word, 0, -strlen($string));
+ }
+
+ /**
+ * Checks whether the given string contains a vowel.
+ *
+ * @param string $string
+ * The string to check.
+ *
+ * @return bool
+ * TRUE if the string contains a vowel, FALSE otherwise.
+ */
+ protected function containsVowel($string) {
+ $inc = 0;
+ $return = FALSE;
+ while ($inc < strlen($string)) {
+ if ($this->isVowel($inc, $string)) {
+ $return = TRUE;
+ break;
+ }
+ $inc++;
+ }
+ return $return;
+ }
+
+ /**
+ * Checks whether the given string is a valid -li prefix.
+ *
+ * @param string $string
+ * The string to check.
+ *
+ * @return bool
+ * TRUE if the given string is a valid -li prefix, FALSE otherwise.
+ */
+ protected function validLi($string) {
+ return in_array($string, [
+ 'c',
+ 'd',
+ 'e',
+ 'g',
+ 'h',
+ 'k',
+ 'm',
+ 'n',
+ 'r',
+ 't',
+ ]);
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/processor/Resources/Ps.php b/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/processor/Resources/Ps.php
new file mode 100644
index 000000000..01bbe1a7a
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/processor/Resources/Ps.php
@@ -0,0 +1,28 @@
+getDatasources() as $datasource) {
+ if ($datasource->getEntityTypeId() == 'user') {
+ return TRUE;
+ }
+ }
+ return FALSE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defaultConfiguration() {
+ return [
+ 'default' => TRUE,
+ 'roles' => [],
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+ $options = array_map(function (RoleInterface $role) {
+ return Html::escape($role->label());
+ }, user_roles());
+
+ $form['default'] = [
+ '#type' => 'radios',
+ '#title' => $this->t('Which users should be indexed?'),
+ '#options' => [
+ 1 => $this->t('All but those from one of the selected roles'),
+ 0 => $this->t('Only those from the selected roles'),
+ ],
+ '#default_value' => (int) $this->configuration['default'],
+ ];
+ $form['roles'] = [
+ '#type' => 'select',
+ '#title' => $this->t('Roles'),
+ '#options' => $options,
+ '#multiple' => TRUE,
+ '#size' => min(4, count($options)),
+ '#default_value' => array_combine($this->configuration['roles'], $this->configuration['roles']),
+ ];
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
+ $values = $form_state->getValues();
+ $values['default'] = (bool) $values['default'];
+ $values['roles'] = array_values(array_filter($values['roles']));
+ $form_state->set('values', $values);
+ $this->setConfiguration($values);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function alterIndexedItems(array &$items) {
+ $selected_roles = array_combine($this->configuration['roles'], $this->configuration['roles']);
+ $default = (bool) $this->configuration['default'];
+
+ /** @var \Drupal\search_api\Item\ItemInterface $item */
+ foreach ($items as $item_id => $item) {
+ $account = $item->getOriginalObject()->getValue();
+ if (!($account instanceof UserInterface)) {
+ continue;
+ }
+
+ $account_roles = $account->getRoles();
+ $account_roles = array_flip($account_roles);
+ $has_some_roles = (bool) array_intersect_key($account_roles, $selected_roles);
+
+ // If $default is TRUE, we want to remove those users with at least one of
+ // the selected roles. Otherwise, we want to remove those with none.
+ if ($default == $has_some_roles) {
+ unset($items[$item_id]);
+ }
+ }
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/processor/Stemmer.php b/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/processor/Stemmer.php
new file mode 100644
index 000000000..ee7132fb7
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/processor/Stemmer.php
@@ -0,0 +1,140 @@
+talking to talk). Currently, this only acts on English language content. It uses the Porter 2 stemmer algorithm (More information). For best results, use after tokenizing."),
+ * stages = {
+ * "pre_index_save" = 0,
+ * "preprocess_index" = 0,
+ * "preprocess_query" = 0,
+ * }
+ * )
+ */
+class Stemmer extends FieldsProcessorPluginBase {
+
+ /**
+ * Static cache for already-generated stems.
+ *
+ * @var string[]
+ */
+ protected $stems = [];
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defaultConfiguration() {
+ $configuration = parent::defaultConfiguration();
+
+ $configuration += [
+ 'exceptions' => [
+ 'texan' => 'texa',
+ 'mexican' => 'mexic',
+ ],
+ ];
+
+ return $configuration;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+ $form = parent::buildConfigurationForm($form, $form_state);
+
+ $description = $this->t('If the algorithm does not stem words in your dataset in the desired way, you can enter specific exceptions in the form of WORD=STEM, where "WORD" is the original word in the text and "STEM" is the resulting stem. List each exception on a separate line.');
+
+ // Convert the keyed array into a config format (word=stem)
+ $default_value = http_build_query($this->configuration['exceptions'], NULL, "\n");
+
+ $form['exceptions'] = [
+ '#type' => 'textarea',
+ '#title' => $this->t('Exceptions'),
+ '#description' => $description,
+ '#default_value' => $default_value,
+ ];
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
+ parent::validateConfigurationForm($form, $form_state);
+
+ $exceptions = $form_state->getValue('exceptions');
+ if (($parsed = parse_ini_string($exceptions)) === FALSE) {
+ $el = $form['exceptions'];
+ $form_state->setError($el, $el['#title'] . ': ' . $this->t('The entered text is not in valid WORD=STEM format.'));
+ }
+ else {
+ $form_state->setValue('exceptions', $parsed);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function preprocessIndexItems(array $items) {
+ foreach ($items as $item) {
+ // Limit this processor to English language data.
+ if ($item->getLanguage() !== 'en') {
+ continue;
+ }
+ foreach ($item->getFields() as $name => $field) {
+ if ($this->testField($name, $field)) {
+ $this->processField($field);
+ }
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function preprocessSearchQuery(QueryInterface $query) {
+ // Only process queries that can (also) return English language content.
+ $languages = $query->getLanguages();
+ if ($languages && !in_array('en', $languages)) {
+ return;
+ }
+ parent::preprocessSearchQuery($query);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function testType($type) {
+ return $this->getDataTypeHelper()->isTextType($type);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function process(&$value) {
+ // In the absence of the tokenizer processor, this ensures split words.
+ $words = preg_split('/[^\p{L}\p{N}]+/u', strip_tags($value), -1, PREG_SPLIT_NO_EMPTY);
+ $stemmed = [];
+ foreach ($words as $i => $word) {
+ // To optimize processing, store processed stems in a static array.
+ if (!isset($this->stems[$word])) {
+ $stem = new Porter2($word, $this->configuration['exceptions']);
+ $this->stems[$word] = $stem->stem();
+ }
+ $stemmed[] = $this->stems[$word];
+ }
+ $value = implode(' ', $stemmed);
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/processor/Stopwords.php b/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/processor/Stopwords.php
new file mode 100644
index 000000000..3d9721dfd
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/processor/Stopwords.php
@@ -0,0 +1,148 @@
+Caution: Only use after both 'Ignore case' and 'Tokenizer' have run."),
+ * stages = {
+ * "pre_index_save" = 0,
+ * "preprocess_index" = -5,
+ * "preprocess_query" = -2,
+ * }
+ * )
+ */
+class Stopwords extends FieldsProcessorPluginBase {
+
+ /**
+ * Holds all words ignored for the last query.
+ *
+ * @var string[]
+ */
+ protected $ignored = [];
+
+ /**
+ * An array whose keys and values are the stopwords set for this processor.
+ *
+ * @var string[]
+ */
+ protected $stopwords;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defaultConfiguration() {
+ $configuration = parent::defaultConfiguration();
+
+ $configuration += [
+ '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',
+ ],
+ ];
+
+ return $configuration;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setConfiguration(array $configuration) {
+ parent::setConfiguration($configuration);
+ unset($this->stopwords);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+ $form = parent::buildConfigurationForm($form, $form_state);
+
+ $stopwords = $this->getConfiguration()['stopwords'];
+ if (is_array($stopwords)) {
+ $default_value = implode("\n", $stopwords);
+ }
+ else {
+ $default_value = $stopwords;
+ }
+ $description = $this->t('Enter a list of stopwords, each on a separate line, that will be removed from content before it is indexed and from search terms before searching. More info about stopwords..', [':url' => 'https://en.wikipedia.org/wiki/Stop_words']);
+
+ $form['stopwords'] = [
+ '#type' => 'textarea',
+ '#title' => $this->t('Stopwords'),
+ '#description' => $description,
+ '#default_value' => $default_value,
+ ];
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
+ // Convert our text input to an array.
+ $form_state->setValue('stopwords', array_filter(array_map('trim', explode("\n", $form_state->getValues()['stopwords'])), 'strlen'));
+
+ parent::submitConfigurationForm($form, $form_state);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function preprocessSearchQuery(QueryInterface $query) {
+ $this->ignored = [];
+ parent::preprocessSearchQuery($query);
+
+ $results = $query->getResults();
+ foreach ($this->ignored as $ignored_search_key) {
+ $results->addIgnoredSearchKey($ignored_search_key);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function testType($type) {
+ return $this->getDataTypeHelper()->isTextType($type);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function process(&$value) {
+ $stopwords = $this->getStopWords();
+ if (empty($stopwords) || !is_string($value)) {
+ return;
+ }
+ $value = trim($value);
+ if (isset($stopwords[$value])) {
+ $this->ignored[$value] = $value;
+ $value = '';
+ }
+ }
+
+ /**
+ * Gets the stopwords for this processor.
+ *
+ * @return string[]
+ * An array whose keys and values are the stopwords set for this processor.
+ */
+ protected function getStopWords() {
+ if (!isset($this->stopwords)) {
+ $this->stopwords = array_combine($this->configuration['stopwords'], $this->configuration['stopwords']);
+ }
+ return $this->stopwords;
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/processor/Tokenizer.php b/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/processor/Tokenizer.php
new file mode 100644
index 000000000..394bfa74e
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/processor/Tokenizer.php
@@ -0,0 +1,328 @@
+ '',
+ 'overlap_cjk' => TRUE,
+ 'minimum_word_size' => 3,
+ ];
+
+ return $configuration;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setConfiguration(array $configuration) {
+ parent::setConfiguration($configuration);
+ unset($this->spaces);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+ $form = parent::buildConfigurationForm($form, $form_state);
+
+ $args = [
+ ':pcre-url' => Url::fromUri('https://php.net/manual/regexp.reference.character-classes.php')->toString(),
+ ':doc-url' => Url::fromUri('https://api.drupal.org/api/drupal/core!lib!Drupal!Component!Utility!Unicode.php/constant/Unicode%3A%3APREG_CLASS_WORD_BOUNDARY/8')->toString(),
+ ];
+ $form['spaces'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Whitespace characters'),
+ '#description' => $this->t('Specify the characters that should be regarded as whitespace and therefore used as word-delimiters. Specify the characters as the inside of a PCRE character class. Leave empty to use a default which should be suitable for most languages with a Latin alphabet.', $args),
+ '#default_value' => $this->configuration['spaces'],
+ ];
+
+ $form['overlap_cjk'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Simple CJK handling'),
+ '#default_value' => $this->configuration['overlap_cjk'],
+ '#description' => $this->t('Whether to apply a simple Chinese/Japanese/Korean tokenizer based on overlapping sequences. Does not affect other languages.'),
+ ];
+
+ $form['minimum_word_size'] = [
+ '#type' => 'number',
+ '#title' => $this->t('Minimum word length to index'),
+ '#default_value' => $this->configuration['minimum_word_size'],
+ '#min' => 1,
+ '#max' => 1000,
+ '#description' => $this->t('The number of characters a word has to be to be indexed. A lower setting means better search result ranking, but also a larger database. Each search query must contain at least one keyword that is this size (or longer).'),
+ ];
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
+ parent::validateConfigurationForm($form, $form_state);
+
+ $spaces = str_replace('/', '\/', trim($form_state->getValues()['spaces']));
+ if ($spaces !== '' && @preg_match('/[' . $spaces . ']+/u', '') === FALSE) {
+ $form_state->setError($form['spaces'], $form['spaces']['#title'] . ': ' . $this->t('The entered text is no valid PCRE character class.'));
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function testType($type) {
+ return $this->getDataTypeHelper()->isTextType($type);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function processField(FieldInterface $field) {
+ parent::processField($field);
+
+ foreach ($field->getValues() as $value) {
+ if ($value instanceof TextValueInterface) {
+ $value->setProperty('tokenized');
+ }
+ }
+ }
+
+ /**
+ * Matches all 'N' Unicode character classes (numbers).
+ *
+ * @return string
+ * A string of Unicode characters to use in the regular expression.
+ */
+ protected function getPregClassNumbers() {
+ return '\x{30}-\x{39}\x{b2}\x{b3}\x{b9}\x{bc}-\x{be}\x{660}-\x{669}\x{6f0}-\x{6f9}' .
+ '\x{966}-\x{96f}\x{9e6}-\x{9ef}\x{9f4}-\x{9f9}\x{a66}-\x{a6f}\x{ae6}-\x{aef}' .
+ '\x{b66}-\x{b6f}\x{be7}-\x{bf2}\x{c66}-\x{c6f}\x{ce6}-\x{cef}\x{d66}-\x{d6f}' .
+ '\x{e50}-\x{e59}\x{ed0}-\x{ed9}\x{f20}-\x{f33}\x{1040}-\x{1049}\x{1369}-' .
+ '\x{137c}\x{16ee}-\x{16f0}\x{17e0}-\x{17e9}\x{17f0}-\x{17f9}\x{1810}-\x{1819}' .
+ '\x{1946}-\x{194f}\x{2070}\x{2074}-\x{2079}\x{2080}-\x{2089}\x{2153}-\x{2183}' .
+ '\x{2460}-\x{249b}\x{24ea}-\x{24ff}\x{2776}-\x{2793}\x{3007}\x{3021}-\x{3029}' .
+ '\x{3038}-\x{303a}\x{3192}-\x{3195}\x{3220}-\x{3229}\x{3251}-\x{325f}\x{3280}-' .
+ '\x{3289}\x{32b1}-\x{32bf}\x{ff10}-\x{ff19}';
+ }
+
+ /**
+ * Matches all 'P' Unicode character classes (punctuation).
+ *
+ * @return string
+ * A string of Unicode characters to use in the regular expression.
+ */
+ protected function getPregClassPunctuation() {
+ return '\x{21}-\x{23}\x{25}-\x{2a}\x{2c}-\x{2f}\x{3a}\x{3b}\x{3f}\x{40}\x{5b}-\x{5d}' .
+ '\x{5f}\x{7b}\x{7d}\x{a1}\x{ab}\x{b7}\x{bb}\x{bf}\x{37e}\x{387}\x{55a}-\x{55f}' .
+ '\x{589}\x{58a}\x{5be}\x{5c0}\x{5c3}\x{5f3}\x{5f4}\x{60c}\x{60d}\x{61b}\x{61f}' .
+ '\x{66a}-\x{66d}\x{6d4}\x{700}-\x{70d}\x{964}\x{965}\x{970}\x{df4}\x{e4f}' .
+ '\x{e5a}\x{e5b}\x{f04}-\x{f12}\x{f3a}-\x{f3d}\x{f85}\x{104a}-\x{104f}\x{10fb}' .
+ '\x{1361}-\x{1368}\x{166d}\x{166e}\x{169b}\x{169c}\x{16eb}-\x{16ed}\x{1735}' .
+ '\x{1736}\x{17d4}-\x{17d6}\x{17d8}-\x{17da}\x{1800}-\x{180a}\x{1944}\x{1945}' .
+ '\x{2010}-\x{2027}\x{2030}-\x{2043}\x{2045}-\x{2051}\x{2053}\x{2054}\x{2057}' .
+ '\x{207d}\x{207e}\x{208d}\x{208e}\x{2329}\x{232a}\x{23b4}-\x{23b6}\x{2768}-' .
+ '\x{2775}\x{27e6}-\x{27eb}\x{2983}-\x{2998}\x{29d8}-\x{29db}\x{29fc}\x{29fd}' .
+ '\x{3001}-\x{3003}\x{3008}-\x{3011}\x{3014}-\x{301f}\x{3030}\x{303d}\x{30a0}' .
+ '\x{30fb}\x{fd3e}\x{fd3f}\x{fe30}-\x{fe52}\x{fe54}-\x{fe61}\x{fe63}\x{fe68}' .
+ '\x{fe6a}\x{fe6b}\x{ff01}-\x{ff03}\x{ff05}-\x{ff0a}\x{ff0c}-\x{ff0f}\x{ff1a}' .
+ '\x{ff1b}\x{ff1f}\x{ff20}\x{ff3b}-\x{ff3d}\x{ff3f}\x{ff5b}\x{ff5d}\x{ff5f}-' .
+ '\x{ff65}';
+ }
+
+ /**
+ * Matches CJK (Chinese, Japanese, Korean) letter-like characters.
+ *
+ * This list is derived from the "East Asian Scripts" section of
+ * http://www.unicode.org/charts/index.html, as well as a comment on
+ * http://unicode.org/reports/tr11/tr11-11.html listing some character
+ * ranges that are reserved for additional CJK ideographs.
+ *
+ * The character ranges do not include numbers, punctuation, or symbols, since
+ * these are handled separately in search. Note that radicals and strokes are
+ * considered symbols. (See
+ * http://www.unicode.org/Public/UNIDATA/extracted/DerivedGeneralCategory.txt)
+ *
+ * @return string
+ * A string of Unicode characters to use in the regular expression.
+ *
+ * @see search_expand_cjk()
+ */
+ protected function getPregClassCjk() {
+ return '\x{1100}-\x{11FF}\x{3040}-\x{309F}\x{30A1}-\x{318E}' .
+ '\x{31A0}-\x{31B7}\x{31F0}-\x{31FF}\x{3400}-\x{4DBF}\x{4E00}-\x{9FCF}' .
+ '\x{A000}-\x{A48F}\x{A4D0}-\x{A4FD}\x{A960}-\x{A97F}\x{AC00}-\x{D7FF}' .
+ '\x{F900}-\x{FAFF}\x{FF21}-\x{FF3A}\x{FF41}-\x{FF5A}\x{FF66}-\x{FFDC}' .
+ '\x{20000}-\x{2FFFD}\x{30000}-\x{3FFFD}';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function processFieldValue(&$value, $type) {
+ $this->prepare();
+
+ $text = $this->simplifyText($value);
+ // Split on spaces. The configured (or default) delimiters have been
+ // replaced by those already in simplifyText().
+ $arr = explode(' ', $text);
+
+ $value = [];
+ foreach ($arr as $token) {
+ if (is_numeric($token) || Unicode::strlen($token) >= $this->configuration['minimum_word_size']) {
+ $value[] = Utility::createTextToken($token);
+ }
+ }
+ }
+
+ /**
+ * Simplifies a string according to indexing rules.
+ *
+ * @param string $text
+ * The text to simplify.
+ *
+ * @return string
+ * The text with tokens split by single spaces.
+ *
+ * @see search_simplify()
+ */
+ protected function simplifyText($text) {
+ // Optionally apply simple CJK handling to the text.
+ if ($this->configuration['overlap_cjk']) {
+ $text = preg_replace_callback('/[' . $this->getPregClassCjk() . ']+/u', [$this, 'expandCjk'], $text);
+ }
+
+ // To improve searching for numerical data such as dates, IP addresses or
+ // version numbers, we consider a group of numerical characters separated
+ // only by punctuation characters to be one piece. This also means, for
+ // example, that searching for "20/03/1984" also returns results with
+ // "20-03-1984" in them.
+ // Readable regular expression: "([number]+)[punctuation]+(?=[number])".
+ $text = preg_replace('/([' . $this->getPregClassNumbers() . ']+)[' . $this->getPregClassPunctuation() . ']+(?=[' . $this->getPregClassNumbers() . '])/u', '\1', $text);
+
+ // Multiple dot and dash groups are word boundaries and replaced with space.
+ // No need to use the Unicode modifier here because 0-127 ASCII characters
+ // can't match higher UTF-8 characters as the leftmost bit of those are 1.
+ $text = preg_replace('/[.-]{2,}/', ' ', $text);
+
+ // The dot, underscore and dash are simply removed. This allows meaningful
+ // search behavior with acronyms and URLs. See Unicode note directly above.
+ $text = preg_replace('/[._-]+/', '', $text);
+
+ // With the exception of the rules above, we consider all punctuation,
+ // marks, spaces, etc, to be a word boundary.
+ $text = preg_replace('/[' . $this->spaces . ']+/u', ' ', $text);
+
+ return trim($text);
+ }
+
+ /**
+ * Splits CJK (Chinese, Japanese, Korean) text into tokens.
+ *
+ * Callback for preg_replace_callback() in simplifyText().
+ *
+ * Normally, searches should match exact words, where a word is defined to be
+ * a sequence of characters delimited by spaces or punctuation. CJK languages
+ * are written in long strings of characters, though, not split up into words.
+ * So in order to allow search matching, we split up CJK text into tokens
+ * consisting of consecutive, overlapping sequences of characters whose length
+ * is equal to the "minimum_word_size" setting. This tokenizing is only done
+ * if the "overlap_cjk" setting is enabled.
+ *
+ * @param array $matches
+ * A PCRE match array, containing the complete match as the only element.
+ *
+ * @return string
+ * Tokenized text, with tokens separated with space characters and starting
+ * and ending with a space.
+ *
+ * @see search_expand_cjk()
+ */
+ protected function expandCjk(array $matches) {
+ $min = $this->configuration['minimum_word_size'];
+ $str = $matches[0];
+ $length = Unicode::strlen($str);
+ // If the text is shorter than the minimum word size, don't tokenize it.
+ if ($length <= $min) {
+ return ' ' . $str . ' ';
+ }
+ $tokens = ' ';
+ // Build a FIFO queue of characters.
+ $chars = [];
+ for ($i = 0; $i < $length; $i++) {
+ // Add the next character off the beginning of the string to the queue.
+ $current = Unicode::substr($str, 0, 1);
+ $str = substr($str, strlen($current));
+ $chars[] = $current;
+ if ($i >= $min - 1) {
+ // Make a token of $min characters, and add it to the token string.
+ $tokens .= implode('', $chars) . ' ';
+ // Shift out the first character in the queue.
+ array_shift($chars);
+ }
+ }
+ return $tokens;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function process(&$value) {
+ // We don't process integers, NULL values or the like.
+ if (is_string($value)) {
+ $this->prepare();
+ $value = trim($this->simplifyText($value));
+ }
+ }
+
+ /**
+ * Prepares the processor by setting the $spaces property.
+ */
+ protected function prepare() {
+ if (!isset($this->spaces)) {
+ if ($this->configuration['spaces'] !== '') {
+ $this->spaces = str_replace('/', '\/', $this->configuration['spaces']);
+ }
+ else {
+ $this->spaces = Unicode::PREG_CLASS_WORD_BOUNDARY;
+ }
+ }
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/processor/Transliteration.php b/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/processor/Transliteration.php
new file mode 100644
index 000000000..95b9cb6b7
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/processor/Transliteration.php
@@ -0,0 +1,143 @@
+setTransliterator($container->get('transliteration'));
+ $processor->setLanguageManager($container->get('language_manager'));
+ $langcode = $processor->getLanguageManager()->getDefaultLanguage()->getId();
+ $processor->setLangcode($langcode);
+
+ return $processor;
+ }
+
+ /**
+ * Retrieves the transliterator.
+ *
+ * @return \Drupal\Component\Transliteration\TransliterationInterface
+ * The transliterator.
+ */
+ public function getTransliterator() {
+ return $this->transliterator ?: \Drupal::service('transliteration');
+ }
+
+ /**
+ * Sets the transliterator.
+ *
+ * @param \Drupal\Component\Transliteration\TransliterationInterface $transliterator
+ * The new transliterator.
+ *
+ * @return $this
+ */
+ public function setTransliterator(TransliterationInterface $transliterator) {
+ $this->transliterator = $transliterator;
+ return $this;
+ }
+
+ /**
+ * Retrieves the language manager.
+ *
+ * @return \Drupal\Core\Language\LanguageManagerInterface
+ * The language manager.
+ */
+ public function getLanguageManager() {
+ return $this->languageManager ?: \Drupal::languageManager();
+ }
+
+ /**
+ * Sets the language manager.
+ *
+ * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
+ * The new language manager.
+ *
+ * @return $this
+ */
+ public function setLanguageManager(LanguageManagerInterface $language_manager) {
+ $this->languageManager = $language_manager;
+ return $this;
+ }
+
+ /**
+ * Retrieves the langcode.
+ *
+ * @return string
+ * The langcode.
+ */
+ public function getLangcode() {
+ return $this->langcode ?: $this->getLanguageManager()
+ ->getDefaultLanguage()
+ ->getId();
+ }
+
+ /**
+ * Sets the langcode.
+ *
+ * @param string $langcode
+ * The new langcode.
+ *
+ * @return $this
+ */
+ public function setLangcode($langcode) {
+ $this->langcode = $langcode;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function process(&$value) {
+ // We don't touch integers, NULL values or the like.
+ if (is_string($value)) {
+ $value = $this->getTransliterator()->transliterate($value, $this->getLangcode());
+ }
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/processor/TypeBoost.php b/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/processor/TypeBoost.php
new file mode 100644
index 000000000..40e09ca20
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/processor/TypeBoost.php
@@ -0,0 +1,153 @@
+ '0.0',
+ '0.1' => '0.1',
+ '0.2' => '0.2',
+ '0.3' => '0.3',
+ '0.5' => '0.5',
+ '0.8' => '0.8',
+ '1.0' => '1.0',
+ '2.0' => '2.0',
+ '3.0' => '3.0',
+ '5.0' => '5.0',
+ '8.0' => '8.0',
+ '13.0' => '13.0',
+ '21.0' => '21.0',
+ ];
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defaultConfiguration() {
+ return [
+ 'boosts' => [],
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildConfigurationForm(array $form, FormStateInterface $formState) {
+ $bundle_boost_options = [
+ '' => $this->t('Use datasource default'),
+ ] + static::$boost_factors;
+
+ foreach ($this->index->getDatasources() as $datasource_id => $datasource) {
+ $datasource_configuration = [];
+ if (isset($this->configuration['boosts'][$datasource_id])) {
+ $datasource_configuration = $this->configuration['boosts'][$datasource_id];
+ }
+ $datasource_configuration += [
+ 'datasource_boost' => 1.0,
+ 'bundle_boosts' => [],
+ ];
+ $datasource_boost = $datasource_configuration['datasource_boost'];
+ $bundle_boosts = $datasource_configuration['bundle_boosts'];
+
+ $form['boosts'][$datasource_id] = [
+ '#type' => 'details',
+ '#title' => $this->t('Boost settings for %datasource', ['%datasource' => $datasource->label()]),
+ '#open' => TRUE,
+ 'datasource_boost' => [
+ '#type' => 'select',
+ '#title' => $this->t('Default boost for items from this datasource'),
+ '#options' => static::$boost_factors,
+ '#default_value' => sprintf('%.1f', $datasource_boost),
+ ],
+ ];
+
+ // Add a boost for every available bundle. Drop the "pseudo-bundle" that
+ // is added when the datasource does not contain any bundles.
+ $bundles = $datasource->getBundles();
+ if (count($bundles) === 1) {
+ // Depending on the datasource, the pseudo-bundle might use the
+ // datasource ID or the entity type ID.
+ unset($bundles[$datasource_id], $bundles[$datasource->getEntityTypeId()]);
+ }
+
+ foreach ($bundles as $bundle => $bundle_label) {
+ $has_value = isset($bundle_boosts[$bundle]);
+ $bundle_boost = $has_value ? $bundle_boosts[$bundle] : '';
+ $form['boosts'][$datasource_id]['bundle_boosts'][$bundle] = [
+ '#type' => 'select',
+ '#title' => $this->t('Boost for the %bundle bundle', ['%bundle' => $bundle_label]),
+ '#options' => $bundle_boost_options,
+ '#default_value' => sprintf('%.1f', $bundle_boost),
+ ];
+ }
+ }
+
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
+ $values = $form_state->getValues();
+ foreach ($this->index->getDatasourceIds() as $datasource_id) {
+ if (!empty($values['boosts'][$datasource_id]['bundle_boosts'])) {
+ foreach ($values['boosts'][$datasource_id]['bundle_boosts'] as $bundle => $boost) {
+ if ($boost === '') {
+ unset($values['boosts'][$datasource_id]['bundle_boosts'][$bundle]);
+ }
+ }
+ if (!$values['boosts'][$datasource_id]['bundle_boosts']) {
+ unset($values['boosts'][$datasource_id]['bundle_boosts']);
+ }
+ }
+ }
+ $form_state->setValues($values);
+ $this->setConfiguration($values);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function preprocessIndexItems(array $items) {
+ $boosts = $this->configuration['boosts'];
+
+ /** @var \Drupal\search_api\Item\ItemInterface $item */
+ foreach ($items as $item) {
+ $datasource_id = $item->getDatasourceId();
+ $bundle = $item->getDatasource()->getItemBundle($item->getOriginalObject());
+
+ $item_boost = (double) isset($boosts[$datasource_id]['datasource_boost']) ? $boosts[$datasource_id]['datasource_boost'] : 1.0;
+ if ($bundle && isset($boosts[$datasource_id]['bundle_boosts'][$bundle])) {
+ $item_boost = (double) $boosts[$datasource_id]['bundle_boosts'][$bundle];
+ }
+
+ $item->setBoost($item_boost);
+ }
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/tracker/Basic.php b/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/tracker/Basic.php
new file mode 100644
index 000000000..f122fbf4e
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Plugin/search_api/tracker/Basic.php
@@ -0,0 +1,439 @@
+setDatabaseConnection($container->get('database'));
+ $tracker->setTimeService($container->get('datetime.time'));
+
+ return $tracker;
+ }
+
+ /**
+ * Retrieves the database connection.
+ *
+ * @return \Drupal\Core\Database\Connection
+ * The database connection used by this plugin.
+ */
+ public function getDatabaseConnection() {
+ return $this->connection ?: \Drupal::database();
+ }
+
+ /**
+ * Sets the database connection.
+ *
+ * @param \Drupal\Core\Database\Connection $connection
+ * The database connection to use.
+ *
+ * @return $this
+ */
+ public function setDatabaseConnection(Connection $connection) {
+ $this->connection = $connection;
+ return $this;
+ }
+
+ /**
+ * Retrieves the time service.
+ *
+ * @return \Drupal\Component\Datetime\TimeInterface
+ * The time service.
+ */
+ public function getTimeService() {
+ return $this->timeService ?: \Drupal::time();
+ }
+
+ /**
+ * Sets the time service.
+ *
+ * @param \Drupal\Component\Datetime\TimeInterface $time_service
+ * The new time service.
+ *
+ * @return $this
+ */
+ public function setTimeService(TimeInterface $time_service) {
+ $this->timeService = $time_service;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defaultConfiguration() {
+ return ['indexing_order' => 'fifo'];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+ $form['indexing_order'] = [
+ '#type' => 'radios',
+ '#title' => $this->t('Indexing order'),
+ '#description' => $this->t('The order in which items will be indexed.'),
+ '#options' => [
+ 'fifo' => $this->t('Index items in the same order in which they were saved'),
+ 'lifo' => $this->t('Index the most recent items first'),
+ ],
+ '#default_value' => $this->configuration['indexing_order'],
+ ];
+
+ return $form;
+ }
+
+ /**
+ * Creates a SELECT statement for this tracker.
+ *
+ * @return \Drupal\Core\Database\Query\SelectInterface
+ * A SELECT statement.
+ */
+ protected function createSelectStatement() {
+ $select = $this->getDatabaseConnection()->select('search_api_item', 'sai');
+ $select->condition('index_id', $this->getIndex()->id());
+ return $select;
+ }
+
+ /**
+ * Creates an INSERT statement for this tracker.
+ *
+ * @return \Drupal\Core\Database\Query\Insert
+ * An INSERT statement.
+ */
+ protected function createInsertStatement() {
+ return $this->getDatabaseConnection()->insert('search_api_item')
+ ->fields(['index_id', 'datasource', 'item_id', 'changed', 'status']);
+ }
+
+ /**
+ * Creates an UPDATE statement for this tracker.
+ *
+ * @return \Drupal\Core\Database\Query\Update
+ * An UPDATE statement.
+ */
+ protected function createUpdateStatement() {
+ return $this->getDatabaseConnection()->update('search_api_item')
+ ->condition('index_id', $this->getIndex()->id());
+ }
+
+ /**
+ * Creates a DELETE statement for this tracker.
+ *
+ * @return \Drupal\Core\Database\Query\Delete
+ * A DELETE Statement.
+ */
+ protected function createDeleteStatement() {
+ return $this->getDatabaseConnection()->delete('search_api_item')
+ ->condition('index_id', $this->getIndex()->id());
+ }
+
+ /**
+ * Creates a SELECT statement which filters on the not indexed items.
+ *
+ * @param string|null $datasource_id
+ * (optional) If specified, only items of the datasource with that ID are
+ * retrieved.
+ *
+ * @return \Drupal\Core\Database\Query\SelectInterface
+ * A SELECT statement.
+ */
+ protected function createRemainingItemsStatement($datasource_id = NULL) {
+ $select = $this->createSelectStatement();
+ $select->fields('sai', ['item_id']);
+ if ($datasource_id) {
+ $select->condition('datasource', $datasource_id);
+ }
+ $select->condition('sai.status', $this::STATUS_NOT_INDEXED, '=');
+ // Use the same direction for both sorts to avoid performance problems.
+ $order = $this->configuration['indexing_order'] === 'lifo' ? 'DESC' : 'ASC';
+ $select->orderBy('sai.changed', $order);
+ // Add a secondary sort on item ID to make the order completely predictable.
+ $select->orderBy('sai.item_id', $order);
+
+ return $select;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function trackItemsInserted(array $ids) {
+ $transaction = $this->getDatabaseConnection()->startTransaction();
+ try {
+ $index_id = $this->getIndex()->id();
+ // Process the IDs in chunks so we don't create an overly large INSERT
+ // statement.
+ foreach (array_chunk($ids, 1000) as $ids_chunk) {
+ // We have to make sure we don't try to insert duplicate items.
+ $select = $this->createSelectStatement()
+ ->fields('sai', ['item_id']);
+ $select->condition('item_id', $ids_chunk, 'IN');
+ $existing = $select
+ ->execute()
+ ->fetchCol();
+ $existing = array_flip($existing);
+
+ $insert = $this->createInsertStatement();
+ foreach ($ids_chunk as $item_id) {
+ if (isset($existing[$item_id])) {
+ continue;
+ }
+ list($datasource_id) = Utility::splitCombinedId($item_id);
+ $insert->values([
+ 'index_id' => $index_id,
+ 'datasource' => $datasource_id,
+ 'item_id' => $item_id,
+ 'changed' => $this->getTimeService()->getRequestTime(),
+ 'status' => $this::STATUS_NOT_INDEXED,
+ ]);
+ }
+ if ($insert->count()) {
+ $insert->execute();
+ }
+ }
+ }
+ catch (\Exception $e) {
+ $this->logException($e);
+ $transaction->rollBack();
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function trackItemsUpdated(array $ids = NULL) {
+ $transaction = $this->getDatabaseConnection()->startTransaction();
+ try {
+ // Process the IDs in chunks so we don't create an overly large UPDATE
+ // statement.
+ $ids_chunks = ($ids !== NULL ? array_chunk($ids, 1000) : [NULL]);
+ foreach ($ids_chunks as $ids_chunk) {
+ $update = $this->createUpdateStatement();
+ $update->fields([
+ 'changed' => $this->getTimeService()->getRequestTime(),
+ 'status' => $this::STATUS_NOT_INDEXED,
+ ]);
+ if ($ids_chunk) {
+ $update->condition('item_id', $ids_chunk, 'IN');
+ }
+ // Update the status of unindexed items only if the item order is LIFO.
+ // (Otherwise, an item that's regularly being updated might never get
+ // indexed.)
+ if ($this->configuration['indexing_order'] === 'fifo') {
+ $update->condition('status', static::STATUS_INDEXED);
+ }
+ $update->execute();
+ }
+ }
+ catch (\Exception $e) {
+ $this->logException($e);
+ $transaction->rollBack();
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function trackAllItemsUpdated($datasource_id = NULL) {
+ $transaction = $this->getDatabaseConnection()->startTransaction();
+ try {
+ $update = $this->createUpdateStatement();
+ $update->fields([
+ 'changed' => $this->getTimeService()->getRequestTime(),
+ 'status' => $this::STATUS_NOT_INDEXED,
+ ]);
+ if ($datasource_id) {
+ $update->condition('datasource', $datasource_id);
+ }
+ $update->execute();
+ }
+ catch (\Exception $e) {
+ $this->logException($e);
+ $transaction->rollBack();
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function trackItemsIndexed(array $ids) {
+ $transaction = $this->getDatabaseConnection()->startTransaction();
+ try {
+ // Process the IDs in chunks so we don't create an overly large UPDATE
+ // statement.
+ $ids_chunks = array_chunk($ids, 1000);
+ foreach ($ids_chunks as $ids_chunk) {
+ $update = $this->createUpdateStatement();
+ $update->fields(['status' => $this::STATUS_INDEXED]);
+ $update->condition('item_id', $ids_chunk, 'IN');
+ $update->execute();
+ }
+ }
+ catch (\Exception $e) {
+ $this->logException($e);
+ $transaction->rollBack();
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function trackItemsDeleted(array $ids = NULL) {
+ $transaction = $this->getDatabaseConnection()->startTransaction();
+ try {
+ // Process the IDs in chunks so we don't create an overly large DELETE
+ // statement.
+ $ids_chunks = ($ids !== NULL ? array_chunk($ids, 1000) : [NULL]);
+ foreach ($ids_chunks as $ids_chunk) {
+ $delete = $this->createDeleteStatement();
+ if ($ids_chunk) {
+ $delete->condition('item_id', $ids_chunk, 'IN');
+ }
+ $delete->execute();
+ }
+ }
+ catch (\Exception $e) {
+ $this->logException($e);
+ $transaction->rollBack();
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function trackAllItemsDeleted($datasource_id = NULL) {
+ $transaction = $this->getDatabaseConnection()->startTransaction();
+ try {
+ $delete = $this->createDeleteStatement();
+ if ($datasource_id) {
+ $delete->condition('datasource', $datasource_id);
+ }
+ $delete->execute();
+ }
+ catch (\Exception $e) {
+ $this->logException($e);
+ $transaction->rollBack();
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getRemainingItems($limit = -1, $datasource_id = NULL) {
+ try {
+ $select = $this->createRemainingItemsStatement($datasource_id);
+ if ($limit >= 0) {
+ $select->range(0, $limit);
+ }
+ return $select->execute()->fetchCol();
+ }
+ catch (\Exception $e) {
+ $this->logException($e);
+ return [];
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getTotalItemsCount($datasource_id = NULL) {
+ try {
+ $select = $this->createSelectStatement();
+ if ($datasource_id) {
+ $select->condition('datasource', $datasource_id);
+ }
+ return (int) $select->countQuery()->execute()->fetchField();
+ }
+ catch (\Exception $e) {
+ $this->logException($e);
+ return 0;
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getIndexedItemsCount($datasource_id = NULL) {
+ try {
+ $select = $this->createSelectStatement();
+ $select->condition('sai.status', $this::STATUS_INDEXED);
+ if ($datasource_id) {
+ $select->condition('datasource', $datasource_id);
+ }
+ return (int) $select->countQuery()->execute()->fetchField();
+ }
+ catch (\Exception $e) {
+ $this->logException($e);
+ return 0;
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getRemainingItemsCount($datasource_id = NULL) {
+ try {
+ $select = $this->createRemainingItemsStatement();
+ if ($datasource_id) {
+ $select->condition('datasource', $datasource_id);
+ }
+ return (int) $select->countQuery()->execute()->fetchField();
+ }
+ catch (\Exception $e) {
+ $this->logException($e);
+ return 0;
+ }
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Plugin/views/EntityFieldRenderer.php b/sites/all/modules/contrib/search/search_api/src/Plugin/views/EntityFieldRenderer.php
new file mode 100644
index 000000000..d886073b0
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Plugin/views/EntityFieldRenderer.php
@@ -0,0 +1,100 @@
+datasourceId;
+ }
+
+ /**
+ * Sets the datasource ID.
+ *
+ * @param string|null $datasource_id
+ * The new datasource ID.
+ *
+ * @return $this
+ */
+ public function setDatasourceId($datasource_id) {
+ $this->datasourceId = $datasource_id;
+ return $this;
+ }
+
+ /**
+ * Determines whether this renderer can handle the given field.
+ *
+ * @param \Drupal\views\Plugin\views\field\FieldHandlerInterface $field
+ * The field for which to check compatibility.
+ *
+ * @return bool
+ * TRUE if this renderer can handle the given field, FALSE otherwise.
+ *
+ * @see EntityFieldRenderer::getRenderableFieldIds()
+ */
+ public function compatibleWithField(FieldHandlerInterface $field) {
+ if ($field instanceof SearchApiEntityField
+ && $field->options['field_rendering']
+ && $field->relationship === $this->relationship
+ && $field->getDatasourceId() === $this->datasourceId
+ && !empty($field->definition['entity_type'])
+ && $field->definition['entity_type'] === $this->getEntityTypeId()) {
+ return TRUE;
+ }
+
+ return FALSE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getEntityTranslationRenderer() {
+ if (!isset($this->entityTranslationRenderer)) {
+ $entity_type = $this->getEntityManager()
+ ->getDefinition($this->getEntityTypeId());
+ $this->entityTranslationRenderer = new EntityTranslationRenderer($this->view, $this->getLanguageManager(), $entity_type);
+ }
+ return $this->entityTranslationRenderer;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getRenderableFieldIds() {
+ $field_ids = [];
+ foreach ($this->view->field as $field_id => $field) {
+ if ($this->compatibleWithField($field)) {
+ $field_ids[] = $field_id;
+ }
+ }
+ return $field_ids;
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Plugin/views/EntityTranslationRenderer.php b/sites/all/modules/contrib/search/search_api/src/Plugin/views/EntityTranslationRenderer.php
new file mode 100644
index 000000000..74a831b74
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Plugin/views/EntityTranslationRenderer.php
@@ -0,0 +1,28 @@
+search_api_language)) {
+ return $row->search_api_language;
+ }
+ // If our normal query plugin is used, this shouldn't really ever happen,
+ // but if it does we fall back to the current request's content language.
+ return $this->languageManager
+ ->getCurrentLanguage(LanguageInterface::TYPE_CONTENT)
+ ->getId();
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Plugin/views/SearchApiHandlerTrait.php b/sites/all/modules/contrib/search/search_api/src/Plugin/views/SearchApiHandlerTrait.php
new file mode 100644
index 000000000..9dea85bd8
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Plugin/views/SearchApiHandlerTrait.php
@@ -0,0 +1,69 @@
+definition['entity_type'])) {
+ return $this->definition['entity_type'];
+ }
+ return parent::getEntityType();
+ }
+
+ /**
+ * Returns the active search index.
+ *
+ * @return \Drupal\search_api\IndexInterface|null
+ * The search index to use with this filter, or NULL if none could be
+ * loaded.
+ */
+ protected function getIndex() {
+ if ($this->getQuery()) {
+ return $this->getQuery()->getIndex();
+ }
+ $base_table = $this->view->storage->get('base_table');
+ return SearchApiQuery::getIndexFromTable($base_table);
+ }
+
+ /**
+ * Retrieves the query plugin.
+ *
+ * @return \Drupal\search_api\Plugin\views\query\SearchApiQuery|null
+ * The query plugin, or NULL if there is no query or it is no Search API
+ * query.
+ */
+ public function getQuery() {
+ if (empty($this->query) || !($this->query instanceof SearchApiQuery)) {
+ return NULL;
+ }
+ return $this->query;
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Plugin/views/argument/SearchApiDate.php b/sites/all/modules/contrib/search/search_api/src/Plugin/views/argument/SearchApiDate.php
new file mode 100644
index 000000000..c38b506b7
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Plugin/views/argument/SearchApiDate.php
@@ -0,0 +1,198 @@
+setDateFormatter($container->get('date.formatter'));
+
+ return $plugin;
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function query($group_by = FALSE) {
+ $this->fillValue();
+ if ($this->value === FALSE) {
+ $this->abort();
+ return;
+ }
+
+ $outer_conjunction = strtoupper($this->operator);
+
+ if (empty($this->options['not'])) {
+ $condition_operator = '=';
+ $inner_conjunction = 'OR';
+ }
+ else {
+ $condition_operator = '<>';
+ $inner_conjunction = 'AND';
+ }
+
+ if (!empty($this->value)) {
+ $outer_conditions = $this->query->createConditionGroup($outer_conjunction);
+ // @todo Refactor to use only a single nested filter, and only if
+ // necessary. $value_conditions will currently only ever contain a
+ // single child – a condition or a nested filter with two conditions.
+ foreach ($this->value as $value) {
+ $value_conditions = $this->query->createConditionGroup($inner_conjunction);
+ $values = explode(';', $value);
+ $values = array_map([$this, 'getTimestamp'], $values);
+ if (in_array(FALSE, $values, TRUE)) {
+ $this->abort();
+ return;
+ }
+ $is_range = (count($values) > 1);
+
+ $inner_conditions = ($is_range ? $this->query->createConditionGroup('AND') : $value_conditions);
+ $range_op = (empty($this->options['not']) ? '>=' : '<');
+ $inner_conditions->addCondition($this->realField, $values[0], $is_range ? $range_op : $condition_operator);
+ if ($is_range) {
+ $range_op = (empty($this->options['not']) ? '<=' : '>');
+ $inner_conditions->addCondition($this->realField, $values[1], $range_op);
+ $value_conditions->addConditionGroup($inner_conditions);
+ }
+ $outer_conditions->addConditionGroup($value_conditions);
+ }
+
+ $this->query->addConditionGroup($outer_conditions);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function title() {
+ if (!empty($this->argument)) {
+ $this->fillValue();
+ $dates = [];
+ foreach ($this->value as $date) {
+ $date_parts = explode(';', $date);
+
+ $ts = $this->getTimestamp($date_parts[0]);
+ $date_string = $this->getDateFormatter()->format($ts, 'short');
+ if (count($date_parts) > 1) {
+ $ts = $this->getTimestamp($date_parts[1]);
+ $date_string .= ' – ' . $this->getDateFormatter()->format($ts, 'short');
+ }
+
+ if ($date_string) {
+ $dates[] = $date_string;
+ }
+ }
+ return $dates ? implode(', ', $dates) : Html::escape($this->argument);
+ }
+
+ return Html::escape($this->argument);
+ }
+
+ /**
+ * Converts a value to a timestamp, if it isn't one already.
+ *
+ * @param string|int $value
+ * The value to convert. Either a timestamp, or a date/time string as
+ * recognized by strtotime().
+ *
+ * @return int|false
+ * The parsed timestamp, or FALSE if an illegal string was passed.
+ */
+ protected function getTimestamp($value) {
+ if (is_numeric($value)) {
+ return (int) $value;
+ }
+
+ return strtotime($value);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function unpackArgumentValue($force_int = FALSE) {
+ // Set up the defaults.
+ if (!isset($this->value)) {
+ $this->value = [];
+ }
+ if (!isset($this->operator)) {
+ $this->operator = 'or';
+ }
+
+ if (empty($this->argument)) {
+ return;
+ }
+
+ if (preg_match('/^([-\d;:\s]+\+)*[-\d;:\s]+$/', $this->argument)) {
+ // The '+' character in a query string may be parsed as ' '.
+ $this->value = explode('+', $this->argument);
+ }
+ elseif (preg_match('/^([-\d;:\s]+,)*[-\d;:\s]+$/', $this->argument)) {
+ $this->operator = 'and';
+ $this->value = explode(',', $this->argument);
+ }
+
+ // Keep an "error" value if invalid strings were given.
+ if (!empty($this->argument) && (empty($this->value) || !is_array($this->value))) {
+ $this->value = FALSE;
+ }
+ }
+
+ /**
+ * Aborts the associated query due to an illegal argument.
+ *
+ * @see \Drupal\search_api\Plugin\views\query\SearchApiQuery::abort()
+ */
+ protected function abort() {
+ $variables['@field'] = $this->definition['group'] . ': ' . $this->definition['title'];
+ $this->query->abort(new FormattableMarkup('Illegal argument passed to @field contextual filter.', $variables));
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Plugin/views/argument/SearchApiFulltext.php b/sites/all/modules/contrib/search/search_api/src/Plugin/views/argument/SearchApiFulltext.php
new file mode 100644
index 000000000..5520d4279
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Plugin/views/argument/SearchApiFulltext.php
@@ -0,0 +1,188 @@
+setParseModeManager($container->get('plugin.manager.search_api.parse_mode'));
+
+ return $plugin;
+ }
+
+ /**
+ * Retrieves the parse mode manager.
+ *
+ * @return \Drupal\search_api\ParseMode\ParseModePluginManager
+ * The parse mode manager.
+ */
+ public function getParseModeManager() {
+ return $this->parseModeManager ?: \Drupal::service('plugin.manager.search_api.parse_mode');
+ }
+
+ /**
+ * Sets the parse mode manager.
+ *
+ * @param \Drupal\search_api\ParseMode\ParseModePluginManager $parse_mode_manager
+ * The new parse mode manager.
+ *
+ * @return $this
+ */
+ public function setParseModeManager(ParseModePluginManager $parse_mode_manager) {
+ $this->parseModeManager = $parse_mode_manager;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defineOptions() {
+ $options = parent::defineOptions();
+
+ $options['parse_mode'] = ['default' => 'terms'];
+ $options['fields'] = ['default' => []];
+ $options['conjunction'] = ['default' => 'AND'];
+
+ return $options;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildOptionsForm(&$form, FormStateInterface $form_state) {
+ parent::buildOptionsForm($form, $form_state);
+
+ $form['parse_mode'] = [
+ '#type' => 'select',
+ '#title' => $this->t('Parse mode'),
+ '#description' => $this->t('Choose how the search keys will be parsed.'),
+ '#options' => $this->getParseModeManager()->getInstancesOptions(),
+ '#default_value' => $this->options['parse_mode'],
+ ];
+ foreach ($this->getParseModeManager()->getInstances() as $key => $mode) {
+ if ($mode->getDescription()) {
+ $states['visible'][':input[name="options[parse_mode]"]']['value'] = $key;
+ $form["parse_mode_{$key}_description"] = [
+ '#type' => 'item',
+ '#title' => $mode->label(),
+ '#description' => $mode->getDescription(),
+ '#states' => $states,
+ ];
+ }
+ }
+
+ $fields = $this->getFulltextFields();
+ if (!empty($fields)) {
+ $form['fields'] = [
+ '#type' => 'select',
+ '#title' => $this->t('Searched fields'),
+ '#description' => $this->t('Select the fields that will be searched. If no fields are selected, all available fulltext fields will be searched.'),
+ '#options' => $fields,
+ '#size' => min(4, count($fields)),
+ '#multiple' => TRUE,
+ '#default_value' => $this->options['fields'],
+ ];
+ $form['conjunction'] = [
+ '#title' => $this->t('Operator'),
+ '#description' => $this->t('Determines how multiple keywords entered for the search will be combined.'),
+ '#type' => 'radios',
+ '#options' => [
+ 'AND' => $this->t('Contains all of these words'),
+ 'OR' => $this->t('Contains any of these words'),
+ ],
+ '#default_value' => $this->options['conjunction'],
+ ];
+ }
+ else {
+ $form['fields'] = [
+ '#type' => 'value',
+ '#value' => [],
+ ];
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function query($group_by = FALSE) {
+ if ($this->options['parse_mode']) {
+ $parse_mode = $this->getParseModeManager()
+ ->createInstance($this->options['parse_mode']);
+ $this->query->setParseMode($parse_mode);
+ }
+ $this->query->getParseMode()->setConjunction($this->options['conjunction']);
+ if ($this->options['fields']) {
+ $this->query->setFulltextFields($this->options['fields']);
+ }
+
+ $old = $this->query->getOriginalKeys();
+ $this->query->keys($this->argument);
+ if ($old) {
+ $keys = &$this->query->getKeys();
+ if (is_array($keys)) {
+ $keys[] = $old;
+ }
+ elseif (is_array($old)) {
+ // We don't support such nonsense.
+ }
+ else {
+ $keys = "($old) ($keys)";
+ }
+ }
+ }
+
+ /**
+ * Retrieves an options list of available fulltext fields.
+ *
+ * @return string[]
+ * An associative array mapping the identifiers of the index's fulltext
+ * fields to their prefixed labels.
+ */
+ protected function getFulltextFields() {
+ $fields = [];
+
+ if (!empty($this->query)) {
+ $index = $this->query->getIndex();
+ }
+ else {
+ $index = SearchApiQuery::getIndexFromTable($this->table);
+ }
+
+ if (!$index) {
+ return [];
+ }
+
+ $fields_info = $index->getFields();
+ foreach ($index->getFulltextFields() as $field_id) {
+ $fields[$field_id] = $fields_info[$field_id]->getPrefixedLabel();
+ }
+
+ return $fields;
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Plugin/views/argument/SearchApiMoreLikeThis.php b/sites/all/modules/contrib/search/search_api/src/Plugin/views/argument/SearchApiMoreLikeThis.php
new file mode 100644
index 000000000..27615b89d
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Plugin/views/argument/SearchApiMoreLikeThis.php
@@ -0,0 +1,93 @@
+ []];
+ return $options;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildOptionsForm(&$form, FormStateInterface $form_state) {
+ parent::buildOptionsForm($form, $form_state);
+ unset($form['break_phrase']);
+ unset($form['not']);
+
+ $index = Index::load(substr($this->table, 17));
+ $fields = [];
+ foreach ($index->getFields() as $key => $field) {
+ $fields[$key] = $field->getLabel();
+ }
+
+ if ($fields) {
+ $form['fields'] = [
+ '#type' => 'select',
+ '#title' => $this->t('Fields for similarity'),
+ '#description' => $this->t('Select the fields that will be used for finding similar content. If no fields are selected, all available fields will be used.'),
+ '#options' => $fields,
+ '#size' => min(8, count($fields)),
+ '#multiple' => TRUE,
+ '#default_value' => $this->options['fields'],
+ ];
+ }
+ else {
+ $form['fields'] = [
+ '#type' => 'value',
+ '#value' => [],
+ ];
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function query($group_by = FALSE) {
+ try {
+ $server = $this->query->getIndex()->getServerInstance();
+ if (!$server->supportsFeature('search_api_mlt')) {
+ $backend_id = $server->getBackendId();
+ $this->getLogger()->error('The search backend "@backend_id" does not offer "More like this" functionality.',
+ ['@backend_id' => $backend_id]);
+ $this->query->abort();
+ return;
+ }
+ $fields = isset($this->options['fields']) ? $this->options['fields'] : [];
+ if (!$fields) {
+ $fields = array_keys($this->query->getIndex()->getFields());
+ }
+ $mlt = [
+ 'id' => $this->argument,
+ 'fields' => $fields,
+ ];
+ $this->query->getSearchApiQuery()->setOption('search_api_mlt', $mlt);
+ }
+ catch (SearchApiException $e) {
+ $this->query->abort($e->getMessage());
+ }
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Plugin/views/argument/SearchApiStandard.php b/sites/all/modules/contrib/search/search_api/src/Plugin/views/argument/SearchApiStandard.php
new file mode 100644
index 000000000..bf6a33fba
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Plugin/views/argument/SearchApiStandard.php
@@ -0,0 +1,212 @@
+ FALSE];
+ $options['not'] = ['default' => FALSE];
+
+ return $options;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildOptionsForm(&$form, FormStateInterface $form_state) {
+ parent::buildOptionsForm($form, $form_state);
+
+ if (empty($this->definition['disable_break_phrase'])) {
+ // Allow passing multiple values.
+ $form['break_phrase'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Allow multiple values'),
+ '#description' => $this->t('If selected, users can enter multiple values in the form of 1+2+3 (for OR) or 1,2,3 (for AND).'),
+ '#default_value' => !empty($this->options['break_phrase']),
+ '#group' => 'options][more',
+ ];
+ }
+
+ $form['not'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Exclude'),
+ '#description' => $this->t('If selected, the values entered for the filter will be excluded rather than limiting the view to those values.'),
+ '#default_value' => !empty($this->options['not']),
+ '#group' => 'options][more',
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validateOptionsForm(&$form, FormStateInterface $form_state) {
+ $option_values = &$form_state->getValue('options');
+ if (empty($option_values)) {
+ return;
+ }
+
+ // Let the plugins do validation.
+ if (!empty($option_values['default_argument_type'])) {
+ $default_id = $option_values['default_argument_type'];
+ $plugin = $this->getPlugin('argument_default', $default_id);
+ if ($plugin) {
+ $plugin->validateOptionsForm($form['argument_default'][$default_id], $form_state, $option_values['argument_default'][$default_id]);
+ }
+ }
+
+ if (!empty($option_values['validate']['type'])) {
+ $sanitized_id = $option_values['validate']['type'];
+ // Correct ID for js sanitized version.
+ $validate_id = static::decodeValidatorId($sanitized_id);
+ $plugin = $this->getPlugin('argument_validator', $validate_id);
+ if ($plugin) {
+ $plugin->validateOptionsForm($form['validate']['options'][$sanitized_id], $form_state, $option_values['validate']['options'][$sanitized_id]);
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitOptionsForm(&$form, FormStateInterface $form_state) {
+ $option_values = &$form_state->getValue('options');
+ if (empty($option_values)) {
+ return;
+ }
+
+ // Let the plugins make submit modifications if necessary.
+ if (!empty($option_values['default_argument_type'])) {
+ $default_id = $option_values['default_argument_type'];
+ $plugin = $this->getPlugin('argument_default', $default_id);
+ if ($plugin) {
+ $options = &$option_values['argument_default'][$default_id];
+ $plugin->submitOptionsForm($form['argument_default'][$default_id], $form_state, $options);
+ // Copy the now submitted options to their final resting place so they get saved.
+ $option_values['default_argument_options'] = $options;
+ }
+ }
+
+ // If the 'Specify validation criteria' checkbox is not checked, reset the
+ // validation options.
+ if (empty($option_values['specify_validation'])) {
+ $option_values['validate']['type'] = 'none';
+ // We need to keep the empty array of options for the 'None' plugin as
+ // it will be needed later.
+ $option_values['validate']['options'] = ['none' => []];
+ $option_values['validate']['fail'] = 'not found';
+ }
+
+ if (!empty($option_values['validate']['type'])) {
+ $sanitized_id = $option_values['validate']['type'];
+ // Correct ID for js sanitized version.
+ $option_values['validate']['type'] = $validate_id = static::decodeValidatorId($sanitized_id);
+ $plugin = $this->getPlugin('argument_validator', $validate_id);
+ if ($plugin) {
+ $options = &$option_values['validate']['options'][$sanitized_id];
+ $plugin->submitOptionsForm($form['validate']['options'][$sanitized_id], $form_state, $options);
+ // Copy the now submitted options to their final resting place so they get saved.
+ $option_values['validate_options'] = $options;
+ }
+ }
+
+ // Clear out the content of title if it's not enabled.
+ if (empty($option_values['title_enable'])) {
+ $option_values['title'] = '';
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function query($group_by = FALSE) {
+ $this->fillValue();
+
+ if (count($this->value) > 1) {
+ $operator = empty($this->options['not']) ? 'IN' : 'NOT IN';
+ $this->query->addCondition($this->realField, $this->value, $operator);
+ }
+ elseif ($this->value) {
+ $operator = empty($this->options['not']) ? '=' : '<>';
+ $this->query->addCondition($this->realField, reset($this->value), $operator);
+ }
+ }
+
+ /**
+ * Fills $this->value and $this->operator with data from the argument.
+ */
+ protected function fillValue() {
+ if (isset($this->value)) {
+ return;
+ }
+
+ $filter = '';
+ if (!empty($this->definition['filter'])) {
+ $filter = $this->definition['filter'];
+ }
+
+ if (!empty($this->options['break_phrase']) && empty($this->definition['disable_break_phrase'])) {
+ $force_int = FALSE;
+ if ($filter == 'intval') {
+ $force_int = TRUE;
+ $filter = '';
+ }
+ $this->unpackArgumentValue($force_int);
+ }
+ else {
+ $this->value = [$this->argument];
+ $this->operator = 'and';
+ }
+
+ if (is_callable($filter)) {
+ $this->value = array_map($filter, $this->value);
+ }
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Plugin/views/argument/SearchApiTerm.php b/sites/all/modules/contrib/search/search_api/src/Plugin/views/argument/SearchApiTerm.php
new file mode 100644
index 000000000..8a1ea8e81
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Plugin/views/argument/SearchApiTerm.php
@@ -0,0 +1,44 @@
+argument)) {
+ $this->fillValue();
+ $terms = [];
+ foreach ($this->value as $tid) {
+ $taxonomy_term = Term::load($tid);
+ if ($taxonomy_term) {
+ $terms[] = Html::escape($taxonomy_term->label());
+ }
+ }
+
+ return $terms ? implode(', ', $terms) : Html::escape($this->argument);
+ }
+ else {
+ return Html::escape($this->argument);
+ }
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Plugin/views/cache/SearchApiCachePluginTrait.php b/sites/all/modules/contrib/search/search_api/src/Plugin/views/cache/SearchApiCachePluginTrait.php
new file mode 100644
index 000000000..9f6e7100d
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Plugin/views/cache/SearchApiCachePluginTrait.php
@@ -0,0 +1,231 @@
+cacheBackend ?: \Drupal::cache($this->resultsBin);
+ }
+
+ /**
+ * Sets the cache backend.
+ *
+ * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
+ * The new cache backend.
+ *
+ * @return $this
+ */
+ public function setCacheBackend(CacheBackendInterface $cache_backend) {
+ $this->cacheBackend = $cache_backend;
+ return $this;
+ }
+
+ /**
+ * Retrieves the cache contexts manager.
+ *
+ * @return \Drupal\Core\Cache\Context\CacheContextsManager
+ * The cache contexts manager.
+ */
+ public function getCacheContextsManager() {
+ return $this->cacheContextsManager ?: \Drupal::service('cache_contexts_manager');
+ }
+
+ /**
+ * Sets the cache contexts manager.
+ *
+ * @param \Drupal\Core\Cache\Context\CacheContextsManager $cache_contexts_manager
+ * The new cache contexts manager.
+ *
+ * @return $this
+ */
+ public function setCacheContextsManager(CacheContextsManager $cache_contexts_manager) {
+ $this->cacheContextsManager = $cache_contexts_manager;
+ return $this;
+ }
+
+ /**
+ * Retrieves the query helper.
+ *
+ * @return \Drupal\search_api\Utility\QueryHelperInterface
+ * The query helper.
+ */
+ public function getQueryHelper() {
+ return $this->queryHelper ?: \Drupal::service('search_api.query_helper');
+ }
+
+ /**
+ * Sets the query helper.
+ *
+ * @param \Drupal\search_api\Utility\QueryHelperInterface $query_helper
+ * The new query helper.
+ *
+ * @return $this
+ */
+ public function setQueryHelper(QueryHelperInterface $query_helper) {
+ $this->queryHelper = $query_helper;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function cacheSet($type) {
+ if ($type != 'results') {
+ parent::cacheSet($type);
+ return;
+ }
+
+ $view = $this->getView();
+ $data = [
+ 'result' => $view->result,
+ 'total_rows' => isset($view->total_rows) ? $view->total_rows : 0,
+ 'current_page' => $view->getCurrentPage(),
+ 'search_api results' => $this->getQuery()->getSearchApiResults(),
+ ];
+
+ $expire = $this->cacheSetMaxAge($type);
+ if ($expire !== Cache::PERMANENT) {
+ $expire += (int) $view->getRequest()->server->get('REQUEST_TIME');
+ }
+ $this->getCacheBackend()
+ ->set($this->generateResultsKey(), $data, $expire, $this->getCacheTags());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function cacheGet($type) {
+ if ($type != 'results') {
+ return parent::cacheGet($type);
+ }
+
+ // Values to set: $view->result, $view->total_rows, $view->execute_time,
+ // $view->current_page.
+ if ($cache = $this->getCacheBackend()->get($this->generateResultsKey())) {
+ $cutoff = $this->cacheExpire($type);
+ if (!$cutoff || $cache->created > $cutoff) {
+ $view = $this->getView();
+ $view->result = $cache->data['result'];
+ $view->total_rows = $cache->data['total_rows'];
+ $view->setCurrentPage($cache->data['current_page']);
+ $view->execute_time = 0;
+
+ // Trick Search API into believing a search happened, to make faceting
+ // et al. work.
+ /** @var \Drupal\search_api\Query\ResultSetInterface $results */
+ $results = $cache->data['search_api results'];
+ $this->getQueryHelper()->addResults($results);
+
+ try {
+ $this->getQuery()->setSearchApiQuery($results->getQuery());
+ }
+ catch (SearchApiException $e) {
+ // Ignore.
+ }
+
+ return TRUE;
+ }
+ }
+ return FALSE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function generateResultsKey() {
+ if (!isset($this->resultsKey)) {
+ $query = $this->getQuery()->getSearchApiQuery();
+ $query->preExecute();
+
+ $view = $this->getView();
+ $build_info = $view->build_info;
+
+ $key_data = [
+ 'build_info' => $build_info,
+ 'pager' => [
+ 'page' => $view->getCurrentPage(),
+ 'items_per_page' => $view->getItemsPerPage(),
+ 'offset' => $view->getOffset(),
+ ],
+ ];
+
+ $display_handler_cache_contexts = $this->displayHandler
+ ->getCacheMetadata()
+ ->getCacheContexts();
+ $key_data += $this->getCacheContextsManager()
+ ->convertTokensToKeys($display_handler_cache_contexts)
+ ->getKeys();
+
+ $this->resultsKey = $view->storage->id() . ':' . $this->displayHandler->display['id'] . ':results:' . Crypt::hashBase64(serialize($key_data));
+ }
+
+ return $this->resultsKey;
+ }
+
+ /**
+ * Retrieves the view to which this plugin belongs.
+ *
+ * @return \Drupal\views\ViewExecutable
+ * The view.
+ */
+ protected function getView() {
+ return $this->view;
+ }
+
+ /**
+ * Retrieves the Search API Views query for the current view.
+ *
+ * @return \Drupal\search_api\Plugin\views\query\SearchApiQuery|null
+ * The Search API Views query associated with the current view.
+ *
+ * @throws \Drupal\search_api\SearchApiException
+ * Thrown if there is no current Views query, or it is no Search API query.
+ */
+ protected function getQuery() {
+ $query = $this->getView()->getQuery();
+ if ($query instanceof SearchApiQuery) {
+ return $query;
+ }
+ throw new SearchApiException('No matching Search API Views query found in view.');
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Plugin/views/cache/SearchApiTagCache.php b/sites/all/modules/contrib/search/search_api/src/Plugin/views/cache/SearchApiTagCache.php
new file mode 100644
index 000000000..28a7728e7
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Plugin/views/cache/SearchApiTagCache.php
@@ -0,0 +1,84 @@
+Caution: Can lead to stale results and might harm performance for complex search pages.")
+ * )
+ */
+class SearchApiTagCache extends Tag {
+
+ use SearchApiCachePluginTrait;
+
+ /**
+ * The entity type manager service.
+ *
+ * @var \Drupal\Core\Entity\EntityTypeManagerInterface|null
+ */
+ protected $entityTypeManager;
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+ /** @var static $cache */
+ $cache = parent::create($container, $configuration, $plugin_id, $plugin_definition);
+
+ $cache->setEntityTypeManager($container->get('entity_type.manager'));
+
+ return $cache;
+ }
+
+ /**
+ * Retrieves the entity type manager.
+ *
+ * @return \Drupal\Core\Entity\EntityTypeManagerInterface
+ * The entity type manager.
+ */
+ public function getEntityTypeManager() {
+ return $this->entityTypeManager ?: \Drupal::service('entity_type.manager');
+ }
+
+ /**
+ * Sets the entity type manager.
+ *
+ * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+ * The new entity type manager.
+ *
+ * @return $this
+ */
+ public function setEntityTypeManager(EntityTypeManagerInterface $entity_type_manager) {
+ $this->entityTypeManager = $entity_type_manager;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCacheTags() {
+ $tags = $this->view->storage->getCacheTags();
+ $tag = 'search_api_list:' . $this->getQuery()->getIndex()->id();
+ $tags = Cache::mergeTags([$tag], $tags);
+ return $tags;
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Plugin/views/cache/SearchApiTimeCache.php b/sites/all/modules/contrib/search/search_api/src/Plugin/views/cache/SearchApiTimeCache.php
new file mode 100644
index 000000000..8a3e7b958
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Plugin/views/cache/SearchApiTimeCache.php
@@ -0,0 +1,31 @@
+Caution: Will lead to stale results and might harm performance for complex search pages.")
+ * )
+ */
+class SearchApiTimeCache extends Time {
+
+ use SearchApiCachePluginTrait;
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Plugin/views/field/SearchApiBoolean.php b/sites/all/modules/contrib/search/search_api/src/Plugin/views/field/SearchApiBoolean.php
new file mode 100644
index 000000000..1812fd223
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Plugin/views/field/SearchApiBoolean.php
@@ -0,0 +1,19 @@
+setEntityDisplayRepository($container->get('entity_display.repository'));
+
+ return $field;
+ }
+
+ /**
+ * Retrieves the entity display repository.
+ *
+ * @return \Drupal\Core\Entity\EntityDisplayRepositoryInterface
+ * The entity entity display repository.
+ */
+ public function getEntityDisplayRepository() {
+ return $this->entityDisplayRepository ?: \Drupal::service('entity_display.repository');
+ }
+
+ /**
+ * Sets the entity display repository.
+ *
+ * @param \Drupal\Core\Entity\EntityDisplayRepositoryInterface $entity_display_repository
+ * The new entity display repository.
+ *
+ * @return $this
+ */
+ public function setEntityDisplayRepository(EntityDisplayRepositoryInterface $entity_display_repository) {
+ $this->entityDisplayRepository = $entity_display_repository;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defineOptions() {
+ $options = parent::defineOptions();
+
+ $options['display_methods'] = ['default' => []];
+
+ return $options;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildOptionsForm(&$form, FormStateInterface $form_state) {
+ parent::buildOptionsForm($form, $form_state);
+
+ $entity_type_id = $this->getTargetEntityTypeId();
+ $view_modes = [];
+ $bundles = [];
+ if ($entity_type_id) {
+ $bundles = $this->getEntityManager()->getBundleInfo($entity_type_id);
+ // In case the field definition specifies the bundles to expect, restrict
+ // the displayed bundles to those.
+ $settings = $this->getFieldDefinition()->getSettings();
+ if (!empty($settings['handler_settings']['target_bundles'])) {
+ $bundles = array_intersect_key($bundles, $settings['handler_settings']['target_bundles']);
+ }
+ foreach ($bundles as $bundle => $info) {
+ $view_modes[$bundle] = $this->getEntityDisplayRepository()
+ ->getViewModeOptionsByBundle($entity_type_id, $bundle);
+ }
+ }
+
+ foreach ($bundles as $bundle => $info) {
+ $args['@bundle'] = $info['label'];
+ $form['display_methods'][$bundle]['display_method'] = [
+ '#type' => 'select',
+ '#title' => $this->t('Display for "@bundle" bundle', $args),
+ '#options' => [
+ '' => $this->t('Hide'),
+ 'id' => $this->t('Raw ID'),
+ 'label' => $this->t('Only label'),
+ ],
+ '#default_value' => 'label',
+ ];
+ $display_method = $this->getDisplayMethod($bundle);
+ if ($display_method !== NULL) {
+ $form['display_methods'][$bundle]['display_method']['#default_value'] = $display_method;
+ }
+ if (!empty($view_modes[$bundle])) {
+ $form['display_methods'][$bundle]['display_method']['#options']['view_mode'] = $this->t('Entity view');
+ if (count($view_modes[$bundle]) > 1) {
+ $form['display_methods'][$bundle]['view_mode'] = [
+ '#type' => 'select',
+ '#title' => $this->t('View mode for "@bundle" bundle', $args),
+ '#options' => $view_modes[$bundle],
+ '#states' => [
+ 'visible' => [
+ ':input[name="options[display_methods][' . $bundle . '][display_method]"]' => [
+ 'value' => 'view_mode',
+ ],
+ ],
+ ],
+ ];
+ if (isset($this->options['display_methods'][$bundle]['view_mode'])) {
+ $form['display_methods'][$bundle]['view_mode']['#default_value'] = $this->options['display_methods'][$bundle]['view_mode'];
+ }
+ }
+ else {
+ reset($view_modes[$bundle]);
+ $form['display_methods'][$bundle]['view_mode'] = [
+ '#type' => 'value',
+ '#value' => key($view_modes[$bundle]),
+ ];
+ }
+ }
+ if (count($bundles) == 1) {
+ $form['display_methods'][$bundle]['display_method']['#title'] = $this->t('Display method');
+ if (!empty($form['display_methods'][$bundle]['view_mode'])) {
+ $form['display_methods'][$bundle]['view_mode']['#title'] = $this->t('View mode');
+ }
+ }
+ }
+
+ $form['link_to_item']['#description'] .= ' ' . $this->t('This will only take effect for entities for which only the entity label is displayed.');
+ $form['link_to_item']['#weight'] = 5;
+ unset($form['use_highlighting']);
+ }
+
+ /**
+ * Return the entity type ID of the entity this field handler should display.
+ *
+ * @return string|null
+ * The entity type ID, or NULL if it couldn't be found.
+ */
+ public function getTargetEntityTypeId() {
+ $field_definition = $this->getFieldDefinition();
+ if ($field_definition->getType() === 'field_item:comment') {
+ return 'comment';
+ }
+ return $field_definition->getSetting('target_type');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function query() {
+ $this->addRetrievedProperty($this->getCombinedPropertyPath());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function preRender(&$values) {
+ parent::preRender($values);
+
+ // The parent method will just have loaded the entity IDs. We now multi-load
+ // the actual objects.
+ $property_path = $this->getCombinedPropertyPath();
+ foreach ($values as $i => $row) {
+ if (!empty($row->{$property_path})) {
+ foreach ((array) $row->{$property_path} as $j => $value) {
+ if (is_scalar($value)) {
+ $to_load[$value][] = [$i, $j];
+ }
+ }
+ }
+ }
+
+ if (empty($to_load)) {
+ return;
+ }
+
+ $entities = $this->getEntityManager()
+ ->getStorage($this->getTargetEntityTypeId())
+ ->loadMultiple(array_keys($to_load));
+ $account = $this->getQuery()->getAccessAccount();
+ foreach ($entities as $id => $entity) {
+ $bundle = $entity->bundle();
+ $operation = ($this->getDisplayMethod($bundle) == 'label') ? 'view label' : 'view';
+ if ($entity->access($operation, $account)) {
+ foreach ($to_load[$id] as list($i, $j)) {
+ if ($entity->access('view', $account)) {
+ $langcode = $values[$i]->search_api_language;
+ if ($entity instanceof TranslatableInterface && $entity->hasTranslation($langcode)) {
+ $entity = $entity->getTranslation($langcode);
+ }
+ $values[$i]->{$property_path}[$j] = $entity;
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function render_item($count, $item) {
+ if (is_array($item['value'])) {
+ return $this->getRenderer()->render($item['value']);
+ }
+ return parent::render_item($count, $item);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getItems(ResultRow $values) {
+ $property_path = $this->getCombinedPropertyPath();
+ if (!empty($values->{$property_path})) {
+ $items = [];
+ foreach ((array) $values->{$property_path} as $value) {
+ if ($value instanceof EntityInterface) {
+ $item = $this->getItem($value);
+ if ($item) {
+ $items[] = $item;
+ }
+ }
+ }
+ return $items;
+ }
+ return [];
+ }
+
+ /**
+ * Creates an item for the given entity.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entity.
+ *
+ * @return array|null
+ * NULL if the entity should not be displayed. Otherwise, an associative
+ * array with at least "value" set, to either a string or a render array,
+ * and possibly also additional alter options.
+ */
+ protected function getItem(EntityInterface $entity) {
+ $bundle = $entity->bundle();
+ $display_method = $this->getDisplayMethod($bundle);
+ if (!$display_method) {
+ return NULL;
+ }
+
+ if (in_array($display_method, ['id', 'label'])) {
+ if ($display_method == 'label') {
+ $item['value'] = $entity->label();
+ }
+ else {
+ $item['value'] = $entity->id();
+ }
+
+ if ($this->options['link_to_item']) {
+ $item['make_link'] = TRUE;
+ $item['url'] = $entity->toUrl('canonical');
+ }
+
+ return $item;
+ }
+
+ $view_mode = $this->options['display_methods'][$bundle]['view_mode'];
+ $build = $this->getEntityManager()
+ ->getViewBuilder($entity->getEntityTypeId())
+ ->view($entity, $view_mode);
+ return [
+ 'value' => $build,
+ ];
+ }
+
+ /**
+ * Retrieves the display method configured for a certain bundle.
+ *
+ * @param string $bundle
+ * The bundle of the entity being displayed.
+ *
+ * @return string|null
+ * The "display_method" option selected for this bundle, or NULL if none was
+ * selected yet.
+ */
+ protected function getDisplayMethod($bundle) {
+ if (!isset($this->options['display_methods'][$bundle]['display_method'])) {
+ return NULL;
+ }
+ return $this->options['display_methods'][$bundle]['display_method'];
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Plugin/views/field/SearchApiEntityField.php b/sites/all/modules/contrib/search/search_api/src/Plugin/views/field/SearchApiEntityField.php
new file mode 100644
index 000000000..d2018031e
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Plugin/views/field/SearchApiEntityField.php
@@ -0,0 +1,332 @@
+definition['fallback_handler']) ? $this->definition['fallback_handler'] : 'search_api';
+ $this->fallbackHandler = Views::handlerManager('field')
+ ->getHandler($options, $fallback_handler_id);
+ $options += ['fallback_options' => []];
+ $fallback_options = $options['fallback_options'] + $options;
+ $this->fallbackHandler->init($view, $display, $fallback_options);
+
+ parent::init($view, $display, $options);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function query($use_groupby = FALSE) {
+ // If we're not using Field API field rendering, just use the query()
+ // implementation of the fallback handler.
+ if (!$this->options['field_rendering']) {
+ $this->fallbackHandler->query();
+ return;
+ }
+
+ // If we do use Field API rendering, we need the entity object for the
+ // parent property.
+ $parent_path = $this->getParentPath();
+ $property_path = $parent_path ? "$parent_path:_object" : '_object';
+ $combined_property_path = Utility::createCombinedId($this->getDatasourceId(), $property_path);
+ $this->addRetrievedProperty($combined_property_path);
+ }
+
+ /**
+ * Retrieves the property path of the parent property.
+ *
+ * @return string|null
+ * The property path of the parent property.
+ */
+ protected function getParentPath() {
+ if (!isset($this->parentPath)) {
+ $combined_property_path = $this->getCombinedPropertyPath();
+ list(, $property_path) = Utility::splitCombinedId($combined_property_path);
+ list($this->parentPath) = Utility::splitPropertyPath($property_path);
+ }
+
+ return $this->parentPath;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defineOptions() {
+ $options = parent::defineOptions();
+
+ // Gather the fallback handler's options, but exclude those just inherited
+ // from the field plugin base (since they would otherwise be duplicated).
+ // To find out which options should be excluded, we take the $options keys
+ // from the parent and remove the keys that come directly from the parent.
+ $fallback_options = [];
+ $fallback_define_options = [$this->fallbackHandler, 'defineOptions'];
+ if (is_callable($fallback_define_options)) {
+ $fallback_options = call_user_func($fallback_define_options);
+ $parent_keys = $this->getParentOptionKeys();
+ $remove_from_fallback = array_diff_key($options, array_flip($parent_keys));
+ $fallback_options = array_diff_key($fallback_options, $remove_from_fallback);
+ }
+
+ $options['field_rendering'] = ['default' => TRUE];
+ $options['fallback_handler'] = ['default' => $this->fallbackHandler->getPluginId()];
+ $options['fallback_options'] = ['contains' => $fallback_options];
+
+ return $options;
+ }
+
+ /**
+ * Retrieves the keys of the options defined by our direct parent.
+ *
+ * That is, this will exclude all options defined by
+ * \Drupal\views\Plugin\views\field\FieldPluginBase, and only include those
+ * defined by \Drupal\views\Plugin\views\field\EntityField.
+ *
+ * @param bool $form_only
+ * (optional) If TRUE, return those fields that will be displayed as the
+ * top-most elements in the config form. Otherwise, return all options
+ * defined by the direct parent handler.
+ *
+ * @return string[]
+ * The keys of options directly defined by our parent class or, if
+ * $form_only was passed, those that would be displayed in its config form.
+ */
+ protected function getParentOptionKeys($form_only = FALSE) {
+ $options = [
+ 'click_sort_column',
+ 'type',
+ 'field_api_classes',
+ 'settings',
+ ];
+ if ($form_only) {
+ $options[] = 'multiple_field_settings';
+ return $options;
+ }
+ $options = array_merge($options, [
+ 'group_column',
+ 'group_columns',
+ 'group_rows',
+ 'delta_limit',
+ 'delta_offset',
+ 'delta_reversed',
+ 'delta_first_last',
+ 'multi_type',
+ 'separator',
+ ]);
+ return $options;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildOptionsForm(&$form, FormStateInterface $form_state) {
+ $form['field_rendering'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Use entity field rendering'),
+ '#description' => $this->t("If checked, Drupal's built-in field rendering mechanism will be used for rendering this field's values, which requires the entity to be loaded. If unchecked, a type-specific, entity-independent rendering mechanism will be used."),
+ '#default_value' => $this->options['field_rendering'],
+ ];
+
+ // Wrap the (immediate) parent options in their own field set, to clean up
+ // the UI when (un)checking the above checkbox.
+ $form['parent_options'] = [
+ '#type' => 'fieldset',
+ '#title' => $this->t('Render settings'),
+ '#states' => [
+ 'visible' => [
+ ':input[name="options[field_rendering]"]' => ['checked' => TRUE],
+ ],
+ ],
+ ];
+
+ // Include the parent options form and move all fields that were added by
+ // our direct parent (\Drupal\views\Plugin\views\field\Field) to the
+ // "parent_options" fieldset.
+ parent::buildOptionsForm($form, $form_state);
+ $parent_keys = $this->getParentOptionKeys(TRUE);
+ foreach ($parent_keys as $key) {
+ if (!empty($form[$key])) {
+ $form[$key]['#fieldset'] = 'parent_options';
+ }
+ }
+ // The Core boolean formatter hard-codes the field name to "field_boolean".
+ // This breaks the parent class's call of rewriteStatesSelector() for fixing
+ // "#states". We therefore apply that behavior again here.
+ if (!empty($form['settings'])) {
+ FormHelper::rewriteStatesSelector($form['settings'], "fields[field_boolean][settings_edit_form]", 'options');
+ }
+
+ // Get the options form for the fallback handler.
+ $fallback_form = [];
+ $this->fallbackHandler->buildOptionsForm($fallback_form, $form_state);
+ // Remove all fields from FieldPluginBase from the fallback form, but leave
+ // those in that were only added by our immediate parent,
+ // \Drupal\views\Plugin\views\field\Field. (For example, the "type" option
+ // is especially prone to conflicts here.) The others come from the plugin
+ // base classes and will be identical, so it would be confusing to include
+ // them twice.
+ $parent_keys[] = '#pre_render';
+ $remove_from_fallback = array_diff_key($form, array_flip($parent_keys));
+ $fallback_form = array_diff_key($fallback_form, $remove_from_fallback);
+ // Fix the "#states" selectors in the fallback form, and put an additional
+ // "#states" directive on it to only be visible for the corresponding
+ // "field_rendering" setting.
+ if ($fallback_form) {
+ FormHelper::rewriteStatesSelector($fallback_form, '"options[', '"options[fallback_options][');
+ $form['fallback_options'] = $fallback_form;
+ $form['fallback_options']['#type'] = 'fieldset';
+ $form['fallback_options']['#title'] = $this->t('Render settings');
+ $form['fallback_options']['#states']['visible'][':input[name="options[field_rendering]"]'] = ['checked' => FALSE];
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function preRender(&$values) {
+ if ($this->options['field_rendering']) {
+ $this->traitPreRender($values);
+ parent::preRender($values);
+ }
+ else {
+ $this->fallbackHandler->preRender($values);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function render(ResultRow $values) {
+ if (!$this->options['field_rendering']) {
+ return $this->fallbackHandler->render($values);
+ }
+ return parent::render($values);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function render_item($count, $item) {
+ if (!$this->options['field_rendering']) {
+ if ($this->fallbackHandler instanceof MultiItemsFieldHandlerInterface) {
+ return $this->fallbackHandler->render_item($count, $item);
+ }
+ return '';
+ }
+ return parent::render_item($count, $item);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getEntityFieldRenderer() {
+ if (!isset($this->entityFieldRenderer)) {
+ // This can be invoked during field handler initialization in which case
+ // view fields are not set yet.
+ if (!empty($this->view->field)) {
+ foreach ($this->view->field as $field) {
+ // An entity field renderer can handle only a single relationship.
+ if (isset($field->entityFieldRenderer)) {
+ if ($field->entityFieldRenderer instanceof EntityFieldRenderer && $field->entityFieldRenderer->compatibleWithField($this)) {
+ $this->entityFieldRenderer = $field->entityFieldRenderer;
+ break;
+ }
+ }
+ }
+ }
+ if (!isset($this->entityFieldRenderer)) {
+ $entity_type = $this->entityManager->getDefinition($this->getEntityType());
+ $this->entityFieldRenderer = new EntityFieldRenderer($this->view, $this->relationship, $this->languageManager, $entity_type, $this->entityManager);
+ $this->entityFieldRenderer->setDatasourceId($this->getDatasourceId());
+ }
+ }
+
+ return $this->entityFieldRenderer;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getItems(ResultRow $values) {
+ if (!$this->options['field_rendering']) {
+ if ($this->fallbackHandler instanceof MultiItemsFieldHandlerInterface) {
+ return $this->fallbackHandler->getItems($values);
+ }
+ return [];
+ }
+
+ if ($values->search_api_datasource != $this->getDatasourceId()) {
+ return [];
+ }
+
+ $parent_path = $this->getParentPath();
+ $combined_parent_path = $this->createCombinedPropertyPath($this->getDatasourceId(), $parent_path);
+ if (empty($values->_relationship_objects[$combined_parent_path])) {
+ return [];
+ }
+ $build = [];
+ foreach (array_keys($values->_relationship_objects[$combined_parent_path]) as $i) {
+ $this->valueIndex = $i;
+ $build[] = parent::getItems($values);
+ }
+ return $build ? call_user_func_array('array_merge', $build) : [];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function renderItems($items) {
+ if (!$this->options['field_rendering']) {
+ if ($this->fallbackHandler instanceof MultiItemsFieldHandlerInterface) {
+ return $this->fallbackHandler->renderItems($items);
+ }
+ return '';
+ }
+
+ return parent::renderItems($items);
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Plugin/views/field/SearchApiFieldTrait.php b/sites/all/modules/contrib/search/search_api/src/Plugin/views/field/SearchApiFieldTrait.php
new file mode 100644
index 000000000..feb418173
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Plugin/views/field/SearchApiFieldTrait.php
@@ -0,0 +1,1306 @@
+typedDataManager ?: \Drupal::service('typed_data_manager');
+ }
+
+ /**
+ * Sets the typed data manager.
+ *
+ * @param \Drupal\Core\TypedData\TypedDataManagerInterface $typed_data_manager
+ * The new typed data manager.
+ *
+ * @return $this
+ */
+ public function setTypedDataManager(TypedDataManagerInterface $typed_data_manager) {
+ $this->typedDataManager = $typed_data_manager;
+ return $this;
+ }
+
+ /**
+ * Retrieves the entity type manager.
+ *
+ * @return \Drupal\Core\Entity\EntityTypeManagerInterface
+ * The entity type manager.
+ */
+ public function getEntityTypeManager() {
+ return $this->entityTypeManager ?: \Drupal::entityTypeManager();
+ }
+
+ /**
+ * Sets the entity type manager.
+ *
+ * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+ * The entity type manager.
+ *
+ * @return $this
+ */
+ public function setEntityTypeManager(EntityTypeManagerInterface $entity_type_manager) {
+ $this->entityTypeManager = $entity_type_manager;
+ return $this;
+ }
+
+ /**
+ * Retrieves the fields helper.
+ *
+ * @return \Drupal\search_api\Utility\FieldsHelperInterface
+ * The fields helper.
+ */
+ public function getFieldsHelper() {
+ return $this->fieldsHelper ?: \Drupal::service('search_api.fields_helper');
+ }
+
+ /**
+ * Sets the fields helper.
+ *
+ * @param \Drupal\search_api\Utility\FieldsHelperInterface $fields_helper
+ * The new fields helper.
+ *
+ * @return $this
+ */
+ public function setFieldsHelper(FieldsHelperInterface $fields_helper) {
+ $this->fieldsHelper = $fields_helper;
+ return $this;
+ }
+
+ /**
+ * Determines whether this field can have multiple values.
+ *
+ * When this can't be reliably determined, the method defaults to TRUE.
+ *
+ * @return bool
+ * TRUE if this field can have multiple values (or if it couldn't be
+ * determined); FALSE otherwise.
+ */
+ public function isMultiple() {
+ return $this instanceof MultiItemsFieldHandlerInterface;
+ }
+
+ /**
+ * Defines the options used by this plugin.
+ *
+ * @return array
+ * Returns the options of this handler/plugin.
+ *
+ * @see \Drupal\views\Plugin\views\PluginBase::defineOptions()
+ */
+ public function defineOptions() {
+ $options = parent::defineOptions();
+
+ $options['link_to_item'] = ['default' => FALSE];
+ $options['use_highlighting'] = ['default' => FALSE];
+
+ if ($this->isMultiple()) {
+ $options['multi_type'] = ['default' => 'separator'];
+ $options['multi_separator'] = ['default' => ', '];
+ }
+
+ return $options;
+ }
+
+ /**
+ * Provide a form to edit options for this plugin.
+ *
+ * @param array|\ArrayAccess $form
+ * The existing form structure, passed by reference.
+ * @param \Drupal\Core\Form\FormStateInterface $form_state
+ * The current form state.
+ *
+ * @see \Drupal\views\Plugin\views\ViewsPluginInterface::buildOptionsForm()
+ */
+ public function buildOptionsForm(&$form, FormStateInterface $form_state) {
+ parent::buildOptionsForm($form, $form_state);
+
+ $form['link_to_item'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Link this field to its item'),
+ '#description' => $this->t('Display this field as a link to its original entity or item.'),
+ '#default_value' => $this->options['link_to_item'],
+ ];
+
+ $form['use_highlighting'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Use highlighted field data'),
+ '#description' => $this->t('Display field with matches of the search keywords highlighted, if available.'),
+ '#default_value' => $this->options['use_highlighting'],
+ ];
+
+ if ($this->isMultiple()) {
+ $form['multi_value_settings'] = [
+ '#type' => 'details',
+ '#title' => $this->t('Multiple values handling'),
+ '#description' => $this->t('If this field contains multiple values for an item, these settings will determine how they are handled.'),
+ '#weight' => 80,
+ ];
+
+ $form['multi_type'] = [
+ '#type' => 'radios',
+ '#title' => $this->t('Display type'),
+ '#options' => [
+ 'ul' => $this->t('Unordered list'),
+ 'ol' => $this->t('Ordered list'),
+ 'separator' => $this->t('Simple separator'),
+ ],
+ '#default_value' => $this->options['multi_type'],
+ '#fieldset' => 'multi_value_settings',
+ '#weight' => 0,
+ ];
+ $form['multi_separator'] = [
+ '#type' => 'textfield',
+ '#title' => $this->t('Separator'),
+ '#default_value' => $this->options['multi_separator'],
+ '#states' => [
+ 'visible' => [
+ ':input[name="options[multi_type]"]' => ['value' => 'separator'],
+ ],
+ ],
+ '#fieldset' => 'multi_value_settings',
+ '#weight' => 1,
+ ];
+ }
+ }
+
+ /**
+ * Adds an ORDER BY clause to the query for click sort columns.
+ *
+ * @param string $order
+ * Either "ASC" or "DESC".
+ *
+ * @see \Drupal\views\Plugin\views\field\FieldHandlerInterface::clickSort()
+ */
+ public function clickSort($order) {
+ $this->getQuery()->sort($this->definition['search_api field'], $order);
+ }
+
+ /**
+ * Determines if this field is click sortable.
+ *
+ * This is the case if this Views field is linked to a certain Search API
+ * field.
+ *
+ * @return bool
+ * TRUE if this field is available for click-sorting, FALSE otherwise.
+ *
+ * @see \Drupal\views\Plugin\views\field\FieldHandlerInterface::clickSortable()
+ */
+ public function clickSortable() {
+ return !empty($this->definition['search_api field']);
+ }
+
+ /**
+ * Add anything to the query that we might need to.
+ *
+ * @see \Drupal\views\Plugin\views\ViewsPluginInterface::query()
+ */
+ public function query() {
+ $combined_property_path = $this->getCombinedPropertyPath();
+ $this->addRetrievedProperty($combined_property_path);
+ if ($this->options['link_to_item']) {
+ $this->addRetrievedProperty("$combined_property_path:_object");
+ }
+ }
+
+ /**
+ * Adds a property to be retrieved.
+ *
+ * @param string $combined_property_path
+ * The combined property path of the property that should be retrieved.
+ * "_object" can be used as a property name to indicate the loaded object is
+ * required.
+ *
+ * @return $this
+ */
+ protected function addRetrievedProperty($combined_property_path) {
+ $this->getQuery()->addRetrievedProperty($combined_property_path);
+
+ list($datasource_id, $property_path) = Utility::splitCombinedId($combined_property_path);
+ $this->retrievedProperties[$datasource_id][$property_path] = $combined_property_path;
+ return $this;
+ }
+
+ /**
+ * Gets the entity matching the current row and relationship.
+ *
+ * @param \Drupal\views\ResultRow $values
+ * An object containing all retrieved values.
+ *
+ * @return \Drupal\Core\Entity\EntityInterface
+ * Returns the entity matching the values.
+ *
+ * @see \Drupal\views\Plugin\views\field\FieldHandlerInterface::getEntity()
+ */
+ public function getEntity(ResultRow $values) {
+ $combined_property_path = $this->getCombinedPropertyPath();
+ list($datasource_id, $property_path) = Utility::splitCombinedId($combined_property_path);
+
+ if ($values->search_api_datasource !== $datasource_id) {
+ return NULL;
+ }
+
+ $value_index = $this->valueIndex;
+ // Only try two levels. Otherwise, we might end up at an entirely different
+ // entity, cause we go too far up.
+ $levels = 2;
+ while ($levels--) {
+ if (!empty($values->_relationship_objects[$combined_property_path][$value_index])) {
+ /** @var \Drupal\Core\TypedData\TypedDataInterface $object */
+ $object = $values->_relationship_objects[$combined_property_path][$value_index];
+ $value = $object->getValue();
+ if ($value instanceof EntityInterface) {
+ return $value;
+ }
+ }
+
+ if (!$property_path) {
+ break;
+ }
+ // For multi-valued fields, the parent's index is not the same as the
+ // field value's index.
+ if (!empty($values->_relationship_parent_indices[$combined_property_path][$value_index])) {
+ $value_index = $values->_relationship_parent_indices[$combined_property_path][$value_index];
+ }
+ list($property_path) = Utility::splitPropertyPath($property_path);
+ $combined_property_path = $this->createCombinedPropertyPath($datasource_id, $property_path);
+ }
+
+ return NULL;
+ }
+
+ /**
+ * Gets the value that's supposed to be rendered.
+ *
+ * This API exists so that other modules can easily set the values of the
+ * field without having the need to change the render method as well.
+ *
+ * Overridden here to provide an easy way to let this method return arbitrary
+ * values, without actually touching the $values array.
+ *
+ * @param \Drupal\views\ResultRow $values
+ * An object containing all retrieved values.
+ * @param string $field
+ * Optional name of the field where the value is stored.
+ *
+ * @see \Drupal\views\Plugin\views\field\FieldHandlerInterface::getValue()
+ */
+ public function getValue(ResultRow $values, $field = NULL) {
+ if (isset($this->overriddenValues[$field])) {
+ return $this->overriddenValues[$field];
+ }
+
+ return parent::getValue($values, $field);
+ }
+
+ /**
+ * Runs before any fields are rendered.
+ *
+ * This gives the handlers some time to set up before any handler has
+ * been rendered.
+ *
+ * @param \Drupal\views\ResultRow[]|\ArrayAccess $values
+ * An array of all ResultRow objects returned from the query.
+ *
+ * @see \Drupal\views\Plugin\views\field\FieldHandlerInterface::preRender()
+ */
+ public function preRender(&$values) {
+ // We deal with the properties one by one, always loading the necessary
+ // values for any nested properties coming afterwards.
+ foreach ($this->expandRequiredProperties() as $properties) {
+ foreach ($properties as $property_path => $info) {
+ $combined_property_path = $info['combined_property_path'];
+ $dependents = $info['dependents'];
+
+ if ($combined_property_path === NULL) {
+ $this->preLoadResultItems($values, $dependents);
+ continue;
+ }
+
+ $property_values = $this->getValuesToExtract($values, $combined_property_path, $dependents);
+ $this->extractPropertyValues($values, $combined_property_path, $property_values, $dependents);
+ $this->checkHighlighting($values, $combined_property_path);
+ }
+ }
+ }
+
+ /**
+ * Expands the properties to retrieve for this field.
+ *
+ * The properties are taken from this object's $retrievedProperties property,
+ * with all their ancestors also added to the array, with the ancestor
+ * properties always ordered before their descendants.
+ *
+ * This will ensure, when dealing with these properties sequentially, that
+ * the parent object necessary to load the "child" property is always already
+ * loaded.
+ *
+ * @return array[][]
+ * The properties to retrieve, keyed by their datasource ID and property
+ * path. The values are associative arrays with the following keys:
+ * - combined_property_path: The "combined property path" of the retrieved
+ * property.
+ * - dependents: An array containing the originally required properties that
+ * led to this property being required.
+ */
+ protected function expandRequiredProperties() {
+ $required_properties = [];
+ foreach ($this->retrievedProperties as $datasource_id => $properties) {
+ if ($datasource_id === '') {
+ $datasource_id = NULL;
+ }
+ foreach ($properties as $property_path => $combined_property_path) {
+ $paths_to_add = [NULL];
+ $path_to_add = '';
+ foreach (explode(':', $property_path) as $component) {
+ $path_to_add .= ($path_to_add ? ':' : '') . $component;
+ $paths_to_add[] = $path_to_add;
+ }
+ foreach ($paths_to_add as $path_to_add) {
+ if (!isset($required_properties[$datasource_id][$path_to_add])) {
+ $required_properties[$datasource_id][$path_to_add] = [
+ 'combined_property_path' => $this->createCombinedPropertyPath($datasource_id, $path_to_add),
+ 'dependents' => [],
+ ];
+ }
+ $required_properties[$datasource_id][$path_to_add]['dependents'][] = $combined_property_path;
+ }
+ }
+ }
+ return $required_properties;
+ }
+
+ /**
+ * Pre-loads the result objects, where necessary.
+ *
+ * @param \Drupal\views\ResultRow[] $values
+ * The Views result rows for which result objects should be loaded.
+ * @param string[] $dependents
+ * The actually required properties (as combined property paths) that
+ * depend on the result objects.
+ */
+ protected function preLoadResultItems(array $values, array $dependents) {
+ $to_load = [];
+ foreach ($values as $i => $row) {
+ // If the object is already set on the result row, we've got nothing to do
+ // here.
+ if (!empty($row->_object)) {
+ continue;
+ }
+ // Same if the object was loaded on the result item already.
+ $object = $row->_item->getOriginalObject(FALSE);
+ if ($object) {
+ $row->_object = $object;
+ $row->_relationship_objects[NULL] = [$object];
+ continue;
+ }
+ // We also don't need to load the object if all field values that depend
+ // on it are already present on the result row.
+ $required = FALSE;
+ foreach ($dependents as $dependent) {
+ if (!isset($row->$dependent)) {
+ $required = TRUE;
+ break;
+ }
+ }
+ if (!$required) {
+ continue;
+ }
+
+ $to_load[$row->search_api_id] = $i;
+ }
+
+ if (!$to_load) {
+ return;
+ }
+
+ $items = $this->getIndex()->loadItemsMultiple(array_keys($to_load));
+ foreach ($to_load as $item_id => $i) {
+ if (!empty($items[$item_id])) {
+ $values[$i]->_object = $items[$item_id];
+ $values[$i]->_relationship_objects[NULL] = [$items[$item_id]];
+ }
+ }
+ }
+
+ /**
+ * Determines and prepares the property values that need to be extracted.
+ *
+ * @param \Drupal\views\ResultRow[] $values
+ * The Views result rows from which property values should be extracted.
+ * @param string $combined_property_path
+ * The combined property path of the property to extract. Or NULL to extract
+ * the result item.
+ * @param string[] $dependents
+ * The actually required properties (as combined property paths) that
+ * depend on this property.
+ *
+ * @return \Drupal\Core\TypedData\TypedDataInterface[][]
+ * The values of the property for each result row, keyed by result row
+ * index.
+ */
+ protected function getValuesToExtract(array $values, $combined_property_path, array $dependents) {
+ list ($datasource_id, $property_path) = Utility::splitCombinedId($combined_property_path);
+
+ // Determine the path of the parent property, and the property key to
+ // take from it for this property.
+ list($parent_path, $name) = Utility::splitPropertyPath($property_path);
+ $combined_parent_path = $this->createCombinedPropertyPath($datasource_id, $parent_path);
+
+ // For top-level properties, we need the definition to check whether its
+ // a processor-generated property later.
+ $property = NULL;
+ if (!$parent_path) {
+ $datasource_properties = $this->getIndex()
+ ->getPropertyDefinitions($datasource_id);
+ if (isset($datasource_properties[$name])) {
+ $property = $datasource_properties[$name];
+ }
+ }
+
+ // Now go through all rows and add the property to them, if necessary.
+ // We then extract the actual values in a second pass in order to be
+ // able to use multi-loading for any encountered entities.
+ /** @var \Drupal\Core\TypedData\TypedDataInterface[][] $property_values */
+ $property_values = [];
+ $entities_to_load = [];
+ foreach ($values as $i => $row) {
+ // Bail for rows with the wrong datasource for this property, or for
+ // which this field doesn't even apply (which will usually be the
+ // same, though).
+ if (($datasource_id && $datasource_id !== $row->search_api_datasource)
+ || !$this->isActiveForRow($row)) {
+ continue;
+ }
+
+ // Then, make sure we even need this property for the current row.
+ // (Will not be the case if all required properties that depend on
+ // this property were already set on the row previously.)
+ $required = FALSE;
+ foreach ($dependents as $dependent) {
+ if (!isset($row->$dependent)) {
+ $required = TRUE;
+ break;
+ }
+ }
+ if (!$required) {
+ continue;
+ }
+
+ // Check whether there are parent objects present. Otherwise, nothing we
+ // can do here.
+ if (empty($row->_relationship_objects[$combined_parent_path])) {
+ continue;
+ }
+
+ // If the property key is "_object", we only needed to load the parent
+ // object(s), so we just copy those to the result row object and we're
+ // done.
+ if ($name === '_object') {
+ // The $row->_object is special, since we also set it in
+ // \Drupal\search_api\Plugin\views\query\SearchApiQuery::addResults()
+ // (conditionally). To keep it consistent, we make it single-valued
+ // here, too.
+ if ($combined_property_path !== '_object') {
+ $row->$combined_property_path = $row->_relationship_objects[$combined_parent_path];
+ }
+ continue;
+ }
+
+ if (empty($row->_relationship_objects[$combined_property_path])) {
+ // Check whether this is a processor-generated property and use
+ // special code to retrieve it in that case.
+ if ($property instanceof ProcessorPropertyInterface) {
+ // Determine whether this property is required.
+ $is_required = in_array($combined_property_path, $dependents);
+ $this->extractProcessorProperty($property, $row, $combined_property_path, $is_required);
+ continue;
+ }
+
+ foreach ($row->_relationship_objects[$combined_parent_path] as $j => $parent) {
+ // Follow references.
+ while ($parent instanceof DataReferenceInterface) {
+ $parent = $parent->getTarget();
+ }
+
+ // At this point we need the parent to be a complex item,
+ // otherwise it can't have any children (and thus, our property
+ // can't be present).
+ if (!($parent instanceof ComplexDataInterface)) {
+ continue;
+ }
+
+ try {
+ // Retrieve the actual typed data for the property and add it to
+ // our property values.
+ $typed_data = $parent->get($name);
+ $property_values[$i][$j] = $typed_data;
+ // Remember any encountered entity references so we can
+ // multi-load them.
+ if ($typed_data instanceof DataReferenceInterface) {
+ /** @var \Drupal\Core\TypedData\DataReferenceDefinitionInterface $definition */
+ $definition = $typed_data->getDataDefinition();
+ $definition = $definition->getTargetDefinition();
+ if ($definition instanceof EntityDataDefinitionInterface) {
+ $entity_type_id = $definition->getEntityTypeId();
+ $entity_type = $this->getEntityTypeManager()
+ ->getDefinition($entity_type_id);
+ if ($entity_type->isStaticallyCacheable()) {
+ $entity_id = $typed_data->getTargetIdentifier();
+ $entities_to_load[$entity_type_id][$entity_id] = $entity_id;
+ }
+ }
+ }
+ }
+ catch (\InvalidArgumentException $e) {
+ // This can easily happen, for example, when requesting a field
+ // that only exists on a different bundle. Unfortunately, there
+ // is no ComplexDataInterface::hasProperty() method, so we can
+ // only catch and ignore the exception.
+ }
+ }
+ }
+ }
+
+ // Multi-load all entities we encountered before.
+ foreach ($entities_to_load as $entity_type_id => $ids) {
+ $this->getEntityTypeManager()
+ ->getStorage($entity_type_id)
+ ->loadMultiple($ids);
+ }
+
+ return $property_values;
+ }
+
+ /**
+ * Extracts a processor-based property from an item.
+ *
+ * @param \Drupal\search_api\Processor\ProcessorPropertyInterface $property
+ * The property definition.
+ * @param \Drupal\views\ResultRow $row
+ * The Views result row.
+ * @param string $combined_property_path
+ * The combined property path of the property to set.
+ * @param bool $is_required
+ * TRUE if the property is directly required, FALSE if it should only be
+ * extracted because some child/ancestor properties are required.
+ */
+ protected function extractProcessorProperty(ProcessorPropertyInterface $property, ResultRow $row, $combined_property_path, $is_required) {
+ $index = $this->getIndex();
+ $processor = $index->getProcessor($property->getProcessorId());
+ if (!$processor) {
+ return;
+ }
+
+ list($datasource_id, $property_path) = Utility::splitCombinedId($combined_property_path);
+
+ // We need to call the processor's addFieldValues() method in order to get
+ // the field value. We do this using a clone of the search item so as to
+ // preserve the original state of the item. We also use a dummy field
+ // object – either a clone of a fitting indexed field (to get its
+ // configuration), or a newly created one.
+ $property_fields = $this->getFieldsHelper()
+ ->filterForPropertyPath($index->getFields(), $datasource_id, $property_path);
+ if ($property_fields) {
+ if (!empty($this->definition['search_api field'])
+ && !empty($property_fields[$this->definition['search_api field']])) {
+ $field_id = $this->definition['search_api field'];
+ $dummy_field = $property_fields[$field_id];
+ // In case this field is also configurable, create a new, unique
+ // combined property path for this field so adding multiple fields based
+ // on the same property works correctly.
+ if ($property instanceof ConfigurablePropertyInterface) {
+ $new_path = $combined_property_path . '|' . $field_id;
+ $this->propertyReplacements[$combined_property_path] = $new_path;
+ $combined_property_path = $new_path;
+ }
+ }
+ else {
+ $dummy_field = reset($property_fields);
+ }
+ $dummy_field = clone $dummy_field;
+ }
+ else {
+ $dummy_field = $this->getFieldsHelper()
+ ->createFieldFromProperty($index, $property, $datasource_id, $property_path, 'tmp', 'string');
+ }
+ /** @var \Drupal\search_api\Item\ItemInterface $dummy_item */
+ $dummy_item = clone $row->_item;
+ $dummy_item->setFields([
+ 'tmp' => $dummy_field,
+ ]);
+ $dummy_item->setFieldsExtracted(TRUE);
+
+ $processor->addFieldValues($dummy_item);
+
+ $row->_relationship_objects[$combined_property_path] = [];
+ $set_values = $is_required && !isset($row->{$combined_property_path});
+ if ($set_values) {
+ $row->$combined_property_path = [];
+ }
+ foreach ($dummy_field->getValues() as $value) {
+ if (!$this->checkEntityAccess($value, $combined_property_path)) {
+ continue;
+ }
+ if ($set_values) {
+ $row->{$combined_property_path}[] = $value;
+ }
+ $typed_data = $this->getTypedDataManager()
+ ->create($property, $value);
+ $row->_relationship_objects[$combined_property_path][] = $typed_data;
+ // Processor-generated properties always have just a single parent: the
+ // result item itself. Therefore, the parent's index is always 0.
+ $row->_relationship_parent_indices[$combined_property_path][] = 0;
+ }
+ }
+
+ /**
+ * Places extracted property values and objects into the result row.
+ *
+ * @param \Drupal\views\ResultRow[] $values
+ * The Views result rows from which property values should be extracted.
+ * @param string $combined_property_path
+ * The combined property path of the property to extract.
+ * @param \Drupal\Core\TypedData\TypedDataInterface[][] $property_values
+ * The values of the property for each result row, keyed by result row
+ * index.
+ * @param string[] $dependents
+ * The actually required properties (as combined property paths) that
+ * depend on this property.
+ */
+ protected function extractPropertyValues(array $values, $combined_property_path, array $property_values, array $dependents) {
+ // Now go through the rows a second time and actually add all objects
+ // and (if necessary) properties.
+ foreach ($values as $i => $row) {
+ if (!empty($property_values[$i])) {
+ // Add the typed data for the property to our relationship objects
+ // for this property path.
+ $row->_relationship_objects[$combined_property_path] = [];
+ foreach ($property_values[$i] as $j => $typed_data) {
+ // If the typed data is an entity, check whether the current
+ // user can access it (and switch to the right translation, if
+ // available).
+ $value = $typed_data->getValue();
+ if ($value instanceof EntityInterface) {
+ if (!$this->checkEntityAccess($value, $combined_property_path)) {
+ continue;
+ }
+ if ($value instanceof TranslatableInterface
+ && $value->hasTranslation($row->search_api_language)) {
+ // PhpStorm isn't able to keep both interfaces in mind at the same
+ // time, so we need to use a third interface here that combines
+ // both.
+ /** @var \Drupal\Core\Entity\ContentEntityInterface $value */
+ $typed_data = $value->getTranslation($row->search_api_language)
+ ->getTypedData();
+ }
+ }
+
+ // To treat list properties correctly regarding possible child
+ // properties, add all the list items individually.
+ if ($typed_data instanceof ListInterface) {
+ foreach ($typed_data as $item) {
+ $row->_relationship_objects[$combined_property_path][] = $item;
+ $row->_relationship_parent_indices[$combined_property_path][] = $j;
+ }
+ }
+ else {
+ $row->_relationship_objects[$combined_property_path][] = $typed_data;
+ $row->_relationship_parent_indices[$combined_property_path][] = $j;
+ }
+ }
+ }
+
+ // Determine whether we want to set field values for this property on this
+ // row. This is the case if the property is one of the explicitly
+ // retrieved properties and not yet set on the result row object. Also, if
+ // we have no objects for this property, we needn't bother anyways, of
+ // course.
+ if (!in_array($combined_property_path, $dependents)
+ || isset($row->$combined_property_path)
+ || empty($row->_relationship_objects[$combined_property_path])) {
+ continue;
+ }
+
+ $row->$combined_property_path = [];
+
+ // Iterate over the typed data objects, extract their values and set
+ // the relationship objects for the next iteration of the outer loop
+ // over properties.
+ foreach ($row->_relationship_objects[$combined_property_path] as $typed_data) {
+ $row->{$combined_property_path}[] = $this->getFieldsHelper()
+ ->extractFieldValues($typed_data);
+ }
+
+ // If we just set any field values on the result row, clean them up
+ // by merging them together (currently it's an array of arrays, but
+ // it should be just a flat array).
+ if ($row->$combined_property_path) {
+ $row->$combined_property_path = call_user_func_array('array_merge', $row->$combined_property_path);
+ }
+ }
+ }
+
+ /**
+ * Replaces extracted property values with highlighted field values.
+ *
+ * @param \Drupal\views\ResultRow[] $values
+ * The Views result rows for which highlighted field values should be added
+ * where applicable and possible.
+ * @param string $combined_property_path
+ * The combined property path of the property for which to add data.
+ */
+ protected function checkHighlighting(array $values, $combined_property_path) {
+ // If using highlighting data wasn't enabled, we can skip all of this
+ // anyways.
+ if (empty($this->options['use_highlighting'])) {
+ return;
+ }
+
+ // Since (currently) only fields can be highlighted, not arbitrary
+ // properties, we needn't even bother if there are no matching fields.
+ $fields = $this->getFieldsForPropertyPath($combined_property_path);
+ if (!$fields) {
+ return;
+ }
+
+ if (!empty($this->propertyReplacements[$combined_property_path])) {
+ $combined_property_path = $this->propertyReplacements[$combined_property_path];
+ }
+
+ foreach ($values as $row) {
+ // We only want highlighting data if we even wanted (and, thus, extracted)
+ // the property's values in the first place.
+ if (!isset($row->$combined_property_path)) {
+ continue;
+ }
+ $highlighted_data = $row->_item->getExtraData('highlighted_fields');
+ if (!$highlighted_data) {
+ continue;
+ }
+ $highlighted_data = array_intersect_key($highlighted_data, $fields);
+ if ($highlighted_data) {
+ // There might be multiple fields with highlight data here, in rare
+ // cases, but it's unclear how to combine them, or choose one over the
+ // other, anyways, so just take the first one.
+ $values = reset($highlighted_data);
+ $values = $this->combineHighlightedValues($row->$combined_property_path, $values);
+ $row->$combined_property_path = $values;
+ }
+ }
+ }
+
+ /**
+ * Retrieves all of the index's fields that match the given property path.
+ *
+ * @param string $combined_property_path
+ * The combined property path to look for.
+ *
+ * @return \Drupal\search_api\Item\FieldInterface[]
+ * All index fields with the given combined property path, keyed by field
+ * ID.
+ */
+ protected function getFieldsForPropertyPath($combined_property_path) {
+ if (!isset($this->fieldsByCombinedPropertyPath)) {
+ $this->fieldsByCombinedPropertyPath = [];
+ foreach ($this->getIndex()->getFields() as $field_id => $field) {
+ $this->fieldsByCombinedPropertyPath[$field->getCombinedPropertyPath()][$field_id] = $field;
+ }
+ }
+ $this->fieldsByCombinedPropertyPath += [$combined_property_path => []];
+ return $this->fieldsByCombinedPropertyPath[$combined_property_path];
+ }
+
+ /**
+ * Combines raw field values with highlighted ones to get a complete set.
+ *
+ * If highlighted field values are set on the result item, not all values
+ * might be included, but only the ones with matches. Since we still want to
+ * show all values, of course, we need to combine the highlighted values with
+ * the ones we extracted.
+ *
+ * @param array $extracted_values
+ * All values for a field.
+ * @param array $highlighted_values
+ * A subset of field values that are highlighted.
+ *
+ * @return array
+ * An array of normal and highlighted field values, avoiding duplicates as
+ * well as possible.
+ */
+ protected function combineHighlightedValues(array $extracted_values, array $highlighted_values) {
+ // Make sure the arrays have consecutive numeric indices. (Is always the
+ // case for $extracted_values.)
+ $highlighted_values = array_values($highlighted_values);
+
+ // Pre-sanitize the highlighted values with a very permissive setting to
+ // make sure the highlighting HTML won't be escaped later.
+ foreach ($highlighted_values as $i => $value) {
+ if (!($value instanceof MarkupInterface)) {
+ $highlighted_values[$i] = $this->sanitizeValue($value, 'xss_admin');
+ }
+ }
+
+ $extracted_count = count($extracted_values);
+ $highlight_count = count($highlighted_values);
+ // If there are (at least) as many highlighted values as normal ones, we are
+ // done here.
+ if ($highlight_count >= $extracted_count) {
+ return $highlighted_values;
+ }
+
+ // We now compute a "normalized" representation for all (extracted and
+ // highlighted) values to be able to find duplicates.
+ $normalize = function ($value) {
+ $value = (string) $value;
+ $value = strip_tags($value);
+ $value = html_entity_decode($value);
+ return $value;
+ };
+ $normalized_extracted = array_map($normalize, $extracted_values);
+ $normalized_highlighted = array_map($normalize, $highlighted_values);
+ $normalized_extracted = array_diff($normalized_extracted, $normalized_highlighted);
+
+ // Make sure that we have no more than $extracted_count values in total.
+ while (count($normalized_extracted) + $highlight_count > $extracted_count) {
+ array_pop($normalized_extracted);
+ }
+
+ // Now combine the two arrays, maintaining the original order by taking a
+ // highlighted value only where the extracted value was removed (probably/
+ // hopefully by the array_diff()).
+ $values = [];
+ for ($i = 0; $i < $extracted_count; ++$i) {
+ if (isset($normalized_extracted[$i])) {
+ $values[] = $extracted_values[$i];
+ }
+ else {
+ $values[] = array_shift($highlighted_values);
+ }
+ }
+ return $values;
+ }
+
+ /**
+ * Determines whether this field is active for the given row.
+ *
+ * This is usually determined by the row's datasource.
+ *
+ * @param \Drupal\views\ResultRow $row
+ * The result row.
+ *
+ * @return bool
+ * TRUE if this field handler might produce output for the given row, FALSE
+ * otherwise.
+ */
+ protected function isActiveForRow(ResultRow $row) {
+ $datasource_ids = [NULL, $row->search_api_datasource];
+ return in_array($this->getDatasourceId(), $datasource_ids, TRUE);
+ }
+
+ /**
+ * Checks whether the searching user has access to the given value.
+ *
+ * If the value is not an entity, this will always return TRUE.
+ *
+ * @param mixed $value
+ * The value to check.
+ * @param string $property_path
+ * The property path of the value.
+ *
+ * @return bool
+ * TRUE if the value is not an entity, or the searching user has access to
+ * it; FALSE otherwise.
+ */
+ protected function checkEntityAccess($value, $property_path) {
+ if (!($value instanceof EntityInterface)) {
+ return TRUE;
+ }
+ if (!empty($this->skipAccessChecks[$property_path])) {
+ return TRUE;
+ }
+ if (!isset($this->accessAccount)) {
+ $this->accessAccount = $this->getQuery()->getAccessAccount() ?: FALSE;
+ }
+ return $value->access('view', $this->accessAccount ?: NULL);
+ }
+
+ /**
+ * Retrieves the combined property path of this field.
+ *
+ * @return string
+ * The combined property path.
+ */
+ public function getCombinedPropertyPath() {
+ if (!isset($this->combinedPropertyPath)) {
+ // Add the property path of any relationships used to arrive at this
+ // field.
+ $path = $this->realField;
+ $relationships = $this->view->relationship;
+ $relationship = $this;
+ // While doing this, also note which relationships are configured to skip
+ // access checks.
+ $skip_access = [];
+ while (!empty($relationship->options['relationship'])) {
+ if (empty($relationships[$relationship->options['relationship']])) {
+ break;
+ }
+ $relationship = $relationships[$relationship->options['relationship']];
+ $path = $relationship->realField . ':' . $path;
+
+ foreach ($skip_access as $i => $temp_path) {
+ $skip_access[$i] = $relationship->realField . ':' . $temp_path;
+ }
+ if (!empty($relationship->options['skip_access'])) {
+ $skip_access[] = $relationship->realField;
+ }
+ }
+ $this->combinedPropertyPath = $path;
+ // Set the field alias to the combined property path so that Views' code
+ // can find the raw values, if necessary.
+ $this->field_alias = $path;
+ // Set the property paths that should skip access checks.
+ $this->skipAccessChecks = array_fill_keys($skip_access, TRUE);
+ }
+ return $this->combinedPropertyPath;
+ }
+
+ /**
+ * Creates a combined property path.
+ *
+ * A combined property path is similar to a "combined ID" in that it contains
+ * information about both the datasource and the property path on that
+ * datasource.
+ *
+ * The difference is that a combined property path, as used in this class, can
+ * be NULL (to reference the original result item).
+ *
+ * @param string|null $datasource_id
+ * The datasource ID, or NULL for a datasource-independent property.
+ * @param string|null $property_path
+ * The property path from the result item to the specified property, or NULL
+ * to reference the result item.
+ *
+ * @return string|null
+ * The combined property path.
+ */
+ protected function createCombinedPropertyPath($datasource_id, $property_path) {
+ if ($property_path === NULL) {
+ return NULL;
+ }
+ return Utility::createCombinedId($datasource_id, $property_path);
+ }
+
+ /**
+ * Retrieves the ID of the datasource to which this field belongs.
+ *
+ * @return string|null
+ * The datasource ID of this field, or NULL if it doesn't belong to a
+ * specific datasource.
+ */
+ public function getDatasourceId() {
+ if (!isset($this->datasourceId)) {
+ list($this->datasourceId) = Utility::splitCombinedId($this->getCombinedPropertyPath());
+ }
+ return $this->datasourceId;
+ }
+
+ /**
+ * Renders a single item of a row.
+ *
+ * @param int $count
+ * The index of the item inside the row.
+ * @param mixed $item
+ * The item for the field to render.
+ *
+ * @return string
+ * The rendered output.
+ *
+ * @see \Drupal\views\Plugin\views\field\MultiItemsFieldHandlerInterface::render_item()
+ */
+ public function render_item($count, $item) {
+ $this->overriddenValues[NULL] = $item['value'];
+ $render = $this->render(new ResultRow());
+ $this->overriddenValues = [];
+ return $render;
+ }
+
+ /**
+ * Gets an array of items for the field.
+ *
+ * Items should be associative arrays with, if possible, "value" as the actual
+ * displayable value of the item, plus any items that might be found in the
+ * "alter" options array for creating links, etc., such as "path", "fragment",
+ * "query", etc. Additionally, items that might be turned into tokens should
+ * also be in this array.
+ *
+ * @param \Drupal\views\ResultRow $values
+ * The result row object containing the values.
+ *
+ * @return array[]
+ * An array of items for the field, with each item being an array itself.
+ *
+ * @see \Drupal\views\Plugin\views\field\PrerenderList::getItems()
+ */
+ public function getItems(ResultRow $values) {
+ $property_path = $this->getCombinedPropertyPath();
+ if (!empty($this->propertyReplacements[$property_path])) {
+ $property_path = $this->propertyReplacements[$property_path];
+ }
+ if (!empty($values->$property_path)) {
+ // Although it's undocumented, the field handler base class assumes items
+ // will always be arrays. See #2648012 for documenting this.
+ $items = [];
+ foreach ((array) $values->$property_path as $i => $value) {
+ $item = [
+ 'value' => $value,
+ ];
+
+ if ($this->options['link_to_item']) {
+ $item['make_link'] = TRUE;
+ $item['url'] = $this->getItemUrl($values, $i);
+ }
+
+ $items[] = $item;
+ }
+ return $items;
+ }
+ return [];
+ }
+
+ /**
+ * Renders all items in this field together.
+ *
+ * @param array|\ArrayAccess $items
+ * The items provided by getItems() for a single row.
+ *
+ * @return string
+ * The rendered items.
+ *
+ * @see \Drupal\views\Plugin\views\field\PrerenderList::renderItems()
+ */
+ public function renderItems($items) {
+ if (!empty($items)) {
+ if ($this->options['multi_type'] == 'separator') {
+ $render = [
+ '#type' => 'inline_template',
+ '#template' => '{{ items|safe_join(separator) }}',
+ '#context' => [
+ 'items' => $items,
+ 'separator' => $this->sanitizeValue($this->options['multi_separator'], 'xss_admin'),
+ ],
+ ];
+ }
+ else {
+ $render = [
+ '#theme' => 'item_list',
+ '#items' => $items,
+ '#title' => NULL,
+ '#list_type' => $this->options['multi_type'],
+ ];
+ }
+ return $this->getRenderer()->render($render);
+ }
+ return '';
+ }
+
+ /**
+ * Sanitizes the value for output.
+ *
+ * @param mixed $value
+ * The value being rendered.
+ * @param string|null $type
+ * (optional) The type of sanitization needed. If not provided,
+ * \Drupal\Component\Utility\Html::escape() is used.
+ *
+ * @return \Drupal\views\Render\ViewsRenderPipelineMarkup
+ * Returns the safe value.
+ *
+ * @see \Drupal\views\Plugin\views\HandlerBase::sanitizeValue()
+ */
+ public function sanitizeValue($value, $type = NULL) {
+ // Pass-through values that are already markup objects.
+ if ($value instanceof MarkupInterface) {
+ return $value;
+ }
+ return parent::sanitizeValue($value, $type);
+ }
+
+ /**
+ * Retrieves an alter options array for linking the given value to its item.
+ *
+ * @param \Drupal\views\ResultRow $row
+ * The Views result row object.
+ * @param int $i
+ * The index in this field's values for which the item link should be
+ * retrieved.
+ *
+ * @return \Drupal\Core\Url|null
+ * The URL for the specified item, or NULL if it couldn't be found.
+ */
+ protected function getItemUrl(ResultRow $row, $i) {
+ $this->valueIndex = $i;
+ if ($entity = $this->getEntity($row)) {
+ return $entity->toUrl('canonical');
+ }
+
+ if (!empty($row->_relationship_objects[NULL][0])) {
+ return $this->getIndex()
+ ->getDatasource($row->search_api_datasource)
+ ->getItemUrl($row->_relationship_objects[NULL][0]);
+ }
+
+ return NULL;
+ }
+
+ /**
+ * Returns the Render API renderer.
+ *
+ * @return \Drupal\Core\Render\RendererInterface
+ * The renderer.
+ *
+ * @see \Drupal\views\Plugin\views\field\FieldPluginBase::getRenderer()
+ */
+ abstract protected function getRenderer();
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Plugin/views/field/SearchApiNumeric.php b/sites/all/modules/contrib/search/search_api/src/Plugin/views/field/SearchApiNumeric.php
new file mode 100644
index 000000000..e03338814
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Plugin/views/field/SearchApiNumeric.php
@@ -0,0 +1,49 @@
+traitDefineOptions();
+
+ $options['format_plural_values'] = ['default' => []];
+
+ return $options;
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Plugin/views/field/SearchApiStandard.php b/sites/all/modules/contrib/search/search_api/src/Plugin/views/field/SearchApiStandard.php
new file mode 100644
index 000000000..cb1356d5a
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Plugin/views/field/SearchApiStandard.php
@@ -0,0 +1,27 @@
+definition['filter_type']) ? $this->definition['filter_type'] : 'plain';
+ return $this->sanitizeValue($item['value'], $type);
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Plugin/views/filter/SearchApiBoolean.php b/sites/all/modules/contrib/search/search_api/src/Plugin/views/filter/SearchApiBoolean.php
new file mode 100644
index 000000000..35abf8172
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Plugin/views/filter/SearchApiBoolean.php
@@ -0,0 +1,18 @@
+valueOptions)) {
+ return $this->valueOptions;
+ }
+
+ $this->valueOptions = [];
+
+ $index = $this->getIndex();
+ if ($index) {
+ foreach ($index->getDatasources() as $datasource_id => $datasource) {
+ $this->valueOptions[$datasource_id] = $datasource->label();
+ }
+ }
+
+ return $this->valueOptions;
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Plugin/views/filter/SearchApiDate.php b/sites/all/modules/contrib/search/search_api/src/Plugin/views/filter/SearchApiDate.php
new file mode 100644
index 000000000..7fe9975cd
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Plugin/views/filter/SearchApiDate.php
@@ -0,0 +1,131 @@
+timeService ?: \Drupal::time();
+ }
+
+ /**
+ * Sets the time service.
+ *
+ * @param \Drupal\Component\Datetime\TimeInterface $time_service
+ * The new time service.
+ *
+ * @return $this
+ */
+ public function setTimeService(TimeInterface $time_service) {
+ $this->timeService = $time_service;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function operators() {
+ $operators = parent::operators();
+ unset($operators['regular_expression']);
+ return $operators;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function acceptExposedInput($input) {
+ if (empty($this->options['exposed'])) {
+ return TRUE;
+ }
+
+ // Unfortunately, this is necessary due to a bug in our parent filter. See
+ // #2704077.
+ if (!empty($this->options['expose']['identifier'])) {
+ $value = &$input[$this->options['expose']['identifier']];
+ if (!is_array($value)) {
+ $value = [
+ 'value' => $value,
+ ];
+ }
+ $value += [
+ 'min' => '',
+ 'max' => '',
+ ];
+ }
+
+ // Store this because it will get overwritten by the grandparent, and the
+ // parent doesn't always restore it correctly.
+ $type = $this->value['type'];
+ $return = parent::acceptExposedInput($input);
+
+ if (!$return) {
+ // If the parent returns FALSE, it doesn't restore the "type" key.
+ $this->value['type'] = $type;
+ // Override for the "(not) empty" operators.
+ $operators = $this->operators();
+ if ($operators[$this->operator]['values'] == 0) {
+ return TRUE;
+ }
+ }
+
+ return $return;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function opBetween($field) {
+ if ($this->value['type'] == 'offset') {
+ $time = $this->getTimeService()->getRequestTime();
+ $a = strtotime($this->value['min'], $time);
+ $b = strtotime($this->value['max'], $time);
+ }
+ else {
+ $a = intval(strtotime($this->value['min'], 0));
+ $b = intval(strtotime($this->value['max'], 0));
+ }
+ $real_field = $this->realField;
+ $operator = strtoupper($this->operator);
+ $group = $this->options['group'];
+ $this->getQuery()->addCondition($real_field, [$a, $b], $operator, $group);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function opSimple($field) {
+ $value = intval(strtotime($this->value['value'], 0));
+ if (!empty($this->value['type']) && $this->value['type'] == 'offset') {
+ $time = $this->getTimeService()->getRequestTime();
+ $value = strtotime($this->value['value'], $time);
+ }
+
+ $this->getQuery()->addCondition($this->realField, $value, $this->operator, $this->options['group']);
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Plugin/views/filter/SearchApiFilterTrait.php b/sites/all/modules/contrib/search/search_api/src/Plugin/views/filter/SearchApiFilterTrait.php
new file mode 100644
index 000000000..42805176c
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Plugin/views/filter/SearchApiFilterTrait.php
@@ -0,0 +1,59 @@
+operatorValues(2)) {
+ unset($form['value']['min'], $form['value']['max']);
+ }
+ }
+ }
+
+ /**
+ * Adds a filter to the search query.
+ *
+ * Overridden to avoid errors because of SQL-specific functionality being used
+ * when "Many To One" is used as a base class.
+ *
+ * @see \Drupal\views\Plugin\views\filter\ManyToOne::opHelper()
+ */
+ protected function opHelper() {
+ if (empty($this->value)) {
+ return;
+ }
+
+ if ($this->operator !== 'and') {
+ $operator = $this->operator === 'not' ? 'NOT IN' : 'IN';
+ $this->getQuery()->addCondition($this->realField, $this->value, $operator, $this->options['group']);
+ return;
+ }
+
+ $condition_group = $this->getQuery()->createConditionGroup();
+ foreach ($this->value as $value) {
+ $condition_group->addCondition($this->realField, $value, '=');
+ }
+ $this->getQuery()->addConditionGroup($condition_group, $this->options['group']);
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Plugin/views/filter/SearchApiFulltext.php b/sites/all/modules/contrib/search/search_api/src/Plugin/views/filter/SearchApiFulltext.php
new file mode 100644
index 000000000..d02be82f2
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Plugin/views/filter/SearchApiFulltext.php
@@ -0,0 +1,389 @@
+setParseModeManager($container->get('plugin.manager.search_api.parse_mode'));
+
+ return $plugin;
+ }
+
+ /**
+ * Retrieves the parse mode manager.
+ *
+ * @return \Drupal\search_api\ParseMode\ParseModePluginManager
+ * The parse mode manager.
+ */
+ public function getParseModeManager() {
+ return $this->parseModeManager ?: \Drupal::service('plugin.manager.search_api.parse_mode');
+ }
+
+ /**
+ * Sets the parse mode manager.
+ *
+ * @param \Drupal\search_api\ParseMode\ParseModePluginManager $parse_mode_manager
+ * The new parse mode manager.
+ *
+ * @return $this
+ */
+ public function setParseModeManager(ParseModePluginManager $parse_mode_manager) {
+ $this->parseModeManager = $parse_mode_manager;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function showOperatorForm(&$form, FormStateInterface $form_state) {
+ parent::showOperatorForm($form, $form_state);
+
+ if (!empty($form['operator'])) {
+ $form['operator']['#description'] = $this->t('Depending on the parse mode set, some of these options might not work as expected. Please either use "@multiple_words" as the parse mode or make sure that the filter behaves as expected for multiple words.', ['@multiple_words' => $this->t('Multiple words')]);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function operatorOptions($which = 'title') {
+ $options = [];
+ foreach ($this->operators() as $id => $info) {
+ $options[$id] = $info[$which];
+ }
+
+ return $options;
+ }
+
+ /**
+ * Returns information about the available operators for this filter.
+ *
+ * @return array[]
+ * An associative array mapping operator identifiers to their information.
+ * The operator information itself is an associative array with the
+ * following keys:
+ * - title: The translated title for the operator.
+ * - short: The translated short title for the operator.
+ * - values: The number of values the operator requires as input.
+ */
+ public function operators() {
+ return [
+ 'and' => [
+ 'title' => $this->t('Contains all of these words'),
+ 'short' => $this->t('and'),
+ 'values' => 1,
+ ],
+ 'or' => [
+ 'title' => $this->t('Contains any of these words'),
+ 'short' => $this->t('or'),
+ 'values' => 1,
+ ],
+ 'not' => [
+ 'title' => $this->t('Contains none of these words'),
+ 'short' => $this->t('not'),
+ 'values' => 1,
+ ],
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defineOptions() {
+ $options = parent::defineOptions();
+
+ $options['parse_mode'] = ['default' => 'terms'];
+ $options['operator']['default'] = 'and';
+
+ $options['min_length']['default'] = '';
+ $options['fields']['default'] = [];
+
+ return $options;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildOptionsForm(&$form, FormStateInterface $form_state) {
+ parent::buildOptionsForm($form, $form_state);
+
+ $form['parse_mode'] = [
+ '#type' => 'select',
+ '#title' => $this->t('Parse mode'),
+ '#description' => $this->t('Choose how the search keys will be parsed.'),
+ '#options' => $this->getParseModeManager()->getInstancesOptions(),
+ '#default_value' => $this->options['parse_mode'],
+ ];
+ foreach ($this->getParseModeManager()->getInstances() as $key => $mode) {
+ if ($mode->getDescription()) {
+ $states['visible'][':input[name="options[parse_mode]"]']['value'] = $key;
+ $form["parse_mode_{$key}_description"] = [
+ '#type' => 'item',
+ '#title' => $mode->label(),
+ '#description' => $mode->getDescription(),
+ '#states' => $states,
+ ];
+ }
+ }
+
+ $fields = $this->getFulltextFields();
+ if (!empty($fields)) {
+ $form['fields'] = [
+ '#type' => 'select',
+ '#title' => $this->t('Searched fields'),
+ '#description' => $this->t('Select the fields that will be searched. If no fields are selected, all available fulltext fields will be searched.'),
+ '#options' => $fields,
+ '#size' => min(4, count($fields)),
+ '#multiple' => TRUE,
+ '#default_value' => $this->options['fields'],
+ ];
+ }
+ else {
+ $form['fields'] = [
+ '#type' => 'value',
+ '#value' => [],
+ ];
+ }
+ if (isset($form['expose'])) {
+ $form['expose']['#weight'] = -5;
+ }
+
+ $form['min_length'] = [
+ '#title' => $this->t('Minimum keyword length'),
+ '#description' => $this->t('Minimum length of each word in the search keys. Leave empty to allow all words.'),
+ '#type' => 'number',
+ '#min' => 1,
+ '#default_value' => $this->options['min_length'],
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function valueForm(&$form, FormStateInterface $form_state) {
+ parent::valueForm($form, $form_state);
+
+ $form['value'] = [
+ '#type' => 'textfield',
+ '#title' => !$form_state->get('exposed') ? $this->t('Value') : '',
+ '#size' => 30,
+ '#default_value' => $this->value,
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function exposedTranslate(&$form, $type) {
+ parent::exposedTranslate($form, $type);
+
+ // We use custom validation for "required", so don't want the Form API to
+ // interfere.
+ // @see ::validateExposed()
+ $form['#required'] = FALSE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validateExposed(&$form, FormStateInterface $form_state) {
+ // Only validate exposed input.
+ if (empty($this->options['exposed']) || empty($this->options['expose']['identifier'])) {
+ return;
+ }
+
+ $identifier = $this->options['expose']['identifier'];
+ $input = &$form_state->getValue($identifier, '');
+
+ if ($this->options['is_grouped'] && isset($this->options['group_info']['group_items'][$input])) {
+ $this->operator = $this->options['group_info']['group_items'][$input]['operator'];
+ $input = &$this->options['group_info']['group_items'][$input]['value'];
+ }
+
+ // Under some circumstances, input will be an array containing the string
+ // value. Not sure why, but just deal with that.
+ while (is_array($input)) {
+ $input = $input ? reset($input) : '';
+ }
+ if (trim($input) === '') {
+ // No input was given by the user. If the filter was set to "required" and
+ // there is a query (not the case when an exposed filter block is
+ // displayed stand-alone), abort it.
+ if (!empty($this->options['expose']['required']) && $this->getQuery()) {
+ $this->getQuery()->abort();
+ }
+ // If the input is empty, there is nothing to validate: return early.
+ return;
+ }
+
+ // Only continue if there is a minimum word length set.
+ if ($this->options['min_length'] < 2) {
+ return;
+ }
+
+ $words = preg_split('/\s+/', $input);
+ foreach ($words as $i => $word) {
+ if (Unicode::strlen($word) < $this->options['min_length']) {
+ unset($words[$i]);
+ }
+ }
+ if (!$words) {
+ $vars['@count'] = $this->options['min_length'];
+ $msg = $this->t('You must include at least one positive keyword with @count characters or more.', $vars);
+ $form_state->setError($form[$identifier], $msg);
+ }
+ $input = implode(' ', $words);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function query() {
+ while (is_array($this->value)) {
+ $this->value = $this->value ? reset($this->value) : '';
+ }
+ // Catch empty strings entered by the user, but not "0".
+ if ($this->value === '') {
+ return;
+ }
+ $fields = $this->options['fields'];
+ $fields = $fields ? $fields : array_keys($this->getFulltextFields());
+ $query = $this->getQuery();
+ if ($query->shouldAbort()) {
+ return;
+ }
+
+ if ($this->options['parse_mode']) {
+ $parse_mode = $this->getParseModeManager()
+ ->createInstance($this->options['parse_mode']);
+ $query->setParseMode($parse_mode);
+ }
+
+ // If something already specifically set different fields, we silently fall
+ // back to mere filtering.
+ $old = $query->getFulltextFields();
+ $use_conditions = $old && (array_diff($old, $fields) || array_diff($fields, $old));
+
+ if ($use_conditions) {
+ $conditions = $query->createConditionGroup('OR');
+ $op = $this->operator === 'not' ? '<>' : '=';
+ foreach ($fields as $field) {
+ $conditions->addCondition($field, $this->value, $op);
+ }
+ $query->addConditionGroup($conditions);
+ return;
+ }
+
+ // If the operator was set to OR or NOT, set OR as the conjunction. It is
+ // also set for NOT since otherwise it would be "not all of these words".
+ if ($this->operator != 'and') {
+ $query->getParseMode()->setConjunction('OR');
+ }
+
+ $query->setFulltextFields($fields);
+ $old = $query->getKeys();
+ $old_original = $query->getOriginalKeys();
+ $query->keys($this->value);
+ if ($this->operator == 'not') {
+ $keys = &$query->getKeys();
+ if (is_array($keys)) {
+ $keys['#negation'] = TRUE;
+ }
+ else {
+ // We can't know how negation is expressed in the server's syntax.
+ }
+ }
+
+ // If there were fulltext keys set, we take care to combine them in a
+ // meaningful way (especially with negated keys).
+ if ($old) {
+ $keys = &$query->getKeys();
+ // Array-valued keys are combined.
+ if (is_array($keys)) {
+ // If the old keys weren't parsed into an array, we instead have to
+ // combine the original keys.
+ if (is_scalar($old)) {
+ $keys = "($old) ({$this->value})";
+ }
+ else {
+ // If the conjunction or negation settings aren't the same, we have to
+ // nest both old and new keys array.
+ if (!empty($keys['#negation']) != !empty($old['#negation']) || $keys['#conjunction'] != $old['#conjunction']) {
+ $keys = [
+ '#conjunction' => 'AND',
+ $old,
+ $keys,
+ ];
+ }
+ // Otherwise, just add all individual words from the old keys to the
+ // new ones.
+ else {
+ foreach (Element::children($old) as $i) {
+ $keys[] = $old[$i];
+ }
+ }
+ }
+ }
+ // If the parse mode was "direct" for both old and new keys, we
+ // concatenate them and set them both via method and reference (to also
+ // update the originalKeys property.
+ elseif (is_scalar($old_original)) {
+ $combined_keys = "($old_original) ($keys)";
+ $query->keys($combined_keys);
+ $keys = $combined_keys;
+ }
+ }
+ }
+
+ /**
+ * Retrieves a list of all available fulltext fields.
+ *
+ * @return string[]
+ * An options list of fulltext field identifiers mapped to their prefixed
+ * labels.
+ */
+ protected function getFulltextFields() {
+ $fields = [];
+ /** @var \Drupal\search_api\IndexInterface $index */
+ $index = Index::load(substr($this->table, 17));
+
+ $fields_info = $index->getFields();
+ foreach ($index->getFulltextFields() as $field_id) {
+ $fields[$field_id] = $fields_info[$field_id]->getPrefixedLabel();
+ }
+
+ return $fields;
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Plugin/views/filter/SearchApiLanguage.php b/sites/all/modules/contrib/search/search_api/src/Plugin/views/filter/SearchApiLanguage.php
new file mode 100644
index 000000000..b342b2731
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Plugin/views/filter/SearchApiLanguage.php
@@ -0,0 +1,43 @@
+value as $i => $value) {
+ if (isset($substitutions[$value])) {
+ $this->value[$i] = $substitutions[$value];
+ }
+ }
+
+ // Only set the languages using $query->setLanguages() if the condition
+ // would be placed directly on the query, as an AND condition.
+ $query = $this->getQuery();
+ $direct_condition = $this->operator == 'in'
+ && $query->getGroupType($this->options['group'])
+ && $query->getGroupOperator() == 'AND';
+ if ($direct_condition) {
+ $query->setLanguages($this->value);
+ }
+ else {
+ parent::query();
+ }
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Plugin/views/filter/SearchApiNumeric.php b/sites/all/modules/contrib/search/search_api/src/Plugin/views/filter/SearchApiNumeric.php
new file mode 100644
index 000000000..242a6596b
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Plugin/views/filter/SearchApiNumeric.php
@@ -0,0 +1,27 @@
+valueOptions === NULL) {
+ $this->valueOptions = [];
+ }
+
+ return $this->valueOptions;
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Plugin/views/filter/SearchApiString.php b/sites/all/modules/contrib/search/search_api/src/Plugin/views/filter/SearchApiString.php
new file mode 100644
index 000000000..545bfba89
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Plugin/views/filter/SearchApiString.php
@@ -0,0 +1,19 @@
+t('contains');
+ $operators['!=']['title'] = $this->t("doesn't contain");
+
+ $supported_operators = [
+ '=' => 1,
+ '!=' => 1,
+ 'empty' => 1,
+ 'not empty' => 1,
+ ];
+ $operators = array_intersect_key($operators, $supported_operators);
+
+ return $operators;
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Plugin/views/filter/SearchApiUser.php b/sites/all/modules/contrib/search/search_api/src/Plugin/views/filter/SearchApiUser.php
new file mode 100644
index 000000000..25f6743be
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Plugin/views/filter/SearchApiUser.php
@@ -0,0 +1,64 @@
+ [
+ 'title' => $this->t('Is one of'),
+ 'short' => $this->t('or'),
+ 'short_single' => $this->t('='),
+ 'method' => 'opHelper',
+ 'values' => 1,
+ 'ensure_my_table' => 'helper',
+ ],
+ 'and' => [
+ 'title' => $this->t('Is all of'),
+ 'short' => $this->t('and'),
+ 'short_single' => $this->t('='),
+ 'method' => 'opHelper',
+ 'values' => 1,
+ 'ensure_my_table' => 'helper',
+ ],
+ 'not' => [
+ 'title' => $this->t('Is none of'),
+ 'short' => $this->t('not'),
+ 'short_single' => $this->t('<>'),
+ 'method' => 'opHelper',
+ 'values' => 1,
+ 'ensure_my_table' => 'helper',
+ ],
+ 'empty' => [
+ 'title' => $this->t('Is empty (NULL)'),
+ 'method' => 'opEmpty',
+ 'short' => $this->t('empty'),
+ 'values' => 0,
+ ],
+ 'not empty' => [
+ 'title' => $this->t('Is not empty (NOT NULL)'),
+ 'method' => 'opEmpty',
+ 'short' => $this->t('not empty'),
+ 'values' => 0,
+ ],
+ ];
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Plugin/views/join/SearchApiJoin.php b/sites/all/modules/contrib/search/search_api/src/Plugin/views/join/SearchApiJoin.php
new file mode 100644
index 000000000..125733e65
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Plugin/views/join/SearchApiJoin.php
@@ -0,0 +1,25 @@
+setModuleHandler($container->get('module_handler'));
+ $plugin->setLogger($container->get('logger.channel.search_api'));
+
+ return $plugin;
+ }
+
+ /**
+ * Loads the search index belonging to the given Views base table.
+ *
+ * @param string $table
+ * The Views base table ID.
+ * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+ * (optional) The entity type manager to use.
+ *
+ * @return \Drupal\search_api\IndexInterface|null
+ * The requested search index, or NULL if it could not be found and loaded.
+ */
+ public static function getIndexFromTable($table, EntityTypeManagerInterface $entity_type_manager = NULL) {
+ // @todo Instead use Views::viewsData() – injected, too – to load the base
+ // table definition and use the "index" (or maybe rename to
+ // "search_api_index") field from there.
+ if (substr($table, 0, 17) == 'search_api_index_') {
+ $index_id = substr($table, 17);
+ if ($entity_type_manager) {
+ return $entity_type_manager->getStorage('search_api_index')
+ ->load($index_id);
+ }
+ return Index::load($index_id);
+ }
+ return NULL;
+ }
+
+ /**
+ * Retrieves the contained entity from a Views result row.
+ *
+ * @param \Drupal\views\ResultRow $row
+ * The Views result row.
+ * @param string $relationship_id
+ * The ID of the view relationship to use.
+ * @param \Drupal\views\ViewExecutable $view
+ * The current view object.
+ *
+ * @return \Drupal\Core\Entity\EntityInterface|null
+ * The entity contained in the result row, if any.
+ */
+ public static function getEntityFromRow(ResultRow $row, $relationship_id, ViewExecutable $view) {
+ if ($relationship_id === 'none') {
+ $object = $row->_object ?: $row->_item->getOriginalObject();
+ $entity = $object->getValue();
+ if ($entity instanceof EntityInterface) {
+ return $entity;
+ }
+ return NULL;
+ }
+
+ // To avoid code duplication, just create a dummy field handler and use it
+ // to retrieve the entity.
+ $handler = new SearchApiStandard([], '', ['title' => '']);
+ $options = ['relationship' => $relationship_id];
+ $handler->init($view, $view->display_handler, $options);
+ return $handler->getEntity($row);
+ }
+
+ /**
+ * Retrieves the module handler.
+ *
+ * @return \Drupal\Core\Extension\ModuleHandlerInterface
+ * The module handler.
+ */
+ public function getModuleHandler() {
+ return $this->moduleHandler ?: \Drupal::moduleHandler();
+ }
+
+ /**
+ * Sets the module handler.
+ *
+ * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+ * The new module handler.
+ *
+ * @return $this
+ */
+ public function setModuleHandler(ModuleHandlerInterface $module_handler) {
+ $this->moduleHandler = $module_handler;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) {
+ try {
+ parent::init($view, $display, $options);
+ $this->index = static::getIndexFromTable($view->storage->get('base_table'));
+ if (!$this->index) {
+ $this->abort(new FormattableMarkup('View %view is not based on Search API but tries to use its query plugin.', ['%view' => $view->storage->label()]));
+ }
+ $this->retrievedProperties = array_fill_keys($this->index->getDatasourceIds(), []);
+ $this->retrievedProperties[NULL] = [];
+ $this->query = $this->index->query();
+ $this->query->addTag('views');
+ $this->query->addTag('views_' . $view->id());
+ $display_type = $display->getPluginId();
+ if ($display_type === 'rest_export') {
+ $display_type = 'rest';
+ }
+ $this->query->setSearchId("views_$display_type:" . $view->id() . '__' . $view->current_display);
+ $this->query->setOption('search_api_view', $view);
+ }
+ catch (\Exception $e) {
+ $this->abort($e->getMessage());
+ }
+ }
+
+ /**
+ * Adds a property to be retrieved.
+ *
+ * Currently doesn't serve any purpose, but might be added to the search query
+ * in the future to help backends that support returning fields determine
+ * which of the fields should actually be returned.
+ *
+ * @param string $combined_property_path
+ * The combined property path of the property that should be retrieved.
+ *
+ * @return $this
+ */
+ public function addRetrievedProperty($combined_property_path) {
+ list($datasource_id, $property_path) = Utility::splitCombinedId($combined_property_path);
+ $this->retrievedProperties[$datasource_id][$property_path] = $combined_property_path;
+ return $this;
+ }
+
+ /**
+ * Adds a field to the table.
+ *
+ * This replicates the interface of Views' default SQL backend to simplify
+ * the Views integration of the Search API. If you are writing Search
+ * API-specific Views code, you should better use the addRetrievedProperty()
+ * method.
+ *
+ * @param string|null $table
+ * Ignored.
+ * @param string $field
+ * The combined property path of the property that should be retrieved.
+ * @param string $alias
+ * (optional) Ignored.
+ * @param array $params
+ * (optional) Ignored.
+ *
+ * @return string
+ * The name that this field can be referred to as (always $field).
+ *
+ * @see \Drupal\views\Plugin\views\query\Sql::addField()
+ * @see \Drupal\search_api\Plugin\views\query\SearchApiQuery::addField()
+ */
+ public function addField($table, $field, $alias = '', array $params = []) {
+ $this->addRetrievedProperty($field);
+ return $field;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defineOptions() {
+ return parent::defineOptions() + [
+ 'bypass_access' => [
+ 'default' => FALSE,
+ ],
+ 'skip_access' => [
+ 'default' => FALSE,
+ ],
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildOptionsForm(&$form, FormStateInterface $form_state) {
+ parent::buildOptionsForm($form, $form_state);
+
+ $form['skip_access'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Skip item access checks'),
+ '#description' => $this->t("By default, an additional access check will be executed for each item returned by the search query. However, since removing results this way will break paging and result counts, it is preferable to configure the view in a way that it will only return accessible results. If you are sure that only accessible results will be returned in the search, or if you want to show results to which the user normally wouldn't have access, you can enable this option to skip those additional access checks. This should be used with care."),
+ '#default_value' => $this->options['skip_access'],
+ '#weight' => -1,
+ ];
+
+ $form['bypass_access'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Bypass access checks'),
+ '#description' => $this->t('If the underlying search index has access checks enabled (for example, through the "Content access" processor), this option allows you to disable them for this view. This will never disable any filters placed on this view.'),
+ '#default_value' => $this->options['bypass_access'],
+ ];
+ $form['bypass_access']['#states']['visible'][':input[name="query[options][skip_access]"]']['checked'] = TRUE;
+ }
+
+ /**
+ * Checks for entity types contained in the current view's index.
+ *
+ * @param bool $return_bool
+ * (optional) If TRUE, returns a boolean instead of a list of datasources.
+ *
+ * @return string[]|bool
+ * If $return_bool is FALSE, an associative array mapping all datasources
+ * containing entities to their entity types. Otherwise, TRUE if there is at
+ * least one such datasource.
+ *
+ * @deprecated Will be removed in a future version of the module. Use
+ * \Drupal\search_api\IndexInterface::getEntityTypes() instead.
+ */
+ public function getEntityTypes($return_bool = FALSE) {
+ $types = $this->index->getEntityTypes();
+ return $return_bool ? (bool) $types : $types;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function build(ViewExecutable $view) {
+ $this->view = $view;
+
+ // Initialize the pager and let it modify the query to add limits. This has
+ // to be done even for aborted queries since it might otherwise lead to a
+ // fatal error when Views tries to access $view->pager.
+ $view->initPager();
+ $view->pager->query();
+
+ // If the query was aborted by some plugin (or, possibly, hook), we don't
+ // need to do anything else here. Adding conditions or other options to an
+ // aborted query doesn't make sense.
+ if ($this->shouldAbort()) {
+ return;
+ }
+
+ // Setup the nested filter structure for this query.
+ if (!empty($this->where)) {
+ // If the different groups are combined with the OR operator, we have to
+ // add a new OR filter to the query to which the filters for the groups
+ // will be added.
+ if ($this->groupOperator === 'OR') {
+ $base = $this->query->createConditionGroup('OR');
+ $this->query->addConditionGroup($base);
+ }
+ else {
+ $base = $this->query;
+ }
+ // Add a nested filter for each filter group, with its set conjunction.
+ foreach ($this->where as $group_id => $group) {
+ if (!empty($group['conditions']) || !empty($group['condition_groups'])) {
+ $group += ['type' => 'AND'];
+ // For filters without a group, we want to always add them directly to
+ // the query.
+ $conditions = ($group_id === '') ? $this->query : $this->query->createConditionGroup($group['type']);
+ if (!empty($group['conditions'])) {
+ foreach ($group['conditions'] as $condition) {
+ list($field, $value, $operator) = $condition;
+ $conditions->addCondition($field, $value, $operator);
+ }
+ }
+ if (!empty($group['condition_groups'])) {
+ foreach ($group['condition_groups'] as $nested_conditions) {
+ $conditions->addConditionGroup($nested_conditions);
+ }
+ }
+ // If no group was given, the filters were already set on the query.
+ if ($group_id !== '') {
+ $base->addConditionGroup($conditions);
+ }
+ }
+ }
+ }
+
+ // Add the "search_api_bypass_access" option to the query, if desired.
+ if (!empty($this->options['bypass_access'])) {
+ $this->query->setOption('search_api_bypass_access', TRUE);
+ }
+
+ // If the View and the Panel conspire to provide an overridden path then
+ // pass that through as the base path.
+ if (($path = $this->view->getPath()) && strpos(Url::fromRoute('')->toString(), $this->view->override_path) !== 0) {
+ $this->query->setOption('search_api_base_path', $path);
+ }
+
+ // Save query information for Views UI.
+ $view->build_info['query'] = (string) $this->query;
+
+ // Add the properties to be retrieved to the query, as information for the
+ // backend.
+ $this->query->setOption('search_api_retrieved_properties', $this->retrievedProperties);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function alter(ViewExecutable $view) {
+ $this->getModuleHandler()->invokeAll('views_query_alter', [$view, $this]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function execute(ViewExecutable $view) {
+ if ($this->shouldAbort()) {
+ if (error_displayable()) {
+ foreach ($this->errors as $msg) {
+ drupal_set_message(Html::escape($msg), 'error');
+ }
+ }
+ $view->result = [];
+ $view->total_rows = 0;
+ $view->execute_time = 0;
+ return;
+ }
+
+ // Calculate the "skip result count" option, if it wasn't already set to
+ // FALSE.
+ $skip_result_count = $this->query->getOption('skip result count', TRUE);
+ if ($skip_result_count) {
+ $skip_result_count = !$view->pager->useCountQuery() && empty($view->get_total_rows);
+ $this->query->setOption('skip result count', $skip_result_count);
+ }
+
+ try {
+ // Trigger pager preExecute().
+ $view->pager->preExecute($this->query);
+
+ // Views passes sometimes NULL and sometimes the integer 0 for "All" in a
+ // pager. If set to 0 items, a string "0" is passed. Therefore, we unset
+ // the limit if an empty value OTHER than a string "0" was passed.
+ if (!$this->limit && $this->limit !== '0') {
+ $this->limit = NULL;
+ }
+ // Set the range. We always set this, as there might be an offset even if
+ // all items are shown.
+ $this->query->range($this->offset, $this->limit);
+
+ $start = microtime(TRUE);
+
+ // Execute the search.
+ $results = $this->query->execute();
+
+ // Store the results.
+ if (!$skip_result_count) {
+ $view->pager->total_items = $results->getResultCount();
+ if (!empty($view->pager->options['offset'])) {
+ $view->pager->total_items -= $view->pager->options['offset'];
+ }
+ $view->total_rows = $view->pager->total_items;
+ }
+ $view->result = [];
+ if ($results->getResultItems()) {
+ $this->addResults($results->getResultItems(), $view);
+ }
+ $view->execute_time = microtime(TRUE) - $start;
+
+ // Trigger pager postExecute().
+ $view->pager->postExecute($view->result);
+ $view->pager->updatePageInfo();
+ }
+ catch (\Exception $e) {
+ $this->abort($e->getMessage());
+ // Recursion to get the same error behaviour as above.
+ $this->execute($view);
+ }
+ }
+
+ /**
+ * Aborts this search query.
+ *
+ * Used by handlers to flag a fatal error which shouldn't be displayed but
+ * still lead to the view returning empty and the search not being executed.
+ *
+ * @param \Drupal\Component\Render\MarkupInterface|string|null $msg
+ * Optionally, a translated, unescaped error message to display.
+ */
+ public function abort($msg = NULL) {
+ if ($msg) {
+ $this->errors[] = $msg;
+ }
+ $this->abort = TRUE;
+ if (isset($this->query)) {
+ $this->query->abort($msg);
+ }
+ }
+
+ /**
+ * Checks whether this query should be aborted.
+ *
+ * @return bool
+ * TRUE if the query should/will be aborted, FALSE otherwise.
+ *
+ * @see SearchApiQuery::abort()
+ */
+ public function shouldAbort() {
+ return $this->abort || !$this->query || $this->query->wasAborted();
+ }
+
+ /**
+ * Adds Search API result items to a view's result set.
+ *
+ * @param \Drupal\search_api\Item\ItemInterface[] $results
+ * The search results.
+ * @param \Drupal\views\ViewExecutable $view
+ * The executed view.
+ */
+ protected function addResults(array $results, ViewExecutable $view) {
+ // Views \Drupal\views\Plugin\views\style\StylePluginBase::renderFields()
+ // uses a numeric results index to key the rendered results.
+ // The ResultRow::index property is the key then used to retrieve these.
+ $count = 0;
+
+ // First, unless disabled, check access for all entities in the results.
+ if (!$this->options['skip_access']) {
+ $account = $this->getAccessAccount();
+ foreach ($results as $item_id => $result) {
+ if (!$result->checkAccess($account)) {
+ unset($results[$item_id]);
+ }
+ }
+ }
+
+ foreach ($results as $item_id => $result) {
+ $values = [];
+ $values['_item'] = $result;
+ $object = $result->getOriginalObject(FALSE);
+ if ($object) {
+ $values['_object'] = $object;
+ $values['_relationship_objects'][NULL] = [$object];
+ }
+ $values['search_api_id'] = $item_id;
+ $values['search_api_datasource'] = $result->getDatasourceId();
+ $values['search_api_language'] = $result->getLanguage();
+ $values['search_api_relevance'] = $result->getScore();
+ $values['search_api_excerpt'] = $result->getExcerpt() ?: '';
+
+ // Gather any properties from the search results.
+ foreach ($result->getFields(FALSE) as $field_id => $field) {
+ if ($field->getValues()) {
+ $path = $field->getCombinedPropertyPath();
+ try {
+ $property = $field->getDataDefinition();
+ // For configurable processor-defined properties, our Views field
+ // handlers use a special property path to distinguish multiple
+ // fields with the same property path. Therefore, we here also set
+ // the values using that special property path so this will work
+ // correctly.
+ if ($property instanceof ConfigurablePropertyInterface) {
+ $path .= '|' . $field_id;
+ }
+ }
+ catch (SearchApiException $e) {
+ // If we're not able to retrieve the data definition at this point,
+ // it doesn't really matter.
+ }
+ $values[$path] = $field->getValues();
+ }
+ }
+
+ $values['index'] = $count++;
+
+ $view->result[] = new ResultRow($values);
+ }
+ }
+
+ /**
+ * Retrieves the conditions placed on this query.
+ *
+ * @return array
+ * The conditions placed on this query, separated by groups, as an
+ * associative array with a structure like this:
+ * - GROUP_ID:
+ * - type: "AND"/"OR"
+ * - conditions:
+ * - [FILTER, VALUE, OPERATOR]
+ * - [FILTER, VALUE, OPERATOR]
+ * …
+ * - condition_groups:
+ * - ConditionGroupInterface object
+ * - ConditionGroupInterface object
+ * …
+ * - GROUP_ID:
+ * …
+ * Returned by reference.
+ */
+ public function &getWhere() {
+ return $this->where;
+ }
+
+ /**
+ * Retrieves the account object to use for access checks for this query.
+ *
+ * @return \Drupal\Core\Session\AccountInterface|null
+ * The account for which to check access to returned or displayed entities.
+ * Or NULL to use the currently logged-in user.
+ */
+ public function getAccessAccount() {
+ $account = $this->getOption('search_api_access_account');
+ if ($account && is_scalar($account)) {
+ $account = User::load($account);
+ }
+ return $account;
+ }
+
+ /**
+ * Returns the Search API query object used by this Views query.
+ *
+ * @return \Drupal\search_api\Query\QueryInterface|null
+ * The search query object used internally by this plugin, if any has been
+ * successfully created. NULL otherwise.
+ */
+ public function getSearchApiQuery() {
+ return $this->query;
+ }
+
+ /**
+ * Sets the Search API query object.
+ *
+ * Usually this is done by the query plugin class itself, but in rare cases
+ * (such as for caching purposes) it might be necessary to set it from
+ * outside.
+ *
+ * @param \Drupal\search_api\Query\QueryInterface $query
+ * The new query.
+ *
+ * @return $this
+ */
+ public function setSearchApiQuery(QueryInterface $query) {
+ $this->query = $query;
+ return $this;
+ }
+
+ /**
+ * Retrieves the Search API result set returned for this query.
+ *
+ * @return \Drupal\search_api\Query\ResultSetInterface
+ * The result set of this query. Might not contain the actual results yet if
+ * the query hasn't been executed yet.
+ */
+ public function getSearchApiResults() {
+ return $this->query->getResults();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function calculateDependencies() {
+ $dependencies = parent::calculateDependencies();
+ $dependencies[$this->index->getConfigDependencyKey()][] = $this->index->getConfigDependencyName();
+ return $dependencies;
+ }
+
+ //
+ // Query interface methods (proxy to $this->query)
+ //
+
+ /**
+ * Retrieves the parse mode.
+ *
+ * @return \Drupal\search_api\ParseMode\ParseModeInterface|null
+ * The parse mode, or NULL if the query was aborted.
+ *
+ * @see \Drupal\search_api\Query\QueryInterface::getParseMode()
+ */
+ public function getParseMode() {
+ if (!$this->shouldAbort()) {
+ return $this->query->getParseMode();
+ }
+ return NULL;
+ }
+
+ /**
+ * Sets the parse mode.
+ *
+ * @param \Drupal\search_api\ParseMode\ParseModeInterface $parse_mode
+ * The parse mode.
+ *
+ * @return $this
+ *
+ * @see \Drupal\search_api\Query\QueryInterface::setParseMode()
+ */
+ public function setParseMode(ParseModeInterface $parse_mode) {
+ if (!$this->shouldAbort()) {
+ $this->query->setParseMode($parse_mode);
+ }
+ return $this;
+ }
+
+ /**
+ * Retrieves the languages that will be searched by this query.
+ *
+ * @return string[]|null
+ * The language codes of languages that will be searched by this query, or
+ * NULL if there shouldn't be any restriction on the language.
+ *
+ * @see \Drupal\search_api\Query\QueryInterface::getLanguages()
+ */
+ public function getLanguages() {
+ if (!$this->shouldAbort()) {
+ return $this->query->getLanguages();
+ }
+ return NULL;
+ }
+
+ /**
+ * Sets the languages that should be searched by this query.
+ *
+ * @param string[]|null $languages
+ * The language codes to search for, or NULL to not restrict the query to
+ * specific languages.
+ *
+ * @return $this
+ *
+ * @see \Drupal\search_api\Query\QueryInterface::setLanguages()
+ */
+ public function setLanguages(array $languages = NULL) {
+ if (!$this->shouldAbort()) {
+ $this->query->setLanguages($languages);
+ }
+ return $this;
+ }
+
+ /**
+ * Creates a new condition group to use with this query object.
+ *
+ * @param string $conjunction
+ * The conjunction to use for the condition group – either 'AND' or 'OR'.
+ * @param string[] $tags
+ * (optional) Tags to set on the condition group.
+ *
+ * @return \Drupal\search_api\Query\ConditionGroupInterface
+ * A condition group object that is set to use the specified conjunction.
+ *
+ * @see \Drupal\search_api\Query\QueryInterface::createConditionGroup()
+ */
+ public function createConditionGroup($conjunction = 'AND', array $tags = []) {
+ if (!$this->shouldAbort()) {
+ return $this->query->createConditionGroup($conjunction, $tags);
+ }
+ return NULL;
+ }
+
+ /**
+ * Sets the keys to search for.
+ *
+ * If this method is not called on the query before execution, this will be a
+ * filter-only query.
+ *
+ * @param string|array|null $keys
+ * A string with the search keys, in one of the formats specified by
+ * getKeys(). A passed string will be parsed according to the set parse
+ * mode. Use NULL to not use any search keys.
+ *
+ * @return $this
+ *
+ * @see \Drupal\search_api\Query\QueryInterface::keys()
+ */
+ public function keys($keys = NULL) {
+ if (!$this->shouldAbort()) {
+ $this->query->keys($keys);
+ }
+ return $this;
+ }
+
+ /**
+ * Sets the fields that will be searched for the search keys.
+ *
+ * If this is not called, all fulltext fields will be searched.
+ *
+ * @param array $fields
+ * An array containing fulltext fields that should be searched.
+ *
+ * @return $this
+ *
+ * @throws \Drupal\search_api\SearchApiException
+ * Thrown if one of the fields isn't of type "text".
+ *
+ * @see \Drupal\search_api\Query\QueryInterface::setFulltextFields()
+ */
+ public function setFulltextFields(array $fields = NULL) {
+ if (!$this->shouldAbort()) {
+ $this->query->setFulltextFields($fields);
+ }
+ return $this;
+ }
+
+ /**
+ * Adds a nested condition group.
+ *
+ * If $group is given, the filter is added to the relevant filter group
+ * instead.
+ *
+ * @param \Drupal\search_api\Query\ConditionGroupInterface $condition_group
+ * A condition group that should be added.
+ * @param string|null $group
+ * (optional) The Views query filter group to add this filter to.
+ *
+ * @return $this
+ *
+ * @see \Drupal\search_api\Query\QueryInterface::addConditionGroup()
+ */
+ public function addConditionGroup(ConditionGroupInterface $condition_group, $group = NULL) {
+ if (!$this->shouldAbort()) {
+ // Ensure all variants of 0 are actually 0. Thus '', 0 and NULL are all
+ // the default group.
+ if (empty($group)) {
+ $group = 0;
+ }
+ $this->where[$group]['condition_groups'][] = $condition_group;
+ }
+ return $this;
+ }
+
+ /**
+ * Adds a new ($field $operator $value) condition filter.
+ *
+ * @param string $field
+ * The ID of the field to filter on, for example "status". The special
+ * fields "search_api_datasource" (filter on datasource ID),
+ * "search_api_language" (filter on language code) and "search_api_id"
+ * (filter on item ID) can be used in addition to all indexed fields on the
+ * index.
+ * However, for filtering on language code, using
+ * \Drupal\search_api\Plugin\views\query\SearchApiQuery::setLanguages is the
+ * preferred method, unless a complex condition containing the language code
+ * is required.
+ * @param mixed $value
+ * The value the field should have (or be related to by the operator). If
+ * $operator is "IN" or "NOT IN", $value has to be an array of values. If
+ * $operator is "BETWEEN" or "NOT BETWEEN", it has to be an array with
+ * exactly two values: the lower bound in key 0 and the upper bound in key 1
+ * (both inclusive). Otherwise, $value must be a scalar.
+ * @param string $operator
+ * The operator to use for checking the constraint. The following operators
+ * are always supported for primitive types: "=", "<>", "<", "<=", ">=",
+ * ">", "IN", "NOT IN", "BETWEEN", "NOT BETWEEN". They have the same
+ * semantics as the corresponding SQL operators. Other operators might be
+ * added by backend features.
+ * If $field is a fulltext field, $operator can only be "=" or "<>", which
+ * are in this case interpreted as "contains" or "doesn't contain",
+ * respectively.
+ * If $value is NULL, $operator also can only be "=" or "<>", meaning the
+ * field must have no or some value, respectively.
+ * @param string|null $group
+ * (optional) The Views query filter group to add this filter to.
+ *
+ * @return $this
+ *
+ * @see \Drupal\search_api\Query\QueryInterface::addCondition()
+ */
+ public function addCondition($field, $value, $operator = '=', $group = NULL) {
+ if (!$this->shouldAbort()) {
+ // Ensure all variants of 0 are actually 0. Thus '', 0 and NULL are all
+ // the default group.
+ if (empty($group)) {
+ $group = 0;
+ }
+ $condition = [$field, $value, $operator];
+ $this->where[$group]['conditions'][] = $condition;
+ }
+ return $this;
+ }
+
+ /**
+ * Adds a simple condition to the query.
+ *
+ * This replicates the interface of Views' default SQL backend to simplify
+ * the Views integration of the Search API. If you are writing Search
+ * API-specific Views code, you should better use the addConditionGroup() or
+ * addCondition() methods.
+ *
+ * @param int $group
+ * The condition group to add these to; groups are used to create AND/OR
+ * sections. Groups cannot be nested. Use 0 as the default group.
+ * If the group does not yet exist it will be created as an AND group.
+ * @param string|\Drupal\Core\Database\Query\ConditionInterface|\Drupal\search_api\Query\ConditionGroupInterface $field
+ * The ID of the field to check; or a filter object to add to the query; or,
+ * for compatibility purposes, a database condition object to transform into
+ * a search filter object and add to the query. If a field ID is passed and
+ * starts with a period (.), it will be stripped.
+ * @param mixed $value
+ * (optional) The value the field should have (or be related to by the
+ * operator). Or NULL if an object is passed as $field.
+ * @param string|null $operator
+ * (optional) The operator to use for checking the constraint. The following
+ * operators are supported for primitive types: "=", "<>", "<", "<=", ">=",
+ * ">". They have the same semantics as the corresponding SQL operators.
+ * If $field is a fulltext field, $operator can only be "=" or "<>", which
+ * are in this case interpreted as "contains" or "doesn't contain",
+ * respectively.
+ * If $value is NULL, $operator also can only be "=" or "<>", meaning the
+ * field must have no or some value, respectively.
+ * To stay compatible with Views, "!=" is supported as an alias for "<>".
+ * If an object is passed as $field, $operator should be NULL.
+ *
+ * @return $this
+ *
+ * @see \Drupal\views\Plugin\views\query\Sql::addWhere()
+ * @see \Drupal\search_api\Plugin\views\query\SearchApiQuery::filter()
+ * @see \Drupal\search_api\Plugin\views\query\SearchApiQuery::condition()
+ */
+ public function addWhere($group, $field, $value = NULL, $operator = NULL) {
+ if ($this->shouldAbort()) {
+ return $this;
+ }
+
+ // Ensure all variants of 0 are actually 0. Thus '', 0 and NULL are all the
+ // default group.
+ if (empty($group)) {
+ $group = 0;
+ }
+
+ if (is_object($field)) {
+ if ($field instanceof ConditionInterface) {
+ $field = $this->transformDbCondition($field);
+ }
+ if ($field instanceof ConditionGroupInterface) {
+ $this->where[$group]['condition_groups'][] = $field;
+ }
+ elseif (!$this->shouldAbort()) {
+ // We only need to abort if that wasn't done by transformDbCondition()
+ // already.
+ $this->abort('Unexpected condition passed to addWhere().');
+ }
+ }
+ else {
+ $condition = [
+ $this->sanitizeFieldId($field),
+ $value,
+ $this->sanitizeOperator($operator),
+ ];
+ $this->where[$group]['conditions'][] = $condition;
+ }
+
+ return $this;
+ }
+
+ /**
+ * Retrieves the conjunction with which multiple filter groups are combined.
+ *
+ * @return string
+ * Either "AND" or "OR".
+ */
+ public function getGroupOperator() {
+ return $this->groupOperator;
+ }
+
+ /**
+ * Returns the group type of the given group.
+ *
+ * @param int $group
+ * The group whose type should be retrieved.
+ *
+ * @return string
+ * The group type – "AND" or "OR".
+ */
+ public function getGroupType($group) {
+ return !empty($this->where[$group]) ? $this->where[$group] : 'AND';
+ }
+
+ /**
+ * Transforms a database condition to an equivalent search filter.
+ *
+ * @param \Drupal\Core\Database\Query\ConditionInterface $db_condition
+ * The condition to transform.
+ *
+ * @return \Drupal\search_api\Query\ConditionGroupInterface|null
+ * A search filter equivalent to $condition, or NULL if the transformation
+ * failed.
+ */
+ protected function transformDbCondition(ConditionInterface $db_condition) {
+ $conditions = $db_condition->conditions();
+ $filter = $this->query->createConditionGroup($conditions['#conjunction']);
+ unset($conditions['#conjunction']);
+ foreach ($conditions as $condition) {
+ if ($condition['operator'] === NULL) {
+ $this->abort('Trying to include a raw SQL condition in a Search API query.');
+ return NULL;
+ }
+ if ($condition['field'] instanceof ConditionInterface) {
+ $nested_filter = $this->transformDbCondition($condition['field']);
+ if ($nested_filter) {
+ $filter->addConditionGroup($nested_filter);
+ }
+ else {
+ return NULL;
+ }
+ }
+ else {
+ $filter->addCondition($this->sanitizeFieldId($condition['field']), $condition['value'], $this->sanitizeOperator($condition['operator']));
+ }
+ }
+ return $filter;
+ }
+
+ /**
+ * Adapts a field ID for use in a Search API query.
+ *
+ * This method will remove a leading period (.), if present. This is done
+ * because in the SQL Views query plugin field IDs are always prefixed with a
+ * table alias (in our case always empty) followed by a period.
+ *
+ * @param string $field_id
+ * The field ID to adapt for use in the Search API.
+ *
+ * @return string
+ * The sanitized field ID.
+ */
+ protected function sanitizeFieldId($field_id) {
+ if ($field_id && $field_id[0] === '.') {
+ $field_id = substr($field_id, 1);
+ }
+ return $field_id;
+ }
+
+ /**
+ * Adapts an operator for use in a Search API query.
+ *
+ * This method maps Views' "!=" to the "<>" Search API uses.
+ *
+ * @param string $operator
+ * The operator to adapt for use in the Search API.
+ *
+ * @return string
+ * The sanitized operator.
+ */
+ protected function sanitizeOperator($operator) {
+ if ($operator === '!=') {
+ $operator = '<>';
+ }
+ elseif (in_array($operator, ['in', 'not in', 'between', 'not between'])) {
+ $operator = strtoupper($operator);
+ }
+ elseif (in_array($operator, ['IS NULL', 'IS NOT NULL'])) {
+ $operator = ($operator == 'IS NULL') ? '=' : '<>';
+ }
+ return $operator;
+ }
+
+ /**
+ * Adds a sort directive to this search query.
+ *
+ * If no sort is manually set, the results will be sorted descending by
+ * relevance.
+ *
+ * @param string $field
+ * The field to sort by. The special fields 'search_api_relevance' (sort by
+ * relevance) and 'search_api_id' (sort by item id) may be used.
+ * @param string $order
+ * The order to sort items in - either 'ASC' or 'DESC'.
+ *
+ * @return $this
+ *
+ * @throws \Drupal\search_api\SearchApiException
+ * Thrown if the field is multi-valued or of a fulltext type.
+ *
+ * @see \Drupal\search_api\Query\QueryInterface::sort()
+ */
+ public function sort($field, $order = 'ASC') {
+ if (!$this->shouldAbort()) {
+ $this->query->sort($field, $order);
+ }
+ return $this;
+ }
+
+ /**
+ * Adds an ORDER BY clause to the query.
+ *
+ * This replicates the interface of Views' default SQL backend to simplify
+ * the Views integration of the Search API. If you are writing Search
+ * API-specific Views code, you should better use the sort() method directly.
+ *
+ * Currently, only random sorting (by passing "rand" as the table) is
+ * supported (for backends that support it), all other calls are silently
+ * ignored.
+ *
+ * @param string|null $table
+ * The table this field is part of. If a formula, enter NULL. If you want to
+ * order the results randomly, use "rand" as table and nothing else.
+ * @param string|null $field
+ * (optional) The field or formula to sort on. If already a field, enter
+ * NULL and put in the alias.
+ * @param string $order
+ * (optional) Either ASC or DESC.
+ * @param string $alias
+ * (optional) The alias to add the field as. In SQL, all fields in the order
+ * by must also be in the SELECT portion. If an $alias isn't specified one
+ * will be generated for from the $field; however, if the $field is a
+ * formula, this alias will likely fail.
+ * @param array $params
+ * (optional) Any parameters that should be passed through to the addField()
+ * call.
+ *
+ * @see \Drupal\views\Plugin\views\query\Sql::addOrderBy()
+ */
+ public function addOrderBy($table, $field = NULL, $order = 'ASC', $alias = '', array $params = []) {
+ $server = $this->getIndex()->getServerInstance();
+ if ($table == 'rand') {
+ if ($server->supportsFeature('search_api_random_sort')) {
+ $this->sort('search_api_random', $order);
+ if ($params) {
+ $this->setOption('search_api_random_sort', $params);
+ }
+ }
+ else {
+ $variables['%server'] = $server->label();
+ $this->getLogger()->warning('Tried to sort results randomly on server %server which does not support random sorting.', $variables);
+ }
+ }
+ }
+
+ /**
+ * Adds a range of results to return.
+ *
+ * This will be saved in the query's options. If called without parameters,
+ * this will remove all range restrictions previously set.
+ *
+ * @param int|null $offset
+ * The zero-based offset of the first result returned.
+ * @param int|null $limit
+ * The number of results to return.
+ *
+ * @return $this
+ *
+ * @see \Drupal\search_api\Query\QueryInterface::range()
+ */
+ public function range($offset = NULL, $limit = NULL) {
+ if (!$this->shouldAbort()) {
+ $this->query->range($offset, $limit);
+ }
+ return $this;
+ }
+
+ /**
+ * Retrieves the index associated with this search.
+ *
+ * @return \Drupal\search_api\IndexInterface
+ * The search index this query should be executed on.
+ *
+ * @see \Drupal\search_api\Query\QueryInterface::getIndex()
+ */
+ public function getIndex() {
+ return $this->index;
+ }
+
+ /**
+ * Retrieves the search keys for this query.
+ *
+ * @return array|string|null
+ * This object's search keys, in the format described by
+ * \Drupal\search_api\ParseMode\ParseModeInterface::parseInput(). Or NULL if
+ * the query doesn't have any search keys set.
+ *
+ * @see \Drupal\search_api\Plugin\views\query\SearchApiQuery::keys()
+ *
+ * @see \Drupal\search_api\Query\QueryInterface::getKeys()
+ */
+ public function &getKeys() {
+ if (!$this->shouldAbort()) {
+ return $this->query->getKeys();
+ }
+ $ret = NULL;
+ return $ret;
+ }
+
+ /**
+ * Retrieves the unparsed search keys for this query as originally entered.
+ *
+ * @return array|string|null
+ * The unprocessed search keys, exactly as passed to this object. Has the
+ * same format as the return value of getKeys().
+ *
+ * @see keys()
+ *
+ * @see \Drupal\search_api\Query\QueryInterface::getOriginalKeys()
+ */
+ public function getOriginalKeys() {
+ if (!$this->shouldAbort()) {
+ return $this->query->getOriginalKeys();
+ }
+ return NULL;
+ }
+
+ /**
+ * Retrieves the fulltext fields that will be searched for the search keys.
+ *
+ * @return string[]|null
+ * An array containing the fields that should be searched for the search
+ * keys.
+ *
+ * @see setFulltextFields()
+ * @see \Drupal\search_api\Query\QueryInterface::getFulltextFields()
+ */
+ public function &getFulltextFields() {
+ if (!$this->shouldAbort()) {
+ return $this->query->getFulltextFields();
+ }
+ $ret = NULL;
+ return $ret;
+ }
+
+ /**
+ * Retrieves the filter object associated with this search query.
+ *
+ * @return \Drupal\search_api\Query\ConditionGroupInterface
+ * This object's associated filter object.
+ *
+ * @see \Drupal\search_api\Query\QueryInterface::getConditionGroup()
+ */
+ public function getFilter() {
+ if (!$this->shouldAbort()) {
+ return $this->query->getConditionGroup();
+ }
+ return NULL;
+ }
+
+ /**
+ * Retrieves the sorts set for this query.
+ *
+ * @return array
+ * An array specifying the sort order for this query. Array keys are the
+ * field names in order of importance, the values are the respective order
+ * in which to sort the results according to the field.
+ *
+ * @see sort()
+ *
+ * @see \Drupal\search_api\Query\QueryInterface::getSorts()
+ */
+ public function &getSort() {
+ if (!$this->shouldAbort()) {
+ return $this->query->getSorts();
+ }
+ $ret = NULL;
+ return $ret;
+ }
+
+ /**
+ * Retrieves an option set on this search query.
+ *
+ * @param string $name
+ * The name of an option.
+ * @param mixed $default
+ * The value to return if the specified option is not set.
+ *
+ * @return mixed
+ * The value of the option with the specified name, if set. NULL otherwise.
+ *
+ * @see \Drupal\search_api\Query\QueryInterface::getOption()
+ */
+ public function getOption($name, $default = NULL) {
+ if (!$this->shouldAbort()) {
+ return $this->query->getOption($name, $default);
+ }
+ return $default;
+ }
+
+ /**
+ * Sets an option for this search query.
+ *
+ * @param string $name
+ * The name of an option. The following options are recognized by default:
+ * - offset: The position of the first returned search results relative to
+ * the whole result in the index.
+ * - limit: The maximum number of search results to return. -1 means no
+ * limit.
+ * - 'search id': A string that will be used as the identifier when storing
+ * this search in the Search API's static cache.
+ * - 'skip result count': If present and set to TRUE, the search's result
+ * count will not be needed. Service classes can check for this option to
+ * possibly avoid executing expensive operations to compute the result
+ * count in cases where it is not needed.
+ * - search_api_access_account: The account which will be used for entity
+ * access checks, if available and enabled for the index.
+ * - search_api_bypass_access: If set to TRUE, entity access checks will be
+ * skipped, even if enabled for the index.
+ * However, contrib modules might introduce arbitrary other keys with their
+ * own, special meaning. (Usually they should be prefixed with the module
+ * name, though, to avoid conflicts.)
+ * @param mixed $value
+ * The new value of the option.
+ *
+ * @return mixed
+ * The option's previous value, or NULL if none was set.
+ *
+ * @see \Drupal\search_api\Query\QueryInterface::setOption()
+ */
+ public function setOption($name, $value) {
+ if (!$this->shouldAbort()) {
+ return $this->query->setOption($name, $value);
+ }
+ return NULL;
+ }
+
+ /**
+ * Retrieves all options set for this search query.
+ *
+ * The return value is a reference to the options so they can also be altered
+ * this way.
+ *
+ * @return array
+ * An associative array of query options.
+ *
+ * @see \Drupal\search_api\Query\QueryInterface::getOptions()
+ */
+ public function &getOptions() {
+ if (!$this->shouldAbort()) {
+ return $this->query->getOptions();
+ }
+ $ret = NULL;
+ return $ret;
+ }
+
+ //
+ // Methods from Views' SQL query plugin, to simplify integration.
+ //
+
+ /**
+ * Ensures a table exists in the query.
+ *
+ * This replicates the interface of Views' default SQL backend to simplify
+ * the Views integration of the Search API. Since the Search API has no
+ * concept of "tables", this method implementation does nothing. If you are
+ * writing Search API-specific Views code, there is therefore no reason at all
+ * to call this method.
+ * See https://www.drupal.org/node/2484565 for more information.
+ *
+ * @return string
+ * An empty string.
+ */
+ public function ensureTable() {
+ return '';
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Plugin/views/relationship/SearchApiRelationship.php b/sites/all/modules/contrib/search/search_api/src/Plugin/views/relationship/SearchApiRelationship.php
new file mode 100644
index 000000000..0db9e44b6
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Plugin/views/relationship/SearchApiRelationship.php
@@ -0,0 +1,98 @@
+entityTypeManager ?: \Drupal::entityTypeManager();
+ }
+
+ /**
+ * Sets the entity type manager.
+ *
+ * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+ * The new entity type manager.
+ *
+ * @return $this
+ */
+ public function setEntityTypeManager(EntityTypeManagerInterface $entity_type_manager) {
+ $this->entityTypeManager = $entity_type_manager;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function defineOptions() {
+ $options = parent::defineOptions();
+ $options['skip_access']['default'] = FALSE;
+ return $options;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildOptionsForm(&$form, FormStateInterface $form_state) {
+ parent::buildOptionsForm($form, $form_state);
+
+ $form['required']['#access'] = FALSE;
+
+ $form['skip_access'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Skip access checks'),
+ '#description' => $this->t('Do not verify that the user has access to the entities referenced through this relationship. This will allow you to display data to the user to which they normally would not have access. This should therefore be used with care.'),
+ '#default_value' => $this->options['skip_access'],
+ '#weight' => -1,
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function query() {
+ $this->alias = $this->field;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function calculateDependencies() {
+ $dependencies = [];
+
+ if (!empty($this->definition['entity type'])) {
+ $entity_type = $this->getEntityTypeManager()
+ ->getDefinition($this->definition['entity type']);
+ if ($entity_type) {
+ $dependencies['module'][] = $entity_type->getProvider();
+ }
+ }
+
+ return $dependencies;
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Plugin/views/row/SearchApiRow.php b/sites/all/modules/contrib/search/search_api/src/Plugin/views/row/SearchApiRow.php
new file mode 100644
index 000000000..67a50ece1
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Plugin/views/row/SearchApiRow.php
@@ -0,0 +1,214 @@
+setEntityTypeManager($container->get('entity_type.manager'));
+ $row->setLogger($container->get('logger.channel.search_api'));
+
+ return $row;
+ }
+
+ /**
+ * Retrieves the entity type manager.
+ *
+ * @return \Drupal\Core\Entity\EntityTypeManagerInterface
+ * The entity type manager.
+ */
+ public function getEntityTypeManager() {
+ return $this->entityTypeManager ?: \Drupal::entityTypeManager();
+ }
+
+ /**
+ * Sets the entity type manager.
+ *
+ * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+ * The new entity type manager.
+ *
+ * @return $this
+ */
+ public function setEntityTypeManager(EntityTypeManagerInterface $entity_type_manager) {
+ $this->entityTypeManager = $entity_type_manager;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) {
+ parent::init($view, $display, $options);
+ $base_table = $view->storage->get('base_table');
+ $this->index = SearchApiQuery::getIndexFromTable($base_table, $this->getEntityTypeManager());
+ if (!$this->index) {
+ $view_label = $view->storage->label();
+ throw new \InvalidArgumentException("View '$view_label' is not based on Search API but tries to use its row plugin.");
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function defineOptions() {
+ $options = parent::defineOptions();
+
+ $options['view_modes'] = ['default' => []];
+
+ return $options;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildOptionsForm(&$form, FormStateInterface $form_state) {
+ parent::buildOptionsForm($form, $form_state);
+
+ /** @var \Drupal\search_api\Datasource\DatasourceInterface $datasource */
+ foreach ($this->index->getDatasources() as $datasource_id => $datasource) {
+ $datasource_label = $datasource->label();
+ $bundles = $datasource->getBundles();
+ if (!$datasource->getViewModes()) {
+ $form['view_modes'][$datasource_id] = [
+ '#type' => 'item',
+ '#title' => $this->t('Default View mode for datasource %name', ['%name' => $datasource_label]),
+ '#description' => $this->t("This datasource doesn't have any view modes available. It is therefore not possible to display results of this datasource using this row plugin."),
+ ];
+ continue;
+ }
+
+ foreach ($bundles as $bundle_id => $bundle_label) {
+ $title = $this->t('View mode for datasource %datasource, bundle %bundle', ['%datasource' => $datasource_label, '%bundle' => $bundle_label]);
+ $view_modes = $datasource->getViewModes($bundle_id);
+ if (!$view_modes) {
+ $form['view_modes'][$datasource_id][$bundle_id] = [
+ '#type' => 'item',
+ '#title' => $title,
+ '#description' => $this->t("This bundle doesn't have any view modes available. It is therefore not possible to display results of this bundle using this row plugin."),
+ ];
+ continue;
+ }
+ $form['view_modes'][$datasource_id][$bundle_id] = [
+ '#type' => 'select',
+ '#options' => $view_modes,
+ '#title' => $title,
+ '#default_value' => key($view_modes),
+ ];
+ if (isset($this->options['view_modes'][$datasource_id][$bundle_id])) {
+ $form['view_modes'][$datasource_id][$bundle_id]['#default_value'] = $this->options['view_modes'][$datasource_id][$bundle_id];
+ }
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function preRender($result) {
+ // Load all result objects at once, before rendering.
+ $items_to_load = [];
+ foreach ($result as $i => $row) {
+ if (empty($row->_object)) {
+ $items_to_load[$i] = $row->search_api_id;
+ }
+ }
+
+ $items = $this->index->loadItemsMultiple($items_to_load);
+ foreach ($items_to_load as $i => $item_id) {
+ if (isset($items[$item_id])) {
+ $result[$i]->_object = $items[$item_id];
+ $result[$i]->_item->setOriginalObject($items[$item_id]);
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function render($row) {
+ $datasource_id = $row->search_api_datasource;
+
+ if (!($row->_object instanceof ComplexDataInterface)) {
+ $context = [
+ '%item_id' => $row->search_api_id,
+ '%view' => $this->view->storage->label(),
+ ];
+ $this->getLogger()->warning('Failed to load item %item_id in view %view.', $context);
+ return '';
+ }
+
+ if (!$this->index->isValidDatasource($datasource_id)) {
+ $context = [
+ '%datasource' => $datasource_id,
+ '%view' => $this->view->storage->label(),
+ ];
+ $this->getLogger()->warning('Item of unknown datasource %datasource returned in view %view.', $context);
+ return '';
+ }
+ // Always use the default view mode if it was not set explicitly in the
+ // options.
+ $view_mode = 'default';
+ $bundle = $this->index->getDatasource($datasource_id)->getItemBundle($row->_object);
+ if (isset($this->options['view_modes'][$datasource_id][$bundle])) {
+ $view_mode = $this->options['view_modes'][$datasource_id][$bundle];
+ }
+
+ try {
+ return $this->index->getDatasource($datasource_id)->viewItem($row->_object, $view_mode);
+ }
+ catch (SearchApiException $e) {
+ $this->logException($e);
+ return '';
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function query() {}
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Plugin/views/sort/SearchApiSort.php b/sites/all/modules/contrib/search/search_api/src/Plugin/views/sort/SearchApiSort.php
new file mode 100644
index 000000000..bf27c08c2
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Plugin/views/sort/SearchApiSort.php
@@ -0,0 +1,38 @@
+orderby to an empty array. Therefore, if that property is set,
+ // we here remove all previous sorts.
+ // @todo Is this still true in D8?
+ // @todo Check whether #2145547 is still a problem here.
+ if (isset($this->query->orderby)) {
+ unset($this->query->orderby);
+ $sort = &$this->query->getSort();
+ $sort = [];
+ }
+ $this->query->sort($this->realField, $this->options['order']);
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Processor/ConfigurablePropertyBase.php b/sites/all/modules/contrib/search/search_api/src/Processor/ConfigurablePropertyBase.php
new file mode 100644
index 000000000..35fde737a
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Processor/ConfigurablePropertyBase.php
@@ -0,0 +1,40 @@
+getValues(), $this->defaultConfiguration());
+ $field->setConfiguration($values);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFieldDescription(FieldInterface $field) {
+ return $this->getDescription();
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Processor/ConfigurablePropertyInterface.php b/sites/all/modules/contrib/search/search_api/src/Processor/ConfigurablePropertyInterface.php
new file mode 100644
index 000000000..9938da959
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Processor/ConfigurablePropertyInterface.php
@@ -0,0 +1,74 @@
+definition['processor_id'];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isHidden() {
+ return !empty($this->definition['hidden']);
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Processor/FieldsProcessorPluginBase.php b/sites/all/modules/contrib/search/search_api/src/Processor/FieldsProcessorPluginBase.php
new file mode 100644
index 000000000..02039eb98
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Processor/FieldsProcessorPluginBase.php
@@ -0,0 +1,564 @@
+setDataTypeHelper($container->get('search_api.data_type_helper'));
+ $processor->setElementInfoManager($container->get('plugin.manager.element_info'));
+
+ return $processor;
+ }
+
+ /**
+ * Retrieves the data type helper.
+ *
+ * @return \Drupal\search_api\Utility\DataTypeHelperInterface
+ * 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\DataTypeHelperInterface $data_type_helper
+ * The new data type helper.
+ *
+ * @return $this
+ */
+ public function setDataTypeHelper(DataTypeHelperInterface $data_type_helper) {
+ $this->dataTypeHelper = $data_type_helper;
+ return $this;
+ }
+
+ /**
+ * Retrieves the element info manager.
+ *
+ * @return \Drupal\Core\Render\ElementInfoManagerInterface
+ * The element info manager.
+ */
+ public function getElementInfoManager() {
+ return $this->elementInfoManager ?: \Drupal::service('plugin.manager.element_info');
+ }
+
+ /**
+ * Sets the element info manager.
+ *
+ * @param \Drupal\Core\Render\ElementInfoManagerInterface $element_info_manager
+ * The new element info manager.
+ *
+ * @return $this
+ */
+ public function setElementInfoManager(ElementInfoManagerInterface $element_info_manager) {
+ $this->elementInfoManager = $element_info_manager;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function preIndexSave() {
+ parent::preIndexSave();
+
+ // If the "all supported fields" option is checked, we need to reset the
+ // fields array and fill it with all fields defined on the index.
+ if ($this->configuration['all_fields']) {
+ $this->configuration['fields'] = [];
+ foreach ($this->index->getFields() as $field_id => $field) {
+ if (!$field->isHidden() && $this->testType($field->getType())) {
+ $this->configuration['fields'][] = $field_id;
+ }
+ }
+ // No need to explicitly check for field renames.
+ return;
+ }
+
+ // Otherwise, if no fields were checked, we also have nothing to do here.
+ if (empty($this->configuration['fields'])) {
+ return;
+ }
+
+ // Apply field ID changes to the fields selected for this processor.
+ $selected_fields = array_flip($this->configuration['fields']);
+ $renames = $this->index->getFieldRenames();
+ $renames = array_intersect_key($renames, $selected_fields);
+ if ($renames) {
+ $new_fields = array_keys(array_diff_key($selected_fields, $renames));
+ $new_fields = array_merge($new_fields, array_values($renames));
+ $this->configuration['fields'] = $new_fields;
+ }
+
+ // Remove fields from the configuration that are no longer compatible with
+ // this processor (or no longer present on the index at all).
+ foreach ($this->configuration['fields'] as $i => $field_id) {
+ $field = $this->index->getField($field_id);
+ if ($field === NULL
+ || $field->isHidden()
+ || !$this->testType($field->getType())) {
+ unset($this->configuration['fields'][$i]);
+ }
+ }
+ // Serialization might be problematic if the array indices aren't completely
+ // sequential.
+ $this->configuration['fields'] = array_values($this->configuration['fields']);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defaultConfiguration() {
+ $configuration = parent::defaultConfiguration();
+
+ // @todo Add "fields" default here, too, and figure out how to replace
+ // current "magic" code dealing with unset option (or whether we even need
+ // to). See #2881665.
+ $configuration += [
+ 'all_fields' => FALSE,
+ ];
+
+ return $configuration;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+ $fields = $this->index->getFields();
+ $field_options = [];
+ $default_fields = [];
+ $all_fields = $this->configuration['all_fields'];
+ $fields_configured = isset($this->configuration['fields']);
+ if ($fields_configured && !$all_fields) {
+ $default_fields = $this->configuration['fields'];
+ }
+ foreach ($fields as $name => $field) {
+ if (!$field->isHidden() && $this->testType($field->getType())) {
+ $field_options[$name] = Html::escape($field->getPrefixedLabel());
+ if ($all_fields || (!$fields_configured && $this->testField($name, $field))) {
+ $default_fields[] = $name;
+ }
+ }
+ }
+
+ $form['all_fields'] = [
+ '#type' => 'checkbox',
+ '#title' => $this->t('Enable on all supported fields'),
+ '#description' => $this->t('Enable this processor for all supported fields. This will also automatically update the setting when new supported fields are added to the index.'),
+ '#default_value' => $all_fields,
+ ];
+
+ // Unfortunately, Form API doesn't seem to automatically add the default
+ // "#pre_render" callbacks to an element if we set some of our own. We
+ // therefore need to explicitly include those, too.
+ $pre_render = $this->getElementInfoManager()
+ ->getInfoProperty('checkboxes', '#pre_render', []);
+ $pre_render[] = [static::class, 'preRenderFieldsCheckboxes'];
+ $form['fields'] = [
+ '#type' => 'checkboxes',
+ '#title' => $this->t('Enable this processor on the following fields'),
+ '#description' => $this->t("Note: The Search API currently doesn't support per-field keywords processing, so this setting will be ignored when preprocessing search keywords. It is therefore usually recommended that you enable the processor for all fields that you intend to use as fulltext search fields, to avoid undesired consequences."),
+ '#options' => $field_options,
+ '#default_value' => $default_fields,
+ '#pre_render' => $pre_render,
+ ];
+
+ return $form;
+ }
+
+ /**
+ * Preprocesses the "fields" checkboxes before rendering.
+ *
+ * Adds "#states" settings to disable the checkboxes when "all_fields" is
+ * checked.
+ *
+ * @param array $element
+ * The form element to process.
+ *
+ * @return array
+ * The processed form element.
+ */
+ public static function preRenderFieldsCheckboxes(array $element) {
+ $parents = $element['#parents'];
+ array_pop($parents);
+ $parents[] = 'all_fields';
+ $name = array_shift($parents);
+ if ($parents) {
+ $name .= '[' . implode('][', $parents) . ']';
+ }
+ $selector = ":input[name=\"$name\"]";
+ $element['#states'] = [
+ 'invisible' => [
+ $selector => ['checked' => TRUE],
+ ],
+ ];
+
+ return $element;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
+ if (!$form_state->getValue('all_fields')) {
+ $fields = array_filter($form_state->getValue('fields', []));
+ if ($fields) {
+ $fields = array_keys($fields);
+ }
+ }
+ else {
+ $fields = array_keys($form['fields']['#options']);
+ }
+ $form_state->setValue('fields', $fields);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function preprocessIndexItems(array $items) {
+ // Annoyingly, this doc comment is needed for PHPStorm. See
+ // http://youtrack.jetbrains.com/issue/WI-23586
+ /** @var \Drupal\search_api\Item\ItemInterface $item */
+ foreach ($items as $item) {
+ foreach ($item->getFields() as $name => $field) {
+ if ($this->testField($name, $field)) {
+ $this->processField($field);
+ }
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function preprocessSearchQuery(QueryInterface $query) {
+ $keys = &$query->getKeys();
+ if (isset($keys)) {
+ $this->processKeys($keys);
+ }
+ $conditions = $query->getConditionGroup();
+ $this->processConditions($conditions->getConditions());
+ }
+
+ /**
+ * Processes a single field's value.
+ *
+ * Calls process() either for each value, or each token, depending on the
+ * type. Also takes care of extracting list values and of fusing returned
+ * tokens back into a one-dimensional array.
+ *
+ * @param \Drupal\search_api\Item\FieldInterface $field
+ * The field to process.
+ */
+ protected function processField(FieldInterface $field) {
+ $values = $field->getValues();
+ $type = $field->getType();
+
+ foreach ($values as $i => &$value) {
+ // We restore the field's type for each run of the loop since we need the
+ // unchanged one as long as the current field value hasn't been updated.
+ if ($value instanceof TextValueInterface) {
+ $tokens = $value->getTokens();
+ if ($tokens !== NULL) {
+ $new_tokens = [];
+ foreach ($tokens as $token) {
+ $token_text = $token->getText();
+ $this->processFieldValue($token_text, $type);
+ if (is_scalar($token_text)) {
+ if ($token_text !== '') {
+ $token->setText($token_text);
+ $new_tokens[] = $token;
+ }
+ }
+ else {
+ $base_boost = $token->getBoost();
+ /** @var \Drupal\search_api\Plugin\search_api\data_type\value\TextTokenInterface $new_token */
+ foreach ($token_text as $new_token) {
+ if ($new_token->getText() !== '') {
+ $new_token->setBoost($new_token->getBoost() * $base_boost);
+ $new_tokens[] = $new_token;
+ }
+ }
+ }
+ }
+ $value->setTokens($new_tokens);
+ }
+ else {
+ $text = $value->getText();
+ if ($text !== '') {
+ $this->processFieldValue($text, $type);
+ if ($text === '') {
+ unset($values[$i]);
+ }
+ elseif (is_scalar($text)) {
+ $value->setText($text);
+ }
+ else {
+ $value->setTokens($text);
+ }
+ }
+ }
+ }
+ elseif ($value !== '') {
+ $this->processFieldValue($value, $type);
+
+ if ($value === '') {
+ unset($values[$i]);
+ }
+ }
+ }
+
+ $field->setValues(array_values($values));
+ }
+
+ /**
+ * Preprocesses the search keywords.
+ *
+ * Calls processKey() for individual strings.
+ *
+ * @param array|string $keys
+ * Either a parsed keys array, or a single keywords string.
+ */
+ protected function processKeys(&$keys) {
+ if (is_array($keys)) {
+ foreach ($keys as $key => &$v) {
+ if (Element::child($key)) {
+ $this->processKeys($v);
+ if ($v === '') {
+ unset($keys[$key]);
+ }
+ }
+ }
+ }
+ else {
+ $this->processKey($keys);
+ }
+ }
+
+ /**
+ * Preprocesses the query conditions.
+ *
+ * @param \Drupal\search_api\Query\ConditionInterface[]|\Drupal\search_api\Query\ConditionGroupInterface[] $conditions
+ * An array of conditions, as returned by
+ * \Drupal\search_api\Query\ConditionGroupInterface::getConditions(),
+ * passed by reference.
+ */
+ protected function processConditions(array &$conditions) {
+ $fields = $this->index->getFields();
+ foreach ($conditions as $key => &$condition) {
+ if ($condition instanceof ConditionInterface) {
+ $field = $condition->getField();
+ if (isset($fields[$field]) && $this->testField($field, $fields[$field])) {
+ // We want to allow processors also to easily remove complete
+ // conditions. However, we can't use empty() or the like, as that
+ // would sort out filters for 0 or NULL. So we specifically check only
+ // for the empty string, and we also make sure the condition value was
+ // actually changed by storing whether it was empty before.
+ $value = $condition->getValue();
+ $empty_string = $value === '';
+ $this->processConditionValue($value);
+
+ // Conditions with (NOT) BETWEEN operator deserve special attention,
+ // as it seems unlikely that it makes sense to completely remove them.
+ // Processors that remove values are normally indicating that this
+ // value can't be in the index – but that's irrelevant for (NOT)
+ // BETWEEN conditions, as any value between the two bounds could still
+ // be included. We therefore never remove a (NOT) BETWEEN condition
+ // and also ignore it when one of the two values got removed. (Note
+ // that this check will also catch empty strings.) Processors who need
+ // different behavior have to override this method.
+ $between_operator = in_array($condition->getOperator(), ['BETWEEN', 'NOT BETWEEN']);
+ if ($between_operator && (!is_array($value) || count($value) < 2)) {
+ continue;
+ }
+
+ if ($value === '' && !$empty_string) {
+ unset($conditions[$key]);
+ }
+ else {
+ $condition->setValue($value);
+ }
+ }
+ }
+ elseif ($condition instanceof ConditionGroupInterface) {
+ $child_conditions = &$condition->getConditions();
+ $this->processConditions($child_conditions);
+ }
+ }
+ }
+
+ /**
+ * Tests whether a certain field should be processed.
+ *
+ * @param string $name
+ * The field's ID.
+ * @param \Drupal\search_api\Item\FieldInterface $field
+ * The field's information.
+ *
+ * @return bool
+ * TRUE if the field should be processed, FALSE otherwise.
+ */
+ protected function testField($name, FieldInterface $field) {
+ if (!isset($this->configuration['fields'])) {
+ return !$field->isHidden() && $this->testType($field->getType());
+ }
+ return in_array($name, $this->configuration['fields'], TRUE);
+ }
+
+ /**
+ * Determines whether a field of a certain type should be preprocessed.
+ *
+ * The default implementation returns TRUE for "text" and "string".
+ *
+ * @param string $type
+ * The type of the field (either when preprocessing the field at index time,
+ * or a condition on the field at query time).
+ *
+ * @return bool
+ * TRUE if fields of that type should be processed, FALSE otherwise.
+ */
+ protected function testType($type) {
+ return $this->getDataTypeHelper()
+ ->isTextType($type, ['text', 'string']);
+ }
+
+ /**
+ * Processes a single text element in a field.
+ *
+ * The default implementation just calls process().
+ *
+ * @param string $value
+ * The string value to preprocess, as a reference. Can be manipulated
+ * directly, nothing has to be returned. Can either be left a string, or
+ * changed into an array of
+ * \Drupal\search_api\Plugin\search_api\data_type\value\TextTokenInterface
+ * objects. Returning anything else will result in undefined behavior.
+ * @param string $type
+ * The field's data type.
+ */
+ protected function processFieldValue(&$value, $type) {
+ $this->process($value);
+ }
+
+ /**
+ * Processes a single search keyword.
+ *
+ * The default implementation just calls process().
+ *
+ * @param string $value
+ * The string value to preprocess, as a reference. Can be manipulated
+ * directly, nothing has to be returned. Can either be left a string, or be
+ * changed into a nested keys array, as defined by
+ * \Drupal\search_api\ParseMode\ParseModeInterface::parseInput().
+ */
+ protected function processKey(&$value) {
+ $this->process($value);
+ }
+
+ /**
+ * Processes a single condition value.
+ *
+ * Called for processing a single condition value. The default implementation
+ * just calls process().
+ *
+ * @param mixed $value
+ * The condition value to preprocess, as a reference. Can be manipulated
+ * directly, nothing has to be returned. Set to an empty string to remove
+ * the condition.
+ */
+ protected function processConditionValue(&$value) {
+ if (is_array($value)) {
+ if ($value) {
+ foreach ($value as $i => $part) {
+ $this->processConditionValue($value[$i]);
+ if ($value[$i] !== $part && $value[$i] === '') {
+ unset($value[$i]);
+ }
+ }
+ if (!$value) {
+ $value = '';
+ }
+ }
+ }
+ else {
+ $this->process($value);
+ }
+ }
+
+ /**
+ * Processes a single string value.
+ *
+ * This method is ultimately called for all text by the standard
+ * implementation, and does nothing by default.
+ *
+ * @param string $value
+ * The string value to preprocess, as a reference. Can be manipulated
+ * directly, nothing has to be returned. Since this can be called for all
+ * value types, $value has to remain a string.
+ */
+ protected function process(&$value) {}
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Processor/ProcessorInterface.php b/sites/all/modules/contrib/search/search_api/src/Processor/ProcessorInterface.php
new file mode 100644
index 000000000..07e433ee6
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Processor/ProcessorInterface.php
@@ -0,0 +1,239 @@
+setFieldsHelper($container->get('search_api.fields_helper'));
+
+ return $processor;
+ }
+
+ /**
+ * Retrieves the fields helper.
+ *
+ * @return \Drupal\search_api\Utility\FieldsHelperInterface
+ * The fields helper.
+ */
+ public function getFieldsHelper() {
+ return $this->fieldsHelper ?: \Drupal::service('search_api.fields_helper');
+ }
+
+ /**
+ * Sets the fields helper.
+ *
+ * @param \Drupal\search_api\Utility\FieldsHelperInterface $fields_helper
+ * The new fields helper.
+ *
+ * @return $this
+ */
+ public function setFieldsHelper(FieldsHelperInterface $fields_helper) {
+ $this->fieldsHelper = $fields_helper;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function supportsIndex(IndexInterface $index) {
+ return TRUE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function supportsStage($stage) {
+ $plugin_definition = $this->getPluginDefinition();
+ return isset($plugin_definition['stages'][$stage]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getWeight($stage) {
+ if (isset($this->configuration['weights'][$stage])) {
+ return $this->configuration['weights'][$stage];
+ }
+ $plugin_definition = $this->getPluginDefinition();
+ if (isset($plugin_definition['stages'][$stage])) {
+ return (int) $plugin_definition['stages'][$stage];
+ }
+ return 0;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setWeight($stage, $weight) {
+ $this->configuration['weights'][$stage] = $weight;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isLocked() {
+ return !empty($this->pluginDefinition['locked']);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isHidden() {
+ return !empty($this->pluginDefinition['hidden']);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getPropertyDefinitions(DatasourceInterface $datasource = NULL) {
+ return [];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addFieldValues(ItemInterface $item) {}
+
+ /**
+ * {@inheritdoc}
+ */
+ public function preIndexSave() {}
+
+ /**
+ * {@inheritdoc}
+ */
+ public function alterIndexedItems(array &$items) {}
+
+ /**
+ * {@inheritdoc}
+ */
+ public function preprocessIndexItems(array $items) {}
+
+ /**
+ * {@inheritdoc}
+ */
+ public function preprocessSearchQuery(QueryInterface $query) {}
+
+ /**
+ * {@inheritdoc}
+ */
+ public function postprocessSearchResults(ResultSetInterface $results) {}
+
+ /**
+ * {@inheritdoc}
+ */
+ public function requiresReindexing(array $old_settings = NULL, array $new_settings = NULL) {
+ // Only require re-indexing for processors that actually run during the
+ // indexing process.
+ return $this->supportsStage(ProcessorInterface::STAGE_PREPROCESS_INDEX);
+ }
+
+ /**
+ * Ensures that a field with certain properties is indexed on the index.
+ *
+ * Can be used as a helper method in preIndexSave().
+ *
+ * @param string|null $datasource_id
+ * The ID of the field's datasource, or NULL for a datasource-independent
+ * field.
+ * @param string $property_path
+ * The field's property path on the datasource.
+ * @param string|null $type
+ * (optional) If set, the field should have this type.
+ *
+ * @return \Drupal\search_api\Item\FieldInterface
+ * A field on the index, possibly newly added, with the specified
+ * properties.
+ *
+ * @throws \Drupal\search_api\SearchApiException
+ * Thrown if there is no property with the specified path, or no type is
+ * given and no default could be determined for the property.
+ */
+ protected function ensureField($datasource_id, $property_path, $type = NULL) {
+ $field = $this->findField($datasource_id, $property_path, $type);
+
+ if (!$field) {
+ $properties = $this->index->getPropertyDefinitions($datasource_id);
+ $property = $this->getFieldsHelper()
+ ->retrieveNestedProperty($properties, $property_path);
+ if (!$property) {
+ $property_id = Utility::createCombinedId($datasource_id, $property_path);
+ $processor_label = $this->label();
+ throw new SearchApiException("Could not find property '$property_id' which is required by the '$processor_label' processor.");
+ }
+ $field = $this->getFieldsHelper()
+ ->createFieldFromProperty($this->index, $property, $datasource_id, $property_path, NULL, $type);
+ $this->index->addField($field);
+ }
+
+ $field->setIndexedLocked();
+ if (isset($type)) {
+ $field->setTypeLocked();
+ }
+ return $field;
+ }
+
+ /**
+ * Finds a certain field in the index.
+ *
+ * @param string|null $datasource_id
+ * The ID of the field's datasource, or NULL for a datasource-independent
+ * field.
+ * @param string $property_path
+ * The field's property path on the datasource.
+ * @param string|null $type
+ * (optional) If set, only return a field if it has this type.
+ *
+ * @return \Drupal\search_api\Item\FieldInterface|null
+ * A field on the index with the desired properties, or NULL if none could
+ * be found.
+ */
+ protected function findField($datasource_id, $property_path, $type = NULL) {
+ foreach ($this->index->getFieldsByDatasource($datasource_id) as $field) {
+ if ($field->getPropertyPath() === $property_path) {
+ if (!isset($type) || $field->getType() === $type) {
+ return $field;
+ }
+ }
+ }
+ return NULL;
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Processor/ProcessorPluginManager.php b/sites/all/modules/contrib/search/search_api/src/Processor/ProcessorPluginManager.php
new file mode 100644
index 000000000..e665310ac
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Processor/ProcessorPluginManager.php
@@ -0,0 +1,69 @@
+setCacheBackend($cache_backend, 'search_api_processors');
+ $this->alterInfo('search_api_processor_info');
+ $this->setStringTranslation($translation);
+ }
+
+ /**
+ * Retrieves information about the available processing stages.
+ *
+ * These are then used by processors in their "stages" definition to specify
+ * in which stages they will run.
+ *
+ * @return array
+ * An associative array mapping stage identifiers to information about that
+ * stage. The information itself is an associative array with the following
+ * keys:
+ * - label: The translated label for this stage.
+ */
+ public function getProcessingStages() {
+ return [
+ ProcessorInterface::STAGE_PREPROCESS_INDEX => [
+ 'label' => $this->t('Preprocess index'),
+ ],
+ ProcessorInterface::STAGE_PREPROCESS_QUERY => [
+ 'label' => $this->t('Preprocess query'),
+ ],
+ ProcessorInterface::STAGE_POSTPROCESS_QUERY => [
+ 'label' => $this->t('Postprocess query'),
+ ],
+ ];
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Processor/ProcessorProperty.php b/sites/all/modules/contrib/search/search_api/src/Processor/ProcessorProperty.php
new file mode 100644
index 000000000..86247b77d
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Processor/ProcessorProperty.php
@@ -0,0 +1,33 @@
+definition['processor_id'];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isHidden() {
+ return !empty($this->definition['hidden']);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isList() {
+ return !empty($this->definition['is_list']) || parent::isList();
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Processor/ProcessorPropertyInterface.php b/sites/all/modules/contrib/search/search_api/src/Processor/ProcessorPropertyInterface.php
new file mode 100644
index 000000000..3b5148fee
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Processor/ProcessorPropertyInterface.php
@@ -0,0 +1,28 @@
+container = $container;
+ $this->drupalProxyOriginalServiceId = $drupal_proxy_original_service_id;
+ }
+
+ /**
+ * Lazy loads the real service from the container.
+ *
+ * @return \Drupal\search_api\ParamConverter\SearchApiConverter
+ * Returns the constructed real service.
+ */
+ protected function lazyLoadItself() {
+ if (!isset($this->service)) {
+ $this->service = $this->container->get($this->drupalProxyOriginalServiceId);
+ }
+
+ return $this->service;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function convert($value, $definition, $name, array $defaults) {
+ return $this->lazyLoadItself()->convert($value, $definition, $name, $defaults);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function applies($definition, $name, Route $route) {
+ return $this->lazyLoadItself()->applies($definition, $name, $route);
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Query/Condition.php b/sites/all/modules/contrib/search/search_api/src/Query/Condition.php
new file mode 100644
index 000000000..74f455bc1
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Query/Condition.php
@@ -0,0 +1,99 @@
+field = $field;
+ $this->value = $value;
+ $this->operator = $operator;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getField() {
+ return $this->field;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setField($field) {
+ $this->field = $field;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getValue() {
+ return $this->value;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setValue($value) {
+ $this->value = $value;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getOperator() {
+ return $this->operator;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setOperator($operator) {
+ $this->operator = $operator;
+ return $this;
+ }
+
+ /**
+ * Implements the magic __toString() method to simplify debugging.
+ */
+ public function __toString() {
+ return "{$this->field} {$this->operator} " . str_replace("\n", "\n ", var_export($this->value, TRUE));
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Query/ConditionGroup.php b/sites/all/modules/contrib/search/search_api/src/Query/ConditionGroup.php
new file mode 100644
index 000000000..1a5861fcc
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Query/ConditionGroup.php
@@ -0,0 +1,120 @@
+conjunction = strtoupper(trim($conjunction)) == 'OR' ? 'OR' : 'AND';
+ $this->tags = array_combine($tags, $tags);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getConjunction() {
+ return $this->conjunction;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addConditionGroup(ConditionGroupInterface $condition_group) {
+ $this->conditions[] = $condition_group;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addCondition($field, $value, $operator = '=') {
+ $this->conditions[] = new Condition($field, $value, $operator);
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function &getConditions() {
+ return $this->conditions;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hasTag($tag) {
+ return isset($this->tags[$tag]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function &getTags() {
+ return $this->tags;
+ }
+
+ /**
+ * Implements the magic __clone() method.
+ *
+ * Takes care to clone nested conditions and condition groups, too.
+ */
+ public function __clone() {
+ foreach ($this->conditions as $i => $condition) {
+ $this->conditions[$i] = clone $condition;
+ }
+ }
+
+ /**
+ * Implements the magic __toString() method to simplify debugging.
+ */
+ public function __toString() {
+ // Special case for a single, nested condition group:
+ if (count($this->conditions) == 1) {
+ return (string) reset($this->conditions);
+ }
+ $ret = [];
+ foreach ($this->conditions as $condition) {
+ $ret[] = str_replace("\n", "\n ", (string) $condition);
+ }
+ return $ret ? "[\n " . implode("\n{$this->conjunction}\n ", $ret) . "\n]" : '';
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Query/ConditionGroupInterface.php b/sites/all/modules/contrib/search/search_api/src/Query/ConditionGroupInterface.php
new file mode 100644
index 000000000..661559df0
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Query/ConditionGroupInterface.php
@@ -0,0 +1,55 @@
+", "<", "<=", ">=",
+ * ">", "IN", "NOT IN", "BETWEEN", "NOT BETWEEN". They have the same
+ * semantics as the corresponding SQL operators. Other operators might be
+ * added by backend features.
+ * If $field is a fulltext field, $operator can only be "=" or "<>", which
+ * are in this case interpreted as "contains" or "doesn't contain",
+ * respectively.
+ * If $value is NULL, $operator also can only be "=" or "<>", meaning the
+ * field must have no or some value, respectively.
+ *
+ * @return $this
+ */
+ public function addCondition($field, $value, $operator = '=');
+
+ /**
+ * Adds a nested condition group.
+ *
+ * @param \Drupal\search_api\Query\ConditionGroupInterface $condition_group
+ * A condition group that should be added.
+ *
+ * @return $this
+ */
+ public function addConditionGroup(ConditionGroupInterface $condition_group);
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Query/Query.php b/sites/all/modules/contrib/search/search_api/src/Query/Query.php
new file mode 100644
index 000000000..c07780f54
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Query/Query.php
@@ -0,0 +1,777 @@
+status()) {
+ $index_label = $index->label();
+ throw new SearchApiException("Can't search on index '$index_label' which is disabled.");
+ }
+ $this->index = $index;
+ $this->results = new ResultSet($this);
+ $this->options = $options;
+ $this->conditionGroup = $this->createConditionGroup('AND');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(IndexInterface $index, array $options = []) {
+ return new static($index, $options);
+ }
+
+ /**
+ * Retrieves the module handler.
+ *
+ * @return \Drupal\Core\Extension\ModuleHandlerInterface
+ * The module handler.
+ */
+ public function getModuleHandler() {
+ return $this->moduleHandler ?: \Drupal::moduleHandler();
+ }
+
+ /**
+ * Sets the module handler.
+ *
+ * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
+ * The new module handler.
+ *
+ * @return $this
+ */
+ public function setModuleHandler(ModuleHandlerInterface $module_handler) {
+ $this->moduleHandler = $module_handler;
+ return $this;
+ }
+
+ /**
+ * Retrieves the parse mode manager.
+ *
+ * @return \Drupal\search_api\ParseMode\ParseModePluginManager
+ * The parse mode manager.
+ */
+ public function getParseModeManager() {
+ return $this->parseModeManager ?: \Drupal::service('plugin.manager.search_api.parse_mode');
+ }
+
+ /**
+ * Sets the parse mode manager.
+ *
+ * @param \Drupal\search_api\ParseMode\ParseModePluginManager $parse_mode_manager
+ * The new parse mode manager.
+ *
+ * @return $this
+ */
+ public function setParseModeManager(ParseModePluginManager $parse_mode_manager) {
+ $this->parseModeManager = $parse_mode_manager;
+ return $this;
+ }
+
+ /**
+ * Retrieves the display plugin manager.
+ *
+ * @return \Drupal\search_api\Display\DisplayPluginManagerInterface
+ * The display plugin manager.
+ */
+ public function getDisplayPluginManager() {
+ return $this->displayPluginManager ?: \Drupal::service('plugin.manager.search_api.display');
+ }
+
+ /**
+ * Sets the display plugin manager.
+ *
+ * @param \Drupal\search_api\Display\DisplayPluginManagerInterface $display_plugin_manager
+ * The new display plugin manager.
+ *
+ * @return $this
+ */
+ public function setDisplayPluginManager(DisplayPluginManagerInterface $display_plugin_manager) {
+ $this->displayPluginManager = $display_plugin_manager;
+ return $this;
+ }
+
+ /**
+ * Retrieves the query helper.
+ *
+ * @return \Drupal\search_api\Utility\QueryHelperInterface
+ * The query helper.
+ */
+ public function getQueryHelper() {
+ return $this->queryHelper ?: \Drupal::service('search_api.query_helper');
+ }
+
+ /**
+ * Sets the query helper.
+ *
+ * @param \Drupal\search_api\Utility\QueryHelperInterface $query_helper
+ * The new query helper.
+ *
+ * @return $this
+ */
+ public function setQueryHelper(QueryHelperInterface $query_helper) {
+ $this->queryHelper = $query_helper;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSearchId($generate = TRUE) {
+ if ($generate && !isset($this->searchId)) {
+ static $num = 0;
+ $this->searchId = 'search_' . ++$num;
+ }
+ return $this->searchId;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setSearchId($search_id) {
+ $this->searchId = $search_id;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDisplayPlugin() {
+ $display_manager = $this->getDisplayPluginManager();
+ if (isset($this->searchId) && $display_manager->hasDefinition($this->searchId)) {
+ return $display_manager->createInstance($this->searchId);
+ }
+ return NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getParseMode() {
+ if (!$this->parseMode) {
+ $this->parseMode = $this->getParseModeManager()->createInstance('terms');
+ }
+ return $this->parseMode;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setParseMode(ParseModeInterface $parse_mode) {
+ $this->parseMode = $parse_mode;
+ if (is_scalar($this->origKeys)) {
+ $this->keys = $parse_mode->parseInput($this->origKeys);
+ }
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getLanguages() {
+ return $this->languages;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setLanguages(array $languages = NULL) {
+ $this->languages = isset($languages) ? array_values($languages) : NULL;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createConditionGroup($conjunction = 'AND', array $tags = []) {
+ return new ConditionGroup($conjunction, $tags);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function keys($keys = NULL) {
+ $this->origKeys = $keys;
+ if (is_scalar($keys)) {
+ $this->keys = $this->getParseMode()->parseInput("$keys");
+ }
+ else {
+ $this->keys = $keys;
+ }
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setFulltextFields(array $fields = NULL) {
+ $this->fields = $fields;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addConditionGroup(ConditionGroupInterface $condition_group) {
+ $this->conditionGroup->addConditionGroup($condition_group);
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addCondition($field, $value, $operator = '=') {
+ $this->conditionGroup->addCondition($field, $value, $operator);
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function sort($field, $order = self::SORT_ASC) {
+ $order = strtoupper(trim($order));
+ $order = $order == self::SORT_DESC ? self::SORT_DESC : self::SORT_ASC;
+ if (!isset($this->sorts[$field])) {
+ $this->sorts[$field] = $order;
+ }
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function range($offset = NULL, $limit = NULL) {
+ $this->options['offset'] = $offset;
+ $this->options['limit'] = $limit;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getProcessingLevel() {
+ return $this->processingLevel;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setProcessingLevel($level) {
+ $this->processingLevel = $level;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function abort($error_message = NULL) {
+ $this->aborted = isset($error_message) ? $error_message : TRUE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function wasAborted() {
+ return $this->aborted !== NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getAbortMessage() {
+ return is_bool($this->aborted) ? $this->aborted : NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function execute() {
+ if ($this->hasExecuted()) {
+ return $this->results;
+ }
+
+ $this->executed = TRUE;
+
+ // Check for aborted status both before and after calling preExecute().
+ if ($this->shouldAbort()) {
+ return $this->results;
+ }
+
+ // Prepare the query for execution by the server.
+ $this->preExecute();
+
+ if ($this->shouldAbort()) {
+ return $this->results;
+ }
+
+ // Execute query.
+ $this->index->getServerInstance()->search($this);
+
+ // Postprocess the search results.
+ $this->postExecute();
+
+ return $this->results;
+ }
+
+ /**
+ * Determines whether the query should be aborted.
+ *
+ * Also prepares the result set if the query should be aborted.
+ *
+ * @return bool
+ * TRUE if the query should be aborted, FALSE otherwise.
+ */
+ protected function shouldAbort() {
+ if (!$this->wasAborted() && $this->languages !== []) {
+ return FALSE;
+ }
+ $this->postExecute();
+ return TRUE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function preExecute() {
+ // Make sure to only execute this once per query, and not for queries with
+ // the "none" processing level.
+ if (!$this->preExecuteRan && $this->processingLevel != self::PROCESSING_NONE) {
+ $this->preExecuteRan = TRUE;
+
+ // Preprocess query.
+ $this->index->preprocessSearchQuery($this);
+
+ // Let modules alter the query.
+ $hooks = ['search_api_query'];
+ foreach ($this->tags as $tag) {
+ $hooks[] = "search_api_query_$tag";
+ }
+ $this->getModuleHandler()->alter($hooks, $this);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function postExecute() {
+ if ($this->processingLevel == self::PROCESSING_NONE) {
+ return;
+ }
+
+ // Postprocess results.
+ $this->index->postprocessSearchResults($this->results);
+
+ // Let modules alter the results.
+ $hooks = ['search_api_results'];
+ foreach ($this->tags as $tag) {
+ $hooks[] = "search_api_results_$tag";
+ }
+ $this->getModuleHandler()->alter($hooks, $this->results);
+
+ // Store the results in the static cache.
+ $this->getQueryHelper()->addResults($this->results);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hasExecuted() {
+ return $this->executed;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getResults() {
+ return $this->results;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getIndex() {
+ return $this->index;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function &getKeys() {
+ return $this->keys;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getOriginalKeys() {
+ return $this->origKeys;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function &getFulltextFields() {
+ return $this->fields;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getConditionGroup() {
+ return $this->conditionGroup;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function &getSorts() {
+ return $this->sorts;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getOption($name, $default = NULL) {
+ return array_key_exists($name, $this->options) ? $this->options[$name] : $default;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setOption($name, $value) {
+ $old = $this->getOption($name);
+ $this->options[$name] = $value;
+ return $old;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function &getOptions() {
+ return $this->options;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addTag($tag) {
+ $this->tags[$tag] = $tag;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hasTag($tag) {
+ return isset($this->tags[$tag]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hasAllTags() {
+ return !array_diff_key(array_flip(func_get_args()), $this->tags);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hasAnyTag() {
+ return (bool) array_intersect_key(array_flip(func_get_args()), $this->tags);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function &getTags() {
+ return $this->tags;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function __clone() {
+ $this->results = $this->getResults()->getCloneForQuery($this);
+ $this->conditionGroup = clone $this->conditionGroup;
+ if ($this->parseMode) {
+ $this->parseMode = clone $this->parseMode;
+ }
+ }
+
+ /**
+ * Implements the magic __sleep() method to avoid serializing the index.
+ */
+ public function __sleep() {
+ $this->indexId = $this->index->id();
+ $keys = $this->traitSleep();
+ return array_diff($keys, ['index']);
+ }
+
+ /**
+ * Implements the magic __wakeup() method to reload the query's index.
+ */
+ public function __wakeup() {
+ if (!isset($this->index)
+ && !empty($this->indexId)
+ && \Drupal::hasContainer()
+ && \Drupal::getContainer()->has('entity_type.manager')) {
+ $this->index = \Drupal::entityTypeManager()
+ ->getStorage('search_api_index')
+ ->load($this->indexId);
+ $this->indexId = NULL;
+ }
+
+ // Sanitize the service IDs saved by the serialization trait to guard
+ // against incomplete service containers. Doesn't need to happen when the
+ // trait's __wakeup() method will return early anyways, though.
+ // @todo Remove once #2909164 gets fixed in Core (and we depend on that Core
+ // version).
+ if (!isset($GLOBALS['__PHPUNIT_BOOTSTRAP']) || \Drupal::hasContainer()) {
+ $container = \Drupal::getContainer();
+ foreach ($this->_serviceIds as $key => $service_id) {
+ if (!$container->has($service_id)) {
+ unset($this->_serviceIds[$key]);
+ }
+ }
+ }
+ $this->traitWakeup();
+ }
+
+ /**
+ * Implements the magic __toString() method to simplify debugging.
+ */
+ public function __toString() {
+ $ret = 'Index: ' . $this->index->id() . "\n";
+ $ret .= 'Keys: ' . str_replace("\n", "\n ", var_export($this->origKeys, TRUE)) . "\n";
+ if (isset($this->keys)) {
+ $ret .= 'Parsed keys: ' . str_replace("\n", "\n ", var_export($this->keys, TRUE)) . "\n";
+ $ret .= 'Searched fields: ' . (isset($this->fields) ? implode(', ', $this->fields) : '[ALL]') . "\n";
+ }
+ if (isset($this->languages)) {
+ $ret .= 'Searched languages: ' . implode(', ', $this->languages) . "\n";
+ }
+ if ($conditions = (string) $this->conditionGroup) {
+ $conditions = str_replace("\n", "\n ", $conditions);
+ $ret .= "Conditions:\n $conditions\n";
+ }
+ if ($this->sorts) {
+ $sorts = [];
+ foreach ($this->sorts as $field => $order) {
+ $sorts[] = "$field $order";
+ }
+ $ret .= 'Sorting: ' . implode(', ', $sorts) . "\n";
+ }
+ $options = $this->sanitizeOptions($this->options);
+ $options = str_replace("\n", "\n ", var_export($options, TRUE));
+ $ret .= 'Options: ' . $options . "\n";
+ return $ret;
+ }
+
+ /**
+ * Sanitizes an array of options in a way that plays nice with var_export().
+ *
+ * @param array $options
+ * An array of options.
+ *
+ * @return array
+ * The sanitized options.
+ */
+ protected function sanitizeOptions(array $options) {
+ foreach ($options as $key => $value) {
+ if (is_object($value)) {
+ $options[$key] = 'object (' . get_class($value) . ')';
+ }
+ elseif (is_array($value)) {
+ $options[$key] = $this->sanitizeOptions($value);
+ }
+ }
+ return $options;
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Query/QueryInterface.php b/sites/all/modules/contrib/search/search_api/src/Query/QueryInterface.php
new file mode 100644
index 000000000..4d29a81be
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Query/QueryInterface.php
@@ -0,0 +1,523 @@
+query = $query;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getQuery() {
+ return $this->query;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getResultCount() {
+ return $this->resultCount;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setResultCount($result_count) {
+ $this->resultCount = $result_count;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getResultItems() {
+ return $this->resultItems;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addResultItem(ItemInterface $result_item) {
+ $this->resultItems[$result_item->getId()] = $result_item;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setResultItems(array $result_items) {
+ $this->resultItems = $result_items;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getWarnings() {
+ return $this->warnings;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addWarning($warning) {
+ $this->warnings[] = $warning;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setWarnings(array $warnings) {
+ $this->warnings = $warnings;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getIgnoredSearchKeys() {
+ return array_values($this->ignoredSearchKeys);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addIgnoredSearchKey($ignored_search_key) {
+ $this->ignoredSearchKeys[$ignored_search_key] = $ignored_search_key;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setIgnoredSearchKeys(array $ignored_search_keys) {
+ $this->ignoredSearchKeys = array_combine($ignored_search_keys, $ignored_search_keys);
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hasExtraData($key) {
+ return array_key_exists($key, $this->extraData);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getExtraData($key, $default = NULL) {
+ return array_key_exists($key, $this->extraData) ? $this->extraData[$key] : $default;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function &getAllExtraData() {
+ return $this->extraData;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setExtraData($key, $data = NULL) {
+ if (isset($data)) {
+ $this->extraData[$key] = $data;
+ }
+ else {
+ unset($this->extraData[$key]);
+ }
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCloneForQuery(QueryInterface $query) {
+ $clone = clone $this;
+ $clone->query = $query;
+ return $clone;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getIterator() {
+ return new \ArrayIterator($this->resultItems);
+ }
+
+ /**
+ * Implements the magic __toString() method to simplify debugging.
+ */
+ public function __toString() {
+ $out = $this->getResultCount() . ' results';
+ if ($this->getResultItems()) {
+ $out .= ':';
+ foreach ($this->getResultItems() as $item) {
+ $item = str_replace("\n", "\n ", "$item");
+ $out .= "\n- " . $item;
+ }
+ }
+ if ($this->getWarnings()) {
+ $out .= "\nWarnings:";
+ foreach ($this->getWarnings() as $warning) {
+ $out .= "\n- " . $warning;
+ }
+ }
+ if ($this->getIgnoredSearchKeys()) {
+ $out .= "\nIgnored keys:";
+ foreach ($this->getIgnoredSearchKeys() as $ignored_key) {
+ $out .= "\n- " . $ignored_key;
+ }
+ }
+ if ($this->getAllExtraData()) {
+ $data = str_replace("\n", "\n ", print_r($this->getAllExtraData(), TRUE));
+ $out .= "\nExtra data: " . $data;
+ }
+ return $out;
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Query/ResultSetInterface.php b/sites/all/modules/contrib/search/search_api/src/Query/ResultSetInterface.php
new file mode 100644
index 000000000..72ab2b9b5
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Query/ResultSetInterface.php
@@ -0,0 +1,194 @@
+taskManager = $task_manager;
+ $this->entityTypeManager = $entity_type_manager;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function getSubscribedEvents() {
+ $events['search_api.task.' . static::TRACK_ITEMS_TASK_TYPE][] = ['trackItems'];
+
+ return $events;
+ }
+
+ /**
+ * Processes all pending index tasks inside a batch run.
+ *
+ * @param array|\ArrayAccess $context
+ * The current batch context.
+ * @param \Drupal\Core\Config\ConfigImporter $config_importer
+ * The config importer.
+ *
+ * @throws \Drupal\search_api\SearchApiException
+ * Thrown if any error occurred while tracking items.
+ */
+ public static function processIndexTasks(&$context, ConfigImporter $config_importer) {
+ $index_task_manager = \Drupal::getContainer()
+ ->get('search_api.index_task_manager');
+
+ if (!isset($context['sandbox']['indexes'])) {
+ $context['sandbox']['indexes'] = [];
+
+ $indexes = \Drupal::entityTypeManager()
+ ->getStorage('search_api_index')
+ ->loadByProperties([
+ 'status' => TRUE,
+ ]);
+ $deleted = $config_importer->getUnprocessedConfiguration('delete');
+
+ /** @var \Drupal\search_api\IndexInterface $index */
+ foreach ($indexes as $index_id => $index) {
+ if (!$index_task_manager->isTrackingComplete($index) && !in_array($index->getConfigDependencyName(), $deleted)) {
+ $context['sandbox']['indexes'][] = $index_id;
+ }
+ }
+ $context['sandbox']['total'] = count($context['sandbox']['indexes']);
+ if (!$context['sandbox']['total']) {
+ $context['finished'] = 1;
+ return;
+ }
+ }
+
+ $index_id = array_shift($context['sandbox']['indexes']);
+ $index = Index::load($index_id);
+ try {
+ if (!($index_task_manager->addItemsOnce($index))) {
+ array_unshift($context['sandbox']['indexes'], $index_id);
+ }
+ }
+ catch (SearchApiException $e) {
+ watchdog_exception('search_api', $e);
+ }
+
+ if (empty($context['sandbox']['indexes'])) {
+ $context['finished'] = 1;
+ }
+ else {
+ $finished = $context['sandbox']['total'] - count($context['sandbox']['indexes']);
+ $context['finished'] = $finished / $context['sandbox']['total'];
+ $args = [
+ '%index' => $index->label(),
+ '@num' => $finished + 1,
+ '@total' => $context['sandbox']['total'],
+ ];
+ $context['message'] = \Drupal::translation()
+ ->translate('Tracking items for search index %index (@num of @total)', $args);
+ }
+ }
+
+ /**
+ * Tracks items according to the given event.
+ *
+ * @param \Drupal\search_api\Task\TaskEvent $event
+ * The task event.
+ */
+ public function trackItems(TaskEvent $event) {
+ $event->stopPropagation();
+
+ $task = $event->getTask();
+ $index = $task->getIndex();
+
+ if (!$index->hasValidTracker()) {
+ $args['%index'] = $index->label();
+ $message = new FormattableMarkup('Index %index does not have a valid tracker set.', $args);
+ $event->setException(new SearchApiException($message));
+ return;
+ }
+
+ $data = $task->getData();
+ $datasource_id = $data['datasource'];
+
+ $reschedule = FALSE;
+ if ($index->isValidDatasource($datasource_id)) {
+ $raw_ids = $index->getDatasource($datasource_id)->getItemIds($data['page']);
+ if ($raw_ids !== NULL) {
+ $reschedule = TRUE;
+ if ($raw_ids) {
+ $index->startBatchTracking();
+ $index->trackItemsInserted($datasource_id, $raw_ids);
+ $index->stopBatchTracking();
+ }
+ }
+ }
+
+ if ($reschedule) {
+ ++$data['page'];
+ $this->taskManager->addTask(static::TRACK_ITEMS_TASK_TYPE, NULL, $index, $data);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function startTracking(IndexInterface $index, array $datasource_ids = NULL) {
+ if (!isset($datasource_ids)) {
+ $datasource_ids = $index->getDatasourceIds();
+ }
+
+ foreach ($datasource_ids as $datasource_id) {
+ $data = [
+ 'datasource' => $datasource_id,
+ 'page' => 0,
+ ];
+ $this->taskManager->addTask(static::TRACK_ITEMS_TASK_TYPE, NULL, $index, $data);
+ }
+ }
+
+ /**
+ * Gets a set of conditions for finding the tracking tasks of the given index.
+ *
+ * @param \Drupal\search_api\IndexInterface $index
+ * The index for which to retrieve tasks.
+ *
+ * @return array
+ * An array of conditions to pass to the Search API task manager.
+ */
+ protected function getTaskConditions(IndexInterface $index) {
+ return [
+ 'type' => static::TRACK_ITEMS_TASK_TYPE,
+ 'index_id' => $index->id(),
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addItemsOnce(IndexInterface $index) {
+ return !$this->taskManager->executeSingleTask($this->getTaskConditions($index));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addItemsBatch(IndexInterface $index) {
+ $this->taskManager->setTasksBatch($this->getTaskConditions($index));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addItemsAll(IndexInterface $index) {
+ $this->taskManager->executeAllTasks($this->getTaskConditions($index));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function stopTracking(IndexInterface $index, array $datasource_ids = NULL) {
+ $valid_tracker = $index->hasValidTracker();
+ if (!isset($datasource_ids)) {
+ $this->taskManager->deleteTasks($this->getTaskConditions($index));
+ if ($valid_tracker) {
+ $index->getTrackerInstance()->trackAllItemsDeleted();
+ }
+ return;
+ }
+
+ // Catch the case of being called with an empty array of datasources.
+ if (!$datasource_ids) {
+ return;
+ }
+
+ $tasks = $this->taskManager->loadTasks($this->getTaskConditions($index));
+ foreach ($tasks as $task_id => $task) {
+ $data = $task->getData();
+ if (in_array($data['datasource'], $datasource_ids)) {
+ $this->taskManager->deleteTask($task_id);
+ }
+ }
+
+ foreach ($datasource_ids as $datasource_id) {
+ $index->getTrackerInstance()->trackAllItemsDeleted($datasource_id);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isTrackingComplete(IndexInterface $index) {
+ return !$this->taskManager->getTasksCount($this->getTaskConditions($index));
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Task/IndexTaskManagerInterface.php b/sites/all/modules/contrib/search/search_api/src/Task/IndexTaskManagerInterface.php
new file mode 100644
index 000000000..410fb81f5
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Task/IndexTaskManagerInterface.php
@@ -0,0 +1,84 @@
+taskManager = $task_manager;
+ $this->entityTypeManager = $entity_type_manager;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function getSubscribedEvents() {
+ $events = [];
+
+ foreach (static::getSupportedTypes() as $type) {
+ $events['search_api.task.' . $type][] = ['processEvent'];
+ }
+
+ return $events;
+ }
+
+ /**
+ * Retrieves the task types supported by this task manager.
+ *
+ * @return string[]
+ * The task types supported by this task manager.
+ */
+ protected static function getSupportedTypes() {
+ return [
+ 'addIndex',
+ 'updateIndex',
+ 'removeIndex',
+ 'deleteItems',
+ 'deleteAllIndexItems',
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCount(ServerInterface $server = NULL) {
+ return $this->taskManager->getTasksCount($this->getTaskConditions($server));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function execute(ServerInterface $server = NULL) {
+ if ($server && !$server->status()) {
+ return FALSE;
+ }
+
+ $conditions = $this->getTaskConditions($server);
+ try {
+ return $this->taskManager->executeAllTasks($conditions, 100);
+ }
+ catch (SearchApiException $e) {
+ $this->logException($e);
+ return FALSE;
+ }
+ }
+
+ /**
+ * Processes a single server task.
+ *
+ * @param \Drupal\search_api\Task\TaskEvent $event
+ * The task event.
+ */
+ public function processEvent(TaskEvent $event) {
+ $event->stopPropagation();
+
+ $task = $event->getTask();
+
+ try {
+ if (!$this->executeTask($task)) {
+ $type = $task->getType();
+ throw new SearchApiException("Task of unknown type '$type' passed to server task manager.");
+ }
+ }
+ catch (SearchApiException $e) {
+ $event->setException($e);
+ }
+ }
+
+ /**
+ * Executes a single server task.
+ *
+ * @param \Drupal\search_api\Task\TaskInterface $task
+ * The task to execute.
+ *
+ * @return bool
+ * TRUE if the task was successfully executed, FALSE if the task type was
+ * unknown.
+ *
+ * @throws \Drupal\search_api\SearchApiException
+ * If any error occurred while executing the task.
+ */
+ protected function executeTask(TaskInterface $task) {
+ $server = $task->getServer();
+ $index = $task->getIndex();
+ $data = $task->getData();
+
+ switch ($task->getType()) {
+ case 'addIndex':
+ if ($index) {
+ $server->getBackend()->addIndex($index);
+ }
+ return TRUE;
+
+ case 'updateIndex':
+ if ($index) {
+ if ($data) {
+ $index->original = $data;
+ }
+ $server->getBackend()->updateIndex($index);
+ }
+ return TRUE;
+
+ case 'removeIndex':
+ $index = $index ?: $data;
+ if ($index) {
+ $server->getBackend()->removeIndex($index);
+ }
+ return TRUE;
+
+ case 'deleteItems':
+ if ($index && !$index->isReadOnly()) {
+ $server->getBackend()->deleteItems($index, $data);
+ }
+ return TRUE;
+
+ case 'deleteAllIndexItems':
+ if ($index && !$index->isReadOnly()) {
+ $server->getBackend()->deleteAllIndexItems($index, $data);
+ }
+ return TRUE;
+ }
+
+ // We didn't know that type of task.
+ return FALSE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setExecuteBatch(ServerInterface $server = NULL) {
+ $this->taskManager->setTasksBatch($this->getTaskConditions($server));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function delete(ServerInterface $server = NULL, $index = NULL, array $types = NULL) {
+ $conditions = $this->getTaskConditions($server);
+ if ($index !== NULL) {
+ $conditions['index_id'] = $index instanceof IndexInterface ? $index->id() : $index;
+ }
+ if ($types !== NULL) {
+ $conditions['type'] = $types;
+ }
+ $this->taskManager->deleteTasks($conditions);
+ }
+
+ /**
+ * Gets a set of conditions for finding the tracking tasks of the given index.
+ *
+ * @param \Drupal\search_api\ServerInterface $server
+ * The server for which to retrieve tasks.
+ *
+ * @return array
+ * An array of conditions to pass to the Search API task manager.
+ */
+ protected function getTaskConditions(ServerInterface $server = NULL) {
+ $conditions['type'] = static::getSupportedTypes();
+ if ($server) {
+ $conditions['server_id'] = $server->id();
+ }
+ return $conditions;
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Task/ServerTaskManagerInterface.php b/sites/all/modules/contrib/search/search_api/src/Task/ServerTaskManagerInterface.php
new file mode 100644
index 000000000..5f9031abc
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Task/ServerTaskManagerInterface.php
@@ -0,0 +1,62 @@
+task = $task;
+ }
+
+ /**
+ * Retrieves the executed task.
+ *
+ * @return \Drupal\search_api\Task\TaskInterface
+ * The task being executed.
+ */
+ public function getTask() {
+ return $this->task;
+ }
+
+ /**
+ * Retrieves any exception that stopped the execution of the task.
+ *
+ * @return \Drupal\search_api\SearchApiException|null
+ * The exception, if any.
+ */
+ public function getException() {
+ return $this->exception;
+ }
+
+ /**
+ * Sets the exception that stopped the execution of the task.
+ *
+ * @param \Drupal\search_api\SearchApiException|null $exception
+ * The exception that occurred.
+ *
+ * @return $this
+ */
+ public function setException(SearchApiException $exception = NULL) {
+ $this->exception = $exception;
+ return $this;
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Task/TaskInterface.php b/sites/all/modules/contrib/search/search_api/src/Task/TaskInterface.php
new file mode 100644
index 000000000..5512da0ba
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Task/TaskInterface.php
@@ -0,0 +1,85 @@
+entityTypeManager = $entity_type_manager;
+ $this->eventDispatcher = $event_dispatcher;
+ $this->setStringTranslation($translation);
+ }
+
+ /**
+ * Returns the entity storage for search tasks.
+ *
+ * @return \Drupal\Core\Entity\EntityStorageInterface
+ * The storage handler.
+ */
+ protected function getTaskStorage() {
+ return $this->entityTypeManager->getStorage('search_api_task');
+ }
+
+ /**
+ * Creates an entity query matching the given search tasks.
+ *
+ * @param array $conditions
+ * (optional) An array of conditions to be matched for the tasks, with
+ * property names keyed to the value (or values, for multiple possibilities)
+ * that the property should have.
+ *
+ * @return \Drupal\Core\Entity\Query\QueryInterface
+ * An entity query for search tasks.
+ */
+ protected function getTasksQuery(array $conditions = []) {
+ $query = $this->getTaskStorage()->getQuery();
+ foreach ($conditions as $property => $values) {
+ $query->condition($property, $values, is_array($values) ? 'IN' : '=');
+ }
+ $query->sort('id');
+ return $query;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getTasksCount(array $conditions = []) {
+ return $this->getTasksQuery($conditions)->count()->execute();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addTask($type, ServerInterface $server = NULL, IndexInterface $index = NULL, $data = NULL) {
+ if (isset($data)) {
+ if ($data instanceof EntityInterface) {
+ $data = [
+ '#entity_type' => $data->getEntityTypeId(),
+ '#values' => $data->toArray(),
+ ];
+ }
+ $data = serialize($data);
+ }
+
+ $task = $this->getTaskStorage()->create([
+ 'type' => $type,
+ 'server_id' => $server ? $server->id() : NULL,
+ 'index_id' => $index ? $index->id() : NULL,
+ 'data' => $data,
+ ]);
+ $task->save();
+ return $task;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function loadTasks(array $conditions = []) {
+ $task_ids = $this->getTasksQuery($conditions)->execute();
+ if ($task_ids) {
+ return $this->getTaskStorage()->loadMultiple($task_ids);
+ }
+ return [];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function deleteTask($task_id) {
+ $task = $this->getTaskStorage()->load($task_id);
+ if ($task) {
+ $task->delete();
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function deleteTasks(array $conditions = []) {
+ $storage = $this->getTaskStorage();
+ while (TRUE) {
+ $task_ids = $this->getTasksQuery($conditions)
+ ->range(0, 100)
+ ->execute();
+ if (!$task_ids) {
+ break;
+ }
+ $tasks = $storage->loadMultiple($task_ids);
+ $storage->delete($tasks);
+ if (count($task_ids) < 100) {
+ break;
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function executeSpecificTask(TaskInterface $task) {
+ $event = new TaskEvent($task);
+ $this->eventDispatcher->dispatch('search_api.task.' . $task->getType(), $event);
+ if (!$event->isPropagationStopped()) {
+ $id = $task->id();
+ $type = $task->getType();
+ throw new SearchApiException("Could not execute task #$id of type '$type'. Type seems to be unknown.");
+ }
+ if ($exception = $event->getException()) {
+ throw $exception;
+ }
+ $task->delete();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function executeSingleTask(array $conditions = []) {
+ $task_id = $this->getTasksQuery($conditions)->range(0, 1)->execute();
+ if ($task_id) {
+ $task_id = reset($task_id);
+ /** @var \Drupal\search_api\Task\TaskInterface $task */
+ $task = $this->getTaskStorage()->load($task_id);
+ $this->executeSpecificTask($task);
+ return TRUE;
+ }
+ return FALSE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function executeAllTasks(array $conditions = [], $limit = NULL) {
+ // We have to use this roundabout way because tasks, during their execution,
+ // might create additional tasks. (For example, see
+ // \Drupal\search_api\Task\IndexTaskManager::trackItems().)
+ $executed = 0;
+ while (TRUE) {
+ $query = $this->getTasksQuery($conditions);
+ if (isset($limit)) {
+ $query->range(0, $limit - $executed);
+ }
+ $task_ids = $query->execute();
+
+ if (!$task_ids) {
+ break;
+ }
+
+ // We can't use multi-load here as a task might delete other tasks, so we
+ // have to make sure each tasks still exists right before it is executed.
+ foreach ($task_ids as $task_id) {
+ /** @var \Drupal\search_api\Task\TaskInterface $task */
+ $task = $this->getTaskStorage()->load($task_id);
+ if ($task) {
+ $this->executeSpecificTask($task);
+ }
+ else {
+ --$executed;
+ }
+ }
+
+ $executed += count($task_ids);
+ if (isset($limit) && $executed >= $limit) {
+ break;
+ }
+ }
+
+ return !$this->getTasksCount($conditions);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setTasksBatch(array $conditions = []) {
+ $task_ids = $this->getTasksQuery($conditions)->range(0, 100)->execute();
+
+ if (!$task_ids) {
+ return;
+ }
+
+ $batch_definition = [
+ 'operations' => [
+ [[$this, 'processBatch'], [$task_ids, $conditions]],
+ ],
+ 'finished' => [$this, 'finishBatch'],
+ ];
+ // Schedule the batch.
+ batch_set($batch_definition);
+ }
+
+ /**
+ * Processes a single pending task as part of a batch operation.
+ *
+ * @param int[] $task_ids
+ * An array of task IDs to execute. Might not contain all task IDs.
+ * @param array $conditions
+ * An array of conditions defining the tasks to be executed. Should be used
+ * to retrieve more task IDs if necessary.
+ * @param array|\ArrayAccess $context
+ * The context of the current batch, as defined in the @link batch Batch
+ * operations @endlink documentation.
+ *
+ * @throws \Drupal\search_api\SearchApiException
+ * Thrown if any error occurred while processing the task.
+ */
+ public function processBatch(array $task_ids, array $conditions, &$context) {
+ // Initialize context information.
+ if (!isset($context['sandbox']['task_ids'])) {
+ $context['sandbox']['task_ids'] = $task_ids;
+ $context['results']['total'] = $this->getTasksCount($conditions);
+ }
+
+ $task_id = array_shift($context['sandbox']['task_ids']);
+ /** @var \Drupal\search_api\Task\TaskInterface $task */
+ $task = $this->getTaskStorage()->load($task_id);
+
+ if ($task) {
+ $this->executeSpecificTask($task);
+ }
+
+ if (!$context['sandbox']['task_ids']) {
+ $context['sandbox']['task_ids'] = $this->getTasksQuery($conditions)
+ ->range(0, 100)
+ ->execute();
+ if (!$context['sandbox']['task_ids']) {
+ $context['finished'] = 1;
+ return;
+ }
+ }
+
+ $pending = $this->getTasksCount($conditions);
+ $context['finished'] = 1 - $pending / $context['results']['total'];
+ $executed = $context['results']['total'] - $pending;
+ $context['message'] = $this->formatPlural(
+ $executed,
+ 'Successfully executed @count pending task.',
+ 'Successfully executed @count pending tasks.'
+ );
+ }
+
+ /**
+ * Finishes an "execute tasks" batch.
+ *
+ * @param bool $success
+ * Indicates whether the batch process was successful.
+ * @param array $results
+ * Results information passed from the processing callback.
+ */
+ public function finishBatch($success, array $results) {
+ // Check if the batch job was successful.
+ if ($success) {
+ $message = $this->formatPlural(
+ $results['total'],
+ 'Successfully executed @count pending task.',
+ 'Successfully executed @count pending tasks.'
+ );
+ drupal_set_message($message);
+ }
+ else {
+ // Notify the user about the batch job failure.
+ drupal_set_message($this->t('An error occurred while trying to execute tasks. Check the logs for details.'), 'error');
+ }
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Task/TaskManagerInterface.php b/sites/all/modules/contrib/search/search_api/src/Task/TaskManagerInterface.php
new file mode 100644
index 000000000..24c9abe1d
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Task/TaskManagerInterface.php
@@ -0,0 +1,133 @@
+setCacheBackend($cache_backend, 'search_api_trackers');
+ $this->alterInfo('search_api_tracker_info');
+ }
+
+ /**
+ * Retrieves an options list of available trackers.
+ *
+ * @return string[]
+ * An associative array mapping the IDs of all available tracker plugins to
+ * their labels.
+ */
+ public function getOptionsList() {
+ $options = [];
+ foreach ($this->getDefinitions() as $plugin_id => $plugin_definition) {
+ $options[$plugin_id] = Html::escape($plugin_definition['label']);
+ }
+ return $options;
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/UnsavedConfigurationInterface.php b/sites/all/modules/contrib/search/search_api/src/UnsavedConfigurationInterface.php
new file mode 100644
index 000000000..d0138a02a
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/UnsavedConfigurationInterface.php
@@ -0,0 +1,78 @@
+entity = $index;
+ $this->tempStore = $temp_store;
+ $this->currentUserId = $current_user_id;
+ }
+
+ /**
+ * Retrieves the entity type manager.
+ *
+ * @return \Drupal\Core\Entity\EntityTypeManagerInterface
+ * The entity type manager.
+ */
+ public function getEntityTypeManager() {
+ return $this->entityTypeManager ?: \Drupal::entityTypeManager();
+ }
+
+ /**
+ * Sets the entity type manager.
+ *
+ * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+ * The new entity type manager.
+ *
+ * @return $this
+ */
+ public function setEntityTypeManager(EntityTypeManagerInterface $entity_type_manager) {
+ $this->entityTypeManager = $entity_type_manager;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setCurrentUserId($current_user_id) {
+ $this->currentUserId = $current_user_id;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hasChanges() {
+ return (bool) $this->lock;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isLocked() {
+ if ($this->lock) {
+ return $this->lock->owner != $this->currentUserId;
+ }
+ return FALSE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getLockOwner() {
+ if (!$this->lock) {
+ return NULL;
+ }
+ $uid = is_numeric($this->lock->owner) ? $this->lock->owner : 0;
+ return $this->getEntityTypeManager()->getStorage('user')->load($uid);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getLastUpdated() {
+ return $this->lock ? $this->lock->updated : NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setLockInformation($lock = NULL) {
+ $this->lock = $lock;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function savePermanent() {
+ // Make sure to overwrite only the index's fields, not just all properties.
+ // Unlike the Views UI, we have several separate pages for editing index
+ // entities, and only one of them is locked. Therefore, this extra step is
+ // necessary, we can't just call $this->entity->save().
+ /** @var \Drupal\search_api\Entity\SearchApiConfigEntityStorage $storage */
+ $storage = $this->getEntityTypeManager()->getStorage('search_api_index');
+ $storage->resetCache([$this->entity->id()]);
+ /** @var \Drupal\search_api\IndexInterface $original */
+ $original = $storage->loadOverrideFree($this->entity->id());
+ $fields = $this->entity->getFields();
+ // Set the correct index object on the field objects.
+ foreach ($fields as $field) {
+ $field->setIndex($original);
+ }
+ $original->setFields($fields);
+ $original->save();
+ // Setting the saved entity as the wrapped one is important if methods like
+ // isReindexing() are called on the object afterwards.
+ $this->entity = $original;
+ $this->discardChanges();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function discardChanges() {
+ $this->tempStore->delete($this->entity->id());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDescription() {
+ return $this->entity->getDescription();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isReadOnly() {
+ return $this->entity->isReadOnly();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getOption($name, $default = NULL) {
+ return $this->entity->getOption($name, $default);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getOptions() {
+ return $this->entity->getOptions();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setOption($name, $option) {
+ $this->entity->setOption($name, $option);
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setOptions(array $options) {
+ $this->entity->setOptions($options);
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDatasources() {
+ return $this->entity->getDatasources();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDatasourceIds() {
+ return $this->entity->getDatasourceIds();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isValidDatasource($datasource_id) {
+ return $this->entity->isValidDatasource($datasource_id);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDatasource($datasource_id) {
+ return $this->entity->getDatasource($datasource_id);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addDatasource(DatasourceInterface $datasource) {
+ $this->entity->addDatasource($datasource);
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function removeDatasource($datasource_id) {
+ $this->entity->removeDatasource($datasource_id);
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setDatasources(array $datasources) {
+ $this->entity->setDatasources($datasources);
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getEntityTypes() {
+ return $this->entity->getEntityTypes();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hasValidTracker() {
+ return $this->entity->hasValidTracker();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getTrackerId() {
+ return $this->entity->getTrackerId();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getTrackerInstance() {
+ return $this->entity->getTrackerInstance();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setTracker(TrackerInterface $tracker) {
+ $this->entity->setTracker($tracker);
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hasValidServer() {
+ return $this->entity->hasValidServer();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isServerEnabled() {
+ return $this->entity->isServerEnabled();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getServerId() {
+ return $this->entity->getServerId();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getServerInstance() {
+ return $this->entity->getServerInstance();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setServer(ServerInterface $server = NULL) {
+ $this->entity->setServer($server);
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getProcessors() {
+ return $this->entity->getProcessors();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getProcessorsByStage($stage, array $overrides = []) {
+ return $this->entity->getProcessorsByStage($stage, $overrides);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isValidProcessor($processor_id) {
+ return $this->entity->isValidProcessor($processor_id);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getProcessor($processor_id) {
+ return $this->entity->getProcessor($processor_id);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addProcessor(ProcessorInterface $processor) {
+ $this->entity->addProcessor($processor);
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function removeProcessor($processor_id) {
+ $this->entity->removeProcessor($processor_id);
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setProcessors(array $processors) {
+ $this->entity->setProcessors($processors);
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function alterIndexedItems(array &$items) {
+ $this->entity->alterIndexedItems($items);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function preprocessIndexItems(array $items) {
+ $this->entity->preprocessIndexItems($items);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function preprocessSearchQuery(QueryInterface $query) {
+ $this->entity->preprocessSearchQuery($query);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function postprocessSearchResults(ResultSetInterface $results) {
+ $this->entity->postprocessSearchResults($results);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addField(FieldInterface $field) {
+ $this->entity->addField($field);
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function renameField($old_field_id, $new_field_id) {
+ $this->entity->renameField($old_field_id, $new_field_id);
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function removeField($field_id) {
+ $this->entity->removeField($field_id);
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setFields(array $fields) {
+ $this->entity->setFields($fields);
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFields($include_server_defined = FALSE) {
+ return $this->entity->getFields($include_server_defined);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getField($field_id) {
+ return $this->entity->getField($field_id);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFieldsByDatasource($datasource_id) {
+ return $this->entity->getFieldsByDatasource($datasource_id);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFulltextFields() {
+ return $this->entity->getFulltextFields();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFieldRenames() {
+ return $this->entity->getFieldRenames();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function discardFieldChanges() {
+ $this->entity->discardFieldChanges();
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getPropertyDefinitions($datasource_id) {
+ return $this->entity->getPropertyDefinitions($datasource_id);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function loadItem($item_id) {
+ return $this->entity->loadItem($item_id);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function loadItemsMultiple(array $item_ids) {
+ return $this->entity->loadItemsMultiple($item_ids);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function indexItems($limit = -1, $datasource_id = NULL) {
+ return $this->entity->indexItems($limit, $datasource_id);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function indexSpecificItems(array $search_objects) {
+ return $this->entity->indexSpecificItems($search_objects);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isBatchTracking() {
+ return $this->entity->isBatchTracking();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function startBatchTracking() {
+ $this->entity->startBatchTracking();
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function stopBatchTracking() {
+ $this->entity->stopBatchTracking();
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function trackItemsInserted($datasource_id, array $ids) {
+ $this->entity->trackItemsInserted($datasource_id, $ids);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function trackItemsUpdated($datasource_id, array $ids) {
+ $this->entity->trackItemsUpdated($datasource_id, $ids);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function trackItemsDeleted($datasource_id, array $ids) {
+ $this->entity->trackItemsDeleted($datasource_id, $ids);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function reindex() {
+ $this->entity->reindex();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function clear() {
+ $this->entity->clear();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function rebuildTracker() {
+ $this->entity->rebuildTracker();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isReindexing() {
+ return $this->entity->isReindexing();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function query(array $options = []) {
+ return $this->entity->query($options);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function enable() {
+ $this->entity->enable();
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function disable() {
+ $this->entity->disable();
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setStatus($status) {
+ $this->entity->setStatus($status);
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setSyncing($status) {
+ $this->entity->setSyncing($status);
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function status() {
+ return $this->entity->status();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isSyncing() {
+ return $this->entity->isSyncing();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isUninstalling() {
+ return $this->entity->isUninstalling();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function get($property_name) {
+ return $this->entity->get($property_name);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function set($property_name, $value) {
+ $this->entity->set($property_name, $value);
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function calculateDependencies() {
+ $this->entity->calculateDependencies();
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function onDependencyRemoval(array $dependencies) {
+ return $this->entity->onDependencyRemoval($dependencies);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDependencies() {
+ return $this->entity->getDependencies();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isInstallable() {
+ return $this->entity->isInstallable();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function trustData() {
+ $this->entity->trustData();
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hasTrustedData() {
+ return $this->entity->hasTrustedData();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function uuid() {
+ return $this->entity->uuid();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function id() {
+ return $this->entity->id();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function language() {
+ return $this->entity->language();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isNew() {
+ return $this->entity->isNew();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function enforceIsNew($value = TRUE) {
+ $this->entity->enforceIsNew($value);
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getEntityTypeId() {
+ return $this->entity->getEntityTypeId();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function bundle() {
+ return $this->entity->bundle();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function label() {
+ return $this->entity->label();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function urlInfo($rel = 'canonical', array $options = []) {
+ return $this->entity->toUrl($rel, $options);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function toUrl($rel = 'canonical', array $options = []) {
+ return $this->entity->toUrl($rel, $options);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function url($rel = 'canonical', $options = []) {
+ return $this->entity->toUrl($rel, $options)->toString();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function link($text = NULL, $rel = 'canonical', array $options = []) {
+ return $this->entity->toLink($text, $rel, $options)->toString();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function toLink($text = NULL, $rel = 'canonical', array $options = []) {
+ return $this->entity->toLink($text, $rel, $options);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function hasLinkTemplate($key) {
+ return $this->entity->hasLinkTemplate($key);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function uriRelationships() {
+ return $this->entity->uriRelationships();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function load($id) {
+ return Index::load($id);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function loadMultiple(array $ids = NULL) {
+ return Index::loadMultiple($ids);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(array $values = []) {
+ return Index::create($values);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function save() {
+ if ($this->tempStore->setIfOwner($this->entity->id(), $this->entity)) {
+ return SAVED_UPDATED;
+ }
+ throw new EntityStorageException('Cannot save temporary index configuration: currently being edited by someone else.');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function delete() {
+ $this->entity->delete();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function preSave(EntityStorageInterface $storage) {
+ $this->entity->preSave($storage);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function postSave(EntityStorageInterface $storage, $update = TRUE) {
+ $this->entity->postSave($storage, $update);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function preCreate(EntityStorageInterface $storage, array &$values) {
+ EntityInterface::preCreate($storage, $values);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function postCreate(EntityStorageInterface $storage) {
+ $this->entity->postCreate($storage);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function preDelete(EntityStorageInterface $storage, array $entities) {
+ EntityInterface::preDelete($storage, $entities);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function postDelete(EntityStorageInterface $storage, array $entities) {
+ EntityInterface::postDelete($storage, $entities);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function postLoad(EntityStorageInterface $storage, array &$entities) {
+ EntityInterface::postLoad($storage, $entities);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createDuplicate() {
+ return new UnsavedIndexConfiguration($this->entity->createDuplicate(), $this->tempStore, $this->currentUserId);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getEntityType() {
+ return $this->entity->getEntityType();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function referencedEntities() {
+ return $this->entity->referencedEntities();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getOriginalId() {
+ return $this->entity->getOriginalId();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCacheTagsToInvalidate() {
+ return $this->entity->getCacheTagsToInvalidate();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setOriginalId($id) {
+ $this->entity->setOriginalId($id);
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function toArray() {
+ return $this->entity->toArray();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getTypedData() {
+ return $this->entity->getTypedData();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getConfigDependencyKey() {
+ return $this->entity->getConfigDependencyKey();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getConfigDependencyName() {
+ return $this->entity->getConfigDependencyName();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getConfigTarget() {
+ return $this->entity->getConfigTarget();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function access($operation, AccountInterface $account = NULL, $return_as_object = FALSE) {
+ return $this->entity->access($operation, $account, $return_as_object);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCacheContexts() {
+ return $this->entity->getCacheContexts();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCacheTags() {
+ return $this->entity->getCacheTags();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCacheMaxAge() {
+ return $this->entity->getCacheMaxAge();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addCacheContexts(array $cache_contexts) {
+ $this->entity->addCacheContexts($cache_contexts);
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addCacheTags(array $cache_tags) {
+ $this->entity->addCacheTags($cache_tags);
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function mergeCacheMaxAge($max_age) {
+ $this->entity->mergeCacheMaxAge($max_age);
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addCacheableDependency($other_object) {
+ $this->entity->addCacheableDependency($other_object);
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setThirdPartySetting($module, $key, $value) {
+ $this->entity->setThirdPartySetting($module, $key, $value);
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getThirdPartySetting($module, $key, $default = NULL) {
+ return $this->entity->getThirdPartySetting($module, $key, $default);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getThirdPartySettings($module) {
+ return $this->entity->getThirdPartySettings($module);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function unsetThirdPartySetting($module, $key) {
+ return $this->entity->unsetThirdPartySetting($module, $key);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getThirdPartyProviders() {
+ return $this->entity->getThirdPartyProviders();
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Utility/CommandHelper.php b/sites/all/modules/contrib/search/search_api/src/Utility/CommandHelper.php
new file mode 100644
index 000000000..ad6fc207e
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Utility/CommandHelper.php
@@ -0,0 +1,704 @@
+entityTypeManager = $entity_type_manager;
+ $this->indexStorage = $entity_type_manager->getStorage('search_api_index');
+ $this->serverStorage = $entity_type_manager->getStorage('search_api_server');
+ $this->moduleHandler = $module_handler;
+ $this->translationFunction = $translation_function;
+ }
+
+ /**
+ * Lists all search indexes.
+ *
+ * @return array
+ * An associative array, keyed by search index ID, each value an associative
+ * array with the following keys:
+ * - id: The ID of the search index.
+ * - name: The human readable name of the search index.
+ * - server: The ID of the server associated with the search index.
+ * - serverName: The human readable name of the server associated with the
+ * search index.
+ * - types: An array of entity type IDs that are tracked in the index.
+ * - typeNames: An array of human readable entity type labels that are
+ * tracked in the index.
+ * - status: Either "enabled" or "disabled".
+ * - limit: The number of items that are processed in a single cron run.
+ *
+ * @throws \Drupal\search_api\SearchApiException
+ * Thrown if an index has a server which couldn't be loaded.
+ */
+ public function indexListCommand() {
+ $indexes = $this->loadIndexes();
+ if (!$indexes) {
+ return [];
+ }
+
+ $rows = [];
+ $none = '(' . $this->t('none') . ')';
+ $enabled = $this->t('enabled');
+ $disabled = $this->t('disabled');
+
+ foreach ($indexes as $index) {
+ $types = [];
+ $type_names = [];
+ foreach ($index->getDatasources() as $datasource) {
+ $types[] = $datasource->getEntityTypeId();
+ $type_names[] = (string) $datasource->label();
+ }
+ $rows[$index->id()] = [
+ 'id' => $index->id(),
+ 'name' => $index->label(),
+ 'server' => $index->getServerId() ?: $none,
+ 'serverName' => $index->getServerId() ? $index->getServerInstance()->label() : $none,
+ 'types' => $types,
+ 'typeNames' => $type_names,
+ 'status' => $index->status() ? $enabled : $disabled,
+ 'limit' => (int) $index->getOption('cron_limit'),
+ ];
+ }
+
+ return $rows;
+ }
+
+ /**
+ * Lists all search indexes with their status.
+ *
+ * @param string[]|null $indexId
+ * (optional) An array of search index IDs, or NULL to list the status of
+ * all indexes.
+ *
+ * @return array
+ * An associative array, keyed by search index ID, each value an associative
+ * array with the following keys:
+ * - id: The ID of the search index.
+ * - name: The human readable name of the search index.
+ * - complete: a percentage of indexation.
+ * - indexed: The amount of indexed items.
+ * - total: The total amount of items.
+ *
+ * @throws \Drupal\search_api\SearchApiException
+ * Thrown if one of the affected indexes had an invalid tracker set.
+ */
+ public function indexStatusCommand(array $indexId = NULL) {
+ $indexes = $this->loadIndexes($indexId);
+ if (!$indexes) {
+ return [];
+ }
+
+ $rows = [];
+ foreach ($indexes as $index) {
+ $indexed = $index->getTrackerInstance()->getIndexedItemsCount();
+ $total = $index->getTrackerInstance()->getTotalItemsCount();
+
+ $complete = '-';
+ if ($total > 0) {
+ $complete = (100 * round($indexed / $total, 3)) . '%';
+ }
+
+ $rows[$index->id()] = [
+ 'id' => $index->id(),
+ 'name' => $index->label(),
+ 'complete' => $complete,
+ 'indexed' => $indexed,
+ 'total' => $total,
+ ];
+ }
+
+ return $rows;
+ }
+
+ /**
+ * Enables one or more disabled search indexes.
+ *
+ * @param array $index_ids
+ * (optional) An array of machine names of indexes to enable. If omitted all
+ * indexes will be enabled.
+ *
+ * @throws \Drupal\search_api\ConsoleException
+ * Thrown if no indexes could be loaded.
+ */
+ public function enableIndexCommand(array $index_ids = NULL) {
+ if (!$this->getIndexCount()) {
+ throw new ConsoleException($this->t('There are no indexes defined. Please create an index before trying to enable it.'));
+ }
+
+ $indexes = $this->loadIndexes($index_ids);
+
+ if (!$indexes) {
+ throw new ConsoleException($this->t('You must specify at least one index to enable.'));
+ }
+
+ foreach ($indexes as $index) {
+ if (!$index->status()) {
+ $this->setIndexState($index);
+ }
+ }
+ }
+
+ /**
+ * Disables one or more enabled search indexes.
+ *
+ * @param array $index_ids
+ * (optional) An array of machine names of indexes to disable. If omitted
+ * all indexes will be disabled.
+ *
+ * @throws \Drupal\search_api\ConsoleException
+ * Thrown if no indexes could be loaded.
+ */
+ public function disableIndexCommand(array $index_ids = NULL) {
+ if (!$this->getIndexCount()) {
+ throw new ConsoleException($this->t('There are no indexes defined. Please create an index before trying to disable it.'));
+ }
+
+ $indexes = $this->loadIndexes($index_ids);
+
+ if (!$indexes) {
+ throw new ConsoleException($this->t('You must specify at least one index to disable.'));
+ }
+
+ foreach ($indexes as $index) {
+ if ($index->status()) {
+ $this->setIndexState($index, FALSE);
+ }
+ }
+ }
+
+ /**
+ * Indexes items on one or more indexes.
+ *
+ * @param string[]|null $indexIds
+ * (optional) An array of index IDs, or NULL if we should index items for
+ * all enabled indexes.
+ * @param int|null $limit
+ * (optional) The maximum number of items to index, or NULL to index all
+ * items.
+ * @param int|null $batchSize
+ * (optional) The maximum number of items to process per batch, or NULL to
+ * index all items at once.
+ *
+ * @return bool
+ * TRUE if any indexes could be loaded, FALSE otherwise.
+ *
+ * @throws \Drupal\search_api\ConsoleException
+ * Thrown if an indexing batch process could not be created.
+ * @throws \Drupal\search_api\SearchApiException
+ * Thrown if one of the affected indexes had an invalid tracker set.
+ */
+ public function indexItemsToIndexCommand(array $indexIds = NULL, $limit = NULL, $batchSize = NULL) {
+ $indexes = $this->loadIndexes($indexIds);
+ if (!$indexes) {
+ return FALSE;
+ }
+
+ $batch_set = FALSE;
+ foreach ($indexes as $index) {
+ if (!$index->status() || $index->isReadOnly()) {
+ continue;
+ }
+ $tracker = $index->getTrackerInstance();
+ $remaining = $tracker->getTotalItemsCount() - $tracker->getIndexedItemsCount();
+
+ if (!$remaining) {
+ $this->logger->info($this->t("The index @index is up to date.", ['@index' => $index->label()]));
+ continue;
+ }
+ else {
+ $arguments = [
+ '@remaining' => $remaining,
+ '@limit' => $limit ? $limit : $this->t('all'),
+ '@index' => $index->label(),
+ ];
+ $this->logger->info($this->t("Found @remaining items to index for @index. Indexing @limit items.", $arguments));
+ }
+
+ // If we pass NULL, it would be used as "no items". -1 is the correct way
+ // to index all items.
+ $current_limit = $limit ?: -1;
+
+ // Get the default batch size.
+ if (!$batchSize) {
+ $cron_limit = $index->getOption('cron_limit');
+ $batchSize = $cron_limit ?: \Drupal::configFactory()
+ ->get('search_api.settings')
+ ->get('default_cron_limit');
+ }
+
+ // Get the number items to index.
+ if (!isset($current_limit) || !is_int($current_limit += 0) || $current_limit <= 0) {
+ $current_limit = $remaining;
+ }
+
+ $arguments = [
+ '@index' => $index->label(),
+ '@limit' => $current_limit,
+ '@batch_size' => $batchSize,
+ ];
+ $this->logger->info($this->t("Indexing a maximum number of @limit items (@batch_size items per batch run) for the index '@index'.", $arguments));
+
+ // Create the batch.
+ try {
+ IndexBatchHelper::create($index, $batchSize, $current_limit);
+ $batch_set = TRUE;
+ }
+ catch (SearchApiException $e) {
+ throw new ConsoleException($this->t("Couldn't create a batch, please check the batch size and limit parameters."));
+ }
+ }
+
+ return $batch_set;
+ }
+
+ /**
+ * Resets the tracker for an index, optionally filtering on entity types.
+ *
+ * @param string[]|null $indexIds
+ * (optional) An array of index IDs, or NULL if we should reset the trackers
+ * of all indexes.
+ * @param string[] $entityTypes
+ * (optional) An array of entity types for which to reset the tracker.
+ *
+ * @return bool
+ * TRUE if any index was affected, FALSE otherwise.
+ *
+ * @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 resetTrackerCommand(array $indexIds = NULL, array $entityTypes = []) {
+ $indexes = $this->loadIndexes($indexIds);
+ if (!$indexes) {
+ return FALSE;
+ }
+
+ foreach ($indexes as $index) {
+ if (!$index->status()) {
+ continue;
+ }
+ if (!empty($entityTypes)) {
+ $datasources = $index->getDatasources();
+ $reindexed_datasources = [];
+ foreach ($datasources as $datasource_id => $datasource) {
+ if (in_array($datasource->getEntityTypeId(), $entityTypes)) {
+ $index->getTrackerInstance()->trackAllItemsUpdated($datasource_id);
+ $reindexed_datasources[] = $datasource->label();
+ }
+ }
+ $this->moduleHandler->invokeAll('search_api_index_reindex', [$index, FALSE]);
+ $arguments = [
+ '!index' => $index->label(),
+ '!datasources' => implode(', ', $reindexed_datasources),
+ ];
+ $this->logger->info($this->t('The following datasources of !index were successfully scheduled for reindexing: !datasources.', $arguments));
+ }
+ else {
+ $index->reindex();
+ $this->logger->info($this->t('!index was successfully scheduled for reindexing.', ['!index' => $index->label()]));
+ }
+ }
+
+ return TRUE;
+ }
+
+ /**
+ * Deletes all items from one or more indexes.
+ *
+ * @param string[]|null $indexIds
+ * (optional) An array of index IDs, or NULL if we should delete all items
+ * from all indexes.
+ *
+ * @return bool
+ * TRUE when the clearing was successful, FALSE when no indexes were found.
+ *
+ * @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 clearIndexCommand(array $indexIds = NULL) {
+ $indexes = $this->loadIndexes($indexIds);
+ if (!$indexes) {
+ return FALSE;
+ }
+ foreach ($indexes as $index) {
+ if ($index->status()) {
+ $index->clear();
+ $this->logger->info($this->t('@index was successfully cleared.', ['@index' => $index->label()]));
+ }
+ }
+
+ return TRUE;
+ }
+
+ /**
+ * Returns an array of results.
+ *
+ * @param string $indexId
+ * The index to search in.
+ * @param string|null $keyword
+ * (optional) The word to search for.
+ *
+ * @return array
+ * An array of results, each of which is represented by an associative
+ * array with the following keys:
+ * - id: The internal ID of the item.
+ * - label: The label of the item, or NULL if it could not be determined.
+ *
+ * @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 searchIndexCommand($indexId, $keyword = NULL) {
+ $indexes = $this->loadIndexes([$indexId]);
+ if (empty($indexes[$indexId])) {
+ throw new ConsoleException($this->t('@index was not found'));
+ }
+
+ $query = $indexes[$indexId]->query();
+ if ($keyword !== NULL) {
+ $query->keys($keyword);
+ }
+
+ $query->range(0, 10);
+ try {
+ $results = $query->execute();
+ }
+ catch (SearchApiException $e) {
+ throw new ConsoleException($e->getMessage(), 0, $e);
+ }
+
+ $rows = [];
+ foreach ($results->getResultItems() as $item) {
+ try {
+ $label = $item->getDatasource()
+ ->getItemLabel($item->getOriginalObject());
+ }
+ catch (SearchApiException $e) {
+ $label = NULL;
+ }
+ $rows[] = [
+ 'id' => $item->getId(),
+ 'label' => $label,
+ ];
+ }
+
+ return $rows;
+ }
+
+ /**
+ * Returns a list of servers created on the page.
+ *
+ * @return array
+ * An associative array, keyed by search server ID, each value an
+ * associative array with the following keys:
+ * - id: The ID of the search server.
+ * - name: The human readable name of the search server.
+ * - status: The enabled status of the server.
+ *
+ * @throws \Drupal\search_api\ConsoleException
+ * Thrown if no servers could be loaded.
+ */
+ public function serverListCommand() {
+ $servers = $this->loadServers();
+ if (count($servers) === 0) {
+ throw new ConsoleException($this->t('There are no servers present.'));
+ }
+
+ $rows = [];
+ foreach ($servers as $server) {
+ $rows[$server->id()] = [
+ 'id' => $server->id(),
+ 'name' => $server->label(),
+ 'status' => $server->status() ? $this->t('enabled') : $this->t('disabled'),
+ ];
+ }
+
+ return $rows;
+ }
+
+ /**
+ * Enables a server.
+ *
+ * @param string $serverId
+ * The server's ID.
+ *
+ * @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 enableServerCommand($serverId) {
+ $servers = $this->loadServers([$serverId]);
+ if (empty($servers)) {
+ throw new ConsoleException($this->t('The server could not be loaded.'));
+ }
+ /** @var \Drupal\search_api\ServerInterface $server */
+ $server = $this->reloadEntityOverrideFree(reset($servers));
+ $server->setStatus(TRUE)->save();
+ }
+
+ /**
+ * Disables a server.
+ *
+ * @param string $serverId
+ * The server's ID.
+ *
+ * @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 disableServerCommand($serverId) {
+ $servers = $this->loadServers([$serverId]);
+ if (empty($servers)) {
+ throw new ConsoleException($this->t('The server could not be loaded.'));
+ }
+ /** @var \Drupal\search_api\ServerInterface $server */
+ $server = $this->reloadEntityOverrideFree(reset($servers));
+ $server->setStatus(FALSE)->save();
+ }
+
+ /**
+ * Clears all indexes on a server.
+ *
+ * @param string $serverId
+ * The ID of the server to 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 clearServerCommand($serverId) {
+ $servers = $this->loadServers([$serverId]);
+ if (empty($servers)) {
+ throw new ConsoleException($this->t('The server could not be loaded.'));
+ }
+ /** @var \Drupal\search_api\ServerInterface $server */
+ $server = $this->reloadEntityOverrideFree(reset($servers));
+
+ foreach ($server->getIndexes() as $index) {
+ $index->clear();
+ }
+ }
+
+ /**
+ * Switches an index to another server.
+ *
+ * @param string $indexId
+ * The ID of the index.
+ * @param string $serverId
+ * The ID of the index's new server.
+ *
+ * @throws \Drupal\search_api\ConsoleException
+ * If either the index or the server couldn't be loaded.
+ */
+ public function setIndexServerCommand($indexId, $serverId) {
+ // Fetch current index and server data.
+ $index = $this->loadIndexes([$indexId]);
+ $server = $this->loadServers([$serverId]);
+
+ $index = reset($index);
+ $server = reset($server);
+
+ if (!$index) {
+ throw new ConsoleException($this->t('Invalid index ID "@index_id".', ['@index_id' => $indexId]));
+ }
+ if (!$server) {
+ throw new ConsoleException($this->t('Invalid server ID "@server_id".', ['@server_id' => $serverId]));
+ }
+
+ // Set the new server on the index.
+ try {
+ /** @var \Drupal\search_api\IndexInterface $index */
+ $index = $this->reloadEntityOverrideFree($index);
+ $index->setServer($server);
+ $index->save();
+ $this->logger->info($this->t('Index @index has been set to use server @server and items have been queued for indexing.', ['@index' => $indexId, '@server' => $serverId]));
+ }
+ catch (EntityStorageException $e) {
+ $this->logger->warning($e->getMessage());
+ $this->logger->warning($this->t('There was an error setting index @index to use server @server, or this index is already configured to use this server.', ['@index' => $indexId, '@server' => $serverId]));
+ }
+ }
+
+ /**
+ * Returns the indexes with the given IDs.
+ *
+ * @param array|null $indexIds
+ * (optional) The IDs of the search indexes to return, or NULL to load all
+ * indexes. An array with a single NULL value is interpreted the same way as
+ * passing NULL.
+ *
+ * @return \Drupal\search_api\IndexInterface[]
+ * An array of search indexes.
+ */
+ public function loadIndexes(array $indexIds = NULL) {
+ if ($indexIds === [NULL]) {
+ $indexIds = NULL;
+ }
+ return $this->indexStorage->loadMultiple($indexIds);
+ }
+
+ /**
+ * Returns the servers with the given IDs.
+ *
+ * @param array|null $serverIds
+ * (optional) The IDs of the search servers to return, or NULL to load all
+ * servers.
+ *
+ * @return \Drupal\search_api\ServerInterface[]
+ * An array of search servers.
+ */
+ public function loadServers(array $serverIds = NULL) {
+ return $this->serverStorage->loadMultiple($serverIds);
+ }
+
+ /**
+ * Returns the total number of search indexes.
+ *
+ * @return int
+ * The number of search indexes on this site.
+ */
+ public function getIndexCount() {
+ return count($this->loadIndexes());
+ }
+
+ /**
+ * Changes the state of a single index.
+ *
+ * @param \Drupal\search_api\IndexInterface $index
+ * The index to be enabled.
+ * @param bool $enable
+ * (optional) TRUE to enable, FALSE to disable the index.
+ */
+ public function setIndexState(IndexInterface $index, $enable = TRUE) {
+ $state_label = $enable ? $this->t('enabled') : $this->t('disabled');
+ $method = $enable ? 'enable' : 'disable';
+
+ if ($index->status() == $enable) {
+ $this->logger->info($this->t("The index @index is already @desired_state.", ['@index' => $index->label(), '@desired_state' => $state_label]));
+ return;
+ }
+ if (!$index->getServerId()) {
+ $this->logger->warning($this->t("Index @index could not be @desired_state because it is not bound to any server.", ['@index' => $index->label(), '@desired_state' => $state_label]));
+ return;
+ }
+
+ $index = $this->reloadEntityOverrideFree($index);
+ $index->$method()->save();
+ $this->logger->info($this->t("The index @index was successfully @desired_state.", ['@index' => $index->label(), '@desired_state' => $state_label]));
+ }
+
+ /**
+ * Loads an override-free copy of a config entity, for saving.
+ *
+ * @param \Drupal\Core\Config\Entity\ConfigEntityInterface $entity
+ * The entity to reload.
+ *
+ * @return \Drupal\Core\Entity\EntityInterface|null
+ * The override-free version of the entity, or NULL if it couldn't be
+ * loaded.
+ */
+ public function reloadEntityOverrideFree(ConfigEntityInterface $entity) {
+ try {
+ /** @var \Drupal\Core\Config\Entity\ConfigEntityStorageInterface $storage */
+ $storage = $this->entityTypeManager->getStorage($entity->getEntityTypeId());
+ return $storage->loadOverrideFree($entity->id());
+ }
+ catch (InvalidPluginDefinitionException $e) {
+ return NULL;
+ }
+ }
+
+ /**
+ * Translates a string using the set translation method.
+ *
+ * @param string $message
+ * The message to translate.
+ * @param array $arguments
+ * (optional) The translation arguments.
+ *
+ * @return string
+ * The translated message.
+ */
+ public function t($message, array $arguments = []) {
+ return call_user_func_array($this->translationFunction, [
+ $message,
+ $arguments,
+ ]);
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Utility/DataTypeHelper.php b/sites/all/modules/contrib/search/search_api/src/Utility/DataTypeHelper.php
new file mode 100644
index 000000000..e535cf3f8
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Utility/DataTypeHelper.php
@@ -0,0 +1,166 @@
+moduleHandler = $moduleHandler;
+ $this->dataTypeManager = $dataTypeManager;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isTextType($type, array $textTypes = ['text']) {
+ if (in_array($type, $textTypes)) {
+ return TRUE;
+ }
+ $dataType = $this->dataTypeManager->createInstance($type);
+ if ($dataType && !$dataType->isDefault()) {
+ return in_array($dataType->getFallbackType(), $textTypes);
+ }
+ return FALSE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFieldTypeMapping() {
+ // Check the cache first.
+ if (!isset($this->fieldTypeMapping)) {
+ // It's easier to write and understand this array in the form of
+ // $searchApiFieldType => [$dataTypes] and flip it below.
+ $defaultMapping = [
+ 'text' => [
+ 'field_item:string_long.string',
+ 'field_item:text_long.string',
+ 'field_item:text_with_summary.string',
+ 'search_api_html',
+ 'search_api_text',
+ ],
+ 'string' => [
+ 'string',
+ 'email',
+ 'uri',
+ 'filter_format',
+ 'duration_iso8601',
+ 'field_item:path',
+ ],
+ 'integer' => [
+ 'integer',
+ 'timespan',
+ ],
+ 'decimal' => [
+ 'decimal',
+ 'float',
+ ],
+ 'date' => [
+ 'date',
+ 'datetime_iso8601',
+ 'timestamp',
+ ],
+ 'boolean' => [
+ 'boolean',
+ ],
+ // Types we know about but want/have to ignore.
+ NULL => [
+ 'language',
+ ],
+ ];
+
+ foreach ($defaultMapping as $searchApiType => $dataTypes) {
+ foreach ($dataTypes as $dataType) {
+ $mapping[$dataType] = $searchApiType;
+ }
+ }
+
+ // Allow other modules to intercept and define what default type they want
+ // to use for their data type.
+ $this->moduleHandler->alter('search_api_field_type_mapping', $mapping);
+
+ $this->fieldTypeMapping = $mapping;
+ }
+
+ return $this->fieldTypeMapping;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDataTypeFallbackMapping(IndexInterface $index) {
+ // Check the cache first.
+ $indexId = $index->id();
+ if (empty($this->dataTypeFallbackMapping[$indexId])) {
+ $server = NULL;
+ try {
+ $server = $index->getServerInstance();
+ }
+ catch (SearchApiException $e) {
+ // If the server isn't available, just ignore it here and return all
+ // custom types.
+ }
+ $this->dataTypeFallbackMapping[$indexId] = [];
+ $dataTypes = $this->dataTypeManager->getInstances();
+ foreach ($dataTypes as $typeId => $dataType) {
+ // We know for sure that we do not need to fall back for the default
+ // data types as they are always present and are required to be
+ // supported by all backends.
+ if (!$dataType->isDefault() && (!$server || !$server->supportsDataType($typeId))) {
+ $this->dataTypeFallbackMapping[$indexId][$typeId] = $dataType->getFallbackType();
+ }
+ }
+ }
+
+ return $this->dataTypeFallbackMapping[$indexId];
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Utility/DataTypeHelperInterface.php b/sites/all/modules/contrib/search/search_api/src/Utility/DataTypeHelperInterface.php
new file mode 100644
index 000000000..ecd635920
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Utility/DataTypeHelperInterface.php
@@ -0,0 +1,49 @@
+entityTypeManager = $entityTypeManager;
+ $this->entityFieldManager = $entityFieldManager;
+ $this->entityBundleInfo = $entityBundleInfo;
+ $this->dataTypeHelper = $dataTypeHelper;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function extractFields(ComplexDataInterface $item, array $fields, $langcode = NULL) {
+ // If a language code was given, get the correct translation (if possible).
+ if ($langcode) {
+ if ($item instanceof TranslatableInterface) {
+ if ($item->hasTranslation($langcode)) {
+ $item = $item->getTranslation($langcode);
+ }
+ }
+ else {
+ $value = $item->getValue();
+ if ($value instanceof ContentEntityInterface) {
+ if ($value->hasTranslation($langcode)) {
+ $item = $value->getTranslation($langcode)->getTypedData();
+ }
+ }
+ }
+ }
+
+ // Figure out which fields are directly on the item and which need to be
+ // extracted from nested items.
+ $directFields = [];
+ $nestedFields = [];
+ foreach (array_keys($fields) as $key) {
+ if (strpos($key, ':') !== FALSE) {
+ list($direct, $nested) = explode(':', $key, 2);
+ $nestedFields[$direct][$nested] = $fields[$key];
+ }
+ else {
+ $directFields[] = $key;
+ }
+ }
+ // Extract the direct fields.
+ $properties = $item->getProperties(TRUE);
+ foreach ($directFields as $key) {
+ if (empty($properties[$key])) {
+ continue;
+ }
+ $data = $item->get($key);
+ foreach ($fields[$key] as $field) {
+ $this->extractField($data, $field);
+ }
+ }
+ // Recurse for all nested fields.
+ foreach ($nestedFields as $direct => $fieldsNested) {
+ if (empty($properties[$direct])) {
+ continue;
+ }
+ $itemNested = $item->get($direct);
+ if ($itemNested instanceof DataReferenceInterface) {
+ $itemNested = $itemNested->getTarget();
+ }
+ if ($itemNested instanceof EntityInterface) {
+ $itemNested = $itemNested->getTypedData();
+ }
+ if ($itemNested instanceof ComplexDataInterface && !$itemNested->isEmpty()) {
+ $this->extractFields($itemNested, $fieldsNested, $langcode);
+ }
+ elseif ($itemNested instanceof ListInterface && !$itemNested->isEmpty()) {
+ foreach ($itemNested as $listItem) {
+ if ($listItem instanceof ComplexDataInterface && !$listItem->isEmpty()) {
+ $this->extractFields($listItem, $fieldsNested, $langcode);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function extractField(TypedDataInterface $data, FieldInterface $field) {
+ $values = $this->extractFieldValues($data);
+
+ foreach ($values as $i => $value) {
+ $field->addValue($value);
+ }
+ $field->setOriginalType($data->getDataDefinition()->getDataType());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function extractFieldValues(TypedDataInterface $data) {
+ if ($data->getDataDefinition()->isList()) {
+ $values = [];
+ foreach ($data as $piece) {
+ $values[] = $this->extractFieldValues($piece);
+ }
+ return $values ? call_user_func_array('array_merge', $values) : [];
+ }
+
+ $value = $data->getValue();
+ $definition = $data->getDataDefinition();
+ if ($definition instanceof ComplexDataDefinitionInterface) {
+ $property = $definition->getMainPropertyName();
+ return isset($value[$property]) ? [$value[$property]] : [];
+ }
+ if (is_array($value)) {
+ return array_values($value);
+ }
+ return [$value];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function extractItemValues(array $items, array $required_properties, $load = TRUE) {
+ $extracted_values = [];
+
+ /** @var \Drupal\search_api\Item\ItemInterface $item */
+ foreach ($items as $i => $item) {
+ $index = $item->getIndex();
+ $item_values = [];
+ /** @var \Drupal\search_api\Item\FieldInterface[][] $missing_fields */
+ $missing_fields = [];
+ $processor_fields = [];
+ $needed_processors = [];
+ foreach ([NULL, $item->getDatasourceId()] as $datasource_id) {
+ if (empty($required_properties[$datasource_id])) {
+ continue;
+ }
+
+ $properties = $index->getPropertyDefinitions($datasource_id);
+ foreach ($required_properties[$datasource_id] as $property_path => $combined_id) {
+ $item_values[$combined_id] = [];
+
+ // If a field with the right property path is already set on the item,
+ // use it. This might actually make problems in case the values have
+ // already been processed in some way, or use a data type that
+ // transformed their original value. But that will hopefully not be a
+ // problem in most situations.
+ foreach ($this->filterForPropertyPath($item->getFields(FALSE), $datasource_id, $property_path) as $field) {
+ $item_values[$combined_id] = $field->getValues();
+ continue 2;
+ }
+
+ // There are no values present on the item for this property. If we
+ // don't want to extract any fields, skip it.
+ if (!$load) {
+ continue;
+ }
+
+ // If the field is not already on the item, we need to extract it. We
+ // set our own combined ID as the field identifier as kind of a hack,
+ // to easily be able to add the field values to $property_values
+ // afterwards.
+ // In case the first part of the property path refers to a
+ // processor-defined property, we need to use the processor to
+ // retrieve the value. Otherwise, we extract it normally from the
+ // data object.
+ $property = NULL;
+ $property_name = Utility::splitPropertyPath($property_path, FALSE)[0];
+ if (isset($properties[$property_name])) {
+ $property = $properties[$property_name];
+ }
+ if ($property instanceof ProcessorPropertyInterface) {
+ $field_info = [
+ 'datasource_id' => $datasource_id,
+ 'property_path' => $property_path,
+ ];
+ if ($property instanceof ConfigurablePropertyInterface) {
+ $field_info['configuration'] = $property->defaultConfiguration();
+ // If the index contains a field with that property, just use the
+ // configuration from there instead of the default configuration.
+ // This will probably be what users expect in most situations.
+ foreach ($this->filterForPropertyPath($index->getFields(), $datasource_id, $property_path) as $field) {
+ $field_info['configuration'] = $field->getConfiguration();
+ break;
+ }
+ }
+ $processor_fields[] = $this->createField($index, $combined_id, $field_info);
+ $needed_processors[$property->getProcessorId()] = TRUE;
+ }
+ elseif ($datasource_id) {
+ $missing_fields[$property_path][] = $this->createField($index, $combined_id);
+ }
+ }
+ }
+ if ($missing_fields) {
+ $this->extractFields($item->getOriginalObject(), $missing_fields);
+ foreach ($missing_fields as $property_fields) {
+ foreach ($property_fields as $field) {
+ $item_values[$field->getFieldIdentifier()] = $field->getValues();
+ }
+ }
+ }
+ if ($processor_fields) {
+ $dummy_item = clone $item;
+ $dummy_item->setFields($processor_fields);
+ $dummy_item->setFieldsExtracted(TRUE);
+ $processors = $index->getProcessorsByStage(ProcessorInterface::STAGE_ADD_PROPERTIES);
+ foreach ($processors as $processor_id => $processor) {
+ if (isset($needed_processors[$processor_id])) {
+ $processor->addFieldValues($dummy_item);
+ }
+ }
+ foreach ($processor_fields as $field) {
+ $item_values[$field->getFieldIdentifier()] = $field->getValues();
+ }
+ }
+
+ $extracted_values[$i] = $item_values;
+ }
+
+ return $extracted_values;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function filterForPropertyPath(array $fields, $datasource_id, $property_path) {
+ $found_fields = [];
+ foreach ($fields as $field_id => $field) {
+ if ($field->getDatasourceId() === $datasource_id && $field->getPropertyPath() === $property_path) {
+ $found_fields[$field_id] = $field;
+ }
+ }
+ return $found_fields;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getNestedProperties(ComplexDataDefinitionInterface $property) {
+ $nestedProperties = $property->getPropertyDefinitions();
+ if ($property instanceof EntityDataDefinitionInterface) {
+ $entity_type_id = $property->getEntityTypeId();
+ $is_content_type = $this->isContentEntityType($entity_type_id);
+ if ($is_content_type) {
+ $bundles = $this->entityBundleInfo->getBundleInfo($entity_type_id);
+ foreach ($bundles as $bundle => $bundleLabel) {
+ $bundleProperties = $this->entityFieldManager
+ ->getFieldDefinitions($entity_type_id, $bundle);
+ $nestedProperties += $bundleProperties;
+ }
+ }
+ }
+ return $nestedProperties;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function retrieveNestedProperty(array $properties, $propertyPath) {
+ list($key, $nestedPath) = Utility::splitPropertyPath($propertyPath, FALSE);
+ if (!isset($properties[$key])) {
+ return NULL;
+ }
+
+ $property = $this->getInnerProperty($properties[$key]);
+ if (!isset($nestedPath)) {
+ return $property;
+ }
+
+ if (!$property instanceof ComplexDataDefinitionInterface) {
+ return NULL;
+ }
+
+ return $this->retrieveNestedProperty($this->getNestedProperties($property), $nestedPath);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getInnerProperty(DataDefinitionInterface $property) {
+ while ($property instanceof ListDataDefinitionInterface) {
+ $property = $property->getItemDefinition();
+ }
+ while ($property instanceof DataReferenceDefinitionInterface) {
+ $property = $property->getTargetDefinition();
+ }
+ return $property;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isContentEntityType($entity_type_id) {
+ try {
+ $definition = $this->entityTypeManager->getDefinition($entity_type_id);
+ return $definition->entityClassImplements(ContentEntityInterface::class);
+ }
+ catch (PluginNotFoundException $e) {
+ return FALSE;
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isFieldIdReserved($fieldId) {
+ return substr($fieldId, 0, 11) == 'search_api_';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createItem(IndexInterface $index, $id, DatasourceInterface $datasource = NULL) {
+ return new Item($index, $id, $datasource);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createItemFromObject(IndexInterface $index, ComplexDataInterface $originalObject, $id = NULL, DatasourceInterface $datasource = NULL) {
+ if (!isset($id)) {
+ if (!isset($datasource)) {
+ throw new \InvalidArgumentException('Need either an item ID or the datasource to create a search item from an object.');
+ }
+ $id = Utility::createCombinedId($datasource->getPluginId(), $datasource->getItemId($originalObject));
+ }
+ $item = $this->createItem($index, $id, $datasource);
+ $item->setOriginalObject($originalObject);
+ return $item;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createField(IndexInterface $index, $fieldIdentifier, array $fieldInfo = []) {
+ $field = new Field($index, $fieldIdentifier);
+
+ foreach ($fieldInfo as $key => $value) {
+ $method = 'set' . Container::camelize($key);
+ if (method_exists($field, $method)) {
+ $field->$method($value);
+ }
+ }
+
+ return $field;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createFieldFromProperty(IndexInterface $index, DataDefinitionInterface $property, $datasourceId, $propertyPath, $fieldId = NULL, $type = NULL) {
+ if (!isset($fieldId)) {
+ $fieldId = $this->getNewFieldId($index, $propertyPath);
+ }
+
+ if (!isset($type)) {
+ $typeMapping = $this->dataTypeHelper->getFieldTypeMapping();
+ $propertyType = $property->getDataType();
+ if (isset($typeMapping[$propertyType])) {
+ $type = $typeMapping[$propertyType];
+ }
+ else {
+ $propertyName = $property->getLabel();
+ throw new SearchApiException("No default data type mapping could be found for property '$propertyName' ($propertyPath) of type '$propertyType'.");
+ }
+ }
+
+ $fieldInfo = [
+ 'label' => $property->getLabel(),
+ 'datasource_id' => $datasourceId,
+ 'property_path' => $propertyPath,
+ 'type' => $type,
+ 'data_definition' => $property,
+ ];
+ if ($property instanceof ConfigurablePropertyInterface) {
+ $fieldInfo['configuration'] = $property->defaultConfiguration();
+ }
+ return $this->createField($index, $fieldId, $fieldInfo);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getNewFieldId(IndexInterface $index, $propertyPath) {
+ list(, $suggestedId) = Utility::splitPropertyPath($propertyPath);
+
+ // Avoid clashes with reserved IDs by removing the reserved "search_api_"
+ // from our suggested ID.
+ $suggestedId = str_replace('search_api_', '', $suggestedId);
+
+ $fieldId = $suggestedId;
+ $i = 0;
+ while ($index->getField($fieldId)) {
+ $fieldId = $suggestedId . '_' . ++$i;
+ }
+
+ while ($this->isFieldIdReserved($fieldId)) {
+ $fieldId = '_' . $fieldId;
+ }
+
+ return $fieldId;
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Utility/FieldsHelperInterface.php b/sites/all/modules/contrib/search/search_api/src/Utility/FieldsHelperInterface.php
new file mode 100644
index 000000000..31a0f58ea
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Utility/FieldsHelperInterface.php
@@ -0,0 +1,260 @@
+datasourcePluginManager = $datasource_plugin_manager;
+ $this->processorPluginManager = $processor_plugin_manager;
+ $this->trackerPluginManager = $tracker_plugin_manager;
+ }
+
+ /**
+ * Creates a plugin object for the given index.
+ *
+ * @param \Drupal\search_api\IndexInterface $index
+ * The index for which to create the plugins.
+ * @param string $type
+ * The type of plugin to create: "datasource", "processor" or "tracker".
+ * @param string $plugin_id
+ * The plugin's ID.
+ * @param array $configuration
+ * (optional) The configuration to set for the plugin.
+ *
+ * @return \Drupal\search_api\Plugin\IndexPluginInterface
+ * The new plugin object.
+ *
+ * @throws \Drupal\search_api\SearchApiException
+ * Thrown if an unknown $type or $plugin_id is given.
+ */
+ protected function createIndexPlugin(IndexInterface $index, $type, $plugin_id, array $configuration = []) {
+ if (!isset($this->{$type . "PluginManager"})) {
+ throw new SearchApiException("Unknown plugin type '$type'");
+ }
+ try {
+ $configuration['#index'] = $index;
+ return $this->{$type . "PluginManager"}->createInstance($plugin_id, $configuration);
+ }
+ catch (PluginException $e) {
+ throw new SearchApiException("Unknown $type plugin with ID '$plugin_id'");
+ }
+ }
+
+ /**
+ * Creates multiple plugin objects for the given index.
+ *
+ * @param \Drupal\search_api\IndexInterface $index
+ * The index for which to create the plugins.
+ * @param string $type
+ * The type of plugin to create: "datasource", "processor" or "tracker".
+ * @param string[]|null $plugin_ids
+ * (optional) The IDs of the plugins to create, or NULL to create instances
+ * for all known plugins of this type.
+ * @param array $configurations
+ * (optional) The configurations to set for the plugins, keyed by plugin ID.
+ * Missing configurations are either taken from the index's stored settings,
+ * if they are present there, or default to an empty array.
+ *
+ * @return \Drupal\search_api\Plugin\IndexPluginInterface[]
+ * The created plugin objects.
+ *
+ * @throws \Drupal\search_api\SearchApiException
+ * Thrown if an unknown $type or plugin ID is given.
+ */
+ protected function createIndexPlugins(IndexInterface $index, $type, array $plugin_ids = NULL, array $configurations = []) {
+ if (!isset($this->{$type . "PluginManager"})) {
+ throw new SearchApiException("Unknown plugin type '$type'");
+ }
+ if ($plugin_ids === NULL) {
+ $plugin_ids = array_keys($this->{$type . "PluginManager"}->getDefinitions());
+ }
+
+ $plugins = [];
+ $index_settings = $index->get($type . '_settings');
+ foreach ($plugin_ids as $plugin_id) {
+ $configuration = [];
+ if (isset($configurations[$plugin_id])) {
+ $configuration = $configurations[$plugin_id];
+ }
+ elseif (isset($index_settings[$plugin_id])) {
+ $configuration = $index_settings[$plugin_id];
+ }
+ $plugins[$plugin_id] = $this->createIndexPlugin($index, $type, $plugin_id, $configuration);
+ }
+
+ return $plugins;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createDatasourcePlugin(IndexInterface $index, $plugin_id, array $configuration = []) {
+ return $this->createIndexPlugin($index, 'datasource', $plugin_id, $configuration);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createProcessorPlugin(IndexInterface $index, $plugin_id, array $configuration = []) {
+ return $this->createIndexPlugin($index, 'processor', $plugin_id, $configuration);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createTrackerPlugin(IndexInterface $index, $plugin_id, array $configuration = []) {
+ return $this->createIndexPlugin($index, 'tracker', $plugin_id, $configuration);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createDatasourcePlugins(IndexInterface $index, array $plugin_ids = NULL, array $configuration = []) {
+ return $this->createIndexPlugins($index, 'datasource', $plugin_ids, $configuration);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createProcessorPlugins(IndexInterface $index, array $plugin_ids = NULL, array $configuration = []) {
+ return $this->createIndexPlugins($index, 'processor', $plugin_ids, $configuration);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createTrackerPlugins(IndexInterface $index, array $plugin_ids = NULL, array $configuration = []) {
+ return $this->createIndexPlugins($index, 'tracker', $plugin_ids, $configuration);
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Utility/PluginHelperInterface.php b/sites/all/modules/contrib/search/search_api/src/Utility/PluginHelperInterface.php
new file mode 100644
index 000000000..bd4141524
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Utility/PluginHelperInterface.php
@@ -0,0 +1,129 @@
+entityTypeManager = $entity_type_manager;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function getSubscribedEvents() {
+ $events[KernelEvents::TERMINATE][] = ['onKernelTerminate'];
+
+ return $events;
+ }
+
+ /**
+ * Indexes all items that were registered for indexing in this page request.
+ *
+ * Invoked by the terminate kernel event.
+ *
+ * @see \Symfony\Component\HttpKernel\Event\PostResponseEvent
+ */
+ public function onKernelTerminate() {
+ foreach ($this->operations as $index_id => $item_ids) {
+ try {
+ $storage = $this->entityTypeManager->getStorage('search_api_index');
+ }
+ catch (InvalidPluginDefinitionException $e) {
+ // It might be possible that the module got uninstalled during the rest
+ // of the page request, or something else happened. To be on the safe
+ // side, catch the exception in case the entity type isn't found.
+ return;
+ }
+
+ /** @var \Drupal\search_api\IndexInterface $index */
+ $index = $storage->load($index_id);
+ // It's possible that the index was deleted in the meantime, so make sure
+ // it's actually there.
+ if (!$index) {
+ continue;
+ }
+
+ try {
+ $items = $index->loadItemsMultiple($item_ids);
+ if ($items) {
+ $index->indexSpecificItems($items);
+ }
+ }
+ catch (SearchApiException $e) {
+ $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);
+ }
+
+ // We usually shouldn't be called twice in a page request, but no harm in
+ // being too careful: Remove the operation once it was executed correctly.
+ unset($this->operations[$index_id]);
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function registerIndexingOperation($index_id, array $item_ids) {
+ foreach ($item_ids as $item_id) {
+ $this->operations[$index_id][$item_id] = $item_id;
+ }
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Utility/PostRequestIndexingInterface.php b/sites/all/modules/contrib/search/search_api/src/Utility/PostRequestIndexingInterface.php
new file mode 100644
index 000000000..f7a72aed9
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Utility/PostRequestIndexingInterface.php
@@ -0,0 +1,20 @@
+requestStack = $requestStack;
+ $this->moduleHandler = $moduleHandler;
+ $this->parseModeManager = $parseModeManager;
+ $this->results = new \SplObjectStorage();
+ $this->null = (object) [];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function createQuery(IndexInterface $index, array $options = []) {
+ $query = Query::create($index, $options);
+
+ $query->setModuleHandler($this->moduleHandler);
+ $query->setParseModeManager($this->parseModeManager);
+ $query->setQueryHelper($this);
+
+ return $query;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addResults(ResultSetInterface $results) {
+ $search_id = $results->getQuery()->getSearchId();
+ $request = $this->getCurrentRequest();
+ if (!isset($this->results[$request])) {
+ $this->results[$request] = [
+ $search_id => $results,
+ ];
+ }
+ else {
+ // It's not possible to directly assign array values to an array inside of
+ // a \SplObjectStorage object. So we have to first retrieve the array,
+ // then add the results to it, then store it again.
+ $cache = $this->results[$request];
+ $cache[$search_id] = $results;
+ $this->results[$request] = $cache;
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getResults($search_id) {
+ $request = $this->getCurrentRequest();
+ if (isset($this->results[$request])) {
+ $results = $this->results[$request];
+ if (!empty($results[$search_id])) {
+ return $this->results[$request][$search_id];
+ }
+ }
+ return NULL;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getAllResults() {
+ $request = $this->getCurrentRequest();
+ if (isset($this->results[$request])) {
+ return $this->results[$request];
+ }
+ return [];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function removeResults($search_id) {
+ $request = $this->getCurrentRequest();
+ if (isset($this->results[$request])) {
+ $cache = $this->results[$request];
+ unset($cache[$search_id]);
+ $this->results[$request] = $cache;
+ }
+ }
+
+ /**
+ * Retrieves the current request.
+ *
+ * If there is no current request, instead of returning NULL this will instead
+ * return a unique object to be used in lieu of a NULL key.
+ *
+ * @return \Symfony\Component\HttpFoundation\Request|object
+ * The current request, if present; or this object's representation of the
+ * NULL key.
+ */
+ protected function getCurrentRequest() {
+ return $this->requestStack->getCurrentRequest() ?: $this->null;
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/src/Utility/QueryHelperInterface.php b/sites/all/modules/contrib/search/search_api/src/Utility/QueryHelperInterface.php
new file mode 100644
index 000000000..62404531b
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/src/Utility/QueryHelperInterface.php
@@ -0,0 +1,64 @@
+ $v) {
+ if (is_array($v)) {
+ if ($v = static::deepCopy($v)) {
+ $copy[$k] = $v;
+ }
+ }
+ elseif (is_object($v)) {
+ $copy[$k] = clone $v;
+ }
+ elseif ($v) {
+ $copy[$k] = $v;
+ }
+ }
+ return $copy;
+ }
+
+ /**
+ * Creates a combined ID from a raw ID and an optional datasource prefix.
+ *
+ * This can be used to created an internal item ID from a datasource ID and a
+ * datasource-specific raw item ID, or a combined property path from a
+ * datasource ID and a property path to identify properties index-wide.
+ *
+ * @param string|null $datasource_id
+ * The ID of the datasource to which the item belongs. Or NULL to return the
+ * raw ID unchanged (option included for compatibility purposes).
+ * @param string $raw_id
+ * The datasource-specific raw item ID of the item (or property).
+ *
+ * @return string
+ * The combined ID, with the datasource prefix separated by
+ * \Drupal\search_api\IndexInterface::DATASOURCE_ID_SEPARATOR.
+ */
+ public static function createCombinedId($datasource_id, $raw_id) {
+ if (!isset($datasource_id)) {
+ return $raw_id;
+ }
+ return $datasource_id . IndexInterface::DATASOURCE_ID_SEPARATOR . $raw_id;
+ }
+
+ /**
+ * Splits an internal ID into its two parts.
+ *
+ * Both internal item IDs and combined property paths are prefixed with the
+ * corresponding datasource ID. This method will split these IDs up again into
+ * their two parts.
+ *
+ * @param string $combined_id
+ * The internal ID, with an optional datasource prefix separated with
+ * \Drupal\search_api\IndexInterface::DATASOURCE_ID_SEPARATOR from the
+ * raw item ID or property path.
+ *
+ * @return array
+ * A numeric array, containing the datasource ID in element 0 and the raw
+ * item ID or property path in element 1. In the case of
+ * datasource-independent properties (that is, when there is no prefix),
+ * element 0 will be NULL.
+ */
+ public static function splitCombinedId($combined_id) {
+ if (strpos($combined_id, IndexInterface::DATASOURCE_ID_SEPARATOR) !== FALSE) {
+ return explode(IndexInterface::DATASOURCE_ID_SEPARATOR, $combined_id, 2);
+ }
+ return [NULL, $combined_id];
+ }
+
+ /**
+ * Splits a property path into two parts along a path separator (:).
+ *
+ * The path is split into one part with a single property name, and one part
+ * with the complete rest of the property path (which might be empty).
+ * Depending on $separate_last the returned single property key will be the
+ * first (FALSE) or last (TRUE) property of the path.
+ *
+ * @param string $property_path
+ * The property path to split.
+ * @param bool $separate_last
+ * (optional) If FALSE, separate the first property of the path. By default,
+ * the last property is separated from the rest.
+ * @param string $separator
+ * (optional) The separator to use.
+ *
+ * @return string[]
+ * An array with indexes 0 and 1, 0 containing the first part of the
+ * property path and 1 the second. If $separate_last is FALSE, index 0 will
+ * always contain a single property name (without any colons) and index 1
+ * might be NULL. If $separate_last is TRUE it's the exact other way round.
+ */
+ public static function splitPropertyPath($property_path, $separate_last = TRUE, $separator = ':') {
+ $function = $separate_last ? 'strrpos' : 'strpos';
+ $pos = $function($property_path, $separator);
+ if ($pos !== FALSE) {
+ return [
+ substr($property_path, 0, $pos),
+ substr($property_path, $pos + 1),
+ ];
+ }
+
+ if ($separate_last) {
+ return [NULL, $property_path];
+ }
+ return [$property_path, NULL];
+ }
+
+ /**
+ * Retrieves all overridden property values for the given config entity.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The config entity to check for overrides.
+ *
+ * @return array
+ * An associative array mapping property names to their overridden values.
+ */
+ public static function getConfigOverrides(EntityInterface $entity) {
+ $entity_type = $entity->getEntityType();
+ if (!($entity_type instanceof ConfigEntityTypeInterface)) {
+ return [];
+ }
+
+ $config_key = $entity_type->getConfigPrefix() . '.' . $entity->id();
+ $overrides = [];
+
+ // Overrides from tagged services.
+ $container = \Drupal::getContainer();
+ if ($container instanceof TaggedContainerInterface) {
+ $tags = $container->findTaggedServiceIds('config.factory.override');
+ foreach (array_keys($tags) as $service_id) {
+ $override = $container->get($service_id);
+ if ($override instanceof ConfigFactoryOverrideInterface) {
+ $service_overrides = $override->loadOverrides([$config_key]);
+ if (!empty($service_overrides[$config_key])) {
+ // Existing overrides take precedence since these will have been
+ // added by events with a higher priority.
+ $arrays = [$service_overrides[$config_key], $overrides];
+ $overrides = NestedArray::mergeDeepArray($arrays, TRUE);
+ }
+ }
+ }
+ }
+
+ // Overrides from settings.php. (This takes precedence over overrides from
+ // services.)
+ if (isset($GLOBALS['config'][$config_key])) {
+ $arrays = [$overrides, $GLOBALS['config'][$config_key]];
+ $overrides = NestedArray::mergeDeepArray($arrays, TRUE);
+ }
+
+ return $overrides;
+ }
+
+ /**
+ * Determines whether this PHP process is running on the command line.
+ *
+ * @return bool
+ * TRUE if this PHP process is running via CLI, FALSE otherwise.
+ */
+ public static function isRunningInCli() {
+ return php_sapi_name() === 'cli';
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/search_api_test/config/schema/search_api_test.schema.yml b/sites/all/modules/contrib/search/search_api/tests/search_api_test/config/schema/search_api_test.schema.yml
new file mode 100644
index 000000000..56c1c9187
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/search_api_test/config/schema/search_api_test.schema.yml
@@ -0,0 +1,35 @@
+plugin.plugin_configuration.search_api_backend.search_api_test:
+ type: mapping
+ label: Test backend
+ mapping:
+ test:
+ type: string
+ label: Test configuration
+ dependencies:
+ type: config_dependencies
+ label: Dependencies
+
+plugin.plugin_configuration.search_api_datasource.search_api_test:
+ type: config_dependencies
+
+plugin.plugin_configuration.search_api_processor.search_api_test:
+ type: config_dependencies
+ label: Test processor
+ 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'
+
+plugin.plugin_configuration.search_api_tracker.search_api_test:
+ type: mapping
+ label: Test tracker
+ mapping:
+ foo:
+ type: string
+ label: Foo
+ dependencies:
+ type: config_dependencies
+ label: Dependencies
diff --git a/sites/all/modules/contrib/search/search_api/tests/search_api_test/search_api_test.info.yml b/sites/all/modules/contrib/search/search_api/tests/search_api_test/search_api_test.info.yml
new file mode 100644
index 000000000..d8242e41c
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/search_api_test/search_api_test.info.yml
@@ -0,0 +1,14 @@
+name: 'Search API Test'
+type: module
+description: 'Support module for Search API tests'
+package: Search
+dependencies:
+ - search_api:search_api
+# 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
diff --git a/sites/all/modules/contrib/search/search_api/tests/search_api_test/search_api_test.module b/sites/all/modules/contrib/search/search_api/tests/search_api_test/search_api_test.module
new file mode 100644
index 000000000..74c5dc2b2
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/search_api_test/search_api_test.module
@@ -0,0 +1,33 @@
+id()];
+ return $grants;
+}
+
+/**
+ * Implement hook_node_access_records().
+ */
+function search_api_test_node_access_records(NodeInterface $node) {
+ $grants[] = [
+ 'realm' => 'search_api_test',
+ 'gid' => $node->getOwnerId(),
+ 'grant_view' => 1,
+ 'grant_update' => 0,
+ 'grant_delete' => 0,
+ 'langcode' => $node->language()->getId(),
+ ];
+
+ return $grants;
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/search_api_test/src/MethodOverrides.php b/sites/all/modules/contrib/search/search_api/tests/search_api_test/src/MethodOverrides.php
new file mode 100644
index 000000000..c2eac6ef1
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/search_api_test/src/MethodOverrides.php
@@ -0,0 +1,50 @@
+getConfiguration() !== ['test' => 'foobar']) {
+ trigger_error('Critical server method called with incorrect backend configuration.', E_USER_ERROR);
+ }
+ return TRUE;
+ }
+
+ /**
+ * Provides an override for the test backend's indexItems() method.
+ *
+ * @param \Drupal\search_api\Backend\BackendInterface $backend
+ * The backend plugin on which the method was called.
+ * @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 array keys of $items.
+ */
+ public static function overrideTestBackendIndexItems(BackendInterface $backend, IndexInterface $index, array $items) {
+ if ($backend->getConfiguration() !== ['test' => 'foobar']) {
+ trigger_error('Server method indexItems() called with incorrect backend configuration.', E_USER_ERROR);
+ }
+ return array_keys($items);
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/search_api_test/src/Plugin/search_api/backend/TestBackend.php b/sites/all/modules/contrib/search/search_api/tests/search_api_test/src/Plugin/search_api/backend/TestBackend.php
new file mode 100644
index 000000000..934623eb2
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/search_api_test/src/Plugin/search_api/backend/TestBackend.php
@@ -0,0 +1,321 @@
+getMethodOverride(__FUNCTION__)) {
+ call_user_func($override, $this);
+ return;
+ }
+ $this->checkError(__FUNCTION__);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function preUpdate() {
+ if ($override = $this->getMethodOverride(__FUNCTION__)) {
+ call_user_func($override, $this);
+ return;
+ }
+ $this->checkError(__FUNCTION__);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function postUpdate() {
+ if ($override = $this->getMethodOverride(__FUNCTION__)) {
+ return call_user_func($override, $this);
+ }
+ $this->checkError(__FUNCTION__);
+ return $this->getReturnValue(__FUNCTION__, FALSE);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function viewSettings() {
+ return [
+ [
+ 'label' => 'Dummy Info',
+ 'info' => 'Dummy Value',
+ 'status' => 'error',
+ ],
+ [
+ 'label' => 'Dummy Info 2',
+ 'info' => 'Dummy Value 2',
+ ],
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getSupportedFeatures() {
+ if ($override = $this->getMethodOverride(__FUNCTION__)) {
+ return call_user_func($override, $this);
+ }
+ return ['search_api_mlt'];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function supportsDataType($type) {
+ if ($override = $this->getMethodOverride(__FUNCTION__)) {
+ return call_user_func($override, $this, $type);
+ }
+ return in_array($type, ['search_api_test', 'search_api_test_altering']);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function defaultConfiguration() {
+ return ['test' => ''];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+ $form['test'] = [
+ '#type' => 'textfield',
+ '#title' => 'Test',
+ '#default_value' => $this->configuration['test'],
+ ];
+ return $form;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function indexItems(IndexInterface $index, array $items) {
+ if ($override = $this->getMethodOverride(__FUNCTION__)) {
+ return call_user_func($override, $this, $index, $items);
+ }
+ $this->checkError(__FUNCTION__);
+
+ $state = \Drupal::state();
+ $key = 'search_api_test.backend.indexed.' . $index->id();
+ $indexed_values = $state->get($key, []);
+ $skip = $state->get('search_api_test.backend.indexItems.skip', []);
+ $skip = array_flip($skip);
+ /** @var \Drupal\search_api\Item\ItemInterface $item */
+ foreach ($items as $id => $item) {
+ if (isset($skip[$id])) {
+ unset($items[$id]);
+ continue;
+ }
+ $indexed_values[$id] = [];
+ foreach ($item->getFields() as $field_id => $field) {
+ $indexed_values[$id][$field_id] = $field->getValues();
+ }
+ }
+ $state->set($key, $indexed_values);
+
+ return array_keys($items);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addIndex(IndexInterface $index) {
+ if ($override = $this->getMethodOverride(__FUNCTION__)) {
+ call_user_func($override, $this, $index);
+ return;
+ }
+ $this->checkError(__FUNCTION__);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function updateIndex(IndexInterface $index) {
+ if ($override = $this->getMethodOverride(__FUNCTION__)) {
+ call_user_func($override, $this, $index);
+ return;
+ }
+ $this->checkError(__FUNCTION__);
+ $index->reindex();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function removeIndex($index) {
+ if ($override = $this->getMethodOverride(__FUNCTION__)) {
+ call_user_func($override, $this, $index);
+ return;
+ }
+ $this->checkError(__FUNCTION__);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function deleteItems(IndexInterface $index, array $item_ids) {
+ if ($override = $this->getMethodOverride(__FUNCTION__)) {
+ call_user_func($override, $this, $index, $item_ids);
+ return;
+ }
+ $this->checkError(__FUNCTION__);
+
+ $state = \Drupal::state();
+ $key = 'search_api_test.backend.indexed.' . $index->id();
+ $indexed_values = $state->get($key, []);
+ /** @var \Drupal\search_api\Item\ItemInterface $item */
+ foreach ($item_ids as $item_id) {
+ unset($indexed_values[$item_id]);
+ }
+ $state->set($key, $indexed_values);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function deleteAllIndexItems(IndexInterface $index, $datasource_id = NULL) {
+ if ($override = $this->getMethodOverride(__FUNCTION__)) {
+ call_user_func($override, $this, $index, $datasource_id);
+ return;
+ }
+ $this->checkError(__FUNCTION__);
+
+ $key = 'search_api_test.backend.indexed.' . $index->id();
+ if (!$datasource_id) {
+ \Drupal::state()->delete($key);
+ return;
+ }
+
+ $indexed = \Drupal::state()->get($key, []);
+ /** @var \Drupal\search_api\Item\ItemInterface $item */
+ foreach (array_keys($indexed) as $item_id) {
+ list($item_datasource_id) = Utility::splitCombinedId($item_id);
+ if ($item_datasource_id == $datasource_id) {
+ unset($indexed[$item_id]);
+ }
+ }
+ \Drupal::state()->set($key, $indexed);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function search(QueryInterface $query) {
+ if ($override = $this->getMethodOverride(__FUNCTION__)) {
+ call_user_func($override, $this, $query);
+ return;
+ }
+ $this->checkError(__FUNCTION__);
+
+ $results = $query->getResults();
+ $result_items = [];
+ $datasources = $query->getIndex()->getDatasources();
+ /** @var \Drupal\search_api\Datasource\DatasourceInterface $datasource */
+ $datasource = reset($datasources);
+ $datasource_id = $datasource->getPluginId();
+ $fields_helper = \Drupal::getContainer()->get('search_api.fields_helper');
+ if ($query->getKeys() && $query->getKeys()[0] == 'test') {
+ $item_id = Utility::createCombinedId($datasource_id, '1');
+ $item = $fields_helper->createItem($query->getIndex(), $item_id, $datasource);
+ $item->setScore(2);
+ $item->setExcerpt('test');
+ $result_items[$item_id] = $item;
+ }
+ elseif ($query->getOption('search_api_mlt')) {
+ $item_id = Utility::createCombinedId($datasource_id, '2');
+ $item = $fields_helper->createItem($query->getIndex(), $item_id, $datasource);
+ $item->setScore(2);
+ $item->setExcerpt('test test');
+ $result_items[$item_id] = $item;
+ }
+ else {
+ $item_id = Utility::createCombinedId($datasource_id, '1');
+ $item = $fields_helper->createItem($query->getIndex(), $item_id, $datasource);
+ $item->setScore(1);
+ $result_items[$item_id] = $item;
+ $item_id = Utility::createCombinedId($datasource_id, '2');
+ $item = $fields_helper->createItem($query->getIndex(), $item_id, $datasource);
+ $item->setScore(1);
+ $result_items[$item_id] = $item;
+ }
+ $results->setResultItems($result_items);
+ $results->setResultCount(count($result_items));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isAvailable() {
+ if ($override = $this->getMethodOverride(__FUNCTION__)) {
+ return call_user_func($override, $this);
+ }
+ return $this->getReturnValue(__FUNCTION__, TRUE);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getDiscouragedProcessors() {
+ if ($override = $this->getMethodOverride(__FUNCTION__)) {
+ return (array) call_user_func($override, $this);
+ }
+ return $this->getReturnValue(__FUNCTION__, []);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function calculateDependencies() {
+ return !empty($this->configuration['dependencies']) ? $this->configuration['dependencies'] : [];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function onDependencyRemoval(array $dependencies) {
+ $remove = $this->getReturnValue(__FUNCTION__, FALSE);
+ if ($remove) {
+ unset($this->configuration['dependencies']);
+ }
+ return $remove;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function checkError($method) {
+ $this->traitCheckError($method);
+ $this->logMethodCall($method);
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/search_api_test/src/Plugin/search_api/data_type/AlteringValueTestDataType.php b/sites/all/modules/contrib/search/search_api/tests/search_api_test/src/Plugin/search_api/data_type/AlteringValueTestDataType.php
new file mode 100644
index 000000000..0b2319484
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/search_api_test/src/Plugin/search_api/data_type/AlteringValueTestDataType.php
@@ -0,0 +1,33 @@
+get('search_api_test.data_type.dependencies', []);
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/search_api_test/src/Plugin/search_api/data_type/TestDataType.php b/sites/all/modules/contrib/search/search_api/tests/search_api_test/src/Plugin/search_api/data_type/TestDataType.php
new file mode 100644
index 000000000..9d3cf2e49
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/search_api_test/src/Plugin/search_api/data_type/TestDataType.php
@@ -0,0 +1,18 @@
+getReturnValue(__FUNCTION__, []);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function calculateDependencies() {
+ return $this->configuration;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function onDependencyRemoval(array $dependencies) {
+ $remove = $this->getReturnValue(__FUNCTION__, FALSE);
+ if ($remove) {
+ $this->configuration = [];
+ }
+ return $remove;
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/search_api_test/src/Plugin/search_api/display/TestDisplay.php b/sites/all/modules/contrib/search/search_api/tests/search_api_test/src/Plugin/search_api/display/TestDisplay.php
new file mode 100644
index 000000000..a2caf6e87
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/search_api_test/src/Plugin/search_api/display/TestDisplay.php
@@ -0,0 +1,18 @@
+get($key)) {
+ return call_user_func($method, $index);
+ }
+
+ return TRUE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function supportsStage($stage_identifier) {
+ return TRUE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function preIndexSave() {
+ $this->logMethodCall(__FUNCTION__, func_get_args());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function alterIndexedItems(array &$items) {
+ $this->logMethodCall(__FUNCTION__, func_get_args());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function preprocessIndexItems(array $items) {
+ $this->logMethodCall(__FUNCTION__, func_get_args());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function preprocessSearchQuery(QueryInterface $query) {
+ $this->logMethodCall(__FUNCTION__, func_get_args());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function postprocessSearchResults(ResultSetInterface $results) {
+ $this->logMethodCall(__FUNCTION__, func_get_args());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function calculateDependencies() {
+ $dependencies = $this->configuration;
+ unset($dependencies['weights']);
+ return $dependencies;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function onDependencyRemoval(array $dependencies) {
+ $remove = $this->getReturnValue(__FUNCTION__, FALSE);
+ if ($remove) {
+ $this->configuration = [];
+ }
+ return $remove;
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/search_api_test/src/Plugin/search_api/tracker/TestTracker.php b/sites/all/modules/contrib/search/search_api/tests/search_api_test/src/Plugin/search_api/tracker/TestTracker.php
new file mode 100644
index 000000000..be40118e7
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/search_api_test/src/Plugin/search_api/tracker/TestTracker.php
@@ -0,0 +1,140 @@
+ 'test',
+ 'dependencies' => [],
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+ return [
+ 'foo' => [
+ '#type' => 'textfield',
+ '#title' => 'Foo',
+ '#default_value' => $this->configuration['foo'],
+ ],
+ ];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function trackItemsInserted(array $ids) {
+ $this->logMethodCall(__FUNCTION__, func_get_args());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function trackItemsUpdated(array $ids) {
+ $this->logMethodCall(__FUNCTION__, func_get_args());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function trackAllItemsUpdated($datasource_id = NULL) {
+ $this->logMethodCall(__FUNCTION__, func_get_args());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function trackItemsIndexed(array $ids) {
+ $this->logMethodCall(__FUNCTION__, func_get_args());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function trackItemsDeleted(array $ids = NULL) {
+ $this->logMethodCall(__FUNCTION__, func_get_args());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function trackAllItemsDeleted($datasource_id = NULL) {
+ $this->logMethodCall(__FUNCTION__, func_get_args());
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getRemainingItems($limit = -1, $datasource_id = NULL) {
+ $this->logMethodCall(__FUNCTION__, func_get_args());
+ return [];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getTotalItemsCount($datasource_id = NULL) {
+ $this->logMethodCall(__FUNCTION__, func_get_args());
+ return 0;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getIndexedItemsCount($datasource_id = NULL) {
+ $this->logMethodCall(__FUNCTION__, func_get_args());
+ return 0;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getRemainingItemsCount($datasource_id = NULL) {
+ $this->logMethodCall(__FUNCTION__, func_get_args());
+ return 0;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function calculateDependencies() {
+ return $this->configuration['dependencies'];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function onDependencyRemoval(array $dependencies) {
+ $remove = $this->getReturnValue(__FUNCTION__, FALSE);
+ if ($remove) {
+ $this->configuration['dependencies'] = [];
+ }
+ return $remove;
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/search_api_test/src/PluginTestTrait.php b/sites/all/modules/contrib/search/search_api/tests/search_api_test/src/PluginTestTrait.php
new file mode 100644
index 000000000..2c06dbe41
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/search_api_test/src/PluginTestTrait.php
@@ -0,0 +1,94 @@
+set($key, $error);
+ }
+
+ /**
+ * Sets the return value for a certain method on a test plugin.
+ *
+ * @param string $plugin_type
+ * The "short" plugin type.
+ * @param string $method
+ * The method name.
+ * @param mixed $value
+ * The value that should be returned.
+ */
+ protected function setReturnValue($plugin_type, $method, $value) {
+ $key = "search_api_test.$plugin_type.return.$method";
+ \Drupal::state()->set($key, $value);
+ }
+
+ /**
+ * Overrides a method for a certain plugin.
+ *
+ * @param string $plugin_type
+ * The "short" plugin type.
+ * @param string $method
+ * The name of the method to override.
+ * @param callable|null $override
+ * The new code of the method, or NULL to use the default.
+ */
+ protected function setMethodOverride($plugin_type, $method, callable $override = NULL) {
+ $key = "search_api_test.$plugin_type.method.$method";
+ \Drupal::state()->set($key, $override);
+ }
+
+ /**
+ * Retrieves the methods called on a given plugin.
+ *
+ * @param string $plugin_type
+ * The "short" plugin type.
+ * @param bool $reset
+ * (optional) If TRUE, also clear the list of called methods for that type.
+ *
+ * @return string[]
+ * The methods called on the given plugin.
+ */
+ protected function getCalledMethods($plugin_type, $reset = TRUE) {
+ $key = "search_api_test.$plugin_type.methods_called";
+ $methods = \Drupal::state()->get($key, []);
+ if ($reset) {
+ \Drupal::state()->delete($key);
+ }
+ return $methods;
+ }
+
+ /**
+ * Retrieves the arguments of a certain method called on the given plugin.
+ *
+ * @param string $plugin_type
+ * The "short" plugin type.
+ * @param string $method
+ * The method name.
+ *
+ * @return array
+ * The arguments of the last call to the method.
+ */
+ protected function getMethodArguments($plugin_type, $method) {
+ $key = "search_api_test.$plugin_type.method_arguments.$method";
+ return \Drupal::state()->get($key);
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/search_api_test/src/TestPluginTrait.php b/sites/all/modules/contrib/search/search_api/tests/search_api_test/src/TestPluginTrait.php
new file mode 100644
index 000000000..1bce7a176
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/search_api_test/src/TestPluginTrait.php
@@ -0,0 +1,133 @@
+getPluginType();
+ $state = \Drupal::state();
+ if ($state->get("search_api_test.$type.exception.$method")) {
+ throw new SearchApiException($method);
+ }
+ }
+
+ /**
+ * Logs a method call to the site state.
+ *
+ * @param string $method
+ * The name of the called method.
+ * @param array $args
+ * (optional) The method arguments.
+ */
+ protected function logMethodCall($method, array $args = []) {
+ $type = $this->getPluginType();
+ $state = \Drupal::state();
+
+ // Log call.
+ $key = "search_api_test.$type.methods_called";
+ $methods_called = $state->get($key, []);
+ $methods_called[] = $method;
+ $state->set($key, $methods_called);
+
+ // Log arguments.
+ $key = "search_api_test.$type.method_arguments.$method";
+ $state->set($key, $args);
+ }
+
+ /**
+ * Retrieves the value to return for a certain method.
+ *
+ * @param string $method
+ * The name of the called method.
+ * @param mixed $default
+ * (optional) The default return value.
+ *
+ * @return mixed
+ * The value to return from the method.
+ */
+ protected function getReturnValue($method, $default = NULL) {
+ $type = $this->getPluginType();
+ $key = "search_api_test.$type.return.$method";
+ return \Drupal::state()->get($key, $default);
+ }
+
+ /**
+ * Retrieves a possible override set for the given method.
+ *
+ * @param string $method
+ * The name of the method.
+ *
+ * @return callable|null
+ * The method override to use, or NULL if none was set.
+ */
+ protected function getMethodOverride($method) {
+ $type = $this->getPluginType();
+ $key = "search_api_test.$type.method.$method";
+ return \Drupal::state()->get($key);
+ }
+
+ /**
+ * Returns the plugin type of this object.
+ *
+ * Equivalent to the last component of the namespace.
+ *
+ * @return string
+ * The "short" plugin type.
+ */
+ protected function getPluginType() {
+ if (!isset($this->pluginType)) {
+ $class = explode("\\", get_class($this));
+ array_pop($class);
+ $this->pluginType = array_pop($class);
+ }
+
+ return $this->pluginType;
+ }
+
+ /**
+ * Implements the magic __call() method.
+ *
+ * Allows the easy definition of additional methods via method overrides.
+ *
+ * @param string $name
+ * The method name.
+ * @param array $arguments
+ * The arguments of the method call.
+ *
+ * @return mixed
+ * The method's return value, if any.
+ *
+ * @see \Drupal\search_api_test\TestPluginTrait::getMethodOverride()
+ */
+ public function __call($name, array $arguments) {
+ if ($override = $this->getMethodOverride($name)) {
+ array_unshift($arguments, $this);
+ return call_user_func_array($override, $arguments);
+ }
+ $class = static::class;
+ throw new \BadMethodCallException("Method $class::$name() doesn't exist.");
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/search_api_test_db/config/install/search_api.index.database_search_index.yml b/sites/all/modules/contrib/search/search_api/tests/search_api_test_db/config/install/search_api.index.database_search_index.yml
new file mode 100644
index 000000000..bd219dac5
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/search_api_test_db/config/install/search_api.index.database_search_index.yml
@@ -0,0 +1,84 @@
+id: database_search_index
+name: 'Test index'
+description: 'An index used for testing'
+read_only: false
+field_settings:
+ id:
+ label: ID
+ type: integer
+ datasource_id: 'entity:entity_test_mulrev_changed'
+ property_path: id
+ name:
+ label: Name
+ type: text
+ datasource_id: 'entity:entity_test_mulrev_changed'
+ property_path: name
+ boost: 5.0
+ created:
+ label: Authored on
+ type: date
+ datasource_id: 'entity:entity_test_mulrev_changed'
+ property_path: created
+ body:
+ label: Body
+ type: text
+ datasource_id: 'entity:entity_test_mulrev_changed'
+ property_path: body
+ type:
+ label: Type
+ type: string
+ datasource_id: 'entity:entity_test_mulrev_changed'
+ property_path: type
+ keywords:
+ label: Keywords
+ type: string
+ datasource_id: 'entity:entity_test_mulrev_changed'
+ property_path: keywords
+ category:
+ label: Category
+ type: string
+ datasource_id: 'entity:entity_test_mulrev_changed'
+ property_path: category
+ width:
+ label: Width
+ type: decimal
+ datasource_id: 'entity:entity_test_mulrev_changed'
+ property_path: width
+processor_settings:
+ add_url:
+ weights:
+ preprocess_index: -30
+ aggregated_field:
+ weights:
+ add_properties: 20
+ rendered_item:
+ weights:
+ add_properties: 0
+ pre_index_save: -10
+options:
+ cron_limit: -1
+ index_directly: false
+datasource_settings:
+ 'entity:entity_test_mulrev_changed': { }
+tracker_settings:
+ 'default': { }
+server: database_search_server
+status: true
+langcode: en
+dependencies:
+ config:
+ - field.field.entity_test_mulrev_changed.article.body
+ - field.field.entity_test_mulrev_changed.article.keywords
+ - field.field.entity_test_mulrev_changed.article.category
+ - field.field.entity_test_mulrev_changed.article.width
+ - field.field.entity_test_mulrev_changed.item.body
+ - field.field.entity_test_mulrev_changed.item.keywords
+ - field.field.entity_test_mulrev_changed.item.category
+ - field.field.entity_test_mulrev_changed.item.width
+ - field.storage.entity_test_mulrev_changed.body
+ - field.storage.entity_test_mulrev_changed.keywords
+ - field.storage.entity_test_mulrev_changed.category
+ - field.storage.entity_test_mulrev_changed.width
+ - search_api.server.database_search_server
+ module:
+ - entity_test
diff --git a/sites/all/modules/contrib/search/search_api/tests/search_api_test_db/config/install/search_api.server.database_search_server.yml b/sites/all/modules/contrib/search/search_api/tests/search_api_test_db/config/install/search_api.server.database_search_server.yml
new file mode 100644
index 000000000..41bb0ae34
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/search_api_test_db/config/install/search_api.server.database_search_server.yml
@@ -0,0 +1,13 @@
+id: database_search_server
+name: 'Database search server'
+description: 'A server used for testing'
+backend: search_api_db
+backend_config:
+ database: 'default:default'
+ min_chars: 3
+ partial_matches: false
+status: true
+langcode: en
+dependencies:
+ module:
+ - search_api_db
diff --git a/sites/all/modules/contrib/search/search_api/tests/search_api_test_db/search_api_test_db.info.yml b/sites/all/modules/contrib/search/search_api/tests/search_api_test_db/search_api_test_db.info.yml
new file mode 100644
index 000000000..6cf28e880
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/search_api_test_db/search_api_test_db.info.yml
@@ -0,0 +1,15 @@
+type: module
+name: 'Database search test'
+description: 'Provides a setup for testing Database Search.'
+package: 'Search API'
+dependencies:
+ - search_api:search_api_db
+ - search_api:search_api_test_example_content
+# 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
diff --git a/sites/all/modules/contrib/search/search_api/tests/search_api_test_db/search_api_test_db.search_api.inc b/sites/all/modules/contrib/search/search_api/tests/search_api_test_db/search_api_test_db.search_api.inc
new file mode 100644
index 000000000..7854ff440
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/search_api_test_db/search_api_test_db.search_api.inc
@@ -0,0 +1,18 @@
+getOption('search_api_test_db_search_api_db_query_alter')) {
+ $db_query->where('0 = 1');
+ }
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.article.body.yml b/sites/all/modules/contrib/search/search_api/tests/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.article.body.yml
new file mode 100644
index 000000000..595a130e6
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.article.body.yml
@@ -0,0 +1,17 @@
+id: entity_test_mulrev_changed.article.body
+status: true
+langcode: en
+field_name: body
+entity_type: entity_test_mulrev_changed
+bundle: article
+label: Body
+description: ''
+required: false
+default_value: { }
+default_value_callback: ''
+settings:
+ display_summary: true
+dependencies:
+ config:
+ - field.storage.entity_test_mulrev_changed.body
+field_type: text_with_summary
diff --git a/sites/all/modules/contrib/search/search_api/tests/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.article.category.yml b/sites/all/modules/contrib/search/search_api/tests/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.article.category.yml
new file mode 100644
index 000000000..7b649f7a9
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.article.category.yml
@@ -0,0 +1,16 @@
+id: entity_test_mulrev_changed.article.category
+status: true
+langcode: en
+field_name: category
+entity_type: entity_test_mulrev_changed
+bundle: article
+label: Category
+description: ''
+required: false
+default_value: { }
+default_value_callback: ''
+settings: { }
+dependencies:
+ config:
+ - field.storage.entity_test_mulrev_changed.category
+field_type: string
diff --git a/sites/all/modules/contrib/search/search_api/tests/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.article.keywords.yml b/sites/all/modules/contrib/search/search_api/tests/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.article.keywords.yml
new file mode 100644
index 000000000..f11dc75c0
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.article.keywords.yml
@@ -0,0 +1,16 @@
+id: entity_test_mulrev_changed.article.keywords
+status: true
+langcode: en
+field_name: keywords
+entity_type: entity_test_mulrev_changed
+bundle: article
+label: Keywords
+description: ''
+required: false
+default_value: { }
+default_value_callback: ''
+settings: { }
+dependencies:
+ config:
+ - field.storage.entity_test_mulrev_changed.keywords
+field_type: string
diff --git a/sites/all/modules/contrib/search/search_api/tests/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.article.width.yml b/sites/all/modules/contrib/search/search_api/tests/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.article.width.yml
new file mode 100644
index 000000000..4453937f2
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.article.width.yml
@@ -0,0 +1,20 @@
+id: entity_test_mulrev_changed.article.width
+status: true
+langcode: en
+field_name: width
+entity_type: entity_test_mulrev_changed
+bundle: article
+label: Width
+description: ''
+required: false
+default_value: { }
+default_value_callback: ''
+settings:
+ min: !!float 0
+ max: null
+ prefix: ''
+ suffix: ''
+dependencies:
+ config:
+ - field.storage.entity_test_mulrev_changed.width
+field_type: float
diff --git a/sites/all/modules/contrib/search/search_api/tests/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.entity_test_mulrev_changed.body.yml b/sites/all/modules/contrib/search/search_api/tests/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.entity_test_mulrev_changed.body.yml
new file mode 100644
index 000000000..c646924a4
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.entity_test_mulrev_changed.body.yml
@@ -0,0 +1,17 @@
+id: entity_test_mulrev_changed.article.body
+status: true
+langcode: en
+field_name: body
+entity_type: entity_test_mulrev_changed
+bundle: entity_test_mulrev_changed
+label: Body
+description: ''
+required: false
+default_value: { }
+default_value_callback: ''
+settings:
+ display_summary: true
+dependencies:
+ config:
+ - field.storage.entity_test_mulrev_changed.body
+field_type: text_with_summary
diff --git a/sites/all/modules/contrib/search/search_api/tests/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.entity_test_mulrev_changed.category.yml b/sites/all/modules/contrib/search/search_api/tests/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.entity_test_mulrev_changed.category.yml
new file mode 100644
index 000000000..328cb3d00
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.entity_test_mulrev_changed.category.yml
@@ -0,0 +1,16 @@
+id: entity_test_mulrev_changed.article.category
+status: true
+langcode: en
+field_name: category
+entity_type: entity_test_mulrev_changed
+bundle: entity_test_mulrev_changed
+label: Category
+description: ''
+required: false
+default_value: { }
+default_value_callback: ''
+settings: { }
+dependencies:
+ config:
+ - field.storage.entity_test_mulrev_changed.category
+field_type: string
diff --git a/sites/all/modules/contrib/search/search_api/tests/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.entity_test_mulrev_changed.keywords.yml b/sites/all/modules/contrib/search/search_api/tests/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.entity_test_mulrev_changed.keywords.yml
new file mode 100644
index 000000000..e13fe772d
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.entity_test_mulrev_changed.keywords.yml
@@ -0,0 +1,16 @@
+id: entity_test_mulrev_changed.article.keywords
+status: true
+langcode: en
+field_name: keywords
+entity_type: entity_test_mulrev_changed
+bundle: entity_test_mulrev_changed
+label: Keywords
+description: ''
+required: false
+default_value: { }
+default_value_callback: ''
+settings: { }
+dependencies:
+ config:
+ - field.storage.entity_test_mulrev_changed.keywords
+field_type: string
diff --git a/sites/all/modules/contrib/search/search_api/tests/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.entity_test_mulrev_changed.width.yml b/sites/all/modules/contrib/search/search_api/tests/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.entity_test_mulrev_changed.width.yml
new file mode 100644
index 000000000..afdffdc50
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.entity_test_mulrev_changed.width.yml
@@ -0,0 +1,20 @@
+id: entity_test_mulrev_changed.article.width
+status: true
+langcode: en
+field_name: width
+entity_type: entity_test_mulrev_changed
+bundle: entity_test_mulrev_changed
+label: Width
+description: ''
+required: false
+default_value: { }
+default_value_callback: ''
+settings:
+ min: !!float 0
+ max: null
+ prefix: ''
+ suffix: ''
+dependencies:
+ config:
+ - field.storage.entity_test_mulrev_changed.width
+field_type: float
diff --git a/sites/all/modules/contrib/search/search_api/tests/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.item.body.yml b/sites/all/modules/contrib/search/search_api/tests/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.item.body.yml
new file mode 100644
index 000000000..6b5fc9d0e
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.item.body.yml
@@ -0,0 +1,17 @@
+id: entity_test_mulrev_changed.item.body
+status: true
+langcode: en
+field_name: body
+entity_type: entity_test_mulrev_changed
+bundle: item
+label: Body
+description: ''
+required: false
+default_value: { }
+default_value_callback: ''
+settings:
+ display_summary: true
+dependencies:
+ config:
+ - field.storage.entity_test_mulrev_changed.body
+field_type: text_with_summary
diff --git a/sites/all/modules/contrib/search/search_api/tests/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.item.category.yml b/sites/all/modules/contrib/search/search_api/tests/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.item.category.yml
new file mode 100644
index 000000000..6322278d5
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.item.category.yml
@@ -0,0 +1,16 @@
+id: entity_test_mulrev_changed.item.category
+status: true
+langcode: en
+field_name: category
+entity_type: entity_test_mulrev_changed
+bundle: item
+label: Category
+description: ''
+required: false
+default_value: { }
+default_value_callback: ''
+settings: { }
+dependencies:
+ config:
+ - field.storage.entity_test_mulrev_changed.category
+field_type: string
diff --git a/sites/all/modules/contrib/search/search_api/tests/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.item.keywords.yml b/sites/all/modules/contrib/search/search_api/tests/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.item.keywords.yml
new file mode 100644
index 000000000..ee70031cb
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.item.keywords.yml
@@ -0,0 +1,16 @@
+id: entity_test_mulrev_changed.item.keywords
+status: true
+langcode: en
+field_name: keywords
+entity_type: entity_test_mulrev_changed
+bundle: item
+label: Keywords
+description: ''
+required: false
+default_value: { }
+default_value_callback: ''
+settings: { }
+dependencies:
+ config:
+ - field.storage.entity_test_mulrev_changed.keywords
+field_type: string
diff --git a/sites/all/modules/contrib/search/search_api/tests/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.item.width.yml b/sites/all/modules/contrib/search/search_api/tests/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.item.width.yml
new file mode 100644
index 000000000..7b7de1175
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/search_api_test_example_content/config/install/field.field.entity_test_mulrev_changed.item.width.yml
@@ -0,0 +1,20 @@
+id: entity_test_mulrev_changed.item.width
+status: true
+langcode: en
+field_name: width
+entity_type: entity_test_mulrev_changed
+bundle: item
+label: Width
+description: ''
+required: false
+default_value: { }
+default_value_callback: ''
+settings:
+ min: !!float 0
+ max: null
+ prefix: ''
+ suffix: ''
+dependencies:
+ config:
+ - field.storage.entity_test_mulrev_changed.width
+field_type: float
diff --git a/sites/all/modules/contrib/search/search_api/tests/search_api_test_example_content/config/install/field.storage.entity_test_mulrev_changed.body.yml b/sites/all/modules/contrib/search/search_api/tests/search_api_test_example_content/config/install/field.storage.entity_test_mulrev_changed.body.yml
new file mode 100644
index 000000000..80b2ed41c
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/search_api_test_example_content/config/install/field.storage.entity_test_mulrev_changed.body.yml
@@ -0,0 +1,16 @@
+id: entity_test_mulrev_changed.body
+status: true
+langcode: en
+field_name: body
+entity_type: entity_test_mulrev_changed
+type: text_with_summary
+settings: { }
+module: text
+locked: false
+cardinality: 1
+translatable: true
+indexes: { }
+dependencies:
+ module:
+ - entity_test
+ - text
diff --git a/sites/all/modules/contrib/search/search_api/tests/search_api_test_example_content/config/install/field.storage.entity_test_mulrev_changed.category.yml b/sites/all/modules/contrib/search/search_api/tests/search_api_test_example_content/config/install/field.storage.entity_test_mulrev_changed.category.yml
new file mode 100644
index 000000000..aaf1b1319
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/search_api_test_example_content/config/install/field.storage.entity_test_mulrev_changed.category.yml
@@ -0,0 +1,16 @@
+id: entity_test_mulrev_changed.category
+status: true
+langcode: en
+field_name: category
+entity_type: entity_test_mulrev_changed
+type: string
+settings:
+ max_length: 255
+module: Core
+locked: false
+cardinality: 1
+translatable: true
+indexes: { }
+dependencies:
+ module:
+ - entity_test
diff --git a/sites/all/modules/contrib/search/search_api/tests/search_api_test_example_content/config/install/field.storage.entity_test_mulrev_changed.keywords.yml b/sites/all/modules/contrib/search/search_api/tests/search_api_test_example_content/config/install/field.storage.entity_test_mulrev_changed.keywords.yml
new file mode 100644
index 000000000..0354534f1
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/search_api_test_example_content/config/install/field.storage.entity_test_mulrev_changed.keywords.yml
@@ -0,0 +1,16 @@
+id: entity_test_mulrev_changed.keywords
+status: true
+langcode: en
+field_name: keywords
+entity_type: entity_test_mulrev_changed
+type: string
+settings:
+ max_length: 255
+module: Core
+locked: false
+cardinality: -1
+translatable: true
+indexes: { }
+dependencies:
+ module:
+ - entity_test
diff --git a/sites/all/modules/contrib/search/search_api/tests/search_api_test_example_content/config/install/field.storage.entity_test_mulrev_changed.width.yml b/sites/all/modules/contrib/search/search_api/tests/search_api_test_example_content/config/install/field.storage.entity_test_mulrev_changed.width.yml
new file mode 100644
index 000000000..18f0b7aab
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/search_api_test_example_content/config/install/field.storage.entity_test_mulrev_changed.width.yml
@@ -0,0 +1,15 @@
+id: entity_test_mulrev_changed.width
+status: true
+langcode: en
+field_name: width
+entity_type: entity_test_mulrev_changed
+type: float
+settings: { }
+module: Core
+locked: false
+cardinality: 1
+translatable: false
+indexes: { }
+dependencies:
+ module:
+ - entity_test
diff --git a/sites/all/modules/contrib/search/search_api/tests/search_api_test_example_content/search_api_test_example_content.info.yml b/sites/all/modules/contrib/search/search_api/tests/search_api_test_example_content/search_api_test_example_content.info.yml
new file mode 100644
index 000000000..833357e35
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/search_api_test_example_content/search_api_test_example_content.info.yml
@@ -0,0 +1,14 @@
+type: module
+name: 'Example Content'
+description: 'Provides field definitions for example content.'
+package: 'Search API'
+dependencies:
+ - drupal:entity_test
+# 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
diff --git a/sites/all/modules/contrib/search/search_api/tests/search_api_test_extraction/config/install/field.field.entity_test.article.links.yml b/sites/all/modules/contrib/search/search_api/tests/search_api_test_extraction/config/install/field.field.entity_test.article.links.yml
new file mode 100644
index 000000000..3abf0decf
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/search_api_test_extraction/config/install/field.field.entity_test.article.links.yml
@@ -0,0 +1,24 @@
+langcode: en
+status: true
+dependencies:
+ config:
+ - field.storage.entity_test_mulrev_changed.links
+id: entity_test_mulrev_changed.article.links
+field_name: links
+entity_type: entity_test_mulrev_changed
+bundle: article
+label: Links
+description: 'Links to other test entities'
+required: false
+translatable: true
+default_value: { }
+default_value_callback: ''
+settings:
+ handler: 'default:entity_test_mulrev_changed'
+ handler_settings:
+ target_bundles:
+ article: article
+ sort:
+ field: _none
+ auto_create: true
+field_type: entity_reference
diff --git a/sites/all/modules/contrib/search/search_api/tests/search_api_test_extraction/config/install/field.storage.entity_test.links.yml b/sites/all/modules/contrib/search/search_api/tests/search_api_test_extraction/config/install/field.storage.entity_test.links.yml
new file mode 100644
index 000000000..f3e5368d3
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/search_api_test_extraction/config/install/field.storage.entity_test.links.yml
@@ -0,0 +1,18 @@
+langcode: en
+status: true
+dependencies:
+ module:
+ - entity_test_mulrev_changed
+id: entity_test_mulrev_changed.links
+field_name: links
+entity_type: entity_test_mulrev_changed
+type: entity_reference
+settings:
+ target_type: entity_test_mulrev_changed
+module: core
+locked: false
+cardinality: -1
+translatable: true
+indexes: { }
+persist_with_no_fields: false
+custom_storage: false
diff --git a/sites/all/modules/contrib/search/search_api/tests/search_api_test_extraction/search_api_test_extraction.info.yml b/sites/all/modules/contrib/search/search_api/tests/search_api_test_extraction/search_api_test_extraction.info.yml
new file mode 100644
index 000000000..645063f64
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/search_api_test_extraction/search_api_test_extraction.info.yml
@@ -0,0 +1,14 @@
+type: module
+name: 'Field values extraction search test'
+description: 'Provides a setup for testing field values extraction.'
+package: 'Search API'
+dependencies:
+ - drupal:entity_test
+# 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
diff --git a/sites/all/modules/contrib/search/search_api/tests/search_api_test_extraction/src/Plugin/search_api/processor/SoulMate.php b/sites/all/modules/contrib/search/search_api/tests/search_api_test_extraction/src/Plugin/search_api/processor/SoulMate.php
new file mode 100644
index 000000000..73d3b0adb
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/search_api_test_extraction/src/Plugin/search_api/processor/SoulMate.php
@@ -0,0 +1,180 @@
+setEntityTypeManager($container->get('entity_type.manager'));
+ $processor->setFieldsHelper($container->get('search_api.fields_helper'));
+
+ return $processor;
+ }
+
+ /**
+ * Retrieves the entity type manager.
+ *
+ * @return \Drupal\Core\Entity\EntityTypeManagerInterface
+ * The entity type manager.
+ */
+ public function getEntityTypeManager() {
+ return $this->entityTypeManager ?: \Drupal::entityTypeManager();
+ }
+
+ /**
+ * Sets the entity type manager.
+ *
+ * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+ * The new entity type manager.
+ *
+ * @return $this
+ */
+ public function setEntityTypeManager(EntityTypeManagerInterface $entity_type_manager) {
+ $this->entityTypeManager = $entity_type_manager;
+ return $this;
+ }
+
+ /**
+ * Retrieves the fields helper.
+ *
+ * @return \Drupal\search_api\Utility\FieldsHelperInterface
+ * The fields helper.
+ */
+ public function getFieldsHelper() {
+ return $this->fieldsHelper ?: \Drupal::service('search_api.fields_helper');
+ }
+
+ /**
+ * Sets the fields helper.
+ *
+ * @param \Drupal\search_api\Utility\FieldsHelperInterface $fields_helper
+ * The new fields helper.
+ *
+ * @return $this
+ */
+ public function setFieldsHelper(FieldsHelperInterface $fields_helper) {
+ $this->fieldsHelper = $fields_helper;
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function supportsIndex(IndexInterface $index) {
+ foreach ($index->getDatasources() as $datasource) {
+ $entity_type_id = $datasource->getEntityTypeId();
+ // The property only works for entities, and doesn't really make sense for
+ // users.
+ if ($entity_type_id && $entity_type_id !== 'user') {
+ return TRUE;
+ }
+ }
+ return FALSE;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getPropertyDefinitions(DatasourceInterface $datasource = NULL) {
+ $properties = [];
+
+ if ($datasource
+ && $datasource->getEntityTypeId()
+ && $datasource->getEntityTypeId() !== 'user') {
+ $definition = [
+ 'label' => $this->t('Soul mate'),
+ 'description' => $this->t("The User with the same UID as the entity's ID"),
+ 'type' => 'entity:user',
+ 'processor_id' => $this->getPluginId(),
+ ];
+ $properties['soul_mate'] = new EntityProcessorProperty($definition);
+ $properties['soul_mate']->setEntityTypeId('user');
+ }
+
+ return $properties;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addFieldValues(ItemInterface $item) {
+ $entity = $item->getOriginalObject()->getValue();
+
+ if (!($entity instanceof EntityInterface)
+ || $entity->getEntityTypeId() === 'user') {
+ return;
+ }
+
+ /** @var \Drupal\search_api\Item\FieldInterface[][] $to_extract */
+ $to_extract = [];
+ foreach ($item->getFields() as $field) {
+ if (!$field->getDatasourceId()) {
+ continue;
+ }
+ $property_path = $field->getPropertyPath();
+ list($direct, $nested) = Utility::splitPropertyPath($property_path, FALSE);
+ if ($direct === 'soul_mate') {
+ $to_extract[$nested][] = $field;
+ }
+ }
+
+ if (!$to_extract) {
+ return;
+ }
+
+ $user = $this->getEntityTypeManager()
+ ->getStorage('user')
+ ->load($entity->id());
+ if (!$user) {
+ return;
+ }
+ $this->getFieldsHelper()
+ ->extractFields($user->getTypedData(), $to_extract, $item->getLanguage());
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/search_api_test_hooks/search_api_test_hooks.info.yml b/sites/all/modules/contrib/search/search_api/tests/search_api_test_hooks/search_api_test_hooks.info.yml
new file mode 100644
index 000000000..758f4cce4
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/search_api_test_hooks/search_api_test_hooks.info.yml
@@ -0,0 +1,14 @@
+name: 'Search API Hooks Test'
+type: module
+description: 'Support module for Search API tests, tests all the hooks.'
+package: Testing
+dependencies:
+ - search_api:search_api
+# 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
diff --git a/sites/all/modules/contrib/search/search_api/tests/search_api_test_hooks/search_api_test_hooks.module b/sites/all/modules/contrib/search/search_api/tests/search_api_test_hooks/search_api_test_hooks.module
new file mode 100644
index 000000000..0591d319d
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/search_api_test_hooks/search_api_test_hooks.module
@@ -0,0 +1,35 @@
+setOption('tag query alter hook', TRUE);
+ $index = $query->getIndex();
+ $fields = $index->getFields();
+ foreach ($index->getDatasources() as $datasource_id => $datasource) {
+ if ($datasource->getEntityTypeId() == 'node') {
+ $field = Utility::createCombinedId($datasource_id, 'nid');
+ if (isset($fields[$field])) {
+ $query->addCondition($field, 2, '<>');
+ }
+ }
+ }
+}
+
+/**
+ * Implements hook_search_api_results_TAG_alter().
+ */
+function search_api_test_hooks_search_api_results_andrew_hill_alter(ResultSetInterface &$results) {
+ drupal_set_message('Llama');
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/search_api_test_hooks/search_api_test_hooks.search_api.inc b/sites/all/modules/contrib/search/search_api/tests/search_api_test_hooks/search_api_test_hooks.search_api.inc
new file mode 100644
index 000000000..9a85b5d0d
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/search_api_test_hooks/search_api_test_hooks.search_api.inc
@@ -0,0 +1,121 @@
+id() == 'webtest_server') {
+ $features[] = 'welcome_to_the_jungle';
+ }
+}
+
+/**
+ * Implements hook_search_api_processor_info_alter().
+ */
+function search_api_test_hooks_search_api_processor_info_alter(array &$processors) {
+ $processors['content_access']['label'] = 'Mystic bounce';
+}
+
+/**
+ * Implements hook_search_api_tracker_info_alter().
+ */
+function search_api_test_hooks_search_api_tracker_info_alter(array &$tracker_info) {
+ $tracker_info['search_api_test']['label'] = 'Good luck';
+}
+
+/**
+ * Implements hook_search_api_displays_alter().
+ */
+function search_api_test_hooks_search_api_displays_alter(array &$displays) {
+ if (isset($displays['views_page:search_api_test_view__page_1'])) {
+ $displays['views_page:search_api_test_view__page_1']['label'] = 'Some funny label for testing';
+ }
+}
+
+/**
+ * Implements hook_search_api_index_items_alter().
+ */
+function search_api_test_hooks_search_api_index_items_alter(IndexInterface $index, array &$items) {
+ unset($items['entity:node/1:en']);
+ drupal_set_message('Stormy');
+}
+
+/**
+ * Implements hook_search_api_items_indexed().
+ */
+function search_api_test_hooks_search_api_items_indexed(IndexInterface $index, array $item_ids) {
+ drupal_set_message('Please set me at ease');
+}
+
+/**
+ * Implements hook_search_api_query_alter().
+ */
+function search_api_test_hooks_search_api_query_alter(QueryInterface &$query) {
+ drupal_set_message('Funky blue note');
+ drupal_set_message(sprintf('Search id: %s', $query->getSearchId(FALSE)));
+ $query->addTag('andrew_hill');
+}
+
+/**
+ * Implements hook_search_api_results_alter().
+ */
+function search_api_test_hooks_search_api_results_alter(ResultSetInterface &$results) {
+ drupal_set_message('Stepping into tomorrow');
+}
+
+/**
+ * Implements hook_search_api_index_reindex().
+ */
+function search_api_test_hooks_search_api_index_reindex(IndexInterface $index, $clear = FALSE) {
+ drupal_set_message('Montara');
+}
+
+/**
+ * Implements hook_search_api_data_type_info_alter().
+ */
+function search_api_test_hooks_search_api_data_type_info_alter(array &$data_type_definitions) {
+ if (isset($data_type_definitions['text'])) {
+ $data_type_definitions['text']['label'] = 'Peace/Dolphin dance';
+ }
+}
+
+/**
+ * Implements hook_search_api_field_type_mapping_alter().
+ */
+function search_api_test_hooks_search_api_field_type_mapping_alter(array &$mapping) {
+ $mapping['datetime_iso8601'] = FALSE;
+ $mapping['timestamp'] = FALSE;
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/search_api_test_tasks/search_api_test_tasks.info.yml b/sites/all/modules/contrib/search/search_api/tests/search_api_test_tasks/search_api_test_tasks.info.yml
new file mode 100644
index 000000000..07ae08a2a
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/search_api_test_tasks/search_api_test_tasks.info.yml
@@ -0,0 +1,14 @@
+name: 'Search API Tasks Test'
+type: module
+description: 'Support module for tests of the Search API tasks system.'
+package: Testing
+dependencies:
+ - search_api:search_api
+# 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
diff --git a/sites/all/modules/contrib/search/search_api/tests/search_api_test_tasks/search_api_test_tasks.services.yml b/sites/all/modules/contrib/search/search_api/tests/search_api_test_tasks/search_api_test_tasks.services.yml
new file mode 100644
index 000000000..630040cfc
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/search_api_test_tasks/search_api_test_tasks.services.yml
@@ -0,0 +1,6 @@
+services:
+ search_api_test_tasks.test_task_worker:
+ class: Drupal\search_api_test_tasks\TestTaskWorker
+ arguments: ['@search_api.task_manager']
+ tags:
+ - { name: event_subscriber }
diff --git a/sites/all/modules/contrib/search/search_api/tests/search_api_test_tasks/src/TestTaskWorker.php b/sites/all/modules/contrib/search/search_api/tests/search_api_test_tasks/src/TestTaskWorker.php
new file mode 100644
index 000000000..91a4b5828
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/search_api_test_tasks/src/TestTaskWorker.php
@@ -0,0 +1,105 @@
+taskManager = $task_manager;
+ }
+
+ /**
+ * Handles a task event successfully.
+ *
+ * @param \Drupal\search_api\Task\TaskEvent $event
+ * The task event.
+ */
+ public function success(TaskEvent $event) {
+ $this->logEvent($event);
+ $event->stopPropagation();
+ }
+
+ /**
+ * Handles a task event with an exception.
+ *
+ * @param \Drupal\search_api\Task\TaskEvent $event
+ * The task event.
+ */
+ public function fail(TaskEvent $event) {
+ $this->logEvent($event);
+ $event->stopPropagation();
+
+ $event->setException(new SearchApiException('fail'));
+ }
+
+ /**
+ * Ignores a task event.
+ *
+ * @param \Drupal\search_api\Task\TaskEvent $event
+ * The task event.
+ */
+ public function ignore(TaskEvent $event) {
+ $this->logEvent($event);
+ }
+
+ /**
+ * Retrieves the event log.
+ *
+ * @return string[][]
+ * The event log, with each event represented by an associative array of the
+ * task's properties.
+ */
+ public function getEventLog() {
+ return $this->eventLog;
+ }
+
+ /**
+ * Logs an event.
+ *
+ * @param \Drupal\search_api\Task\TaskEvent $event
+ * The event.
+ */
+ protected function logEvent(TaskEvent $event) {
+ $this->eventLog[] = $event->getTask()->toArray();
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/search_api_test_views/config/install/views.view.search_api_test_cache.yml b/sites/all/modules/contrib/search/search_api/tests/search_api_test_views/config/install/views.view.search_api_test_cache.yml
new file mode 100644
index 000000000..55f598e7e
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/search_api_test_views/config/install/views.view.search_api_test_cache.yml
@@ -0,0 +1,286 @@
+langcode: en
+status: true
+dependencies:
+ config:
+ - search_api.index.database_search_index
+ module:
+ - search_api
+id: search_api_test_cache
+label: 'Search API Test search view caching'
+module: views
+description: ''
+tag: ''
+base_table: search_api_index_database_search_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:
+ bypass_access: true
+ skip_access: true
+ exposed_form:
+ type: basic
+ options:
+ submit_button: Apply
+ reset_button: false
+ reset_button_label: Reset
+ exposed_sorts_label: 'Sort by'
+ expose_sort_order: true
+ sort_asc_label: Asc
+ sort_desc_label: Desc
+ pager:
+ type: mini
+ options:
+ items_per_page: 10
+ offset: 0
+ id: 0
+ total_pages: null
+ 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
+ tags:
+ previous: ‹‹
+ next: ››
+ style:
+ type: default
+ row:
+ type: fields
+ options:
+ default_field_elements: true
+ inline: { }
+ separator: ''
+ hide_empty: false
+ fields:
+ name:
+ id: name
+ table: search_api_datasource_database_search_index_entity_entity_test_mulrev_changed
+ field: name
+ relationship: none
+ group_type: group
+ admin_label: ''
+ label: ''
+ exclude: false
+ alter:
+ alter_text: false
+ text: ''
+ make_link: false
+ path: ''
+ absolute: false
+ external: false
+ replace_spaces: false
+ path_case: none
+ trim_whitespace: false
+ alt: ''
+ rel: ''
+ link_class: ''
+ prefix: ''
+ suffix: ''
+ target: ''
+ nl2br: false
+ max_length: 0
+ word_boundary: true
+ ellipsis: true
+ more_link: false
+ more_link_text: ''
+ more_link_path: ''
+ strip_tags: false
+ trim: false
+ preserve_tags: ''
+ html: false
+ element_type: ''
+ element_class: ''
+ element_label_type: ''
+ element_label_class: ''
+ element_label_colon: false
+ element_wrapper_type: ''
+ element_wrapper_class: ''
+ element_default_classes: true
+ empty: ''
+ hide_empty: false
+ empty_zero: false
+ hide_alter_empty: true
+ click_sort_column: value
+ type: string
+ settings:
+ link_to_entity: false
+ group_column: value
+ group_columns: { }
+ group_rows: true
+ delta_limit: 0
+ delta_offset: 0
+ delta_reversed: false
+ delta_first_last: false
+ multi_type: separator
+ separator: ', '
+ field_api_classes: false
+ field_rendering: false
+ fallback_handler: search_api
+ fallback_options:
+ link_to_item: false
+ use_highlighting: true
+ multi_separator: ', '
+ entity_type: entity_test_mulrev_changed
+ plugin_id: search_api_field
+ filters:
+ search_api_fulltext:
+ id: search_api_fulltext
+ table: search_api_index_database_search_index
+ field: search_api_fulltext
+ relationship: none
+ group_type: group
+ admin_label: ''
+ operator: and
+ value: ''
+ group: 1
+ exposed: true
+ expose:
+ operator_id: search_api_fulltext_op
+ label: 'Fulltext search'
+ description: ''
+ use_operator: true
+ operator: search_api_fulltext_op
+ identifier: search_api_fulltext
+ required: false
+ 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:
+ search_api_id:
+ id: search_api_id
+ table: search_api_index_database_search_index
+ field: search_api_id
+ relationship: none
+ group_type: group
+ admin_label: ''
+ order: ASC
+ exposed: false
+ expose:
+ label: ''
+ plugin_id: search_api
+ title: 'Search API Test search view caching'
+ header:
+ result:
+ id: result
+ table: views
+ field: result
+ relationship: none
+ group_type: group
+ admin_label: ''
+ empty: true
+ content: 'Displaying @total search results'
+ plugin_id: result
+ footer: { }
+ empty: { }
+ relationships: { }
+ arguments: { }
+ display_extenders: { }
+ cache_metadata:
+ max-age: -1
+ contexts:
+ - 'languages:language_content'
+ - 'languages:language_interface'
+ - url
+ - url.query_args
+ tags: { }
+ none:
+ display_plugin: page
+ id: none
+ display_title: Disabled
+ position: 1
+ display_options:
+ display_extenders: { }
+ path: search-api-test-search-view-caching-none
+ display_description: ''
+ cache_metadata:
+ max-age: -1
+ contexts:
+ - 'languages:language_content'
+ - 'languages:language_interface'
+ - url
+ - url.query_args
+ tags: { }
+ tag:
+ display_plugin: page
+ id: tag
+ display_title: Tag
+ position: 1
+ display_options:
+ display_extenders: { }
+ path: search-api-test-search-view-caching-tag
+ display_description: ''
+ cache:
+ type: search_api_tag
+ options: { }
+ defaults:
+ cache: false
+ cache_metadata:
+ max-age: -1
+ contexts:
+ - 'languages:language_content'
+ - 'languages:language_interface'
+ - url
+ - url.query_args
+ tags: { }
+ time:
+ display_plugin: page
+ id: time
+ display_title: Time
+ position: 1
+ display_options:
+ display_extenders: { }
+ path: search-api-test-search-view-caching-time
+ display_description: ''
+ cache:
+ type: search_api_time
+ options:
+ results_lifespan: 3600
+ results_lifespan_custom: 0
+ output_lifespan: 3600
+ output_lifespan_custom: 0
+ defaults:
+ cache: false
+ cache_metadata:
+ max-age: -1
+ contexts:
+ - 'languages:language_content'
+ - 'languages:language_interface'
+ - url
+ - url.query_args
+ tags: { }
diff --git a/sites/all/modules/contrib/search/search_api/tests/search_api_test_views/config/install/views.view.search_api_test_sorts.yml b/sites/all/modules/contrib/search/search_api/tests/search_api_test_views/config/install/views.view.search_api_test_sorts.yml
new file mode 100644
index 000000000..092eabb4d
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/search_api_test_views/config/install/views.view.search_api_test_sorts.yml
@@ -0,0 +1,317 @@
+base_field: search_api_id
+base_table: search_api_index_database_search_index
+core: 8.x
+description: ''
+status: true
+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:
+ skip_access: true
+ exposed_form:
+ type: basic
+ options:
+ submit_button: Search
+ reset_button: false
+ reset_button_label: Reset
+ exposed_sorts_label: 'Sort by'
+ expose_sort_order: false
+ sort_asc_label: Asc
+ sort_desc_label: Desc
+ pager:
+ type: full
+ options:
+ items_per_page: 10
+ offset: 0
+ id: 0
+ total_pages: null
+ expose:
+ items_per_page: false
+ items_per_page_label: 'Items per page'
+ items_per_page_options: '5, 10, 20, 40, 60'
+ items_per_page_options_all: false
+ items_per_page_options_all_label: '- All -'
+ offset: false
+ offset_label: Offset
+ tags:
+ previous: '‹ previous'
+ next: 'next ›'
+ first: '« first'
+ last: 'last »'
+ quantity: 9
+ style:
+ type: default
+ row:
+ type: search_api
+ options:
+ view_modes:
+ 'entity:entity_test_mulrev_changed':
+ article: default
+ page: default
+ filters:
+ search_api_fulltext:
+ id: search_api_fulltext
+ table: search_api_index_database_search_index
+ field: search_api_fulltext
+ relationship: none
+ group_type: group
+ admin_label: ''
+ operator: and
+ value: ''
+ group: 1
+ exposed: true
+ expose:
+ operator_id: search_api_fulltext_op
+ label: 'Fulltext search'
+ description: ''
+ use_operator: true
+ operator: search_api_fulltext_op
+ identifier: search_api_fulltext
+ required: false
+ 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: { }
+ min_length: 3
+ fields: { }
+ plugin_id: search_api_fulltext
+ id:
+ plugin_id: search_api_numeric
+ id: id
+ table: search_api_index_database_search_index
+ field: id
+ relationship: none
+ admin_label: ''
+ operator: '='
+ group: 1
+ exposed: true
+ expose:
+ operator_id: id_op
+ label: ''
+ description: ''
+ use_operator: true
+ operator: id_op
+ identifier: id
+ required: false
+ remember: false
+ multiple: false
+ remember_roles:
+ authenticated: authenticated
+ anonymous: '0'
+ administrator: '0'
+ is_grouped: false
+ created:
+ plugin_id: search_api_date
+ id: created
+ table: search_api_index_database_search_index
+ field: created
+ relationship: none
+ admin_label: ''
+ operator: '='
+ group: 1
+ exposed: true
+ expose:
+ operator_id: created_op
+ label: ''
+ description: ''
+ use_operator: true
+ operator: created_op
+ identifier: created
+ required: false
+ remember: false
+ multiple: false
+ remember_roles:
+ authenticated: authenticated
+ anonymous: '0'
+ administrator: '0'
+ is_grouped: false
+ keywords:
+ plugin_id: search_api_string
+ id: keywords
+ table: search_api_index_database_search_index
+ field: keywords
+ relationship: none
+ admin_label: ''
+ operator: '='
+ group: 1
+ exposed: true
+ expose:
+ operator_id: keywords_op
+ label: ''
+ description: ''
+ use_operator: true
+ operator: keywords_op
+ identifier: keywords
+ required: false
+ remember: false
+ multiple: false
+ remember_roles:
+ authenticated: authenticated
+ anonymous: '0'
+ administrator: '0'
+ is_grouped: false
+ search_api_language:
+ plugin_id: search_api_language
+ id: search_api_language
+ table: search_api_index_database_search_index
+ field: search_api_language
+ relationship: none
+ admin_label: ''
+ operator: 'in'
+ group: 1
+ exposed: true
+ expose:
+ operator_id: language_op
+ label: ''
+ description: ''
+ use_operator: true
+ operator: language_op
+ identifier: language
+ required: false
+ remember: false
+ multiple: false
+ remember_roles:
+ authenticated: authenticated
+ anonymous: '0'
+ administrator: '0'
+ is_grouped: false
+ search_api_datasource:
+ plugin_id: search_api_datasource
+ id: search_api_datasource
+ table: search_api_index_database_search_index
+ field: search_api_datasource
+ relationship: none
+ admin_label: ''
+ operator: or
+ value: { }
+ group: 1
+ exposed: true
+ expose:
+ operator_id: datasource_op
+ label: ''
+ description: ''
+ use_operator: true
+ operator: datasource_op
+ identifier: datasource
+ required: false
+ remember: false
+ multiple: true
+ remember_roles:
+ authenticated: authenticated
+ anonymous: '0'
+ administrator: '0'
+ reduce: false
+ is_grouped: false
+ sorts:
+ search_api_relevance:
+ id: search_api_relevance
+ table: search_api_index_default
+ field: search_api_relevance
+ relationship: none
+ group_type: group
+ admin_label: ''
+ order: DESC
+ exposed: true
+ expose:
+ label: Relevance
+ plugin_id: search_api
+ search_api_id_desc:
+ id: search_api_id_desc
+ table: search_api_index_database_search_index
+ field: search_api_id
+ relationship: none
+ group_type: group
+ admin_label: ''
+ order: DESC
+ exposed: true
+ expose:
+ label: 'id desc'
+ plugin_id: search_api
+ search_api_id:
+ id: search_api_id
+ table: search_api_index_database_search_index
+ field: search_api_id
+ relationship: none
+ group_type: group
+ admin_label: ''
+ order: ASC
+ exposed: false
+ expose:
+ label: ''
+ plugin_id: search_api
+ title: 'Fulltext test index'
+ header:
+ result:
+ id: result
+ table: views
+ field: result
+ relationship: none
+ group_type: group
+ admin_label: ''
+ content: 'Displaying @total search results'
+ plugin_id: result
+ footer: { }
+ empty: { }
+ relationships: { }
+ arguments:
+ search_api_datasource:
+ plugin_id: search_api
+ id: search_api_datasource
+ table: search_api_index_database_search_index
+ field: search_api_datasource
+ break_phrase: true
+ type:
+ plugin_id: search_api
+ id: type
+ table: search_api_index_database_search_index
+ field: type
+ break_phrase: false
+ not: true
+ keywords:
+ plugin_id: search_api
+ id: keywords
+ table: search_api_index_database_search_index
+ field: keywords
+ break_phrase: true
+ page_1:
+ display_plugin: page
+ id: page_1
+ display_title: Page
+ position: 1
+ display_options:
+ path: search-api-test-sorts
+label: 'Search API sorts test search view'
+module: views
+id: search_api_test_sorts
+tag: ''
+langcode: en
+dependencies:
+ module:
+ - search_api
+ - search_api_test_views
diff --git a/sites/all/modules/contrib/search/search_api/tests/search_api_test_views/config/install/views.view.search_api_test_view.yml b/sites/all/modules/contrib/search/search_api/tests/search_api_test_views/config/install/views.view.search_api_test_view.yml
new file mode 100644
index 000000000..98df161cd
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/search_api_test_views/config/install/views.view.search_api_test_view.yml
@@ -0,0 +1,350 @@
+base_field: search_api_id
+base_table: search_api_index_database_search_index
+core: 8.x
+description: ''
+status: true
+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:
+ skip_access: true
+ exposed_form:
+ type: basic
+ 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
+ pager:
+ type: full
+ options:
+ items_per_page: 10
+ offset: 0
+ id: 0
+ total_pages: null
+ expose:
+ items_per_page: false
+ items_per_page_label: 'Items per page'
+ items_per_page_options: '5, 10, 20, 40, 60'
+ items_per_page_options_all: false
+ items_per_page_options_all_label: '- All -'
+ offset: false
+ offset_label: Offset
+ tags:
+ previous: '‹ previous'
+ next: 'next ›'
+ first: '« first'
+ last: 'last »'
+ quantity: 9
+ style:
+ type: default
+ row:
+ type: search_api
+ options:
+ view_modes:
+ 'entity:entity_test_mulrev_changed':
+ article: default
+ page: default
+ filters:
+ search_api_fulltext:
+ id: search_api_fulltext
+ table: search_api_index_database_search_index
+ field: search_api_fulltext
+ relationship: none
+ group_type: group
+ admin_label: ''
+ operator: and
+ value: ''
+ group: 1
+ exposed: true
+ expose:
+ operator_id: search_api_fulltext_op
+ label: 'Fulltext search'
+ description: ''
+ use_operator: true
+ operator: search_api_fulltext_op
+ identifier: search_api_fulltext
+ required: false
+ 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: { }
+ min_length: 3
+ fields: { }
+ plugin_id: search_api_fulltext
+ id:
+ plugin_id: search_api_numeric
+ id: id
+ table: search_api_index_database_search_index
+ field: id
+ relationship: none
+ admin_label: ''
+ operator: '='
+ group: 1
+ exposed: true
+ expose:
+ operator_id: id_op
+ label: ''
+ description: ''
+ use_operator: true
+ operator: id_op
+ identifier: id
+ required: false
+ remember: false
+ multiple: false
+ remember_roles:
+ authenticated: authenticated
+ anonymous: '0'
+ administrator: '0'
+ is_grouped: false
+ created:
+ plugin_id: search_api_date
+ id: created
+ table: search_api_index_database_search_index
+ field: created
+ relationship: none
+ admin_label: ''
+ operator: '='
+ group: 1
+ exposed: true
+ expose:
+ operator_id: created_op
+ label: ''
+ description: ''
+ use_operator: true
+ operator: created_op
+ identifier: created
+ required: false
+ remember: false
+ multiple: false
+ remember_roles:
+ authenticated: authenticated
+ anonymous: '0'
+ administrator: '0'
+ is_grouped: false
+ keywords:
+ plugin_id: search_api_string
+ id: keywords
+ table: search_api_index_database_search_index
+ field: keywords
+ relationship: none
+ admin_label: ''
+ operator: '='
+ group: 1
+ exposed: true
+ expose:
+ operator_id: keywords_op
+ label: ''
+ description: ''
+ use_operator: true
+ operator: keywords_op
+ identifier: keywords
+ required: false
+ remember: false
+ multiple: false
+ remember_roles:
+ authenticated: authenticated
+ anonymous: '0'
+ administrator: '0'
+ is_grouped: false
+ name:
+ plugin_id: search_api_text
+ id: name
+ table: search_api_index_database_search_index
+ field: name
+ relationship: none
+ admin_label: ''
+ operator: '='
+ group: 1
+ exposed: true
+ expose:
+ operator_id: name_op
+ label: ''
+ description: ''
+ use_operator: true
+ operator: name_op
+ identifier: name
+ required: false
+ remember: false
+ multiple: false
+ remember_roles:
+ authenticated: authenticated
+ anonymous: '0'
+ administrator: '0'
+ is_grouped: false
+ search_api_language:
+ plugin_id: search_api_language
+ id: search_api_language
+ table: search_api_index_database_search_index
+ field: search_api_language
+ relationship: none
+ admin_label: ''
+ operator: 'in'
+ group: 1
+ exposed: true
+ expose:
+ operator_id: language_op
+ label: ''
+ description: ''
+ use_operator: true
+ operator: language_op
+ identifier: language
+ required: false
+ remember: false
+ multiple: false
+ remember_roles:
+ authenticated: authenticated
+ anonymous: '0'
+ administrator: '0'
+ is_grouped: false
+ search_api_datasource:
+ plugin_id: search_api_datasource
+ id: search_api_datasource
+ table: search_api_index_database_search_index
+ field: search_api_datasource
+ relationship: none
+ admin_label: ''
+ operator: or
+ value: { }
+ group: 1
+ exposed: true
+ expose:
+ operator_id: datasource_op
+ label: ''
+ description: ''
+ use_operator: true
+ operator: datasource_op
+ identifier: datasource
+ required: false
+ remember: false
+ multiple: true
+ remember_roles:
+ authenticated: authenticated
+ anonymous: '0'
+ administrator: '0'
+ reduce: false
+ is_grouped: false
+ sorts:
+ search_api_id:
+ id: search_api_id
+ table: search_api_index_database_search_index
+ field: search_api_id
+ relationship: none
+ group_type: group
+ admin_label: ''
+ order: ASC
+ exposed: false
+ expose:
+ label: ''
+ plugin_id: search_api
+ title: 'Fulltext test index'
+ header:
+ result:
+ id: result
+ table: views
+ field: result
+ relationship: none
+ group_type: group
+ admin_label: ''
+ content: 'Displaying @total search results'
+ plugin_id: result
+ footer: { }
+ empty: { }
+ relationships: { }
+ arguments:
+ search_api_datasource:
+ plugin_id: search_api
+ id: search_api_datasource
+ table: search_api_index_database_search_index
+ field: search_api_datasource
+ break_phrase: true
+ type:
+ plugin_id: search_api
+ id: type
+ table: search_api_index_database_search_index
+ field: type
+ break_phrase: false
+ not: true
+ keywords:
+ plugin_id: search_api
+ id: keywords
+ table: search_api_index_database_search_index
+ field: keywords
+ break_phrase: true
+ page_1:
+ display_plugin: page
+ id: page_1
+ display_title: Page
+ position: 1
+ display_options:
+ path: search-api-test
+ block_1:
+ display_plugin: block
+ id: block_1
+ display_title: Block
+ position: 2
+ display_options:
+ display_extenders: { }
+ defaults:
+ use_ajax: false
+ use_ajax: true
+ rest_export_1:
+ display_plugin: rest_export
+ id: rest_export_1
+ display_title: 'REST export'
+ position: 1
+ display_options:
+ display_extenders: { }
+ path: search-api-rest-test
+ pager:
+ type: some
+ options:
+ items_per_page: 10
+ offset: 0
+ style:
+ type: serializer
+ options:
+ grouping: { }
+ uses_fields: false
+ formats: { }
+ row:
+ type: data_entity
+ options: { }
+label: 'Search API Test search view'
+module: views
+id: search_api_test_view
+tag: ''
+langcode: en
+dependencies:
+ module:
+ - search_api
+ - search_api_test_views
diff --git a/sites/all/modules/contrib/search/search_api/tests/search_api_test_views/search_api_test_views.info.yml b/sites/all/modules/contrib/search/search_api/tests/search_api_test_views/search_api_test_views.info.yml
new file mode 100644
index 000000000..fa71ea3ac
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/search_api_test_views/search_api_test_views.info.yml
@@ -0,0 +1,17 @@
+name: 'Search API Views Test'
+type: module
+description: 'Support module for Search API Views tests'
+package: Search
+dependencies:
+ - search_api:search_api
+ - search_api:search_api_test_db
+ - drupal:node
+ - drupal:views
+# 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
diff --git a/sites/all/modules/contrib/search/search_api/tests/search_api_test_views/search_api_test_views.module b/sites/all/modules/contrib/search/search_api/tests/search_api_test_views/search_api_test_views.module
new file mode 100644
index 000000000..5e31a7fa1
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/search_api_test_views/search_api_test_views.module
@@ -0,0 +1,27 @@
+getOption('search_api_retrieved_properties');
+ if ($properties) {
+ $message = [];
+ foreach ($properties as $datasource_properties) {
+ foreach ($datasource_properties as $combined_property_path) {
+ $message[] = "'$combined_property_path'";
+ }
+ }
+ drupal_set_message(implode(' ', $message));
+ }
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Functional/CacheabilityTest.php b/sites/all/modules/contrib/search/search_api/tests/src/Functional/CacheabilityTest.php
new file mode 100644
index 000000000..785b98098
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Functional/CacheabilityTest.php
@@ -0,0 +1,86 @@
+setUpExampleStructure();
+ $this->insertExampleContent();
+
+ \Drupal::getContainer()
+ ->get('search_api.index_task_manager')
+ ->addItemsAll(Index::load($this->indexId));
+ $this->indexItems($this->indexId);
+ }
+
+ /**
+ * Tests the cacheability settings of Search API.
+ */
+ public function testFramework() {
+ $this->drupalLogin($this->adminUser);
+
+ // Verify that the search results are marked as uncacheable.
+ $this->drupalGet('search-api-test');
+ $this->assertSession()->statusCodeEquals(200);
+ $this->assertSession()->responseHeaderEquals('x-drupal-dynamic-cache', 'UNCACHEABLE');
+ $this->assertTrue(strpos($this->drupalGetHeader('cache-control'), 'no-cache'));
+
+ // Verify that the search results are displayed.
+ $this->assertSession()->pageTextContains('foo test');
+ $this->assertSession()->pageTextContains('foo baz');
+ }
+
+ /**
+ * Tests that indexing or deleting items clears the cache.
+ */
+ public function testViewsCacheAddRemoveContent() {
+ $entity = $this->addTestEntity(6, [
+ 'name' => 'Fresh node',
+ 'body' => 'test foobar Case',
+ 'type' => 'item',
+ ]);
+ // Prime page cache before indexing.
+ $this->drupalGet('search-api-test-search-view-caching-tag');
+ $this->assertSession()->pageTextContains('Displaying 5 search results');
+
+ $this->indexItems($this->indexId);
+
+ // Check that the newly indexed node is visible on the search index.
+ $this->drupalGet('search-api-test-search-view-caching-tag');
+ $this->assertSession()->pageTextContains('Displaying 6 search results');
+
+ $entity->delete();
+
+ // Check that the deleted entity is now no longer shown.
+ $this->drupalGet('search-api-test-search-view-caching-tag');
+ $this->assertSession()->pageTextContains('Displaying 5 search results');
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Functional/ConfigOverrideIntegrationTest.php b/sites/all/modules/contrib/search/search_api/tests/src/Functional/ConfigOverrideIntegrationTest.php
new file mode 100644
index 000000000..4f595cad2
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Functional/ConfigOverrideIntegrationTest.php
@@ -0,0 +1,210 @@
+ 'Overridden server',
+ 'required' => TRUE,
+ ];
+ $settings['config']['search_api.server.test_server']['status'] = (object) [
+ 'value' => TRUE,
+ 'required' => TRUE,
+ ];
+ $settings['config']['search_api.server.test_server']['backend_config']['test'] = (object) [
+ 'value' => 'foobar',
+ 'required' => TRUE,
+ ];
+ $settings['config']['search_api.index.test_index']['name'] = (object) [
+ 'value' => 'Overridden index',
+ 'required' => TRUE,
+ ];
+ $this->writeSettings($settings);
+
+ $permissions = [
+ 'administer search_api',
+ 'access administration pages',
+ 'administer nodes',
+ 'bypass node access',
+ 'administer content types',
+ ];
+ $this->adminUser = $this->drupalCreateUser($permissions);
+ $this->drupalLogin($this->adminUser);
+ }
+
+ /**
+ * Tests that the UI works correctly with config entity overrides present.
+ */
+ public function testConfigOverrideIntegration() {
+ $base_path = 'admin/config/search/search-api';
+ $new_user = $this->drupalCreateUser();
+
+ // Set up a trap through method overrides to ensure that critical methods
+ // are only ever called with the correct, overridden backend configuration.
+ $override = [MethodOverrides::class, 'overrideTestBackendMethod'];
+ $methods = [
+ 'postInsert',
+ 'preUpdate',
+ 'postUpdate',
+ 'addIndex',
+ 'updateIndex',
+ 'removeIndex',
+ 'deleteItems',
+ 'deleteAllIndexItems',
+ 'search',
+ 'isAvailable',
+ 'getDiscouragedProcessors',
+ ];
+ foreach ($methods as $method) {
+ $this->setMethodOverride('backend', $method, $override);
+ }
+ // indexItems() needs a special override since it needs to return item IDs.
+ $override = [MethodOverrides::class, 'overrideTestBackendIndexItems'];
+ $this->setMethodOverride('backend', 'indexItems', $override);
+
+ // Add the server.
+ $this->drupalGet("$base_path/add-server");
+ $edit = [
+ 'id' => 'test_server',
+ 'name' => 'Test server',
+ 'backend' => 'search_api_test',
+ ];
+ $this->submitForm($edit, 'Save');
+ $this->assertSession()
+ ->pageTextContains('The server was successfully saved.');
+ $this->assertSession()->addressEquals($base_path . '/server/test_server');
+
+ // Add the index.
+ $this->drupalGet("$base_path/add-index");
+ $edit = [
+ 'id' => 'test_index',
+ 'name' => 'Test index',
+ 'server' => 'test_server',
+ 'datasources[entity:user]' => TRUE,
+ 'options[index_directly]' => FALSE,
+ ];
+ $this->submitForm($edit, 'Save');
+ $this->assertSession()
+ ->pageTextContains('Please configure the used datasources.');
+ $this->submitForm([], 'Save');
+ $this->checkForMetaRefresh();
+ $this->assertSession()
+ ->pageTextContains('The index was successfully saved.');
+ $this->assertSession()->addressEquals($base_path . '/index/test_index');
+
+ // Overview page displays overridden values.
+ $this->drupalGet($base_path);
+ $this->assertSession()->pageTextContains('Overridden server');
+ $this->assertSession()->responseNotContains('Test server');
+ $this->assertSession()->pageTextContains('Overridden index');
+ $this->assertSession()->responseNotContains('Test index');
+
+ // Normal server page displays overridden values.
+ $this->drupalGet($base_path . '/server/test_server');
+ $this->assertSession()->pageTextContains('Overridden server');
+ $this->assertSession()->responseNotContains('Test server');
+ $this->assertSession()->pageTextContains('Overridden index');
+ $this->assertSession()->responseNotContains('Test index');
+ $this->assertSession()->pageTextContains('enabled');
+ $this->assertSession()->responseNotContains('disabled');
+
+ // Disabling the server isn't possible.
+ $this->clickLink('disable');
+ $this->assertSession()->pageTextContains('Test server');
+ $this->assertSession()->responseNotContains('Overridden server');
+ $this->submitForm([], 'Disable');
+ $this->drupalGet($base_path . '/server/test_server');
+ $this->assertSession()->pageTextContains('enabled');
+ $this->assertSession()->responseNotContains('disabled');
+
+ // The "Edit" form shows the override-free server.
+ $this->drupalGet($base_path . '/server/test_server/edit');
+ $this->assertSession()->pageTextContains('Test server');
+ $this->assertSession()->responseNotContains('Overridden server');
+ $this->assertSession()->responseNotContains('foobar');
+ $edit = [
+ 'name' => 'New server name',
+ 'backend_config[test]' => 'nonsense',
+ ];
+ $this->submitForm($edit, 'Save');
+
+ // "View" tab still shows overrides, "Edit" tab still doesn't.
+ $this->assertSession()->addressEquals($base_path . '/server/test_server');
+ $this->assertSession()->pageTextContains('Overridden server');
+ $this->assertSession()->responseNotContains('New server name');
+ $this->drupalGet($base_path . '/server/test_server/edit');
+ $this->assertSession()->pageTextContains('New server name');
+ $this->assertSession()->responseNotContains('Overridden server');
+ $this->assertSession()->responseContains('nonsense');
+ $this->assertSession()->responseNotContains('foobar');
+
+ // Server "clear" form and command use overridden server.
+ $this->drupalGet($base_path . '/server/test_server/clear');
+ $this->assertSession()->pageTextContains('Overridden server');
+ $this->assertSession()->responseNotContains('New server name');
+ $this->submitForm([], 'Confirm');
+
+ // Index "View" tab uses overridden index and server.
+ $this->drupalGet($base_path . '/index/test_index');
+ $this->assertSession()->pageTextContains('Overridden index');
+ $this->assertSession()->responseNotContains('Test index');
+ $this->assertSession()->pageTextContains('Overridden server');
+ $this->assertSession()->responseNotContains('New server name');
+ $this->assertSession()->pageTextContains('enabled');
+ $this->assertSession()->responseNotContains('disabled');
+
+ // Index items, see if that triggers an error.
+ $this->submitForm([], 'Index now');
+ $this->checkForMetaRefresh();
+
+ // Same for deleting an item.
+ $new_user->delete();
+
+ // The "Edit", "Fields", "Processors" and "Disable" tabs/forms should use
+ // the override-free index version.
+ foreach (['edit', 'fields', 'fields/add/nojs', 'processors', 'disable'] as $tab) {
+ $this->drupalGet("$base_path/index/test_index/$tab");
+ $this->assertSession()->pageTextContains('Test index');
+ $this->assertSession()->responseNotContains('Overridden index');
+ }
+
+ // The "Reindex" and "Clear" forms and commands both use the overridden
+ // index.
+ foreach (['reindex', 'clear'] as $tab) {
+ $this->drupalGet("$base_path/index/test_index/$tab");
+ $this->assertSession()->pageTextContains('Overridden index');
+ $this->assertSession()->responseNotContains('Test index');
+ }
+ // Clear the index, see if that triggers an error.
+ $this->submitForm([], 'Confirm');
+
+ // The server "Delete" form also uses overrides.
+ $this->drupalGet($base_path . '/server/test_server/delete');
+ $this->assertSession()->pageTextContains('Overridden server');
+ $this->assertSession()->responseNotContains('New server name');
+ $this->assertSession()->pageTextContains('Overridden index');
+ $this->assertSession()->responseNotContains('Test index');
+
+ // Delete the server, see if that triggers any errors.
+ $this->submitForm([], 'Delete');
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Functional/ExampleContentTrait.php b/sites/all/modules/contrib/search/search_api/tests/src/Functional/ExampleContentTrait.php
new file mode 100644
index 000000000..c24f7510a
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Functional/ExampleContentTrait.php
@@ -0,0 +1,123 @@
+addTestEntity(1, [
+ 'name' => 'foo bar baz foobaz föö smile' . $smiley,
+ 'body' => 'test test case Case casE',
+ 'type' => 'item',
+ 'keywords' => ['Orange', 'orange', 'örange', 'Orange', $smiley],
+ 'category' => 'item_category',
+ ]);
+ $this->addTestEntity(2, [
+ 'name' => 'foo test foobuz',
+ 'body' => 'bar test casE',
+ 'type' => 'item',
+ 'keywords' => ['orange', 'apple', 'grape'],
+ 'category' => 'item_category',
+ ]);
+ $this->addTestEntity(3, [
+ 'name' => 'bar',
+ 'body' => 'test foobar Case',
+ 'type' => 'item',
+ ]);
+ $this->addTestEntity(4, [
+ 'name' => 'foo baz',
+ 'body' => 'test test test',
+ 'type' => 'article',
+ 'keywords' => ['apple', 'strawberry', 'grape'],
+ 'category' => 'article_category',
+ 'width' => '1.0',
+ ]);
+ $this->addTestEntity(5, [
+ 'name' => 'bar baz',
+ 'body' => 'foo',
+ 'type' => 'article',
+ 'keywords' => ['orange', 'strawberry', 'grape', 'banana'],
+ 'category' => 'article_category',
+ 'width' => '2.0',
+ ]);
+ $count = \Drupal::entityQuery('entity_test_mulrev_changed')->count()->execute();
+ $this->assertEquals(5, $count, "$count items inserted.");
+ }
+
+ /**
+ * Creates and saves a test entity with the given values.
+ *
+ * @param int $id
+ * The entity's ID.
+ * @param array $values
+ * The entity's property values.
+ *
+ * @return \Drupal\entity_test\Entity\EntityTestMulRevChanged
+ * The created entity.
+ */
+ protected function addTestEntity($id, array $values) {
+ $storage = \Drupal::entityTypeManager()->getStorage('entity_test_mulrev_changed');
+ $values['id'] = $id;
+ $this->entities[$id] = $storage->create($values);
+ $this->entities[$id]->save();
+ return $this->entities[$id];
+ }
+
+ /**
+ * Indexes all (unindexed) items on the specified index.
+ *
+ * @param string $index_id
+ * The ID of the index on which items should be indexed.
+ *
+ * @return int
+ * The number of successfully indexed items.
+ */
+ protected function indexItems($index_id) {
+ /** @var \Drupal\search_api\IndexInterface $index */
+ $index = Index::load($index_id);
+ return $index->indexItems();
+ }
+
+ /**
+ * Returns the item IDs for the given entity IDs.
+ *
+ * @param array $entity_ids
+ * An array of entity IDs.
+ *
+ * @return string[]
+ * An array of item IDs.
+ */
+ protected function getItemIds(array $entity_ids) {
+ $translate_ids = function ($entity_id) {
+ return Utility::createCombinedId('entity:entity_test_mulrev_changed', $entity_id . ':en');
+ };
+ return array_map($translate_ids, $entity_ids);
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Functional/FieldIntegrationTest.php b/sites/all/modules/contrib/search/search_api/tests/src/Functional/FieldIntegrationTest.php
new file mode 100644
index 000000000..ea66987d6
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Functional/FieldIntegrationTest.php
@@ -0,0 +1,50 @@
+getFields();
+
+ // Load and parse the same configuration file.
+ $yaml_file = __DIR__ . '/../../search_api_test_db/config/install/search_api.index.database_search_index.yml';
+ $index_configuration = Yaml::decode(file_get_contents($yaml_file));
+ $field_settings = $index_configuration['field_settings'];
+
+ // Check that all the fields defined in the config file made it into the
+ // index.
+ $this->assertEquals(array_keys($fields), array_keys($field_settings));
+
+ // Make sure that the fields have the same type.
+ foreach ($field_settings as $setting) {
+ $this->assertArrayHasKey($setting['property_path'], $fields);
+ $field = $fields[$setting['property_path']];
+ $this->assertEquals($setting['label'], $field->getLabel());
+ $this->assertEquals($setting['datasource_id'], $field->getDatasourceId());
+ $this->assertEquals($setting['type'], $field->getType());
+ }
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Functional/HooksTest.php b/sites/all/modules/contrib/search/search_api/tests/src/Functional/HooksTest.php
new file mode 100644
index 000000000..e2f863dc2
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Functional/HooksTest.php
@@ -0,0 +1,164 @@
+drupalCreateNode(['type' => 'page', 'title' => 'node - 1']);
+ $this->drupalCreateNode(['type' => 'page', 'title' => 'node - 2']);
+ $this->drupalCreateNode(['type' => 'page', 'title' => 'node - 3']);
+ $this->drupalCreateNode(['type' => 'page', 'title' => 'node - 4']);
+
+ // Create an index and server to work with.
+ $this->server = $this->getTestServer();
+ $index = $this->getTestIndex();
+
+ // Add the test processor to the index so we can make sure that all expected
+ // processor methods are called, too.
+ /** @var \Drupal\search_api\Processor\ProcessorInterface $processor */
+ $processor = \Drupal::getContainer()
+ ->get('search_api.plugin_helper')
+ ->createProcessorPlugin($index, 'search_api_test');
+ $index->addProcessor($processor)->save();
+
+ // Parts of this test actually use the "database_search_index" from the
+ // search_api_test_db module (via the test view). Set the processor there,
+ // too.
+ $index = Index::load('database_search_index');
+ $processor = \Drupal::getContainer()
+ ->get('search_api.plugin_helper')
+ ->createProcessorPlugin($index, 'search_api_test');
+ $index->addProcessor($processor)->save();
+
+ // Reset the called methods on the processor.
+ $this->getCalledMethods('processor');
+
+ // Log in, so we can test all the things.
+ $this->drupalLogin($this->adminUser);
+ }
+
+ /**
+ * Tests various operations via the Search API's admin UI.
+ */
+ public function testHooks() {
+ // hook_search_api_backend_info_alter() was invoked.
+ $this->drupalGet('admin/config/search/search-api/add-server');
+ $this->assertSession()->pageTextContains('Slims return');
+
+ // hook_search_api_datasource_info_alter() was invoked.
+ $this->drupalGet('admin/config/search/search-api/add-index');
+ $this->assertSession()->pageTextContains('Distant land');
+ // hook_search_api_tracker_info_alter() was invoked.
+ $this->assertSession()->pageTextContains('Good luck');
+
+ // hook_search_api_processor_info_alter() was invoked.
+ $this->drupalGet($this->getIndexPath('processors'));
+ $this->assertSession()->pageTextContains('Mystic bounce');
+
+ // hook_search_api_parse_mode_info_alter was invoked.
+ $definition = \Drupal::getContainer()
+ ->get('plugin.manager.search_api.parse_mode')
+ ->getDefinition('direct');
+ $this->assertEquals('Song for My Father', $definition['label']);
+
+ // Saving the index should trigger the processor's preIndexSave() method.
+ $this->submitForm([], 'Save');
+ $processor_methods = $this->getCalledMethods('processor');
+ $this->assertEquals(['preIndexSave'], $processor_methods);
+
+ $this->drupalGet($this->getIndexPath());
+ // Duplication on value 'Index now' with summary.
+ $this->submitForm([], 'Index now');
+ $this->checkForMetaRefresh();
+ $this->assertSession()->pageTextContains('Successfully indexed 4 items.');
+
+ // During indexing, alterIndexedItems() and preprocessIndexItems() should be
+ // called on the processor.
+ $processor_methods = $this->getCalledMethods('processor');
+ $expected = ['alterIndexedItems', 'preprocessIndexItems'];
+ $this->assertEquals($expected, $processor_methods);
+
+ // hook_search_api_index_items_alter() was invoked, this removed node:1.
+ // hook_search_api_query_TAG_alter() was invoked, this removed node:3.
+ $this->assertSession()->pageTextContains('There are 2 items indexed on the server for this index.');
+ $this->assertSession()->pageTextContains('Stormy');
+
+ // hook_search_api_items_indexed() was invoked.
+ $this->assertSession()->pageTextContains('Please set me at ease');
+
+ // hook_search_api_index_reindex() was invoked.
+ $this->drupalGet($this->getIndexPath('reindex'));
+ $this->submitForm([], 'Confirm');
+ $this->assertSession()->pageTextContains('Montara');
+
+ // hook_search_api_data_type_info_alter() was invoked.
+ $this->drupalGet($this->getIndexPath('fields'));
+ $this->assertSession()->pageTextContains('Peace/Dolphin dance');
+ // The implementation of hook_search_api_field_type_mapping_alter() has
+ // removed all dates, so we can't see any timestamp anymore in the page.
+ $url_options['query']['datasource'] = 'entity:node';
+ $this->drupalGet($this->getIndexPath('fields/add/nojs'), $url_options);
+ $this->assertSession()->pageTextContains('Add fields to index');
+ $this->assertSession()->pageTextNotContains('timestamp');
+
+ $this->drupalGet('search-api-test');
+ $this->assertSession()->pageTextContains('Search id: views_page:search_api_test_view__page_1');
+ // hook_search_api_query_alter() was invoked.
+ $this->assertSession()->pageTextContains('Funky blue note');
+ // hook_search_api_results_alter() was invoked.
+ $this->assertSession()->pageTextContains('Stepping into tomorrow');
+ // hook_search_api_results_TAG_alter() was invoked.
+ $this->assertSession()->pageTextContains('Llama');
+
+ // The query alter methods of the processor were called.
+ $processor_methods = $this->getCalledMethods('processor');
+ $expected = ['preprocessSearchQuery', 'postprocessSearchResults'];
+ $this->assertEquals($expected, $processor_methods);
+
+ // hook_search_api_server_features_alter() is triggered.
+ $this->assertTrue($this->server->supportsFeature('welcome_to_the_jungle'));
+
+ $displays = \Drupal::getContainer()->get('plugin.manager.search_api.display')
+ ->getInstances();
+ // hook_search_api_displays_alter was invoked.
+ $display_label = $displays['views_page:search_api_test_view__page_1']->label();
+ $this->assertEquals('Some funny label for testing', $display_label);
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Functional/IntegrationTest.php b/sites/all/modules/contrib/search/search_api/tests/src/Functional/IntegrationTest.php
new file mode 100644
index 000000000..39d678daa
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Functional/IntegrationTest.php
@@ -0,0 +1,1581 @@
+indexStorage = \Drupal::entityTypeManager()->getStorage('search_api_index');
+
+ $permissions = [
+ 'administer search_api',
+ 'access administration pages',
+ 'administer nodes',
+ 'bypass node access',
+ 'administer content types',
+ 'administer node fields',
+ ];
+ $this->adminUser = $this->drupalCreateUser($permissions);
+ $this->adminUser2 = $this->drupalCreateUser($permissions);
+ $this->drupalLogin($this->adminUser);
+ }
+
+ /**
+ * Tests various operations via the Search API's admin UI.
+ */
+ public function testFramework() {
+ $this->createServer();
+ $this->createServerDuplicate();
+ $this->checkServerAvailability();
+ $this->createIndex();
+ $this->createIndexDuplicate();
+ $this->editServer();
+ $this->editIndex();
+ $this->checkUserIndexCreation();
+ $this->checkContentEntityTracking();
+
+ $this->enableAllProcessors();
+ $this->checkFieldLabels();
+
+ $this->addFieldsToIndex();
+ $this->checkDataTypesTable();
+ $this->removeFieldsFromIndex();
+ $this->checkReferenceFieldsNonBaseFields();
+
+ $this->configureFilter();
+ $this->configureFilterPage();
+ $this->checkProcessorChanges();
+ $this->changeProcessorFieldBoost();
+
+ $this->setReadOnly();
+ $this->disableEnableIndex();
+ $this->changeIndexDatasource();
+ $this->changeIndexServer();
+ $this->checkIndexing();
+ $this->checkIndexActions();
+
+ $this->deleteServer();
+ }
+
+ /**
+ * Tests what happens when an index has an integer as id/label.
+ *
+ * This needs to be in a separate test because we want to test the content
+ * tracking behavior as well as the fields / processors editing and adding
+ * without messing with the other index. This test also makes sure that the
+ * server also has an integer as id/label.
+ */
+ public function testIntegerIndex() {
+ Server::create([
+ 'id' => 456,
+ 'name' => 789,
+ 'description' => 'WebTest server' . ' description',
+ 'backend' => $this->serverBackend,
+ 'backend_config' => [],
+ ])->save();
+
+ $this->drupalCreateNode(['type' => 'article']);
+ $this->drupalCreateNode(['type' => 'article']);
+
+ $this->drupalGet('admin/config/search/search-api/add-index');
+
+ $this->indexId = 123;
+ $edit = [
+ 'name' => $this->indexId,
+ 'id' => $this->indexId,
+ 'status' => 1,
+ 'description' => 'test Index:: 123~',
+ 'server' => 456,
+ 'datasources[entity:node]' => TRUE,
+ ];
+ $this->submitForm($edit, 'Save');
+ $this->assertSession()->statusCodeEquals(200);
+ $this->assertSession()
+ ->pageTextContains('Please configure the used datasources.');
+ $this->submitForm([], 'Save');
+ $this->checkForMetaRefresh();
+ $this->assertSession()->statusCodeEquals(200);
+ $this->assertSession()
+ ->pageTextContains('The index was successfully saved.');
+ $this->assertEquals(2, $this->countTrackedItems());
+
+ $this->enableAllProcessors();
+ $this->checkFieldLabels();
+
+ $this->addFieldsToIndex();
+ $this->addFieldsWithDependenciesToIndex();
+ $this->removeFieldsDependencies();
+ $this->removeFieldsFromIndex();
+ $this->checkUnsavedChanges();
+
+ $this->configureFilter();
+ $this->configureFilterPage();
+ $this->checkProcessorChanges();
+ $this->changeProcessorFieldBoost();
+
+ $this->setReadOnly();
+ $this->disableEnableIndex();
+ $this->changeIndexDatasource();
+ $this->changeIndexServer();
+ $this->checkIndexing();
+ $this->checkIndexActions();
+ }
+
+ /**
+ * Tests creating a search server via the UI.
+ *
+ * @param string $server_id
+ * The ID of the server to create.
+ */
+ protected function createServer($server_id = '_test_server') {
+ $this->serverId = $server_id;
+ $server_name = 'Search API &{}<>! Server';
+ $server_description = 'A >server< used for testing &.';
+ $settings_path = 'admin/config/search/search-api/add-server';
+
+ $this->drupalGet($settings_path);
+ $this->assertSession()->statusCodeEquals(200);
+
+ $edit = [
+ 'name' => '',
+ 'status' => 1,
+ 'description' => 'A server used for testing.',
+ 'backend' => $this->serverBackend,
+ ];
+ $this->submitForm($edit, 'Save');
+ $this->assertSession()->pageTextContains(new FormattableMarkup('@name field is required.', ['@name' => 'Server name']));
+
+ $edit = [
+ 'name' => $server_name,
+ 'status' => 1,
+ 'description' => $server_description,
+ 'backend' => $this->serverBackend,
+ ];
+ $this->submitForm($edit, 'Save');
+ $this->assertSession()->pageTextContains(new FormattableMarkup('@name field is required.', ['@name' => 'Machine-readable name']));
+
+ $edit += [
+ 'id' => $this->serverId,
+ ];
+
+ $this->configureBackendAndSave($edit);
+
+ $this->assertSession()->pageTextContains('The server was successfully saved.');
+ $this->assertSession()->addressEquals('admin/config/search/search-api/server/' . $this->serverId);
+ $this->assertHtmlEscaped($server_name);
+ $this->assertHtmlEscaped($server_description);
+
+ $this->drupalGet('admin/config/search/search-api');
+ $this->assertHtmlEscaped($server_name);
+ $this->assertHtmlEscaped($server_description);
+ }
+
+ /**
+ * Lets derived backend integration tests fill their server create form.
+ *
+ * @param array $edit
+ * The common server form values so far.
+ */
+ protected function configureBackendAndSave(array $edit) {
+ // Nothing to configure here for the test backend.
+ $this->submitForm($edit, 'Save');
+ }
+
+ /**
+ * Tests creating a search server with an existing machine name.
+ */
+ protected function createServerDuplicate() {
+ $server_add_page = 'admin/config/search/search-api/add-server';
+ $this->drupalGet($server_add_page);
+
+ $edit = [
+ 'name' => $this->serverId,
+ 'id' => $this->serverId,
+ 'backend' => $this->serverBackend,
+ ];
+
+ // Try to submit an server with a duplicate machine name.
+ $this->submitForm($edit, 'Save');
+ $this->assertSession()->pageTextContains('The machine-readable name is already in use. It must be unique.');
+ }
+
+ /**
+ * Tests creating a search index via the UI.
+ */
+ protected function createIndex() {
+ $settings_path = 'admin/config/search/search-api/add-index';
+ $this->indexId = 'test_index';
+ $index_description = 'An >index< used for &! tęsting.';
+ $index_name = 'Search >API< test &!^* index';
+ $index_datasource = 'entity:node';
+
+ $this->drupalGet($settings_path);
+ $this->assertSession()->statusCodeEquals(200);
+
+ // Make sure datasource and tracker plugin descriptions are displayed.
+ $dummy_index = Index::create();
+ foreach (['createDatasourcePlugins', 'createTrackerPlugins'] as $method) {
+ /** @var \Drupal\search_api\Plugin\IndexPluginInterface[] $plugins */
+ $plugins = \Drupal::getContainer()
+ ->get('search_api.plugin_helper')
+ ->$method($dummy_index);
+ foreach ($plugins as $plugin) {
+ $description = strip_tags($plugin->getDescription());
+ $description = Html::decodeEntities($description);
+ $this->assertSession()->pageTextContains($description);
+ }
+ }
+
+ // Test form validation (required fields).
+ $edit = [
+ 'status' => 1,
+ 'description' => $index_description,
+ ];
+ $this->submitForm($edit, 'Save');
+ $this->assertSession()->pageTextContains('Index name field is required.');
+ $this->assertSession()->pageTextContains('Machine-readable name field is required.');
+ $this->assertSession()->pageTextContains('Data sources field is required.');
+
+ $edit = [
+ 'name' => $index_name,
+ 'id' => $this->indexId,
+ 'status' => 1,
+ 'description' => $index_description,
+ 'server' => $this->serverId,
+ 'datasources[' . $index_datasource . ']' => TRUE,
+ ];
+
+ $this->submitForm($edit, 'Save');
+ $this->assertSession()->pageTextContains('Please configure the used datasources.');
+
+ $this->submitForm([], 'Save');
+ $this->checkForMetaRefresh();
+ $this->assertSession()->pageTextContains('The index was successfully saved.');
+
+ $this->assertSession()->addressEquals($this->getIndexPath());
+ $this->assertHtmlEscaped($index_name);
+
+ $this->drupalGet($this->getIndexPath('edit'));
+ $this->assertHtmlEscaped($index_name);
+
+ $index = $this->getIndex(TRUE);
+
+ $this->assertTrue($index, 'Index was correctly created.');
+ $this->assertEquals($edit['name'], $index->label(), 'Name correctly inserted.');
+ $this->assertEquals($edit['id'], $index->id(), 'Index ID correctly inserted.');
+ $this->assertTrue($index->status(), 'Index status correctly inserted.');
+ $this->assertEquals($edit['description'], $index->getDescription(), 'Index ID correctly inserted.');
+ $this->assertEquals($edit['server'], $index->getServerId(), 'Index server ID correctly inserted.');
+ $this->assertEquals($index_datasource, $index->getDatasourceIds()[0], 'Index datasource id correctly inserted.');
+
+ // Test the "Save and add fields" button.
+ $index2_id = 'test_index2';
+ $edit['id'] = $index2_id;
+ unset($edit['server']);
+ $this->drupalGet($settings_path);
+ $this->submitForm($edit, 'Save and add fields');
+ $this->assertSession()->pageTextContains('Please configure the used datasources.');
+
+ $this->submitForm([], 'Save and add fields');
+ $this->assertSession()->pageTextContains('The index was successfully saved.');
+ $this->indexStorage->resetCache([$index2_id]);
+ $index = $this->indexStorage->load($index2_id);
+ $this->assertSession()->addressEquals($index->toUrl('add-fields'));
+
+ $this->drupalGet('admin/config/search/search-api');
+ $this->assertHtmlEscaped($index_name);
+ $this->assertHtmlEscaped($index_description);
+ }
+
+ /**
+ * Tests creating a search index with an existing machine name.
+ */
+ protected function createIndexDuplicate() {
+ $index_add_page = 'admin/config/search/search-api/add-index';
+ $this->drupalGet($index_add_page);
+
+ $edit = [
+ 'name' => $this->indexId,
+ 'id' => $this->indexId,
+ 'server' => $this->serverId,
+ 'datasources[entity:node]' => TRUE,
+ ];
+
+ // Try to submit an index with a duplicate machine name.
+ $this->submitForm($edit, 'Save');
+ $this->assertSession()->pageTextContains('The machine-readable name is already in use. It must be unique.');
+
+ // Try to submit an index with a duplicate machine name after form
+ // rebuilding via datasource submit.
+ $this->submitForm($edit, 'datasources_configure');
+ $this->submitForm($edit, 'Save');
+ $this->assertSession()->pageTextContains('The machine-readable name is already in use. It must be unique.');
+
+ // Try to submit an index with a duplicate machine name after form
+ // rebuilding via datasource submit using AJAX.
+ $this->submitForm($edit, 'datasources_configure');
+ $this->submitForm($edit, 'Save');
+ $this->assertSession()->pageTextContains('The machine-readable name is already in use. It must be unique.');
+ }
+
+ /**
+ * Tests whether editing a server works correctly.
+ */
+ protected function editServer() {
+ $path = 'admin/config/search/search-api/server/' . $this->serverId . '/edit';
+ $this->drupalGet($path);
+
+ // Check if it's possible to change the machine name.
+ $elements = $this->xpath('//form[@id="search-api-server-edit-form"]/div[contains(@class, "form-item-id")]/input[@disabled]');
+ $this->assertEquals(1, count($elements), 'Machine name cannot be changed.');
+
+ $tracked_items_before = $this->countTrackedItems();
+
+ $edit = [
+ 'name' => 'Test server',
+ ];
+ $this->submitForm($edit, 'Save');
+
+ /** @var \Drupal\search_api\IndexInterface $index */
+ $index = $this->indexStorage->load($this->indexId);
+ $remaining = $index->getTrackerInstance()->getRemainingItemsCount();
+ $this->assertEquals(0, $remaining, 'Index was not scheduled for re-indexing when saving its server.');
+
+ $this->setReturnValue('backend', 'postUpdate', TRUE);
+ $this->drupalGet($path);
+ $this->submitForm($edit, 'Save');
+
+ $tracked_items = $this->countTrackedItems();
+ $remaining = $index->getTrackerInstance()->getRemainingItemsCount();
+ $this->assertEquals($tracked_items, $remaining, 'Backend could trigger re-indexing upon save.');
+ $this->assertEquals($tracked_items_before, $tracked_items, 'Items are still tracked after re-indexing was triggered.');
+ }
+
+ /**
+ * Tests editing a search index via the UI.
+ */
+ protected function editIndex() {
+ $tracked_items = $this->countTrackedItems();
+ $edit_path = 'admin/config/search/search-api/index/' . $this->indexId . '/edit';
+ $this->drupalGet($edit_path);
+
+ // Check if it's possible to change the machine name.
+ $elements = $this->xpath('//form[@id="search-api-index-edit-form"]/div[contains(@class, "form-item-id")]/input[@disabled]');
+ $this->assertEquals(1, count($elements), 'Machine name cannot be changed.');
+
+ // Test the AJAX functionality for configuring the tracker.
+ $edit = ['tracker' => 'search_api_test'];
+ $this->submitForm($edit, 'tracker_configure');
+ $edit['tracker_config[foo]'] = 'foobar';
+ $this->submitForm($edit, 'Save');
+ $this->checkForMetaRefresh();
+ $this->assertSession()->statusCodeEquals(200);
+ $this->assertSession()->pageTextContains('The index was successfully saved.');
+
+ // Verify that everything was changed correctly.
+ $index = $this->getIndex(TRUE);
+ $tracker = $index->getTrackerInstance();
+ $this->assertTrue($tracker instanceof TestTracker, get_class($tracker));
+ $this->assertTrue($tracker instanceof TestTracker, 'Tracker was successfully switched.');
+ $configuration = [
+ 'foo' => 'foobar',
+ 'dependencies' => [],
+ ];
+ $this->assertEquals($configuration, $tracker->getConfiguration(), 'Tracker config was successfully saved.');
+ $this->assertEquals($tracked_items, $this->countTrackedItems(), 'Items are still correctly tracked.');
+
+ // Revert back to the default tracker for the rest of the test.
+ $this->drupalGet($edit_path);
+ $edit = ['tracker' => 'default'];
+ $this->submitForm($edit, 'tracker_configure');
+ $edit['tracker_config[indexing_order]'] = 'fifo';
+ $this->submitForm($edit, 'Save');
+ $this->checkForMetaRefresh();
+ $this->assertSession()->statusCodeEquals(200);
+ $this->assertSession()->pageTextContains('The index was successfully saved.');
+ $index = $this->getIndex(TRUE);
+ $tracker = $index->getTrackerInstance();
+ $this->assertTrue($tracker instanceof Basic, 'Tracker was successfully switched.');
+ }
+
+ /**
+ * Tests that an entity without bundles can be used as a data source.
+ */
+ protected function checkUserIndexCreation() {
+ $edit = [
+ 'name' => 'IndexName',
+ 'id' => 'user_index',
+ 'datasources[entity:user]' => TRUE,
+ ];
+
+ $this->drupalGet('admin/config/search/search-api/add-index');
+ $this->submitForm($edit, 'Save');
+ $this->assertSession()->statusCodeEquals(200);
+ $this->assertSession()->pageTextContains('Please configure the used datasources.');
+
+ $this->submitForm([], 'Save');
+ $this->assertSession()->statusCodeEquals(200);
+ $this->assertSession()->pageTextContains('The index was successfully saved.');
+ $this->assertSession()->pageTextContains($edit['name']);
+ }
+
+ /**
+ * Tests the server availability.
+ */
+ protected function checkServerAvailability() {
+ $this->drupalGet('admin/config/search/search-api/server/' . $this->serverId . '/edit');
+
+ $this->drupalGet('admin/config/search/search-api');
+ $this->assertSession()->statusCodeEquals(200);
+ $this->assertSession()->responseContains('Enabled');
+
+ $this->setReturnValue('backend', 'isAvailable', FALSE);
+ $this->drupalGet('admin/config/search/search-api');
+ $this->assertSession()->statusCodeEquals(200);
+ $this->assertSession()->responseContains('Unavailable');
+
+ $this->setReturnValue('backend', 'isAvailable', TRUE);
+ }
+
+ /**
+ * Tests whether the tracking information is properly maintained.
+ *
+ * Will especially test the bundle option of the content entity datasource.
+ */
+ protected function checkContentEntityTracking() {
+ // Initially there should be no tracked items, because there are no nodes.
+ $tracked_items = $this->countTrackedItems();
+ $this->assertEquals(0, $tracked_items, 'No items are tracked yet.');
+
+ // Add two articles and two pages (one of them "invisible" to Search API).
+ $article1 = $this->drupalCreateNode(['type' => 'article']);
+ $this->drupalCreateNode(['type' => 'article']);
+ $this->drupalCreateNode(['type' => 'page']);
+ $page2 = Node::create([
+ 'body' => [
+ [
+ 'value' => $this->randomMachineName(32),
+ 'format' => filter_default_format(),
+ ],
+ ],
+ 'title' => $this->randomMachineName(8),
+ 'type' => 'page',
+ 'uid' => \Drupal::currentUser()->id(),
+ ]);
+ $page2->search_api_skip_tracking = TRUE;
+ $page2->save();
+
+ // The 3 new nodes without "search_api_skip_tracking" property set should
+ // have been added to the tracking table immediately.
+ $tracked_items = $this->countTrackedItems();
+ $this->assertEquals(3, $tracked_items, 'Three items are tracked.');
+
+ $this->getCalledMethods('backend');
+ $page2->delete();
+ $methods = $this->getCalledMethods('backend');
+ $this->assertEquals([], $methods, 'Tracking of a delete operation could successfully be prevented.');
+
+ // Test disabling the index.
+ $settings_path = $this->getIndexPath('edit');
+ $this->drupalGet($settings_path);
+ $edit = [
+ 'status' => FALSE,
+ 'datasource_configs[entity:node][bundles][default]' => 0,
+ 'datasource_configs[entity:node][bundles][selected][article]' => FALSE,
+ 'datasource_configs[entity:node][bundles][selected][page]' => FALSE,
+ ];
+ $this->submitForm($edit, 'Save');
+ $this->assertSession()->pageTextContains('The index was successfully saved.');
+
+ $tracked_items = $this->countTrackedItems();
+ $this->assertEquals(0, $tracked_items, 'No items are tracked.');
+
+ // Test re-enabling the index.
+ $this->drupalGet($settings_path);
+
+ $edit = [
+ 'status' => TRUE,
+ 'datasource_configs[entity:node][bundles][default]' => 0,
+ 'datasource_configs[entity:node][bundles][selected][article]' => TRUE,
+ 'datasource_configs[entity:node][bundles][selected][page]' => TRUE,
+ ];
+ $this->submitForm($edit, 'Save');
+ $this->checkForMetaRefresh();
+ $this->assertSession()->pageTextContains('The index was successfully saved.');
+
+ $tracked_items = $this->countTrackedItems();
+ $this->assertEquals(3, $tracked_items, 'Three items are tracked.');
+
+ // Uncheck "default" and don't select any bundles. This should remove all
+ // items from the tracking table.
+ $edit = [
+ 'status' => TRUE,
+ 'datasource_configs[entity:node][bundles][default]' => 0,
+ 'datasource_configs[entity:node][bundles][selected][article]' => FALSE,
+ 'datasource_configs[entity:node][bundles][selected][page]' => FALSE,
+ ];
+ $this->drupalGet($settings_path);
+ $this->submitForm($edit, 'Save');
+ $this->checkForMetaRefresh();
+ $this->assertSession()->pageTextContains('The index was successfully saved.');
+
+ $tracked_items = $this->countTrackedItems();
+ $this->assertEquals(0, $tracked_items, 'No items are tracked.');
+
+ // Leave "default" unchecked and select the "article" bundle. This should
+ // re-add the two articles to the tracking table.
+ $edit = [
+ 'status' => TRUE,
+ 'datasource_configs[entity:node][bundles][default]' => 0,
+ 'datasource_configs[entity:node][bundles][selected][article]' => TRUE,
+ 'datasource_configs[entity:node][bundles][selected][page]' => FALSE,
+ ];
+ $this->drupalGet($settings_path);
+ $this->submitForm($edit, 'Save');
+ $this->checkForMetaRefresh();
+ $this->assertSession()->pageTextContains('The index was successfully saved.');
+
+ $tracked_items = $this->countTrackedItems();
+ $this->assertEquals(2, $tracked_items, 'Two items are tracked.');
+
+ // Leave "default" unchecked and select only the "page" bundle. This should
+ // result in only the page being present in the tracking table.
+ $edit = [
+ 'status' => TRUE,
+ 'datasource_configs[entity:node][bundles][default]' => 0,
+ 'datasource_configs[entity:node][bundles][selected][article]' => FALSE,
+ 'datasource_configs[entity:node][bundles][selected][page]' => TRUE,
+ ];
+ $this->drupalGet($settings_path);
+ $this->submitForm($edit, 'Save');
+ $this->checkForMetaRefresh();
+ $this->assertSession()->pageTextContains('The index was successfully saved.');
+
+ $tracked_items = $this->countTrackedItems();
+ $this->assertEquals(1, $tracked_items, 'One item is tracked.');
+
+ // Check "default" again and select the "article" bundle. This shouldn't
+ // change the tracking table, which should still only contain the page.
+ $edit = [
+ 'status' => TRUE,
+ 'datasource_configs[entity:node][bundles][default]' => 1,
+ 'datasource_configs[entity:node][bundles][selected][article]' => TRUE,
+ 'datasource_configs[entity:node][bundles][selected][page]' => FALSE,
+ ];
+ $this->drupalGet($settings_path);
+ $this->submitForm($edit, 'Save');
+ $this->checkForMetaRefresh();
+ $this->assertSession()->pageTextContains('The index was successfully saved.');
+
+ $tracked_items = $this->countTrackedItems();
+ $this->assertEquals(1, $tracked_items, 'One item is tracked.');
+
+ // Leave "default" checked but now select only the "page" bundle. This
+ // should result in only the articles being tracked.
+ $edit = [
+ 'status' => TRUE,
+ 'datasource_configs[entity:node][bundles][default]' => 1,
+ 'datasource_configs[entity:node][bundles][selected][article]' => FALSE,
+ 'datasource_configs[entity:node][bundles][selected][page]' => TRUE,
+ ];
+ $this->drupalGet($settings_path);
+ $this->submitForm($edit, 'Save');
+ $this->checkForMetaRefresh();
+ $this->assertSession()->pageTextContains('The index was successfully saved.');
+
+ $tracked_items = $this->countTrackedItems();
+ $this->assertEquals(2, $tracked_items, 'Two items are tracked.');
+
+ // Index items, then check whether updating an article is handled correctly.
+ $this->triggerPostRequestIndexing();
+ $this->getCalledMethods('backend');
+ $article1->save();
+ $methods = $this->getCalledMethods('backend');
+ $this->assertEquals([], $methods, 'No items were indexed right away (before end of page request).');
+ $this->triggerPostRequestIndexing();
+ $methods = $this->getCalledMethods('backend');
+ $this->assertEquals(['indexItems'], $methods, 'Update successfully tracked.');
+
+ $article1->search_api_skip_tracking = TRUE;
+ $article1->save();
+ $methods = $this->getCalledMethods('backend');
+ $this->assertEquals([], $methods, 'Tracking of entity update successfully prevented.');
+ unset($article1->search_api_skip_tracking);
+
+ // Delete an article. That should remove it from the item table.
+ $article1->delete();
+
+ $tracked_items = $this->countTrackedItems();
+ $this->assertEquals(1, $tracked_items, 'One item is tracked.');
+ }
+
+ /**
+ * Counts the number of tracked items in the test index.
+ *
+ * @return int
+ * The number of tracked items in the test index.
+ */
+ protected function countTrackedItems() {
+ return $this->getIndex()->getTrackerInstance()->getTotalItemsCount();
+ }
+
+ /**
+ * Counts the number of unindexed items in the test index.
+ *
+ * @return int
+ * The number of unindexed items in the test index.
+ */
+ protected function countRemainingItems() {
+ return $this->getIndex()->getTrackerInstance()->getRemainingItemsCount();
+ }
+
+ /**
+ * Counts the number of items indexed on the server for the test index.
+ *
+ * @return int
+ * The number of items indexed on the server for the test index.
+ */
+ protected function countItemsOnServer() {
+ $key = 'search_api_test.backend.indexed.' . $this->indexId;
+ return count(\Drupal::state()->get($key, []));
+ }
+
+ /**
+ * Enables all processors.
+ */
+ public function enableAllProcessors() {
+ $this->drupalGet($this->getIndexPath('processors'));
+
+ $edit = [
+ 'status[content_access]' => 1,
+ 'status[entity_status]' => 1,
+ 'status[highlight]' => 1,
+ 'status[html_filter]' => 1,
+ 'status[ignorecase]' => 1,
+ 'status[ignore_character]' => 1,
+ 'status[stopwords]' => 1,
+ 'status[tokenizer]' => 1,
+ 'status[transliteration]' => 1,
+ ];
+ $this->submitForm($edit, 'Save');
+ $this->assertSession()->pageTextContains('The indexing workflow was successfully edited.');
+ }
+
+ /**
+ * Tests that field labels are always properly escaped.
+ */
+ protected function checkFieldLabels() {
+ $content_type_name = '&%@Content()_=';
+
+ // Add a new content type with funky chars.
+ $edit = [
+ 'name' => $content_type_name,
+ 'type' => '_content_',
+ ];
+ $this->drupalGet('admin/structure/types/add');
+ $this->assertSession()->statusCodeEquals(200);
+ $this->submitForm($edit, 'Save and manage fields');
+
+ // Add a field to that content type with funky chars.
+ $field_name = '^6%{[*>.<"field';
+ FieldStorageConfig::create([
+ 'field_name' => 'field__field_',
+ 'type' => 'string',
+ 'entity_type' => 'node',
+ ])->save();
+ FieldConfig::create([
+ 'field_name' => 'field__field_',
+ 'entity_type' => 'node',
+ 'bundle' => '_content_',
+ 'label' => $field_name,
+ ])->save();
+
+ $url_options['query']['datasource'] = 'entity:node';
+ $this->drupalGet($this->getIndexPath('fields/add/nojs'), $url_options);
+ $this->assertHtmlEscaped($field_name);
+ $this->assertSession()->responseContains('(field__field_)');
+
+ $this->addField('entity:node', 'field__field_', $field_name);
+
+ $this->drupalGet($this->getIndexPath('fields'));
+ $this->assertHtmlEscaped($field_name);
+
+ $edit = [
+ 'datasource_configs[entity:node][bundles][default]' => 1,
+ ];
+ $this->drupalGet($this->getIndexPath('edit'));
+ $this->assertHtmlEscaped($content_type_name);
+ $this->submitForm($edit, 'Save');
+ $this->assertSession()->pageTextContains('The index was successfully saved.');
+
+ $this->addField(NULL, 'rendered_item', 'Rendered HTML output');
+ $this->assertHtmlEscaped($content_type_name);
+ $this->submitForm([], 'Save');
+ $this->assertSession()->pageTextContains(' The field configuration was successfully saved.');
+
+ $this->addField(NULL, 'aggregated_field', 'Aggregated field');
+ $this->assertHtmlEscaped($field_name);
+ $this->submitForm(['fields[entity:node/field__field_]' => TRUE], 'Save');
+ $this->assertSession()->pageTextContains(' The field configuration was successfully saved.');
+ }
+
+ /**
+ * Tests whether adding fields to the index works correctly.
+ */
+ protected function addFieldsToIndex() {
+ // Make sure that hidden properties are not displayed.
+ $url_options['query']['datasource'] = '';
+ $this->drupalGet($this->getIndexPath('fields/add/nojs'), $url_options);
+ $this->assertSession()->pageTextNotContains('Node access information');
+
+ $fields = [
+ 'nid' => 'ID',
+ 'title' => 'Title',
+ 'body' => 'Body',
+ 'revision_log' => 'Revision log message',
+ 'uid:entity:name' => 'Authored by » User » Name',
+ ];
+ foreach ($fields as $property_path => $label) {
+ $this->addField('entity:node', $property_path, $label);
+ }
+
+ $index = $this->getIndex(TRUE);
+ $fields = $index->getFields();
+
+ $this->assertTrue(empty($fields['nid']), 'Field changes have not been persisted.');
+ $this->drupalGet($this->getIndexPath('fields'));
+ $this->submitForm([], 'Save changes');
+ $this->assertSession()->pageTextContains('The changes were successfully saved.');
+
+ $index = $this->getIndex(TRUE);
+ $fields = $index->getFields();
+
+ $this->assertArrayHasKey('nid', $fields, 'nid field is indexed.');
+
+ // Ensure that we aren't offered to index properties of the "Content type"
+ // property.
+ $path = $this->getIndexPath('fields/add/nojs');
+ $url_options = ['query' => ['datasource' => 'entity:node']];
+ $this->drupalGet($path, $url_options);
+ $this->assertSession()->responseNotContains('property_path=type');
+
+ // The "Content access" processor correctly marked fields as locked.
+ $this->assertArrayHasKey('uid', $fields, 'uid field is indexed.');
+ $this->assertTrue($fields['uid']->isIndexedLocked(), 'uid field is locked.');
+ $this->assertTrue($fields['uid']->isTypeLocked(), 'uid field is type-locked.');
+ $this->assertEquals('integer', $fields['uid']->getType(), 'uid field has type integer.');
+ $this->assertArrayHasKey('status', $fields, 'status field is indexed.');
+ $this->assertTrue($fields['status']->isIndexedLocked(), 'status field is locked.');
+ $this->assertTrue($fields['status']->isTypeLocked(), 'status field is type-locked.');
+ $this->assertEquals('boolean', $fields['status']->getType(), 'status field has type boolean.');
+
+ // Check that a 'parent_data_type.data_type' Search API field type => data
+ // type mapping relationship works.
+ $this->assertArrayHasKey('body', $fields, 'body field is indexed.');
+ $this->assertEquals('text', $fields['body']->getType(), 'Complex field mapping relationship works.');
+
+ // Test renaming of fields.
+ $edit = [
+ 'fields[title][title]' => 'new_title',
+ 'fields[title][id]' => 'new_id',
+ 'fields[title][type]' => 'text',
+ 'fields[title][boost]' => '21.0',
+ 'fields[revision_log][type]' => 'search_api_test',
+ ];
+ $this->drupalGet($this->getIndexPath('fields'));
+ $this->submitForm($edit, 'Save changes');
+ $this->assertSession()->pageTextContains('The changes were successfully saved.');
+
+ $index = $this->getIndex(TRUE);
+ $fields = $index->getFields();
+
+ $this->assertArrayHasKey('new_id', $fields, 'title field is indexed.');
+ $this->assertEquals($edit['fields[title][title]'], $fields['new_id']->getLabel(), 'title field title is saved.');
+ $this->assertEquals($edit['fields[title][id]'], $fields['new_id']->getFieldIdentifier(), 'title field id value is saved.');
+ $this->assertEquals($edit['fields[title][type]'], $fields['new_id']->getType(), 'title field type is text.');
+ $this->assertEquals($edit['fields[title][boost]'], $fields['new_id']->getBoost(), 'title field boost value is 21.');
+
+ $this->assertArrayHasKey('revision_log', $fields, 'revision_log field is indexed.');
+ $this->assertEquals($edit['fields[revision_log][type]'], $fields['revision_log']->getType(), 'revision_log field type is search_api_test.');
+
+ // Reset field values to original.
+ $edit = [
+ 'fields[new_id][title]' => 'Title',
+ 'fields[new_id][id]' => 'title',
+ ];
+ $this->drupalGet($this->getIndexPath('fields'));
+ $this->submitForm($edit, 'Save changes');
+ $this->assertSession()->pageTextContains('The changes were successfully saved.');
+
+ // Make sure that property paths are correctly displayed.
+ $this->assertSession()->pageTextContains('uid:entity:name');
+ }
+
+ /**
+ * Tests if the data types table is available and contains correct values.
+ */
+ protected function checkDataTypesTable() {
+ $this->drupalGet($this->getIndexPath('fields'));
+ $rows = $this->xpath('//*[@id="search-api-data-types-table"]/*/table/tbody/tr');
+ $this->assertTrue(is_array($rows) && !empty($rows), 'Found a datatype listing.');
+
+ /** @var \Behat\Mink\Element\NodeElement $row */
+ foreach ($rows as $row) {
+ $columns = $row->findAll('xpath', '/td');
+ $label = $columns[0]->getText();
+ $icon = basename($columns[2]->find('xpath', '/img')->getAttribute('src'));
+ $fallback = $columns[3]->getText();
+
+ // Make sure we display the right icon and fallback column.
+ if (strpos($label, 'Unsupported') === 0) {
+ $this->assertEquals('error.svg', $icon, 'An error icon is shown for unsupported data types.');
+ $this->assertNotEquals($fallback, '', 'The fallback data type label is not empty for unsupported data types.');
+ }
+ else {
+ $this->assertEquals('check.svg', $icon, 'A check icon is shown for supported data types.');
+ $this->assertEquals('', $fallback, 'The fallback data type label is empty for supported data types.');
+ }
+ }
+ }
+
+ /**
+ * Adds a field for a specific property to the index.
+ *
+ * @param string|null $datasource_id
+ * The property's datasource's ID, or NULL if it is a datasource-independent
+ * property.
+ * @param string $property_path
+ * The property path.
+ * @param string|null $label
+ * (optional) If given, the label to check for in the success message.
+ */
+ protected function addField($datasource_id, $property_path, $label = NULL) {
+ $path = $this->getIndexPath('fields/add/nojs');
+ $url_options = ['query' => ['datasource' => $datasource_id]];
+ list($parent_path) = Utility::splitPropertyPath($property_path);
+ if ($parent_path) {
+ $url_options['query']['property_path'] = $parent_path;
+ }
+ if ($this->getUrl() !== $this->buildUrl($path, $url_options)) {
+ $this->drupalGet($path, $url_options);
+ }
+
+ // Unfortunately it doesn't seem possible to specify the clicked button by
+ // anything other than label, so we have to pass it as extra POST data.
+ $combined_property_path = Utility::createCombinedId($datasource_id, $property_path);
+ $this->assertSession()->responseContains('name="' . $combined_property_path . '"');
+ $this->submitForm([], $combined_property_path);
+ if ($label) {
+ $args['%label'] = $label;
+ $this->assertSession()->responseContains(new FormattableMarkup('Field %label was added to the index.', $args));
+ }
+ }
+
+ /**
+ * Tests field dependencies.
+ */
+ protected function addFieldsWithDependenciesToIndex() {
+ // Add a new link field.
+ FieldStorageConfig::create([
+ 'field_name' => 'field_link',
+ 'type' => 'link',
+ 'entity_type' => 'node',
+ ])->save();
+ FieldConfig::create([
+ 'field_name' => 'field_link',
+ 'entity_type' => 'node',
+ 'bundle' => 'article',
+ 'label' => 'Link',
+ ])->save();
+
+ // Add a new image field, for both articles and basic pages.
+ FieldStorageConfig::create([
+ 'field_name' => 'field_image',
+ 'type' => 'image',
+ 'entity_type' => 'node',
+ ])->save();
+ FieldConfig::create([
+ 'field_name' => 'field_image',
+ 'entity_type' => 'node',
+ 'bundle' => 'article',
+ 'label' => 'Image',
+ ])->save();
+ FieldConfig::create([
+ 'field_name' => 'field_image',
+ 'entity_type' => 'node',
+ 'bundle' => 'page',
+ 'label' => 'Image',
+ ])->save();
+
+ $fields = [
+ 'field_link' => 'Link',
+ 'field_image' => 'Image',
+ ];
+ foreach ($fields as $property_path => $label) {
+ $this->addField('entity:node', $property_path, $label);
+ }
+ $this->drupalGet($this->getIndexPath('fields'));
+ $this->submitForm([], 'Save changes');
+
+ // Check that index configuration is updated with dependencies.
+ $field_dependencies = (array) \Drupal::config('search_api.index.' . $this->indexId)->get('dependencies.config');
+ $this->assertTrue(in_array('field.storage.node.field_link', $field_dependencies), 'The link field has been added as a dependency of the index.');
+ $this->assertTrue(in_array('field.storage.node.field_image', $field_dependencies), 'The image field has been added as a dependency of the index.');
+ }
+
+ /**
+ * Tests whether removing fields on which the index depends works correctly.
+ */
+ protected function removeFieldsDependencies() {
+ // Remove a field and make sure that doing so does not remove the search
+ // index.
+ $this->drupalGet('admin/structure/types/manage/article/fields/node.article.field_link/delete');
+ $this->assertSession()->pageTextNotContains('The listed configuration will be deleted.');
+ $this->assertSession()->pageTextContains('Search index');
+
+ $this->submitForm([], 'Delete');
+ $this->drupalGet('admin/structure/types/manage/article/fields/node.article.field_image/delete');
+ $this->submitForm([], 'Delete');
+
+ $this->assertNotNull($this->getIndex(), 'Index was not deleted.');
+
+ $this->drupalGet($this->getIndexPath('fields'));
+ $this->assertSession()->statusCodeEquals(200);
+ $this->assertSession()->pageTextNotContains('field_link');
+ $this->assertSession()->fieldExists('fields[field_image][id]');
+ $this->assertSession()
+ ->fieldValueEquals('fields[field_image][id]', 'field_image');
+
+ $field_dependencies = \Drupal::config('search_api.index.' . $this->indexId)->get('dependencies.config');
+ $this->assertFalse(in_array('field.storage.node.field_link', (array) $field_dependencies), "The link field has been removed from the index's dependencies.");
+ $this->assertTrue(in_array('field.storage.node.field_image', (array) $field_dependencies), "The image field has been removed from the index's dependencies.");
+ }
+
+ /**
+ * Tests whether removing fields from the index works correctly.
+ */
+ protected function removeFieldsFromIndex() {
+ // Find the "Remove" link for the "body" field.
+ $links = $this->xpath('//a[@data-drupal-selector=:id]', [':id' => 'edit-fields-body-remove']);
+ $this->assertNotEmpty($links, 'Found "Remove" link for body field');
+ $this->assertInternalType('array', $links);
+ $url_target = $this->getAbsoluteUrl($links[0]->getAttribute('href'));
+ $this->drupalGet($url_target);
+ $this->drupalGet($this->getIndexPath('fields'));
+ $this->submitForm([], 'Save changes');
+
+ $index = $this->getIndex(TRUE);
+ $fields = $index->getFields();
+ $this->assertTrue(!isset($fields['body']), 'The body field has been removed from the index.');
+ }
+
+ /**
+ * Tests whether unsaved fields changes work correctly.
+ */
+ protected function checkUnsavedChanges() {
+ $this->addField('entity:node', 'changed', 'Changed');
+ $this->drupalGet($this->getIndexPath('fields'));
+ $this->assertSession()->pageTextContains('You have unsaved changes.');
+
+ // Log in a different admin user.
+ $this->drupalLogin($this->adminUser2);
+
+ // Construct the message that should be displayed.
+ $username = [
+ '#theme' => 'username',
+ '#account' => $this->adminUser,
+ ];
+ $args = [
+ '@user' => \Drupal::getContainer()->get('renderer')->renderPlain($username),
+ ':url' => $this->getIndex()->toUrl('break-lock-form')->toString(),
+ ];
+ $message = (string) new FormattableMarkup('This index is being edited by user @user, and is therefore locked from editing by others. This lock is @age old. Click here to break this lock.', $args);
+ // Since we can't predict the age that will be shown, just check for
+ // everything else.
+ $message_parts = explode('@age', $message);
+
+ $this->drupalGet($this->getIndexPath('fields/add/nojs'));
+ $this->assertSession()->responseContains($message_parts[0]);
+ $this->assertSession()->responseContains($message_parts[1]);
+ $this->assertFalse($this->xpath('//input[not(@disabled)]'));
+ $this->drupalGet($this->getIndexPath('fields/edit/rendered_item'));
+ $this->assertSession()->responseContains($message_parts[0]);
+ $this->assertSession()->responseContains($message_parts[1]);
+ $this->assertFalse($this->xpath('//input[not(@disabled)]'));
+ $this->drupalGet($this->getIndexPath('fields'));
+ $this->assertSession()->responseContains($message_parts[0]);
+ $this->assertSession()->responseContains($message_parts[1]);
+ $this->assertFalse($this->xpath('//input[not(@disabled)]'));
+ $match_result = preg_match('#fields/break-lock">([^<>]*?)#', $message, $m);
+ $this->assertTrue($match_result);
+ $this->clickLink($m[1]);
+
+ $this->assertSession()->responseContains(new FormattableMarkup('By breaking this lock, any unsaved changes made by @user will be lost.', $args));
+ $this->submitForm([], 'Break lock');
+ $this->assertSession()->pageTextContains('The lock has been broken. You may now edit this search index.');
+ // Make sure the field has not been added to the index.
+ $index = $this->getIndex(TRUE);
+ $fields = $index->getFields();
+ $this->assertTrue(!isset($fields['changed']), 'The changed field has not been added to the index.');
+
+ // Find the "Remove" link for the "title" field.
+ $links = $this->xpath('//a[@data-drupal-selector=:id]', [':id' => 'edit-fields-title-remove']);
+ $this->assertNotEmpty($links, 'Found "Remove" link for title field');
+ $this->assertInternalType('array', $links);
+ $url_target = $this->getAbsoluteUrl($links[0]->getAttribute('href'));
+ $this->drupalGet($url_target);
+
+ $this->assertSession()->pageTextContains('You have unsaved changes.');
+ $this->submitForm([], 'Cancel');
+
+ $this->assertArrayHasKey('title', $fields, 'The title field has not been removed from the index.');
+ }
+
+ /**
+ * Tests if non-base fields of referenced entities can be added.
+ */
+ protected function checkReferenceFieldsNonBaseFields() {
+ // Add a new entity_reference field.
+ $field_label = 'reference_field';
+ FieldStorageConfig::create([
+ 'field_name' => 'field__reference_field_',
+ 'type' => 'entity_reference',
+ 'entity_type' => 'node',
+ 'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED,
+ 'settings' => [
+ 'allowed_values' => [
+ [
+ 'target_type' => 'node',
+ ],
+ ],
+ ],
+ ])->save();
+ FieldConfig::create([
+ 'field_name' => 'field__reference_field_',
+ 'entity_type' => 'node',
+ 'bundle' => 'article',
+ 'label' => $field_label,
+ ])->save();
+ EntityFormDisplay::load('node.article.default')
+ ->setComponent('field__reference_field_', [
+ 'type' => 'entity_reference_autocomplete',
+ ])
+ ->save();
+
+ $node_label = $this->getIndex()->getDatasource('entity:node')->label();
+ $field_label = "$field_label » $node_label » $field_label";
+ $this->addField('entity:node', 'field__reference_field_:entity:field__reference_field_', $field_label);
+ $this->drupalGet($this->getIndexPath('fields'));
+ $this->submitForm([], 'Save changes');
+
+ $this->drupalGet('node/2/edit');
+ $edit = ['field__reference_field_[0][target_id]' => 'Something (2)'];
+ $this->drupalGet('node/2/edit');
+ $this->submitForm($edit, 'Save');
+ $indexed_values = \Drupal::state()->get("search_api_test.backend.indexed.{$this->indexId}", []);
+ $this->assertEquals([2], $indexed_values['entity:node/2:en']['field__reference_field_'], 'Correct value indexed for nested non-base field.');
+ }
+
+ /**
+ * Tests that configuring a processor works.
+ */
+ protected function configureFilter() {
+ $edit = [
+ 'status[ignorecase]' => 1,
+ 'processors[ignorecase][settings][fields][title]' => 'title',
+ 'processors[ignorecase][settings][fields][field__field_]' => FALSE,
+ ];
+ $this->drupalGet($this->getIndexPath('processors'));
+ $this->submitForm($edit, 'Save');
+ $index = $this->getIndex(TRUE);
+ try {
+ $configuration = $index->getProcessor('ignorecase')->getConfiguration();
+ unset($configuration['weights']);
+ $expected = [
+ 'fields' => [
+ 'title',
+ ],
+ 'all_fields' => FALSE,
+ ];
+ $this->assertEquals($expected, $configuration, 'Title field enabled for ignore case filter.');
+ }
+ catch (SearchApiException $e) {
+ $this->fail('"Ignore case" processor not enabled.');
+ }
+ $this->assertSession()
+ ->pageTextContains('The indexing workflow was successfully edited.');
+ }
+
+ /**
+ * Tests that the "no values changed" message on the "Processors" tab works.
+ */
+ public function configureFilterPage() {
+ $this->drupalGet($this->getIndexPath('processors'));
+ $this->submitForm([], 'Save');
+ $this->assertSession()->pageTextContains('No values were changed.');
+ }
+
+ /**
+ * Tests that changing or a processor doesn't always trigger reindexing.
+ */
+ protected function checkProcessorChanges() {
+ $edit = [
+ 'status[ignorecase]' => 1,
+ 'processors[ignorecase][settings][fields][title]' => 'title',
+ ];
+ // Enable just the ignore case processor, just to have a clean default state
+ // before testing.
+ $this->drupalGet($this->getIndexPath('processors'));
+ $this->submitForm($edit, 'Save');
+ $this->assertSession()->statusCodeEquals(200);
+
+ $this->submitForm($edit, 'Save');
+ $this->assertSession()->statusCodeEquals(200);
+ $this->assertSession()->pageTextContains('No values were changed.');
+ $this->assertSession()->pageTextNotContains('All content was scheduled for reindexing so the new settings can take effect.');
+
+ $edit['processors[ignorecase][settings][fields][title]'] = FALSE;
+ $this->submitForm($edit, 'Save');
+ $this->assertSession()->statusCodeEquals(200);
+ $this->assertSession()->pageTextContains('All content was scheduled for reindexing so the new settings can take effect.');
+ }
+
+ /**
+ * Tests that a field added by a processor can be changed.
+ *
+ * For most fields added by processors, such as the "URL field" processor,
+ * only be the "Indexed" checkbox should be locked, not type and boost. This
+ * method verifies this.
+ */
+ protected function changeProcessorFieldBoost() {
+ // Add the URL field.
+ $this->addField(NULL, 'search_api_url', 'URI');
+
+ // Change the boost of the field.
+ $fields_path = $this->getIndexPath('fields');
+ $this->drupalGet($fields_path);
+ $this->submitForm(['fields[url][boost]' => '8.0'], 'Save changes');
+ $this->assertSession()->pageTextContains('The changes were successfully saved.');
+ $option_field = $this->assertSession()
+ ->optionExists('edit-fields-url-boost', '8.0');
+ $this->assertTrue($option_field->hasAttribute('selected'), 'Boost is correctly saved.');
+
+ // Change the type of the field.
+ $this->drupalGet($fields_path);
+ $this->submitForm(['fields[url][type]' => 'text'], 'Save changes');
+ $this->assertSession()->pageTextContains('The changes were successfully saved.');
+ $option_field = $this->assertSession()
+ ->optionExists('edit-fields-url-type', 'text');
+ $this->assertTrue($option_field->hasAttribute('selected'), 'Type is correctly saved.');
+ }
+
+ /**
+ * Sets an index to "read only" and checks if it reacts correctly.
+ *
+ * The expected behavior is that, when an index is set to "read only", it
+ * keeps tracking but won't index any items.
+ */
+ protected function setReadOnly() {
+ $index = $this->getIndex(TRUE);
+ $index->reindex();
+
+ $index_path = $this->getIndexPath();
+ $settings_path = $index_path . '/edit';
+
+ // Re-enable tracking of all bundles. After this there should be two
+ // unindexed items tracked by the index.
+ $edit = [
+ 'status' => TRUE,
+ 'read_only' => TRUE,
+ 'datasource_configs[entity:node][bundles][default]' => 0,
+ 'datasource_configs[entity:node][bundles][selected][article]' => TRUE,
+ 'datasource_configs[entity:node][bundles][selected][page]' => TRUE,
+ ];
+ $this->drupalGet($settings_path);
+ $this->submitForm($edit, 'Save');
+ $this->checkForMetaRefresh();
+ $this->assertSession()->pageTextContains('The index was successfully saved.');
+
+ $index = $this->getIndex(TRUE);
+ $remaining_before = $this->countRemainingItems();
+
+ $this->drupalGet($index_path);
+
+ $this->assertSession()->pageTextNotContains('Index now');
+
+ // Also try indexing via the API to make sure it is really not possible.
+ $indexed = $this->indexItems();
+ $this->assertEquals(0, $indexed, 'No items were indexed after setting the index to "read only".');
+ $remaining_after = $this->countRemainingItems();
+ $this->assertEquals($remaining_before, $remaining_after, 'No items were indexed after setting the index to "read only".');
+
+ // Disable "read only" and verify indexing now works again.
+ $edit = [
+ 'read_only' => FALSE,
+ 'datasource_configs[entity:node][bundles][default]' => 1,
+ 'datasource_configs[entity:node][bundles][selected][article]' => FALSE,
+ 'datasource_configs[entity:node][bundles][selected][page]' => FALSE,
+ ];
+ $this->drupalGet($settings_path);
+ $this->submitForm($edit, 'Save');
+ $this->checkForMetaRefresh();
+ $this->assertSession()->pageTextContains('The index was successfully saved.');
+
+ $this->drupalGet($index_path);
+ $this->submitForm([], 'Index now');
+ $this->checkForMetaRefresh();
+
+ $remaining_after = $index->getTrackerInstance()->getRemainingItemsCount();
+ $this->assertEquals(0, $remaining_after, 'Items were indexed after removing the "read only" flag.');
+ }
+
+ /**
+ * Disables and enables an index and checks if it reacts correctly.
+ *
+ * The expected behavior is that, when an index is disabled, all its items
+ * are removed from both the tracker and the server.
+ *
+ * When it is enabled again, the items are re-added to the tracker.
+ */
+ protected function disableEnableIndex() {
+ // Disable the index and check that no items are tracked.
+ $settings_path = $this->getIndexPath('edit');
+ $edit = [
+ 'status' => FALSE,
+ ];
+ $this->drupalGet($settings_path);
+ $this->submitForm($edit, 'Save');
+ $this->assertSession()->pageTextContains('The index was successfully saved.');
+
+ $tracked_items = $this->countTrackedItems();
+ $this->assertEquals(0, $tracked_items, 'No items are tracked after disabling the index.');
+ $tracked_items = \Drupal::database()->select('search_api_item', 'i')->countQuery()->execute()->fetchField();
+ $this->assertEquals(0, $tracked_items, 'No items left in tracking table.');
+
+ // @todo Also try to verify whether the items got deleted from the server.
+
+ // Re-enable the index and check that the items are tracked again.
+ $edit = [
+ 'status' => TRUE,
+ ];
+ $this->drupalGet($settings_path);
+ $this->submitForm($edit, 'Save');
+ $this->checkForMetaRefresh();
+ $this->assertSession()->pageTextContains('The index was successfully saved.');
+
+ $tracked_items = $this->countTrackedItems();
+ $this->assertEquals(2, $tracked_items, 'After enabling the index, 2 items are tracked.');
+ }
+
+ /**
+ * Changes the index's datasources and checks if it reacts correctly.
+ *
+ * The expected behavior is that, when an index's datasources are changed, the
+ * tracker should remove all items from the datasources it no longer needs to
+ * handle and add the new ones.
+ */
+ protected function changeIndexDatasource() {
+ $index = $this->getIndex(TRUE);
+ $index->reindex();
+
+ $user_count = \Drupal::entityQuery('user')->count()->execute();
+ $node_count = \Drupal::entityQuery('node')->count()->execute();
+
+ // Enable indexing of users.
+ $settings_path = $this->getIndexPath('edit');
+ $edit = [
+ 'datasources[entity:user]' => TRUE,
+ 'datasources[entity:node]' => TRUE,
+ ];
+ $this->drupalGet($settings_path);
+ $this->submitForm($edit, 'Save');
+ $this->assertSession()->pageTextContains('Please configure the used datasources.');
+ $this->submitForm([], 'Save');
+ $this->checkForMetaRefresh();
+ $this->assertSession()->pageTextContains('The index was successfully saved.');
+
+ $tracked_items = $this->countTrackedItems();
+ $this->assertEquals($user_count + $node_count, $tracked_items, 'Correct number of items tracked after enabling the "User" datasource.');
+
+ // Disable indexing of users again.
+ $edit = [
+ 'datasources[entity:user]' => FALSE,
+ 'datasources[entity:node]' => TRUE,
+ ];
+ $this->drupalGet($settings_path);
+ $this->submitForm($edit, 'Save');
+ $this->assertSession()->pageTextContains('The index was successfully saved.');
+
+ $this->executeTasks();
+
+ $tracked_items = $this->countTrackedItems();
+ $this->assertEquals($node_count, $tracked_items, 'Correct number of items tracked after disabling the "User" datasource.');
+ }
+
+ /**
+ * Changes the index's server and checks if it reacts correctly.
+ *
+ * The expected behavior is that, when an index's server is changed, all of
+ * the index's items should be removed from the previous server and marked as
+ * "unindexed" in the tracker.
+ */
+ protected function changeIndexServer() {
+ $node_count = \Drupal::entityQuery('node')->count()->execute();
+ $this->assertEquals($node_count, $this->countTrackedItems(), 'All nodes are correctly tracked by the index.');
+
+ // Index all remaining items on the index.
+ $this->indexItems();
+
+ $remaining_items = $this->countRemainingItems();
+ $this->assertEquals(0, $remaining_items, 'All items have been successfully indexed.');
+
+ // Create a second search server.
+ $this->createServer('test_server_2');
+
+ // Change the index's server to the new one.
+ $settings_path = $this->getIndexPath('edit');
+ $edit = [
+ 'server' => $this->serverId,
+ ];
+ $this->drupalGet($settings_path);
+ $this->submitForm($edit, 'Save');
+ $this->assertSession()->pageTextContains('The index was successfully saved.');
+
+ // After saving the new index, we should have called reindex.
+ $remaining_items = $this->countRemainingItems();
+ $this->assertEquals($node_count, $remaining_items, 'All items still need to be indexed.');
+ }
+
+ /**
+ * Tests whether indexing via the UI works correctly.
+ */
+ protected function checkIndexing() {
+ $node = $this->drupalCreateNode(['type' => 'article']);
+ $this->drupalCreateNode(['type' => 'article']);
+ $this->drupalCreateNode(['type' => 'article']);
+ $this->drupalCreateNode(['type' => 'article']);
+
+ // Skip indexing for one node.
+ $key = 'search_api_test.backend.indexItems.skip';
+ \Drupal::state()->set($key, ['entity:node/' . $node->id() . ':' . $node->language()->getId()]);
+
+ // Ensure all items need to be indexed.
+ $this->getIndex()->reindex();
+
+ $this->drupalPostForm($this->getIndexPath(), [], 'Index now');
+ $this->assertSession()->statusCodeEquals(200);
+ $this->checkForMetaRefresh();
+ $count = \Drupal::entityQuery('node')->count()->execute() - 1;
+ $this->assertSession()->pageTextContains("Successfully indexed $count items.");
+ $this->assertSession()->pageTextContains('1 item could not be indexed.');
+ $this->assertSession()->pageTextNotContains("Couldn't index items.");
+ $this->assertSession()->pageTextNotContains('An error occurred');
+
+ $this->drupalPostForm($this->getIndexPath(), [], 'Index now');
+ $this->assertSession()->statusCodeEquals(200);
+ $this->checkForMetaRefresh();
+ $this->assertSession()->pageTextContains("Couldn't index items.");
+ $this->assertSession()->pageTextNotContains('An error occurred');
+
+ \Drupal::state()->set($key, []);
+ $this->setError('backend', 'indexItems');
+ $this->drupalPostForm($this->getIndexPath(), [], 'Index now');
+ $this->assertSession()->statusCodeEquals(200);
+ $this->checkForMetaRefresh();
+ $this->assertSession()->pageTextContains("Couldn't index items.");
+ $this->assertSession()->pageTextNotContains('An error occurred');
+
+ $this->setError('backend', 'indexItems', FALSE);
+ $this->drupalPostForm($this->getIndexPath(), [], 'Index now');
+ $this->assertSession()->statusCodeEquals(200);
+ $this->checkForMetaRefresh();
+ $this->assertSession()->pageTextContains("Successfully indexed 1 item.");
+ $this->assertSession()->pageTextNotContains('could not be indexed.');
+ $this->assertSession()->pageTextNotContains("Couldn't index items.");
+ $this->assertSession()->pageTextNotContains('An error occurred');
+ }
+
+ /**
+ * Tests the various actions on the index status form.
+ */
+ protected function checkIndexActions() {
+ $assert_session = $this->assertSession();
+ $index = $this->getIndex();
+ $tracker = $index->getTrackerInstance();
+ $label = $index->label();
+ $this->indexItems();
+
+ // Manipulate the tracking information to make it slightly off (so
+ // rebuilding the tracker will be necessary).
+ $deleted = \Drupal::database()->delete('search_api_item')
+ ->condition('index_id', $index->id())
+ ->condition('item_id', Utility::createCombinedId('entity:node', '2:en'))
+ ->execute();
+ $this->assertEquals(1, $deleted);
+ $manipulated_items_count = \Drupal::entityQuery('node')->count()->execute() - 1;
+
+ $this->assertEquals($manipulated_items_count, $tracker->getIndexedItemsCount());
+ $this->assertEquals($manipulated_items_count, $tracker->getTotalItemsCount());
+ $this->assertEquals($manipulated_items_count + 1, $this->countItemsOnServer());
+
+ $this->drupalPostForm($this->getIndexPath('reindex'), [], 'Confirm');
+ $assert_session->pageTextContains("The search index $label was successfully reindexed.");
+ $this->assertEquals(0, $tracker->getIndexedItemsCount());
+ $this->assertEquals($manipulated_items_count, $tracker->getTotalItemsCount());
+ $this->assertEquals($manipulated_items_count + 1, $this->countItemsOnServer());
+ $this->indexItems();
+
+ $this->drupalPostForm($this->getIndexPath('clear'), [], 'Confirm');
+ $assert_session->pageTextContains("All items were successfully deleted from search index $label.");
+ $this->assertEquals(0, $tracker->getIndexedItemsCount());
+ $this->assertEquals($manipulated_items_count, $tracker->getTotalItemsCount());
+ $this->assertEquals(0, $this->countItemsOnServer());
+ $this->indexItems();
+
+ $this->drupalPostForm($this->getIndexPath('rebuild-tracker'), [], 'Confirm');
+ $assert_session->pageTextContains("The tracking information for search index $label will be rebuilt.");
+ $this->assertEquals(0, $tracker->getIndexedItemsCount());
+ $this->assertEquals($manipulated_items_count + 1, $tracker->getTotalItemsCount());
+ $this->assertEquals($manipulated_items_count, $this->countItemsOnServer());
+ $this->indexItems();
+ }
+
+ /**
+ * Tests deleting a search server via the UI.
+ */
+ protected function deleteServer() {
+ $server = Server::load($this->serverId);
+
+ // Load confirmation form.
+ $this->drupalGet('admin/config/search/search-api/server/' . $this->serverId . '/delete');
+ $this->assertSession()->statusCodeEquals(200);
+ $this->assertSession()->responseContains(new FormattableMarkup('Are you sure you want to delete the search server %name?', ['%name' => $server->label()]));
+ $this->assertSession()->pageTextContains('Deleting a server will disable all its indexes and their searches.');
+
+ // Confirm deletion.
+ $this->submitForm([], 'Delete');
+ $this->assertSession()->responseContains(new FormattableMarkup('The search server %name has been deleted.', ['%name' => $server->label()]));
+ $this->assertFalse(Server::load($this->serverId), 'Server could not be found anymore.');
+ $this->assertSession()->addressEquals('admin/config/search/search-api');
+
+ // Confirm that the index hasn't been deleted.
+ $this->indexStorage->resetCache([$this->indexId]);
+ /** @var \Drupal\search_api\IndexInterface $index */
+ $index = $this->indexStorage->load($this->indexId);
+ $this->assertTrue($index, 'The index associated with the server was not deleted.');
+ $this->assertFalse($index->status(), 'The index associated with the server was disabled.');
+ $this->assertNull($index->getServerId(), 'The index was removed from the server.');
+ }
+
+ /**
+ * Retrieves test index.
+ *
+ * @param bool $reset
+ * (optional) If TRUE, reset the entity cache before loading.
+ *
+ * @return \Drupal\search_api\IndexInterface
+ * The test index.
+ */
+ protected function getIndex($reset = FALSE) {
+ if ($reset) {
+ $this->indexStorage->resetCache([$this->indexId]);
+ }
+ return $this->indexStorage->load($this->indexId);
+ }
+
+ /**
+ * Indexes all (unindexed) items on the specified index.
+ *
+ * @return int
+ * The number of successfully indexed items.
+ */
+ protected function indexItems() {
+ /** @var \Drupal\search_api\IndexInterface $index */
+ $index = Index::load($this->indexId);
+ return $index->indexItems();
+ }
+
+ /**
+ * Ensures that all occurrences of the string are properly escaped.
+ *
+ * This makes sure that the string is only mentioned in an escaped version and
+ * is never double escaped.
+ *
+ * @param string $string
+ * The raw string to check for.
+ */
+ protected function assertHtmlEscaped($string) {
+ $this->assertSession()->responseContains(Html::escape($string));
+ $this->assertSession()->responseNotContains(Html::escape(Html::escape($string)));
+ $this->assertSession()->responseNotContains($string);
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Functional/LanguageIntegrationTest.php b/sites/all/modules/contrib/search/search_api/tests/src/Functional/LanguageIntegrationTest.php
new file mode 100644
index 000000000..c56d3475b
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Functional/LanguageIntegrationTest.php
@@ -0,0 +1,131 @@
+save();
+ ConfigurableLanguage::createFromLangcode('xx-lolspeak')->save();
+
+ // Create an index and server to work with.
+ $this->getTestServer();
+ $this->getTestIndex();
+
+ // Log in, so we can test all the things.
+ $this->drupalLogin($this->adminUser);
+ }
+
+ /**
+ * Tests indexing with different language settings trough the UI.
+ */
+ public function testIndexSettings() {
+ // Create 2 articles.
+ $article1 = $this->drupalCreateNode(['type' => 'article']);
+ $article2 = $this->drupalCreateNode(['type' => 'article']);
+
+ // Those 2 new nodes should be added to the tracking table immediately.
+ $tracked_items = $this->countTrackedItems();
+ $this->assertEquals(2, $tracked_items, 'Two items are tracked.');
+
+ // Add translations.
+ $translation = ['title' => 'test NL', 'body' => 'NL body'];
+ $article1->addTranslation('nl', $translation)->save();
+ $translation = ['title' => 'test2 NL', 'body' => 'NL body2'];
+ $article2->addTranslation('nl', $translation)->save();
+ $translation = ['title' => 'cats', 'body' => 'Cats test'];
+ $article1->addTranslation('xx-lolspeak', $translation)->save();
+
+ // The translations should be tracked as well, so we have a total of 5
+ // indexed items.
+ $tracked_items = $this->countTrackedItems();
+ $this->assertEquals(5, $tracked_items, 'Five items are tracked.');
+
+ // Clear index.
+ $this->drupalGet($this->getIndexPath());
+ $this->submitForm([], 'Clear all indexed data');
+ $this->submitForm([], 'Confirm');
+
+ // Make sure all 5 items are successfully indexed.
+ $this->drupalGet($this->getIndexPath());
+ $this->submitForm([], 'Index now');
+ $this->checkForMetaRefresh();
+ $this->assertSession()->pageTextContains('Successfully indexed 5 items');
+
+ // Change the datasource to disallow indexing of dutch.
+ $form_values = [
+ 'datasource_configs[entity:node][languages][default]' => 1,
+ 'datasource_configs[entity:node][languages][selected][nl]' => 1,
+ ];
+ $this->drupalGet($this->getIndexPath('edit'));
+ $this->submitForm($form_values, 'Save');
+ $this->checkForMetaRefresh();
+ $this->assertSession()->statusCodeEquals(200);
+ $this->assertSession()->pageTextContains('The index was successfully saved.');
+
+ // Make sure that we only have 3 indexed items now. The 2 original nodes
+ // + 1 translation in lolspeak, the 2 dutch translations should be ignored.
+ $this->drupalGet($this->getIndexPath());
+ $this->submitForm([], 'Index now');
+ $this->checkForMetaRefresh();
+ $this->assertSession()->pageTextContains('Successfully indexed 3 items');
+
+ // Change the datasource to only allow indexing of dutch.
+ $form_values = [
+ 'datasource_configs[entity:node][languages][default]' => 0,
+ 'datasource_configs[entity:node][languages][selected][nl]' => 1,
+ 'datasource_configs[entity:node][bundles][default]' => 0,
+ 'datasource_configs[entity:node][bundles][selected][article]' => 1,
+ ];
+ $this->drupalGet($this->getIndexPath('edit'));
+ $this->submitForm($form_values, 'Save');
+ $this->checkForMetaRefresh();
+ $this->assertSession()->statusCodeEquals(200);
+ // Completed 1 of 1.
+ $this->assertSession()->pageTextContains('The index was successfully saved.');
+
+ // Make sure that we only have 2 index items. The only indexed items should
+ // be the dutch translations.
+ $this->drupalGet($this->getIndexPath());
+ $this->submitForm([], 'Index now');
+ $this->checkForMetaRefresh();
+ $this->assertSession()->pageTextContains('Successfully indexed 2 items');
+ }
+
+ /**
+ * Counts the number of tracked items in the test index.
+ *
+ * @return int
+ * The number of tracked items in the test index.
+ */
+ protected function countTrackedItems() {
+ /** @var \Drupal\search_api\IndexInterface $index */
+ $index = Index::load($this->indexId);
+ return $index->getTrackerInstance()->getTotalItemsCount();
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Functional/OverviewPageTest.php b/sites/all/modules/contrib/search/search_api/tests/src/Functional/OverviewPageTest.php
new file mode 100644
index 000000000..cb0be3a7d
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Functional/OverviewPageTest.php
@@ -0,0 +1,273 @@
+drupalLogin($this->adminUser);
+
+ $this->overviewPageUrl = 'admin/config/search/search-api';
+ }
+
+ /**
+ * Tests various scenarios for the overview page.
+ *
+ * Uses a single method to save time.
+ */
+ public function testOverviewPage() {
+ $this->checkServerAndIndexCreation();
+ $this->checkServerAndIndexStatusChanges();
+ $this->checkOperations();
+ $this->checkOverviewPermissions();
+ }
+
+ /**
+ * Tests the creation of a server and an index.
+ */
+ protected function checkServerAndIndexCreation() {
+ $server_name = 'WebTest server';
+ $index_name = 'WebTest index';
+ $actions = [
+ [Url::fromRoute('entity.search_api_server.add_form'), 'Add server'],
+ [Url::fromRoute('entity.search_api_index.add_form'), 'Add index'],
+ ];
+
+ // Enable the "Local actions" block so we can verify which local actions are
+ // displayed.
+ Block::create([
+ 'id' => 'classy_local_actions',
+ 'theme' => 'classy',
+ 'weight' => -20,
+ 'plugin' => 'local_actions_block',
+ 'region' => 'content',
+ ])->save();
+
+ // Make sure the overview is empty.
+ $this->drupalGet($this->overviewPageUrl);
+ $this->assertLocalAction($actions);
+
+ $this->assertSession()->pageTextNotContains($server_name);
+ $this->assertSession()->pageTextNotContains($index_name);
+
+ // Test whether a newly created server appears on the overview page.
+ $server = $this->getTestServer();
+
+ $this->drupalGet($this->overviewPageUrl);
+
+ $this->assertSession()->pageTextContains($server_name);
+ $this->assertSession()->responseContains($server->get('description'));
+ $server_class = Html::cleanCssIdentifier($server->getEntityTypeId() . '-' . $server->id());
+ $servers = $this->xpath('//tr[contains(@class,"' . $server_class . '") and contains(@class, "search-api-list-enabled")]');
+ $this->assertNotEmpty($servers, 'Server is in proper table');
+
+ // Test whether a newly created index appears on the overview page.
+ $index = $this->getTestIndex();
+
+ $this->drupalGet($this->overviewPageUrl);
+
+ $this->assertSession()->pageTextContains($index_name);
+ $this->assertSession()->responseContains($index->get('description'));
+ $index_class = Html::cleanCssIdentifier($index->getEntityTypeId() . '-' . $index->id());
+ $fields = $this->xpath('//tr[contains(@class,"' . $index_class . '") and contains(@class, "search-api-list-enabled")]');
+ $this->assertNotEmpty($fields, 'Index is in proper table');
+ $this->assertSession()->linkNotExists('Execute pending tasks', 'No pending tasks to execute.');
+
+ // Tests that the "Execute pending tasks" local action is correctly
+ // displayed when there are pending tasks.
+ \Drupal::getContainer()
+ ->get('search_api.task_manager')
+ ->addTask('deleteItems', $server, $index, ['']);
+ // Due to an (apparent) Core bug we need to clear the cache, otherwise the
+ // "local actions" block gets displayed from cache (without the link). See
+ // #2722237.
+ \Drupal::cache('render')->invalidateAll();
+ $this->drupalGet($this->overviewPageUrl);
+ $this->assertSession()->linkExists('Execute pending tasks', 0);
+ }
+
+ /**
+ * Tests enable/disable operations for servers and indexes through the UI.
+ */
+ protected function checkServerAndIndexStatusChanges() {
+ $server = $this->getTestServer();
+ $this->assertEntityStatusChange($server);
+
+ // Re-create the index for this test.
+ $this->getTestIndex()->delete();
+ $index = $this->getTestIndex();
+ $this->assertEntityStatusChange($index);
+
+ // Disable the server and test that both itself and the index have been
+ // disabled.
+ $server->setStatus(FALSE)->save();
+ $this->drupalGet($this->overviewPageUrl);
+ $server_class = Html::cleanCssIdentifier($server->getEntityTypeId() . '-' . $server->id());
+ $index_class = Html::cleanCssIdentifier($index->getEntityTypeId() . '-' . $index->id());
+ $servers = $this->xpath('//tr[contains(@class,"' . $server_class . '") and contains(@class, "search-api-list-disabled")]');
+ $this->assertNotEmpty($servers, 'The server has been disabled.');
+ $indexes = $this->xpath('//tr[contains(@class,"' . $index_class . '") and contains(@class, "search-api-list-disabled")]');
+ $this->assertNotEmpty($indexes, 'The index has been disabled.');
+
+ // Test that an index can't be enabled if its server is disabled.
+ // @todo A non-working "Enable" link should not be displayed at all.
+ $this->clickLink('Enable', 1);
+ $this->drupalGet($this->overviewPageUrl);
+ $indexes = $this->xpath('//tr[contains(@class,"' . $index_class . '") and contains(@class, "search-api-list-disabled")]');
+ $this->assertNotEmpty($indexes, 'The index could not be enabled.');
+
+ // Enable the server and try again.
+ $server->setStatus(TRUE)->save();
+ $this->drupalGet($this->overviewPageUrl);
+
+ // This time the server is enabled so the first 'enable' link belongs to the
+ // index.
+ $this->clickLink('Enable');
+ $this->drupalGet($this->overviewPageUrl);
+ $indexes = $this->xpath('//tr[contains(@class,"' . $index_class . '") and contains(@class, "search-api-list-enabled")]');
+ $this->assertNotEmpty($indexes, 'The index has been enabled.');
+
+ // Create a new index without a server assigned and test that it can't be
+ // enabled. The overview UI is not very consistent at the moment, so test
+ // using API functions for now.
+ $index2 = Index::create([
+ 'id' => 'test_index_2',
+ 'name' => 'WebTest index 2',
+ 'datasource_settings' => [
+ 'entity:node' => [],
+ ],
+ ]);
+ $index2->save();
+ $this->assertFalse($index2->status(), 'The newly created index without a server is disabled by default.');
+
+ $index2->setStatus(TRUE)->save();
+ $this->assertFalse($index2->status(), 'The newly created index without a server cannot be enabled.');
+ }
+
+ /**
+ * Asserts enable/disable operations for a search server or index.
+ *
+ * @param \Drupal\search_api\ServerInterface|\Drupal\search_api\IndexInterface $entity
+ * A search server or index.
+ */
+ protected function assertEntityStatusChange($entity) {
+ $this->drupalGet($this->overviewPageUrl);
+ $row_class = Html::cleanCssIdentifier($entity->getEntityTypeId() . '-' . $entity->id());
+ $rows = $this->xpath('//tr[contains(@class,"' . $row_class . '") and contains(@class, "search-api-list-enabled")]');
+ $this->assertNotEmpty($rows, 'The newly created entity is enabled by default.');
+
+ // The first "Disable" link on the page belongs to our server, the second
+ // one to our index.
+ $this->clickLink('Disable', $entity instanceof ServerInterface ? 0 : 1);
+
+ // Submit the confirmation form and test that the entity has been disabled.
+ $this->submitForm([], 'Disable');
+ $rows = $this->xpath('//tr[contains(@class,"' . $row_class . '") and contains(@class, "search-api-list-disabled")]');
+ $this->assertNotEmpty($rows, 'The entity has been disabled.');
+
+ // Now enable the entity and verify that the operation succeeded.
+ $this->clickLink('Enable');
+ $this->drupalGet($this->overviewPageUrl);
+ $rows = $this->xpath('//tr[contains(@class,"' . $row_class . '") and contains(@class, "search-api-list-enabled")]');
+ $this->assertNotEmpty($rows, 'The entity has benn enabled.');
+ }
+
+ /**
+ * Tests server operations in the overview page.
+ */
+ protected function checkOperations() {
+ $server = $this->getTestServer();
+
+ $this->drupalGet($this->overviewPageUrl);
+ $basic_url = $this->urlGenerator->generateFromRoute('entity.search_api_server.canonical', ['search_api_server' => $server->id()]);
+ $destination = '';
+ // Drupal 8.5.x introduced "destination" parameters to all operations links
+ // by default, so we now need to take that into account.
+ // @todo Remove once we depend on 8.5.
+ if (version_compare(\Drupal::VERSION, '8.5.x-dev', '>=')) {
+ $destination = "?destination=" . $this->urlGenerator->generateFromRoute('search_api.overview');
+ }
+ $this->assertSession()->responseContains("Edit");
+ $this->assertSession()->responseContains("Disable");
+ $this->assertSession()->responseContains("Delete");
+ $this->assertSession()->responseNotContains("Enable");
+
+ $server->setStatus(FALSE)->save();
+ $this->drupalGet($this->overviewPageUrl);
+
+ // Since \Drupal\Core\Access\CsrfTokenGenerator uses the current session ID,
+ // we cannot verify the validity of the token from here.
+ $params = $destination ? "$destination&token=" : '?token=';
+ $this->assertSession()->responseContains("assertSession()->responseNotContains("Disable");
+ }
+
+ /**
+ * Tests that the overview has the correct permissions set.
+ */
+ protected function checkOverviewPermissions() {
+ $this->drupalGet('admin/config');
+ $this->assertSession()->pageTextContains('Search API');
+
+ $this->drupalGet($this->overviewPageUrl);
+ $this->assertSession()->statusCodeEquals(200);
+
+ $this->drupalLogin($this->unauthorizedUser);
+ $this->drupalGet($this->overviewPageUrl);
+ $this->assertSession()->statusCodeEquals(403);
+ }
+
+ /**
+ * Asserts local actions in the page output.
+ *
+ * @param array $actions
+ * A list of expected action link titles, keyed by the hrefs.
+ */
+ protected function assertLocalAction(array $actions) {
+ $elements = $this->xpath('//a[contains(@class, :class)]', [
+ ':class' => 'button-action',
+ ]);
+ $index = 0;
+ foreach ($actions as $action) {
+ /** @var \Drupal\Core\Url $url */
+ list($url, $title) = $action;
+ // SimpleXML gives us the unescaped text, not the actual escaped markup,
+ // so use a pattern instead to check the raw content.
+ // This behaviour is a bug in libxml, see
+ // https://bugs.php.net/bug.php?id=49437.
+ $this->assertSession()->responseMatches('@]*class="[^"]*button-action[^"]*"[^>]*>' . preg_quote($title, '@') . '@');
+ $this->assertEquals($url->toString(), $elements[$index++]->getAttribute('href'));
+ }
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Functional/ProcessorIntegrationTest.php b/sites/all/modules/contrib/search/search_api/tests/src/Functional/ProcessorIntegrationTest.php
new file mode 100644
index 000000000..3f24e8ecc
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Functional/ProcessorIntegrationTest.php
@@ -0,0 +1,786 @@
+drupalLogin($this->adminUser);
+
+ $this->indexId = 'test_index';
+ $index = Index::create([
+ 'name' => 'Test index',
+ 'id' => $this->indexId,
+ 'status' => 1,
+ 'datasource_settings' => [
+ 'entity:node' => [],
+ 'entity:user' => [],
+ ],
+ ]);
+
+ // Add a node and a taxonomy term reference, both of which could be used to
+ // create a hierarchy.
+ $this->createEntityReferenceField(
+ 'node',
+ 'page',
+ 'term_field',
+ NULL,
+ 'taxonomy_term',
+ 'default',
+ [],
+ FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED
+ );
+ $this->createEntityReferenceField(
+ 'node',
+ 'page',
+ 'parent_reference',
+ NULL,
+ 'node',
+ 'default',
+ ['target_bundles' => ['page' => 'page']]
+ );
+
+ // Index the taxonomy and entity reference fields.
+ $term_field = new Field($index, 'term_field');
+ $term_field->setType('integer');
+ $term_field->setPropertyPath('term_field');
+ $term_field->setDatasourceId('entity:node');
+ $term_field->setLabel('Terms');
+ $index->addField($term_field);
+
+ $parent_reference = new Field($index, 'parent_reference');
+ $parent_reference->setType('integer');
+ $parent_reference->setPropertyPath('parent_reference');
+ $parent_reference->setDatasourceId('entity:node');
+ $parent_reference->setLabel('Terms');
+ $index->addField($parent_reference);
+ $index->save();
+ }
+
+ /**
+ * Tests the admin UI for processors.
+ *
+ * Calls the other test methods in this class, named check*Integration(), to
+ * avoid the overhead of having one test per processor.
+ */
+ public function testProcessorIntegration() {
+ // Some processors are always enabled.
+ $enabled = ['add_url', 'aggregated_field', 'rendered_item'];
+ $actual_processors = array_keys($this->loadIndex()->getProcessors());
+ sort($actual_processors);
+ $this->assertEquals($enabled, $actual_processors);
+
+ // Ensure the hidden processors aren't displayed in the UI.
+ $this->loadProcessorsTab();
+ $hidden = $enabled;
+ foreach ($hidden as $processor_id) {
+ $this->assertSession()->responseNotContains(Html::escape($processor_id));
+ }
+
+ $this->checkAggregatedFieldsIntegration();
+
+ $this->checkContentAccessIntegration();
+ $enabled[] = 'content_access';
+ sort($enabled);
+ $actual_processors = array_keys($this->loadIndex()->getProcessors());
+ sort($actual_processors);
+ $this->assertEquals($enabled, $actual_processors);
+
+ $this->checkEntityBundleBoostIntegration();
+ $enabled[] = 'type_boost';
+ sort($enabled);
+ $actual_processors = array_keys($this->loadIndex()->getProcessors());
+ sort($actual_processors);
+ $this->assertEquals($enabled, $actual_processors);
+
+ $this->checkHighlightIntegration();
+ $enabled[] = 'highlight';
+ sort($enabled);
+ $actual_processors = array_keys($this->loadIndex()->getProcessors());
+ sort($actual_processors);
+ $this->assertEquals($enabled, $actual_processors);
+
+ $this->checkHtmlFilterIntegration();
+ $enabled[] = 'html_filter';
+ sort($enabled);
+ $actual_processors = array_keys($this->loadIndex()->getProcessors());
+ sort($actual_processors);
+ $this->assertEquals($enabled, $actual_processors);
+
+ $this->checkIgnoreCaseIntegration();
+ $enabled[] = 'ignorecase';
+ sort($enabled);
+ $actual_processors = array_keys($this->loadIndex()->getProcessors());
+ sort($actual_processors);
+ $this->assertEquals($enabled, $actual_processors);
+
+ $this->checkIgnoreCharactersIntegration();
+ $enabled[] = 'ignore_character';
+ sort($enabled);
+ $actual_processors = array_keys($this->loadIndex()->getProcessors());
+ sort($actual_processors);
+ $this->assertEquals($enabled, $actual_processors);
+
+ $this->checkEntityStatusIntegration();
+ $enabled[] = 'entity_status';
+ sort($enabled);
+ $actual_processors = array_keys($this->loadIndex()->getProcessors());
+ sort($actual_processors);
+ $this->assertEquals($enabled, $actual_processors);
+
+ $this->checkRoleFilterIntegration();
+ $enabled[] = 'role_filter';
+ sort($enabled);
+ $actual_processors = array_keys($this->loadIndex()->getProcessors());
+ sort($actual_processors);
+ $this->assertEquals($enabled, $actual_processors);
+
+ $this->checkRenderedItemIntegration();
+
+ $this->checkStopWordsIntegration();
+ $enabled[] = 'stopwords';
+ sort($enabled);
+ $actual_processors = array_keys($this->loadIndex()->getProcessors());
+ sort($actual_processors);
+ $this->assertEquals($enabled, $actual_processors);
+
+ $this->checkTokenizerIntegration();
+ $enabled[] = 'tokenizer';
+ sort($enabled);
+ $actual_processors = array_keys($this->loadIndex()->getProcessors());
+ sort($actual_processors);
+ $this->assertEquals($enabled, $actual_processors);
+
+ $this->checkTransliterationIntegration();
+ $enabled[] = 'transliteration';
+ sort($enabled);
+ $actual_processors = array_keys($this->loadIndex()->getProcessors());
+ sort($actual_processors);
+ $this->assertEquals($enabled, $actual_processors);
+
+ $this->checkAddHierarchyIntegration();
+ $enabled[] = 'hierarchy';
+ sort($enabled);
+ $actual_processors = array_keys($this->loadIndex()->getProcessors());
+ sort($actual_processors);
+ $this->assertEquals($enabled, $actual_processors);
+
+ $this->checkStemmerIntegration();
+ $enabled[] = 'stemmer';
+ sort($enabled);
+ $actual_processors = array_keys($this->loadIndex()->getProcessors());
+ sort($actual_processors);
+ $this->assertEquals($enabled, $actual_processors);
+
+ // The 'add_url' processor is not available to be removed because it's
+ // locked.
+ $this->checkUrlFieldIntegration();
+
+ // Check the order of the displayed processors.
+ $stages = [
+ ProcessorInterface::STAGE_PREPROCESS_INDEX,
+ ProcessorInterface::STAGE_PREPROCESS_QUERY,
+ ProcessorInterface::STAGE_POSTPROCESS_QUERY,
+ ];
+ /** @var \Drupal\search_api\Processor\ProcessorInterface[] $processors */
+ $processors = \Drupal::getContainer()
+ ->get('search_api.plugin_helper')
+ ->createProcessorPlugins($this->loadIndex());
+ $page = $this->getSession()->getPage();
+ foreach ($stages as $stage) {
+ // Since the order of processors with the same weight is undefined, we
+ // can't just use $index->getProcessorsByStage() and assertEquals().
+ $previous_weight = NULL;
+ $class = 'search-api-stage-wrapper-' . Html::cleanCssIdentifier($stage);
+ /** @var \Behat\Mink\Element\NodeElement $element */
+ foreach ($page->findAll('css', ".$class tr.draggable") as $element) {
+ // Since processors are shown right away when checked in the form, they
+ // are even included in the form when disabled – just hidden. We can
+ // check the "style" attribute to identify them.
+ if (preg_match('/\bsearch-api-processor-weight--([-a-z0-9]+)\b/', $element->getAttribute('class'), $m)) {
+ $processor_id = str_replace('-', '_', $m[1]);
+ $weight = $processors[$processor_id]->getWeight($stage);
+ if ($previous_weight !== NULL) {
+ $this->assertGreaterThanOrEqual($previous_weight, $weight, "Correct order of processor $processor_id in stage $stage");
+ }
+ $previous_weight = $weight;
+ }
+ }
+ }
+
+ // Check whether disabling processors also works correctly.
+ $this->loadProcessorsTab();
+ $edit = [
+ 'status[stopwords]' => FALSE,
+ ];
+ $this->submitForm($edit, 'Save');
+ $enabled = array_values(array_diff($enabled, ['stopwords']));
+ $actual_processors = array_keys($this->loadIndex()->getProcessors());
+ sort($actual_processors);
+ $this->assertEquals($enabled, $actual_processors);
+
+ // After disabling some datasource, all related processors should be
+ // disabled also.
+ $this->drupalGet('admin/config/search/search-api/index/' . $this->indexId . '/edit');
+ $this->drupalPostForm(NULL, ['datasources[entity:user]' => FALSE], 'Save');
+ $processors = $this->loadIndex()->getProcessors();
+ $this->assertArrayNotHasKey('role_filter', $processors);
+ $this->drupalGet('admin/config/search/search-api/index/' . $this->indexId . '/processors');
+ $this->assertSession()->pageTextNotContains('Role filter');
+ }
+
+ /**
+ * Tests that processors discouraged by the backend are correctly hidden.
+ *
+ * @see https://www.drupal.org/node/2228739
+ */
+ public function testLimitProcessors() {
+ $this->loadProcessorsTab();
+ $this->assertSession()->statusCodeEquals(200);
+ $this->assertSession()->pageTextContains('Highlight');
+ $this->assertSession()->pageTextContains('Ignore character');
+ $this->assertSession()->pageTextContains('Tokenizer');
+ $this->assertSession()->pageTextContains('Stopwords');
+
+ // Create a new server with the "search_api_test" backend.
+ $server = Server::create([
+ 'id' => 'webtest_server',
+ 'name' => 'WebTest server',
+ 'description' => 'WebTest server',
+ 'backend' => 'search_api_test',
+ 'backend_config' => [],
+ ]);
+ $server->save();
+ $processors = [
+ 'highlight',
+ 'ignore_character',
+ 'tokenizer',
+ 'stopwords',
+ ];
+ $this->setReturnValue('backend', 'getDiscouragedProcessors', $processors);
+
+ // Use the newly created server.
+ $settings_path = 'admin/config/search/search-api/index/' . $this->indexId . '/edit';
+ $this->drupalGet($settings_path);
+ $this->submitForm(['server' => 'webtest_server'], 'Save');
+
+ // Load the processors again and check that they are not shown anymore.
+ $this->loadProcessorsTab();
+ $this->assertSession()->statusCodeEquals(200);
+ $this->assertSession()->pageTextNotContains('Highlight');
+ $this->assertSession()->pageTextNotContains('Ignore character');
+ $this->assertSession()->pageTextNotContains('Tokenizer');
+ $this->assertSession()->pageTextNotContains('Stopwords');
+ }
+
+ /**
+ * Tests the integration of the "Aggregated fields" processor.
+ */
+ public function checkAggregatedFieldsIntegration() {
+ $index = $this->loadIndex();
+ $index->removeProcessor('aggregated_field');
+ $index->save();
+
+ $this->assertTrue($this->loadIndex()->isValidProcessor('aggregated_field'), 'The "Aggregated fields" processor cannot be disabled.');
+
+ $options['query']['datasource'] = '';
+ $this->drupalGet($this->getIndexPath('fields/add/nojs'), $options);
+
+ // See \Drupal\search_api\Tests\IntegrationTest::addField().
+ $this->assertSession()->responseContains('name="aggregated_field"');
+ $this->submitForm([], 'aggregated_field');
+ $args['%label'] = 'Aggregated field';
+ $this->assertSession()->responseContains(new FormattableMarkup('Field %label was added to the index.', $args));
+ $this->assertSession()->addressEquals($this->getIndexPath('fields/edit/aggregated_field'));
+ $edit = [
+ 'type' => 'first',
+ 'fields[entity:node/title]' => 'title',
+ 'fields[entity:node/type]' => 'type',
+ 'fields[entity:node/uid]' => 'uid',
+ ];
+ $this->submitForm($edit, 'Save');
+
+ $this->assertSession()->addressEquals($this->getIndexPath('fields'));
+ $this->assertSession()->responseContains('The field configuration was successfully saved.');
+ }
+
+ /**
+ * Tests the UI for the "Content access" processor.
+ */
+ public function checkContentAccessIntegration() {
+ $this->enableProcessor('content_access');
+
+ // Ensure the fields required for the "Content access" processor are now
+ // indexed.
+ $index = $this->loadIndex();
+ $index->save();
+ $content_access_fields = [
+ 'status' => [
+ 'datasource_id' => 'entity:node',
+ 'property_path' => 'status',
+ 'type' => 'boolean',
+ 'indexed_locked' => TRUE,
+ 'type_locked' => TRUE,
+ ],
+ 'uid' => [
+ 'datasource_id' => 'entity:node',
+ 'property_path' => 'uid',
+ 'type' => 'integer',
+ 'indexed_locked' => TRUE,
+ 'type_locked' => TRUE,
+ ],
+ 'node_grants' => [
+ 'property_path' => 'search_api_node_grants',
+ 'type' => 'string',
+ 'indexed_locked' => TRUE,
+ 'type_locked' => TRUE,
+ 'hidden' => TRUE,
+ ],
+ ];
+ $index_fields = $index->getFields();
+ foreach ($content_access_fields as $field_id => $settings) {
+ $this->assertTrue(!empty($index_fields[$field_id]), "Field $field_id (required by \"Content access\" processor) is present.");
+ $field_settings = $index_fields[$field_id]->getSettings();
+ unset($field_settings['label'], $field_settings['dependencies']);
+ $this->assertEquals($settings, $field_settings, "Field $field_id has the correct settings.");
+ }
+ }
+
+ /**
+ * Tests the UI for the "Type-specific boosting" processor.
+ */
+ public function checkEntityBundleBoostIntegration() {
+ $configuration = [
+ 'boosts' => [
+ 'entity:node' => [
+ 'datasource_boost' => '3.0',
+ 'bundle_boosts' => [
+ 'article' => '5.0',
+ ],
+ ],
+ 'entity:user' => [
+ 'datasource_boost' => '1.0',
+ ],
+ ],
+ ];
+ $form_values = $configuration;
+ $form_values['boosts']['entity:node']['bundle_boosts']['page'] = '';
+
+ $this->editSettingsForm($configuration, 'type_boost', $form_values);
+ }
+
+ /**
+ * Tests the UI for the "Highlight" processor.
+ */
+ public function checkHighlightIntegration() {
+ $configuration = [
+ 'highlight' => 'never',
+ 'excerpt' => FALSE,
+ 'excerpt_length' => 128,
+ 'prefix' => '',
+ 'suffix' => '',
+ ];
+ $this->editSettingsForm($configuration, 'highlight');
+ }
+
+ /**
+ * Tests the UI for the "HTML filter" processor.
+ */
+ public function checkHtmlFilterIntegration() {
+ $configuration = [
+ 'tags' => <<checkValidationError($configuration, 'html_filter', 'Tags is not a valid YAML map.');
+ $configuration = [
+ 'tags' => << '']),
+ new FormattableMarkup('Boost value for tag @tag must be numeric.', ['@tag' => '']),
+ new FormattableMarkup('Boost value for tag @tag must be non-negative.', ['@tag' => '']),
+ ];
+ $this->checkValidationError($configuration, 'html_filter', $errors);
+
+ $configuration = $form_values = [
+ 'title' => FALSE,
+ 'alt' => FALSE,
+ 'tags' => [
+ 'h1' => 10,
+ ],
+ ];
+ $form_values['tags'] = 'h1: 10';
+ $this->editSettingsForm($configuration, 'html_filter', $form_values);
+ }
+
+ /**
+ * Tests the UI for the "Ignore case" processor.
+ */
+ public function checkIgnoreCaseIntegration() {
+ $this->editSettingsForm([], 'ignorecase');
+ }
+
+ /**
+ * Tests the UI for the "Ignore characters" processor.
+ */
+ public function checkIgnoreCharactersIntegration() {
+ $configuration = [
+ 'ignorable' => ':)',
+ ];
+ $this->checkValidationError($configuration, 'ignore_character', 'The entered text is no valid regular expression.');
+
+ $configuration = $form_values = [
+ 'ignorable' => '[¿¡!?,.]',
+ 'strip' => [
+ 'character_sets' => [
+ 'Pc' => 'Pc',
+ 'Pd' => 'Pd',
+ 'Pe' => 'Pe',
+ 'Pf' => 'Pf',
+ 'Pi' => 'Pi',
+ 'Po' => 'Po',
+ 'Ps' => 'Ps',
+ 'Cc' => 'Cc',
+ 'Cf' => FALSE,
+ 'Co' => FALSE,
+ 'Mc' => FALSE,
+ 'Me' => FALSE,
+ 'Mn' => FALSE,
+ 'Sc' => FALSE,
+ 'Sk' => FALSE,
+ 'Sm' => FALSE,
+ 'So' => FALSE,
+ 'Zl' => FALSE,
+ 'Zp' => FALSE,
+ 'Zs' => FALSE,
+ ],
+ ],
+ ];
+ $this->editSettingsForm($configuration, 'ignore_character', $form_values);
+ }
+
+ /**
+ * Tests the UI for the "Entity status" processor.
+ */
+ public function checkEntityStatusIntegration() {
+ $this->enableProcessor('entity_status');
+ }
+
+ /**
+ * Tests the UI for the "Role filter" processor.
+ */
+ public function checkRoleFilterIntegration() {
+ $this->enableProcessor('role_filter');
+
+ $configuration = [
+ 'default' => 1,
+ 'roles' => [
+ 'anonymous',
+ ],
+ ];
+ $this->editSettingsForm($configuration, 'role_filter');
+ }
+
+ /**
+ * Tests the integration of the "Rendered item" processor.
+ */
+ public function checkRenderedItemIntegration() {
+ $index = $this->loadIndex();
+ $index->removeProcessor('rendered_item');
+ $index->save();
+
+ $this->assertTrue($this->loadIndex()->isValidProcessor('rendered_item'), 'The "Rendered item" processor cannot be disabled.');
+
+ $options['query']['datasource'] = '';
+ $this->drupalGet($this->getIndexPath('fields/add/nojs'), $options);
+
+ // See \Drupal\search_api\Tests\IntegrationTest::addField().
+ $this->assertSession()->responseContains('name="rendered_item"');
+ $this->submitForm([], 'rendered_item');
+ $args['%label'] = 'Rendered HTML output';
+ $this->assertSession()->responseContains(new FormattableMarkup('Field %label was added to the index.', $args));
+ $this->assertSession()->addressEquals($this->getIndexPath('fields/edit/rendered_item'));
+ $edit = [
+ 'roles[]' => ['authenticated'],
+ 'view_mode[entity:node][article]' => 'default',
+ 'view_mode[entity:node][page]' => 'teaser',
+ ];
+ $this->submitForm($edit, 'Save');
+
+ $this->assertSession()->addressEquals($this->getIndexPath('fields'));
+ $this->assertSession()->responseContains('The field configuration was successfully saved.');
+ }
+
+ /**
+ * Tests the UI for the "Stopwords" processor.
+ */
+ public function checkStopWordsIntegration() {
+ $configuration = [
+ 'stopwords' => ['the'],
+ ];
+ $form_values = [
+ 'stopwords' => 'the',
+ ];
+ $this->editSettingsForm($configuration, 'stopwords', $form_values);
+ }
+
+ /**
+ * Tests the UI for the "Tokenizer" processor.
+ */
+ public function checkTokenizerIntegration() {
+ $configuration = [
+ 'spaces' => '[:foobar:]',
+ ];
+ $this->checkValidationError($configuration, 'tokenizer', 'The entered text is no valid PCRE character class.');
+
+ $configuration = [
+ 'spaces' => '',
+ 'overlap_cjk' => FALSE,
+ 'minimum_word_size' => 2,
+ ];
+ $this->editSettingsForm($configuration, 'tokenizer');
+ }
+
+ /**
+ * Tests the UI for the "Transliteration" processor.
+ */
+ public function checkTransliterationIntegration() {
+ $this->editSettingsForm([], 'transliteration');
+ }
+
+ /**
+ * Tests the hierarchy processor.
+ */
+ protected function checkAddHierarchyIntegration() {
+ $configuration = [
+ 'fields' => [
+ 'term_field' => 'taxonomy_term-parent',
+ 'parent_reference' => 'node-parent_reference',
+ ],
+ ];
+ $edit = [
+ 'fields' => [
+ 'term_field' => ['status' => 1],
+ 'parent_reference' => ['status' => 1],
+ ],
+ ];
+ $this->editSettingsForm($configuration, 'hierarchy', $edit, TRUE, FALSE);
+ }
+
+ /**
+ * Tests the UI for the "Stemmer" processor.
+ */
+ public function checkStemmerIntegration() {
+ $this->enableProcessor('stemmer');
+ $configuration = [
+ 'exceptions' => ['indian' => 'india'],
+ ];
+ $form_values = [
+ 'exceptions' => 'indian=india',
+ ];
+ $this->editSettingsForm($configuration, 'stemmer', $form_values);
+ }
+
+ /**
+ * Tests the integration of the "URL field" processor.
+ */
+ public function checkUrlFieldIntegration() {
+ $index = $this->loadIndex();
+ $index->removeProcessor('add_url');
+ $index->save();
+
+ $this->assertTrue($this->loadIndex()->isValidProcessor('add_url'), 'The "Add URL" processor cannot be disabled.');
+ }
+
+ /**
+ * Tests that a processor can be enabled.
+ *
+ * @param string $processor_id
+ * The ID of the processor to enable.
+ */
+ protected function enableProcessor($processor_id) {
+ $this->loadProcessorsTab();
+
+ $edit = [
+ "status[$processor_id]" => 1,
+ ];
+ $this->submitForm($edit, 'Save');
+ $this->assertTrue($this->loadIndex()->isValidProcessor($processor_id), "Successfully enabled the '$processor_id' processor.'");
+ }
+
+ /**
+ * Enables a processor with a given configuration.
+ *
+ * @param array $configuration
+ * The configuration to set for the processor.
+ * @param string $processor_id
+ * The ID of the processor to edit.
+ * @param array|null $form_values
+ * (optional) The processor configuration to set, as it appears in the form.
+ * Only relevant if the processor does some processing on the form values
+ * before storing them, like parsing YAML or cleaning up checkboxes values.
+ * Defaults to using $configuration as-is.
+ * @param bool $enable
+ * (optional) If TRUE, explicitly enable the processor. If FALSE, it should
+ * already be enabled.
+ * @param bool $unset_fields
+ * (optional) If TRUE, the "fields" property will be removed from the
+ * actual configuration prior to comparing with the given configuration.
+ */
+ protected function editSettingsForm(array $configuration, $processor_id, array $form_values = NULL, $enable = TRUE, $unset_fields = TRUE) {
+ if (!isset($form_values)) {
+ $form_values = $configuration;
+ }
+
+ $this->loadProcessorsTab();
+
+ $edit = $this->getFormValues($form_values, "processors[$processor_id][settings]");
+ if ($enable) {
+ $edit["status[$processor_id]"] = 1;
+ }
+ $this->submitForm($edit, 'Save');
+
+ $processor = $this->loadIndex()->getProcessor($processor_id);
+ $this->assertTrue($processor, "Successfully enabled the '$processor_id' processor.'");
+ if ($processor) {
+ $actual_configuration = $processor->getConfiguration();
+ unset($actual_configuration['weights']);
+ if ($unset_fields) {
+ unset($actual_configuration['fields']);
+ }
+ $configuration += $processor->defaultConfiguration();
+ $this->assertEquals($configuration, $actual_configuration, "Processor configuration for processor '$processor_id' was set correctly.");
+ }
+ }
+
+ /**
+ * Makes sure that the given form values will fail when submitted.
+ *
+ * @param array $form_values
+ * The form values, relative to the processor form.
+ * @param string $processor_id
+ * The processor's ID.
+ * @param string[]|string $messages
+ * Either the expected error message or an array of expected error messages.
+ */
+ protected function checkValidationError(array $form_values, $processor_id, $messages) {
+ $this->loadProcessorsTab();
+
+ $edit = $this->getFormValues($form_values, "processors[$processor_id][settings]");
+ $edit["status[$processor_id]"] = 1;
+ $this->submitForm($edit, 'Save');
+
+ if (!is_array($messages)) {
+ $messages = [$messages];
+ }
+ foreach ($messages as $message) {
+ $this->assertSession()->responseContains($message);
+ }
+ $this->assertSession()->pageTextNotContains('The indexing workflow was successfully edited.');
+ $this->assertSession()->pageTextNotContains('No values were changed.');
+
+ $this->loadProcessorsTab(TRUE);
+ }
+
+ /**
+ * Converts a configuration array into an array of form values.
+ *
+ * @param array $configuration
+ * The configuration to convert.
+ * @param string $prefix
+ * The common prefix for all form values.
+ *
+ * @return string[]
+ * An array of form values ready for submission.
+ */
+ protected function getFormValues(array $configuration, $prefix) {
+ $edit = [];
+
+ foreach ($configuration as $key => $value) {
+ $key = $prefix . "[$key]";
+ if (is_array($value)) {
+ // Handling of numerically indexed and associative arrays needs to be
+ // different.
+ if ($value == array_values($value)) {
+ $key .= '[]';
+ $edit[$key] = $value;
+ }
+ else {
+ $edit += $this->getFormValues($value, $key);
+ }
+ }
+ else {
+ $edit[$key] = $value;
+ }
+ }
+
+ return $edit;
+ }
+
+ /**
+ * Loads the test index's "Processors" tab in the test browser, if necessary.
+ *
+ * @param bool $force
+ * (optional) If TRUE, even load the tab if we are already on it.
+ */
+ protected function loadProcessorsTab($force = FALSE) {
+ $settings_path = 'admin/config/search/search-api/index/' . $this->indexId . '/processors';
+ if ($force || $this->getAbsoluteUrl($settings_path) != $this->getUrl()) {
+ $this->drupalGet($settings_path);
+ }
+ }
+
+ /**
+ * Loads the search index used by this test.
+ *
+ * @return \Drupal\search_api\IndexInterface
+ * The search index used by this test.
+ */
+ protected function loadIndex() {
+ $index_storage = \Drupal::entityTypeManager()->getStorage('search_api_index');
+ $index_storage->resetCache([$this->indexId]);
+
+ return $index_storage->load($this->indexId);
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Functional/SearchApiBrowserTestBase.php b/sites/all/modules/contrib/search/search_api/tests/src/Functional/SearchApiBrowserTestBase.php
new file mode 100644
index 000000000..125b30183
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Functional/SearchApiBrowserTestBase.php
@@ -0,0 +1,197 @@
+adminUser = $this->drupalCreateUser($this->adminUserPermissions);
+ $this->unauthorizedUser = $this->drupalCreateUser(['access administration pages']);
+ $this->anonymousUser = $this->drupalCreateUser();
+
+ // Get the URL generator.
+ $this->urlGenerator = $this->container->get('url_generator');
+
+ // Create an article node type, if not already present.
+ if (!NodeType::load('article')) {
+ $this->drupalCreateContentType([
+ 'type' => 'article',
+ 'name' => 'Article',
+ ]);
+ }
+
+ // Create a page node type, if not already present.
+ if (!NodeType::load('page')) {
+ $this->drupalCreateContentType([
+ 'type' => 'page',
+ 'name' => 'Page',
+ ]);
+ }
+
+ // 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);
+ }
+ }
+
+ /**
+ * Creates or loads a server.
+ *
+ * @return \Drupal\search_api\ServerInterface
+ * A search server.
+ */
+ public function getTestServer() {
+ $server = Server::load('webtest_server');
+ if (!$server) {
+ $server = Server::create([
+ 'id' => 'webtest_server',
+ 'name' => 'WebTest server',
+ 'description' => 'WebTest server' . ' description',
+ 'backend' => 'search_api_test',
+ 'backend_config' => [],
+ ]);
+ $server->save();
+ }
+
+ return $server;
+ }
+
+ /**
+ * Creates or loads an index.
+ *
+ * @return \Drupal\search_api\IndexInterface
+ * A search index.
+ */
+ public function getTestIndex() {
+ $this->indexId = 'webtest_index';
+ $index = Index::load($this->indexId);
+ if (!$index) {
+ $index = Index::create([
+ 'id' => $this->indexId,
+ 'name' => 'WebTest index',
+ 'description' => 'WebTest index' . ' description',
+ 'server' => 'webtest_server',
+ 'datasource_settings' => [
+ 'entity:node' => [],
+ ],
+ ]);
+ $index->save();
+ }
+
+ return $index;
+ }
+
+ /**
+ * Returns the system path for the test index.
+ *
+ * @param string|null $tab
+ * (optional) If set, the path suffix for a specific index tab.
+ *
+ * @return string
+ * A system path.
+ */
+ protected function getIndexPath($tab = NULL) {
+ $path = 'admin/config/search/search-api/index/' . $this->indexId;
+ if ($tab) {
+ $path .= "/$tab";
+ }
+ return $path;
+ }
+
+ /**
+ * Executes all pending Search API tasks.
+ */
+ protected function executeTasks() {
+ $task_manager = \Drupal::getContainer()->get('search_api.task_manager');
+ $task_manager->executeAllTasks();
+ $this->assertEquals(0, $task_manager->getTasksCount(), 'No more pending tasks.');
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Functional/ViewsTest.php b/sites/all/modules/contrib/search/search_api/tests/src/Functional/ViewsTest.php
new file mode 100644
index 000000000..4a1106fdd
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Functional/ViewsTest.php
@@ -0,0 +1,953 @@
+save();
+
+ \Drupal::getContainer()
+ ->get('search_api.index_task_manager')
+ ->addItemsAll(Index::load($this->indexId));
+ $this->insertExampleContent();
+ $this->indexItems($this->indexId);
+
+ // 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);
+ }
+ }
+
+ /**
+ * Tests a view with exposed filters.
+ */
+ public function testView() {
+ $this->checkResults([], array_keys($this->entities), 'Unfiltered search');
+
+ $this->checkResults(
+ ['search_api_fulltext' => 'foobar'],
+ [3],
+ 'Search for a single word'
+ );
+ $this->checkResults(
+ ['search_api_fulltext' => 'foo test'],
+ [1, 2, 4],
+ 'Search for multiple words'
+ );
+ $query = [
+ 'search_api_fulltext' => 'foo test',
+ 'search_api_fulltext_op' => 'or',
+ ];
+ $this->checkResults($query, [1, 2, 3, 4, 5], 'OR search for multiple words');
+ $query = [
+ 'search_api_fulltext' => 'foobar',
+ 'search_api_fulltext_op' => 'not',
+ ];
+ $this->checkResults($query, [1, 2, 4, 5], 'Negated search');
+ $query = [
+ 'search_api_fulltext' => 'foo test',
+ 'search_api_fulltext_op' => 'not',
+ ];
+ $this->checkResults($query, [], 'Negated search for multiple words');
+ $query = [
+ 'search_api_fulltext' => 'fo',
+ ];
+ $label = 'Search for short word';
+ $this->checkResults($query, [], $label);
+ $this->assertSession()->pageTextContains('You must include at least one positive keyword with 3 characters or more');
+ $query = [
+ 'search_api_fulltext' => 'foo to test',
+ ];
+ $label = 'Fulltext search including short word';
+ $this->checkResults($query, [1, 2, 4], $label);
+ $this->assertSession()->pageTextNotContains('You must include at least one positive keyword with 3 characters or more');
+
+ $this->checkResults(['id[value]' => 2], [2], 'Search with ID filter');
+
+ $query = [
+ 'id[min]' => 2,
+ 'id[max]' => 4,
+ 'id_op' => 'between',
+ ];
+ $this->checkResults($query, [2, 3, 4], 'Search with ID "in between" filter');
+
+ $query = [
+ 'id[min]' => 2,
+ 'id[max]' => 4,
+ 'id_op' => 'not between',
+ ];
+ $this->checkResults($query, [1, 5], 'Search with ID "not in between" filter');
+
+ $query = [
+ 'id[value]' => 2,
+ 'id_op' => '>',
+ ];
+ $this->checkResults($query, [3, 4, 5], 'Search with ID "greater than" filter');
+ $query = [
+ 'id[value]' => 2,
+ 'id_op' => '!=',
+ ];
+ $this->checkResults($query, [1, 3, 4, 5], 'Search with ID "not equal" filter');
+ $query = [
+ 'id_op' => 'empty',
+ ];
+ $this->checkResults($query, [], 'Search with ID "empty" filter');
+ $query = [
+ 'id_op' => 'not empty',
+ ];
+ $this->checkResults($query, [1, 2, 3, 4, 5], 'Search with ID "not empty" filter');
+
+ $yesterday = strtotime('-1DAY');
+ $query = [
+ 'created[value]' => date('Y-m-d', $yesterday),
+ 'created_op' => '>',
+ ];
+ $this->checkResults($query, [1, 2, 3, 4, 5], 'Search with "Created after" filter');
+ $query = [
+ 'created[value]' => date('Y-m-d', $yesterday),
+ 'created_op' => '<',
+ ];
+ $this->checkResults($query, [], 'Search with "Created before" filter');
+ $query = [
+ 'created_op' => 'empty',
+ ];
+ $this->checkResults($query, [], 'Search with "empty creation date" filter');
+ $query = [
+ 'created_op' => 'not empty',
+ ];
+ $this->checkResults($query, [1, 2, 3, 4, 5], 'Search with "not empty creation date" filter');
+
+ $this->checkResults(['keywords[value]' => 'apple'], [2, 4], 'Search with Keywords filter');
+ $query = [
+ 'keywords[min]' => 'aardvark',
+ 'keywords[max]' => 'calypso',
+ 'keywords_op' => 'between',
+ ];
+ $this->checkResults($query, [2, 4, 5], 'Search with Keywords "in between" filter');
+
+ // For the keywords filters with comparison operators, exclude entity 1
+ // since that contains all the uppercase and special characters weirdness.
+ $query = [
+ 'id[value]' => 1,
+ 'id_op' => '!=',
+ 'keywords[value]' => 'melon',
+ 'keywords_op' => '>=',
+ ];
+ $this->checkResults($query, [2, 4, 5], 'Search with Keywords "greater than or equal" filter');
+ $query = [
+ 'id[value]' => 1,
+ 'id_op' => '!=',
+ 'keywords[value]' => 'banana',
+ 'keywords_op' => '<',
+ ];
+ $this->checkResults($query, [2, 4], 'Search with Keywords "less than" filter');
+ $query = [
+ 'keywords[value]' => 'orange',
+ 'keywords_op' => '!=',
+ ];
+ $this->checkResults($query, [3, 4], 'Search with Keywords "not equal" filter');
+ $query = [
+ 'keywords_op' => 'empty',
+ ];
+ $label = 'Search with Keywords "empty" filter';
+ $this->checkResults($query, [3], $label, 'all/all/all');
+ $query = [
+ 'keywords_op' => 'not empty',
+ ];
+ $this->checkResults($query, [1, 2, 4, 5], 'Search with Keywords "not empty" filter');
+
+ $query = [
+ 'name[value]' => 'foo',
+ ];
+ $this->checkResults($query, [1, 2, 4], 'Search with Name "contains" filter');
+ $query = [
+ 'name[value]' => 'foo',
+ 'name_op' => '!=',
+ ];
+ $this->checkResults($query, [3, 5], 'Search with Name "doesn\'t contain" filter');
+ $query = [
+ 'name_op' => 'empty',
+ ];
+ $this->checkResults($query, [], 'Search with Name "empty" filter');
+ $query = [
+ 'name_op' => 'not empty',
+ ];
+ $this->checkResults($query, [1, 2, 3, 4, 5], 'Search with Name "not empty" filter');
+
+ $query = [
+ 'language' => ['***LANGUAGE_site_default***'],
+ ];
+ $this->checkResults($query, [1, 2, 3, 4, 5], 'Search with "Page content language" filter');
+ $query = [
+ 'language' => ['en'],
+ ];
+ $this->checkResults($query, [1, 2, 3, 4, 5], 'Search with "English" language filter');
+ $query = [
+ 'language' => [Language::LANGCODE_NOT_SPECIFIED],
+ ];
+ $this->checkResults($query, [], 'Search with "Not specified" language filter');
+ $query = [
+ 'language' => [
+ '***LANGUAGE_language_interface***',
+ 'zxx',
+ ],
+ ];
+ $this->checkResults($query, [1, 2, 3, 4, 5], 'Search with multiple languages filter');
+
+ $query = [
+ 'search_api_fulltext' => 'foo to test',
+ 'id[value]' => 2,
+ 'id_op' => '>',
+ 'keywords_op' => 'not empty',
+ ];
+ $this->checkResults($query, [4], 'Search with multiple filters');
+
+ // Test contextual filters. Configured contextual filters are:
+ // 1: datasource
+ // 2: type (not = true)
+ // 3: keywords (break_phrase = true)
+ $this->checkResults([], [4, 5], 'Search with arguments', 'entity:entity_test_mulrev_changed/item/grape');
+
+ // "Type" doesn't have "break_phrase" enabled, so the second argument won't
+ // have any effect.
+ $this->checkResults([], [2, 4, 5], 'Search with arguments', 'all/item+article/strawberry+apple');
+
+ $this->checkResults([], [], 'Search with unknown datasource argument', 'entity:foobar/all/all');
+
+ $query = [
+ 'id[value]' => 1,
+ 'id_op' => '!=',
+ 'keywords[value]' => 'melon',
+ 'keywords_op' => '>=',
+ ];
+ $this->checkResults($query, [2, 5], 'Search with arguments and filters', 'entity:entity_test_mulrev_changed/all/orange');
+
+ // Make sure the datasource filter works correctly with multiple selections.
+ $index = Index::load($this->indexId);
+ $datasource = \Drupal::getContainer()
+ ->get('search_api.plugin_helper')
+ ->createDatasourcePlugin($index, 'entity:user');
+ $index->addDatasource($datasource);
+ $index->save();
+
+ $query = [
+ 'datasource' => ['entity:user', 'entity:entity_test_mulrev_changed'],
+ 'datasource_op' => 'or',
+ ];
+ $this->checkResults($query, [1, 2, 3, 4, 5], 'Search with multiple datasource filters (OR)');
+
+ $query = [
+ 'datasource' => ['entity:user', 'entity:entity_test_mulrev_changed'],
+ 'datasource_op' => 'and',
+ ];
+ $this->checkResults($query, [], 'Search with multiple datasource filters (AND)');
+
+ $query = [
+ 'datasource' => ['entity:user'],
+ 'datasource_op' => 'not',
+ ];
+ $this->checkResults($query, [1, 2, 3, 4, 5], 'Search for non-user results');
+
+ $query = [
+ 'datasource' => ['entity:entity_test_mulrev_changed'],
+ 'datasource_op' => 'not',
+ ];
+ $this->checkResults($query, [], 'Search for non-test entity results');
+
+ $query = [
+ 'datasource' => ['entity:user', 'entity:entity_test_mulrev_changed'],
+ 'datasource_op' => 'not',
+ ];
+ $this->checkResults($query, [], 'Search for results of no available datasource');
+
+ $this->regressionTests();
+
+ // Make sure there was a display plugin created for this view.
+ /** @var \Drupal\search_api\Display\DisplayInterface[] $displays */
+ $displays = \Drupal::getContainer()
+ ->get('plugin.manager.search_api.display')
+ ->getInstances();
+
+ $display_id = 'views_page:search_api_test_view__page_1';
+ $this->assertArrayHasKey($display_id, $displays, 'A display plugin was created for the test view page display.');
+ $this->assertArrayHasKey('views_block:search_api_test_view__block_1', $displays, 'A display plugin was created for the test view block display.');
+ $this->assertArrayHasKey('views_rest:search_api_test_view__rest_export_1', $displays, 'A display plugin was created for the test view block display.');
+ $this->assertEquals('/search-api-test', $displays[$display_id]->getPath(), 'Display returns the correct path.');
+ $view_url = Url::fromUserInput('/search-api-test')->toString();
+ $this->assertEquals($view_url, $displays[$display_id]->getUrl()->toString(), 'Display returns the correct URL.');
+ $this->assertNull($displays['views_block:search_api_test_view__block_1']->getPath(), 'Block display returns the correct path.');
+ $this->assertEquals('/search-api-rest-test', $displays['views_rest:search_api_test_view__rest_export_1']->getPath(), 'REST display returns the correct path.');
+
+ $this->assertEquals('database_search_index', $displays[$display_id]->getIndex()->id(), 'Display returns the correct search index.');
+
+ $admin_user = $this->drupalCreateUser([
+ 'administer search_api',
+ 'access administration pages',
+ 'administer views',
+ ]);
+ $this->drupalLogin($admin_user);
+
+ // Delete the page display for the view.
+ $this->drupalGet('admin/structure/views/view/search_api_test_view');
+ $this->submitForm([], 'Delete Page');
+ $this->submitForm([], 'Save');
+
+ drupal_flush_all_caches();
+
+ $displays = \Drupal::getContainer()
+ ->get('plugin.manager.search_api.display')
+ ->getInstances();
+ $this->assertArrayNotHasKey('views_page:search_api_test_view__page_1', $displays, 'No display plugin was created for the test view page display.');
+ $this->assertArrayHasKey('views_block:search_api_test_view__block_1', $displays, 'A display plugin was created for the test view block display.');
+ $this->assertArrayHasKey('views_rest:search_api_test_view__rest_export_1', $displays, 'A display plugin was created for the test view block display.');
+ }
+
+ /**
+ * Contains regression tests for previous, fixed bugs.
+ */
+ protected function regressionTests() {
+ $this->regressionTest2869121();
+ }
+
+ /**
+ * Tests setting the "Fulltext search" filter to "Required".
+ *
+ * This previously caused problems with form validation and caching.
+ *
+ * @see https://www.drupal.org/node/2869121
+ * @see https://www.drupal.org/node/2873246
+ * @see https://www.drupal.org/node/2871030
+ */
+ protected function regressionTest2869121() {
+ // Make sure setting the fulltext filter to "Required" works as expected.
+ $view = View::load('search_api_test_view');
+ $displays = $view->get('display');
+ $displays['default']['display_options']['filters']['search_api_fulltext']['expose']['required'] = TRUE;
+ $displays['default']['display_options']['cache']['type'] = 'search_api_time';
+ $view->set('display', $displays);
+ $view->save();
+
+ $this->checkResults([], [], 'Search without required fulltext keywords');
+ $this->assertSession()->responseNotContains('Error message');
+ $this->checkResults(
+ ['search_api_fulltext' => 'foo test'],
+ [1, 2, 4],
+ 'Search for multiple words'
+ );
+ $this->assertSession()->responseNotContains('Error message');
+ $this->checkResults(
+ ['search_api_fulltext' => 'fo'],
+ [],
+ 'Search for short word'
+ );
+ $this->assertSession()->pageTextContains('You must include at least one positive keyword with 3 characters or more');
+
+ // Make sure this also works with the exposed form in a block, and doesn't
+ // throw fatal errors on all pages with the block.
+ $view = View::load('search_api_test_view');
+ $displays = $view->get('display');
+ $displays['page_1']['display_options']['exposed_block'] = TRUE;
+ $view->set('display', $displays);
+ $view->save();
+
+ Block::create([
+ 'id' => 'search_api_test_view',
+ 'theme' => 'classy',
+ 'weight' => -20,
+ 'plugin' => 'views_exposed_filter_block:search_api_test_view-page_1',
+ 'region' => 'content',
+ ])->save();
+
+ $this->drupalGet('');
+ // We submit the form three times, to make extra sure all Views caches are
+ // triggered.
+ for ($i = 0; $i < 3; ++$i) {
+ // Flush the page-level caches to make sure the Views cache plugin is
+ // used (so we could reproduce the bug if it's there).
+ \Drupal::getContainer()->get('cache.page')->deleteAll();
+ \Drupal::getContainer()->get('cache.dynamic_page_cache')->deleteAll();
+ $this->submitForm([], 'Search');
+ $this->assertSession()->addressEquals('search-api-test');
+ $this->assertSession()->responseNotContains('Error message');
+ $this->assertSession()->pageTextNotContains('search results');
+ // Make sure the Views cache was used, none of the two page caches.
+ $this->assertSession()->responseHeaderEquals('X-Drupal-Cache', 'MISS');
+ $this->assertSession()
+ ->responseHeaderEquals('X-Drupal-Dynamic-Cache', 'MISS');
+ }
+ }
+
+ /**
+ * Checks the Views results for a certain set of parameters.
+ *
+ * @param array $query
+ * The GET parameters to set for the view.
+ * @param int[]|null $expected_results
+ * (optional) The IDs of the expected results; or NULL to skip checking the
+ * results.
+ * @param string $label
+ * (optional) A label for this search, to include in assert messages.
+ * @param string $arguments
+ * (optional) A string to append to the search path.
+ */
+ protected function checkResults(array $query, array $expected_results = NULL, $label = 'Search', $arguments = '') {
+ $this->drupalGet('search-api-test/' . $arguments, ['query' => $query]);
+
+ if (isset($expected_results)) {
+ $count = count($expected_results);
+ if ($count) {
+ $this->assertSession()->pageTextContains("Displaying $count search results");
+ }
+ else {
+ $this->assertSession()->pageTextNotContains('search results');
+ }
+
+ $expected_results = array_combine($expected_results, $expected_results);
+ $actual_results = [];
+ foreach ($this->entities as $id => $entity) {
+ $entity_label = Html::escape($entity->label());
+ if (strpos($this->getSession()->getPage()->getContent(), ">$entity_label<") !== FALSE) {
+ $actual_results[$id] = $id;
+ }
+ }
+ $this->assertEquals($expected_results, $actual_results, "$label returned correct results.");
+ }
+ }
+
+ /**
+ * Tests results are ordered correctly and react to exposed sorts.
+ */
+ public function testViewSorts() {
+ // Check default ordering, first exposed sort in config is
+ // search_api_relevance.
+ $this->checkResultsOrder([], [1, 2, 3, 4, 5]);
+
+ // Make sure the exposed sort works.
+ $query = [
+ 'sort_by' => 'search_api_id_desc',
+ ];
+ $this->checkResultsOrder($query, [5, 4, 3, 2, 1]);
+ }
+
+ /**
+ * Checks whether Views results are in a certain order in the sorts test view.
+ *
+ * @param array $query
+ * The GET parameters to set for the view.
+ * @param int[] $expected_results
+ * The IDs of the expected results.
+ *
+ * @see views.view.search_api_test_sorts.yml
+ */
+ protected function checkResultsOrder(array $query, array $expected_results) {
+ $this->drupalGet('search-api-test-sorts', ['query' => $query]);
+
+ $web_assert = $this->assertSession();
+ $rows_xpath = '//div[contains(@class, "views-row")]';
+ $web_assert->elementsCount('xpath', $rows_xpath, count($expected_results));
+ foreach (array_values($expected_results) as $i => $id) {
+ $entity_label = Html::escape($this->entities[$id]->label());
+ // XPath offsets are 1-based, not 0-based.
+ ++$i;
+ $web_assert->elementContains('xpath', "($rows_xpath)[$i]", $entity_label);
+ }
+ }
+
+ /**
+ * Tests the Views admin UI and field handlers.
+ */
+ public function testViewsAdmin() {
+ // Add a field from a related entity to the index to test whether it gets
+ // displayed correctly.
+ /** @var \Drupal\search_api\IndexInterface $index */
+ $index = Index::load($this->indexId);
+ $datasource_id = 'entity:entity_test_mulrev_changed';
+ $field = \Drupal::getContainer()
+ ->get('search_api.fields_helper')
+ ->createField($index, 'author', [
+ 'label' => 'Author name',
+ 'type' => 'string',
+ 'datasource_id' => $datasource_id,
+ 'property_path' => 'user_id:entity:name',
+ ]);
+ $index->addField($field);
+ $field = \Drupal::getContainer()
+ ->get('search_api.fields_helper')
+ ->createField($index, 'rendered_item', [
+ 'label' => 'Rendered HTML output',
+ 'type' => 'text',
+ 'property_path' => 'rendered_item',
+ 'configuration' => [
+ 'roles' => [AccountInterface::ANONYMOUS_ROLE],
+ 'view_mode' => [],
+ ],
+ ]);
+ $index->addField($field);
+ $index->save();
+
+ // Add some Dutch nodes.
+ foreach ([1, 2, 3, 4, 5] as $id) {
+ $entity = EntityTestMulRevChanged::load($id);
+ $entity = $entity->addTranslation('nl', [
+ 'body' => "dutch node $id",
+ 'category' => "dutch category $id",
+ 'keywords' => ["dutch $id A", "dutch $id B"],
+ ]);
+ $entity->save();
+ }
+ $this->entities = EntityTestMulRevChanged::loadMultiple();
+ $this->indexItems($this->indexId);
+
+ // For viewing the user name and roles of the user associated with test
+ // entities, the logged-in user needs to have the permission to administer
+ // both users and permissions.
+ $permissions = [
+ 'administer search_api',
+ 'access administration pages',
+ 'administer views',
+ 'administer users',
+ 'administer permissions',
+ ];
+ $admin_user = $this->drupalCreateUser($permissions);
+ $this->drupalLogin($admin_user);
+
+ $this->drupalGet('admin/structure/views/view/search_api_test_view');
+ $this->assertSession()->statusCodeEquals(200);
+
+ // Set the user IDs associated with our test entities.
+ $users[] = $this->createUser();
+ $users[] = $this->createUser();
+ $users[] = $this->createUser();
+ $this->entities[1]->setOwnerId($users[0]->id())->save();
+ $this->entities[2]->setOwnerId($users[0]->id())->save();
+ $this->entities[3]->setOwnerId($users[1]->id())->save();
+ $this->entities[4]->setOwnerId($users[1]->id())->save();
+ $this->entities[5]->setOwnerId($users[2]->id())->save();
+
+ // Switch to "Table" format.
+ $this->clickLink('Unformatted list');
+ $this->assertSession()->statusCodeEquals(200);
+ $edit = [
+ 'style[type]' => 'table',
+ ];
+ $this->submitForm($edit, 'Apply');
+ $this->assertSession()->statusCodeEquals(200);
+ $this->submitForm([], 'Apply');
+ $this->assertSession()->statusCodeEquals(200);
+
+ // Add the "User ID" relationship.
+ $this->clickLink('Add relationships');
+ $edit = [
+ 'name[search_api_datasource_database_search_index_entity_entity_test_mulrev_changed.user_id]' => 'search_api_datasource_database_search_index_entity_entity_test_mulrev_changed.user_id',
+ ];
+ $this->submitForm($edit, 'Add and configure relationships');
+ $this->submitForm([], 'Apply');
+
+ // Add new fields. First check that the listing seems correct.
+ $this->clickLink('Add fields');
+ $this->assertSession()->statusCodeEquals(200);
+ $this->assertSession()->pageTextContains('Test entity - revisions and data table datasource');
+ $this->assertSession()->pageTextContains('Authored on');
+ $this->assertSession()->pageTextContains('Body (indexed field)');
+ $this->assertSession()->pageTextContains('Index Test index');
+ $this->assertSession()->pageTextContains('Item ID');
+ $this->assertSession()->pageTextContains('Excerpt');
+ $this->assertSession()->pageTextContains('The search result excerpted to show found search terms');
+ $this->assertSession()->pageTextContains('Relevance');
+ $this->assertSession()->pageTextContains('The relevance of this search result with respect to the query');
+ $this->assertSession()->pageTextContains('Language code');
+ $this->assertSession()->pageTextContains('The user language code.');
+ $this->assertSession()->pageTextContains('(No description available)');
+ $this->assertSession()->pageTextNotContains('Error: missing help');
+
+ // Then add some fields.
+ $fields = [
+ 'views.counter',
+ 'search_api_datasource_database_search_index_entity_entity_test_mulrev_changed.id',
+ 'search_api_index_database_search_index.search_api_datasource',
+ 'search_api_datasource_database_search_index_entity_entity_test_mulrev_changed.body',
+ 'search_api_index_database_search_index.category',
+ 'search_api_index_database_search_index.keywords',
+ 'search_api_datasource_database_search_index_entity_entity_test_mulrev_changed.user_id',
+ 'search_api_entity_user.name',
+ 'search_api_index_database_search_index.author',
+ 'search_api_entity_user.roles',
+ 'search_api_index_database_search_index.rendered_item',
+ ];
+ $edit = [];
+ foreach ($fields as $field) {
+ $edit["name[$field]"] = $field;
+ }
+ $this->submitForm($edit, 'Add and configure fields');
+ $this->assertSession()->statusCodeEquals(200);
+
+ // @todo For some strange reason, the "roles" field form is not included
+ // automatically in the series of field forms shown to us by Views. Deal
+ // with this graciously (since it's not really our fault, I hope), but it
+ // would be great to have this working normally.
+ $get_field_id = function ($key) {
+ return Utility::splitPropertyPath($key, TRUE, '.')[1];
+ };
+ $fields = array_map($get_field_id, $fields);
+ $fields = array_combine($fields, $fields);
+ for ($i = 0; $i < count($fields); ++$i) {
+ $field = $this->submitFieldsForm();
+ if (!$field) {
+ break;
+ }
+ unset($fields[$field]);
+ }
+ foreach ($fields as $field) {
+ $this->drupalGet('admin/structure/views/nojs/handler/search_api_test_view/page_1/field/' . $field);
+ $this->submitFieldsForm();
+ }
+
+ // Add click sorting for all fields where this is possible.
+ $this->clickLink('Settings', 0);
+ $edit = [
+ 'style_options[info][search_api_datasource][sortable]' => 1,
+ 'style_options[info][category][sortable]' => 1,
+ 'style_options[info][keywords][sortable]' => 1,
+ ];
+ $this->submitForm($edit, 'Apply');
+
+ // Add a filter for the "Name" field.
+ $this->clickLink('Add filter criteria');
+ $edit = [
+ 'name[search_api_index_database_search_index.name]' => 'search_api_index_database_search_index.name',
+ ];
+ $this->submitForm($edit, 'Add and configure filter criteria');
+ $edit = [
+ 'options[expose_button][checkbox][checkbox]' => 1,
+ ];
+ $this->submitForm($edit, 'Expose filter');
+ $this->submitPluginForm([]);
+
+ // Save the view.
+ $this->submitForm([], 'Save');
+ $this->assertSession()->statusCodeEquals(200);
+
+ // Check the results.
+ $this->drupalGet('search-api-test');
+ $this->assertSession()->statusCodeEquals(200);
+
+ foreach ($this->entities as $id => $entity) {
+ $fields = [
+ 'search_api_datasource',
+ 'id',
+ 'body',
+ 'category',
+ 'keywords',
+ 'user_id',
+ 'user_id:name',
+ 'user_id:roles',
+ ];
+ foreach ($fields as $field) {
+ $field_entity = $entity;
+ while (strpos($field, ':')) {
+ list($direct_property, $field) = Utility::splitPropertyPath($field, FALSE);
+ if (empty($field_entity->{$direct_property}[0]->entity)) {
+ continue 2;
+ }
+ $field_entity = $field_entity->{$direct_property}[0]->entity;
+ }
+ // Check that both the English and the Dutch entity are present in the
+ // results, with their correct field values.
+ $entities = [$field_entity];
+ if ($field_entity->hasTranslation('nl')) {
+ $entities[] = $field_entity->getTranslation('nl');
+ }
+ foreach ($entities as $i => $field_entity) {
+ if ($field != 'search_api_datasource') {
+ $data = \Drupal::getContainer()
+ ->get('search_api.fields_helper')
+ ->extractFieldValues($field_entity->get($field));
+ if (!$data) {
+ $data = ['[EMPTY]'];
+ }
+ }
+ else {
+ $data = [$datasource_id];
+ }
+ $row_num = 2 * $id + $i - 1;
+ $prefix = "#$row_num [$field] ";
+ $text = $prefix . implode("|$prefix", $data);
+ $this->assertSession()->pageTextContains($text);
+ // Special case for field "author", which duplicates content of
+ // "name".
+ if ($field === 'name') {
+ $text = str_replace('[name]', '[author]', $text);
+ $this->assertSession()->pageTextContains($text);
+ }
+ }
+ }
+ }
+
+ // Check whether the expected retrieved properties were listed on the page.
+ // Since the fields with the "field_rendering" option enabled will need the
+ // complete loaded entity, these are only present as "_object" here.
+ // @see search_api_test_views_search_api_query_alter()
+ $retrieved_properties = [
+ Utility::createCombinedId($datasource_id, 'id'),
+ Utility::createCombinedId($datasource_id, '_object'),
+ Utility::createCombinedId($datasource_id, 'keywords'),
+ Utility::createCombinedId($datasource_id, 'user_id'),
+ Utility::createCombinedId($datasource_id, 'user_id:entity:_object'),
+ Utility::createCombinedId($datasource_id, 'user_id:entity:roles'),
+ Utility::createCombinedId(NULL, 'rendered_item'),
+ ];
+ foreach ($retrieved_properties as $combined_property_path) {
+ $this->assertSession()->pageTextContains("'$combined_property_path'");
+ }
+
+ // Check that click-sorting works correctly.
+ $options = [
+ 'query' => [
+ 'order' => 'category',
+ 'sort' => 'asc',
+ ],
+ ];
+ $this->drupalGet('search-api-test', $options);
+ $this->assertSession()->statusCodeEquals(200);
+ $ordered_categories = [
+ '[EMPTY]',
+ 'article_category',
+ 'article_category',
+ 'dutch category 1',
+ 'dutch category 2',
+ 'dutch category 3',
+ 'dutch category 4',
+ 'dutch category 5',
+ 'item_category',
+ 'item_category',
+ ];
+ foreach ($ordered_categories as $i => $category) {
+ ++$i;
+ $this->assertSession()->pageTextContains("#$i [category] $category");
+ }
+ $options['query']['sort'] = 'desc';
+ $this->drupalGet('search-api-test', $options);
+ $this->assertSession()->statusCodeEquals(200);
+ foreach (array_reverse($ordered_categories) as $i => $category) {
+ ++$i;
+ $this->assertSession()->pageTextContains("#$i [category] $category");
+ }
+
+ // Check the results with an anonymous visitor. All "name" fields should be
+ // empty.
+ $this->drupalLogout();
+ $this->drupalGet('search-api-test');
+ $this->assertSession()->statusCodeEquals(200);
+ $html = $this->getSession()->getPage()->getContent();
+ $this->assertEquals(10, substr_count($html, '[name] [EMPTY]'));
+
+ // Set "Skip access checks" on the "user_id" relationship and check again.
+ // The "name" field should now be listed regardless.
+ $this->drupalLogin($admin_user);
+ $this->drupalGet('admin/structure/views/nojs/handler/search_api_test_view/page_1/relationship/user_id');
+ $this->submitForm(['options[skip_access]' => 1], 'Apply');
+ $this->submitForm([], 'Save');
+ $this->assertSession()->statusCodeEquals(200);
+
+ $this->drupalLogout();
+ $this->drupalGet('search-api-test');
+ $this->assertSession()->statusCodeEquals(200);
+ $this->assertSession()->pageTextNotContains('[name] [EMPTY]');
+
+ // Run regression tests.
+ $this->drupalLogin($admin_user);
+ $this->adminUiRegressionTests();
+ }
+
+ /**
+ * Submits the field handler config form currently displayed.
+ *
+ * @return string|null
+ * The field ID of the field whose form was submitted. Or NULL if the
+ * current page is no field form.
+ */
+ protected function submitFieldsForm() {
+ $url_parts = explode('/', $this->getUrl());
+ $field = array_pop($url_parts);
+ if (array_pop($url_parts) != 'field') {
+ return NULL;
+ }
+
+ $edit['options[fallback_options][multi_separator]'] = '|';
+ $edit['options[alter][alter_text]'] = TRUE;
+ $edit['options[alter][text]'] = "#{{counter}} [$field] {{ $field }}";
+ $edit['options[empty]'] = "#{{counter}} [$field] [EMPTY]";
+
+ switch ($field) {
+ case 'counter':
+ $edit = [
+ 'options[exclude]' => TRUE,
+ ];
+ break;
+
+ case 'id':
+ $edit['options[field_rendering]'] = FALSE;
+ break;
+
+ case 'search_api_datasource':
+ unset($edit['options[fallback_options][multi_separator]']);
+ break;
+
+ case 'body':
+ break;
+
+ case 'category':
+ break;
+
+ case 'keywords':
+ $edit['options[field_rendering]'] = FALSE;
+ break;
+
+ case 'user_id':
+ $edit['options[field_rendering]'] = FALSE;
+ $edit['options[fallback_options][display_methods][user][display_method]'] = 'id';
+ break;
+
+ case 'author':
+ break;
+
+ case 'roles':
+ $edit['options[field_rendering]'] = FALSE;
+ $edit['options[fallback_options][display_methods][user_role][display_method]'] = 'id';
+ break;
+
+ case 'rendered_item':
+ // "Rendered item" isn't based on a Field API field, so there is no
+ // "Fallback options" form (added otherwise by SearchApiEntityField).
+ unset($edit['options[fallback_options][multi_separator]']);
+ break;
+ }
+
+ $this->submitPluginForm($edit);
+
+ return $field;
+ }
+
+ /**
+ * Submits a Views plugin's configuration form.
+ *
+ * @param array $edit
+ * The values to set in the form.
+ */
+ protected function submitPluginForm(array $edit) {
+ $button_label = 'Apply';
+ $buttons = $this->xpath('//input[starts-with(@value, :label)]', [':label' => $button_label]);
+ if ($buttons) {
+ $button_label = $buttons[0]->getAttribute('value');
+ }
+
+ $this->submitForm($edit, $button_label);
+ $this->assertSession()->statusCodeEquals(200);
+ }
+
+ /**
+ * Contains regression tests for previous, fixed bugs in the Views UI.
+ */
+ protected function adminUiRegressionTests() {
+ $this->regressionTest2883807();
+ }
+
+ /**
+ * Verifies that adding a contextual filter doesn't trigger a notice.
+ *
+ * @see https://www.drupal.org/node/2883807
+ */
+ protected function regressionTest2883807() {
+ $this->drupalGet('admin/structure/views/nojs/add-handler/search_api_test_view/page_1/argument');
+ $edit = [
+ 'name[search_api_index_database_search_index.author]' => TRUE,
+ ];
+ $this->submitForm($edit, 'Add and configure contextual filters');
+ $this->submitForm([], 'Apply');
+ $this->submitForm([], 'Save');
+ }
+
+ /**
+ * Checks whether highlighting of results works correctly.
+ *
+ * @see views.view.search_api_test_cache.yml
+ */
+ public function testHighlighting() {
+ // Add the Highlight processor to the search index.
+ $index = Index::load('database_search_index');
+ $processor = $this->container
+ ->get('search_api.plugin_helper')
+ ->createProcessorPlugin($index, 'highlight');
+ $index->addProcessor($processor);
+ $index->save();
+
+ $path = 'search-api-test-search-view-caching-none';
+ $this->drupalGet($path);
+ $this->assertSession()->responseContains('foo bar baz');
+
+ $options['query']['search_api_fulltext'] = 'foo';
+ $this->drupalGet($path, $options);
+ $this->assertSession()->responseContains('foo bar baz');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function initConfig(ContainerInterface $container) {
+ parent::initConfig($container);
+
+ // This will just set the Drupal state to include the necessary bundles for
+ // our test entity type. Otherwise, fields from those bundles won't be found
+ // and thus removed from the test index. (We can't do it in setUp(), before
+ // calling the parent method, since the container isn't set up at that
+ // point.)
+ $bundles = [
+ 'entity_test_mulrev_changed' => ['label' => 'Entity Test Bundle'],
+ 'item' => ['label' => 'item'],
+ 'article' => ['label' => 'article'],
+ ];
+ \Drupal::state()->set('entity_test_mulrev_changed.bundles', $bundles);
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/FunctionalJavascript/AjaxTest.php b/sites/all/modules/contrib/search/search_api/tests/src/FunctionalJavascript/AjaxTest.php
new file mode 100644
index 000000000..929080762
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/FunctionalJavascript/AjaxTest.php
@@ -0,0 +1,162 @@
+drupalCreateContentType([
+ 'type' => 'article',
+ 'name' => 'Article',
+ ]);
+ }
+
+ // Create a page node type, if not already present.
+ if (!NodeType::load('page')) {
+ $this->drupalCreateContentType([
+ 'type' => 'page',
+ 'name' => 'Page',
+ ]);
+ }
+
+ // 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);
+ }
+
+ $permissions = [
+ 'administer search_api',
+ 'access administration pages',
+ 'administer nodes',
+ 'bypass node access',
+ 'administer content types',
+ 'administer node fields',
+ ];
+ $this->drupalLogin($this->drupalCreateUser($permissions));
+ }
+
+ /**
+ * Tests AJAX functionality in the Search API module.
+ */
+ public function testAjax() {
+ $this->checkServerBackendAjax();
+ $this->checkIndexDatasourceAjax();
+ }
+
+ /**
+ * Tests AJAX display of backend config forms when a new backend is selected.
+ */
+ protected function checkServerBackendAjax() {
+ $assert_session = $this->assertSession();
+
+ $this->drupalGet('admin/config/search/search-api/add-server');
+ $assert_session->pageTextContains('Database');
+ $assert_session->pageTextContains('Test backend');
+
+ $backend_config = '[data-drupal-selector="edit-backend-config"]';
+ $assert_session->elementNotExists('css', "$backend_config input");
+ $assert_session->elementNotExists('css', "$backend_config select");
+ $assert_session->elementNotExists('css', "$backend_config button");
+
+ $this->click('input.form-radio[name="backend"][value="search_api_db"]');
+
+ $element = $assert_session->waitForElement('css', "$backend_config input");
+ $this->assertNotEmpty($element);
+ $assert_session->elementExists('css', "$backend_config select[name=\"backend_config[min_chars]\"]");
+ }
+
+ /**
+ * Tests AJAX display of backend config forms when a new backend is selected.
+ */
+ protected function checkIndexDatasourceAjax() {
+ $assert_session = $this->assertSession();
+
+ $this->drupalGet('admin/config/search/search-api/add-index');
+ $assert_session->pageTextContains('Datasources');
+ $assert_session->pageTextContains('Content');
+
+ $datasource_config = '[data-drupal-selector="edit-datasource-configs"]';
+ $assert_session->elementNotExists('css', "$datasource_config input");
+ $assert_session->elementNotExists('css', "$datasource_config select");
+ $assert_session->elementNotExists('css', "$datasource_config button");
+
+ $this->click('input.form-checkbox[name="datasources[entity:node]"]');
+
+ $element = $assert_session->waitForElement('css', "$datasource_config input");
+ $this->assertNotEmpty($element);
+ $assert_session->elementExists('css', "$datasource_config [name=\"datasource_configs[entity:node][bundles][default]\"]");
+
+ $field = $assert_session
+ ->elementExists('css', 'input[data-drupal-selector="edit-name"]');
+ $field->setValue('Test index');
+ $element = $assert_session->waitForElementVisible('css', '.field-suffix .machine-name-value');
+ $this->assertNotEmpty($element);
+ $this->assertEquals('test_index', $element->getText());
+
+ $this->click('[data-drupal-selector="edit-actions-submit"]');
+ }
+
+ /**
+ * Tests JS-based display of processors when they are added.
+ */
+ protected function checkIndexProcessorJavascript() {
+ $assert_session = $this->assertSession();
+
+ $this->drupalGet('admin/config/search/search-api/index/test_index/processors');
+ $assert_session->pageTextContains('Highlight');
+ $assert_session->checkboxNotChecked('status[highlight]');
+
+ $postprocess = '[data-drupal-selector="edit-weights-postprocess-query"]';
+ $postprocess_highlight = $postprocess . ' [data-drupal-selector="edit-weights-postprocess-query-order-highlight"]';
+ $element = $assert_session->elementExists('css', $postprocess_highlight);
+ $this->assertFalse($element->isVisible());
+
+ $settings = '[data-drupal-selector="edit-processor-settings"]';
+ $highlight_settings = $settings . ' a[href="#edit-processors-html-filter-settings"]';
+ $element = $assert_session->elementExists('css', $highlight_settings);
+ $this->assertFalse($element->isVisible());
+
+ $this->click('input.form-checkbox[name="status[highlight]"]');
+
+ $element = $assert_session->elementExists('css', $postprocess_highlight);
+ $this->assertFalse($element->isVisible());
+
+ $element = $assert_session->elementExists('css', $highlight_settings);
+ $this->assertFalse($element->isVisible());
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Kernel/BackendTestBase.php b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/BackendTestBase.php
new file mode 100644
index 000000000..840381921
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/BackendTestBase.php
@@ -0,0 +1,1178 @@
+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_test_example_content');
+ $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->setUpExampleStructure();
+ }
+
+ /**
+ * Tests various indexing scenarios for the search backend.
+ *
+ * Uses a single method to save time.
+ */
+ public function testBackend() {
+ $this->insertExampleContent();
+ $this->checkDefaultServer();
+ $this->checkServerBackend();
+ $this->checkDefaultIndex();
+ $this->updateIndex();
+ $this->searchNoResults();
+ $this->indexItems($this->indexId);
+ $this->searchSuccess();
+ $this->checkFacets();
+ $this->checkSecondServer();
+ $this->regressionTests();
+ $this->clearIndex();
+
+ $this->indexItems($this->indexId);
+ $this->backendSpecificRegressionTests();
+ $this->checkBackendSpecificFeatures();
+ $this->clearIndex();
+
+ $this->enableHtmlFilter();
+ $this->indexItems($this->indexId);
+ $this->disableHtmlFilter();
+ $this->clearIndex();
+
+ $this->searchNoResults();
+ $this->regressionTests2();
+
+ $this->checkIndexWithoutFields();
+
+ $this->checkModuleUninstall();
+ }
+
+ /**
+ * Tests the correct setup of the server backend.
+ */
+ abstract protected function checkServerBackend();
+
+ /**
+ * Checks whether changes to the index's fields are picked up by the server.
+ */
+ abstract protected function updateIndex();
+
+ /**
+ * Tests that a second server doesn't interfere with the first.
+ */
+ abstract protected function checkSecondServer();
+
+ /**
+ * Tests whether removing the configuration again works as it should.
+ */
+ abstract protected function checkModuleUninstall();
+
+ /**
+ * Checks backend specific features.
+ */
+ protected function checkBackendSpecificFeatures() {}
+
+ /**
+ * Runs backend specific regression tests.
+ */
+ protected function backendSpecificRegressionTests() {}
+
+ /**
+ * Tests the server that was installed through default configuration files.
+ */
+ protected function checkDefaultServer() {
+ $server = $this->getServer();
+ $this->assertTrue((bool) $server, 'The server was successfully created.');
+ }
+
+ /**
+ * Tests the index that was installed through default configuration files.
+ */
+ protected function checkDefaultIndex() {
+ $index = $this->getIndex();
+ $this->assertTrue((bool) $index, 'The index was successfully created.');
+
+ $this->assertEquals(["entity:entity_test_mulrev_changed"], $index->getDatasourceIds(), 'Datasources are set correctly.');
+ $this->assertEquals('default', $index->getTrackerId(), 'Tracker is set correctly.');
+
+ $this->assertEquals(5, $index->getTrackerInstance()->getTotalItemsCount(), 'Correct item count.');
+ $this->assertEquals(0, $index->getTrackerInstance()->getIndexedItemsCount(), 'All items still need to be indexed.');
+ }
+
+ /**
+ * Enables the "HTML Filter" processor for the index.
+ */
+ protected function enableHtmlFilter() {
+ $index = $this->getIndex();
+
+ /** @var \Drupal\search_api\Processor\ProcessorInterface $processor */
+ $processor = \Drupal::getContainer()
+ ->get('search_api.plugin_helper')
+ ->createProcessorPlugin($index, 'html_filter');
+ $index->addProcessor($processor)->save();
+
+ $this->assertArrayHasKey('html_filter', $index->getProcessors(), 'HTML filter processor is added.');
+ }
+
+ /**
+ * Disables the "HTML Filter" processor for the index.
+ */
+ protected function disableHtmlFilter() {
+ $index = $this->getIndex();
+ $index->removeProcessor('html_filter');
+ $index->save();
+
+ $this->assertArrayNotHasKey('html_filter', $index->getProcessors(), 'HTML filter processor is removed.');
+ }
+
+ /**
+ * Builds a search query for testing purposes.
+ *
+ * Used as a helper method during testing.
+ *
+ * @param string|array|null $keys
+ * (optional) The search keys to set, if any.
+ * @param string[] $conditions
+ * (optional) Conditions to set on the query, in the format "field,value".
+ * @param string[]|null $fields
+ * (optional) Fulltext fields to search for the keys.
+ * @param bool $place_id_sort
+ * (optional) Whether to place a default sort on the item ID.
+ *
+ * @return \Drupal\search_api\Query\QueryInterface
+ * A search query on the test index.
+ */
+ protected function buildSearch($keys = NULL, array $conditions = [], array $fields = NULL, $place_id_sort = TRUE) {
+ static $i = 0;
+
+ $query = $this->getIndex()->query();
+ if ($keys) {
+ $query->keys($keys);
+ if ($fields) {
+ $query->setFulltextFields($fields);
+ }
+ }
+ foreach ($conditions as $condition) {
+ list($field, $value) = explode(',', $condition, 2);
+ $query->addCondition($field, $value);
+ }
+ $query->range(0, 10);
+ if ($place_id_sort) {
+ // Use the normal "id" and the magic "search_api_id" field alternately, to
+ // make sure both work as expected.
+ $query->sort((++$i % 2) ? 'id' : 'search_api_id');
+ }
+
+ return $query;
+ }
+
+ /**
+ * Tests that a search on the index doesn't have any results.
+ */
+ protected function searchNoResults() {
+ $results = $this->buildSearch('test')->execute();
+ $this->assertResults([], $results, 'Search before indexing');
+ }
+
+ /**
+ * Tests whether some test searches have the correct results.
+ */
+ protected function searchSuccess() {
+ $results = $this->buildSearch('test')->range(1, 2)->execute();
+ $this->assertEquals(4, $results->getResultCount(), 'Search for »test« returned correct number of results.');
+ $this->assertEquals($this->getItemIds([2, 3]), array_keys($results->getResultItems()), 'Search for »test« returned correct result.');
+ $this->assertEmpty($results->getIgnoredSearchKeys());
+ $this->assertEmpty($results->getWarnings());
+
+ $id = $this->getItemIds([2])[0];
+ $this->assertEquals($id, key($results->getResultItems()));
+ $this->assertEquals($id, $results->getResultItems()[$id]->getId());
+ $this->assertEquals('entity:entity_test_mulrev_changed', $results->getResultItems()[$id]->getDatasourceId());
+
+ $results = $this->buildSearch('test foo')->execute();
+ $this->assertResults([1, 2, 4], $results, 'Search for »test foo«');
+
+ $results = $this->buildSearch('foo', ['type,item'])->execute();
+ $this->assertResults([1, 2], $results, 'Search for »foo«');
+
+ $keys = [
+ '#conjunction' => 'AND',
+ 'test',
+ [
+ '#conjunction' => 'OR',
+ 'baz',
+ 'foobar',
+ ],
+ [
+ '#conjunction' => 'OR',
+ '#negation' => TRUE,
+ 'bar',
+ 'fooblob',
+ ],
+ ];
+ $results = $this->buildSearch($keys)->execute();
+ $this->assertResults([4], $results, 'Complex search 1');
+
+ $query = $this->buildSearch();
+ $conditions = $query->createConditionGroup('OR');
+ $conditions->addCondition('name', 'bar');
+ $conditions->addCondition('body', 'bar');
+ $query->addConditionGroup($conditions);
+ $results = $query->execute();
+ $this->assertResults([1, 2, 3, 5], $results, 'Search with multi-field fulltext filter');
+
+ $results = $this->buildSearch()
+ ->addCondition('keywords', ['grape', 'apple'], 'IN')
+ ->execute();
+ $this->assertResults([2, 4, 5], $results, 'Query with IN filter');
+
+ $results = $this->buildSearch()->addCondition('keywords', ['grape', 'apple'], 'NOT IN')->execute();
+ $this->assertResults([1, 3], $results, 'Query with NOT IN filter');
+
+ $results = $this->buildSearch()->addCondition('width', ['0.9', '1.5'], 'BETWEEN')->execute();
+ $this->assertResults([4], $results, 'Query with BETWEEN filter');
+
+ $results = $this->buildSearch()
+ ->addCondition('width', ['0.9', '1.5'], 'NOT BETWEEN')
+ ->execute();
+ $this->assertResults([1, 2, 3, 5], $results, 'Query with NOT BETWEEN filter');
+
+ $results = $this->buildSearch()
+ ->setLanguages(['und', 'en'])
+ ->addCondition('keywords', ['grape', 'apple'], 'IN')
+ ->execute();
+ $this->assertResults([2, 4, 5], $results, 'Query with IN filter');
+
+ $results = $this->buildSearch()
+ ->setLanguages(['und'])
+ ->execute();
+ $this->assertResults([], $results, 'Query with languages');
+
+ $query = $this->buildSearch();
+ $conditions = $query->createConditionGroup('OR')
+ ->addCondition('search_api_language', 'und')
+ ->addCondition('width', ['0.9', '1.5'], 'BETWEEN');
+ $query->addConditionGroup($conditions);
+ $results = $query->execute();
+ $this->assertResults([4], $results, 'Query with search_api_language filter');
+
+ $results = $this->buildSearch()
+ ->addCondition('search_api_language', 'und')
+ ->addCondition('width', ['0.9', '1.5'], 'BETWEEN')
+ ->execute();
+ $this->assertResults([], $results, 'Query with search_api_language filter');
+
+ $results = $this->buildSearch()
+ ->addCondition('search_api_language', ['und', 'en'], 'IN')
+ ->addCondition('width', ['0.9', '1.5'], 'BETWEEN')
+ ->execute();
+ $this->assertResults([4], $results, 'Query with search_api_language filter');
+
+ $results = $this->buildSearch()
+ ->addCondition('search_api_language', ['und', 'de'], 'NOT IN')
+ ->addCondition('width', ['0.9', '1.5'], 'BETWEEN')
+ ->execute();
+ $this->assertResults([4], $results, 'Query with search_api_language "NOT IN" filter');
+
+ $results = $this->buildSearch()
+ ->addCondition('search_api_id', $this->getItemIds([1])[0])
+ ->execute();
+ $this->assertResults([1], $results, 'Query with search_api_id filter');
+
+ $results = $this->buildSearch()
+ ->addCondition('search_api_id', $this->getItemIds([2, 4]), 'NOT IN')
+ ->execute();
+ $this->assertResults([1, 3, 5], $results, 'Query with search_api_id "NOT IN" filter');
+
+ $results = $this->buildSearch()
+ ->addCondition('search_api_id', $this->getItemIds([3])[0], '>')
+ ->execute();
+ $this->assertResults([4, 5], $results, 'Query with search_api_id "greater than" filter');
+
+ $results = $this->buildSearch()
+ ->addCondition('search_api_datasource', 'foobar')
+ ->execute();
+ $this->assertResults([], $results, 'Query for a non-existing datasource');
+
+ $results = $this->buildSearch()
+ ->addCondition('search_api_datasource', ['foobar', 'entity:entity_test_mulrev_changed'], 'IN')
+ ->execute();
+ $this->assertResults([1, 2, 3, 4, 5], $results, 'Query with search_api_id "IN" filter');
+
+ $results = $this->buildSearch()
+ ->addCondition('search_api_datasource', ['foobar', 'entity:entity_test_mulrev_changed'], 'NOT IN')
+ ->execute();
+ $this->assertResults([], $results, 'Query with search_api_id "NOT IN" filter');
+
+ // For a query without keys, all of these except for the last one should
+ // have no effect. Therefore, we expect results with IDs in descending
+ // order.
+ $results = $this->buildSearch(NULL, [], [], FALSE)
+ ->sort('search_api_relevance')
+ ->sort('search_api_datasource', QueryInterface::SORT_DESC)
+ ->sort('search_api_language')
+ ->sort('search_api_id', QueryInterface::SORT_DESC)
+ ->execute();
+ $this->assertResults([5, 4, 3, 2, 1], $results, 'Query with magic sorts');
+ }
+
+ /**
+ * Tests whether facets work correctly.
+ */
+ protected function checkFacets() {
+ $query = $this->buildSearch();
+ $conditions = $query->createConditionGroup('OR', ['facet:' . 'category']);
+ $conditions->addCondition('category', 'article_category');
+ $query->addConditionGroup($conditions);
+ $facets['category'] = [
+ 'field' => 'category',
+ 'limit' => 0,
+ 'min_count' => 1,
+ 'missing' => TRUE,
+ 'operator' => 'or',
+ ];
+ $query->setOption('search_api_facets', $facets);
+ $results = $query->execute();
+ $this->assertResults([4, 5], $results, 'OR facets query');
+ $expected = [
+ ['count' => 2, 'filter' => '"article_category"'],
+ ['count' => 2, 'filter' => '"item_category"'],
+ ['count' => 1, 'filter' => '!'],
+ ];
+ $category_facets = $results->getExtraData('search_api_facets')['category'];
+ usort($category_facets, [$this, 'facetCompare']);
+ $this->assertEquals($expected, $category_facets, 'Correct OR facets were returned');
+
+ $query = $this->buildSearch();
+ $conditions = $query->createConditionGroup('OR', ['facet:' . 'category']);
+ $conditions->addCondition('category', 'article_category');
+ $query->addConditionGroup($conditions);
+ $conditions = $query->createConditionGroup('AND');
+ $conditions->addCondition('category', NULL, '<>');
+ $query->addConditionGroup($conditions);
+ $facets['category'] = [
+ 'field' => 'category',
+ 'limit' => 0,
+ 'min_count' => 1,
+ 'missing' => TRUE,
+ 'operator' => 'or',
+ ];
+ $query->setOption('search_api_facets', $facets);
+ $results = $query->execute();
+ $this->assertResults([4, 5], $results, 'OR facets query');
+ $expected = [
+ ['count' => 2, 'filter' => '"article_category"'],
+ ['count' => 2, 'filter' => '"item_category"'],
+ ];
+ $category_facets = $results->getExtraData('search_api_facets')['category'];
+ usort($category_facets, [$this, 'facetCompare']);
+ $this->assertEquals($expected, $category_facets, 'Correct OR facets were returned');
+ }
+
+ /**
+ * Executes regression tests for issues that were already fixed.
+ */
+ protected function regressionTests() {
+ $this->regressionTest2007872();
+ $this->regressionTest1863672();
+ $this->regressionTest2040543();
+ $this->regressionTest2111753();
+ $this->regressionTest2127001();
+ $this->regressionTest2136409();
+ $this->regressionTest1658964();
+ $this->regressionTest2469547();
+ $this->regressionTest1403916();
+ $this->regressionTest2783987();
+ $this->regressionTest2809753();
+ $this->regressionTest2767609();
+ $this->regressionTest2745655();
+ }
+
+ /**
+ * Regression tests for missing results when using OR filters.
+ *
+ * @see https://www.drupal.org/node/2007872
+ */
+ protected function regressionTest2007872() {
+ $results = $this->buildSearch('test', [], [], FALSE)
+ ->sort('id')
+ ->sort('type')
+ ->execute();
+ $this->assertResults([1, 2, 3, 4], $results, 'Sorting on field with NULLs');
+
+ $query = $this->buildSearch(NULL, [], [], FALSE);
+ $conditions = $query->createConditionGroup('OR');
+ $conditions->addCondition('id', 3);
+ $conditions->addCondition('type', 'article');
+ $query->addConditionGroup($conditions);
+ $query->sort('search_api_id', QueryInterface::SORT_DESC);
+ $results = $query->execute();
+ $this->assertResults([5, 4, 3], $results, 'OR filter on field with NULLs');
+ }
+
+ /**
+ * Regression tests for same content multiple times in the search result.
+ *
+ * Error was caused by multiple terms for filter.
+ *
+ * @see https://www.drupal.org/node/1863672
+ */
+ protected function regressionTest1863672() {
+ $query = $this->buildSearch();
+ $conditions = $query->createConditionGroup('OR');
+ $conditions->addCondition('keywords', 'orange');
+ $conditions->addCondition('keywords', 'apple');
+ $query->addConditionGroup($conditions);
+ $results = $query->execute();
+ $this->assertResults([1, 2, 4, 5], $results, 'OR filter on multi-valued field');
+
+ $query = $this->buildSearch();
+ $conditions = $query->createConditionGroup('OR');
+ $conditions->addCondition('keywords', 'orange');
+ $conditions->addCondition('keywords', 'strawberry');
+ $query->addConditionGroup($conditions);
+ $conditions = $query->createConditionGroup('OR');
+ $conditions->addCondition('keywords', 'apple');
+ $conditions->addCondition('keywords', 'grape');
+ $query->addConditionGroup($conditions);
+ $results = $query->execute();
+ $this->assertResults([2, 4, 5], $results, 'Multiple OR filters on multi-valued field');
+
+ $query = $this->buildSearch();
+ $conditions1 = $query->createConditionGroup('OR');
+ $conditions = $query->createConditionGroup('AND');
+ $conditions->addCondition('keywords', 'orange');
+ $conditions->addCondition('keywords', 'apple');
+ $conditions1->addConditionGroup($conditions);
+ $conditions = $query->createConditionGroup('AND');
+ $conditions->addCondition('keywords', 'strawberry');
+ $conditions->addCondition('keywords', 'grape');
+ $conditions1->addConditionGroup($conditions);
+ $query->addConditionGroup($conditions1);
+ $results = $query->execute();
+ $this->assertResults([2, 4, 5], $results, 'Complex nested filters on multi-valued field');
+ }
+
+ /**
+ * Regression tests for (none) facet shown when feature is set to "no".
+ *
+ * @see https://www.drupal.org/node/2040543
+ */
+ protected function regressionTest2040543() {
+ $query = $this->buildSearch();
+ $facets['category'] = [
+ 'field' => 'category',
+ 'limit' => 0,
+ 'min_count' => 1,
+ 'missing' => TRUE,
+ ];
+ $query->setOption('search_api_facets', $facets);
+ $query->range(0, 0);
+ $results = $query->execute();
+ $expected = [
+ ['count' => 2, 'filter' => '"article_category"'],
+ ['count' => 2, 'filter' => '"item_category"'],
+ ['count' => 1, 'filter' => '!'],
+ ];
+ $type_facets = $results->getExtraData('search_api_facets')['category'];
+ usort($type_facets, [$this, 'facetCompare']);
+ $this->assertEquals($expected, $type_facets, 'Correct facets were returned');
+
+ $query = $this->buildSearch();
+ $facets['category']['missing'] = FALSE;
+ $query->setOption('search_api_facets', $facets);
+ $query->range(0, 0);
+ $results = $query->execute();
+ $expected = [
+ ['count' => 2, 'filter' => '"article_category"'],
+ ['count' => 2, 'filter' => '"item_category"'],
+ ];
+ $type_facets = $results->getExtraData('search_api_facets')['category'];
+ usort($type_facets, [$this, 'facetCompare']);
+ $this->assertEquals($expected, $type_facets, 'Correct facets were returned');
+ }
+
+ /**
+ * Regression tests for searching for multiple words using "OR" condition.
+ *
+ * @see https://www.drupal.org/node/2111753
+ */
+ protected function regressionTest2111753() {
+ $keys = [
+ '#conjunction' => 'OR',
+ 'foo',
+ 'test',
+ ];
+ $query = $this->buildSearch($keys, [], ['name']);
+ $results = $query->execute();
+ $this->assertResults([1, 2, 4], $results, 'OR keywords');
+
+ $query = $this->buildSearch($keys, [], ['name', 'body']);
+ $query->range(0, 0);
+ $results = $query->execute();
+ $this->assertEquals(5, $results->getResultCount(), 'Multi-field OR keywords returned correct number of results.');
+ $this->assertFalse($results->getResultItems(), 'Multi-field OR keywords returned correct result.');
+ $this->assertEmpty($results->getIgnoredSearchKeys());
+ $this->assertEmpty($results->getWarnings());
+
+ $keys = [
+ '#conjunction' => 'OR',
+ 'foo',
+ 'test',
+ [
+ '#conjunction' => 'AND',
+ 'bar',
+ 'baz',
+ ],
+ ];
+ $query = $this->buildSearch($keys, [], ['name']);
+ $results = $query->execute();
+ $this->assertResults([1, 2, 4, 5], $results, 'Nested OR keywords');
+
+ $keys = [
+ '#conjunction' => 'OR',
+ [
+ '#conjunction' => 'AND',
+ 'foo',
+ 'test',
+ ],
+ [
+ '#conjunction' => 'AND',
+ 'bar',
+ 'baz',
+ ],
+ ];
+ $query = $this->buildSearch($keys, [], ['name', 'body']);
+ $results = $query->execute();
+ $this->assertResults([1, 2, 4, 5], $results, 'Nested multi-field OR keywords');
+ }
+
+ /**
+ * Regression tests for non-working operator "contains none of these words".
+ *
+ * @see https://www.drupal.org/node/2127001
+ */
+ protected function regressionTest2127001() {
+ $keys = [
+ '#conjunction' => 'AND',
+ '#negation' => TRUE,
+ 'foo',
+ 'bar',
+ ];
+ $results = $this->buildSearch($keys)->execute();
+ $this->assertResults([3, 4], $results, 'Negated AND fulltext search');
+
+ $keys = [
+ '#conjunction' => 'OR',
+ '#negation' => TRUE,
+ 'foo',
+ 'baz',
+ ];
+ $results = $this->buildSearch($keys)->execute();
+ $this->assertResults([3], $results, 'Negated OR fulltext search');
+
+ $keys = [
+ '#conjunction' => 'AND',
+ 'test',
+ [
+ '#conjunction' => 'AND',
+ '#negation' => TRUE,
+ 'foo',
+ 'bar',
+ ],
+ ];
+ $results = $this->buildSearch($keys)->execute();
+ $this->assertResults([3, 4], $results, 'Nested NOT AND fulltext search');
+ }
+
+ /**
+ * Regression tests for handling of NULL filters.
+ *
+ * @see https://www.drupal.org/node/2136409
+ */
+ protected function regressionTest2136409() {
+ $query = $this->buildSearch();
+ $query->addCondition('category', NULL);
+ $results = $query->execute();
+ $this->assertResults([3], $results, 'NULL filter');
+
+ $query = $this->buildSearch();
+ $query->addCondition('category', NULL, '<>');
+ $results = $query->execute();
+ $this->assertResults([1, 2, 4, 5], $results, 'NOT NULL filter');
+ }
+
+ /**
+ * Regression tests for facets with counts of 0.
+ *
+ * @see https://www.drupal.org/node/1658964
+ */
+ protected function regressionTest1658964() {
+ $query = $this->buildSearch();
+ $facets['type'] = [
+ 'field' => 'type',
+ 'limit' => 0,
+ 'min_count' => 0,
+ 'missing' => TRUE,
+ ];
+ $query->setOption('search_api_facets', $facets);
+ $query->addCondition('type', 'article');
+ $query->range(0, 0);
+ $results = $query->execute();
+ $expected = [
+ ['count' => 2, 'filter' => '"article"'],
+ ['count' => 0, 'filter' => '!'],
+ ['count' => 0, 'filter' => '"item"'],
+ ];
+ $facets = $results->getExtraData('search_api_facets', [])['type'];
+ usort($facets, [$this, 'facetCompare']);
+ $this->assertEquals($expected, $facets, 'Correct facets were returned');
+ }
+
+ /**
+ * Regression tests for facets on fulltext fields.
+ *
+ * @see https://www.drupal.org/node/2469547
+ */
+ protected function regressionTest2469547() {
+ $query = $this->buildSearch();
+ $facets = [];
+ $facets['body'] = [
+ 'field' => 'body',
+ 'limit' => 0,
+ 'min_count' => 1,
+ 'missing' => FALSE,
+ ];
+ $query->setOption('search_api_facets', $facets);
+ $query->addCondition('id', 5, '<>');
+ $query->range(0, 0);
+ $results = $query->execute();
+ $expected = [
+ ['count' => 4, 'filter' => '"test"'],
+ ['count' => 2, 'filter' => '"Case"'],
+ ['count' => 2, 'filter' => '"casE"'],
+ ['count' => 1, 'filter' => '"bar"'],
+ ['count' => 1, 'filter' => '"case"'],
+ ['count' => 1, 'filter' => '"foobar"'],
+ ];
+ // We can't guarantee the order of returned facets, since "bar" and "foobar"
+ // both occur once, so we have to manually sort the returned facets first.
+ $facets = $results->getExtraData('search_api_facets', [])['body'];
+ usort($facets, [$this, 'facetCompare']);
+ $this->assertEquals($expected, $facets, 'Correct facets were returned for a fulltext field.');
+ }
+
+ /**
+ * Regression tests for multi word search results sets and wrong facet counts.
+ *
+ * @see https://www.drupal.org/node/1403916
+ */
+ protected function regressionTest1403916() {
+ $query = $this->buildSearch('test foo');
+ $facets = [];
+ $facets['type'] = [
+ 'field' => 'type',
+ 'limit' => 0,
+ 'min_count' => 1,
+ 'missing' => TRUE,
+ ];
+ $query->setOption('search_api_facets', $facets);
+ $query->range(0, 0);
+ $results = $query->execute();
+ $expected = [
+ ['count' => 2, 'filter' => '"item"'],
+ ['count' => 1, 'filter' => '"article"'],
+ ];
+ $facets = $results->getExtraData('search_api_facets', [])['type'];
+ usort($facets, [$this, 'facetCompare']);
+ $this->assertEquals($expected, $facets, 'Correct facets were returned');
+ }
+
+ /**
+ * Regression test for facet with "min_count" greater than 1.
+ *
+ * @see https://www.drupal.org/node/2783987
+ */
+ protected function regressionTest2783987() {
+ $query = $this->buildSearch('test foo');
+ $facets = [];
+ $facets['type'] = [
+ 'field' => 'type',
+ 'limit' => 0,
+ 'min_count' => 2,
+ 'missing' => TRUE,
+ ];
+ $query->setOption('search_api_facets', $facets);
+ $query->range(0, 0);
+ $results = $query->execute();
+ $expected = [
+ ['count' => 2, 'filter' => '"item"'],
+ ];
+ $facets = $results->getExtraData('search_api_facets', [])['type'];
+ $this->assertEquals($expected, $facets, 'Correct facets were returned');
+ }
+
+ /**
+ * Regression test for multiple facets.
+ *
+ * @see https://www.drupal.org/node/2809753
+ */
+ protected function regressionTest2809753() {
+ $query = $this->buildSearch();
+ $condition_group = $query->createConditionGroup('OR', ['facet:type']);
+ $condition_group->addCondition('type', 'article');
+ $query->addConditionGroup($condition_group);
+ $facets['type'] = [
+ 'field' => 'type',
+ 'limit' => 0,
+ 'min_count' => 1,
+ 'missing' => FALSE,
+ 'operator' => 'or',
+ ];
+ $facets['category'] = [
+ 'field' => 'category',
+ 'limit' => 0,
+ 'min_count' => 1,
+ 'missing' => FALSE,
+ 'operator' => 'or',
+ ];
+ $query->setOption('search_api_facets', $facets);
+ $results = $query->execute();
+
+ $this->assertResults([4, 5], $results, 'Multi-facets query');
+ $expected = [
+ ['count' => 3, 'filter' => '"item"'],
+ ['count' => 2, 'filter' => '"article"'],
+ ];
+ $type_facets = $results->getExtraData('search_api_facets')['type'];
+ usort($type_facets, [$this, 'facetCompare']);
+ $this->assertEquals($expected, $type_facets, 'Correct facets were returned for first facet');
+ $expected = [
+ ['count' => 2, 'filter' => '"article_category"'],
+ ];
+ $category_facets = $results->getExtraData('search_api_facets')['category'];
+ $this->assertEquals($expected, $category_facets, 'Correct facets were returned for second facet');
+ }
+
+ /**
+ * Regression test for conditions with empty strings as values.
+ *
+ * @see https://www.drupal.org/node/2767609
+ */
+ protected function regressionTest2767609() {
+ $results = $this->buildSearch(NULL, ['type,'])->execute();
+ $this->assertResults([], $results, 'Search for empty-string type');
+
+ $results = $this->buildSearch(NULL, ['category,'])->execute();
+ $this->assertResults([], $results, 'Search for empty-string category');
+
+ $results = $this->buildSearch()
+ ->addCondition('category', '', '<>')
+ ->execute();
+ $this->assertResults([1, 2, 3, 4, 5], $results, 'Search for items with category not an empty string');
+
+ // It's not clear what the results for "category < ''" should be, but in
+ // combination with the BETWEEN this should never return results.
+ $results = $this->buildSearch()
+ ->addCondition('category', '', '<')
+ ->addCondition('category', ['', 'foo'], 'BETWEEN')
+ ->addCondition('category', ['', 'a', 'b'], 'NOT IN')
+ ->execute();
+ $this->assertResults([], $results, 'Search with various empty-string filters');
+ }
+
+ /**
+ * Tests (NOT) NULL conditions on fulltext fields.
+ *
+ * @see https://www.drupal.org/node/2745655
+ */
+ protected function regressionTest2745655() {
+ $name = $this->entities[3]->name[0]->value;
+ $this->entities[3]->name[0]->value = NULL;
+ $this->entities[3]->save();
+ $this->indexItems($this->indexId);
+
+ $results = $this->buildSearch()
+ ->addCondition('name', NULL)
+ ->execute();
+ $this->assertResults([3], $results, 'Search for items without name');
+
+ $results = $this->buildSearch()
+ ->addCondition('name', NULL, '<>')
+ ->execute();
+ $this->assertResults([1, 2, 4, 5], $results, 'Search for items with name');
+
+ $this->entities[3]->set('name', [$name]);
+ $this->entities[3]->save();
+ $this->indexItems($this->indexId);
+ }
+
+ /**
+ * Compares two facet filters to determine their order.
+ *
+ * Used as a callback for usort() in regressionTests().
+ *
+ * Will first compare the counts, ranking facets with higher count first, and
+ * then by filter value.
+ *
+ * @param array $a
+ * The first facet filter.
+ * @param array $b
+ * The second facet filter.
+ *
+ * @return int
+ * -1 or 1 if the first filter should, respectively, come before or after
+ * the second; 0 if both facet filters are equal.
+ */
+ protected function facetCompare(array $a, array $b) {
+ if ($a['count'] != $b['count']) {
+ return $b['count'] - $a['count'];
+ }
+ return strcmp($a['filter'], $b['filter']);
+ }
+
+ /**
+ * Clears the test index.
+ */
+ protected function clearIndex() {
+ $this->getIndex()->clear();
+ }
+
+ /**
+ * Executes regression tests which are unpractical to run in between.
+ */
+ protected function regressionTests2() {
+ // Create a "prices" field on the test entity type.
+ FieldStorageConfig::create([
+ 'field_name' => 'prices',
+ 'entity_type' => 'entity_test_mulrev_changed',
+ 'type' => 'decimal',
+ 'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED,
+ ])->save();
+ FieldConfig::create([
+ 'field_name' => 'prices',
+ 'entity_type' => 'entity_test_mulrev_changed',
+ 'bundle' => 'item',
+ 'label' => 'Prices',
+ ])->save();
+
+ $this->regressionTest1916474();
+ $this->regressionTest2284199();
+ $this->regressionTest2471509();
+ $this->regressionTest2616804();
+ }
+
+ /**
+ * Regression tests for correctly indexing multiple float/decimal fields.
+ *
+ * @see https://www.drupal.org/node/1916474
+ */
+ protected function regressionTest1916474() {
+ $index = $this->getIndex();
+ $this->addField($index, 'prices', 'decimal');
+ $success = $index->save();
+ $this->assertTrue($success, 'The index field settings were successfully changed.');
+
+ // Reset the static cache so the new values will be available.
+ $this->resetEntityCache('server');
+ $this->resetEntityCache();
+
+ $this->addTestEntity(6, [
+ 'prices' => ['3.5', '3.25', '3.75', '3.5'],
+ 'type' => 'item',
+ ]);
+
+ $this->indexItems($this->indexId);
+
+ $query = $this->buildSearch(NULL, ['prices,3.25']);
+ $results = $query->execute();
+ $this->assertResults([6], $results, 'Filter on decimal field');
+
+ $query = $this->buildSearch(NULL, ['prices,3.5']);
+ $results = $query->execute();
+ $this->assertResults([6], $results, 'Filter on decimal field');
+
+ // Use the "prices" field, since we've added it now, to also check for
+ // proper handling of (NOT) BETWEEN for multi-valued fields.
+ $query = $this->buildSearch()
+ ->addCondition('prices', [3.6, 3.8], 'BETWEEN');
+ $results = $query->execute();
+ $this->assertResults([6], $results, 'BETWEEN filter on multi-valued field');
+
+ $query = $this->buildSearch()
+ ->addCondition('prices', [3.6, 3.8], 'NOT BETWEEN');
+ $results = $query->execute();
+ $this->assertResults([1, 2, 3, 4, 5], $results, 'NOT BETWEEN filter on multi-valued field');
+ }
+
+ /**
+ * Regression tests for problems with taxonomy term parent.
+ *
+ * @see https://www.drupal.org/node/2284199
+ */
+ protected function regressionTest2284199() {
+ $this->addTestEntity(7, ['type' => 'item']);
+
+ $count = $this->indexItems($this->indexId);
+ $this->assertEquals(1, $count, 'Indexing an item with an empty value for a non string field worked.');
+ }
+
+ /**
+ * Regression tests for strings longer than 50 chars.
+ *
+ * @see https://www.drupal.org/node/2471509
+ * @see https://www.drupal.org/node/2616268
+ */
+ protected function regressionTest2471509() {
+ $this->addTestEntity(8, [
+ 'name' => 'Article with long body',
+ 'type' => 'article',
+ 'body' => 'astringlongerthanfiftycharactersthatcantbestoredbythedbbackend',
+ ]);
+ $count = $this->indexItems($this->indexId);
+ $this->assertEquals(1, $count, 'Indexing an item with a word longer than 50 characters worked.');
+
+ $index = $this->getIndex();
+ $index->getField('body')->setType('string');
+ $index->save();
+ $count = $this->indexItems($this->indexId);
+ $this->assertEquals(count($this->entities), $count, 'Switching type from text to string worked.');
+
+ // For a string field, 50 characters shouldn't be a problem.
+ $query = $this->buildSearch(NULL, ['body,astringlongerthanfiftycharactersthatcantbestoredbythedbbackend']);
+ $results = $query->execute();
+ $this->assertResults([8], $results, 'Filter on new string field');
+
+ $index->getField('body')->setType('text');
+ $index->save();
+ $count = $this->indexItems($this->indexId);
+ $this->assertEquals(count($this->entities), $count, 'All items needed to be re-indexed after switching type from string to text.');
+ }
+
+ /**
+ * Regression tests for multibyte characters exceeding 50 byte.
+ *
+ * @see https://www.drupal.org/node/2616804
+ */
+ protected function regressionTest2616804() {
+ // The word has 28 Unicode characters but 56 bytes. Verify that it is still
+ // indexed correctly.
+ $mb_word = 'äöüßáŧæøðđŋħĸµäöüßáŧæøðđŋħĸµ';
+ // We put the word 8 times into the body so we can also verify that the 255
+ // character limit for strings counts characters, not bytes.
+ $mb_body = implode(' ', array_fill(0, 8, $mb_word));
+ $this->addTestEntity(9, [
+ 'name' => 'Test item 9',
+ 'type' => 'item',
+ 'body' => $mb_body,
+ ]);
+ $count = $this->indexItems($this->indexId);
+ $this->assertEquals(1, $count, 'Indexing an item with a word with 28 multi-byte characters worked.');
+
+ $query = $this->buildSearch($mb_word);
+ $results = $query->execute();
+ $this->assertResults([9], $results, 'Search for word with 28 multi-byte characters');
+
+ $query = $this->buildSearch($mb_word . 'ä');
+ $results = $query->execute();
+ $this->assertResults([], $results, 'Search for unknown word with 29 multi-byte characters');
+
+ // Test the same body when indexed as a string (255 characters limit should
+ // not be reached).
+ $index = $this->getIndex();
+ $index->getField('body')->setType('string');
+ $index->save();
+ $entity_count = count($this->entities);
+ $count = $this->indexItems($this->indexId);
+ $this->assertEquals($entity_count, $count, 'Switching type from text to string worked.');
+
+ $query = $this->buildSearch(NULL, ["body,$mb_body"]);
+ $results = $query->execute();
+ $this->assertResults([9], $results, 'Search for body with 231 multi-byte characters');
+
+ $query = $this->buildSearch(NULL, ["body,{$mb_body}ä"]);
+ $results = $query->execute();
+ $this->assertResults([], $results, 'Search for unknown body with 232 multi-byte characters');
+
+ $index->getField('body')->setType('text');
+ $index->save();
+ }
+
+ /**
+ * Checks the correct handling of an index without fields.
+ *
+ * @return \Drupal\search_api\IndexInterface
+ * The created test index.
+ */
+ protected function checkIndexWithoutFields() {
+ $index = Index::create([
+ 'id' => 'test_index_2',
+ 'name' => 'Test index 2',
+ 'status' => TRUE,
+ 'server' => $this->serverId,
+ 'datasource_settings' => [
+ 'entity:entity_test_mulrev_changed' => [],
+ ],
+ 'tracker_settings' => [
+ 'default' => [],
+ ],
+ ]);
+ $index->save();
+
+ $indexed_count = $this->indexItems($index->id());
+ $this->assertEquals(count($this->entities), $indexed_count);
+
+ $search_count = $index->query()->execute()->getResultCount();
+ $this->assertEquals(count($this->entities), $search_count);
+
+ return $index;
+ }
+
+ /**
+ * Asserts that the given result set complies with expectations.
+ *
+ * @param int[] $result_ids
+ * The expected result item IDs, as raw entity IDs.
+ * @param \Drupal\search_api\Query\ResultSetInterface $results
+ * The returned result set.
+ * @param string $search_label
+ * (optional) A label for the search to include in assertion messages.
+ * @param string[] $ignored
+ * (optional) The ignored keywords that should be present, if any.
+ * @param string[] $warnings
+ * (optional) The ignored warnings that should be present, if any.
+ */
+ protected function assertResults(array $result_ids, ResultSetInterface $results, $search_label = 'Search', array $ignored = [], array $warnings = []) {
+ $this->assertEquals(count($result_ids), $results->getResultCount(), "$search_label returned correct number of results.");
+ if ($result_ids) {
+ $this->assertEquals($this->getItemIds($result_ids), array_keys($results->getResultItems()), "$search_label returned correct results.");
+ }
+ $this->assertEquals($ignored, $results->getIgnoredSearchKeys());
+ $this->assertEquals($warnings, $results->getWarnings());
+ }
+
+ /**
+ * Retrieves the search server used by this test.
+ *
+ * @return \Drupal\search_api\ServerInterface
+ * The search server.
+ */
+ protected function getServer() {
+ return Server::load($this->serverId);
+ }
+
+ /**
+ * Retrieves the search index used by this test.
+ *
+ * @return \Drupal\search_api\IndexInterface
+ * The search index.
+ */
+ protected function getIndex() {
+ return Index::load($this->indexId);
+ }
+
+ /**
+ * Adds a field to a search index.
+ *
+ * The index will not be saved automatically.
+ *
+ * @param \Drupal\search_api\IndexInterface $index
+ * The search index.
+ * @param string $property_name
+ * The property's name.
+ * @param string $type
+ * (optional) The field type.
+ */
+ protected function addField(IndexInterface $index, $property_name, $type = 'text') {
+ $field_info = [
+ 'label' => $property_name,
+ 'type' => $type,
+ 'datasource_id' => 'entity:entity_test_mulrev_changed',
+ 'property_path' => $property_name,
+ ];
+ $field = \Drupal::getContainer()
+ ->get('search_api.fields_helper')
+ ->createField($index, $property_name, $field_info);
+ $index->addField($field);
+ $index->save();
+ }
+
+ /**
+ * Resets the entity cache for the specified entity.
+ *
+ * @param string $type
+ * (optional) The type of entity whose cache should be reset. Either "index"
+ * or "server".
+ */
+ protected function resetEntityCache($type = 'index') {
+ $entity_type_id = 'search_api_' . $type;
+ \Drupal::entityTypeManager()
+ ->getStorage($entity_type_id)
+ ->resetCache([$this->{$type . 'Id'}]);
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Kernel/BasicTrackerTest.php b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/BasicTrackerTest.php
new file mode 100644
index 000000000..b7c273b7e
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/BasicTrackerTest.php
@@ -0,0 +1,383 @@
+installSchema('search_api', ['search_api_item']);
+ $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->index = Index::create([
+ 'id' => 'index',
+ 'tracker_settings' => [
+ 'default' => [],
+ ],
+ ]);
+ $this->tracker = $this->index->getTrackerInstance();
+ $this->timeService = new TestTimeService();
+ $this->tracker->setTimeService($this->timeService);
+ }
+
+ /**
+ * Tests tracking.
+ *
+ * @param string $indexing_order
+ * The indexing order setting to use – "fifo" or "lifo".
+ *
+ * @dataProvider trackingDataProvider
+ */
+ public function testTracking($indexing_order) {
+ // Add a logger that throws an exception when used, so a caught exception
+ // within any of the tracker methods will still cause a test fail.
+ // @todo Change to createMock() once we depend on Drupal 8.5+. See #2930839.
+ /** @var \PHPUnit_Framework_MockObject_MockObject|\Psr\Log\LoggerInterface $logger */
+ $logger = $this->getMock(LoggerInterface::class);
+ $logger->method('log')
+ ->willReturnCallback(function ($level, $message, array $variables) {
+ $error = 'Tracking operation threw ';
+ $error .= strtr($message, $variables);
+ throw new \Exception($error);
+ });
+ $this->tracker->setLogger($logger);
+
+ $this->tracker->setConfiguration(['indexing_order' => $indexing_order]);
+ $datasource_1 = 'test1';
+ $datasource_2 = 'test2';
+ $ids = [];
+ foreach ([$datasource_1, $datasource_2] as $num => $datasource_id) {
+ foreach ([1, 2, 3] as $raw_id) {
+ $ids[$num][] = Utility::createCombinedId($datasource_id, $raw_id);
+ }
+ }
+
+ // Make sure we start from a "clean slate".
+ $this->assertIndexingStatus(0, 0);
+ $this->assertIndexingStatus(0, 0, $datasource_1);
+ $this->assertIndexingStatus(0, 0, $datasource_2);
+
+ // Make sure tracking items as deleted, updated or indexed has no effect if
+ // none were inserted before.
+ $this->tracker->trackItemsDeleted([$ids[0][0]]);
+ $this->assertIndexingStatus(0, 0);
+ $this->tracker->trackItemsUpdated([$ids[0][0]]);
+ $this->assertIndexingStatus(0, 0);
+ $this->tracker->trackItemsIndexed([$ids[0][0]]);
+ $this->assertIndexingStatus(0, 0);
+
+ // Now, finally, actually do something sensible and insert some items.
+ $this->tracker->trackItemsInserted([$ids[0][0]]);
+ $this->tracker->trackItemsInserted([$ids[1][1]]);
+ $this->timeService->advanceTime();
+ $this->tracker->trackItemsInserted([$ids[0][2], $ids[1][0]]);
+ $this->timeService->advanceTime();
+ // Make sure re-inserting an item doesn't cause problems.
+ $this->tracker->trackItemsInserted([$ids[0][1], $ids[1][2], $ids[0][0]]);
+ $this->timeService->advanceTime();
+
+ $this->assertIndexingStatus(0, 6);
+ $this->assertIndexingStatus(0, 3, $datasource_1);
+ $this->assertIndexingStatus(0, 3, $datasource_2);
+
+ // Make sure the remaining items are returned as expected.
+ $fifo = $indexing_order === 'fifo';
+ $to_index = $this->tracker->getRemainingItems(4);
+ sort($to_index);
+ if ($fifo) {
+ $expected = [$ids[0][0], $ids[0][2], $ids[1][0], $ids[1][1]];
+ }
+ else {
+ $expected = [$ids[0][1], $ids[0][2], $ids[1][0], $ids[1][2]];
+ }
+ $this->assertEquals($expected, $to_index);
+
+ $to_index = $this->tracker->getRemainingItems(1, $datasource_1);
+ if ($fifo) {
+ $expected = [$ids[0][0]];
+ }
+ else {
+ $expected = [$ids[0][1]];
+ }
+ $this->assertEquals($expected, $to_index);
+
+ $to_index = $this->tracker->getRemainingItems(-1);
+ sort($to_index);
+ $expected = array_merge($ids[0], $ids[1]);
+ $this->assertEquals($expected, $to_index);
+
+ $to_index = $this->tracker->getRemainingItems(-1, $datasource_2);
+ sort($to_index);
+ $this->assertEquals($ids[1], $to_index);
+
+ // Make sure that tracking an unindexed item as updated will not affect its
+ // position for FIFO, but will get it to the front for LIFO. (If we do this
+ // with the item that's in front for FIFO anyways, we can use the same code
+ // in both cases.)
+ $this->tracker->trackItemsUpdated([$ids[0][0]]);
+ $this->timeService->advanceTime();
+ $to_index = $this->tracker->getRemainingItems(1, $datasource_1);
+ $this->assertEquals([$ids[0][0]], $to_index);
+
+ // Make sure calling methods with an empty $ids array doesn't blow anything
+ // up.
+ $this->tracker->trackItemsInserted([]);
+ $this->tracker->trackItemsUpdated([]);
+ $this->tracker->trackItemsIndexed([]);
+ $this->tracker->trackItemsDeleted([]);
+
+ // None of this should have changed the indexing status of any items.
+ $this->assertIndexingStatus(0, 6);
+ $this->assertIndexingStatus(0, 3, $datasource_1);
+ $this->assertIndexingStatus(0, 3, $datasource_2);
+
+ // Now, change the status of some of the items.
+ $this->tracker->trackItemsIndexed([$ids[0][0], $ids[0][1], $ids[1][0]]);
+ $this->assertIndexingStatus(3, 6);
+ $this->assertIndexingStatus(2, 3, $datasource_1);
+ $this->assertIndexingStatus(1, 3, $datasource_2);
+ $to_index = $this->tracker->getRemainingItems(-1);
+ sort($to_index);
+ $expected = [$ids[0][2], $ids[1][1], $ids[1][2]];
+ $this->assertEquals($expected, $to_index);
+
+ $this->tracker->trackItemsUpdated([$ids[0][0], $ids[0][2]]);
+ $this->timeService->advanceTime();
+ $this->assertIndexingStatus(2, 6);
+ $this->assertIndexingStatus(1, 3, $datasource_1);
+ $this->assertIndexingStatus(1, 3, $datasource_2);
+ $to_index = $this->tracker->getRemainingItems(-1);
+ sort($to_index);
+ array_unshift($expected, $ids[0][0]);
+ $this->assertEquals($expected, $to_index);
+
+ $this->tracker->trackItemsDeleted([$ids[1][0], $ids[1][2]]);
+ $this->assertIndexingStatus(1, 4);
+ $this->assertIndexingStatus(1, 3, $datasource_1);
+ $this->assertIndexingStatus(0, 1, $datasource_2);
+ $to_index = $this->tracker->getRemainingItems(-1);
+ sort($to_index);
+ // The last element of $expected is $ids[1][2], which we just deleted.
+ unset($expected[3]);
+ $this->assertEquals($expected, $to_index);
+
+ // Make sure the right items are "at the front" of the queue in each case.
+ if ($fifo) {
+ // These are the only two (remaining) items that were never indexed, so
+ // they still have their original insert time stamp and thus go first.
+ $expected = [$ids[0][2], $ids[1][1]];
+ }
+ else {
+ // We just tracked an update for both of these, so they go first.
+ $expected = [$ids[0][0], $ids[0][2]];
+ }
+ $to_index = $this->tracker->getRemainingItems(2);
+ sort($to_index);
+ $this->assertEquals($expected, $to_index);
+
+ // Some more status changes.
+ $this->tracker->trackItemsInserted([$ids[1][2]]);
+ $this->timeService->advanceTime();
+ $this->assertIndexingStatus(1, 5);
+ $this->assertIndexingStatus(1, 3, $datasource_1);
+ $this->assertIndexingStatus(0, 2, $datasource_2);
+
+ $this->tracker->trackItemsIndexed(array_merge($ids[0], $ids[1]));
+ $this->assertIndexingStatus(5, 5);
+ $this->assertIndexingStatus(3, 3, $datasource_1);
+ $this->assertIndexingStatus(2, 2, $datasource_2);
+
+ $this->tracker->trackAllItemsUpdated($datasource_1);
+ $this->timeService->advanceTime();
+ $this->assertIndexingStatus(2, 5);
+ $this->assertIndexingStatus(0, 3, $datasource_1);
+ $this->assertIndexingStatus(2, 2, $datasource_2);
+
+ $this->tracker->trackItemsIndexed([$ids[0][0]]);
+ $this->assertIndexingStatus(3, 5);
+ $this->assertIndexingStatus(1, 3, $datasource_1);
+ $this->assertIndexingStatus(2, 2, $datasource_2);
+
+ $this->tracker->trackAllItemsUpdated();
+ $this->timeService->advanceTime();
+ $this->assertIndexingStatus(0, 5);
+ $this->assertIndexingStatus(0, 3, $datasource_1);
+ $this->assertIndexingStatus(0, 2, $datasource_2);
+
+ $this->tracker->trackAllItemsDeleted($datasource_2);
+ $this->assertIndexingStatus(0, 3);
+ $this->assertIndexingStatus(0, 3, $datasource_1);
+ $this->assertIndexingStatus(0, 0, $datasource_2);
+
+ $this->tracker->trackAllItemsDeleted();
+ $this->assertIndexingStatus(0, 0);
+ $this->assertIndexingStatus(0, 0, $datasource_1);
+ $this->assertIndexingStatus(0, 0, $datasource_2);
+ }
+
+ /**
+ * Provides test data for testTracking().
+ *
+ * @return array[]
+ * An array of argument arrays for testTracking().
+ *
+ * @see \Drupal\Tests\search_api\Kernel\BasicTrackerTest::testTracking()
+ */
+ public function trackingDataProvider() {
+ return [
+ 'FIFO' => ['fifo'],
+ 'LIFO' => ['lifo'],
+ ];
+ }
+
+ /**
+ * Tests whether a method properly handles exceptions.
+ *
+ * @param string $tracker_method
+ * The method to test.
+ * @param array $args
+ * (optional) The arguments to pass to the method.
+ * @param bool $uses_transaction
+ * (optional) Whether the method is expected to use a transaction (and roll
+ * it back upon encountering an exception).
+ *
+ * @dataProvider exceptionHandlingDataProvider
+ */
+ public function testExceptionHandling($tracker_method, array $args = [], $uses_transaction = FALSE) {
+ /** @var \PHPUnit_Framework_MockObject_MockObject|\Drupal\Core\Database\Connection $connection */
+ $connection = $this->getMockBuilder(Connection::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ foreach (['select', 'insert', 'update', 'delete'] as $method) {
+ $connection->method($method)->willThrowException(new \Exception());
+ }
+ $transaction = $this->getMockBuilder(Transaction::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $rolled_back = FALSE;
+ $rollback = function () use (&$rolled_back) {
+ $rolled_back = TRUE;
+ };
+ $transaction->method('rollback')->willReturnCallback($rollback);
+ $connection->method('startTransaction')->willReturn($transaction);
+ $this->tracker->setDatabaseConnection($connection);
+
+ /** @var \PHPUnit_Framework_MockObject_MockObject|\Psr\Log\LoggerInterface $logger */
+ $logger = $this->getMock(LoggerInterface::class);
+ $log = [];
+ $logger->method('log')->willReturnCallback(function () use (&$log) {
+ $log[] = func_get_args();
+ });
+ $this->tracker->setLogger($logger);
+
+ call_user_func_array([$this->tracker, $tracker_method], $args);
+
+ $this->assertCount(1, $log);
+ $this->assertStringStartsWith('%type', $log[0][1]);
+
+ if ($uses_transaction) {
+ $this->assertEquals(TRUE, $rolled_back);
+ }
+ }
+
+ /**
+ * Provides test data for testExceptionHandling().
+ *
+ * @return array[]
+ * An array of argument arrays for testExceptionHandling().
+ *
+ * @see \Drupal\Tests\search_api\Kernel\BasicTrackerTest::testExceptionHandling()
+ */
+ public function exceptionHandlingDataProvider() {
+ return [
+ 'trackItemsInserted()' => ['trackItemsInserted', [['']], TRUE],
+ 'trackItemsUpdated()' => ['trackItemsUpdated', [['']], TRUE],
+ 'trackAllItemsUpdated()' => ['trackAllItemsUpdated', [], TRUE],
+ 'trackItemsIndexed()' => ['trackItemsIndexed', [['']], TRUE],
+ 'trackItemsDeleted()' => ['trackItemsDeleted', [], TRUE],
+ 'trackAllItemsDeleted()' => ['trackAllItemsDeleted', [], TRUE],
+ 'getRemainingItems()' => ['getRemainingItems'],
+ 'getTotalItemsCount()' => ['getTotalItemsCount'],
+ 'getIndexedItemsCount()' => ['getIndexedItemsCount'],
+ 'getRemainingItemsCount()' => ['getRemainingItemsCount'],
+ ];
+ }
+
+ /**
+ * Asserts that the current tracking status is as expected.
+ *
+ * @param int $indexed
+ * The expected number of indexed items.
+ * @param int $total
+ * The expected total number of items.
+ * @param string|null $datasource_id
+ * (optional) The datasource for which to check indexing status, or NULL to
+ * check for the whole index.
+ */
+ protected function assertIndexingStatus($indexed, $total, $datasource_id = NULL) {
+ $datasource = $datasource_id ? " for datasource $datasource_id" : '';
+ $actual_indexed = $this->tracker->getIndexedItemsCount($datasource_id);
+ $this->assertEquals($indexed, $actual_indexed, "$actual_indexed items indexed$datasource, $indexed expected.");
+ $actual_total = $this->tracker->getTotalItemsCount($datasource_id);
+ $this->assertEquals($total, $actual_total, "$actual_total items tracked in total$datasource, $total expected.");
+ $remaining = $total - $indexed;
+ $actual_remaining = $this->tracker->getRemainingItemsCount($datasource_id);
+ $this->assertEquals($remaining, $actual_remaining, "$actual_remaining items remaining to be indexed$datasource, $remaining expected.");
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Kernel/ConfigEntity/ConfigOverrideKernelTest.php b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/ConfigEntity/ConfigOverrideKernelTest.php
new file mode 100644
index 000000000..86a73e735
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/ConfigEntity/ConfigOverrideKernelTest.php
@@ -0,0 +1,216 @@
+installSchema('search_api', ['search_api_item']);
+ $this->installSchema('system', ['router']);
+ $this->installSchema('user', ['users_data']);
+ $this->installEntitySchema('user');
+ $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);
+ }
+
+ // Set up overrides.
+ $GLOBALS['config']['search_api.server.test_server'] = [
+ 'name' => 'Overridden server',
+ 'backend' => 'search_api_test',
+ 'backend_config' => [
+ 'test' => 'foobar',
+ ],
+ ];
+ $GLOBALS['config']['search_api.index.test_index'] = [
+ 'name' => 'Overridden index',
+ 'server' => 'test_server',
+ 'processor_settings' => [
+ 'search_api_test' => [],
+ ],
+ ];
+
+ // Create a test server and index.
+ $this->server = Server::create([
+ 'id' => 'test_server',
+ 'name' => 'Test server',
+ 'backend' => 'does not exist',
+ ]);
+ $this->index = Index::create([
+ 'id' => 'test_index',
+ 'name' => 'Test index',
+ 'server' => 'unknown_server',
+ 'datasource_settings' => [
+ 'entity:user' => [],
+ ],
+ 'tracker_settings' => [
+ 'default' => [],
+ ],
+ ]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function tearDown() {
+ parent::tearDown();
+
+ unset($GLOBALS['config']['search_api.server.test_server']);
+ unset($GLOBALS['config']['search_api.index.test_index']);
+ }
+
+ /**
+ * Checks whether saving an index with overrides works correctly.
+ */
+ public function testIndexSave() {
+ $this->server->save();
+
+ // Even though no processors are set on the index, saving it should trigger
+ // the test processor's preIndexSave() method (since we added that processor
+ // in the override).
+ $this->assertEmpty($this->index->getProcessorsByStage(ProcessorInterface::STAGE_PRE_INDEX_SAVE));
+ $this->index->save();
+ $this->assertEquals(['preIndexSave'], $this->getCalledMethods('processor'));
+ $this->assertEmpty($this->index->getProcessorsByStage(ProcessorInterface::STAGE_PRE_INDEX_SAVE));
+
+ // Verify the override is correctly present when loading the index.
+ /** @var \Drupal\search_api\IndexInterface $index */
+ $index = Index::load($this->index->id());
+ $this->assertEquals('Overridden index', $index->label());
+ $server = $index->getServerInstance();
+ $this->assertNotEmpty($server);
+ $this->assertEquals('Overridden server', $server->label());
+ $this->assertTrue($index->status());
+
+ // Verify that overrides are not present when loading the index
+ // override-free.
+ /** @var \Drupal\search_api\Entity\SearchApiConfigEntityStorage $index_storage */
+ $index_storage = \Drupal::entityTypeManager()
+ ->getStorage('search_api_index');
+ $index = $index_storage->loadOverrideFree($index->id());
+ $this->assertEquals('Test index', $index->label());
+
+ // Try to change the index's name (starting from the override-free index)
+ // and verify a copy with overrides is used for post-save operations.
+ $args = [];
+ $this->setMethodOverride('backend', 'updateIndex', function () use (&$args) {
+ $args = func_get_args();
+ });
+ $index->set('name', 'New index name')->save();
+ $this->assertCount(2, $args);
+ $index = $args[1];
+ $this->assertEquals('Overridden index', $index->label());
+
+ // Verify the override is correctly present when loading the index.
+ $index = Index::load($this->index->id());
+ $this->assertEquals('Overridden index', $index->label());
+ $this->assertEquals('Overridden server', $index->getServerInstance()
+ ->label());
+
+ // Verify the new name is included when loading the index override-free.
+ $index = $index_storage->loadOverrideFree($index->id());
+ $this->assertEquals('New index name', $index->label());
+ }
+
+ /**
+ * Checks whether saving a server with overrides works correctly.
+ */
+ public function testServerSave() {
+ // Verify that in postInsert() the backend overrides are already applied.
+ $passed_config = [];
+ $passed_name = NULL;
+ $override = function (BackendInterface $backend) use (&$passed_config, &$passed_name) {
+ $passed_config = $backend->getConfiguration();
+ $passed_name = $backend->getServer()->label();
+ };
+ $this->setMethodOverride('backend', 'postInsert', $override);
+ $this->server->save();
+ $this->assertEquals(['test' => 'foobar'], $passed_config);
+ $this->assertEquals('Overridden server', $passed_name);
+ $this->assertEquals('Test server', $this->server->label());
+ $this->assertTrue($this->server->status());
+
+ // Save the index.
+ $this->index->save();
+
+ // Verify that on load, the overrides are correctly applied.
+ $server = Server::load($this->server->id());
+ $this->assertEquals('Overridden server', $server->label());
+ $this->assertTrue($server->status());
+ $this->assertEquals('does not exist', $this->server->getBackendId());
+
+ // Verify that in preUpdate() the backend overrides are already applied.
+ $this->setMethodOverride('backend', 'preUpdate', $override);
+ $this->server->save();
+ $this->assertEquals(['test' => 'foobar'], $passed_config);
+
+ // Verify that overriding "status" prevents the server's indexes from being
+ // disabled when attempting to disable the server.
+ $GLOBALS['config']['search_api.server.test_server']['status'] = TRUE;
+ $this->server->disable()->save();
+ \Drupal::configFactory()->clearStaticCache();
+ $index = Index::load($this->index->id());
+ $this->assertTrue($index->status());
+ $server = Server::load($this->server->id());
+ $this->assertTrue($server->status());
+
+ // Verify that overrides are not present when loading the server
+ // override-free.
+ /** @var \Drupal\search_api\Entity\SearchApiConfigEntityStorage $server_storage */
+ $server_storage = \Drupal::entityTypeManager()
+ ->getStorage('search_api_server');
+ $server = $server_storage->loadOverrideFree($server->id());
+ $this->assertEquals('Test server', $server->label());
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Kernel/ConfigEntity/DependencyRemovalTest.php b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/ConfigEntity/DependencyRemovalTest.php
new file mode 100644
index 000000000..014e0a97d
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/ConfigEntity/DependencyRemovalTest.php
@@ -0,0 +1,633 @@
+installEntitySchema('search_api_task');
+ $this->installConfig('search_api');
+
+ // Create the index object, but don't save it yet since we want to change
+ // its settings anyways in every test.
+ $this->index = Index::create([
+ 'id' => 'test_index',
+ 'name' => 'Test index',
+ 'tracker_settings' => [
+ 'default' => [],
+ ],
+ 'datasource_settings' => [
+ 'entity:user' => [],
+ ],
+ ]);
+
+ // Use a search server as the dependency, since we have that available
+ // anyways. The entity type should not matter at all, though.
+ $this->dependency = Server::create([
+ 'id' => 'dependency',
+ 'name' => 'Test dependency',
+ 'backend' => 'search_api_test',
+ ]);
+ $this->dependency->save();
+ }
+
+ /**
+ * Tests index with a field dependency that gets removed.
+ */
+ public function testFieldDependency() {
+ // Add new field storage and field definitions. Use an indirect reference
+ // for the field to test whether this can also be handled correctly.
+ $this->createEntityReferenceField('user', 'user', 'parent', 'Parent', 'user');
+ $parent_field_storage = FieldStorageConfig::loadByName('user', 'parent');
+ /** @var \Drupal\field\FieldStorageConfigInterface $field_storage */
+ $field_storage = FieldStorageConfig::create([
+ 'field_name' => 'field_search',
+ 'type' => 'string',
+ 'entity_type' => 'user',
+ ]);
+ $field_storage->save();
+ $field_search = FieldConfig::create([
+ 'field_name' => 'field_search',
+ 'field_type' => 'string',
+ 'entity_type' => 'user',
+ 'bundle' => 'user',
+ 'label' => 'Search Field',
+ ]);
+ $field_search->save();
+
+ // Add fields to the index.
+ $fields_helper = \Drupal::getContainer()->get('search_api.fields_helper');
+ $field = $fields_helper->createFieldFromProperty($this->index, $field_storage->getPropertyDefinition('value'), 'entity:user', 'field_search', 'search', 'string');
+ $this->index->addField($field);
+ $field = $fields_helper->createFieldFromProperty($this->index, $field_storage->getPropertyDefinition('value'), 'entity:user', 'parent:entity:field_search', 'parent_search', 'string');
+ $this->index->addField($field);
+ $field = $fields_helper->createFieldFromProperty($this->index, $parent_field_storage->getPropertyDefinition('target_id'), 'entity:user', 'parent', 'parent', 'string');
+ $this->index->addField($field);
+ $this->index->save();
+
+ // New field has been added to the list of dependencies.
+ $config_dependencies = \Drupal::config('search_api.index.' . $this->index->id())->get('dependencies.config');
+ $this->assertContains($parent_field_storage->getConfigDependencyName(), $config_dependencies);
+ $this->assertContains($field_storage->getConfigDependencyName(), $config_dependencies);
+
+ // Remove a dependent field.
+ $parent_field_storage->delete();
+
+ // Index has not been deleted and index dependencies were updated.
+ $this->reloadIndex();
+ $index = \Drupal::config('search_api.index.' . $this->index->id());
+ $dependencies = $index->get('dependencies');
+ $this->assertFalse(isset($dependencies['config'][$parent_field_storage->getConfigDependencyName()]));
+ $this->assertContains($field_storage->getConfigDependencyName(), $config_dependencies);
+ // Correct fields were removed.
+ $this->assertEquals(['search'], array_keys($index->get('field_settings')));
+
+ // Remove a dependent field.
+ $field_storage->delete();
+
+ // Index has not been deleted and index dependencies were updated.
+ $this->reloadIndex();
+ $dependencies = \Drupal::config('search_api.index.' . $this->index->id())->get('dependencies');
+ $this->assertFalse(isset($dependencies['config'][$field_storage->getConfigDependencyName()]));
+ // Last field was removed.
+ $this->assertEquals([], array_keys($index->get('field_settings')));
+ }
+
+ /**
+ * Tests a backend with a dependency that gets removed.
+ *
+ * If the dependency does not get removed, proper cascading to the index is
+ * also verified.
+ *
+ * @param bool $remove_dependency
+ * Whether to remove the dependency from the backend when the object
+ * depended on is deleted.
+ *
+ * @dataProvider dependencyTestDataProvider
+ */
+ public function testBackendDependency($remove_dependency) {
+ $dependency_key = $this->dependency->getConfigDependencyKey();
+ $dependency_name = $this->dependency->getConfigDependencyName();
+
+ // Create a server using the test backend, and set the dependency in the
+ // configuration.
+ /** @var \Drupal\search_api\ServerInterface $server */
+ $server = Server::create([
+ 'id' => 'test_server',
+ 'name' => 'Test server',
+ 'backend' => 'search_api_test',
+ 'backend_config' => [
+ 'dependencies' => [
+ $dependency_key => [
+ $dependency_name,
+ ],
+ ],
+ ],
+ ]);
+ $server->save();
+ $server_dependency_key = $server->getConfigDependencyKey();
+ $server_dependency_name = $server->getConfigDependencyName();
+
+ // Set the server on the index and save that, too. However, we don't want
+ // the index enabled, since that would lead to all kinds of overhead which
+ // is completely irrelevant for this test.
+ $this->index->setServer($server);
+ $this->index->disable();
+ $this->index->save();
+
+ // Check that the dependencies were calculated correctly.
+ $server_dependencies = $server->getDependencies();
+ $this->assertContains($dependency_name, $server_dependencies[$dependency_key], 'Backend dependency correctly inserted');
+ $index_dependencies = $this->index->getDependencies();
+ $this->assertContains($server_dependency_name, $index_dependencies[$server_dependency_key], 'Server dependency correctly inserted');
+
+ // Tell the backend plugin whether it should successfully remove the
+ // dependency.
+ $this->setReturnValue('backend', 'onDependencyRemoval', $remove_dependency);
+
+ // Delete the backend's dependency.
+ $this->dependency->delete();
+
+ // Reload the index and check it's still there.
+ $this->reloadIndex();
+ $this->assertInstanceOf('Drupal\search_api\IndexInterface', $this->index, 'Index not removed');
+
+ // Reload the server.
+ $storage = \Drupal::entityTypeManager()->getStorage('search_api_server');
+ $storage->resetCache();
+ $server = $storage->load($server->id());
+
+ if ($remove_dependency) {
+ $this->assertInstanceOf('Drupal\search_api\ServerInterface', $server, 'Server was not removed');
+ $this->assertArrayNotHasKey('dependencies', $server->get('backend_config'), 'Backend config was adapted');
+ // @todo Logically, this should not be changed: if the server does not get
+ // removed, there is no need to adapt the index's configuration.
+ // However, the way this config dependency cascading is actually
+ // implemented in
+ // \Drupal\Core\Config\ConfigManager::getConfigEntitiesToChangeOnDependencyRemoval()
+ // does not seem to follow that logic, but just computes the complete
+ // tree of dependencies once and operates generally on the assumption
+ // that all of them will be deleted. See #2642374.
+ // $this->assertEquals($server->id(), $this->index->getServerId(), "Index's server was not changed");
+ }
+ else {
+ $this->assertNull($server, 'Server was removed');
+ $this->assertEquals(NULL, $this->index->getServerId(), 'Index server was changed');
+ }
+ }
+
+ /**
+ * Tests a datasource with a dependency that gets removed.
+ *
+ * @param bool $remove_dependency
+ * Whether to remove the dependency from the datasource when the object
+ * depended on is deleted.
+ *
+ * @dataProvider dependencyTestDataProvider
+ */
+ public function testDatasourceDependency($remove_dependency) {
+ // Add the datasource to the index and save it. The datasource configuration
+ // contains the dependencies it will return – in our case, we use the test
+ // server.
+ $dependency_key = $this->dependency->getConfigDependencyKey();
+ $dependency_name = $this->dependency->getConfigDependencyName();
+
+ // Also index users, to verify that they are unaffected by the processor.
+ $datasources = \Drupal::getContainer()
+ ->get('search_api.plugin_helper')
+ ->createDatasourcePlugins($this->index, ['entity:user', 'search_api_test'], [
+ 'search_api_test' => [
+ $dependency_key => [$dependency_name],
+ ],
+ ]);
+ $this->index->setDatasources($datasources);
+
+ $this->index->save();
+
+ // Check the dependencies were calculated correctly.
+ $dependencies = $this->index->getDependencies();
+ $this->assertContains($dependency_name, $dependencies[$dependency_key], 'Datasource dependency correctly inserted');
+
+ // Tell the datasource plugin whether it should successfully remove the
+ // dependency.
+ $this->setReturnValue('datasource', 'onDependencyRemoval', $remove_dependency);
+
+ // Delete the datasource's dependency.
+ $this->dependency->delete();
+
+ // Reload the index and check it's still there.
+ $this->reloadIndex();
+ $this->assertInstanceOf('Drupal\search_api\IndexInterface', $this->index, 'Index not removed');
+
+ // Make sure the dependency has been removed, one way or the other.
+ $dependencies = $this->index->getDependencies();
+ $dependencies += [$dependency_key => []];
+ $this->assertNotContains($dependency_name, $dependencies[$dependency_key], 'Datasource dependency removed from index');
+
+ // Depending on whether the plugin should have removed the dependency or
+ // not, make sure the right action was taken.
+ $datasources = $this->index->getDatasources();
+ if ($remove_dependency) {
+ $this->assertArrayHasKey('search_api_test', $datasources, 'Datasource not removed');
+ $this->assertEmpty($datasources['search_api_test']->getConfiguration(), 'Datasource settings adapted');
+ }
+ else {
+ $this->assertArrayNotHasKey('search_api_test', $datasources, 'Datasource removed');
+ }
+ }
+
+ /**
+ * Tests removing the (hard) dependency of the index's single datasource.
+ */
+ public function testSingleDatasourceDependency() {
+ // Add the datasource to the index and save it. The datasource configuration
+ // contains the dependencies it will return – in our case, we use the test
+ // server.
+ $dependency_key = $this->dependency->getConfigDependencyKey();
+ $dependency_name = $this->dependency->getConfigDependencyName();
+ $datasources['search_api_test'] = \Drupal::getContainer()
+ ->get('search_api.plugin_helper')
+ ->createDatasourcePlugin($this->index, 'search_api_test', [
+ $dependency_key => [$dependency_name],
+ ]);
+ $this->index->setDatasources($datasources);
+
+ $this->index->save();
+
+ // Since in this test the index will be removed, we need a mock key/value
+ // store (the index will purge any unsaved configuration of it upon
+ // deletion, which uses a "user-shared temp store", which in turn uses a
+ // key/value store).
+ $mock = $this->getMock('Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface');
+ $mock_factory = $this->getMock('Drupal\Core\KeyValueStore\KeyValueExpirableFactoryInterface');
+ $mock_factory->method('get')->willReturn($mock);
+ $this->container->set('keyvalue.expirable', $mock_factory);
+
+ // Delete the datasource's dependency.
+ $this->dependency->delete();
+
+ // Reload the index to ensure it was deleted.
+ $this->reloadIndex();
+ $this->assertNull($this->index, 'Index was removed');
+ }
+
+ /**
+ * Tests a processor with a dependency that gets removed.
+ *
+ * @param bool $remove_dependency
+ * Whether to remove the dependency from the processor when the object
+ * depended on is deleted.
+ *
+ * @dataProvider dependencyTestDataProvider
+ */
+ public function testProcessorDependency($remove_dependency) {
+ // Add the processor to the index and save it. The processor configuration
+ // contains the dependencies it will return – in our case, we use the test
+ // server.
+ $dependency_key = $this->dependency->getConfigDependencyKey();
+ $dependency_name = $this->dependency->getConfigDependencyName();
+
+ /** @var \Drupal\search_api\Processor\ProcessorInterface $processor */
+ $processor = \Drupal::getContainer()
+ ->get('search_api.plugin_helper')
+ ->createProcessorPlugin($this->index, 'search_api_test', [
+ $dependency_key => [$dependency_name],
+ ]);
+ $this->index->addProcessor($processor);
+ $this->index->save();
+
+ // Check the dependencies were calculated correctly.
+ $dependencies = $this->index->getDependencies();
+ $this->assertContains($dependency_name, $dependencies[$dependency_key], 'Processor dependency correctly inserted');
+
+ // Tell the processor plugin whether it should successfully remove the
+ // dependency.
+ $this->setReturnValue('processor', 'onDependencyRemoval', $remove_dependency);
+
+ // Delete the processor's dependency.
+ $this->dependency->delete();
+
+ // Reload the index and check it's still there.
+ $this->reloadIndex();
+ $this->assertInstanceOf('Drupal\search_api\IndexInterface', $this->index, 'Index not removed');
+
+ // Make sure the dependency has been removed, one way or the other.
+ $dependencies = $this->index->getDependencies();
+ $dependencies += [$dependency_key => []];
+ $this->assertNotContains($dependency_name, $dependencies[$dependency_key], 'Processor dependency removed from index');
+
+ // Depending on whether the plugin should have removed the dependency or
+ // not, make sure the right action was taken.
+ $processors = $this->index->getProcessors();
+ if ($remove_dependency) {
+ $this->assertArrayHasKey('search_api_test', $processors, 'Processor not removed');
+ $this->assertEmpty($processors['search_api_test']->getConfiguration(), 'Processor settings adapted');
+ }
+ else {
+ $this->assertArrayNotHasKey('search_api_test', $processors, 'Processor removed');
+ }
+ }
+
+ /**
+ * Tests a tracker with a dependency that gets removed.
+ *
+ * @param bool $remove_dependency
+ * Whether to remove the dependency from the tracker when the object
+ * depended on is deleted.
+ *
+ * @dataProvider dependencyTestDataProvider
+ */
+ public function testTrackerDependency($remove_dependency) {
+ // Set the tracker for the index and save it. The tracker configuration
+ // contains the dependencies it will return – in our case, we use the test
+ // server.
+ $dependency_key = $this->dependency->getConfigDependencyKey();
+ $dependency_name = $this->dependency->getConfigDependencyName();
+
+ /** @var \Drupal\search_api\Tracker\TrackerInterface $tracker */
+ $tracker = \Drupal::getContainer()
+ ->get('search_api.plugin_helper')
+ ->createTrackerPlugin($this->index, 'search_api_test', [
+ 'dependencies' => [
+ $dependency_key => [
+ $dependency_name,
+ ],
+ ],
+ ]);
+ $this->index->setTracker($tracker);
+ $this->index->save();
+
+ // Check the dependencies were calculated correctly.
+ $dependencies = $this->index->getDependencies();
+ $this->assertContains($dependency_name, $dependencies[$dependency_key], 'Tracker dependency correctly inserted');
+
+ // Tell the datasource plugin whether it should successfully remove the
+ // dependency.
+ $this->setReturnValue('tracker', 'onDependencyRemoval', $remove_dependency);
+
+ // Delete the tracker's dependency.
+ $this->dependency->delete();
+
+ // Reload the index and check it's still there.
+ $this->reloadIndex();
+ $this->assertInstanceOf('Drupal\search_api\IndexInterface', $this->index, 'Index not removed');
+
+ // Make sure the dependency has been removed, one way or the other.
+ $dependencies = $this->index->getDependencies();
+ $dependencies += [$dependency_key => []];
+ $this->assertNotContains($dependency_name, $dependencies[$dependency_key], 'Tracker dependency removed from index');
+
+ // Depending on whether the plugin should have removed the dependency or
+ // not, make sure the right action was taken.
+ $tracker_instance = $this->index->getTrackerInstance();
+ $tracker_id = $tracker_instance->getPluginId();
+ $tracker_config = $tracker_instance->getConfiguration();
+ if ($remove_dependency) {
+ $this->assertEquals('search_api_test', $tracker_id, 'Tracker not reset');
+ $this->assertEmpty($tracker_config['dependencies'], 'Tracker settings adapted');
+ }
+ else {
+ $this->assertEquals('default', $tracker_id, 'Tracker was reset');
+ $this->assertEquals($tracker_instance->defaultConfiguration(), $tracker_config, 'Tracker settings were cleared');
+ }
+ }
+
+ /**
+ * Data provider for this class's test methods.
+ *
+ * If $remove_dependency is TRUE, in Plugin::onDependencyRemoval() it clears
+ * its configuration (and thus its dependency, in those test plugins) and
+ * returns TRUE, which the index will take as "all OK, dependency removed" and
+ * leave the plugin where it is, only with updated configuration.
+ *
+ * If $remove_dependency is FALSE, Plugin::onDependencyRemoval() will do
+ * nothing and just return FALSE, the index says "oh, that plugin still has
+ * that removed dependency, so I should better remove the plugin" and the
+ * plugin gets removed.
+ *
+ * @return array
+ * An array of argument arrays for this class's test methods.
+ */
+ public function dependencyTestDataProvider() {
+ return [
+ 'Remove dependency' => [TRUE],
+ 'Keep dependency' => [FALSE],
+ ];
+ }
+
+ /**
+ * Tests whether module dependencies are handled correctly.
+ */
+ public function testModuleDependency() {
+ // Test with all types of plugins at once.
+ /** @var \Drupal\search_api\Datasource\DatasourceInterface $datasource */
+ $datasource = \Drupal::getContainer()
+ ->get('search_api.plugin_helper')
+ ->createDatasourcePlugin($this->index, 'search_api_test');
+ $this->index->addDatasource($datasource);
+ $datasource = \Drupal::getContainer()
+ ->get('search_api.plugin_helper')
+ ->createDatasourcePlugin($this->index, 'entity:user');
+ $this->index->addDatasource($datasource);
+
+ /** @var \Drupal\search_api\Processor\ProcessorInterface $processor */
+ $processor = \Drupal::getContainer()
+ ->get('search_api.plugin_helper')
+ ->createProcessorPlugin($this->index, 'search_api_test');
+ $this->index->addProcessor($processor);
+
+ /** @var \Drupal\search_api\Tracker\TrackerInterface $tracker */
+ $tracker = \Drupal::getContainer()
+ ->get('search_api.plugin_helper')
+ ->createTrackerPlugin($this->index, 'search_api_test');
+ $this->index->setTracker($tracker);
+
+ $this->index->save();
+
+ // Check the dependencies were calculated correctly.
+ $dependencies = $this->index->getDependencies();
+ $this->assertContains('search_api_test', $dependencies['module'], 'Module dependency correctly inserted');
+
+ // When the index resets the tracker, it needs to know the ID of the default
+ // tracker.
+ $this->installConfig('search_api');
+
+ // Disabling modules in Kernel tests normally doesn't trigger any kind of
+ // reaction, just removes it from the list of modules (for example, to avoid
+ // calling of a hook). Therefore, we have to trigger that behavior
+ // ourselves.
+ \Drupal::getContainer()->get('config.manager')->uninstall('module', 'search_api_test');
+
+ // Reload the index and check it's still there.
+ $this->reloadIndex();
+ $this->assertInstanceOf('Drupal\search_api\IndexInterface', $this->index, 'Index not removed');
+
+ // Make sure the dependency has been removed.
+ $dependencies = $this->index->getDependencies();
+ $dependencies += ['module' => []];
+ $this->assertNotContains('search_api_test', $dependencies['module'], 'Module dependency removed from index');
+
+ // Make sure all the plugins have been removed.
+ $this->assertNotContains('search_api_test', $this->index->getDatasources(), 'Datasource was removed');
+ $this->assertArrayNotHasKey('search_api_test', $this->index->getProcessors(), 'Processor was removed');
+ $this->assertEquals('default', $this->index->getTrackerId(), 'Tracker was reset');
+ }
+
+ /**
+ * Tests whether dependencies of used data types are handled correctly.
+ *
+ * @param string $dependency_type
+ * The type of dependency that should be set on the data type (and then
+ * removed): "module" or "config".
+ *
+ * @dataProvider dataTypeDependencyTestDataProvider
+ */
+ public function testDataTypeDependency($dependency_type) {
+ switch ($dependency_type) {
+ case 'module':
+ $type = 'search_api_test';
+ $config_dependency_key = 'module';
+ $config_dependency_name = 'search_api_test';
+ break;
+
+ case 'config':
+ $type = 'search_api_test_altering';
+ $config_dependency_key = $this->dependency->getConfigDependencyKey();
+ $config_dependency_name = $this->dependency->getConfigDependencyName();
+ \Drupal::state()->set('search_api_test.data_type.dependencies', [
+ $config_dependency_key => [
+ $config_dependency_name,
+ ],
+ ]);
+ break;
+
+ default:
+ $this->fail();
+ return;
+ }
+
+ // Use the "user" datasource (to not get a module dependency via that) and
+ // add a field with the given data type.
+ $datasources = \Drupal::getContainer()
+ ->get('search_api.plugin_helper')
+ ->createDatasourcePlugins($this->index, ['entity:user']);
+ $this->index->setDatasources($datasources);
+ $field = \Drupal::getContainer()
+ ->get('search_api.fields_helper')
+ ->createField($this->index, 'uid', [
+ 'label' => 'ID',
+ 'datasource_id' => 'entity:user',
+ 'property_path' => 'uid',
+ 'type' => $type,
+ ]);
+ $this->index->addField($field);
+ // Set the server to NULL to not have a dependency on that by default.
+ $this->index->setServer(NULL);
+ $this->index->save();
+
+ // Check the dependencies were calculated correctly.
+ $dependencies = $this->index->getDependencies();
+ $dependencies += [$config_dependency_key => []];
+ $this->assertContains($config_dependency_name, $dependencies[$config_dependency_key], 'Data type dependency correctly inserted');
+
+ switch ($dependency_type) {
+ case 'module':
+ // Disabling modules in Kernel tests normally doesn't trigger any kind of
+ // reaction, just removes it from the list of modules (for example, to
+ // avoid calling any of its hooks). Therefore, we have to trigger that
+ // behavior ourselves.
+ \Drupal::getContainer()
+ ->get('config.manager')
+ ->uninstall('module', 'search_api_test');
+ break;
+
+ case 'config':
+ $this->dependency->delete();
+ break;
+ }
+
+ // Reload the index and check it's still there.
+ $this->reloadIndex();
+ $this->assertInstanceOf('Drupal\search_api\IndexInterface', $this->index, 'Index not removed');
+
+ // Make sure the dependency has been removed.
+ $dependencies = $this->index->getDependencies();
+ $dependencies += [$config_dependency_key => []];
+ $this->assertNotContains($config_dependency_name, $dependencies[$config_dependency_key], 'Data type dependency correctly removed');
+
+ // Make sure the field type has changed.
+ $field = $this->index->getField('uid');
+ $this->assertNotNull($field, 'Field was not removed');
+ $this->assertEquals('string', $field->getType(), 'Field type was changed to fallback type');
+ }
+
+ /**
+ * Data provider for testDataTypeDependency().
+ *
+ * @return array
+ * An array of argument arrays for
+ * \Drupal\Tests\search_api\Kernel\DependencyRemovalTest::testDataTypeDependency().
+ */
+ public function dataTypeDependencyTestDataProvider() {
+ return [
+ 'Module dependency' => ['module'],
+ 'Config dependency' => ['config'],
+ ];
+ }
+
+ /**
+ * Reloads the index with the latest copy from storage.
+ */
+ protected function reloadIndex() {
+ $storage = \Drupal::entityTypeManager()->getStorage('search_api_index');
+ $storage->resetCache();
+ $this->index = $storage->load($this->index->id());
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Datasource/BundlelessEntityTest.php b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Datasource/BundlelessEntityTest.php
new file mode 100644
index 000000000..a5c212f38
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Datasource/BundlelessEntityTest.php
@@ -0,0 +1,91 @@
+/:.
+ *
+ * @group search_api
+ */
+class BundlelessEntityTest extends KernelTestBase {
+
+ /**
+ * The entity type used in the test.
+ *
+ * @var string
+ */
+ protected $testEntityTypeId = 'user';
+
+ /**
+ * {@inheritdoc}
+ */
+ public static $modules = [
+ 'search_api',
+ 'user',
+ 'system',
+ ];
+
+ /**
+ * The search index used for testing.
+ *
+ * @var \Drupal\search_api\IndexInterface
+ */
+ protected $index;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setUp() {
+ parent::setUp();
+
+ $this->installEntitySchema('search_api_task');
+ $this->installConfig(['user']);
+
+ // Create a test index.
+ $this->index = Index::create([
+ 'name' => 'Test Index',
+ 'id' => 'test_index',
+ 'status' => FALSE,
+ 'datasource_settings' => [
+ 'entity:' . $this->testEntityTypeId => [],
+ ],
+ 'tracker_settings' => [
+ 'default' => [],
+ ],
+ ]);
+ $this->index->save();
+ }
+
+ /**
+ * Tests that view modes are returned correctly.
+ */
+ public function testViewModes() {
+ $datasource = $this->index->getDatasource('entity:' . $this->testEntityTypeId);
+
+ $bundles = $datasource->getBundles();
+ $expected = [
+ 'user' => 'User',
+ ];
+ $this->assertEquals($expected, $bundles);
+
+ $view_modes = $datasource->getViewModes('user');
+ $expected = [
+ 'compact' => 'Compact',
+ 'default' => 'Default',
+ 'full' => 'User account',
+ ];
+ ksort($view_modes);
+ $this->assertEquals($expected, $view_modes);
+
+ $view_modes = $datasource->getViewModes();
+ ksort($view_modes);
+ $this->assertEquals($expected, $view_modes);
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Datasource/ContentEntityDatasourceTest.php b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Datasource/ContentEntityDatasourceTest.php
new file mode 100644
index 000000000..c2bc267af
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Datasource/ContentEntityDatasourceTest.php
@@ -0,0 +1,257 @@
+set('entity_test.translation', TRUE);
+
+ $this->installSchema('search_api', ['search_api_item']);
+ $this->installEntitySchema('entity_test_mulrev_changed');
+ $this->installConfig(['language']);
+
+ // Create some languages.
+ for ($i = 0; $i < 2; ++$i) {
+ ConfigurableLanguage::create([
+ 'id' => 'l' . $i,
+ 'label' => 'language - ' . $i,
+ 'weight' => $i,
+ ])->save();
+ }
+
+ // Create a test index.
+ $this->index = Index::create([
+ 'name' => 'Test Index',
+ 'id' => 'test_index',
+ 'status' => FALSE,
+ 'datasource_settings' => [
+ 'entity:' . $this->testEntityTypeId => [],
+ ],
+ 'tracker_settings' => [
+ 'default' => [],
+ ],
+ ]);
+ $this->datasource = $this->index->getDatasource('entity:' . $this->testEntityTypeId);
+
+ $this->setUpExampleStructure();
+
+ foreach (['item', 'article'] as $i => $bundle) {
+ $entity = EntityTestMulRevChanged::create([
+ 'id' => $i + 1,
+ 'type' => $bundle,
+ 'langcode' => 'l0',
+ ]);
+ $entity->save();
+ $entity->addTranslation('l1')->save();
+ }
+
+ $this->allItemIds = ['1:l0', '1:l1', '2:l0', '2:l1'];
+ }
+
+ /**
+ * Tests entity loading.
+ *
+ * @covers ::loadMultiple
+ */
+ public function testEntityLoading() {
+ $loaded_items = $this->datasource->loadMultiple($this->allItemIds);
+ $this->assertCorrectItems($this->allItemIds, $loaded_items);
+
+ $this->datasource->setConfiguration([
+ 'bundles' => [
+ 'default' => FALSE,
+ 'selected' => ['item'],
+ ],
+ 'languages' => [
+ 'default' => TRUE,
+ 'selected' => ['l0'],
+ ],
+ ]);
+ $loaded_items = $this->datasource->loadMultiple($this->allItemIds);
+ $this->assertCorrectItems(['1:l1'], $loaded_items);
+
+ $this->datasource->setConfiguration([
+ 'bundles' => [
+ 'default' => TRUE,
+ 'selected' => ['item'],
+ ],
+ 'languages' => [
+ 'default' => FALSE,
+ 'selected' => ['l0', 'l1'],
+ ],
+ ]);
+ $loaded_items = $this->datasource->loadMultiple($this->allItemIds);
+ $this->assertCorrectItems(['2:l0', '2:l1'], $loaded_items);
+ }
+
+ /**
+ * Asserts that the given array of loaded items is correct.
+ *
+ * @param string[] $expected_ids
+ * The expected item IDs, sorted.
+ * @param \Drupal\Core\TypedData\ComplexDataInterface[] $loaded_items
+ * The loaded items.
+ */
+ protected function assertCorrectItems(array $expected_ids, array $loaded_items) {
+ $loaded_ids = array_keys($loaded_items);
+ sort($loaded_ids);
+ $this->assertEquals($expected_ids, $loaded_ids);
+
+ foreach ($loaded_items as $item_id => $item) {
+ $this->assertInstanceOf(EntityAdapter::class, $item);
+ $entity = $item->getValue();
+ $this->assertInstanceOf(EntityTestMulRevChanged::class, $entity);
+ list($id, $langcode) = explode(':', $item_id);
+ $this->assertEquals($id, $entity->id());
+ $this->assertEquals($langcode, $entity->language()->getId());
+ }
+ }
+
+ /**
+ * Verifies that paged item discovery works correctly.
+ *
+ * @covers ::getPartialItemIds
+ */
+ public function testItemDiscovery() {
+ // Set page size to 1 to also test paging.
+ \Drupal::configFactory()
+ ->getEditable('search_api.settings')
+ ->set('tracking_page_size', 1)
+ ->save();
+
+ // Test item discovery with various bundle/language combinations.
+ $discovered_ids = $this->getItemIds();
+ $this->assertEquals($this->allItemIds, $discovered_ids);
+
+ $discovered_ids = $this->getItemIds(['item']);
+ $this->assertEquals(['1:l0', '1:l1'], $discovered_ids);
+
+ $discovered_ids = $this->getItemIds(['item'], []);
+ $this->assertEquals(['1:l0', '1:l1'], $discovered_ids);
+
+ $discovered_ids = $this->getItemIds(NULL, ['l0']);
+ $this->assertEquals(['1:l0', '2:l0'], $discovered_ids);
+
+ $discovered_ids = $this->getItemIds([], ['l0']);
+ $this->assertEquals(['1:l0', '2:l0'], $discovered_ids);
+
+ $discovered_ids = $this->getItemIds(['item'], ['l0']);
+ $this->assertEquals(['1:l0', '1:l1', '2:l0'], $discovered_ids);
+
+ $discovered_ids = $this->getItemIds(['item', 'article'], ['l0']);
+ $this->assertEquals($this->allItemIds, $discovered_ids);
+
+ $discovered_ids = $this->getItemIds(['item'], ['l0', 'l1']);
+ $this->assertEquals($this->allItemIds, $discovered_ids);
+
+ $discovered_ids = $this->getItemIds(['item', 'article'], []);
+ $this->assertEquals($this->allItemIds, $discovered_ids);
+
+ $discovered_ids = $this->getItemIds([], ['l0', 'l1']);
+ $this->assertEquals($this->allItemIds, $discovered_ids);
+
+ $discovered_ids = $this->getItemIds([], []);
+ $this->assertEquals([], $discovered_ids);
+
+ $discovered_ids = $this->getItemIds([], NULL);
+ $this->assertEquals([], $discovered_ids);
+
+ $discovered_ids = $this->getItemIds(NULL, []);
+ $this->assertEquals([], $discovered_ids);
+ }
+
+ /**
+ * Retrieves the IDs of all matching items from the test datasource.
+ *
+ * Will automatically use paging to go through the entire result set.
+ *
+ * If both $bundles and $languages are specified, they are combined with OR.
+ *
+ * @param string[]|null $bundles
+ * (optional) The bundles for which all item IDs should be returned; or NULL
+ * to retrieve IDs from all enabled bundles in this datasource.
+ * @param string[]|null $languages
+ * (optional) The languages for which all item IDs should be returned; or
+ * NULL to retrieve IDs from all enabled languages in this datasource.
+ *
+ * @return string[]
+ * All discovered item IDs.
+ *
+ * @see \Drupal\search_api\Plugin\search_api\datasource\EntityDatasourceInterface::getPartialItemIds()
+ */
+ protected function getItemIds(array $bundles = NULL, array $languages = NULL) {
+ $discovered_ids = [];
+ for ($page = 0;; ++$page) {
+ $new_ids = $this->datasource->getPartialItemIds($page, $bundles, $languages);
+ if ($new_ids === NULL) {
+ break;
+ }
+ $discovered_ids = array_merge($discovered_ids, $new_ids);
+ }
+ sort($discovered_ids);
+ return $discovered_ids;
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Datasource/DatasourceTaskTest.php b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Datasource/DatasourceTaskTest.php
new file mode 100644
index 000000000..e9094b161
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Datasource/DatasourceTaskTest.php
@@ -0,0 +1,251 @@
+set('entity_test.translation', TRUE);
+
+ // Define the bundles for our test entity type. (Should happen before we
+ // install its entity schema.)
+ $bundles = [
+ 'article' => [
+ 'label' => 'Article',
+ ],
+ 'item' => [
+ 'label' => 'Item',
+ ],
+ ];
+ \Drupal::state()->set($this->testEntityTypeId . '.bundles', $bundles);
+
+ $this->installSchema('search_api', ['search_api_item']);
+ $this->installSchema('system', ['sequences']);
+ $this->installEntitySchema('entity_test_mulrev_changed');
+ $this->installEntitySchema('search_api_task');
+
+ $this->taskManager = $this->container->get('search_api.task_manager');
+
+ // Create some languages.
+ $this->installConfig(['language']);
+ for ($i = 0; $i < 3; ++$i) {
+ /** @var \Drupal\language\ConfigurableLanguageInterface $language */
+ $language = ConfigurableLanguage::create([
+ 'id' => 'l' . $i,
+ 'label' => 'language - ' . $i,
+ 'weight' => $i,
+ ]);
+ $language->save();
+ }
+
+ $this->installConfig('search_api');
+
+ // Create a test server.
+ $this->server = Server::create([
+ 'name' => 'Test Server',
+ 'id' => 'test_server',
+ 'status' => 1,
+ 'backend' => 'search_api_test',
+ ]);
+ $this->server->save();
+
+ // Create a test index.
+ $this->index = Index::create([
+ 'name' => 'Test Index',
+ 'id' => 'test_index',
+ 'status' => 1,
+ 'datasource_settings' => [
+ 'entity:' . $this->testEntityTypeId => [],
+ ],
+ 'tracker_settings' => [
+ 'default' => [],
+ ],
+ 'server' => $this->server->id(),
+ 'options' => ['index_directly' => FALSE],
+ ]);
+ $this->index->save();
+
+ $this->taskManager->deleteTasks();
+ }
+
+ /**
+ * Tests that datasource config changes are reflected correctly.
+ */
+ public function testItemTranslations() {
+ // Test retrieving language and translations when no translations are
+ // available.
+ /** @var \Drupal\entity_test\Entity\EntityTestMulRevChanged $entity_1 */
+ $uid = $this->container->get('current_user')->id();
+ $entity_1 = EntityTestMulRevChanged::create([
+ 'id' => 1,
+ 'name' => 'test 1',
+ 'user_id' => $uid,
+ 'type' => 'item',
+ 'langcode' => 'l0',
+ ]);
+ $entity_1->save();
+ $entity_1->addTranslation('l1')->save();
+ $entity_1->addTranslation('l2')->save();
+
+ /** @var \Drupal\entity_test\Entity\EntityTestMulRevChanged $entity_2 */
+ $entity_2 = EntityTestMulRevChanged::create([
+ 'id' => 2,
+ 'name' => 'test 2',
+ 'user_id' => $uid,
+ 'type' => 'article',
+ 'langcode' => 'l1',
+ ]);
+ $entity_2->save();
+ $entity_2->addTranslation('l0')->save();
+ $entity_2->addTranslation('l2')->save();
+
+ $index = $this->index;
+ $tracker = $index->getTrackerInstance();
+ $datasource_id = 'entity:' . $this->testEntityTypeId;
+ $datasource = $index->getDatasource($datasource_id);
+
+ $get_ids = function (array $raw_ids) use ($datasource_id) {
+ foreach ($raw_ids as $i => $id) {
+ $raw_ids[$i] = Utility::createCombinedId($datasource_id, $id);
+ }
+ return $raw_ids;
+ };
+
+ $this->assertEquals(6, $tracker->getTotalItemsCount());
+ $this->assertEquals(6, $tracker->getRemainingItemsCount());
+
+ $configuration = [
+ 'bundles' => [
+ 'default' => TRUE,
+ 'selected' => [
+ 'item',
+ ],
+ ],
+ 'languages' => [
+ 'default' => FALSE,
+ 'selected' => [
+ 'l0',
+ 'l2',
+ ],
+ ],
+ ];
+ $datasource->setConfiguration($configuration);
+ $index->save();
+
+ $this->runBatch();
+
+ $expected = $get_ids(['2:l0', '2:l2']);
+ $this->assertEquals(count($expected), $tracker->getTotalItemsCount());
+ $remaining = $tracker->getRemainingItems();
+ sort($remaining);
+ $this->assertEquals($expected, $remaining);
+
+ $configuration['bundles']['default'] = FALSE;
+ $configuration['bundles']['selected'][] = 'article';
+ $configuration['languages']['selected'] = ['l0'];
+ $datasource->setConfiguration($configuration);
+ $index->save();
+
+ $this->runBatch();
+
+ $expected = $get_ids(['1:l0', '2:l0']);
+ $this->assertEquals(count($expected), $tracker->getTotalItemsCount());
+ $remaining = $tracker->getRemainingItems();
+ sort($remaining);
+ $this->assertEquals($expected, $remaining);
+
+ $configuration['languages']['selected'][] = 'l1';
+ $datasource->setConfiguration($configuration);
+ $index->save();
+
+ $this->runBatch();
+
+ $expected = $get_ids(['1:l0', '1:l1', '2:l0', '2:l1']);
+ $this->assertEquals(count($expected), $tracker->getTotalItemsCount());
+ $remaining = $tracker->getRemainingItems();
+ sort($remaining);
+ $this->assertEquals($expected, $remaining);
+
+ $configuration['bundles']['selected'] = ['article'];
+ $datasource->setConfiguration($configuration);
+ $index->save();
+
+ $this->runBatch();
+
+ $expected = $get_ids(['2:l0', '2:l1']);
+ $this->assertEquals(count($expected), $tracker->getTotalItemsCount());
+ $remaining = $tracker->getRemainingItems();
+ sort($remaining);
+ $this->assertEquals($expected, $remaining);
+ }
+
+ /**
+ * Runs the currently set batch, if any.
+ */
+ protected function runBatch() {
+ $batch = &batch_get();
+ if ($batch) {
+ $batch['progressive'] = FALSE;
+ batch_process();
+ }
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Datasource/EntityStringIdTest.php b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Datasource/EntityStringIdTest.php
new file mode 100644
index 000000000..f61e162c9
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Datasource/EntityStringIdTest.php
@@ -0,0 +1,153 @@
+/:.
+ *
+ * @group search_api
+ */
+class EntityStringIdTest extends KernelTestBase {
+
+ /**
+ * The test entity type used in the test.
+ *
+ * @var string
+ */
+ protected $testEntityTypeId = 'entity_test_string_id';
+
+ /**
+ * The search server used for testing.
+ *
+ * @var \Drupal\search_api\ServerInterface
+ */
+ protected $server;
+
+ /**
+ * The search index used for testing.
+ *
+ * @var \Drupal\search_api\IndexInterface
+ */
+ protected $index;
+
+ /**
+ * {@inheritdoc}
+ */
+ public static $modules = [
+ 'search_api',
+ 'search_api_test',
+ 'language',
+ 'user',
+ 'system',
+ 'entity_test',
+ ];
+
+ /**
+ * An array of language codes.
+ *
+ * @var string[]
+ */
+ protected $langcodes;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setUp() {
+ parent::setUp();
+
+ $this->installSchema('search_api', [
+ 'search_api_item',
+ ]);
+ $this->installEntitySchema('entity_test_string_id');
+ $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);
+ }
+
+ // Create a test server.
+ $this->server = Server::create([
+ 'name' => 'Test Server',
+ 'id' => 'test_server',
+ 'status' => 1,
+ 'backend' => 'search_api_test',
+ ]);
+ $this->server->save();
+
+ // Create a test index.
+ $this->index = Index::create([
+ 'name' => 'Test Index',
+ 'id' => 'test_index',
+ 'status' => 1,
+ 'datasource_settings' => [
+ 'entity:' . $this->testEntityTypeId => [],
+ ],
+ 'tracker_settings' => [
+ 'default' => [],
+ ],
+ 'server' => $this->server->id(),
+ 'options' => ['index_directly' => FALSE],
+ ]);
+ $this->index->save();
+ }
+
+ /**
+ * Tests indexing of entities with string IDs.
+ *
+ * @param string $entity_id
+ * An entity ID for which to check indexing.
+ *
+ * @dataProvider entityStringIdList
+ */
+ public function testUriStringId($entity_id) {
+ $entity = EntityTestStringId::create([
+ 'id' => $entity_id,
+ 'name' => 'String Test',
+ 'user_id' => $this->container->get('current_user')->id(),
+ ]);
+ $entity->save();
+
+ // Test that the datasource returns the correct item IDs.
+ $datasource = $this->index->getDatasource('entity:' . $this->testEntityTypeId);
+ $datasource_item_ids = $datasource->getItemIds();
+ $expected = [
+ $entity_id . ':und',
+ ];
+ $this->assertEquals($expected, $datasource_item_ids, 'Datasource returns correct item ids.');
+
+ // Test indexing the new entity.
+ $this->assertEquals(0, $this->index->getTrackerInstance()->getIndexedItemsCount(), 'The index is empty.');
+ $this->assertEquals(1, $this->index->getTrackerInstance()->getTotalItemsCount(), 'There is one item to be indexed.');
+ $this->index->indexItems();
+ $this->assertEquals(1, $this->index->getTrackerInstance()->getIndexedItemsCount(), 'One item has been indexed.');
+ }
+
+ /**
+ * Provides string IDs to test.
+ *
+ * @return array
+ * An array of arrays which contain a list of parameters to be passed to the
+ * testUriStringId() test method.
+ */
+ public function entityStringIdList() {
+ return [
+ 'Normal machine name' => ['short_string_id'],
+ 'URL ID (with special characters)' => ['http://drupal.org'],
+ 'Long ID' => [str_repeat('a', 100)],
+ ];
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Datasource/LanguageKernelTest.php b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Datasource/LanguageKernelTest.php
new file mode 100644
index 000000000..b0794f116
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Datasource/LanguageKernelTest.php
@@ -0,0 +1,358 @@
+set('entity_test.translation', TRUE);
+
+ $this->installSchema('search_api', ['search_api_item']);
+ $this->installEntitySchema('entity_test_mulrev_changed');
+ $this->installEntitySchema('search_api_task');
+ $this->installEntitySchema('field_storage_config');
+ $this->installEntitySchema('field_config');
+ $this->installConfig('search_api');
+
+ // Create the default languages.
+ $this->installConfig(['language']);
+ $this->langcodes = [];
+ for ($i = 0; $i < 3; ++$i) {
+ /** @var \Drupal\language\Entity\ConfigurableLanguage $language */
+ $language = ConfigurableLanguage::create([
+ 'id' => 'l' . $i,
+ 'label' => 'language - ' . $i,
+ 'weight' => $i,
+ ]);
+ $this->langcodes[$i] = $language->getId();
+ $language->save();
+ }
+
+ // Create an entity reference field on the test entity type.
+ FieldStorageConfig::create([
+ 'field_name' => 'link',
+ 'entity_type' => 'entity_test_mulrev_changed',
+ 'type' => 'entity_reference',
+ 'cardinality' => 1,
+ 'settings' => [
+ 'target_type' => 'entity_test_mulrev_changed',
+ ],
+ ])->save();
+ FieldConfig::create([
+ 'field_name' => 'link',
+ 'entity_type' => 'entity_test_mulrev_changed',
+ 'bundle' => 'entity_test_mulrev_changed',
+ 'label' => 'Link',
+ ])->save();
+
+ // 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);
+ }
+
+ // Create a test server.
+ $this->server = Server::create([
+ 'name' => 'Test Server',
+ 'id' => 'test_server',
+ 'status' => 1,
+ 'backend' => 'search_api_test',
+ ]);
+ $this->server->save();
+
+ // Create a test index.
+ $this->index = Index::create([
+ 'name' => 'Test Index',
+ 'id' => 'test_index',
+ 'status' => 1,
+ 'datasource_settings' => [
+ 'entity:' . $this->testEntityTypeId => [],
+ ],
+ 'tracker_settings' => [
+ 'default' => [],
+ ],
+ 'field_settings' => [
+ 'link' => [
+ 'label' => 'Link name',
+ 'type' => 'string',
+ 'datasource_id' => 'entity:entity_test_mulrev_changed',
+ 'property_path' => 'link:entity:name',
+ ],
+ ],
+ 'server' => $this->server->id(),
+ 'options' => ['index_directly' => FALSE],
+ ]);
+ $this->index->save();
+ }
+
+ /**
+ * Tests translation handling of the content entity datasource.
+ */
+ public function testItemTranslations() {
+ // Test retrieving language and translations when no translations are
+ // available.
+ /** @var \Drupal\entity_test\Entity\EntityTestMulRevChanged $entity_1 */
+ $entity_1 = EntityTestMulRevChanged::create([
+ 'id' => 1,
+ 'name' => 'test 1',
+ 'user_id' => $this->container->get('current_user')->id(),
+ ]);
+ $entity_1->save();
+ $entity_1->set('link', $entity_1->id());
+ $this->assertEquals('en', $entity_1->language()->getId(), new FormattableMarkup('%entity_type: Entity language set to site default.', ['%entity_type' => $this->testEntityTypeId]));
+ $this->assertFalse($entity_1->getTranslationLanguages(FALSE), new FormattableMarkup('%entity_type: No translations are available', ['%entity_type' => $this->testEntityTypeId]));
+
+ /** @var \Drupal\entity_test\Entity\EntityTestMulRevChanged $entity_2 */
+ $entity_2 = EntityTestMulRevChanged::create([
+ 'id' => 2,
+ 'name' => 'test 2',
+ 'user_id' => $this->container->get('current_user')->id(),
+ ]);
+ $entity_2->save();
+ $this->assertEquals('en', $entity_2->language()->getId(), new FormattableMarkup('%entity_type: Entity language set to site default.', ['%entity_type' => $this->testEntityTypeId]));
+ $this->assertFalse($entity_2->getTranslationLanguages(FALSE), new FormattableMarkup('%entity_type: No translations are available', ['%entity_type' => $this->testEntityTypeId]));
+
+ // Test that the datasource returns the correct item IDs.
+ $datasource = $this->index->getDatasource('entity:' . $this->testEntityTypeId);
+ $datasource_item_ids = $datasource->getItemIds();
+ sort($datasource_item_ids);
+ $expected = [
+ '1:en',
+ '2:en',
+ ];
+ $this->assertEquals($expected, $datasource_item_ids, 'Datasource returns correct item ids.');
+
+ // Test indexing the new entity.
+ $this->assertEquals(0, $this->index->getTrackerInstance()->getIndexedItemsCount(), 'The index is empty.');
+ $this->assertEquals(2, $this->index->getTrackerInstance()->getTotalItemsCount(), 'There are two items to be indexed.');
+ $this->index->indexItems();
+ $this->assertEquals(2, $this->index->getTrackerInstance()->getIndexedItemsCount(), 'Two items have been indexed.');
+
+ // Now, make the first entity language-specific by assigning a language.
+ $default_langcode = $this->langcodes[0];
+ $entity_1->get('langcode')->setValue($default_langcode);
+ $entity_1->save();
+ $this->assertEquals(\Drupal::languageManager()->getLanguage($this->langcodes[0]), $entity_1->language(), new FormattableMarkup('%entity_type: Entity language retrieved.', ['%entity_type' => $this->testEntityTypeId]));
+ $this->assertFalse($entity_1->getTranslationLanguages(FALSE), new FormattableMarkup('%entity_type: No translations are available', ['%entity_type' => $this->testEntityTypeId]));
+
+ // Test that the datasource returns the correct item IDs.
+ $datasource_item_ids = $datasource->getItemIds();
+ sort($datasource_item_ids);
+ $expected = [
+ '1:' . $this->langcodes[0],
+ '2:en',
+ ];
+ $this->assertEquals($expected, $datasource_item_ids, 'Datasource returns correct item ids.');
+
+ // Test that the index needs to be updated.
+ $this->assertEquals(1, $this->index->getTrackerInstance()->getIndexedItemsCount(), 'The updated item needs to be reindexed.');
+ $this->assertEquals(2, $this->index->getTrackerInstance()->getTotalItemsCount(), 'There are two items in total.');
+
+ // Set two translations for the first entity and test that the datasource
+ // returns three separate item IDs, one for each translation.
+ $translation = $entity_1->addTranslation($this->langcodes[1]);
+ $this->assertEquals(1, $entity_1->link[0]->entity->id());
+ $translation->set('name', 'test 1 - ' . $this->langcodes[1]);
+ $translation->set('link', $entity_1->id());
+ $translation->save();
+ $translation = $entity_1->addTranslation($this->langcodes[2]);
+ $translation->set('name', 'test 1 - ' . $this->langcodes[2]);
+ $translation->set('link', $entity_1->id());
+ $translation->save();
+ $this->assertTrue($entity_1->getTranslationLanguages(FALSE), new FormattableMarkup('%entity_type: Translations are available', ['%entity_type' => $this->testEntityTypeId]));
+
+ $datasource_item_ids = $datasource->getItemIds();
+ sort($datasource_item_ids);
+ $expected = [
+ '1:' . $this->langcodes[0],
+ '1:' . $this->langcodes[1],
+ '1:' . $this->langcodes[2],
+ '2:en',
+ ];
+ $this->assertEquals($expected, $datasource_item_ids, 'Datasource returns correct item ids for a translated entity.');
+
+ foreach ($datasource->loadMultiple($datasource_item_ids) as $id => $object) {
+ // Test whether the item reports the correct language.
+ list($entity_id, $langcode) = explode(':', $id, 2);
+ $item = \Drupal::getContainer()
+ ->get('search_api.fields_helper')
+ ->createItemFromObject($this->index, $object, NULL, $datasource);
+ $this->assertEquals($langcode, $item->getLanguage(), "Item with ID '$id' has the correct language set.");
+
+ // Test whether nested field extraction works correctly.
+ if ($entity_id == 1) {
+ $field = $item->getField('link');
+ $translation_label = $entity_1->getTranslation($langcode)->label();
+ $this->assertEquals([$translation_label], $field->getValues());
+ }
+ }
+
+ // Tests that a query with an empty array of languages will return an empty
+ // result set, without going through the server. (Our test backend wouldn't
+ // care about languages.)
+ $results = $this->index->query()->setLanguages([])->execute();
+ $this->assertEquals(0, $results->getResultCount(), 'Query with empty languages list returned correct number of results.');
+ $this->assertEquals([], $results->getResultItems(), 'Query with empty languages list returned correct result.');
+
+ // Test that the index needs to be updated.
+ $this->assertEquals(1, $this->index->getTrackerInstance()->getIndexedItemsCount(), 'The updated items needs to be reindexed.');
+ $this->assertEquals(4, $this->index->getTrackerInstance()->getTotalItemsCount(), 'There are four items in total.');
+
+ // Delete one translation and test that the datasource returns only three
+ // items.
+ $entity_1->removeTranslation($this->langcodes[2]);
+ $entity_1->save();
+
+ $datasource_item_ids = $datasource->getItemIds();
+ sort($datasource_item_ids);
+ $expected = [
+ '1:' . $this->langcodes[0],
+ '1:' . $this->langcodes[1],
+ '2:en',
+ ];
+ $this->assertEquals($expected, $datasource_item_ids, 'Datasource returns correct item ids for a translated entity.');
+
+ // Test reindexing.
+ $this->assertEquals(3, $this->index->getTrackerInstance()->getTotalItemsCount(), 'There are three items in total.');
+ $this->assertEquals(1, $this->index->getTrackerInstance()->getIndexedItemsCount(), 'The updated items needs to be reindexed.');
+ $this->index->indexItems();
+ $this->assertEquals(3, $this->index->getTrackerInstance()->getIndexedItemsCount(), 'Three items are indexed.');
+ }
+
+ /**
+ * Tests tracking when the datasource has only specific languages enabled.
+ */
+ public function testDatasourceLanguagesSetting() {
+ $datasource_id = 'entity:' . $this->testEntityTypeId;
+ $datasource = $this->index->getDatasource($datasource_id);
+ $datasource->setConfiguration([
+ 'languages' => [
+ 'default' => FALSE,
+ 'selected' => [$this->langcodes[0], $this->langcodes[2]],
+ ],
+ ]);
+ $this->index->save();
+
+ // Create new entities with our three custom languages and one with the
+ // default language (English).
+ /** @var \Drupal\entity_test\Entity\EntityTestMulRevChanged[] $entities */
+ $entities = [];
+ $uid = $this->container->get('current_user')->id();
+ foreach ([0, 1, 2, 3] as $i) {
+ $entity = EntityTestMulRevChanged::create([
+ 'id' => $i + 1,
+ 'name' => 'test 1',
+ 'user_id' => $uid,
+ ]);
+ if (!empty($this->langcodes[$i])) {
+ $entity->set('langcode', $this->langcodes[$i]);
+ }
+ $entity->save();
+ $entities[$i] = $entity;
+ }
+
+ // Only the ones with languages "l0" and "l2" should have been tracked.
+ $tracker = $this->index->getTrackerInstance();
+ $this->assertEquals(2, $tracker->getTotalItemsCount());
+ $expected = [
+ Utility::createCombinedId($datasource_id, "1:{$this->langcodes[0]}"),
+ Utility::createCombinedId($datasource_id, "3:{$this->langcodes[2]}"),
+ ];
+ $actual = $tracker->getRemainingItems();
+ sort($actual);
+ $this->assertEquals($expected, $actual);
+
+ // Add a translation for a disabled language. It should not be tracked.
+ $entities[0]->addTranslation($this->langcodes[1])->save();
+ $this->assertEquals(2, $tracker->getTotalItemsCount());
+ $actual = $tracker->getRemainingItems();
+ sort($actual);
+ $this->assertEquals($expected, $actual);
+
+ // Add a translation for an enabled language. It should be tracked.
+ $langcode = $this->langcodes[0];
+ $entities[1]->addTranslation($langcode)->save();
+ $this->assertEquals(3, $tracker->getTotalItemsCount());
+ $expected_1 = $expected;
+ $expected_1[] = Utility::createCombinedId($datasource_id, "2:$langcode");
+ sort($expected_1);
+ $actual = $tracker->getRemainingItems();
+ sort($actual);
+ $this->assertEquals($expected_1, $actual);
+
+ // Remove that translation again and verify that it's also deleted from the
+ // tracker.
+ $entities[1]->removeTranslation($langcode);
+ $entities[1]->save();
+ $this->assertEquals(2, $tracker->getTotalItemsCount());
+ $actual = $tracker->getRemainingItems();
+ sort($actual);
+ $this->assertEquals($expected, $actual);
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Index/IndexChangesTest.php b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Index/IndexChangesTest.php
new file mode 100644
index 000000000..9a4733878
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Index/IndexChangesTest.php
@@ -0,0 +1,450 @@
+installSchema('search_api', [
+ 'search_api_item',
+ ]);
+ $this->installEntitySchema('entity_test_mulrev_changed');
+ $this->installEntitySchema('search_api_task');
+ $this->installEntitySchema('user');
+ $this->installConfig('search_api');
+
+ $this->taskManager = $this->container->get('search_api.task_manager');
+
+ User::create([
+ 'uid' => 1,
+ 'name' => 'root',
+ 'langcode' => 'en',
+ ])->save();
+
+ EntityTestMulRevChanged::create([
+ 'id' => 1,
+ 'name' => 'test 1',
+ ])->save();
+
+ // Create a test server.
+ $this->server = Server::create([
+ 'name' => 'Test Server',
+ 'id' => 'test_server',
+ 'status' => 1,
+ 'backend' => 'search_api_test',
+ ]);
+ $this->server->save();
+
+ // Create a test index (but don't save it yet).
+ $this->index = Index::create([
+ 'name' => 'Test Index',
+ 'id' => 'test_index',
+ 'status' => 1,
+ 'tracker_settings' => [
+ 'default' => [],
+ ],
+ 'datasource_settings' => [
+ 'entity:user' => [],
+ 'entity:entity_test_mulrev_changed' => [],
+ ],
+ 'server' => $this->server->id(),
+ 'options' => ['index_directly' => FALSE],
+ ]);
+
+ $this->taskManager->deleteTasks();
+ }
+
+ /**
+ * Tests correct reactions when a new datasource is added.
+ */
+ public function testDatasourceAdded() {
+ $this->index->set('datasource_settings', [
+ 'entity:user' => [],
+ ]);
+ $this->index->save();
+
+ $tracker = $this->index->getTrackerInstance();
+
+ $expected = [
+ Utility::createCombinedId('entity:user', '1:en'),
+ ];
+ $this->assertEquals($expected, $tracker->getRemainingItems());
+
+ /** @var \Drupal\search_api\Datasource\DatasourceInterface $datasource */
+ $datasource = \Drupal::getContainer()
+ ->get('search_api.plugin_helper')
+ ->createDatasourcePlugin($this->index, 'entity:entity_test_mulrev_changed');
+ $this->index->addDatasource($datasource)->save();
+
+ $this->taskManager->executeAllTasks();
+
+ $expected = [
+ Utility::createCombinedId('entity:entity_test_mulrev_changed', '1:en'),
+ Utility::createCombinedId('entity:user', '1:en'),
+ ];
+ $remaining_items = $tracker->getRemainingItems();
+ sort($remaining_items);
+ $this->assertEquals($expected, $remaining_items);
+
+ User::create([
+ 'uid' => 2,
+ 'name' => 'someone',
+ 'langcode' => 'en',
+ ])->save();
+ EntityTestMulRevChanged::create([
+ 'id' => 2,
+ 'name' => 'test 2',
+ ])->save();
+
+ $expected = [
+ Utility::createCombinedId('entity:entity_test_mulrev_changed', '1:en'),
+ Utility::createCombinedId('entity:entity_test_mulrev_changed', '2:en'),
+ Utility::createCombinedId('entity:user', '1:en'),
+ Utility::createCombinedId('entity:user', '2:en'),
+ ];
+ $remaining_items = $tracker->getRemainingItems();
+ sort($remaining_items);
+ $this->assertEquals($expected, $remaining_items);
+
+ $this->getCalledMethods('backend');
+ $indexed = $this->index->indexItems();
+ $this->assertEquals(4, $indexed);
+ $this->assertEquals(['indexItems'], $this->getCalledMethods('backend'));
+
+ $indexed_items = array_keys($this->getIndexedItems());
+ sort($indexed_items);
+ $this->assertEquals($expected, $indexed_items);
+ $this->assertEquals(0, $tracker->getRemainingItemsCount());
+ }
+
+ /**
+ * Tests correct reactions when a datasource is removed.
+ */
+ public function testDatasourceRemoved() {
+ $info = [
+ 'datasource_id' => 'entity:entity_test_mulrev_changed',
+ 'property_path' => 'id',
+ ];
+ $field = \Drupal::getContainer()
+ ->get('search_api.fields_helper')
+ ->createField($this->index, 'id', $info);
+ $this->index->addField($field);
+
+ $processor = \Drupal::getContainer()
+ ->get('search_api.plugin_helper')
+ ->createProcessorPlugin($this->index, 'search_api_test');
+ $this->index->addProcessor($processor);
+ $this->setMethodOverride('processor', 'supportsIndex', function (IndexInterface $index) {
+ return in_array('entity:entity_test_mulrev_changed', $index->getDatasourceIds());
+ });
+
+ $this->index->save();
+
+ $this->assertArrayHasKey('search_api_test', $this->index->getProcessors());
+
+ $tracker = $this->index->getTrackerInstance();
+
+ $expected = [
+ Utility::createCombinedId('entity:entity_test_mulrev_changed', '1:en'),
+ Utility::createCombinedId('entity:user', '1:en'),
+ ];
+ $remaining_items = $tracker->getRemainingItems();
+ sort($remaining_items);
+ $this->assertEquals($expected, $remaining_items);
+
+ $this->getCalledMethods('backend');
+ $indexed = $this->index->indexItems();
+ $this->assertEquals(2, $indexed);
+ $this->assertEquals(['indexItems'], $this->getCalledMethods('backend'));
+
+ $indexed_items = array_keys($this->getIndexedItems());
+ sort($indexed_items);
+ $this->assertEquals($expected, $indexed_items);
+ $this->assertEquals(0, $tracker->getRemainingItemsCount());
+
+ $this->index->removeDatasource('entity:entity_test_mulrev_changed')->save();
+
+ $this->assertArrayNotHasKey('id', $this->index->getFields());
+ $this->assertArrayNotHasKey('search_api_test', $this->index->getProcessors());
+
+ $this->assertEquals(1, $tracker->getTotalItemsCount());
+
+ $expected = [
+ Utility::createCombinedId('entity:user', '1:en'),
+ ];
+ $indexed_items = array_keys($this->getIndexedItems());
+ sort($indexed_items);
+ $this->assertEquals($expected, $indexed_items);
+ $this->assertEquals(['updateIndex', 'deleteAllIndexItems'], $this->getCalledMethods('backend'));
+
+ User::create([
+ 'uid' => 2,
+ 'name' => 'someone',
+ 'langcode' => 'en',
+ ])->save();
+ EntityTestMulRevChanged::create([
+ 'id' => 2,
+ 'name' => 'test 2',
+ ])->save();
+
+ $this->assertEquals(2, $tracker->getTotalItemsCount());
+
+ $indexed = $this->index->indexItems();
+ $this->assertGreaterThanOrEqual(1, $indexed);
+ $this->assertEquals(['indexItems'], $this->getCalledMethods('backend'));
+
+ $expected = [
+ Utility::createCombinedId('entity:user', '1:en'),
+ Utility::createCombinedId('entity:user', '2:en'),
+ ];
+ $indexed_items = array_keys($this->getIndexedItems());
+ sort($indexed_items);
+ $this->assertEquals($expected, $indexed_items);
+ $this->assertEquals(0, $tracker->getRemainingItemsCount());
+ }
+
+ /**
+ * Tests correct reaction when the index's tracker changes.
+ */
+ public function testTrackerChange() {
+ $this->index->save();
+
+ /** @var \Drupal\search_api\Tracker\TrackerInterface $tracker */
+ $tracker = \Drupal::getContainer()
+ ->get('search_api.plugin_helper')
+ ->createTrackerPlugin($this->index, 'search_api_test');
+ $this->index->setTracker($tracker)->save();
+
+ $this->taskManager->executeAllTasks();
+
+ $methods = $this->getCalledMethods('tracker');
+ $expected = [
+ 'trackItemsInserted',
+ 'trackItemsInserted',
+ ];
+ $this->assertEquals($expected, $methods);
+
+ /** @var \Drupal\search_api\Tracker\TrackerInterface $tracker */
+ $tracker = \Drupal::getContainer()
+ ->get('search_api.plugin_helper')
+ ->createTrackerPlugin($this->index, 'default');
+ $this->index->setTracker($tracker)->save();
+
+ $this->taskManager->executeAllTasks();
+
+ $methods = $this->getCalledMethods('tracker');
+ $this->assertEquals(['trackAllItemsDeleted'], $methods);
+ $arguments = $this->getMethodArguments('tracker', 'trackAllItemsDeleted');
+ $this->assertEquals([], $arguments);
+ }
+
+ /**
+ * Tests correct reaction when a processor adding a property is removed.
+ */
+ public function testPropertyProcessorRemoved() {
+ $processor = $this->container
+ ->get('plugin.manager.search_api.processor')
+ ->createInstance('add_url', [
+ '#index' => $this->index,
+ ]);
+ $this->index->addProcessor($processor);
+
+ $fields_helper = \Drupal::getContainer()->get('search_api.fields_helper');
+ $info = [
+ 'datasource_id' => 'entity:entity_test_mulrev_changed',
+ 'property_path' => 'id',
+ ];
+ $this->index->addField($fields_helper->createField($this->index, 'id', $info));
+ $info = [
+ 'property_path' => 'search_api_url',
+ ];
+ $this->index->addField($fields_helper->createField($this->index, 'url', $info));
+
+ $this->index->save();
+
+ $fields = array_keys($this->index->getFields());
+ sort($fields);
+ $this->assertEquals(['id', 'url'], $fields);
+
+ $this->index->removeProcessor('add_url')->save();
+
+ $fields = array_keys($this->index->getFields());
+ $this->assertEquals(['id'], $fields);
+ }
+
+ /**
+ * Tests correct reaction when a bundle containing a property is removed.
+ */
+ public function testPropertyBundleRemoved() {
+ entity_test_create_bundle('bundle1', NULL, 'entity_test_mulrev_changed');
+ entity_test_create_bundle('bundle2', NULL, 'entity_test_mulrev_changed');
+
+ $this->enableModules(['field', 'text']);
+ $this->installEntitySchema('field_storage_config');
+ $this->installEntitySchema('field_config');
+ $this->installConfig('field');
+
+ FieldStorageConfig::create([
+ 'field_name' => 'field1',
+ 'entity_type' => 'entity_test_mulrev_changed',
+ 'type' => 'text',
+ ])->save();
+ FieldConfig::create([
+ 'field_name' => 'field1',
+ 'entity_type' => 'entity_test_mulrev_changed',
+ 'bundle' => 'bundle1',
+ ])->save();
+ FieldStorageConfig::create([
+ 'field_name' => 'field2',
+ 'entity_type' => 'entity_test_mulrev_changed',
+ 'type' => 'text',
+ ])->save();
+ FieldConfig::create([
+ 'field_name' => 'field2',
+ 'entity_type' => 'entity_test_mulrev_changed',
+ 'bundle' => 'bundle2',
+ ])->save();
+
+ $datasource_id = 'entity:entity_test_mulrev_changed';
+ $datasource = $this->container
+ ->get('plugin.manager.search_api.datasource')
+ ->createInstance($datasource_id, [
+ '#index' => $this->index,
+ 'bundles' => [
+ 'default' => TRUE,
+ 'selected' => [],
+ ],
+ ]);
+ $this->index->setDatasources([$datasource_id => $datasource]);
+
+ $fields_helper = \Drupal::getContainer()->get('search_api.fields_helper');
+ $info = [
+ 'datasource_id' => $datasource_id,
+ 'property_path' => 'field1',
+ ];
+ $this->index->addField($fields_helper->createField($this->index, 'field1', $info));
+ $info = [
+ 'datasource_id' => $datasource_id,
+ 'property_path' => 'field2',
+ ];
+ $this->index->addField($fields_helper->createField($this->index, 'field2', $info));
+
+ $this->index->save();
+
+ $fields = array_keys($this->index->getFields());
+ sort($fields);
+ $this->assertEquals(['field1', 'field2'], $fields);
+
+ $this->index->getDatasource($datasource_id)->setConfiguration([
+ 'bundles' => [
+ 'default' => TRUE,
+ 'selected' => ['bundle2'],
+ ],
+ ]);
+ $this->index->save();
+
+ $fields = array_keys($this->index->getFields());
+ $this->assertEquals(['field1'], $fields);
+ }
+
+ /**
+ * Tests correct behavior when a field ID is changed.
+ */
+ public function testFieldRenamed() {
+ $datasource_id = 'entity:entity_test_mulrev_changed';
+ $info = [
+ 'datasource_id' => $datasource_id,
+ 'property_path' => 'name',
+ ];
+ $field = \Drupal::getContainer()
+ ->get('search_api.fields_helper')
+ ->createField($this->index, 'name', $info);
+ $this->index->addField($field);
+ $this->assertEquals([], $this->index->getFieldRenames());
+
+ $this->index->renameField('name', 'name1');
+ $this->assertEquals(['name1' => $field], $this->index->getFields());
+ $this->assertEquals(['name' => 'name1'], $this->index->getFieldRenames());
+
+ // Saving resets the field IDs.
+ $this->index->save();
+ $this->assertEquals([], $this->index->getFieldRenames());
+ $this->assertEquals('name1', $this->index->getField('name1')->getOriginalFieldIdentifier());
+ }
+
+ /**
+ * Retrieves the indexed items from the test backend.
+ *
+ * @return array
+ * The indexed items, keyed by their item IDs and containing associative
+ * arrays with their field values.
+ */
+ protected function getIndexedItems() {
+ $key = 'search_api_test.backend.indexed.' . $this->index->id();
+ return \Drupal::state()->get($key, []);
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Index/IndexImportTest.php b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Index/IndexImportTest.php
new file mode 100644
index 000000000..9d4d4a2d5
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Index/IndexImportTest.php
@@ -0,0 +1,115 @@
+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_test_db']);
+
+ $this->storage = $this->container->get('entity_type.manager')->getStorage('search_api_index');
+ }
+
+ /**
+ * Tests processors are set after import.
+ */
+ public function testIndexImport() {
+ // Check initial conditions.
+ /** @var \Drupal\search_api\IndexInterface $index */
+ $index = $this->storage->load('database_search_index');
+ $this->assertFalse($index->isValidProcessor('stopwords'), 'Processor is not in index');
+
+ // Prepare the import by creating a copy of the active config in sync.
+ /** @var \Drupal\Core\Config\StorageInterface $sync */
+ $sync = $this->container->get('config.storage.sync');
+ /** @var \Drupal\Core\Config\StorageInterface $active */
+ $active = $this->container->get('config.storage');
+ $this->copyConfig($active, $sync);
+
+ // Make changes to the configuration in "sync" so there actually is
+ // something to import.
+ $expected_stopwords = ['a', 'an', 'and', 'are', 'as'];
+ $import_config = $sync->read('search_api.index.database_search_index');
+ $import_config['processor_settings']['stopwords'] = [
+ 'weights' => [
+ 'preprocess_query' => -10,
+ 'postprocess_query' => -10,
+ ],
+ 'fields' => [
+ 'name',
+ ],
+ 'stopwords' => $expected_stopwords,
+ ];
+ $sync->write('search_api.index.database_search_index', $import_config);
+
+ // Import the test configuration.
+ $config_importer = $this->configImporter();
+ $this->assertTrue($config_importer->hasUnprocessedConfigurationChanges(), 'Import prepared');
+ $config_importer->import();
+
+ // Ensure the static cache is clear and check that our change was correctly
+ // imported.
+ $this->storage->resetCache(['database_search_index']);
+ /** @var \Drupal\search_api\IndexInterface $imported_index */
+ $imported_index = $this->storage->load('database_search_index');
+ $this->assertTrue($imported_index->isValidProcessor('stopwords'), 'Processor is in index after import');
+
+ // Check that the processor does not have the default configuration.
+ $processor_config = $imported_index->getProcessor('stopwords')->getConfiguration();
+ $this->assertArrayHasKey('stopwords', $processor_config, 'Stopwords are configured');
+ $actual_stopwords = $processor_config['stopwords'];
+ $this->assertEquals($expected_stopwords, $actual_stopwords, 'Processor config was correctly set during import');
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Index/IndexLoadItemsTest.php b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Index/IndexLoadItemsTest.php
new file mode 100644
index 000000000..f0b4ad8ed
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Index/IndexLoadItemsTest.php
@@ -0,0 +1,97 @@
+installEntitySchema('search_api_task');
+
+ $server = Server::create([
+ 'id' => 'test',
+ 'backend' => 'search_api_test',
+ ]);
+ $this->index = Index::create([
+ 'tracker_settings' => [
+ 'search_api_test' => [],
+ ],
+ 'datasource_settings' => [
+ 'search_api_test' => [],
+ ],
+ ]);
+ $this->index->setServer($server);
+ }
+
+ /**
+ * Verifies that missing items are correctly detected and removed.
+ */
+ public function testMissingItems() {
+ $item_ids = [
+ 'search_api_test/1',
+ 'search_api_test/2',
+ ];
+ $items = $this->index->loadItemsMultiple($item_ids);
+ $this->assertEquals([], $items, 'No items loaded from test datasource.');
+ $methods = $this->getCalledMethods('tracker');
+ $this->assertContains('trackItemsDeleted', $methods, 'Unknown items deleted from tracker.');
+ $args = $this->getMethodArguments('tracker', 'trackItemsDeleted');
+ $this->assertEquals([$item_ids], $args, 'Correct items deleted from tracker.');
+ $methods = $this->getCalledMethods('backend');
+ $this->assertContains('deleteItems', $methods, 'Unknown items deleted from server.');
+
+ // If an error occurs while retrieving the datasource (which will happen for
+ // "unknown/1"), the items should not be deleted from tracking and the
+ // server.
+ $expected_deletions = $item_ids;
+ $item_ids = [
+ 'search_api_test/1',
+ 'search_api_test/2',
+ 'search_api_test/3',
+ 'unknown/1',
+ ];
+ $this->setReturnValue('datasource', 'loadMultiple', ['3' => '']);
+ $items = $this->index->loadItemsMultiple($item_ids);
+ $expected_items = ['search_api_test/3' => ''];
+ $this->assertEquals($expected_items, $items, 'Expected items loaded from test datasource.');
+ $methods = $this->getCalledMethods('tracker');
+ $this->assertContains('trackItemsDeleted', $methods, 'Unknown items deleted from tracker.');
+ $args = $this->getMethodArguments('tracker', 'trackItemsDeleted');
+ $this->assertEquals([$expected_deletions], $args, 'Correct items deleted from tracker.');
+ $methods = $this->getCalledMethods('backend');
+ $this->assertContains('deleteItems', $methods, 'Unknown items deleted from server.');
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Index/IndexStorageTest.php b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Index/IndexStorageTest.php
new file mode 100644
index 000000000..92a78481d
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Index/IndexStorageTest.php
@@ -0,0 +1,98 @@
+installSchema('system', 'key_value_expire');
+ $this->installEntitySchema('search_api_task');
+ $this->installConfig('search_api');
+
+ $this->storage = $this->container
+ ->get('entity_type.manager')
+ ->getStorage('search_api_index');
+ }
+
+ /**
+ * Tests all CRUD operations as a queue of operations.
+ */
+ public function testIndexCrud() {
+ $this->assertTrue($this->storage instanceof ConfigEntityStorage, 'The Search API Index storage controller is loaded.');
+
+ $index = $this->indexCreate();
+ $this->indexLoad($index);
+ $this->indexDelete($index);
+ }
+
+ /**
+ * Tests whether creating an index works correctly.
+ *
+ * @return \Drupal\search_api\IndexInterface
+ * The newly created search index.
+ */
+ protected function indexCreate() {
+ $index_data = [
+ 'id' => 'test',
+ 'name' => 'Index test name',
+ ];
+
+ $index = $this->storage->create($index_data);
+ $this->assertTrue($index instanceof IndexInterface, 'The newly created entity is a search index.');
+ $index->save();
+
+ return $index;
+ }
+
+ /**
+ * Tests whether loading an index works correctly.
+ *
+ * @param \Drupal\search_api\IndexInterface $index
+ * The index used for the test.
+ */
+ protected function indexLoad(IndexInterface $index) {
+ $loaded_index = $this->storage->load($index->id());
+ $this->assertSame($index->label(), $loaded_index->label());
+ }
+
+ /**
+ * Tests whether deleting an index works correctly.
+ *
+ * @param \Drupal\search_api\IndexInterface $index
+ * The index used for the test.
+ */
+ protected function indexDelete(IndexInterface $index) {
+ $this->storage->delete([$index]);
+ $loaded_index = $this->storage->load($index->id());
+ $this->assertNull($loaded_index);
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Kernel/PostRequestIndexingTrait.php b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/PostRequestIndexingTrait.php
new file mode 100644
index 000000000..dddcfc010
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/PostRequestIndexingTrait.php
@@ -0,0 +1,18 @@
+get('search_api.post_request_indexing')
+ ->onKernelTerminate();
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Processor/AddHierarchyTest.php b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Processor/AddHierarchyTest.php
new file mode 100644
index 000000000..b19948f70
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Processor/AddHierarchyTest.php
@@ -0,0 +1,310 @@
+ [
+ 'apple',
+ 'pear',
+ ],
+ 'vegetable' => [
+ 'radish',
+ 'turnip',
+ ],
+ ];
+
+ /**
+ * The nodes created for testing.
+ *
+ * @var \Drupal\node\NodeInterface[]
+ */
+ protected $nodes = [];
+
+ /**
+ * Hierarchical taxonomy terms.
+ *
+ * This is keyed by "type.item", for example: "fruit.pear".
+ *
+ * @var \Drupal\taxonomy\TermInterface[]
+ */
+ protected $terms = [];
+
+ /**
+ * Vocabulary to test with when using taxonomy for the hierarchy.
+ *
+ * @var \Drupal\taxonomy\Entity\Vocabulary
+ */
+ protected $vocabulary;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setUp($processor = NULL) {
+ parent::setUp();
+
+ $this->installConfig(['filter']);
+ $this->installEntitySchema('taxonomy_term');
+ $this->createTaxonomyHierarchy();
+
+ // Create a node type for testing.
+ $type = NodeType::create([
+ 'type' => 'page',
+ 'name' => 'page',
+ ]);
+ $type->save();
+
+ // Add the taxonomy field to page type.
+ $this->createEntityReferenceField(
+ 'node',
+ 'page',
+ 'term_field',
+ NULL,
+ 'taxonomy_term',
+ 'default',
+ [],
+ FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED
+ );
+
+ // Add a generic entity reference field.
+ $this->createEntityReferenceField(
+ 'node',
+ 'page',
+ 'parent_reference',
+ NULL,
+ 'node',
+ 'default',
+ []
+ );
+
+ // Index the taxonomy field.
+ $term_field = new Field($this->index, 'term_field');
+ $term_field->setType('integer');
+ $term_field->setPropertyPath('term_field');
+ $term_field->setDatasourceId('entity:node');
+ $term_field->setLabel('Terms');
+ $this->index->addField($term_field);
+
+ // Index the entity reference field.
+ $reference_field = new Field($this->index, 'parent_reference');
+ $reference_field->setType('integer');
+ $reference_field->setPropertyPath('parent_reference');
+ $reference_field->setDatasourceId('entity:node');
+ $reference_field->setLabel('Parent page');
+ $this->index->addField($reference_field);
+
+ // Add the "Index hierarchy" processor only now (not in parent method) since
+ // it can only be enabled once there are actually hierarchical fields.
+ $this->processor = \Drupal::getContainer()
+ ->get('search_api.plugin_helper')
+ ->createProcessorPlugin($this->index, 'hierarchy');
+ $this->index->addProcessor($this->processor);
+
+ // Add the node datasource to the index.
+ $datasources = \Drupal::getContainer()
+ ->get('search_api.plugin_helper')
+ ->createDatasourcePlugins($this->index, ['entity:node']);
+ $this->index->setDatasources($datasources);
+ $this->index->save();
+ $this->container
+ ->get('search_api.index_task_manager')
+ ->addItemsAll($this->index);
+ $index_storage = $this->container
+ ->get('entity_type.manager')
+ ->getStorage('search_api_index');
+ $index_storage->resetCache([$this->index->id()]);
+ $this->index = $index_storage->load($this->index->id());
+ }
+
+ /**
+ * Helper function to create the hierarchy with taxonomy terms.
+ */
+ protected function createTaxonomyHierarchy() {
+ $this->vocabulary = $this->createVocabulary();
+
+ foreach (static::$hierarchy as $type => $items) {
+ // Add the 'type' item, and nest items underneath.
+ $this->terms[$type] = $type_term = $this->createTerm($this->vocabulary, [
+ 'name' => $type,
+ ]);
+ foreach ($items as $item) {
+ $this->terms["$type.$item"] = $this->createTerm($this->vocabulary, [
+ 'name' => $item,
+ 'parent' => $type_term,
+ ]);
+ }
+ }
+ }
+
+ /**
+ * Tests taxonomy-based hierarchy indexing.
+ *
+ * @covers ::preprocessIndexItems
+ */
+ public function testPreprocessIndexItemsTaxonomy() {
+ // Add hierarchical terms to 3 nodes.
+ foreach (['vegetable.turnip', 'vegetable', 'fruit.pear'] as $i => $term) {
+ $this->nodes[$i] = $this->createNode([
+ 'type' => 'page',
+ 'term_field' => [
+ 'target_id' => $this->terms[$term]->id(),
+ ],
+ ]);
+ }
+ $this->index->reindex();
+ $this->indexItems();
+
+ // By default, hierarchy is not indexed, so a search for 'vegetable' should
+ // only return node 2.
+ $query = new Query($this->index);
+ $query->addCondition('term_field', $this->terms['vegetable']->id());
+ $result = $query->execute();
+ $expected = ['node' => [1]];
+ $this->assertResults($result, $expected);
+
+ // Enable hierarchical indexing.
+ $processor = $this->index->getProcessor('hierarchy');
+ $processor->setConfiguration([
+ 'fields' => [
+ 'term_field' => 'taxonomy_term-parent',
+ ],
+ ]);
+ $this->index->save();
+ $this->indexItems();
+
+ // Query for "vegetable" should return 2 items:
+ // Node 1 is "vegetable.turnip" and node 2 is just "vegetable".
+ $query = new Query($this->index);
+ $query->addCondition('term_field', $this->terms['vegetable']->id());
+ $result = $query->execute();
+ $expected = ['node' => [0, 1]];
+ $this->assertResults($result, $expected);
+
+ // A search for just turnips should return node 1 only.
+ $query = new Query($this->index);
+ $query->addCondition('term_field', $this->terms['vegetable.turnip']->id());
+ $result = $query->execute();
+ $expected = ['node' => [0]];
+ $this->assertResults($result, $expected);
+
+ // Also add a term with multiple parents.
+ $this->terms['avocado'] = $this->createTerm($this->vocabulary, [
+ 'name' => 'Avocado',
+ 'parent' => [$this->terms['fruit']->id(), $this->terms['vegetable']->id()],
+ ]);
+ $this->nodes[3] = $this->createNode([
+ 'type' => 'page',
+ 'term_field' => [
+ 'target_id' => $this->terms['avocado']->id(),
+ ],
+ ]);
+ $this->index->reindex();
+ $this->indexItems();
+
+ // Searching for 'fruit' or 'vegetable' should return this new node.
+ $query = new Query($this->index);
+ $query->addCondition('term_field', $this->terms['fruit']->id());
+ $result = $query->execute();
+ $expected = ['node' => [2, 3]];
+ $this->assertResults($result, $expected);
+
+ $query = new Query($this->index);
+ $query->addCondition('term_field', $this->terms['vegetable']->id());
+ $result = $query->execute();
+ $expected = ['node' => [0, 1, 3]];
+ $this->assertResults($result, $expected);
+ }
+
+ /**
+ * Tests non-taxonomy-based hierarchy.
+ *
+ * @covers ::preprocessIndexItems
+ * @covers ::addHierarchyValues
+ */
+ public function testPreprocessIndexItems() {
+ // Setup the nodes to follow the hierarchy.
+ foreach (static::$hierarchy as $type => $items) {
+ $this->nodes[] = $type_node = $this->createNode([
+ 'title' => $type,
+ ]);
+ foreach ($items as $item) {
+ $this->nodes[] = $this->createNode([
+ 'title' => $item,
+ 'parent_reference' => ['target_id' => $type_node->id()],
+ ]);
+ }
+ }
+ // Add a third tier of hierarchy for specific types of radishes.
+ foreach (['Cherry Belle', 'Snow Belle', 'Daikon'] as $item) {
+ $this->nodes[] = $this->createNode([
+ 'title' => $item,
+ 'parent_reference' => ['target_id' => $this->nodes[5]->id()],
+ ]);
+ }
+ $this->index->reindex();
+ $this->indexItems();
+
+ // Initially hierarchy is excluded, so "vegetable" should only return nodes
+ // 5 and 6.
+ $query = new Query($this->index);
+ $query->addCondition('parent_reference', $this->nodes[3]->id());
+ $result = $query->execute();
+ $expected = ['node' => [4, 5]];
+ $this->assertResults($result, $expected);
+
+ // Enable hierarchical indexing.
+ $processor = $this->index->getProcessor('hierarchy');
+ $processor->setConfiguration([
+ 'fields' => [
+ 'parent_reference' => 'node-parent_reference',
+ ],
+ ]);
+ $this->index->save();
+ $this->indexItems();
+
+ // A search for "vegetable" should now include the hierarchy.
+ $query = new Query($this->index);
+ $query->addCondition('parent_reference', $this->nodes[3]->id());
+ $result = $query->execute();
+ $expected = ['node' => [4, 5, 6, 7, 8]];
+ $this->assertResults($result, $expected);
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Processor/ContentAccessTest.php b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Processor/ContentAccessTest.php
new file mode 100644
index 000000000..252fc3c73
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Processor/ContentAccessTest.php
@@ -0,0 +1,376 @@
+ 'page', 'name' => 'page']);
+ $type->save();
+
+ // Create anonymous user role.
+ $role = Role::create([
+ 'id' => 'anonymous',
+ 'label' => 'anonymous',
+ ]);
+ $role->save();
+
+ // Insert the anonymous user into the database, as the user table is inner
+ // joined by \Drupal\comment\CommentStorage.
+ User::create([
+ 'uid' => 0,
+ 'name' => '',
+ ])->save();
+
+ // Create a node with attached comment.
+ $values = [
+ 'status' => NodeInterface::PUBLISHED,
+ 'type' => 'page',
+ 'title' => 'test title',
+ ];
+ $this->nodes[0] = Node::create($values);
+ $this->nodes[0]->save();
+
+ $comment_type = CommentType::create([
+ 'id' => 'comment',
+ 'target_entity_type_id' => 'node',
+ ]);
+ $comment_type->save();
+
+ $this->installConfig(['comment']);
+ $this->addDefaultCommentField('node', 'page');
+
+ $comment = Comment::create([
+ 'status' => CommentInterface::PUBLISHED,
+ 'entity_type' => 'node',
+ 'entity_id' => $this->nodes[0]->id(),
+ 'field_name' => 'comment',
+ 'body' => 'test body',
+ 'comment_type' => $comment_type->id(),
+ ]);
+ $comment->save();
+
+ $this->comments[] = $comment;
+
+ $values = [
+ 'status' => NodeInterface::PUBLISHED,
+ 'type' => 'page',
+ 'title' => 'some title',
+ ];
+ $this->nodes[1] = Node::create($values);
+ $this->nodes[1]->save();
+
+ $values = [
+ 'status' => NodeInterface::NOT_PUBLISHED,
+ 'type' => 'page',
+ 'title' => 'other title',
+ ];
+ $this->nodes[2] = Node::create($values);
+ $this->nodes[2]->save();
+
+ // Also index users, to verify that they are unaffected by the processor.
+ $datasources = \Drupal::getContainer()
+ ->get('search_api.plugin_helper')
+ ->createDatasourcePlugins($this->index, [
+ 'entity:comment',
+ 'entity:node',
+ 'entity:user',
+ ]);
+ $this->index->setDatasources($datasources);
+ $this->index->save();
+
+ \Drupal::getContainer()->get('search_api.index_task_manager')->addItemsAll($this->index);
+ $index_storage = \Drupal::entityTypeManager()->getStorage('search_api_index');
+ $index_storage->resetCache([$this->index->id()]);
+ $this->index = $index_storage->load($this->index->id());
+ }
+
+ /**
+ * Tests searching when content is accessible to all.
+ */
+ public function testQueryAccessAll() {
+ $permissions = ['access content', 'access comments'];
+ user_role_grant_permissions('anonymous', $permissions);
+ $this->index->reindex();
+ $this->indexItems();
+ $this->assertEquals(5, $this->index->getTrackerInstance()->getIndexedItemsCount(), '5 items indexed, as expected.');
+
+ $query = \Drupal::getContainer()
+ ->get('search_api.query_helper')
+ ->createQuery($this->index);
+ $result = $query->execute();
+
+ $expected = [
+ 'user' => [0],
+ 'comment' => [0],
+ 'node' => [0, 1],
+ ];
+ $this->assertResults($result, $expected);
+ }
+
+ /**
+ * Tests searching when only comments are accessible.
+ */
+ public function testQueryAccessComments() {
+ user_role_grant_permissions('anonymous', ['access comments']);
+ $this->index->reindex();
+ $this->indexItems();
+ $this->assertEquals(5, $this->index->getTrackerInstance()->getIndexedItemsCount(), '5 items indexed, as expected.');
+
+ $query = \Drupal::getContainer()
+ ->get('search_api.query_helper')
+ ->createQuery($this->index);
+ $result = $query->execute();
+
+ $this->assertResults($result, ['user' => [0], 'comment' => [0]]);
+ }
+
+ /**
+ * Tests searching for own unpublished content.
+ */
+ public function testQueryAccessOwn() {
+ // Create the user that will be passed into the query.
+ $permissions = [
+ 'access content',
+ 'access comments',
+ 'view own unpublished content',
+ ];
+ $authenticated_user = $this->createUser($permissions);
+ $uid = $authenticated_user->id();
+
+ $values = [
+ 'status' => NodeInterface::NOT_PUBLISHED,
+ 'type' => 'page',
+ 'title' => 'foo',
+ 'uid' => $uid,
+ ];
+ $this->nodes[3] = Node::create($values);
+ $this->nodes[3]->save();
+ $this->indexItems();
+ $this->assertEquals(7, $this->index->getTrackerInstance()->getIndexedItemsCount(), '7 items indexed, as expected.');
+
+ $query = \Drupal::getContainer()
+ ->get('search_api.query_helper')
+ ->createQuery($this->index);
+ $query->setOption('search_api_access_account', $authenticated_user);
+ $result = $query->execute();
+
+ $expected = ['user' => [0, $uid], 'node' => [3]];
+ $this->assertResults($result, $expected);
+ }
+
+ /**
+ * Tests building the query when content is accessible based on node grants.
+ */
+ public function testQueryAccessWithNodeGrants() {
+ // Create the user that will be passed into the query.
+ $permissions = [
+ 'access content',
+ ];
+ $authenticated_user = $this->createUser($permissions);
+
+ Database::getConnection()->insert('node_access')
+ ->fields([
+ 'nid' => $this->nodes[0]->id(),
+ 'langcode' => $this->nodes[0]->language()->getId(),
+ 'gid' => $authenticated_user->id(),
+ 'realm' => 'search_api_test',
+ 'grant_view' => 1,
+ ])
+ ->execute();
+
+ $this->index->reindex();
+ $this->indexItems();
+ $query = \Drupal::getContainer()
+ ->get('search_api.query_helper')
+ ->createQuery($this->index);
+ $query->setOption('search_api_access_account', $authenticated_user);
+ $result = $query->execute();
+
+ $expected = [
+ 'user' => [0, $authenticated_user->id()],
+ 'node' => [0],
+ ];
+ $this->assertResults($result, $expected);
+ }
+
+ /**
+ * Tests comment indexing when all users have access to content.
+ */
+ public function testContentAccessAll() {
+ user_role_grant_permissions('anonymous', ['access content', 'access comments']);
+ $items = [];
+ foreach ($this->comments as $comment) {
+ $items[] = [
+ 'datasource' => 'entity:comment',
+ 'item' => $comment->getTypedData(),
+ 'item_id' => $comment->id(),
+ 'text' => 'Comment: ' . $comment->id(),
+ ];
+ }
+ $items = $this->generateItems($items);
+
+ // Add the processor's field values to the items.
+ foreach ($items as $item) {
+ $this->processor->addFieldValues($item);
+ }
+
+ foreach ($items as $item) {
+ $this->assertEquals(['node_access__all'], $item->getField('node_grants')->getValues());
+ }
+ }
+
+ /**
+ * Tests comment indexing when hook_node_grants() takes effect.
+ */
+ public function testContentAccessWithNodeGrants() {
+ $items = [];
+ foreach ($this->comments as $comment) {
+ $items[] = [
+ 'datasource' => 'entity:comment',
+ 'item' => $comment->getTypedData(),
+ 'item_id' => $comment->id(),
+ 'field_text' => 'Text: &' . $comment->id(),
+ ];
+ }
+ $items = $this->generateItems($items);
+
+ // Add the processor's field values to the items.
+ foreach ($items as $item) {
+ $this->processor->addFieldValues($item);
+ }
+
+ foreach ($items as $item) {
+ $this->assertEquals(['node_access_search_api_test:0'], $item->getField('node_grants')->getValues());
+ }
+ }
+
+ /**
+ * Tests that acquiring node grants leads to re-indexing of that node.
+ */
+ public function testNodeGrantsChange() {
+ $this->index->setOption('index_directly', FALSE)->save();
+ $this->indexItems();
+ $remaining = $this->index->getTrackerInstance()->getRemainingItems();
+ $this->assertEquals([], $remaining, 'All items were indexed.');
+
+ /** @var \Drupal\node\NodeAccessControlHandlerInterface $access_control_handler */
+ $access_control_handler = \Drupal::entityTypeManager()
+ ->getAccessControlHandler('node');
+ $access_control_handler->acquireGrants($this->nodes[0]);
+
+ $expected = [
+ 'entity:comment/' . $this->comments[0]->id() . ':en',
+ 'entity:node/' . $this->nodes[0]->id() . ':en',
+ ];
+ $remaining = $this->index->getTrackerInstance()->getRemainingItems();
+ sort($remaining);
+ $this->assertEquals($expected, $remaining, 'The expected items were marked as "changed" when changing node access grants.');
+ }
+
+ /**
+ * Tests whether the "search_api_bypass_access" query option is respected.
+ */
+ public function testQueryAccessBypass() {
+ $this->index->reindex();
+ $this->indexItems();
+ $this->assertEquals(5, $this->index->getTrackerInstance()->getIndexedItemsCount(), '5 items indexed, as expected.');
+
+ $query = \Drupal::getContainer()
+ ->get('search_api.query_helper')
+ ->createQuery($this->index, ['search_api_bypass_access' => TRUE]);
+ $result = $query->execute();
+
+ $expected = [
+ 'user' => [0],
+ 'comment' => [0],
+ 'node' => [0, 1, 2],
+ ];
+ $this->assertResults($result, $expected);
+ }
+
+ /**
+ * Tests whether the property is correctly added by the processor.
+ */
+ public function testAlterPropertyDefinitions() {
+ // Check for added properties when no datasource is given.
+ $properties = $this->processor->getPropertyDefinitions(NULL);
+ $this->assertTrue(array_key_exists('search_api_node_grants', $properties), 'The Properties where modified with the "search_api_node_grants".');
+ $this->assertTrue(($properties['search_api_node_grants'] instanceof DataDefinitionInterface), 'The "search_api_node_grants" key contains a valid DataDefinition instance.');
+ $this->assertEquals('string', $properties['search_api_node_grants']->getDataType(), 'Correct DataType set in the DataDefinition.');
+
+ // Verify that there are no properties if a datasource is given.
+ $properties = $this->processor->getPropertyDefinitions($this->index->getDatasource('entity:node'));
+ $this->assertEquals([], $properties, '"search_api_node_grants" property not added when data source is given.');
+ }
+
+ /**
+ * Creates a new user account.
+ *
+ * @param string[] $permissions
+ * The permissions to set for the user.
+ *
+ * @return \Drupal\user\UserInterface
+ * The new user object.
+ */
+ protected function createUser(array $permissions) {
+ $role = Role::create(['id' => 'role', 'name' => 'Role test']);
+ $role->save();
+ user_role_grant_permissions($role->id(), $permissions);
+
+ $values = [
+ 'uid' => 2,
+ 'name' => 'Test',
+ 'roles' => [$role->id()],
+ ];
+ $authenticated_user = User::create($values);
+ $authenticated_user->enforceIsNew();
+ $authenticated_user->save();
+
+ return $authenticated_user;
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Processor/ProcessorTestBase.php b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Processor/ProcessorTestBase.php
new file mode 100644
index 000000000..af6a68a59
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Processor/ProcessorTestBase.php
@@ -0,0 +1,189 @@
+installSchema('node', ['node_access']);
+ $this->installSchema('search_api', ['search_api_item']);
+ $this->installEntitySchema('user');
+ $this->installEntitySchema('node');
+ $this->installEntitySchema('comment');
+ $this->installEntitySchema('search_api_task');
+ $this->installSchema('comment', ['comment_entity_statistics']);
+ $this->installConfig(['field']);
+ $this->installConfig('search_api');
+
+ Action::create([
+ 'id' => 'foo',
+ 'label' => 'Foobaz',
+ 'plugin' => 'comment_publish_action',
+ ])->save();
+
+ // 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->server = Server::create([
+ 'id' => 'server',
+ 'name' => 'Server & Name',
+ 'status' => TRUE,
+ 'backend' => 'search_api_db',
+ 'backend_config' => [
+ 'min_chars' => 3,
+ 'database' => 'default:default',
+ ],
+ ]);
+ $this->server->save();
+
+ $this->index = Index::create([
+ 'id' => 'index',
+ 'name' => 'Index name',
+ 'status' => TRUE,
+ 'datasource_settings' => [
+ 'entity:comment' => [],
+ 'entity:node' => [],
+ ],
+ 'server' => 'server',
+ 'tracker_settings' => [
+ 'default' => [],
+ ],
+ ]);
+ $this->index->setServer($this->server);
+
+ $field_subject = new Field($this->index, 'subject');
+ $field_subject->setType('text');
+ $field_subject->setPropertyPath('subject');
+ $field_subject->setDatasourceId('entity:comment');
+ $field_subject->setLabel('Subject');
+
+ $field_title = new Field($this->index, 'title');
+ $field_title->setType('text');
+ $field_title->setPropertyPath('title');
+ $field_title->setDatasourceId('entity:node');
+ $field_title->setLabel('Title');
+
+ $this->index->addField($field_subject);
+ $this->index->addField($field_title);
+
+ if ($processor) {
+ $this->processor = \Drupal::getContainer()
+ ->get('search_api.plugin_helper')
+ ->createProcessorPlugin($this->index, $processor);
+ $this->index->addProcessor($this->processor);
+ }
+ $this->index->save();
+ }
+
+ /**
+ * Generates some test items.
+ *
+ * @param array[] $items
+ * Array of items to be transformed into proper search item objects. Each
+ * item in this array is an associative array with the following keys:
+ * - datasource: The datasource plugin ID.
+ * - item: The item object to be indexed.
+ * - item_id: The datasource-specific raw item ID.
+ * - *: Any other keys will be treated as property paths, and their values
+ * as a single value for a field with that property path.
+ *
+ * @return \Drupal\search_api\Item\ItemInterface[]
+ * The generated test items.
+ */
+ public function generateItems(array $items) {
+ /** @var \Drupal\search_api\Item\ItemInterface[] $extracted_items */
+ $extracted_items = [];
+ foreach ($items as $item) {
+ $id = Utility::createCombinedId($item['datasource'], $item['item_id']);
+ $extracted_items[$id] = \Drupal::getContainer()
+ ->get('search_api.fields_helper')
+ ->createItemFromObject($this->index, $item['item'], $id);
+ foreach ([NULL, $item['datasource']] as $datasource_id) {
+ foreach ($this->index->getFieldsByDatasource($datasource_id) as $key => $field) {
+ /** @var \Drupal\search_api\Item\FieldInterface $field */
+ $field = clone $field;
+ if (isset($item[$field->getPropertyPath()])) {
+ $field->addValue($item[$field->getPropertyPath()]);
+ }
+ $extracted_items[$id]->setField($key, $field);
+ }
+ }
+ }
+
+ return $extracted_items;
+ }
+
+ /**
+ * Indexes all (unindexed) items.
+ *
+ * @return int
+ * The number of successfully indexed items.
+ */
+ protected function indexItems() {
+ return $this->index->indexItems();
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Processor/RenderedItemTest.php b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Processor/RenderedItemTest.php
new file mode 100644
index 000000000..e8bd64466
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Processor/RenderedItemTest.php
@@ -0,0 +1,406 @@
+installConfig(['system', 'filter', 'node', 'comment', 'user']);
+ $this->installSchema('system', ['router']);
+ \Drupal::service('router.builder')->rebuild();
+
+ // Create the default languages and a new one for translations.
+ $this->installConfig(['language']);
+ /** @var \Drupal\language\Entity\ConfigurableLanguage $language */
+ $language = ConfigurableLanguage::create([
+ 'id' => 'de',
+ 'label' => 'German',
+ 'weight' => 0,
+ ]);
+ $language->save();
+
+ // Creates node types for testing.
+ foreach (['article', 'page'] as $type_id) {
+ $type = NodeType::create([
+ 'type' => $type_id,
+ 'name' => $type_id,
+ ]);
+ $type->save();
+ node_add_body_field($type);
+ }
+ CommentType::create([
+ 'id' => 'comment',
+ 'label' => 'comment',
+ 'target_entity_type_id' => 'node',
+ ])->save();
+ FieldStorageConfig::create([
+ 'field_name' => 'comment',
+ 'type' => 'comment',
+ 'entity_type' => 'node',
+ ])->save();
+ FieldConfig::create([
+ 'field_name' => 'comment',
+ 'entity_type' => 'node',
+ 'bundle' => 'page',
+ 'label' => 'Comments',
+ ])->save();
+
+ // Insert the anonymous user into the database.
+ $anonymous_user = User::create([
+ 'uid' => 0,
+ 'name' => '',
+ ]);
+ $anonymous_user->save();
+
+ // Default node values for all nodes we create below.
+ $node_data = [
+ 'status' => NodeInterface::PUBLISHED,
+ 'type' => 'page',
+ 'title' => '',
+ 'body' => ['value' => '', 'summary' => '', 'format' => 'plain_text'],
+ 'uid' => $anonymous_user->id(),
+ ];
+
+ // Create some test nodes with valid user on it for rendering a picture.
+ $node_data['title'] = 'Title for node 1';
+ $node_data['body']['value'] = 'value for node 1';
+ $node_data['body']['summary'] = 'summary for node 1';
+ $this->nodes[1] = Node::create($node_data);
+ $this->nodes[1]->save();
+ $node_data['title'] = 'Title for node 2';
+ $node_data['body']['value'] = 'value for node 2';
+ $node_data['body']['summary'] = 'summary for node 2';
+ $this->nodes[2] = Node::create($node_data);
+ $this->nodes[2]->save();
+ $node_data['type'] = 'article';
+ $node_data['title'] = 'Title for node 3';
+ $node_data['body']['value'] = 'value for node 3';
+ $node_data['body']['summary'] = 'summary for node 3';
+ $this->nodes[3] = Node::create($node_data);
+ $this->nodes[3]->save();
+
+ // Add a field based on the "rendered_item" property.
+ $field_info = [
+ 'type' => 'text',
+ 'property_path' => 'rendered_item',
+ 'configuration' => [
+ 'roles' => ['anonymous'],
+ 'view_mode' => [
+ 'entity:node' => [
+ 'page' => 'full',
+ 'article' => 'teaser',
+ ],
+ 'entity:user' => [
+ 'user' => 'compact',
+ ],
+ 'entity:comment' => [
+ 'comment' => 'full',
+ ],
+ ],
+ ],
+ ];
+ $field = \Drupal::getContainer()
+ ->get('search_api.fields_helper')
+ ->createField($this->index, 'rendered_item', $field_info);
+ $this->index->addField($field);
+ $datasources = \Drupal::getContainer()
+ ->get('search_api.plugin_helper')
+ ->createDatasourcePlugins($this->index);
+ $this->index->setDatasources($datasources);
+ $this->index->save();
+
+ // Enable the classy and stable themes as the tests rely on markup from
+ // that. Set stable as the active theme, but make classy the default. The
+ // processor should switch to classy to perform the rendering.
+ \Drupal::service('theme_handler')->install(['classy']);
+ \Drupal::service('theme_handler')->install(['stable']);
+ \Drupal::configFactory()->getEditable('system.theme')->set('default', 'classy')->save();
+ \Drupal::theme()->setActiveTheme(\Drupal::service('theme.initialization')->initTheme('stable'));
+ }
+
+ /**
+ * Tests whether the rendered_item field is correctly filled by the processor.
+ */
+ public function testAddFieldValues() {
+ $this->nodes[4] = $this->nodes[3]->addTranslation('de');
+ $this->nodes[4]->set('title', 'Titel für Knoten 4');
+ $this->nodes[4]->set('body', [
+ 'value' => 'Körper für Knoten 4',
+ 'summary' => 'Zusammenfassung für Knoten 4',
+ ]);
+ $this->nodes[4]->save();
+
+ $this->assertEquals('en', $this->nodes[1]->language()->getId());
+ $this->assertEquals('en', $this->nodes[2]->language()->getId());
+ $this->assertEquals('en', $this->nodes[3]->language()->getId());
+ $this->assertEquals('de', $this->nodes[4]->language()->getId());
+
+ $items = [];
+ foreach ($this->nodes as $i => $node) {
+ $items[] = [
+ 'datasource' => 'entity:node',
+ 'item' => $node->getTypedData(),
+ 'item_id' => $i,
+ ];
+ }
+ $user = User::create([
+ 'uid' => 2,
+ 'name' => 'User #2',
+ ]);
+ $items[] = [
+ 'datasource' => 'entity:user',
+ 'item' => $user->getTypedData(),
+ 'item_id' => $user->id(),
+ ];
+ $comment = Comment::create([
+ 'entity_type' => 'node',
+ 'entity_id' => 1,
+ 'field_name' => 'comment',
+ 'cid' => 1,
+ 'comment_type' => 'comment',
+ 'subject' => 'Subject of comment 1',
+ ]);
+ $comment->save();
+ $items[] = [
+ 'datasource' => 'entity:comment',
+ 'item' => $comment->getTypedData(),
+ 'item_id' => $comment->id(),
+ ];
+ $items = $this->generateItems($items);
+
+ // Add the processor's field values to the items.
+ foreach ($items as $item) {
+ $this->processor->addFieldValues($item);
+ }
+
+ foreach ($items as $key => $item) {
+ list($datasource_id, $entity_id) = Utility::splitCombinedId($key);
+ $type = $this->index->getDatasource($datasource_id)->label();
+
+ $field = $item->getField('rendered_item');
+ $this->assertEquals('text', $field->getType(), "$type item $entity_id rendered value is identified as text.");
+ /** @var \Drupal\search_api\Plugin\search_api\data_type\value\TextValueInterface[] $values */
+ $values = $field->getValues();
+ // Test that the value is properly wrapped in a
+ // \Drupal\search_api\Plugin\search_api\data_type\value\TextValueInterface
+ // object, which contains a string (not, for example, some markup object).
+ $this->assertInstanceOf('Drupal\search_api\Plugin\search_api\data_type\value\TextValueInterface', $values[0], "$type item $entity_id rendered value is properly wrapped in a text value object.");
+ $field_value = $values[0]->getText();
+ $this->assertInternalType('string', $field_value, "$type item $entity_id rendered value is a string.");
+ $this->assertEquals(1, count($values), "$type item $entity_id rendered value is a single value.");
+
+ switch ($datasource_id) {
+ case 'entity:node':
+ $this->checkRenderedNode($this->nodes[$entity_id], $field_value);
+ break;
+
+ case 'entity:user':
+ $this->checkRenderedUser($user, $field_value);
+ break;
+
+ case 'entity:comment':
+ $this->checkRenderedComment($comment, $field_value);
+ break;
+
+ default:
+ $this->assertTrue(FALSE);
+ }
+
+ }
+ }
+
+ /**
+ * Verifies that a certain node has been rendered correctly.
+ *
+ * @param \Drupal\node\NodeInterface $node
+ * The node.
+ * @param string $field_value
+ * The rendered field value.
+ */
+ protected function checkRenderedNode(NodeInterface $node, $field_value) {
+ // These tests rely on the template not changing. However, if we'd only
+ // check whether the field values themselves are included, there could
+ // easier be false positives. For example, the title text was present even
+ // when the processor was broken, because the schema metadata was also
+ // adding it to the output.
+ $nid = $node->id();
+ $full_view = $node->bundle() === 'page';
+ $view_mode = $full_view ? 'full' : 'teaser';
+ $this->assertContains("view-mode-$view_mode", $field_value, 'Node item ' . $nid . " rendered in view-mode \"$view_mode\".");
+ $this->assertContains('field--name-title', $field_value, 'Node item ' . $nid . ' has a rendered title field.');
+ $this->assertContains('>' . $node->label() . '<', $field_value, 'Node item ' . $nid . ' has a rendered title inside HTML-Tags.');
+ $this->assertContains('>Member for<', $field_value, 'Node item ' . $nid . ' has rendered member information HTML-Tags.');
+ if ($full_view) {
+ $body_value = $node->get('body')->getValue()[0]['value'] . '<';
+ }
+ else {
+ $body_value = $node->get('body')->getValue()[0]['summary'] . '<';
+ }
+ $this->assertContains('>' . $body_value, $field_value, 'Node item ' . $nid . ' has rendered content inside HTML-Tags.');
+ }
+
+ /**
+ * Verifies that a certain user has been rendered correctly.
+ *
+ * @param \Drupal\user\UserInterface $user
+ * The user.
+ * @param string $field_value
+ * The rendered field value.
+ */
+ protected function checkRenderedUser(UserInterface $user, $field_value) {
+ $this->assertContains('>Member for<', $field_value);
+ }
+
+ /**
+ * Verifies that a certain comment has been rendered correctly.
+ *
+ * @param \Drupal\comment\CommentInterface $comment
+ * The comment.
+ * @param string $field_value
+ * The rendered field value.
+ */
+ protected function checkRenderedComment(CommentInterface $comment, $field_value) {
+ $this->assertContains('>' . $comment->label() . '<', $field_value);
+ }
+
+ /**
+ * Tests that hiding a rendered item works.
+ */
+ public function testHideRenderedItem() {
+ // Change the processor configuration to make sure that that the rendered
+ // item content will be empty.
+ $field = $this->index->getField('rendered_item');
+ $config = $field->getConfiguration();
+ $config['view_mode'] = [
+ 'entity:node' => [
+ 'page' => '',
+ 'article' => '',
+ ],
+ ];
+ $field->setConfiguration($config);
+
+ // Create items that we can index.
+ $items = [];
+ foreach ($this->nodes as $node) {
+ $items[] = [
+ 'datasource' => 'entity:node',
+ 'item' => $node->getTypedData(),
+ 'item_id' => $node->id(),
+ 'text' => 'text for ' . $node->id(),
+ ];
+ }
+ $items = $this->generateItems($items);
+
+ // Add the processor's field values to the items.
+ foreach ($items as $item) {
+ $this->processor->addFieldValues($item);
+ }
+
+ // Verify that no field values were added.
+ foreach ($items as $key => $item) {
+ $rendered_item = $item->getField('rendered_item');
+ $this->assertEmpty($rendered_item->getValues(), 'No rendered_item field value added when disabled for content type.');
+ }
+ }
+
+ /**
+ * Tests whether the property is correctly added by the processor.
+ */
+ public function testAlterPropertyDefinitions() {
+ // Check for added properties when no datasource is given.
+ $properties = $this->processor->getPropertyDefinitions(NULL);
+ $this->assertTrue(array_key_exists('rendered_item', $properties), 'The Properties where modified with the "rendered_item".');
+ $this->assertInstanceOf('Drupal\search_api\Plugin\search_api\processor\Property\RenderedItemProperty', $properties['rendered_item'], 'Added property has the correct class.');
+ $this->assertTrue(($properties['rendered_item'] instanceof DataDefinitionInterface), 'The "rendered_item" contains a valid DataDefinition instance.');
+ $this->assertEquals('search_api_html', $properties['rendered_item']->getDataType(), 'Correct DataType set in the DataDefinition.');
+
+ // Verify that there are no properties if a datasource is given.
+ $properties = $this->processor->getPropertyDefinitions($this->index->getDatasource('entity:node'));
+ $this->assertEquals([], $properties, '"render_item" property not added when data source is given.');
+ }
+
+ /**
+ * Tests whether the processor reacts correctly to removed dependencies.
+ */
+ public function testDependencyRemoval() {
+ $expected = [
+ 'config' => [
+ 'core.entity_view_mode.comment.full',
+ 'core.entity_view_mode.node.full',
+ 'core.entity_view_mode.node.teaser',
+ 'core.entity_view_mode.user.compact',
+ ],
+ ];
+ $this->assertEquals($expected, $this->processor->calculateDependencies());
+
+ EntityViewMode::load('node.teaser')->delete();
+ $expected = [
+ 'entity:node' => [
+ 'page' => 'full',
+ ],
+ 'entity:user' => [
+ 'user' => 'compact',
+ ],
+ 'entity:comment' => [
+ 'comment' => 'full',
+ ],
+ ];
+ // We need to reload the index.
+ $index = Index::load($this->index->id());
+ $field_config = $index->getField('rendered_item')->getConfiguration();
+ $this->assertEquals($expected, $field_config['view_mode']);
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Processor/TypeBoostTest.php b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Processor/TypeBoostTest.php
new file mode 100644
index 000000000..6f042a6a6
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Processor/TypeBoostTest.php
@@ -0,0 +1,117 @@
+ 'article',
+ 'name' => 'Article',
+ ]);
+ $article_node_type->save();
+ }
+
+ // Create a page node type, if not already present.
+ if (!NodeType::load('page')) {
+ $page_node_type = NodeType::create([
+ 'type' => 'page',
+ 'name' => 'Page',
+ ]);
+ $page_node_type->save();
+ }
+
+ // Setup a node index.
+ $datasources = \Drupal::getContainer()
+ ->get('search_api.plugin_helper')
+ ->createDatasourcePlugins($this->index, ['entity:node']);
+ $this->index->setDatasources($datasources);
+ $this->index->save();
+ $this->container
+ ->get('search_api.index_task_manager')
+ ->addItemsAll($this->index);
+ $index_storage = $this->container
+ ->get('entity_type.manager')
+ ->getStorage('search_api_index');
+ $index_storage->resetCache([$this->index->id()]);
+ $this->index = $index_storage->load($this->index->id());
+ }
+
+ /**
+ * Tests that the correct boost is set on items.
+ *
+ * @covers ::preprocessIndexItems
+ */
+ public function testEntityBundleBoost() {
+ // Enable the processor indexing.
+ $processor = $this->index->getProcessor('type_boost');
+ $configuration = [
+ 'boosts' => [
+ 'entity:node' => [
+ 'datasource_boost' => '3.0',
+ 'bundle_boosts' => [
+ 'article' => '5.0',
+ ],
+ ],
+ ],
+ ];
+ $processor->setConfiguration($configuration);
+ $this->index->setProcessors(['type_boost' => $processor]);
+ $this->index->save();
+
+ // Create a node for both node types.
+ $nodes = [];
+ foreach (['article', 'page'] as $node_type) {
+ $node = Node::create([
+ 'status' => NodeInterface::PUBLISHED,
+ 'type' => $node_type,
+ 'title' => $this->randomString(),
+ ]);
+ $node->save();
+ $nodes[$node->id()] = $node->getTypedData();
+ }
+
+ // Prepare and generate Search API items.
+ $items = [];
+ foreach ($nodes as $nid => $node) {
+ $items[] = [
+ 'datasource' => 'entity:node',
+ 'item' => $node,
+ 'item_id' => $nid,
+ ];
+ }
+ $items = $this->generateItems($items);
+
+ // Preprocess items.
+ $this->index->preprocessIndexItems($items);
+
+ // Check boost value on article node.
+ $boost_expected = $configuration['boosts']['entity:node']['bundle_boosts']['article'];
+ $boost_actual = sprintf('%.1f', $items['entity:node/1']->getBoost());
+ $this->assertEquals($boost_expected, $boost_actual);
+
+ // Check boost value on page node.
+ $boost_expected = $configuration['boosts']['entity:node']['datasource_boost'];
+ $boost_actual = sprintf('%.1f', $items['entity:node/2']->getBoost());
+ $this->assertEquals($boost_expected, $boost_actual);
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Kernel/ResultsTrait.php b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/ResultsTrait.php
new file mode 100644
index 000000000..172339cb9
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/ResultsTrait.php
@@ -0,0 +1,46 @@
+getResultItems());
+ sort($results);
+
+ $ids = [];
+ foreach ($expected as $entity_type => $items) {
+ $datasource_id = "entity:$entity_type";
+ foreach ($items as $i) {
+ if ($entity_type == 'user') {
+ $id = $i . ':en';
+ }
+ else {
+ /** @var \Drupal\Core\Entity\EntityInterface $entity */
+ $entity = $this->{"{$entity_type}s"}[$i];
+ $id = $entity->id() . ':en';
+ }
+ $ids[] = Utility::createCombinedId($datasource_id, $id);
+ }
+ }
+ sort($ids);
+
+ $this->assertEquals($ids, $results);
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Server/ServerChangesTest.php b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Server/ServerChangesTest.php
new file mode 100644
index 000000000..fdf668c73
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Server/ServerChangesTest.php
@@ -0,0 +1,130 @@
+installEntitySchema('user');
+ $this->installSchema('search_api', ['search_api_item']);
+ $this->installSchema('system', ['key_value_expire']);
+ $this->installEntitySchema('search_api_task');
+ $this->installConfig('search_api');
+
+ // Create a test server.
+ $this->server = Server::create([
+ 'name' => 'Test Server',
+ 'id' => 'test_server',
+ 'status' => 1,
+ 'backend' => 'search_api_test',
+ ]);
+ $this->server->save();
+
+ // Create a test index.
+ $this->index = Index::create([
+ 'name' => 'Test index',
+ 'id' => 'test_index',
+ 'status' => 1,
+ 'datasource_settings' => [
+ 'entity:user' => [],
+ ],
+ 'tracker_settings' => [
+ 'default' => [],
+ ],
+ 'server' => $this->server->id(),
+ 'options' => ['index_directly' => FALSE],
+ ]);
+
+ // Reset the list of called backend methods.
+ $this->getCalledMethods('backend');
+ }
+
+ /**
+ * Tests adding and removing of indexes.
+ */
+ public function testAddRemoveIndex() {
+ $this->index->save();
+ $this->index->setServer(NULL)->save();
+ $this->index->setServer($this->server)->enable()->save();
+ $this->server->disable()->save();
+ $this->index->setServer(NULL)->save();
+ $this->server->enable()->save();
+ $this->index->setServer($this->server)->enable()->save();
+ $this->index->delete();
+
+ $methods = $this->getCalledMethods('backend');
+ $methods = array_intersect($methods, ['addIndex', 'removeIndex']);
+ $expected = [
+ 'addIndex',
+ 'removeIndex',
+ 'addIndex',
+ 'removeIndex',
+ 'addIndex',
+ 'removeIndex',
+ ];
+ $this->assertEquals($expected, array_values($methods));
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Server/ServerStorageTest.php b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Server/ServerStorageTest.php
new file mode 100644
index 000000000..f330af55d
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Server/ServerStorageTest.php
@@ -0,0 +1,131 @@
+installSchema('system', 'key_value_expire');
+
+ $this->installEntitySchema('search_api_task');
+ $this->storage = $this->container->get('entity_type.manager')->getStorage('search_api_server');
+ }
+
+ /**
+ * Tests all CRUD operations as a queue of operations.
+ */
+ public function testServerCrud() {
+ $this->assertTrue($this->storage instanceof ConfigEntityStorage, 'The Search API Server storage controller is loaded.');
+
+ $server = $this->serverCreate();
+ $this->serverLoad($server);
+ $this->serverUpdate($server);
+ $this->serverDelete($server);
+ }
+
+ /**
+ * Tests whether creating a server works correctly.
+ *
+ * @return \Drupal\search_api\ServerInterface
+ * The newly created search server.
+ */
+ public function serverCreate() {
+ $server_data = [
+ 'id' => 'test_server',
+ 'name' => 'Test server',
+ 'backend' => 'search_api_test',
+ ];
+ $server = $this->storage->create($server_data);
+
+ $this->assertTrue($server instanceof ServerInterface, 'The newly created entity is a Search API Server.');
+ $server->save();
+
+ $key = 'search_api_test.methods_called.' . $server->id();
+ $methods_called = \Drupal::state()->get($key, []);
+ $this->assertNotContains('preUpdate', $methods_called, 'Backend::preUpdate() not called for initial save.');
+ $this->assertNotContains('postUpdate', $methods_called, 'Backend::postUpdate() not called for initial save.');
+
+ return $server;
+ }
+
+ /**
+ * Tests whether loading a server works correctly.
+ *
+ * @param \Drupal\search_api\ServerInterface $server
+ * The server used for this test.
+ */
+ public function serverLoad(ServerInterface $server) {
+ $loaded_server = $this->storage->load($server->id());
+ $this->assertSame($server->label(), $loaded_server->label());
+
+ $this->storage->resetCache();
+ $loaded_server = $this->storage->load($server->id());
+ $this->assertSame($server->label(), $loaded_server->label());
+ }
+
+ /**
+ * Tests whether updating a server works correctly.
+ *
+ * @param \Drupal\search_api\ServerInterface $server
+ * The server used for this test.
+ */
+ public function serverUpdate(ServerInterface $server) {
+ $server->set('name', $server->label() . ' - edited');
+ $server->save();
+
+ $methods_called = $this->getCalledMethods('backend');
+ $this->assertContains('preUpdate', $methods_called, 'Backend::preUpdate() called for update.');
+ $this->assertContains('postUpdate', $methods_called, 'Backend::postUpdate() called for update.');
+
+ $this->serverLoad($server);
+ }
+
+ /**
+ * Tests whether deleting a server works correctly.
+ *
+ * @param \Drupal\search_api\ServerInterface $server
+ * The server used for this test.
+ */
+ public function serverDelete(ServerInterface $server) {
+ $this->storage->delete([$server]);
+ $loaded_server = $this->storage->load($server->id());
+ $this->assertNull($loaded_server);
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Server/ServerTaskTest.php b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Server/ServerTaskTest.php
new file mode 100644
index 000000000..1af903fc0
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Server/ServerTaskTest.php
@@ -0,0 +1,463 @@
+installEntitySchema('user');
+ $this->installEntitySchema('search_api_task');
+ $this->installSchema('search_api', ['search_api_item']);
+ $this->installConfig('search_api');
+
+ // Create a test server.
+ $this->server = Server::create([
+ 'name' => 'Test Server',
+ 'id' => 'test_server',
+ 'status' => 1,
+ 'backend' => 'search_api_test',
+ ]);
+ $this->server->save();
+
+ // Create a test index.
+ $this->index = Index::create([
+ 'name' => 'Test index',
+ 'id' => 'test_index',
+ 'status' => 1,
+ 'datasource_settings' => [
+ 'entity:user' => [],
+ ],
+ 'tracker_settings' => [
+ 'default' => [],
+ ],
+ 'server' => $this->server->id(),
+ 'options' => ['index_directly' => FALSE],
+ ]);
+ $this->index->save();
+
+ $this->taskManager = $this->container->get('search_api.task_manager');
+ $this->serverTaskManager = $this->container->get('search_api.server_task_manager');
+
+ // Reset the list of called backend methods.
+ $this->getCalledMethods('backend');
+ }
+
+ /**
+ * Tests task system integration for the server's addIndex() method.
+ */
+ public function testAddIndex() {
+ // Since we want to add the index, we should first remove it (even though it
+ // shouldn't matter – just for logic consistency).
+ $this->index->setServer(NULL);
+ $this->index->save();
+
+ // Set exception for addIndex() and reset the list of successful backend
+ // method calls.
+ $this->setError('backend', 'addIndex');
+ $this->getCalledMethods('backend');
+
+ // Try to add the index.
+ $this->server->addIndex($this->index);
+ $this->assertEquals([], $this->getCalledMethods('backend'), 'addIndex correctly threw an exception.');
+ $tasks = $this->getServerTasks();
+ if (count($tasks) == 1) {
+ $task_created = $tasks[0]->type === 'addIndex';
+ }
+ $this->assertTrue(!empty($task_created), 'The addIndex task was successfully added.');
+ if ($tasks) {
+ $this->assertEquals($this->index->id(), $tasks[0]->index_id, 'The right index ID was used for the addIndex task.');
+ }
+
+ // Check whether other task-system-integrated methods now fail, too.
+ $this->server->updateIndex($this->index);
+ $this->assertEquals([], $this->getCalledMethods('backend'), 'updateIndex was not executed.');
+ $tasks = $this->getServerTasks();
+ if (count($tasks) == 2) {
+ $this->assertTrue(TRUE, "Second task ('updateIndex') was added.");
+ $this->assertEquals('addIndex', $tasks[0]->type, 'First task stayed the same.');
+ $this->assertEquals('updateIndex', $tasks[1]->type, 'New task was queued as last.');
+ }
+ else {
+ $this->fail("Second task (updateIndex) was not added.");
+ }
+
+ // Let addIndex() succeed again, then trigger the task execution with a cron
+ // run.
+ $this->setError('backend', 'addIndex', FALSE);
+ search_api_cron();
+ $this->assertEquals([], $this->getServerTasks(), 'Server tasks were correctly executed.');
+ $this->assertEquals(['addIndex', 'updateIndex'], $this->getCalledMethods('backend'), 'Right methods were called during task execution.');
+ }
+
+ /**
+ * Tests task system integration for the server's updateIndex() method.
+ */
+ public function testUpdateIndex() {
+ // Set exception for updateIndex().
+ $this->setError('backend', 'updateIndex');
+
+ // Try to update the index.
+ $this->server->updateIndex($this->index);
+ $this->assertEquals([], $this->getCalledMethods('backend'), 'updateIndex correctly threw an exception.');
+ $tasks = $this->getServerTasks();
+ if (count($tasks) == 1) {
+ $task_created = $tasks[0]->type === 'updateIndex';
+ }
+ $this->assertTrue(!empty($task_created), 'The updateIndex task was successfully added.');
+ if ($tasks) {
+ $this->assertEquals($this->index->id(), $tasks[0]->index_id, 'The right index ID was used for the updateIndex task.');
+ }
+
+ // Check whether other task-system-integrated methods now fail, too.
+ $this->server->deleteAllIndexItems($this->index);
+ $this->assertEquals([], $this->getCalledMethods('backend'), 'deleteAllIndexItems was not executed.');
+ $tasks = $this->getServerTasks();
+ if (count($tasks) == 2) {
+ $this->assertTrue(TRUE, "Second task ('deleteAllIndexItems') was added.");
+ $this->assertEquals('updateIndex', $tasks[0]->type, 'First task stayed the same.');
+ $this->assertEquals('deleteAllIndexItems', $tasks[1]->type, 'New task was queued as last.');
+ }
+ else {
+ $this->fail("Second task (deleteAllIndexItems) was not added.");
+ }
+
+ // Let updateIndex() succeed again, then trigger the task execution with a
+ // call to indexItems().
+ $this->setError('backend', 'updateIndex', FALSE);
+ $this->server->indexItems($this->index, []);
+
+ $expected_methods = [
+ 'updateIndex',
+ 'deleteAllIndexItems',
+ 'indexItems',
+ ];
+ $this->assertEquals([], $this->getServerTasks(), 'Server tasks were correctly executed.');
+ $this->assertEquals($expected_methods, $this->getCalledMethods('backend'), 'Right methods were called during task execution.');
+ }
+
+ /**
+ * Tests task system integration for the server's removeIndex() method.
+ */
+ public function testRemoveIndex() {
+ // Set exception for updateIndex() and removeIndex().
+ $this->setError('backend', 'updateIndex');
+ $this->setError('backend', 'removeIndex');
+
+ // First try to update the index and fail. Then try to remove it and check
+ // that the tasks were set correctly.
+ $this->server->updateIndex($this->index);
+ $this->server->removeIndex($this->index);
+ $this->assertEquals([], $this->getCalledMethods('backend'), 'updateIndex and removeIndex correctly threw exceptions.');
+ $tasks = $this->getServerTasks();
+ if (count($tasks) == 1) {
+ $task_created = $tasks[0]->type === 'removeIndex';
+ }
+ $this->assertTrue(!empty($task_created), 'The removeIndex task was successfully added and other tasks removed.');
+ if ($tasks) {
+ $this->assertEquals($this->index->id(), $tasks[0]->index_id, 'The right index ID was used for the removeIndex task.');
+ }
+
+ // Check whether other task-system-integrated methods now fail, too.
+ try {
+ $this->server->indexItems($this->index, []);
+ $this->fail('Pending server tasks did not prevent indexing of items.');
+ }
+ catch (SearchApiException $e) {
+ $label = $this->index->label();
+ $expected_message = "Could not index items on index '$label' because pending server tasks could not be executed.";
+ $this->assertEquals($expected_message, $e->getMessage(), 'Pending server tasks prevented indexing of items.');
+ }
+ $this->assertEquals([], $this->getCalledMethods('backend'), 'indexItems was not executed.');
+ $tasks = $this->getServerTasks();
+ $this->assertEquals(1, count($tasks), 'No task added for indexItems.');
+
+ // Let removeIndex() succeed again, then trigger the task execution with a
+ // cron run.
+ $this->setError('backend', 'removeIndex', FALSE);
+ search_api_cron();
+ $this->assertEquals([], $this->getServerTasks(), 'Server tasks were correctly executed.');
+ $this->assertEquals(['removeIndex'], $this->getCalledMethods('backend'), 'Right methods were called during task execution.');
+ }
+
+ /**
+ * Tests task system integration for the server's deleteItems() method.
+ */
+ public function testDeleteItems() {
+ // Set exception for deleteItems().
+ $this->setError('backend', 'deleteItems');
+
+ // Try to update the index.
+ $this->server->deleteItems($this->index, []);
+ $this->assertEquals([], $this->getCalledMethods('backend'), 'deleteItems correctly threw an exception.');
+ $tasks = $this->getServerTasks();
+ if (count($tasks) == 1) {
+ $task_created = $tasks[0]->type === 'deleteItems';
+ }
+ $this->assertTrue(!empty($task_created), 'The deleteItems task was successfully added.');
+ if ($tasks) {
+ $this->assertEquals($this->index->id(), $tasks[0]->index_id, 'The right index ID was used for the deleteItems task.');
+ }
+
+ // Check whether other task-system-integrated methods now fail, too.
+ $this->server->updateIndex($this->index);
+ $this->assertEquals([], $this->getCalledMethods('backend'), 'updateIndex was not executed.');
+ $tasks = $this->getServerTasks();
+ if (count($tasks) == 2) {
+ $this->assertTrue(TRUE, "Second task ('updateIndex') was added.");
+ $this->assertEquals('deleteItems', $tasks[0]->type, 'First task stayed the same.');
+ $this->assertEquals('updateIndex', $tasks[1]->type, 'New task was queued as last.');
+ }
+ else {
+ $this->fail("Second task (updateIndex) was not added.");
+ }
+
+ // Let deleteItems() succeed again, then trigger the task execution
+ // with a cron run.
+ $this->setError('backend', 'deleteItems', FALSE);
+ search_api_cron();
+ $this->assertEquals([], $this->getServerTasks(), 'Server tasks were correctly executed.');
+ $this->assertEquals(['deleteItems', 'updateIndex'], $this->getCalledMethods('backend'), 'Right methods were called during task execution.');
+ }
+
+ /**
+ * Tests task system integration for the deleteAllIndexItems() method.
+ */
+ public function testDeleteAllIndexItems() {
+ // Set exception for deleteAllIndexItems().
+ $this->setError('backend', 'deleteAllIndexItems');
+
+ // Try to update the index.
+ $this->server->deleteAllIndexItems($this->index);
+ $this->assertEquals([], $this->getCalledMethods('backend'), 'deleteAllIndexItems correctly threw an exception.');
+ $tasks = $this->getServerTasks();
+ if (count($tasks) == 1) {
+ $task_created = $tasks[0]->type === 'deleteAllIndexItems';
+ }
+ $this->assertTrue(!empty($task_created), 'The deleteAllIndexItems task was successfully added.');
+ if ($tasks) {
+ $this->assertEquals($this->index->id(), $tasks[0]->index_id, 'The right index ID was used for the deleteAllIndexItems task.');
+ }
+
+ // Check whether other task-system-integrated methods now fail, too.
+ $this->server->updateIndex($this->index);
+ $this->assertEquals([], $this->getCalledMethods('backend'), 'updateIndex was not executed.');
+ $tasks = $this->getServerTasks();
+ if (count($tasks) == 2) {
+ $this->assertTrue(TRUE, "Second task ('updateIndex') was added.");
+ $this->assertEquals('deleteAllIndexItems', $tasks[0]->type, 'First task stayed the same.');
+ $this->assertEquals('updateIndex', $tasks[1]->type, 'New task was queued as last.');
+ }
+ else {
+ $this->fail("Second task (updateIndex) was not added.");
+ }
+
+ // Let deleteAllIndexItems() succeed again, then trigger the task execution
+ // with a call to indexItems().
+ $this->setError('backend', 'deleteAllIndexItems', FALSE);
+ $this->server->indexItems($this->index, []);
+
+ $expected_methods = [
+ 'deleteAllIndexItems',
+ 'updateIndex',
+ 'indexItems',
+ ];
+ $this->assertEquals([], $this->getServerTasks(), 'Server tasks were correctly executed.');
+ $this->assertEquals($expected_methods, $this->getCalledMethods('backend'), 'Right methods were called during task execution.');
+ }
+
+ /**
+ * Verifies that no more than 100 items will be executed at once.
+ */
+ public function testTaskCountLimit() {
+ // Create 100 tasks.
+ for ($i = 0; $i < 101; ++$i) {
+ $this->taskManager->addTask('deleteItems', $this->server, $this->index, ['']);
+ }
+
+ // Verify that a new operation cannot be executed.
+ $this->server->updateIndex($this->index);
+
+ $methods = $this->getCalledMethods('backend');
+ $this->assertCount(100, $methods, '100 pending tasks were executed upon new operation.');
+ $filter = function ($method) {
+ return $method != 'deleteItems';
+ };
+ $this->assertEmpty(array_filter($methods, $filter), 'The new operation was not executed.');
+ $this->assertEquals(2, $this->serverTaskManager->getCount($this->server), 'A task was created for the new operation.');
+ }
+
+ /**
+ * Tests the correct automatic removal of tasks upon certain operations.
+ */
+ public function testAutomaticTaskRemoval() {
+ // Create a second server and index and add tasks for them.
+ $server2 = Server::create([
+ 'name' => 'Test Server 2',
+ 'id' => 'test_server_2',
+ 'status' => 1,
+ 'backend' => 'search_api_test',
+ ]);
+ $server2->save();
+ $this->taskManager->addTask('removeIndex', $server2, $this->index);
+
+ $index_values = $this->index->toArray();
+ unset($index_values['uuid']);
+ $index_values['id'] = 'test_index_2';
+ $index2 = Index::create($index_values);
+ $index2->save();
+ // Reset the called backend methods.
+ $this->getCalledMethods('backend');
+
+ // Verify that adding an index ignores all tasks related to that index.
+ $this->addTasks($index2);
+ $this->server->addIndex($this->index);
+ $this->assertEquals(['addIndex', 'addIndex'], $this->getCalledMethods('backend'), 'Re-adding an index ignored all its tasks.');
+ $this->assertEquals(0, $this->serverTaskManager->getCount($this->server), 'No pending tasks for server.');
+ $this->assertEquals(1, $this->serverTaskManager->getCount(), 'The tasks of other servers were not touched.');
+
+ // Verify that removing an index ignores all tasks related to that index.
+ $this->addTasks($index2);
+ $this->server->removeIndex($this->index);
+ $this->assertEquals(['addIndex', 'removeIndex'], $this->getCalledMethods('backend'), 'Removing an index ignored all its tasks.');
+ $this->assertEquals(0, $this->serverTaskManager->getCount($this->server), 'No pending tasks for server.');
+ $this->assertEquals(1, $this->serverTaskManager->getCount(), 'The tasks of other servers were not touched.');
+
+ // Verify that deleting all of an index's items ignores all other deletion
+ // tasks related to that index.
+ $this->addTasks($index2);
+ $this->server->deleteAllIndexItems($this->index);
+ $called_methods = [
+ 'addIndex',
+ 'removeIndex',
+ 'addIndex',
+ 'updateIndex',
+ 'deleteAllIndexItems',
+ ];
+ $this->assertEquals($called_methods, $this->getCalledMethods('backend'), 'Deleting all items of an index ignored all its deletion tasks.');
+ $this->assertEquals(0, $this->serverTaskManager->getCount($this->server), 'No pending tasks for server.');
+ $this->assertEquals(1, $this->serverTaskManager->getCount(), 'The tasks of other servers were not touched.');
+
+ // Verify that removing all items from the server automatically removes all
+ // item deletion tasks as well.
+ $this->addTasks($index2);
+ $this->server->deleteAllItems();
+ // deleteAllIndexItems() is called twice – once for each index.
+ $this->assertEquals(['deleteAllIndexItems', 'deleteAllIndexItems'], $this->getCalledMethods('backend'), "Deleting all items from a server didn't execute any tasks.");
+ $this->assertEquals(4, $this->serverTaskManager->getCount($this->server), 'Deleting all items from a server removed all its item deletion tasks.');
+ $this->assertEquals(5, $this->serverTaskManager->getCount(), 'The tasks of other servers were not touched.');
+
+ // Verify that deleting a server also deletes all of its tasks.
+ $this->addTasks($index2);
+ $this->setError('backend', 'addIndex');
+ $this->setError('backend', 'updateIndex');
+ $this->setError('backend', 'removeIndex');
+ $this->setError('backend', 'deleteItems');
+ $this->setError('backend', 'deleteAllIndexItems');
+ $this->server->delete();
+ $this->assertEquals(0, $this->serverTaskManager->getCount($this->server), 'Upon server deletion, all of its tasks were deleted, too.');
+ $this->assertEquals(1, $this->serverTaskManager->getCount(), 'The tasks of other servers were not touched.');
+ }
+
+ /**
+ * Adds one task of each type for this test's server.
+ *
+ * @param \Drupal\search_api\IndexInterface $second_index
+ * A second index, for which one additional "addIndex" task is created.
+ */
+ protected function addTasks(IndexInterface $second_index) {
+ $this->taskManager->addTask('addIndex', $this->server, $second_index);
+ $this->taskManager->addTask('removeIndex', $this->server, $this->index);
+ $this->taskManager->addTask('addIndex', $this->server, $this->index);
+ $this->taskManager->addTask('updateIndex', $this->server, $this->index);
+ $this->taskManager->addTask('deleteItems', $this->server, $this->index, []);
+ $this->taskManager->addTask('deleteAllIndexItems', $this->server, $this->index);
+ }
+
+ /**
+ * Retrieves the tasks set on the test server.
+ *
+ * @return object[]
+ * All tasks read from the database for the test server, with numeric keys
+ * starting with 0.
+ */
+ protected function getServerTasks() {
+ $tasks = [];
+ $select = \Drupal::database()->select('search_api_task', 't');
+ $select->fields('t')
+ ->orderBy('id')
+ ->condition('server_id', $this->server->id());
+ foreach ($select->execute() as $task) {
+ if ($task->data) {
+ $task->data = unserialize($task->data);
+ }
+ $tasks[] = $task;
+ }
+ return $tasks;
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Kernel/System/CliTest.php b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/System/CliTest.php
new file mode 100644
index 000000000..0c418d144
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/System/CliTest.php
@@ -0,0 +1,140 @@
+installSchema('search_api', ['search_api_item']);
+ $this->installEntitySchema('entity_test_mulrev_changed');
+ $this->installEntitySchema('search_api_task');
+ $this->installConfig('search_api');
+
+ // Create a test server.
+ $this->server = Server::create([
+ 'name' => 'Test server',
+ 'id' => 'test',
+ 'status' => 1,
+ 'backend' => 'search_api_test',
+ ]);
+ $this->server->save();
+
+ // Disable the use of batches for item tracking to simulate a CLI
+ // environment.
+ if (!Utility::isRunningInCli()) {
+ \Drupal::state()->set('search_api_use_tracking_batch', FALSE);
+ }
+ }
+
+ /**
+ * Tests tracking of items when saving an index through the CLI.
+ */
+ public function testItemTracking() {
+ EntityTestMulRevChanged::create([
+ 'name' => 'foo bar baz föö smile' . json_decode('"\u1F601"'),
+ 'body' => 'test test case Case casE',
+ 'type' => 'entity_test_mulrev_changed',
+ 'keywords' => ['Orange', 'orange', 'örange', 'Orange'],
+ 'category' => 'item_category',
+ ])->save();
+ EntityTestMulRevChanged::create([
+ 'name' => 'foo bar baz föö smile',
+ 'body' => 'test test case Case casE',
+ 'type' => 'entity_test_mulrev_changed',
+ 'keywords' => ['strawberry', 'llama'],
+ 'category' => 'item_category',
+ ])->save();
+
+ // Create a test index.
+ /** @var \Drupal\search_api\IndexInterface $index */
+ $index = Index::create([
+ 'name' => 'Test index',
+ 'id' => 'index',
+ 'status' => 1,
+ 'datasource_settings' => [
+ 'entity:entity_test_mulrev_changed' => [],
+ ],
+ 'tracker_settings' => [
+ 'default' => [],
+ ],
+ 'server' => $this->server->id(),
+ 'options' => ['index_directly' => TRUE],
+ ]);
+ $index->save();
+
+ $total_items = $index->getTrackerInstance()->getTotalItemsCount();
+ $indexed_items = $index->getTrackerInstance()->getIndexedItemsCount();
+
+ $this->assertEquals(2, $total_items, 'The 2 items are tracked.');
+ $this->assertEquals(0, $indexed_items, 'No items are indexed');
+
+ EntityTestMulRevChanged::create([
+ 'name' => 'foo bar baz föö smile',
+ 'body' => 'test test case Case casE',
+ 'type' => 'entity_test_mulrev_changed',
+ 'keywords' => ['strawberry', 'llama'],
+ 'category' => 'item_category',
+ ])->save();
+ EntityTestMulRevChanged::create([
+ 'name' => 'foo bar baz föö smile',
+ 'body' => 'test test case Case casE',
+ 'type' => 'entity_test_mulrev_changed',
+ 'keywords' => ['strawberry', 'llama'],
+ 'category' => 'item_category',
+ ])->save();
+
+ $total_items = $index->getTrackerInstance()->getTotalItemsCount();
+ $indexed_items = $index->getTrackerInstance()->getIndexedItemsCount();
+
+ $this->assertEquals(4, $total_items, 'All 4 items are tracked.');
+ $this->assertEquals(0, $indexed_items, 'No items are indexed.');
+
+ $this->triggerPostRequestIndexing();
+
+ $total_items = $index->getTrackerInstance()->getTotalItemsCount();
+ $indexed_items = $index->getTrackerInstance()->getIndexedItemsCount();
+
+ $this->assertEquals(4, $total_items, 'All 4 items are tracked.');
+ $this->assertEquals(2, $indexed_items, '2 items are indexed.');
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Kernel/System/CommandHelperTest.php b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/System/CommandHelperTest.php
new file mode 100644
index 000000000..4961d5a1a
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/System/CommandHelperTest.php
@@ -0,0 +1,399 @@
+installSchema('search_api', ['search_api_item']);
+ $this->installSchema('system', ['key_value_expire', 'sequences']);
+ $this->installEntitySchema('entity_test_mulrev_changed');
+ $this->installEntitySchema('search_api_task');
+ $this->installConfig('search_api');
+
+ // Disable the use of batches for item tracking to simulate a CLI
+ // environment.
+ if (!Utility::isRunningInCli()) {
+ \Drupal::state()->set('search_api_use_tracking_batch', FALSE);
+ }
+
+ // Create a test server.
+ Server::create([
+ 'name' => 'Pink pony server',
+ 'id' => 'test_server',
+ 'status' => TRUE,
+ 'backend' => 'search_api_test',
+ ])->save();
+
+ Role::create([
+ 'id' => 'anonymous',
+ 'label' => 'anonymous',
+ ])->save();
+ user_role_grant_permissions('anonymous', ['view test entity']);
+
+ Index::create([
+ 'name' => 'Test Index',
+ 'id' => 'test_index',
+ 'status' => TRUE,
+ 'datasource_settings' => [
+ 'entity:entity_test_mulrev_changed' => [],
+ ],
+ 'tracker_settings' => [
+ 'default' => [],
+ ],
+ 'server' => 'test_server',
+ 'options' => ['index_directly' => FALSE],
+ ])->save();
+ Index::create([
+ 'name' => 'Secondary index.',
+ 'id' => 'second_index',
+ 'status' => FALSE,
+ 'datasource_settings' => [
+ 'entity:entity_test_mulrev_changed' => [],
+ ],
+ 'tracker_settings' => [
+ 'default' => [],
+ ],
+ 'server' => 'test_server',
+ 'options' => ['index_directly' => FALSE],
+ ])->save();
+
+ $this->setUpExampleStructure();
+ $this->insertExampleContent();
+ $this->indexItems('test_index');
+
+ $this->systemUnderTest = new CommandHelper(\Drupal::entityTypeManager(), \Drupal::moduleHandler(), 't');
+ $this->systemUnderTest->setLogger(new NullLogger());
+ }
+
+ /**
+ * Tests the "List indexes" command.
+ *
+ * @covers ::indexListCommand
+ */
+ public function testListCommand() {
+ $results = $this->systemUnderTest->indexListCommand();
+ $this->assertInternalType('array', $results);
+ $this->assertCount(2, $results);
+ $this->assertArrayHasKey('test_index', $results);
+ $this->assertArrayHasKey('second_index', $results);
+ $this->assertArrayHasKey('id', $results['test_index']);
+ $this->assertArrayHasKey('server', $results['test_index']);
+ $this->assertArrayHasKey('status', $results['test_index']);
+ $this->assertSame('test_index', $results['test_index']['id']);
+ $this->assertSame('test_server', $results['test_index']['server']);
+ $this->assertSame('enabled', (string) $results['test_index']['status']);
+ $this->assertSame('second_index', $results['second_index']['id']);
+ $this->assertSame('test_server', $results['second_index']['server']);
+ $this->assertSame('disabled', (string) $results['second_index']['status']);
+
+ $index = Index::load('test_index');
+ $index->delete();
+
+ $results = $this->systemUnderTest->indexListCommand();
+ $this->assertInternalType('array', $results);
+ $this->assertArrayNotHasKey('test_index', $results);
+ $this->assertArrayHasKey('second_index', $results);
+ }
+
+ /**
+ * Tests the "Index status" command.
+ *
+ * @covers ::indexStatusCommand
+ */
+ public function testStatusCommand() {
+ $results = $this->systemUnderTest->indexStatusCommand();
+ $this->assertInternalType('array', $results);
+ $this->assertCount(2, $results);
+ $this->assertArrayHasKey('test_index', $results);
+ $this->assertArrayHasKey('id', $results['test_index']);
+ $this->assertArrayHasKey('name', $results['test_index']);
+ $this->assertSame('test_index', $results['test_index']['id']);
+ $this->assertSame('Test Index', $results['test_index']['name']);
+ $this->assertSame('second_index', $results['second_index']['id']);
+ $this->assertSame('Secondary index.', $results['second_index']['name']);
+
+ $this->assertSame(5, $results['test_index']['total']);
+ $this->assertSame(5, $results['test_index']['indexed']);
+ $this->assertSame('100%', $results['test_index']['complete']);
+ }
+
+ /**
+ * Tests the enable index command.
+ *
+ * @covers ::enableIndexCommand
+ */
+ public function testEnableIndexCommand() {
+ $index = Index::load('second_index');
+ $this->assertFalse($index->status());
+ $this->systemUnderTest->enableIndexCommand(['second_index']);
+ $index = Index::load('second_index');
+ $this->assertTrue($index->status());
+
+ $this->setExpectedException(ConsoleException::class);
+ $this->systemUnderTest->enableIndexCommand(['foo']);
+ }
+
+ /**
+ * Tests the enable index command.
+ *
+ * @covers ::enableIndexCommand
+ */
+ public function testEnableIndexWithNoIndexes() {
+ /** @var \Drupal\search_api\IndexInterface[] $indexes */
+ $indexes = Index::loadMultiple();
+ foreach ($indexes as $index) {
+ $index->delete();
+ }
+
+ $this->setExpectedException(ConsoleException::class);
+ $this->systemUnderTest->enableIndexCommand(['second_index']);
+ }
+
+ /**
+ * Tests the clear index command.
+ *
+ * @covers ::clearIndexCommand
+ */
+ public function testClearIndexCommand() {
+ /** @var \Drupal\search_api\IndexInterface $index */
+ $index = Index::load('test_index');
+ $this->assertSame(5, $index->getTrackerInstance()->getIndexedItemsCount());
+ $this->systemUnderTest->clearIndexCommand(['test_index']);
+ $this->assertSame(0, $index->getTrackerInstance()->getIndexedItemsCount());
+ }
+
+ /**
+ * Tests the disable index command.
+ *
+ * @covers ::disableIndexCommand
+ */
+ public function testDisableIndexCommand() {
+ /** @var \Drupal\search_api\IndexInterface $index */
+ $index = Index::load('test_index');
+ $this->assertTrue($index->status());
+ $this->systemUnderTest->disableIndexCommand(['test_index']);
+ $index = Index::load('test_index');
+ $this->assertFalse($index->status());
+
+ $this->setExpectedException(ConsoleException::class);
+ $this->systemUnderTest->disableIndexCommand(['foo']);
+ }
+
+ /**
+ * Tests the indexItemsToIndexCommand.
+ *
+ * @covers ::indexItemsToIndexCommand
+ */
+ public function testIndexItemsToIndexCommand() {
+ /** @var \Drupal\search_api\IndexInterface $index */
+ $index = Index::load('test_index');
+ $this->assertSame(5, $index->getTrackerInstance()->getIndexedItemsCount());
+ $index->clear();
+ $this->assertSame(0, $index->getTrackerInstance()->getIndexedItemsCount());
+ $this->systemUnderTest->indexItemsToIndexCommand(['test_index'], 10, 10);
+ $this->runBatch();
+ $this->assertSame(5, $index->getTrackerInstance()->getIndexedItemsCount());
+ }
+
+ /**
+ * Tests resetTrackerCommand.
+ *
+ * @covers ::resetTrackerCommand
+ */
+ public function testResetTrackerCommand() {
+ /** @var \Drupal\search_api\IndexInterface $index */
+ $index = Index::load('test_index');
+ $this->assertSame(5, $index->getTrackerInstance()->getIndexedItemsCount());
+ $this->assertSame(5, $index->getTrackerInstance()->getTotalItemsCount());
+ $this->systemUnderTest->resetTrackerCommand(['test_index']);
+ $this->assertSame(0, $index->getTrackerInstance()->getIndexedItemsCount());
+ $this->assertSame(5, $index->getTrackerInstance()->getTotalItemsCount());
+ }
+
+ /**
+ * Tests searchIndexCommand.
+ *
+ * @covers ::searchIndexCommand
+ */
+ public function testSearchIndexCommand() {
+ $results = $this->systemUnderTest->searchIndexCommand('test_index');
+ $this->assertNotEmpty($results);
+ $this->assertCount(2, $results);
+ $results = $this->systemUnderTest->searchIndexCommand('test_index', 'test');
+ $this->assertNotEmpty($results);
+ $this->assertCount(1, $results);
+ }
+
+ /**
+ * Tests the server list command.
+ *
+ * @covers ::serverListCommand
+ */
+ public function testServerListCommand() {
+ $result = $this->systemUnderTest->serverListCommand();
+ $this->assertInternalType('array', $result);
+ $this->assertCount(1, $result);
+ $this->assertArrayHasKey('test_server', $result);
+ $this->assertSame('test_server', $result['test_server']['id']);
+ $this->assertSame('Pink pony server', $result['test_server']['name']);
+ $this->assertSame('enabled', (string) $result['test_server']['status']);
+
+ /** @var \Drupal\search_api\ServerInterface $server */
+ $server = Server::load('test_server');
+ $server->setStatus(FALSE);
+ $server->save();
+
+ $result = $this->systemUnderTest->serverListCommand();
+ $this->assertInternalType('array', $result);
+ $this->assertCount(1, $result);
+ $this->assertArrayHasKey('test_server', $result);
+ $this->assertSame('test_server', $result['test_server']['id']);
+ $this->assertSame('Pink pony server', $result['test_server']['name']);
+ $this->assertSame('disabled', (string) $result['test_server']['status']);
+
+ $server->delete();
+ $this->setExpectedException(ConsoleException::class);
+ $this->systemUnderTest->serverListCommand();
+ }
+
+ /**
+ * Tests the server enable command.
+ *
+ * @covers ::enableServerCommand
+ */
+ public function testServerEnableCommand() {
+ /** @var \Drupal\search_api\ServerInterface $server */
+ $server = Server::load('test_server');
+ $server->setStatus(FALSE);
+ $server->save();
+
+ $this->systemUnderTest->enableServerCommand('test_server');
+ $server = Server::load('test_server');
+ $this->assertTrue($server->status());
+
+ $this->setExpectedException(ConsoleException::class);
+ $this->systemUnderTest->enableServerCommand('foo');
+ }
+
+ /**
+ * Tests the server disable command.
+ *
+ * @covers ::disableServerCommand
+ */
+ public function testServerDisableCommand() {
+ $this->systemUnderTest->disableServerCommand('test_server');
+ /** @var \Drupal\search_api\ServerInterface $server */
+ $server = Server::load('test_server');
+ $this->assertFalse($server->status());
+
+ $this->setExpectedException(ConsoleException::class);
+ $this->systemUnderTest->enableServerCommand('foo');
+ }
+
+ /**
+ * Tests the clear server command.
+ *
+ * @covers ::clearServerCommand
+ */
+ public function testClearServerCommand() {
+ /** @var \Drupal\search_api\IndexInterface $index */
+ $index = Index::load('test_index');
+ $this->assertSame(5, $index->getTrackerInstance()->getIndexedItemsCount());
+ $this->systemUnderTest->clearServerCommand('test_server');
+ $this->assertSame(0, $index->getTrackerInstance()->getIndexedItemsCount());
+ }
+
+ /**
+ * Tests setIndexServerCommand.
+ *
+ * @covers ::setIndexServerCommand
+ */
+ public function testSetIndexServerCommand() {
+ /** @var \Drupal\search_api\IndexInterface $index */
+ $index = Index::load('test_index');
+ $index->setServer(NULL);
+ $index->save();
+
+ $index = Index::load('test_index');
+ $this->assertSame(NULL, $index->getServerId());
+
+ $this->systemUnderTest->setIndexServerCommand('test_index', 'test_server');
+
+ $index = Index::load('test_index');
+ $this->assertSame('test_server', $index->getServerId());
+ }
+
+ /**
+ * Tests setIndexServerCommand.
+ *
+ * @covers ::setIndexServerCommand
+ */
+ public function testSetIndexServerCommandWithInvalidIndex() {
+ $this->setExpectedException(ConsoleException::class);
+ $this->systemUnderTest->setIndexServerCommand('foo', 'test_server');
+ }
+
+ /**
+ * Tests setIndexServerCommand.
+ *
+ * @covers ::setIndexServerCommand
+ */
+ public function testSetIndexServerCommandWithInvalidServer() {
+ $this->setExpectedException(ConsoleException::class);
+ $this->systemUnderTest->setIndexServerCommand('test_index', 'bar');
+ }
+
+ /**
+ * Runs the currently set batch, if any exists.
+ */
+ protected function runBatch() {
+ $batch = &batch_get();
+ if ($batch) {
+ $batch['progressive'] = FALSE;
+ batch_process();
+ }
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Kernel/System/CustomDataTypesTest.php b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/System/CustomDataTypesTest.php
new file mode 100644
index 000000000..5c87c5ea3
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/System/CustomDataTypesTest.php
@@ -0,0 +1,181 @@
+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_test_db']);
+
+ // Create test entities.
+ $this->entities[1] = EntityTestMulRevChanged::create([
+ 'name' => 'foo bar baz föö smile' . json_decode('"\u1F601"'),
+ 'body' => 'test test case Case casE',
+ 'type' => 'entity_test_mulrev_changed',
+ 'keywords' => ['Orange', 'orange', 'örange', 'Orange'],
+ 'category' => 'item_category',
+ ]);
+ $this->entities[2] = EntityTestMulRevChanged::create([
+ 'name' => 'foo bar baz föö smile',
+ 'body' => 'test test case Case casE',
+ 'type' => 'entity_test_mulrev_changed',
+ 'keywords' => ['strawberry', 'llama'],
+ 'category' => 'item_category',
+ ]);
+ $this->entities[1]->save();
+ $this->entities[2]->save();
+
+ // Create a test server.
+ $this->server = Server::create([
+ 'name' => 'Server test ~',
+ 'id' => 'test',
+ 'status' => 1,
+ 'backend' => 'search_api_test',
+ ]);
+ $this->server->save();
+
+ // Set the server (determines the supported data types) and remove all
+ // non-base fields from the index (since their config isn't installed).
+ $this->index = Index::load('database_search_index');
+ $this->index->setServer($this->server)
+ ->removeField('body')
+ ->removeField('keywords')
+ ->removeField('category')
+ ->removeField('width');
+ }
+
+ /**
+ * Tests custom data types integration.
+ */
+ public function testCustomDataTypes() {
+ $original_value = $this->entities[1]->get('name')->getValue()[0]['value'];
+ $original_type = $this->index->getField('name')->getType();
+
+ $item = $this->index->loadItem('entity:entity_test_mulrev_changed/1:en');
+ $item = \Drupal::getContainer()
+ ->get('search_api.fields_helper')
+ ->createItemFromObject($this->index, $item, 'entity:entity_test_mulrev_changed/1:en');
+
+ $name_field = $item->getField('name');
+ $processed_value = $name_field->getValues()[0];
+ $processed_type = $name_field->getType();
+ $label = $name_field->getLabel();
+
+ $this->assertEquals($original_value, $processed_value, 'The processed value matches the original value');
+ $this->assertEquals($original_type, $processed_type, 'The processed type matches the original type.');
+ $this->assertEquals('Name', $label, 'The label is correctly set.');
+
+ // Reset the fields on the item and change to the supported data type.
+ $item->setFieldsExtracted(FALSE);
+ $item->setFields([]);
+ $field = $this->index->getField('name')
+ ->setType('search_api_test')
+ ->setLabel("Test");
+ $this->index->addField($field);
+
+ $name_field = $item->getField('name');
+ $processed_value = $name_field->getValues()[0];
+ $processed_type = $name_field->getType();
+
+ $this->assertEquals($original_value, $processed_value, 'The processed value matches the original value');
+ $this->assertEquals('search_api_test', $processed_type, 'The processed type matches the new type.');
+ $this->assertEquals('Test', $name_field->getLabel(), 'The label is correctly set.');
+
+ // Reset the fields on the item and change to the non-supported data type.
+ $item->setFieldsExtracted(FALSE);
+ $item->setFields([]);
+ $field = $this->index->getField('name')
+ ->setType('search_api_test_unsupported');
+ $this->index->addField($field);
+ $name_field = $item->getField('name');
+
+ $processed_value = $name_field->getValues()[0];
+ $processed_type = $name_field->getType();
+
+ $this->assertEquals($original_value, $processed_value, 'The processed value matches the original value');
+ $this->assertEquals('integer', $processed_type, 'The processed type matches the fallback type.');
+
+ // Reset the fields on the item and change to the data altering data type.
+ $item->setFieldsExtracted(FALSE);
+ $item->setFields([]);
+ $field = $this->index->getField('name')
+ ->setType('search_api_test_altering');
+ $this->index->addField($field);
+ $name_field = $item->getField('name');
+
+ $processed_value = $name_field->getValues()[0];
+ $processed_type = $name_field->getType();
+
+ $this->assertEquals(strlen($original_value), $processed_value, 'The processed value matches the altered original value');
+ $this->assertEquals('search_api_test_altering', $processed_type, 'The processed type matches the defined type.');
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Kernel/System/FieldValuesExtractionTest.php b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/System/FieldValuesExtractionTest.php
new file mode 100644
index 000000000..f1474807c
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/System/FieldValuesExtractionTest.php
@@ -0,0 +1,299 @@
+installSchema('system', ['sequences']);
+ $this->installEntitySchema('entity_test_mulrev_changed');
+ $this->installEntitySchema('user');
+ $this->installConfig(['search_api_test_extraction', 'user']);
+ $entity_storage = \Drupal::entityTypeManager()
+ ->getStorage('entity_test_mulrev_changed');
+
+ $this->entities[0] = $entity_storage->create([
+ 'type' => 'article',
+ 'name' => 'Article 1',
+ 'links' => [],
+ ]);
+ $this->entities[0]->save();
+ $this->entities[1] = $entity_storage->create([
+ 'type' => 'article',
+ 'name' => 'Article 2',
+ 'links' => [],
+ ]);
+ $this->entities[1]->save();
+ $this->entities[2] = $entity_storage->create([
+ 'type' => 'article',
+ 'name' => 'Article 3',
+ 'links' => [
+ ['target_id' => $this->entities[0]->id()],
+ ['target_id' => $this->entities[1]->id()],
+ ],
+ ]);
+ $this->entities[2]->save();
+ $this->entities[3] = $entity_storage->create([
+ 'type' => 'article',
+ 'name' => 'Article 4',
+ 'links' => [
+ ['target_id' => $this->entities[0]->id()],
+ ['target_id' => $this->entities[2]->id()],
+ ],
+ ]);
+ $this->entities[2]->save();
+
+ user_role_grant_permissions('anonymous', ['view test entity']);
+
+ User::create([
+ 'id' => $this->entities[0],
+ 'name' => 'Test user',
+ ])->save();
+
+ $this->index = Index::create([
+ 'field_settings' => [
+ 'foo' => [
+ 'type' => 'text',
+ 'datasource_id' => 'entity:entity_test_mulrev_changed',
+ 'property_path' => 'name',
+ ],
+ 'bar' => [
+ 'type' => 'text',
+ 'property_path' => 'rendered_item',
+ 'configuration' => [
+ 'roles' => [
+ 'anonymous' => 'anonymous',
+ ],
+ 'view_mode' => [
+ 'entity:entity_test_mulrev_changed' => [
+ 'article' => 'default',
+ ],
+ ],
+ ],
+ ],
+ ],
+ 'datasource_settings' => [
+ 'entity:entity_test_mulrev_changed' => [],
+ ],
+ ]);
+
+ $this->fieldsHelper = $this->container->get('search_api.fields_helper');
+ }
+
+ /**
+ * Tests extraction of field values, as used during indexing.
+ *
+ * @covers ::extractFields
+ * @covers ::extractField
+ * @covers ::extractFieldValues
+ */
+ public function testFieldValuesExtraction() {
+ $object = $this->entities[3]->getTypedData();
+ /** @var \Drupal\search_api\Item\FieldInterface[][] $fields */
+ $fields = [
+ 'type' => [$this->fieldsHelper->createField($this->index, 'type')],
+ 'name' => [$this->fieldsHelper->createField($this->index, 'name')],
+ 'links:entity:name' => [
+ $this->fieldsHelper->createField($this->index, 'links'),
+ $this->fieldsHelper->createField($this->index, 'links_1'),
+ ],
+ 'links:entity:links:entity:name' => [
+ $this->fieldsHelper->createField($this->index, 'links_links'),
+ ],
+ ];
+ $this->fieldsHelper->extractFields($object, $fields);
+
+ $values = [];
+ foreach ($fields as $property_path => $property_fields) {
+ foreach ($property_fields as $field) {
+ $field_values = $field->getValues();
+ sort($field_values);
+ if (!isset($values[$property_path])) {
+ $values[$property_path] = $field_values;
+ }
+ else {
+ $this->assertEquals($field_values, $values[$property_path], 'Second extraction provided the same results as the first.');
+ }
+ }
+ }
+
+ $expected = [
+ 'type' => ['article'],
+ 'name' => ['Article 4'],
+ 'links:entity:name' => [
+ 'Article 1',
+ 'Article 3',
+ ],
+ 'links:entity:links:entity:name' => [
+ 'Article 1',
+ 'Article 2',
+ ],
+ ];
+ $this->assertEquals($expected, $values, 'Field values were correctly extracted');
+ }
+
+ /**
+ * Tests extraction of properties, as used in processors or for result lists.
+ *
+ * @covers ::extractItemValues
+ */
+ public function testPropertyValuesExtraction() {
+ $items['foobar'] = $this->fieldsHelper->createItemFromObject(
+ $this->index,
+ $this->entities[0]->getTypedData(),
+ Utility::createCombinedId('entity:entity_test_mulrev_changed', '0:en')
+ );
+
+ $properties = [
+ NULL => [
+ 'rendered_item' => 'a',
+ // Since there is no field defined on "aggregated_field" for the index,
+ // we won't be able to extract it.
+ 'aggregated_field' => 'b',
+ 'search_api_url' => 'c',
+ ],
+ 'entity:entity_test_mulrev_changed' => [
+ 'name' => 'd',
+ 'type' => 'e',
+ 'soul_mate:name' => 'f',
+ ],
+ 'unknown_datasource' => [
+ 'name' => 'x',
+ ],
+ ];
+
+ $expected = [
+ 'foobar' => [
+ 'a' => [],
+ 'b' => [],
+ 'c' => [],
+ 'd' => [],
+ 'e' => [],
+ 'f' => [],
+ ],
+ ];
+ $values = $this->fieldsHelper->extractItemValues($items, $properties, FALSE);
+ ksort($values['foobar']);
+ $this->assertEquals($expected, $values);
+
+ $expected = [
+ 'foobar' => [
+ // 'a' => 'Tested separately.',
+ 'b' => [],
+ 'c' => ['/entity_test_mulrev_changed/manage/1'],
+ 'd' => ['Article 1'],
+ 'e' => ['article'],
+ 'f' => ['Test user'],
+ ],
+ ];
+ $values = $this->fieldsHelper->extractItemValues($items, $properties);
+ ksort($values['foobar']);
+ $this->assertArrayHasKey('a', $values['foobar']);
+ $this->assertNotEmpty($values['foobar']['a']);
+ $this->assertContains('Article 1', $values['foobar']['a'][0]);
+ unset($values['foobar']['a']);
+ $this->assertEquals($expected, $values);
+
+ $items['foobar']->setFields([
+ 'aa' => $this->fieldsHelper->createField($this->index, 'aa_foo', [
+ 'property_path' => 'aggregated_field',
+ 'values' => [1, 2],
+ ]),
+ 'bb' => $this->fieldsHelper->createField($this->index, 'bb_foo', [
+ 'property_path' => 'rendered_item',
+ 'values' => [3],
+ ]),
+ 'cc' => $this->fieldsHelper->createField($this->index, 'cc_foo', [
+ 'datasource_id' => 'entity:entity_test_mulrev_changed',
+ 'property_path' => 'type',
+ 'values' => [4],
+ ]),
+ 'dd' => $this->fieldsHelper->createField($this->index, 'dd_foo', [
+ 'datasource_id' => 'entity:entity_test_mulrev_changed',
+ 'property_path' => 'soul_mate:name',
+ 'values' => [5],
+ ]),
+ ]);
+
+ $expected = [
+ 'foobar' => [
+ 'a' => [3],
+ 'b' => [1, 2],
+ 'c' => [],
+ 'd' => [],
+ 'e' => [4],
+ 'f' => [5],
+ ],
+ ];
+ $values = $this->fieldsHelper->extractItemValues($items, $properties, FALSE);
+ ksort($values['foobar']);
+ $this->assertEquals($expected, $values);
+
+ $expected = [
+ 'foobar' => [
+ 'a' => [3],
+ 'b' => [1, 2],
+ 'c' => ['/entity_test_mulrev_changed/manage/1'],
+ 'd' => ['Article 1'],
+ 'e' => [4],
+ 'f' => [5],
+ ],
+ ];
+ $values = $this->fieldsHelper->extractItemValues($items, $properties);
+ ksort($values['foobar']);
+ $this->assertEquals($expected, $values);
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Kernel/System/QueryTest.php b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/System/QueryTest.php
new file mode 100644
index 000000000..0766c3360
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/System/QueryTest.php
@@ -0,0 +1,228 @@
+installSchema('search_api', ['search_api_item']);
+ $this->installEntitySchema('entity_test_mulrev_changed');
+ $this->installEntitySchema('search_api_task');
+ $this->installConfig('search_api');
+
+ // Create a test server.
+ $server = Server::create([
+ 'name' => 'Test Server',
+ 'id' => 'test_server',
+ 'status' => 1,
+ 'backend' => 'search_api_test',
+ ]);
+ $server->save();
+
+ // Create a test index.
+ Index::create([
+ 'name' => 'Test Index',
+ 'id' => 'test_index',
+ 'status' => 1,
+ 'datasource_settings' => [
+ 'search_api_test' => [],
+ ],
+ 'processor_settings' => [
+ 'search_api_test' => [],
+ ],
+ 'tracker_settings' => [
+ 'default' => [],
+ ],
+ 'server' => $server->id(),
+ 'options' => ['index_directly' => FALSE],
+ ])->save();
+ $this->index = Index::load('test_index');
+ }
+
+ /**
+ * Tests that processing levels are working correctly.
+ *
+ * @param int $level
+ * The processing level to test.
+ * @param bool $hooks_and_processors_invoked
+ * (optional) Whether hooks and processors should be invoked with this
+ * processing level.
+ *
+ * @dataProvider testProcessingLevelDataProvider
+ */
+ public function testProcessingLevel($level, $hooks_and_processors_invoked = TRUE) {
+ /** @var \Drupal\search_api\Processor\ProcessorInterface $processor */
+ $processor = $this->container->get('plugin.manager.search_api.processor')
+ ->createInstance('search_api_test', ['#index' => $this->index]);
+ $this->index->addProcessor($processor)->save();
+
+ $query = $this->index->query();
+ if ($level != QueryInterface::PROCESSING_FULL) {
+ $query->setProcessingLevel($level);
+ }
+ $this->assertEquals($level, $query->getProcessingLevel());
+ $query->addTag('andrew_hill');
+
+ // @todo Use \Drupal::messenger() once we depend on Drupal 8.5+. See
+ // #2931730.
+ drupal_get_messages();
+ $query->execute();
+ $messages = drupal_get_messages();
+
+ $methods = $this->getCalledMethods('processor');
+ if ($hooks_and_processors_invoked) {
+ // @todo Replace "status" with MessengerInterface::TYPE_STATUS once we
+ // depend on Drupal 8.5+. See #2931730.
+ $expected = [
+ 'status' => [
+ 'Funky blue note',
+ 'Search id: ',
+ 'Stepping into tomorrow',
+ 'Llama',
+ ],
+ ];
+ $this->assertEquals($expected, $messages);
+ $this->assertTrue($query->getOption('tag query alter hook'));
+ $this->assertContains('preprocessSearchQuery', $methods);
+ $this->assertContains('postprocessSearchResults', $methods);
+ }
+ else {
+ $this->assertEmpty($messages);
+ $this->assertFalse($query->getOption('tag query alter hook'));
+ $this->assertNotContains('preprocessSearchQuery', $methods);
+ $this->assertNotContains('postprocessSearchResults', $methods);
+ }
+ }
+
+ /**
+ * Provides test data for the testProcessingLevel() method.
+ *
+ * @return array[]
+ * Arrays of method arguments for the
+ * \Drupal\Tests\search_api\Kernel\QueryTest::testProcessingLevel() method.
+ */
+ public function testProcessingLevelDataProvider() {
+ return [
+ 'none' => [QueryInterface::PROCESSING_NONE, FALSE],
+ 'basic' => [QueryInterface::PROCESSING_BASIC],
+ 'full' => [QueryInterface::PROCESSING_FULL],
+ ];
+ }
+
+ /**
+ * Tests that queries can be cloned.
+ */
+ public function testQueryCloning() {
+ $query = $this->index->query();
+ $this->assertEquals(0, $query->getResults()->getResultCount());
+ $cloned_query = clone $query;
+ $cloned_query->getResults()->setResultCount(1);
+ $this->assertEquals(0, $query->getResults()->getResultCount());
+ $this->assertEquals(1, $cloned_query->getResults()->getResultCount());
+ }
+
+ /**
+ * Tests that serialization of queries works correctly.
+ */
+ public function testQuerySerialization() {
+ $query = Query::create($this->index);
+ $tags = ['tag1', 'tag2'];
+ $query->keys('foo bar')
+ ->addCondition('field1', 'value', '<')
+ ->addCondition('field2', [15, 25], 'BETWEEN')
+ ->addConditionGroup($query->createConditionGroup('OR', $tags)
+ ->addCondition('field2', 'foo')
+ ->addCondition('field3', 1, '<>')
+ )
+ ->sort('field1', Query::SORT_DESC)
+ ->sort('field2');
+ $query->setOption('option1', ['foo' => 'bar']);
+ $translation = $this->container->get('string_translation');
+ $query->setStringTranslation($translation);
+
+ $cloned_query = clone $query;
+ $unserialized_query = unserialize(serialize($query));
+ $this->assertEquals($cloned_query, $unserialized_query);
+ }
+
+ /**
+ * Tests that the results cache works correctly.
+ */
+ public function testResultsCache() {
+ /** @var \Drupal\search_api\Query\QueryInterface[] $results */
+ $results = [];
+ $search_ids = ['foo', 'bar'];
+ foreach ($search_ids as $search_id) {
+ $results[$search_id] = $this->index->query()
+ ->setSearchId($search_id)
+ ->execute();
+ }
+
+ $results_cache = \Drupal::getContainer()
+ ->get('search_api.query_helper');
+ foreach ($search_ids as $search_id) {
+ $this->assertSame($results[$search_id], $results_cache->getResults($search_id));
+ }
+ $this->assertNull($results_cache->getResults('foobar'));
+
+ $this->assertSame($results, $results_cache->getAllResults());
+
+ $results_cache->removeResults('foo');
+ unset($results['foo']);
+ $this->assertSame($results, $results_cache->getAllResults());
+ }
+
+ /**
+ * Tests whether the display plugin integration works correctly.
+ */
+ public function testDisplayPluginIntegration() {
+ $query = $this->index->query();
+ $this->assertSame(NULL, $query->getSearchId(FALSE));
+ $this->assertSame('search_1', $query->getSearchId());
+ $this->assertSame('search_1', $query->getSearchId(FALSE));
+ $this->assertSame(NULL, $query->getDisplayPlugin());
+
+ $query = $this->index->query()->setSearchId('search_api_test');
+ $this->assertInstanceOf('Drupal\search_api_test\Plugin\search_api\display\TestDisplay', $query->getDisplayPlugin());
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Kernel/System/SerializationTest.php b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/System/SerializationTest.php
new file mode 100644
index 000000000..5c624dad7
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/System/SerializationTest.php
@@ -0,0 +1,328 @@
+createTestIndex();
+
+ // Make some changes to the index to ensure they're saved, too.
+ $field_helper = \Drupal::getContainer()->get('search_api.fields_helper');
+ $field_info = [
+ 'type' => 'date',
+ 'datasource_id' => 'entity:node',
+ 'property_path' => 'uid:entity:created',
+ ];
+ $index->addField($field_helper->createField($index, 'test1', $field_info));
+ $plugin_creation_helper = \Drupal::getContainer()
+ ->get('search_api.plugin_helper');
+ $index->addDatasource($plugin_creation_helper->createDatasourcePlugin($index, 'entity:user'));
+ $index->addProcessor($plugin_creation_helper->createProcessorPlugin($index, 'highlight'));
+ $index->setTracker($plugin_creation_helper->createTrackerPlugin($index, 'search_api_test'));
+
+ /** @var \Drupal\search_api\IndexInterface $serialized */
+ $serialized = unserialize(serialize($index));
+
+ $this->assertNotEmpty($serialized);
+ $storage = \Drupal::entityTypeManager()->getStorage('search_api_index');
+ $index->preSave($storage);
+ $serialized->preSave($storage);
+ $this->assertEquals($index->toArray(), $serialized->toArray());
+
+ // Make sure no object properties will be serialized for an index.
+ $index->getDatasources();
+ $index->getFields();
+ $index->getProcessors();
+ $index->getTrackerInstance();
+ $index->getPropertyDefinitions(NULL);
+
+ $contains_object = function ($var) use (&$contains_object) {
+ if (is_object($var)) {
+ return TRUE;
+ }
+ if (is_array($var)) {
+ foreach ($var as $key => $value) {
+ if ($contains_object($value)) {
+ return TRUE;
+ }
+ }
+ }
+ return FALSE;
+ };
+ $to_serialize = $index->__sleep();
+ foreach ($to_serialize as $property) {
+ $this->assertFalse($contains_object($index->get($property)), "Serialized property \$$property contains an object.");
+ }
+ }
+
+ /**
+ * Tests that serialization of server entities doesn't lead to data loss.
+ */
+ public function testServerSerialization() {
+ // As our test server, just use the one from the DB Defaults module.
+ $path = __DIR__ . '/../../../../modules/search_api_db/search_api_db_defaults/config/optional/search_api.server.default_server.yml';
+ $values = Yaml::decode(file_get_contents($path));
+ $server = new Server($values, 'search_api_server');
+
+ $serialized = unserialize(serialize($server));
+
+ $this->assertNotEmpty($serialized);
+ $this->assertEquals($server, $serialized);
+ }
+
+ /**
+ * Tests that serialization of search queries works correctly.
+ */
+ public function testQuerySerialization() {
+ $query = $this->createTestQuery();
+ $this->setMockIndexStorage();
+
+ $serialized = unserialize(serialize($query));
+
+ $this->assertNotEmpty($serialized);
+ $this->assertEquals((string) $query, (string) $serialized);
+
+ // Call serialize() on the restored query to make "equals" work correctly.
+ // (__sleep() sets some properties as a by-product which the serialized
+ // version doesn't have – namely, $indexId and $_serviceIds.)
+ serialize($serialized);
+ $this->assertEquals($query, $serialized);
+ }
+
+ /**
+ * Tests that cloning of search queries works correctly.
+ */
+ public function testQueryCloning() {
+ $query = $this->createTestQuery();
+ // Since Drupal's DB layer sometimes has problems with side-effects of
+ // __toString(), we here try to make sure this won't happen to us.
+ $this->assertInternalType('string', (string) $query);
+
+ $clone = clone $query;
+
+ // Modify the original query. None of this should change the clone in any
+ // way.
+ $query->setOption('test1', 'foo');
+ $query->getParseMode()->setConjunction('AND');
+ $query->addCondition('test1', 'bar');
+ $condition_group_1 = $query->getConditionGroup()->getConditions()[1];
+ $condition_group_2 = $condition_group_1->getConditions()[2];
+ $condition_group_3 = $query->createConditionGroup('AND');
+ $condition_group_1->addCondition('test1', 'foobar');
+ $condition_group_2->addCondition('test1', 'foobar');
+ $condition_group_3->addCondition('test1', 'foobar');
+ $query->getResults()->addWarning('This query is very dumb.');
+
+ $query_2 = $this->createTestQuery();
+ $this->assertEquals($query_2, $clone);
+ $this->assertNotSame($query->getResults(), $clone->getResults());
+ $this->assertNotSame($query->getResults(), $query_2->getResults());
+ $this->assertNotSame($query->getConditionGroup(), $clone->getConditionGroup());
+ $this->assertNotSame($query->getConditionGroup(), $query_2->getConditionGroup());
+ $this->assertNotSame($query->getParseMode(), $clone->getParseMode());
+ $this->assertNotSame($query->getParseMode(), $query_2->getParseMode());
+ }
+
+ /**
+ * Tests that cloning of items works correctly.
+ */
+ public function testItemCloning() {
+ $item = $this->createTestItem();
+
+ $clone = clone $item;
+
+ $item->setBoost(3);
+ $item->setExcerpt('Test 1');
+ $item->getExtraData('foo')->bar = 2;
+ $item->setExtraData('test', 3);
+ $item->setLanguage('de');
+ $item->setScore(3.14);
+ $item->getField('test')->setLabel('Foobar');
+
+ $item_2 = $this->createTestItem();
+ $this->assertEquals($item_2, $clone);
+ $this->assertNotSame($item->getExtraData('foo'), $clone->getExtraData('foo'));
+ $this->assertNotSame($item->getField('test'), $clone->getField('test'));
+ $this->assertNotSame($item->getField('foo'), $clone->getField('foo'));
+ }
+
+ /**
+ * Tests that serialization of fields works correctly.
+ */
+ public function testFieldSerialization() {
+ $field = $this->createTestField('test', 'entity:entity:entity_test_mulrev_changed');
+ $this->setMockIndexStorage();
+
+ $serialized = unserialize(serialize($field));
+
+ // Call serialize() on the restored query to make "equals" work correctly.
+ // (__sleep() sets some properties as a by-product which the serialized
+ // version doesn't have – $indexId, in this case.)
+ serialize($serialized);
+
+ $this->assertEquals($field, $serialized);
+ }
+
+ /**
+ * Creates a search query for use in this test.
+ *
+ * @return \Drupal\search_api\Query\QueryInterface
+ * A test query.
+ */
+ protected function createTestQuery() {
+ $query = $this->createTestIndex()->query([
+ 'foo' => 'bar',
+ ]);
+
+ $query->getParseMode()->setConjunction('OR');
+ $query->keys('test foobar');
+ $query->setFulltextFields(['foo', 'bar']);
+
+ $query->addCondition('title', 'foo', '<>');
+ $condition_group_1 = $query->createConditionGroup('OR', ['foobar']);
+ $condition_group_1->addCondition('foo', 'bar');
+ $query->addConditionGroup($condition_group_1);
+ $condition_group_1->addCondition('bar', [1, 5], 'BETWEEN');
+ $condition_group_2 = $query->createConditionGroup('AND', ['baz']);
+ $condition_group_2->addCondition('baz', 2, '>');
+ $condition_group_2->addCondition('baz', NULL, '<>');
+ $condition_group_1->addConditionGroup($condition_group_2);
+
+ $query->addTag('serialization_test');
+
+ $query->getResults()->addWarning('This query is dumb.');
+
+ return $query;
+ }
+
+ /**
+ * Creates an item for testing purposes.
+ *
+ * @return \Drupal\search_api\Item\ItemInterface
+ * A test item.
+ */
+ protected function createTestItem() {
+ $index = $this->createTestIndex();
+ $datasource = \Drupal::getContainer()
+ ->get('search_api.plugin_helper')
+ ->createDatasourcePlugin($index, 'entity:user');
+ $item = new Item($index, 'id', $datasource);
+
+ $item->setBoost(2);
+ $item->setExcerpt('Foo bar baz');
+ $item->setExtraData('foo', (object) ['bar' => 1]);
+ $item->setExtraData('test', 1);
+ $item->setLanguage('en');
+ $item->setScore(4);
+ $item->setFields([
+ 'test' => $this->createTestField(),
+ 'foo' => $this->createTestField('foo', 'entity:entity_test_mulrev_changed'),
+ ]);
+ $item->setFieldsExtracted(TRUE);
+
+ return $item;
+ }
+
+ /**
+ * Creates a field for testing purposes.
+ *
+ * @param string $id
+ * (optional) The field ID (and property path).
+ * @param string|null $datasource_id
+ * (optional) The field's datasource ID.
+ *
+ * @return \Drupal\search_api\Item\FieldInterface
+ * A test field.
+ */
+ protected function createTestField($id = 'test', $datasource_id = NULL) {
+ $index = $this->createTestIndex();
+
+ $field = new Field($index, $id);
+ $field->setDatasourceId($datasource_id);
+ $field->setPropertyPath($id);
+ $field->setLabel('Foo');
+ $field->setDescription('Bar');
+ $field->setType('float');
+ $field->setBoost(2);
+ $field->setIndexedLocked();
+ $field->setConfiguration([
+ 'foo' => 'bar',
+ 'test' => TRUE,
+ ]);
+ $field->setValues([1, 3, 5]);
+
+ return $field;
+ }
+
+ /**
+ * Creates an index entity object for testing purposes.
+ *
+ * @return \Drupal\search_api\Entity\Index
+ * A test index.
+ */
+ protected function createTestIndex() {
+ if (!$this->index) {
+ // As our test index, just use the one from the DB Defaults module.
+ $path = __DIR__ . '/../../../../modules/search_api_db/search_api_db_defaults/config/optional/search_api.index.default_index.yml';
+ $index_values = Yaml::decode(file_get_contents($path));
+ $index = new Index($index_values, 'search_api_index');
+ $this->index = $index;
+ }
+
+ return $this->index;
+ }
+
+ /**
+ * Sets a mock entity type manager to be able to load the test index.
+ */
+ protected function setMockIndexStorage() {
+ $index = $this->createTestIndex();
+
+ $storage = $this->getMock(EntityStorageInterface::class);
+ $storage->method('load')->willReturnMap([
+ [$index->id(), $index],
+ ]);
+ $entity_type_manager = $this->getMock(EntityTypeManagerInterface::class);
+ $entity_type_manager->method('getStorage')->willReturnMap([
+ ['search_api_index', $storage],
+ ]);
+ \Drupal::getContainer()->set('entity_type.manager', $entity_type_manager);
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Kernel/System/TaskTest.php b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/System/TaskTest.php
new file mode 100644
index 000000000..ef24eb0d1
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/System/TaskTest.php
@@ -0,0 +1,291 @@
+installEntitySchema('search_api_task');
+
+ $this->taskManager = $this->container->get('search_api.task_manager');
+ $this->taskWorker = $this->container->get('search_api_test_tasks.test_task_worker');
+
+ // Create a test server.
+ $this->server = Server::create([
+ 'name' => 'Test Server',
+ 'id' => 'test_server',
+ 'status' => 1,
+ 'backend' => 'search_api_test',
+ ]);
+ $this->server->save();
+
+ // Create a test index.
+ $this->index = Index::create([
+ 'name' => 'Test index',
+ 'id' => 'test_index',
+ 'status' => 0,
+ 'datasource_settings' => [
+ 'entity:user' => [],
+ ],
+ 'tracker_settings' => [
+ 'default' => [],
+ ],
+ ]);
+ $this->index->save();
+ }
+
+ /**
+ * Tests successful task execution.
+ */
+ public function testTaskSuccess() {
+ $task = $this->addTask('success');
+ $this->assertEquals(1, $this->taskManager->getTasksCount());
+ $this->taskManager->executeSingleTask();
+ $this->assertEquals(0, $this->taskManager->getTasksCount());
+ $this->assertEquals($task->toArray(), $this->taskWorker->getEventLog()[0]);
+ }
+
+ /**
+ * Tests failed task execution.
+ */
+ public function testTaskFail() {
+ $task = $this->addTask('fail', $this->server);
+ $this->assertEquals(1, $this->taskManager->getTasksCount());
+ try {
+ $this->taskManager->executeAllTasks([
+ 'server_id' => $this->server->id(),
+ ]);
+ $this->fail('Exception expected');
+ }
+ catch (SearchApiException $e) {
+ $this->assertEquals('fail', $e->getMessage());
+ }
+ $this->assertEquals(1, $this->taskManager->getTasksCount());
+ $this->assertEquals($task->toArray(), $this->taskWorker->getEventLog()[0]);
+ }
+
+ /**
+ * Tests ignored task execution.
+ */
+ public function testTaskIgnored() {
+ $task = $this->addTask('ignore', NULL, $this->index, 'foobar');
+ $type = $task->getType();
+ $this->assertEquals(1, $this->taskManager->getTasksCount());
+ try {
+ $this->taskManager->executeAllTasks([
+ 'type' => [$type, 'unknown'],
+ 'index_id' => $this->index->id(),
+ ]);
+ $this->fail('Exception expected');
+ }
+ catch (SearchApiException $e) {
+ $id = $task->id();
+ $this->assertEquals("Could not execute task #$id of type '$type'. Type seems to be unknown.", $e->getMessage());
+ }
+ $this->assertEquals(1, $this->taskManager->getTasksCount());
+ $this->assertEquals($task->toArray(), $this->taskWorker->getEventLog()[0]);
+ }
+
+ /**
+ * Tests unknown task execution.
+ */
+ public function testTaskUnknown() {
+ $task = $this->addTask('unknown');
+ $this->assertEquals(1, $this->taskManager->getTasksCount());
+ try {
+ $this->taskManager->executeAllTasks();
+ $this->fail('Exception expected');
+ }
+ catch (SearchApiException $e) {
+ $id = $task->id();
+ $type = $task->getType();
+ $this->assertEquals("Could not execute task #$id of type '$type'. Type seems to be unknown.", $e->getMessage());
+ }
+ $this->assertEquals(1, $this->taskManager->getTasksCount());
+ $this->assertEquals([], $this->taskWorker->getEventLog());
+ }
+
+ /**
+ * Tests that multiple pending tasks are treated correctly.
+ */
+ public function testMultipleTasks() {
+ // Add some tasks to the system. We use explicit indexes since we want to
+ // verify that the tasks are executed in a different order than the one they
+ // were added, if appropriate $conditions parameters are given.
+ $tasks = [];
+ $tasks[0] = $this->addTask('success', $this->server, $this->index, ['foo' => 1, 'bar']);
+ $tasks[6] = $this->addTask('fail');
+ $tasks[1] = $this->addTask('success', $this->server, NULL, TRUE);
+ $tasks[4] = $this->addTask('success', NULL, NULL, 1);
+ $tasks[2] = $this->addTask('fail', $this->server, $this->index);
+ $tasks[5] = $this->addTask('success');
+ $tasks[3] = $this->addTask('success', NULL, $this->index);
+
+ $num = count($tasks);
+ $this->assertEquals($num, $this->taskManager->getTasksCount());
+
+ $this->taskManager->executeSingleTask();
+ $this->assertEquals(--$num, $this->taskManager->getTasksCount());
+
+ $this->taskManager->executeSingleTask([
+ 'server_id' => $this->server->id(),
+ ]);
+ $this->assertEquals(--$num, $this->taskManager->getTasksCount());
+
+ try {
+ $this->taskManager->executeAllTasks([
+ 'server_id' => $this->server->id(),
+ ]);
+ $this->fail('Exception expected');
+ }
+ catch (SearchApiException $e) {
+ $this->assertEquals('fail', $e->getMessage());
+ }
+ $this->assertEquals($num, $this->taskManager->getTasksCount());
+
+ $tasks[2]->delete();
+ $this->assertEquals(--$num, $this->taskManager->getTasksCount());
+
+ $this->taskManager->executeSingleTask([
+ 'index_id' => $this->index->id(),
+ ]);
+ $this->assertEquals(--$num, $this->taskManager->getTasksCount());
+
+ $this->taskManager->executeAllTasks([
+ 'type' => ['search_api_test_tasks.success', 'foobar'],
+ ]);
+ $this->assertEquals($num -= 2, $this->taskManager->getTasksCount());
+
+ $tasks[7] = $this->addTask('success');
+ $tasks[8] = $this->addTask('success');
+ $tasks[9] = $this->addTask('fail');
+ $tasks[10] = $this->addTask('success');
+ $num += 4;
+
+ try {
+ $this->taskManager->executeAllTasks();
+ $this->fail('Exception expected');
+ }
+ catch (SearchApiException $e) {
+ $this->assertEquals('fail', $e->getMessage());
+ }
+ $this->assertEquals($num, $this->taskManager->getTasksCount());
+
+ $tasks[6]->delete();
+ $this->assertEquals(--$num, $this->taskManager->getTasksCount());
+
+ try {
+ $this->taskManager->executeAllTasks();
+ $this->fail('Exception expected');
+ }
+ catch (SearchApiException $e) {
+ $this->assertEquals('fail', $e->getMessage());
+ }
+ $this->assertEquals($num -= 2, $this->taskManager->getTasksCount());
+
+ $tasks[9]->delete();
+ $this->assertEquals(--$num, $this->taskManager->getTasksCount());
+
+ $this->taskManager->executeAllTasks();
+ $this->assertEquals(0, $this->taskManager->getTasksCount());
+
+ $to_array = function (TaskInterface $task) {
+ return $task->toArray();
+ };
+ $tasks = array_map($to_array, $tasks);
+ $this->assertEquals($tasks, $this->taskWorker->getEventLog());
+ }
+
+ /**
+ * Adds a new pending task.
+ *
+ * @param string $type
+ * The type of task, without "search_api_test_tasks." prefix.
+ * @param \Drupal\search_api\ServerInterface|null $server
+ * (optional) The search server associated with the task, if any.
+ * @param \Drupal\search_api\IndexInterface|null $index
+ * (optional) The search index associated with the task, if any.
+ * @param mixed|null $data
+ * (optional) Additional, type-specific data to save with the task.
+ *
+ * @return \Drupal\search_api\Task\TaskInterface
+ * The new task.
+ */
+ protected function addTask($type, ServerInterface $server = NULL, IndexInterface $index = NULL, $data = NULL) {
+ $type = "search_api_test_tasks.$type";
+ $count_before = $this->taskManager->getTasksCount();
+ $conditions = [
+ 'type' => $type,
+ 'server_id' => $server ? $server->id() : NULL,
+ 'index_id' => $index ? $index->id() : NULL,
+ ];
+ $conditions = array_filter($conditions);
+ $count_before_conditions = $this->taskManager->getTasksCount($conditions);
+
+ $task = $this->taskManager->addTask($type, $server, $index, $data);
+
+ $count_after = $this->taskManager->getTasksCount();
+ $this->assertEquals($count_before + 1, $count_after);
+ $count_after_conditions = $this->taskManager->getTasksCount($conditions);
+ $this->assertEquals($count_before_conditions + 1, $count_after_conditions);
+
+ return $task;
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Kernel/TestTimeService.php b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/TestTimeService.php
new file mode 100644
index 000000000..0b7d640f4
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/TestTimeService.php
@@ -0,0 +1,49 @@
+requestTime = time();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getRequestTime() {
+ return $this->requestTime;
+ }
+
+ /**
+ * Advances the reported request time.
+ *
+ * @param int $seconds
+ * (optional) Number of seconds by which to advance the reported request
+ * time.
+ *
+ * @return $this
+ */
+ public function advanceTime($seconds = 1) {
+ $this->requestTime += $seconds;
+ return $this;
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Views/TestMemoryBackend.php b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Views/TestMemoryBackend.php
new file mode 100644
index 000000000..3b624cc9b
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Views/TestMemoryBackend.php
@@ -0,0 +1,36 @@
+requestTime ?: parent::getRequestTime();
+ }
+
+ /**
+ * Sets the request time.
+ *
+ * @param int $time
+ * The request time to set.
+ */
+ public function setRequestTime($time) {
+ $this->requestTime = $time;
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Views/ViewsDisplayCachingTest.php b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Views/ViewsDisplayCachingTest.php
new file mode 100644
index 000000000..a0aa28650
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Views/ViewsDisplayCachingTest.php
@@ -0,0 +1,377 @@
+installSchema('search_api', ['search_api_item']);
+
+ $this->installEntitySchema('entity_test_mulrev_changed');
+ $this->installEntitySchema('search_api_task');
+
+ $this->installConfig([
+ 'search_api',
+ 'search_api_test_example_content',
+ 'search_api_test_db',
+ 'search_api_test_views',
+ ]);
+
+ $this->entityTypeManager = $this->container->get('entity_type.manager');
+ $this->viewExecutableFactory = $this->container->get('views.executable');
+
+ // Use the test search index from the search_api_test_db module.
+ $this->index = Index::load('database_search_index');
+
+ // Use a test cache backend that allows to tamper with the request time so
+ // we can test time based caching.
+ $this->cache = new TestMemoryBackend();
+ $this->container->set('cache.data', $this->cache);
+
+ // Create some demo content and index it.
+ $this->createDemoContent();
+ $this->index->indexItems();
+ }
+
+ /**
+ * Tests whether the search display plugin for a new view is available.
+ *
+ * @param string $display_id
+ * The tested Views display's ID.
+ * @param string[] $expected_cache_tags
+ * The expected cache tags for the executed view.
+ * @param string[] $expected_cache_contexts
+ * The expected cache contexts for the executed view.
+ * @param int $expected_max_age
+ * The expected max cache age for the executed view.
+ * @param bool $expected_results_cache
+ * TRUE if the results cache is expected to be populated after executing the
+ * view, FALSE otherwise.
+ *
+ * @dataProvider displayCacheabilityProvider
+ */
+ public function testDisplayCacheability($display_id, array $expected_cache_tags, array $expected_cache_contexts, $expected_max_age, $expected_results_cache) {
+ $view = $this->getView('search_api_test_cache', $display_id);
+
+ // Before the search is executed, the query should not be cached.
+ $this->assertViewsResultsCacheNotPopulated($view);
+
+ // Execute the search and assert the cacheability metadata.
+ $this->assertViewsCacheability($view, $expected_cache_tags, $expected_cache_contexts, $expected_max_age);
+
+ // AssertViewsCache() destroys the view, get a fresh copy to continue the
+ // test.
+ $view = $this->getView('search_api_test_cache', $display_id);
+
+ // The query has been executed. The query should now be cached if the test
+ // case expects it.
+ if ($expected_results_cache) {
+ $this->assertViewsResultsCachePopulated($view);
+
+ // Trigger the event that would cause a cache invalidation for the plugin
+ // under test. Now the query result should not be cached any more.
+ $this->triggerInvalidation($display_id);
+ }
+
+ $this->assertViewsResultsCacheNotPopulated($view);
+ }
+
+ /**
+ * Checks that the query result of the given view is not currently cached.
+ *
+ * @param \Drupal\views\ViewExecutable $view
+ * The view to check.
+ */
+ protected function assertViewsResultsCacheNotPopulated(ViewExecutable $view) {
+ $this->assertEmpty($this->getResultsCache($view));
+ }
+
+ /**
+ * Checks that the query result of the given view is currently cached.
+ *
+ * @param \Drupal\views\ViewExecutable $view
+ * The view to check.
+ */
+ protected function assertViewsResultsCachePopulated(ViewExecutable $view) {
+ $this->assertNotEmpty($this->getResultsCache($view));
+ }
+
+ /**
+ * Returns the current query result cache of the given view.
+ *
+ * @param \Drupal\views\ViewExecutable $view
+ * The view for which to return the query result cache.
+ *
+ * @return false|object
+ * The cache object, or FALSE if the query cache is not populated.
+ */
+ protected function getResultsCache(ViewExecutable $view) {
+ /** @var \Drupal\views\Plugin\views\cache\CachePluginBase $cache_plugin */
+ $cache_plugin = $view->display_handler->getPlugin('cache');
+
+ // Ensure that the views query is built.
+ $view->build();
+ return $this->cache->get($cache_plugin->generateResultsKey());
+ }
+
+ /**
+ * Checks the cacheability metadata of the given view.
+ *
+ * @param \Drupal\views\ViewExecutable $view
+ * The view to test.
+ * @param string[] $expected_cache_tags
+ * The expected cache tags.
+ * @param string[] $expected_cache_contexts
+ * The expected cache contexts.
+ * @param int $expected_max_age
+ * The expected cache max age.
+ */
+ protected function assertViewsCacheability(ViewExecutable $view, array $expected_cache_tags, array $expected_cache_contexts, $expected_max_age) {
+ $build = $this->assertViewsCacheTags($view, NULL, FALSE, $expected_cache_tags);
+ $this->assertCacheContexts($expected_cache_contexts, $build);
+ $this->assertCacheMaxAge($expected_max_age, $build);
+ }
+
+ /**
+ * Triggers the event that should provoke a cache invalidation.
+ *
+ * @param string $plugin_type
+ * The views cache plugin type for which a cache invalidation should be
+ * triggered. Can be 'none', 'tag' or 'time'.
+ */
+ protected function triggerInvalidation($plugin_type) {
+ switch ($plugin_type) {
+ // When using the 'tag' based caching strategy, create and index a new
+ // entity of the type that is used in the index. This should clear it.
+ case 'tag':
+ EntityTestMulRevChanged::create(['name' => 'Tomahawk'])->save();
+ $this->index->indexItems();
+ break;
+
+ // When using 'time' based caching, pretend to be more than 1 hour in the
+ // future.
+ case 'time':
+ $this->cache->setRequestTime($this->cache->getRequestTime() + 3700);
+ break;
+ }
+ }
+
+ /**
+ * Creates some test axes.
+ */
+ protected function createDemoContent() {
+ foreach (['Glaive', 'Halberd', 'Hurlbat'] as $name) {
+ EntityTestMulRevChanged::create(['name' => $name])->save();
+ }
+ }
+
+ /**
+ * Loads a view from configuration and returns its executable object.
+ *
+ * @param string $id
+ * The view ID to load.
+ * @param string $display_id
+ * The display ID to set.
+ *
+ * @return \Drupal\views\ViewExecutable
+ * A view executable instance, from the loaded entity.
+ */
+ protected function getView($id, $display_id) {
+ /** @var \Drupal\views\ViewEntityInterface $view */
+ $view = $this->entityTypeManager->getStorage('view')->load($id);
+ $executable = $this->viewExecutableFactory->get($view);
+ $executable->setDisplay($display_id);
+ $executable->setExposedInput(['search_api_fulltext' => 'Glaive']);
+ return $executable;
+ }
+
+ /**
+ * Checks if the cache contexts in the given render array are as expected.
+ *
+ * @param string[] $expected_contexts
+ * An array of cache contexts that are expected to be present in the given
+ * render array.
+ * @param array $render_array
+ * The render array of which to check the cache contexts.
+ */
+ protected function assertCacheContexts(array $expected_contexts, array $render_array) {
+ // Merge in the default cache contexts in the expected contexts.
+ $default_contexts = [
+ // Default contexts that are always provided by core.
+ 'languages:' . LanguageInterface::TYPE_INTERFACE,
+ 'theme',
+ 'user.permissions',
+ // We are showing translatable content, so it varies by content language.
+ 'languages:' . LanguageInterface::TYPE_CONTENT,
+ // Our view has a pager, so we vary by query arguments.
+ // @see \Drupal\views\Plugin\views\pager\SqlBase::getCacheContexts()
+ 'url.query_args',
+ // Since our view has exposed filters, it varies by url.
+ // @see \Drupal\views\Plugin\views\exposed_form\ExposedFormPluginBase::getCacheContexts()
+ 'url',
+ ];
+
+ $expected_contexts = Cache::mergeContexts($expected_contexts, $default_contexts);
+ $actual_contexts = CacheableMetadata::createFromRenderArray($render_array)->getCacheContexts();
+
+ sort($expected_contexts);
+ sort($actual_contexts);
+
+ $this->assertEquals($expected_contexts, $actual_contexts);
+ }
+
+ /**
+ * Checks if the cache max-age in the given render array is as expected.
+ *
+ * @param int $expected_max_age
+ * The max-age value that is expected to be present in the given render
+ * array.
+ * @param array $render_array
+ * The render array of which to check the cache max-age.
+ */
+ protected function assertCacheMaxAge($expected_max_age, array $render_array) {
+ $actual_max_age = CacheableMetadata::createFromRenderArray($render_array)->getCacheMaxAge();
+ $this->assertEquals($expected_max_age, $actual_max_age);
+ }
+
+ /**
+ * Data provider for testDisplayCacheability().
+ *
+ * @return array
+ * Array of method call argument arrays for testDisplayCacheability().
+ *
+ * @see ::testDisplayCacheability
+ */
+ public function displayCacheabilityProvider() {
+ return [
+ // First test case, using the 'none' caching plugin that is included with
+ // Views. This is expected to disable caching.
+ [
+ 'none',
+ // It is expected that only the configuration of the view itself is
+ // available as a cache tag.
+ [
+ 'config:views.view.search_api_test_cache',
+ ],
+ // No specific cache contexts are expected to be present.
+ [],
+ // It is expected that the cache max-age is set to zero, effectively
+ // disabling the cache.
+ 0,
+ // It is expected that no results are cached.
+ FALSE,
+ ],
+
+ // Test case using cache tags based caching. This should provide relevant
+ // cache tags so that the results can be cached permanently, but be
+ // invalidated whenever relevant changes occur.
+ [
+ 'tag',
+ [
+ // It is expected that the configuration of the view itself is
+ // available as a cache tag, so that the caches are invalidated if the
+ // view configuration changes.
+ 'config:views.view.search_api_test_cache',
+ // Caches should also be invalidated if any items on the index are
+ // indexed or deleted.
+ 'search_api_list:database_search_index',
+ ],
+ // No specific cache contexts are expected to be present.
+ [],
+ // For tag based caching it is expected that the cache life time is set
+ // to permanent.
+ Cache::PERMANENT,
+ // It is expected that views results can be cached.
+ TRUE,
+ ],
+
+ // Test case using time based caching. This should invalidate the caches
+ // after a predefined time period.
+ [
+ 'time',
+ [
+ // It is expected that the configuration of the view itself is
+ // available as a cache tag, so that the caches are invalidated if the
+ // view configuration changes. No other tags should be available.
+ 'config:views.view.search_api_test_cache',
+ ],
+ // No specific cache contexts are expected to be present.
+ [],
+ // It is expected that the cache max-age is set to 1 hour.
+ 3600,
+ // It is expected that views results can be cached.
+ TRUE,
+ ],
+ ];
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Views/ViewsDisplayTest.php b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Views/ViewsDisplayTest.php
new file mode 100644
index 000000000..d76f747ad
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Views/ViewsDisplayTest.php
@@ -0,0 +1,98 @@
+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_test_example_content',
+ 'search_api_test_db',
+ ]);
+ }
+
+ /**
+ * Tests whether the search display plugin for a new view is available.
+ */
+ public function testViewsPageDisplayPluginAvailable() {
+ // Retrieve the display plugins once first, to fill the cache.
+ $displays = $this->container
+ ->get('plugin.manager.search_api.display')
+ ->getDefinitions();
+ $this->assertArrayNotHasKey('views_page:search_api_test_view__page_1', $displays);
+
+ // Then, install our test view and see whether its search display becomes
+ // available right away, without manually clearing the cache first.
+ $this->installConfig('search_api_test_views');
+ $displays = $this->container
+ ->get('plugin.manager.search_api.display')
+ ->getDefinitions();
+ $this->assertArrayHasKey('views_page:search_api_test_view__page_1', $displays);
+ }
+
+ /**
+ * Tests the dependency information on the display.
+ */
+ public function testDependencyInfo() {
+ $this->installConfig('search_api_test_views');
+
+ /** @var \Drupal\search_api\Display\DisplayInterface $display */
+ $display = $this->container
+ ->get('plugin.manager.search_api.display')
+ ->createInstance('views_page:search_api_test_view__page_1');
+
+ $this->assertEquals('views_page:search_api_test_view__page_1', $display->getPluginId());
+
+ $dependencies = $display->calculateDependencies();
+ $this->assertArrayHasKey('module', $dependencies);
+ $this->assertArrayHasKey('config', $dependencies);
+ $this->assertContains('search_api', $dependencies['module']);
+ $this->assertContains('search_api.index.database_search_index', $dependencies['config']);
+ $this->assertContains('views.view.search_api_test_view', $dependencies['config']);
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Views/ViewsFieldTraitTest.php b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Views/ViewsFieldTraitTest.php
new file mode 100644
index 000000000..f3cdcae6f
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Views/ViewsFieldTraitTest.php
@@ -0,0 +1,249 @@
+installEntitySchema('entity_test_mulrev_changed');
+ $this->installEntitySchema('user');
+
+ $this->installConfig([
+ 'search_api_test_example_content',
+ 'user',
+ ]);
+
+ for ($i = 1; $i <= 3; ++$i) {
+ $this->users[$i] = User::create([
+ 'uid' => $i,
+ 'name' => "User $i",
+ ]);
+ $this->users[$i]->save();
+
+ $this->entities[$i] = EntityTestMulRevChanged::create([
+ 'id' => $i,
+ 'user_id' => $i,
+ 'name' => "Test entity $i name",
+ 'body' => "Test entity $i body",
+ ]);
+ $this->entities[$i]->save();
+ }
+
+ // Create the test index, but don't save it. We don't need it saved for this
+ // test and saving it would need more time and additional code (e.g., would
+ // track all the entities we create).
+ $datasource_id = 'entity:entity_test_mulrev_changed';
+ $this->index = Index::create([
+ 'field_settings' => [
+ 'aggregated_field' => [
+ 'label' => 'Aggregated field',
+ 'property_path' => 'aggregated_field',
+ 'type' => 'text',
+ 'configuration' => [
+ 'type' => 'union',
+ 'fields' => [
+ Utility::createCombinedId($datasource_id, 'name'),
+ Utility::createCombinedId($datasource_id, 'body'),
+ Utility::createCombinedId('entity:user', 'name'),
+ ],
+ ],
+ ],
+ ],
+ 'datasource_settings' => [
+ $datasource_id => [],
+ 'entity:user' => [],
+ ],
+ 'processor_settings' => [
+ 'aggregated_field' => [],
+ ],
+ ]);
+
+ $this->field = new ViewsTestField([], 'search_api', []);
+ /** @var \Drupal\search_api\Plugin\views\query\SearchApiQuery|\PHPUnit_Framework_MockObject_MockObject $query */
+ $query = $this->getMockBuilder(SearchApiQuery::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $query->method('getIndex')->willReturn($this->index);
+ $query->method('getAccessAccount')->willReturn($this->users[1]);
+ $this->field->setQuery($query);
+ }
+
+ /**
+ * Tests that entity loading in the pre-render phase works correctly.
+ *
+ * @covers ::preRender
+ */
+ public function testPreRender() {
+ $datasource_id = 'entity:entity_test_mulrev_changed';
+ $entity_name = Utility::createCombinedId($datasource_id, 'name');
+ $entity_body = Utility::createCombinedId($datasource_id, 'body');
+ $entity_user_name = Utility::createCombinedId($datasource_id, 'user_id:entity:name');
+ $user_name = Utility::createCombinedId('entity:user', 'name');
+
+ /** @var \Drupal\views\ResultRow[] $values */
+ $values = [];
+
+ $item_id = Utility::createCombinedId($datasource_id, '1:en');
+ $item = new Item($this->index, $item_id);
+ $item->setOriginalObject($this->entities[1]->getTypedData());
+ $values[] = new ResultRow([
+ '_item' => $item,
+ '_object' => $item->getOriginalObject(),
+ '_relationship_objects' => [
+ NULL => [$item->getOriginalObject()],
+ ],
+ ]);
+ $item_id = Utility::createCombinedId($datasource_id, '2:en');
+ $values[] = new ResultRow([
+ '_item' => new Item($this->index, $item_id),
+ $entity_name => [
+ 'Fake value',
+ ],
+ $entity_user_name => [
+ 'Other fake value',
+ ],
+ ]);
+ $item_id = Utility::createCombinedId('entity:user', '3:en');
+ $values[] = new ResultRow([
+ '_item' => new Item($this->index, $item_id),
+ ]);
+ $item_id = Utility::createCombinedId($datasource_id, '3:en');
+ $item = new Item($this->index, $item_id);
+ $values[] = new ResultRow([
+ '_item' => $item,
+ ]);
+
+ // Set some other required properties which can easily be set automatically.
+ foreach ($values as $row) {
+ $row->search_api_id = $row->_item->getId();
+ $row->search_api_datasource = $row->_item->getDatasourceId();
+ $row->search_api_language = 'en';
+ }
+
+ $this->field->addRetrievedProperty($entity_name);
+ $this->field->addRetrievedProperty("$entity_body:_object");
+ $this->field->addRetrievedProperty($entity_user_name);
+ $this->field->addRetrievedProperty($user_name);
+ $this->field->addRetrievedProperty('aggregated_field');
+
+ $this->field->preRender($values);
+
+ $this->assertObjectHasAttribute($entity_name, $values[0]);
+ $this->assertEquals(['Test entity 1 name'], $values[0]->$entity_name);
+ $this->assertObjectHasAttribute("$entity_body:_object", $values[0]);
+ $this->assertCount(1, $values[0]->{"$entity_body:_object"});
+ $this->assertInstanceOf(TypedDataInterface::class, $values[0]->{"$entity_body:_object"}[0]);
+ $this->assertArrayHasKey($entity_body, $values[0]->_relationship_objects);
+ $this->assertNotEmpty($values[0]->_relationship_objects[$entity_body]);
+ $this->assertObjectHasAttribute($entity_user_name, $values[0]);
+ $this->assertEquals(['User 1'], $values[0]->$entity_user_name);
+ $this->assertTrue(empty($values[0]->$user_name), 'User name should not be extracted for non-user entity.');
+ $this->assertObjectHasAttribute('aggregated_field', $values[0]);
+ $this->assertContains('Test entity 1 name', $values[0]->aggregated_field);
+ $this->assertContains('Test entity 1 body', $values[0]->aggregated_field);
+
+ $this->assertObjectHasAttribute($entity_name, $values[1]);
+ $this->assertEquals(['Fake value'], $values[1]->$entity_name);
+ $this->assertArrayHasKey($entity_body, $values[1]->_relationship_objects);
+ $this->assertNotEmpty($values[1]->_relationship_objects[$entity_body]);
+ $this->assertObjectHasAttribute($entity_user_name, $values[1]);
+ $this->assertEquals(['Other fake value'], $values[1]->$entity_user_name);
+ $this->assertTrue(empty($values[1]->$user_name), 'User name should not be extracted for non-user entity.');
+ $this->assertObjectHasAttribute('aggregated_field', $values[1]);
+ $this->assertContains('Test entity 2 name', $values[1]->aggregated_field);
+ $this->assertContains('Test entity 2 body', $values[1]->aggregated_field);
+
+ // Since we provided the values on the row, most relationship objects should
+ // not have been loaded.
+ $this->assertArrayNotHasKey($entity_name, $values[1]->_relationship_objects);
+ $this->assertArrayNotHasKey(Utility::createCombinedId($datasource_id, 'user_id'), $values[1]->_relationship_objects);
+ $this->assertArrayNotHasKey(Utility::createCombinedId($datasource_id, 'user_id:entity'), $values[1]->_relationship_objects);
+ $this->assertArrayNotHasKey($entity_user_name, $values[1]->_relationship_objects);
+
+ $this->assertObjectHasAttribute($user_name, $values[2]);
+ $this->assertEquals(['User 3'], $values[2]->$user_name);
+ $this->assertTrue(empty($values[2]->$entity_name), 'Test entity name should not be extracted for user entity.');
+ $this->assertTrue(empty($values[2]->$entity_user_name), 'Test entity author name should not be extracted for user entity.');
+ $this->assertObjectHasAttribute('aggregated_field', $values[2]);
+ $this->assertEquals(['User 3'], $values[2]->aggregated_field);
+
+ $this->assertObjectHasAttribute($entity_name, $values[3]);
+ $this->assertEquals(['Test entity 3 name'], $values[3]->$entity_name);
+ $this->assertObjectHasAttribute("$entity_body:_object", $values[3]);
+ $this->assertCount(1, $values[3]->{"$entity_body:_object"});
+ $this->assertInstanceOf(TypedDataInterface::class, $values[3]->{"$entity_body:_object"}[0]);
+ $this->assertArrayHasKey($entity_body, $values[3]->_relationship_objects);
+ $this->assertNotEmpty($values[3]->_relationship_objects[$entity_body]);
+ $this->assertObjectHasAttribute($entity_user_name, $values[3]);
+ $this->assertEquals(['User 3'], $values[3]->$entity_user_name);
+ $this->assertTrue(empty($values[3]->$user_name), 'User name should not be extracted for non-user entity.');
+ $this->assertObjectHasAttribute('aggregated_field', $values[3]);
+ $this->assertContains('Test entity 3 name', $values[3]->aggregated_field);
+ $this->assertContains('Test entity 3 body', $values[3]->aggregated_field);
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Views/ViewsPropertyExtractionTest.php b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Views/ViewsPropertyExtractionTest.php
new file mode 100644
index 000000000..8e167457f
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/Views/ViewsPropertyExtractionTest.php
@@ -0,0 +1,313 @@
+getMock(IndexInterface::class);
+ $property2 = $this->getMock(ConfigurablePropertyInterface::class);
+ $property2->method('getProcessorId')->willReturn('processor2');
+ $property2->method('getDataType')->willReturn('string');
+ $property2->method('defaultConfiguration')->willReturn([]);
+ $property2->method('getClass')->willReturn(StringData::class);
+ $index->method('getPropertyDefinitions')->willReturnMap([
+ [
+ NULL,
+ [
+ 'property1' => new ProcessorProperty([
+ 'processor_id' => 'processor1',
+ ]),
+ ],
+ ],
+ [
+ $datasource_id,
+ [
+ 'property2' => $property2,
+ ],
+ ],
+ ]);
+ $generate_add_field_values = function ($value) {
+ return function (ItemInterface $item) use ($value) {
+ foreach ($item->getFields() as $field) {
+ $values = (array) $value;
+ $config = $field->getConfiguration();
+ if (is_scalar($value) && !empty($config[$value])) {
+ $values = (array) $config[$value];
+ }
+ $field->setValues($values);
+ }
+ };
+ };
+ $value1 = $processor_property_value ?: 'Processor 1';
+ $processor1 = $this->getMock(ProcessorInterface::class);
+ $processor1->method('addFieldValues')
+ ->willReturnCallback($generate_add_field_values($value1));
+ $value2 = $processor_property_value ?: 'Processor 2';
+ $processor2 = $this->getMock(ProcessorInterface::class);
+ $processor2->method('addFieldValues')
+ ->willReturnCallback($generate_add_field_values($value2));
+ $index->method('getProcessor')->willReturnMap([
+ ['processor1', $processor1],
+ ['processor2', $processor2],
+ ]);
+ $fields_helper = $this->container->get('search_api.fields_helper');
+ $property_path_split = Utility::splitCombinedId($property_path);
+ $fields = [];
+ if ($return_fields) {
+ $fields = [
+ 'foo' => $fields_helper->createField($index, 'foo', [
+ 'datasource_id' => $property_path_split[0],
+ 'property_path' => $property_path_split[1],
+ 'configuration' => [
+ 'Processor 2' => 'foobar',
+ ],
+ ]),
+ 'test' => $fields_helper->createField($index, 'test', [
+ 'datasource_id' => $property_path_split[0],
+ 'property_path' => $property_path_split[1],
+ 'configuration' => [
+ 'Processor 2' => 'Override',
+ ],
+ ]),
+ ];
+ }
+ $index->method('getFields')->willReturn($fields);
+
+ $query = $this->getMockBuilder(SearchApiQuery::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $query->method('getIndex')->willReturn($index);
+
+ /** @var \Drupal\views\ViewExecutable $view */
+ $view = $this->getMockBuilder(ViewExecutable::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $view->query = $query;
+
+ /** @var \Drupal\views\Plugin\views\display\DisplayPluginBase $display */
+ $display = $this->getMockBuilder(DisplayPluginBase::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $configuration = [
+ 'real field' => $property_path,
+ 'search_api field' => 'test',
+ ];
+ $field = new SearchApiStandard($configuration, '', []);
+ $options = [
+ 'link_to_item' => TRUE,
+ 'use_highlighting' => TRUE,
+ ];
+ $field->init($view, $display, $options);
+ $field->query();
+
+ $user = User::create([
+ 'name' => 'Test user',
+ ]);
+ $object = $user->getTypedData();
+ $id = Utility::createCombinedId($datasource_id, $user->id());
+ $item = $fields_helper->createItemFromObject($index, $object, $id);
+ $row = new ResultRow([
+ '_item' => $item,
+ '_object' => $object,
+ '_relationship_objects' => [
+ NULL => [$object],
+ ],
+ 'search_api_datasource' => $datasource_id,
+ ]);
+ if ($pre_set) {
+ $row->$property_path = ['Pre-set'];
+ }
+ // For the configurable property, also set the values for the special
+ // property path.
+ if ($property_path === 'entity:user/property2') {
+ $special_path = "$property_path|test";
+ if ($pre_set) {
+ $row->$special_path = ['Pre-set'];
+ }
+ // Whether that special property path will actually be used by the field
+ // plugin depends on whether it finds a matching field.
+ if ($return_fields) {
+ $property_path = $special_path;
+ }
+ }
+ if ($set_highlighting) {
+ $item->setExtraData('highlighted_fields', [
+ 'test' => [
+ 'Highlighted value',
+ ],
+ ]);
+ }
+ $values = [$row];
+
+ $field->preRender($values);
+
+ $this->assertEquals((array) $expected, $row->$property_path);
+ }
+
+ /**
+ * Provides test data for the property extraction test.
+ *
+ * @return array[]
+ * Array of argument lists for testPropertyExtraction().
+ *
+ * @see \Drupal\Tests\search_api\Kernel\Views\ViewsPropertyExtractionTest::testPropertyExtraction()
+ */
+ public function propertyExtractionDataProvider() {
+ return [
+ 'extract normal property' => [
+ 'entity:user/name',
+ 'Test user',
+ ],
+ 'use normal property' => [
+ 'entity:user/name',
+ 'Pre-set',
+ TRUE,
+ ],
+ 'extract processor property' => [
+ 'property1',
+ 'Processor 1',
+ ],
+ 'use processor property' => [
+ 'property1',
+ 'Pre-set',
+ TRUE,
+ ],
+ 'extract configurable property' => [
+ 'entity:user/property2',
+ 'Override',
+ ],
+ 'use configurable property' => [
+ 'entity:user/property2',
+ 'Pre-set',
+ TRUE,
+ ],
+ 'use overridden configurable property' => [
+ 'entity:user/property2',
+ 'Processor 2',
+ FALSE,
+ FALSE,
+ ],
+ 'highlighted property 1' => [
+ 'entity:user/name',
+ 'Highlighted value',
+ FALSE,
+ TRUE,
+ TRUE,
+ ],
+ 'highlighted property 2' => [
+ 'entity:user/name',
+ 'Highlighted value',
+ TRUE,
+ TRUE,
+ TRUE,
+ ],
+ 'highlighted processor property 1' => [
+ 'property1',
+ 'Highlighted value',
+ FALSE,
+ TRUE,
+ TRUE,
+ ],
+ 'highlighted processor property 2' => [
+ 'property1',
+ 'Highlighted value',
+ TRUE,
+ TRUE,
+ TRUE,
+ ],
+ 'highlighted configurable property 1' => [
+ 'entity:user/property2',
+ 'Highlighted value',
+ FALSE,
+ TRUE,
+ TRUE,
+ ],
+ 'highlighted configurable property 2' => [
+ 'entity:user/property2',
+ 'Highlighted value',
+ TRUE,
+ TRUE,
+ TRUE,
+ ],
+ 'multi-value highlighting 1' => [
+ 'property1',
+ ['Highlighted value', 'Other value'],
+ FALSE,
+ TRUE,
+ TRUE,
+ ['Highlighted value', 'Other value'],
+ ],
+ 'multi-value highlighting 2' => [
+ 'property1',
+ ['Other value', 'Highlighted value'],
+ FALSE,
+ TRUE,
+ TRUE,
+ ['Other value', 'Highlighted value'],
+ ],
+ 'multi-value highlighting 3' => [
+ 'property1',
+ ['Other value', 'Highlighted value'],
+ FALSE,
+ TRUE,
+ TRUE,
+ ['Other value', 'Foobar'],
+ ],
+ ];
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Kernel/ViewsTestField.php b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/ViewsTestField.php
new file mode 100644
index 000000000..8af320f83
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Kernel/ViewsTestField.php
@@ -0,0 +1,55 @@
+query = $query;
+ return $this;
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Unit/BackendPluginBaseTest.php b/sites/all/modules/contrib/search/search_api/tests/src/Unit/BackendPluginBaseTest.php
new file mode 100644
index 000000000..459f54c9b
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Unit/BackendPluginBaseTest.php
@@ -0,0 +1,62 @@
+getMock(IndexInterface::class);
+ $index->method('getFulltextFields')->willReturn(['field1', 'field2']);
+
+ $query = $this->getMock(QueryInterface::class);
+ $query->method('getFulltextFields')->willReturn($query_fields);
+ $query->method('getIndex')->willReturn($index);
+
+ $backend = new TestBackend([], '', []);
+ $class = new \ReflectionClass(TestBackend::class);
+ $method = $class->getMethod('getQueryFulltextFields');
+ $method->setAccessible(TRUE);
+ $this->assertEquals($expected, $method->invokeArgs($backend, [$query]));
+ }
+
+ /**
+ * Provides test data for testGetQueryFulltextFields().
+ *
+ * @return array[]
+ * An array of argument arrays for testGetQueryFulltextFields().
+ */
+ public function getQueryFulltextFieldsDataProvider() {
+ return [
+ 'null fields' => [NULL, ['field1', 'field2']],
+ 'field1' => [['field1'], ['field1']],
+ 'field2' => [['field2'], ['field2']],
+ 'all fields' => [['field1', 'field2'], ['field1', 'field2']],
+ 'invalid fields' => [['field1', 'foo'], ['field1']],
+ ];
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Unit/ItemFieldTest.php b/sites/all/modules/contrib/search/search_api/tests/src/Unit/ItemFieldTest.php
new file mode 100644
index 000000000..0755eec97
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Unit/ItemFieldTest.php
@@ -0,0 +1,80 @@
+getMock('Drupal\search_api\DataType\DataTypeInterface');
+ $data_type->expects($this->any())
+ ->method('getValue')
+ ->willReturnCallback(function ($v) {
+ return "*$v";
+ });
+
+ /** @var \Drupal\search_api\DataType\DataTypePluginManager|\PHPUnit_Framework_MockObject_MockObject $data_type_manager */
+ $data_type_manager = $this->getMockBuilder('Drupal\search_api\DataType\DataTypePluginManager')
+ ->disableOriginalConstructor()
+ ->getMock();
+ $data_type_manager->expects($this->any())
+ ->method('hasDefinition')
+ ->willReturn(TRUE);
+ $data_type_manager->expects($this->any())
+ ->method('createInstance')
+ ->willReturn($data_type);
+
+ $index = new Index([], 'search_api_index');
+
+ $this->field = new Field($index, 'field');
+ $this->field->setDataTypeManager($data_type_manager);
+ }
+
+ /**
+ * Tests setting the Values.
+ *
+ * @covers ::setValues
+ */
+ public function testSetValues() {
+ $values = ['*foo', '*bar'];
+ $this->field->setValues($values);
+ $this->assertEquals($values, $this->field->getValues());
+ }
+
+ /**
+ * Tests adding a value.
+ *
+ * Ensures that a string passed to addValue() is processed by the data type
+ * plugin.
+ *
+ * @covers ::addValue
+ */
+ public function testAddValue() {
+ $this->field->setValues(['*foo']);
+ $this->field->addValue('bar');
+ $this->assertEquals(['*foo', '*bar'], $this->field->getValues());
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Unit/LocalTasksTest.php b/sites/all/modules/contrib/search/search_api/tests/src/Unit/LocalTasksTest.php
new file mode 100644
index 000000000..95fa0ece6
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Unit/LocalTasksTest.php
@@ -0,0 +1,96 @@
+directoryList = ['search_api' => $module_path];
+ }
+
+ /**
+ * Tests whether the server's local tasks are present at the given route.
+ *
+ * @param string $route
+ * The route to test.
+ *
+ * @dataProvider getPageRoutesServer
+ */
+ public function testLocalTasksServer($route) {
+ $tasks = [
+ 0 => [
+ 'entity.search_api_server.canonical',
+ 'entity.search_api_server.edit_form',
+ ],
+ ];
+ $this->assertLocalTasks($route, $tasks);
+ }
+
+ /**
+ * Provides a list of routes to test.
+ *
+ * @return array[]
+ * An array containing arrays with the arguments for a
+ * testLocalTasksServer() call.
+ */
+ public function getPageRoutesServer() {
+ return [
+ ['entity.search_api_server.canonical'],
+ ['entity.search_api_server.edit_form'],
+ ];
+ }
+
+ /**
+ * Tests whether the index's local tasks are present at the given route.
+ *
+ * @param string $route
+ * The route to test.
+ *
+ * @dataProvider getPageRoutesIndex
+ */
+ public function testLocalTasksIndex($route) {
+ $tasks = [
+ 0 => [
+ 'entity.search_api_index.canonical',
+ 'entity.search_api_index.edit_form',
+ 'entity.search_api_index.fields',
+ 'entity.search_api_index.processors',
+ ],
+ ];
+ $this->assertLocalTasks($route, $tasks);
+ }
+
+ /**
+ * Provides a list of routes to test.
+ *
+ * @return array[]
+ * An array containing arrays with the arguments for a
+ * testLocalTasksIndex() call.
+ */
+ public function getPageRoutesIndex() {
+ return [
+ ['entity.search_api_index.canonical'],
+ ['entity.search_api_index.edit_form'],
+ ['entity.search_api_index.fields'],
+ ['entity.search_api_index.processors'],
+ ];
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Unit/Processor/AddURLTest.php b/sites/all/modules/contrib/search/search_api/tests/src/Unit/Processor/AddURLTest.php
new file mode 100644
index 000000000..59857c029
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Unit/Processor/AddURLTest.php
@@ -0,0 +1,138 @@
+setUpMockContainer();
+
+ // Create a mock for the URL to be returned.
+ $url = $this->getMockBuilder('Drupal\Core\Url')
+ ->disableOriginalConstructor()
+ ->getMock();
+ $url->expects($this->any())
+ ->method('toString')
+ ->will($this->returnValue('http://www.example.com/node/example'));
+
+ // Mock the data source of the indexer to return the mocked url object.
+ $datasource = $this->getMock('Drupal\search_api\Datasource\DatasourceInterface');
+ $datasource->expects($this->any())
+ ->method('getItemUrl')
+ ->withAnyParameters()
+ ->will($this->returnValue($url));
+
+ // Create a mock for the index to return the datasource mock.
+ /** @var \Drupal\search_api\IndexInterface $index */
+ $index = $this->index = $this->getMock('Drupal\search_api\IndexInterface');
+ $this->index->expects($this->any())
+ ->method('getDatasource')
+ ->with('entity:node')
+ ->will($this->returnValue($datasource));
+
+ // Create the tested processor and set the mocked indexer.
+ $this->processor = new AddURL([], 'add_url', []);
+ $this->processor->setIndex($index);
+ /** @var \Drupal\Core\StringTranslation\TranslationInterface $translation */
+ $translation = $this->getStringTranslationStub();
+ $this->processor->setStringTranslation($translation);
+ }
+
+ /**
+ * Tests whether the "URI" field is correctly filled by the processor.
+ */
+ public function testAddFieldValues() {
+ /** @var \Drupal\node\Entity\Node $node */
+ $node = $this->getMockBuilder('Drupal\node\Entity\Node')
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $body_value = ['Some text value'];
+ $fields = [
+ 'search_api_url' => [
+ 'type' => 'string',
+ ],
+ 'entity:node/body' => [
+ 'type' => 'text',
+ 'values' => $body_value,
+ ],
+ ];
+ $items = $this->createItems($this->index, 2, $fields, EntityAdapter::createFromEntity($node));
+
+ // Add the processor's field values to the items.
+ foreach ($items as $item) {
+ $this->processor->addFieldValues($item);
+ }
+
+ // Check the valid item.
+ $field = $items[$this->itemIds[0]]->getField('url');
+ $this->assertEquals(['http://www.example.com/node/example'], $field->getValues(), 'Valid URL added as value to the field.');
+
+ // Check that no other fields were changed.
+ $field = $items[$this->itemIds[0]]->getField('body');
+ $this->assertEquals($body_value, $field->getValues(), 'Body field was not changed.');
+
+ // Check the second item to be sure that all are processed.
+ $field = $items[$this->itemIds[1]]->getField('url');
+ $this->assertEquals(['http://www.example.com/node/example'], $field->getValues(), 'Valid URL added as value to the field in the second item.');
+ }
+
+ /**
+ * Tests whether the properties are correctly altered.
+ *
+ * @see \Drupal\search_api\Plugin\search_api\processor\AddURL::alterPropertyDefinitions()
+ */
+ public function testAlterPropertyDefinitions() {
+ // Check for added properties when no datasource is given.
+ $properties = $this->processor->getPropertyDefinitions(NULL);
+ $property_added = array_key_exists('search_api_url', $properties);
+ $this->assertTrue($property_added, 'The "search_api_url" property was added to the properties.');
+ if ($property_added) {
+ $this->assertInstanceOf('Drupal\Core\TypedData\DataDefinitionInterface', $properties['search_api_url'], 'The "search_api_url" property contains a valid data definition.');
+ if ($properties['search_api_url'] instanceof DataDefinitionInterface) {
+ $this->assertEquals('string', $properties['search_api_url']->getDataType(), 'Correct data type set in the data definition.');
+ $this->assertEquals('URI', $properties['search_api_url']->getLabel(), 'Correct label set in the data definition.');
+ $this->assertEquals('A URI where the item can be accessed', $properties['search_api_url']->getDescription(), 'Correct description set in the data definition.');
+ }
+ }
+
+ // Verify that there are no properties if a datasource is given.
+ $datasource = $this->getMock('Drupal\search_api\Datasource\DatasourceInterface');
+ $properties = $this->processor->getPropertyDefinitions($datasource);
+ $this->assertEmpty($properties, 'Datasource-specific properties did not get changed.');
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Unit/Processor/AggregatedFieldsTest.php b/sites/all/modules/contrib/search/search_api/tests/src/Unit/Processor/AggregatedFieldsTest.php
new file mode 100644
index 000000000..6e588d2fd
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Unit/Processor/AggregatedFieldsTest.php
@@ -0,0 +1,434 @@
+getMock(DatasourceInterface::class);
+ $datasource->expects($this->any())
+ ->method('getPropertyDefinitions')
+ ->willReturn([]);
+ $this->index = new Index([
+ 'datasourceInstances' => [
+ 'entity:test1' => $datasource,
+ 'entity:test2' => $datasource,
+ ],
+ 'processorInstances' => [],
+ 'field_settings' => [
+ 'foo' => [
+ 'type' => 'string',
+ 'datasource_id' => 'entity:test1',
+ 'property_path' => 'foo',
+ ],
+ 'bar' => [
+ 'type' => 'string',
+ 'datasource_id' => 'entity:test1',
+ 'property_path' => 'foo:bar',
+ ],
+ 'bla' => [
+ 'type' => 'string',
+ 'datasource_id' => 'entity:test2',
+ 'property_path' => 'foobaz:bla',
+ ],
+ 'aggregated_field' => [
+ 'type' => 'text',
+ 'property_path' => 'aggregated_field',
+ ],
+ ],
+ ], 'search_api_index');
+ $this->processor = new AggregatedFields(['#index' => $this->index], 'aggregated_field', []);
+ $this->index->addProcessor($this->processor);
+ $this->setUpMockContainer();
+
+ $plugin_helper = $this->getMockBuilder(PluginHelperInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $plugin_helper->method('getProcessorsByStage')
+ ->willReturn([]);
+ $this->container->set('search_api.plugin_helper', $plugin_helper);
+
+ // We want to check correct data type handling, so we need a somewhat more
+ // complex mock-up for the datatype plugin handler.
+ /** @var \PHPUnit_Framework_MockObject_MockObject|\Drupal\search_api\DataType\DataTypePluginManager $data_type_manager */
+ $data_type_manager = $this->container->get('plugin.manager.search_api.data_type');
+ $data_type_manager->method('hasDefinition')
+ ->willReturn(TRUE);
+ $this->valueCallback = function ($value) {
+ if (is_numeric($value)) {
+ return $value + 1;
+ }
+ else {
+ return '*' . $value;
+ }
+ };
+ $data_type = $this->getMock(DataTypeInterface::class);
+ $data_type->method('getValue')
+ ->willReturnCallback($this->valueCallback);
+ $data_type_manager->method('createInstance')
+ ->willReturnMap([
+ ['text', [], $data_type],
+ ]);
+ }
+
+ /**
+ * Tests aggregated fields of the given type.
+ *
+ * @param string $type
+ * The aggregation type to test.
+ * @param array $expected
+ * The expected values for the two items.
+ * @param bool $integer
+ * (optional) TRUE if the items' normal fields should contain integers,
+ * FALSE otherwise.
+ *
+ * @dataProvider aggregationTestsDataProvider
+ */
+ public function testAggregation($type, array $expected, $integer = FALSE) {
+ // Add the field configuration.
+ $configuration = [
+ 'type' => $type,
+ 'fields' => [
+ 'entity:test1/foo',
+ 'entity:test1/foo:bar',
+ 'entity:test2/foobaz:bla',
+ ],
+ ];
+ $this->index->getField($this->fieldId)->setConfiguration($configuration);
+
+ if ($integer) {
+ $field_values = [
+ 'foo' => [2, 4],
+ 'bar' => [16],
+ 'bla' => [7],
+ ];
+ }
+ else {
+ $field_values = [
+ 'foo' => ['foo', 'bar'],
+ 'bar' => ['baz'],
+ 'bla' => ['foobar'],
+ ];
+ }
+ $items = [];
+ $i = 0;
+ foreach (['entity:test1', 'entity:test2'] as $datasource_id) {
+ $this->itemIds[$i++] = $item_id = Utility::createCombinedId($datasource_id, '1:en');
+ $item = \Drupal::getContainer()
+ ->get('search_api.fields_helper')
+ ->createItem($this->index, $item_id);
+ foreach ([NULL, $datasource_id] as $field_datasource_id) {
+ foreach ($this->index->getFieldsByDatasource($field_datasource_id) as $field_id => $field) {
+ $field = clone $field;
+ if (!empty($field_values[$field_id])) {
+ $field->setValues($field_values[$field_id]);
+ }
+ $item->setField($field_id, $field);
+ }
+ }
+ $item->setFieldsExtracted(TRUE);
+ $items[$item_id] = $item;
+ }
+
+ // Add the processor's field values to the items.
+ foreach ($items as $item) {
+ $this->processor->addFieldValues($item);
+ }
+
+ $this->assertEquals(array_map($this->valueCallback, $expected[0]), $items[$this->itemIds[0]]->getField($this->fieldId)->getValues(), 'Correct aggregation for item 1.');
+ $this->assertEquals(array_map($this->valueCallback, $expected[1]), $items[$this->itemIds[1]]->getField($this->fieldId)->getValues(), 'Correct aggregation for item 2.');
+ }
+
+ /**
+ * Provides test data for aggregation tests.
+ *
+ * @return array
+ * An array containing test data sets, with each being an array of
+ * arguments to pass to the test method.
+ *
+ * @see static::testAggregation()
+ */
+ public function aggregationTestsDataProvider() {
+ return [
+ '"Union" aggregation' => [
+ 'union',
+ [
+ ['foo', 'bar', 'baz'],
+ ['foobar'],
+ ],
+ ],
+ '"Concatenation" aggregation' => [
+ 'concat',
+ [
+ ["foo\n\nbar\n\nbaz"],
+ ['foobar'],
+ ],
+ ],
+ '"Sum" aggregation' => [
+ 'sum',
+ [
+ [22],
+ [7],
+ ],
+ TRUE,
+ ],
+ '"Count" aggregation' => [
+ 'count',
+ [
+ [3],
+ [1],
+ ],
+ ],
+ '"Maximum" aggregation' => [
+ 'max',
+ [
+ [16],
+ [7],
+ ],
+ TRUE,
+ ],
+ '"Minimum" aggregation' => [
+ 'min',
+ [
+ [2],
+ [7],
+ ],
+ TRUE,
+ ],
+ '"First" aggregation' => [
+ 'first',
+ [
+ ['foo'],
+ ['foobar'],
+ ],
+ ],
+ '"Last" aggregation' => [
+ 'last',
+ [
+ ['baz'],
+ ['foobar'],
+ ],
+ ],
+ ];
+ }
+
+ /**
+ * Tests whether the properties are correctly altered.
+ *
+ * @see \Drupal\search_api\Plugin\search_api\processor\AggregatedFields::getPropertyDefinitions()
+ */
+ public function testGetPropertyDefinitions() {
+ /** @var \Drupal\Core\StringTranslation\TranslationInterface $translation */
+ $translation = $this->getStringTranslationStub();
+ $this->processor->setStringTranslation($translation);
+
+ // Check for added properties when no datasource is given.
+ /** @var \Drupal\search_api\Processor\ProcessorPropertyInterface[] $properties */
+ $properties = $this->processor->getPropertyDefinitions(NULL);
+
+ $this->assertArrayHasKey('aggregated_field', $properties, 'The "aggregated_field" property was added to the properties.');
+ $this->assertInstanceOf(AggregatedFieldProperty::class, $properties['aggregated_field'], 'The "aggregated_field" property has the correct class.');
+ $this->assertEquals('string', $properties['aggregated_field']->getDataType(), 'Correct data type set in the data definition.');
+ $this->assertEquals($translation->translate('Aggregated field'), $properties['aggregated_field']->getLabel(), 'Correct label set in the data definition.');
+ $expected_description = $translation->translate('An aggregation of multiple other fields.');
+ $this->assertEquals($expected_description, $properties['aggregated_field']->getDescription(), 'Correct description set in the data definition.');
+
+ // Verify that there are no properties if a datasource is given.
+ $datasource = $this->getMock(DatasourceInterface::class);
+ $properties = $this->processor->getPropertyDefinitions($datasource);
+ $this->assertEmpty($properties, 'Datasource-specific properties did not get changed.');
+ }
+
+ /**
+ * Tests that field extraction in the processor works correctly.
+ */
+ public function testFieldExtraction() {
+ /** @var \Drupal\Tests\search_api\Unit\TestComplexDataInterface|\PHPUnit_Framework_MockObject_MockObject $object */
+ $object = $this->getMock(TestComplexDataInterface::class);
+ $bar_foo_property = $this->getMock(TypedDataInterface::class);
+ $bar_foo_property->method('getValue')
+ ->willReturn('value3');
+ $bar_foo_property->method('getDataDefinition')
+ ->willReturn(new DataDefinition());
+ $bar_property = $this->getMock(TestComplexDataInterface::class);
+ $bar_property->method('get')
+ ->willReturnMap([
+ ['foo', $bar_foo_property],
+ ]);
+ $bar_property->method('getProperties')
+ ->willReturn([
+ 'foo' => TRUE,
+ ]);
+ $foobar_property = $this->getMock(TypedDataInterface::class);
+ $foobar_property->method('getValue')
+ ->willReturn('wrong_value2');
+ $foobar_property->method('getDataDefinition')
+ ->willReturn(new DataDefinition());
+ $object->method('get')
+ ->willReturnMap([
+ ['bar', $bar_property],
+ ['foobar', $foobar_property],
+ ]);
+ $object->method('getProperties')
+ ->willReturn([
+ 'bar' => TRUE,
+ 'foobar' => TRUE,
+ ]);
+
+ /** @var \Drupal\search_api\IndexInterface|\PHPUnit_Framework_MockObject_MockObject $index */
+ $index = $this->getMock(IndexInterface::class);
+
+ $fields_helper = \Drupal::getContainer()->get('search_api.fields_helper');
+ $field = $fields_helper->createField($index, 'aggregated_field', [
+ 'property_path' => 'aggregated_field',
+ 'configuration' => [
+ 'type' => 'union',
+ 'fields' => [
+ 'aggregated_field',
+ 'foo',
+ 'entity:test1/bar:foo',
+ 'entity:test1/baz',
+ 'entity:test2/foobar',
+ ],
+ ],
+ ]);
+ $index->method('getFields')->willReturn([
+ 'aggregated_field' => $field,
+ ]);
+ $index->method('getPropertyDefinitions')
+ ->willReturnMap([
+ [
+ NULL,
+ [
+ 'foo' => new ProcessorProperty([
+ 'processor_id' => 'processor1',
+ ]),
+ ],
+ ],
+ [
+ 'entity:test1',
+ [
+ 'bar' => new DataDefinition(),
+ 'foobar' => new DataDefinition(),
+ ],
+ ],
+ ]);
+ $processor_mock = $this->getMock(ProcessorInterface::class);
+ $processor_mock->method('addFieldValues')
+ ->willReturnCallback(function (ItemInterface $item) {
+ foreach ($item->getFields(FALSE) as $field) {
+ if ($field->getCombinedPropertyPath() == 'foo') {
+ $field->setValues(['value4', 'value5']);
+ }
+ }
+ });
+ $index->method('getProcessorsByStage')
+ ->willReturnMap([
+ [
+ ProcessorInterface::STAGE_ADD_PROPERTIES,
+ [],
+ [
+ 'aggregated_field' => $this->processor,
+ 'processor1' => $processor_mock,
+ ],
+ ],
+ ]);
+ $this->processor->setIndex($index);
+
+ /** @var \Drupal\search_api\Datasource\DatasourceInterface|\PHPUnit_Framework_MockObject_MockObject $datasource */
+ $datasource = $this->getMock(DatasourceInterface::class);
+ $datasource->method('getPluginId')
+ ->willReturn('entity:test1');
+
+ $item = $fields_helper->createItem($index, 'id', $datasource);
+ $item->setOriginalObject($object);
+ $item->setField('aggregated_field', clone $field);
+ $item->setField('test1', $fields_helper->createField($index, 'test1', [
+ 'property_path' => 'baz',
+ 'values' => [
+ 'wrong_value1',
+ ],
+ ]));
+ $item->setField('test2', $fields_helper->createField($index, 'test2', [
+ 'datasource_id' => 'entity:test1',
+ 'property_path' => 'baz',
+ 'values' => [
+ 'value1',
+ 'value2',
+ ],
+ ]));
+ $item->setFieldsExtracted(TRUE);
+
+ $this->processor->addFieldValues($item);
+
+ $expected = [
+ 'value1',
+ 'value2',
+ 'value3',
+ 'value4',
+ 'value5',
+ ];
+ $actual = $item->getField('aggregated_field')->getValues();
+ sort($actual);
+ $this->assertEquals($expected, $actual);
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Unit/Processor/EntityStatusTest.php b/sites/all/modules/contrib/search/search_api/tests/src/Unit/Processor/EntityStatusTest.php
new file mode 100644
index 000000000..bf6140ee2
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Unit/Processor/EntityStatusTest.php
@@ -0,0 +1,179 @@
+setUpMockContainer();
+
+ $this->processor = new EntityStatus([], 'entity_status', []);
+
+ $this->index = $this->getMock('Drupal\search_api\IndexInterface');
+
+ foreach (['node', 'comment', 'user', 'file'] as $entity_type) {
+ $datasource = $this->getMock('Drupal\search_api\Datasource\DatasourceInterface');
+ $datasource->expects($this->any())
+ ->method('getEntityTypeId')
+ ->will($this->returnValue($entity_type));
+ $this->datasources["entity:$entity_type"] = $datasource;
+ }
+ }
+
+ /**
+ * Tests whether supportsIndex() returns TRUE for an index containing nodes.
+ *
+ * @param string[]|null $datasource_ids
+ * The IDs of datasources the index should have, or NULL if it should have
+ * all of them.
+ * @param bool $expected
+ * Whether the processor is supposed to support that index.
+ *
+ * @dataProvider supportsIndexDataProvider
+ */
+ public function testSupportsIndex(array $datasource_ids = NULL, $expected) {
+ if ($datasource_ids !== NULL) {
+ $datasource_ids = array_flip($datasource_ids);
+ $this->datasources = array_intersect_key($this->datasources, $datasource_ids);
+ }
+ $this->index->method('getDatasources')
+ ->will($this->returnValue($this->datasources));
+
+ // In supportsIndex(), the entity status processor will use the entity type
+ // manager to get the definition of each datasource's entity type and then
+ // check whether it implements \Drupal\Core\Entity\EntityPublishedInterface.
+ // We therefore need to ensure each of these calls returns an appropriate
+ // value.
+ $self = $this;
+ $entity_type_manager = $this->getMock(EntityTypeManagerInterface::class);
+ $entity_type_manager->method('getDefinition')
+ ->willReturnCallback(function ($entity_type_id) use ($self) {
+ $entity_type = $self->getMock(EntityTypeInterface::class);
+ $publishable = in_array($entity_type_id, ['node', 'comment']);
+ $entity_type->method('entityClassImplements')
+ ->willReturnMap([
+ [EntityPublishedInterface::class, $publishable],
+ ]);
+ return $entity_type;
+ });
+ $this->container->set('entity_type.manager', $entity_type_manager);
+
+ $this->assertEquals($expected, EntityStatus::supportsIndex($this->index));
+ }
+
+ /**
+ * Provides data for the testSupportsIndex() tests.
+ *
+ * @return array[]
+ * Array of parameter arrays for testSupportsIndex().
+ */
+ public function supportsIndexDataProvider() {
+ return [
+ 'all datasources' => [NULL, TRUE],
+ 'node datasource' => [['entity:node'], TRUE],
+ 'comment datasource' => [['entity:comment'], TRUE],
+ 'user datasource' => [['entity:user'], TRUE],
+ 'file datasource' => [['entity:file'], FALSE],
+ ];
+ }
+
+ /**
+ * Tests if unpublished/inactive entities are removed from the indexed items.
+ */
+ public function testAlterItems() {
+ $entity_types = [
+ 'node' => [
+ 'class' => Node::class,
+ 'method' => 'isPublished',
+ ],
+ 'comment' => [
+ 'class' => Comment::class,
+ 'method' => 'isPublished',
+ ],
+ 'user' => [
+ 'class' => User::class,
+ 'method' => 'isActive',
+ ],
+ 'file' => [
+ 'class' => File::class,
+ ],
+ ];
+ $fields_helper = \Drupal::getContainer()->get('search_api.fields_helper');
+ $items = [];
+ foreach ($entity_types as $entity_type => $info) {
+ $datasource_id = "entity:$entity_type";
+ foreach ([1 => TRUE, 2 => FALSE] as $i => $status) {
+ $item_id = Utility::createCombinedId($datasource_id, "$i:en");
+ $item = $fields_helper->createItem($this->index, $item_id, $this->datasources[$datasource_id]);
+ $entity = $this->getMockBuilder($info['class'])
+ ->disableOriginalConstructor()
+ ->getMock();
+ if (isset($info['method'])) {
+ $entity->method($info['method'])
+ ->will($this->returnValue($status));
+ }
+ /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
+ $item->setOriginalObject(EntityAdapter::createFromEntity($entity));
+ $items[$item_id] = $item;
+ }
+ }
+
+ $this->processor->alterIndexedItems($items);
+ $expected = [
+ Utility::createCombinedId('entity:node', '1:en'),
+ Utility::createCombinedId('entity:comment', '1:en'),
+ Utility::createCombinedId('entity:user', '1:en'),
+ Utility::createCombinedId('entity:file', '1:en'),
+ Utility::createCombinedId('entity:file', '2:en'),
+ ];
+ $this->assertEquals($expected, array_keys($items));
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Unit/Processor/FieldsProcessorPluginBaseTest.php b/sites/all/modules/contrib/search/search_api/tests/src/Unit/Processor/FieldsProcessorPluginBaseTest.php
new file mode 100644
index 000000000..13fd9c446
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Unit/Processor/FieldsProcessorPluginBaseTest.php
@@ -0,0 +1,673 @@
+setUpMockContainer();
+ $this->index = $this->getMock('Drupal\search_api\IndexInterface');
+ $this->index->expects($this->any())
+ ->method('status')
+ ->will($this->returnValue(TRUE));
+ $items = $this->getTestItem();
+ $fields = $items[$this->itemIds[0]]->getFields();
+ $this->index->expects($this->any())
+ ->method('getFields')
+ ->will($this->returnValue($fields));
+
+ $this->processor = new TestFieldsProcessorPlugin(['#index' => $this->index], '', []);
+ }
+
+ /**
+ * Tests whether the processor handles field changes correctly.
+ */
+ public function testFieldRenaming() {
+ $configuration['fields'] = [
+ 'float_field',
+ 'float_field_2',
+ 'string_field',
+ 'text_field',
+ 'text_field_2',
+ ];
+ $this->processor->setConfiguration($configuration);
+
+ $override = function ($type) {
+ return in_array($type, ['string', 'text']);
+ };
+ $this->processor->setMethodOverride('testType', $override);
+
+ $this->index->method('getFieldRenames')
+ ->willReturn([
+ 'float_field' => 'foo',
+ 'text_field' => 'bar',
+ ]);
+ $this->index->method('getField')
+ ->willReturnMap([
+ ['float_field', (new Field($this->index, ''))->setType('float')],
+ ['float_field_2', NULL],
+ ['string_field', (new Field($this->index, ''))->setType('string')],
+ ['bar', (new Field($this->index, ''))->setType('text')],
+ ['text_field_2', (new Field($this->index, ''))->setType('text')],
+ ]);
+
+ $this->processor->preIndexSave();
+
+ $fields = $this->processor->getConfiguration()['fields'];
+ sort($fields);
+ $expected = [
+ 'bar',
+ 'string_field',
+ 'text_field_2',
+ ];
+ $this->assertEquals($expected, $fields);
+ }
+
+ /**
+ * Tests whether the "Enable on all supported fields" option works correctly.
+ *
+ * We want the option to make sure that all supported fields are automatically
+ * added to the processor, without adding unsupported ones or keeping old ones
+ * that were removed.
+ */
+ public function testAllFields() {
+ $configuration['all_fields'] = TRUE;
+ $configuration['fields'] = [
+ 'float_field',
+ 'string_field',
+ ];
+ $this->processor->setConfiguration($configuration);
+
+ $override = function ($type) {
+ return in_array($type, ['string', 'text']);
+ };
+ $this->processor->setMethodOverride('testType', $override);
+
+ // Since it's not possible to override an already specified method on a mock
+ // object, we need to create a new mock object for the index in this test.
+ /** @var \Drupal\search_api\IndexInterface|\PHPUnit_Framework_MockObject_MockObject $index */
+ $index = $this->getMock(IndexInterface::class);
+ $index->method('getFields')->willReturn([
+ 'float_field' => (new Field($this->index, ''))->setType('float'),
+ 'float_field_2' => (new Field($this->index, ''))->setType('float'),
+ 'string_field' => (new Field($this->index, ''))->setType('string'),
+ 'text_field' => (new Field($this->index, ''))->setType('text'),
+ 'text_field_2' => (new Field($this->index, ''))->setType('text'),
+ ]);
+ $this->processor->setIndex($index);
+
+ $this->processor->preIndexSave();
+
+ $fields = $this->processor->getConfiguration()['fields'];
+ sort($fields);
+ $expected = [
+ 'string_field',
+ 'text_field',
+ 'text_field_2',
+ ];
+ $this->assertEquals($expected, $fields);
+ }
+
+ /**
+ * Tests whether the default implementation of testType() works correctly.
+ */
+ public function testTestTypeDefault() {
+ $items = $this->getTestItem();
+ $this->processor->preprocessIndexItems($items);
+ $this->assertFieldsProcessed($items, ['text_field', 'string_field']);
+ }
+
+ /**
+ * Tests whether overriding of testType() works correctly.
+ */
+ public function testTestTypeOverride() {
+ $override = function ($type) {
+ return \Drupal::getContainer()
+ ->get('search_api.data_type_helper')
+ ->isTextType($type, ['string', 'integer']);
+ };
+ $this->processor->setMethodOverride('testType', $override);
+
+ $items = $this->getTestItem();
+ $this->processor->preprocessIndexItems($items);
+ $this->assertFieldsProcessed($items, ['string_field', 'integer_field']);
+ }
+
+ /**
+ * Tests whether selecting fields works correctly.
+ */
+ public function testTestField() {
+ // testType() shouldn't have any effect anymore when fields are configured.
+ $override = function () {
+ return FALSE;
+ };
+ $this->processor->setMethodOverride('testType', $override);
+ $configuration['fields'] = ['text_field', 'float_field'];
+ $this->processor->setConfiguration($configuration);
+
+ $items = $this->getTestItem();
+ $this->processor->preprocessIndexItems($items);
+ $this->assertFieldsProcessed($items, ['text_field', 'float_field']);
+ }
+
+ /**
+ * Tests whether overriding of processFieldValue() works correctly.
+ */
+ public function testProcessFieldValueOverride() {
+ $override = function (&$value, &$type) {
+ // Check whether the passed $type matches the one included in the value.
+ if (strpos($value, "{$type}_field") !== FALSE) {
+ $value = "&$value";
+ }
+ else {
+ $value = "/$value";
+ }
+ };
+ $this->processor->setMethodOverride('processFieldValue', $override);
+
+ $items = $this->getTestItem();
+ $this->processor->preprocessIndexItems($items);
+ $this->assertFieldsProcessed($items, ['text_field', 'string_field'], '&');
+ }
+
+ /**
+ * Tests whether removing values in processFieldValue() works correctly.
+ */
+ public function testProcessFieldRemoveValue() {
+ $override = function (&$value) {
+ if ($value != 'bar') {
+ $value = "*$value";
+ }
+ else {
+ $value = '';
+ }
+ };
+ $this->processor->setMethodOverride('processFieldValue', $override);
+
+ $fields = [
+ 'field1' => [
+ 'type' => 'string',
+ 'values' => [
+ 'foo',
+ 'bar',
+ ],
+ ],
+ ];
+ $items = $this->createItems($this->index, 1, $fields);
+
+ $this->processor->preprocessIndexItems($items);
+
+ $item_fields = $items[$this->itemIds[0]]->getFields();
+ $this->assertEquals(['*foo'], $item_fields['field1']->getValues(), 'Field value was correctly removed.');
+ }
+
+ /**
+ * Tests whether the processField() method operates correctly.
+ */
+ public function testProcessFieldsTokenized() {
+ $override = function (&$value, $type) {
+ switch ($type) {
+ case 'integer':
+ ++$value;
+ return;
+
+ case 'string':
+ $value = "++$value";
+ return;
+ }
+
+ if (strpos($value, ' ')) {
+ $value = TestFieldsProcessorPlugin::createTokenizedText($value, 4)->getTokens();
+ }
+ elseif ($value == 'bar') {
+ $value = TestFieldsProcessorPlugin::createTokenizedText('*bar', 2)->getTokens();
+ }
+ elseif ($value == 'baz') {
+ $value = '';
+ }
+ else {
+ $value = "*$value";
+ }
+ };
+ $this->processor->setMethodOverride('processFieldValue', $override);
+
+ $value = TestFieldsProcessorPlugin::createTokenizedText('foobar baz', 3);
+ $tokens = $value->getTokens();
+ $tokens[] = new TextToken('foo bar', 2);
+ $value->setTokens($tokens);
+ $fields = [
+ 'field1' => [
+ 'type' => 'text',
+ 'values' => [
+ TestFieldsProcessorPlugin::createTokenizedText('foo bar baz', 3),
+ $value,
+ new TextValue('foo'),
+ new TextValue('foo bar'),
+ new TextValue('bar'),
+ new TextValue('baz'),
+ ],
+ ],
+ 'field2' => [
+ 'type' => 'integer',
+ 'values' => [
+ 1,
+ 3,
+ ],
+ ],
+ 'field3' => [
+ 'type' => 'string',
+ 'values' => [
+ 'foo',
+ 'foo bar baz',
+ ],
+ ],
+ ];
+ $items = $this->createItems($this->index, 1, $fields);
+
+ $this->processor->setConfiguration([
+ 'fields' => ['field1', 'field2', 'field3'],
+ ]);
+
+ $this->processor->preprocessIndexItems($items);
+
+ $fields = $items[$this->itemIds[0]]->getFields();
+
+ /** @var \Drupal\search_api\Plugin\search_api\data_type\value\TextValueInterface[] $values */
+ $values = $fields['field1']->getValues();
+ $summary = [];
+ foreach ($values as $i => $value) {
+ $summary[$i]['text'] = $value->toText();
+ $tokens = $value->getTokens();
+ if ($tokens !== NULL) {
+ $summary[$i]['tokens'] = [];
+ foreach ($tokens as $token) {
+ $summary[$i]['tokens'][] = [
+ 'text' => $token->getText(),
+ 'boost' => $token->getBoost(),
+ ];
+ }
+ }
+ }
+ $expected = [
+ [
+ 'text' => '*foo *bar',
+ 'tokens' => [
+ [
+ 'text' => '*foo',
+ 'boost' => 3,
+ ],
+ [
+ 'text' => '*bar',
+ 'boost' => 6,
+ ],
+ ],
+ ],
+ [
+ 'text' => '*foobar foo bar',
+ 'tokens' => [
+ [
+ 'text' => '*foobar',
+ 'boost' => 3,
+ ],
+ [
+ 'text' => 'foo',
+ 'boost' => 8,
+ ],
+ [
+ 'text' => 'bar',
+ 'boost' => 8,
+ ],
+ ],
+ ],
+ [
+ 'text' => '*foo',
+ ],
+ [
+ 'text' => 'foo bar',
+ 'tokens' => [
+ [
+ 'text' => 'foo',
+ 'boost' => 4,
+ ],
+ [
+ 'text' => 'bar',
+ 'boost' => 4,
+ ],
+ ],
+ ],
+ [
+ 'text' => '*bar',
+ 'tokens' => [
+ [
+ 'text' => '*bar',
+ 'boost' => 2,
+ ],
+ ],
+ ],
+ ];
+ $this->assertEquals($expected, $summary);
+
+ $expected = [2, 4];
+ $this->assertEquals('integer', $fields['field2']->getType());
+ $this->assertEquals($expected, $fields['field2']->getValues());
+
+ $expected = ['++foo', '++foo bar baz'];
+ $this->assertEquals('string', $fields['field3']->getType());
+ $this->assertEquals($expected, $fields['field3']->getValues());
+ }
+
+ /**
+ * Tests whether preprocessing of queries without search keys works correctly.
+ */
+ public function testProcessKeysNoKeys() {
+ $query = \Drupal::getContainer()
+ ->get('search_api.query_helper')
+ ->createQuery($this->index);
+
+ $this->processor->preprocessSearchQuery($query);
+
+ $this->assertNull($query->getKeys(), 'Query without keys was correctly ignored.');
+ }
+
+ /**
+ * Tests whether preprocessing of simple search keys works correctly.
+ */
+ public function testProcessKeysSimple() {
+ $query = \Drupal::getContainer()
+ ->get('search_api.query_helper')
+ ->createQuery($this->index);
+ $keys = &$query->getKeys();
+ $keys = 'foo';
+
+ $this->processor->preprocessSearchQuery($query);
+
+ $this->assertEquals('*foo', $query->getKeys(), 'Search keys were correctly preprocessed.');
+ }
+
+ /**
+ * Tests whether preprocessing of complex search keys works correctly.
+ */
+ public function testProcessKeysComplex() {
+ $query = \Drupal::getContainer()
+ ->get('search_api.query_helper')
+ ->createQuery($this->index);
+ $keys = &$query->getKeys();
+ $keys = [
+ '#conjunction' => 'OR',
+ 'foo',
+ [
+ '#conjunction' => 'AND',
+ 'bar',
+ 'baz',
+ '#negation' => TRUE,
+ ],
+ ];
+
+ $this->processor->preprocessSearchQuery($query);
+
+ $expected = [
+ '#conjunction' => 'OR',
+ '*foo',
+ [
+ '#conjunction' => 'AND',
+ '*bar',
+ '*baz',
+ '#negation' => TRUE,
+ ],
+ ];
+ $this->assertEquals($expected, $query->getKeys(), 'Search keys were correctly preprocessed.');
+ }
+
+ /**
+ * Tests whether overriding of processKey() works correctly.
+ */
+ public function testProcessKeyOverride() {
+ $override = function (&$value) {
+ if ($value != 'baz') {
+ $value = "&$value";
+ }
+ else {
+ $value = '';
+ }
+ };
+ $this->processor->setMethodOverride('processKey', $override);
+
+ $query = \Drupal::getContainer()
+ ->get('search_api.query_helper')
+ ->createQuery($this->index);
+ $keys = &$query->getKeys();
+ $keys = [
+ '#conjunction' => 'OR',
+ 'foo',
+ [
+ '#conjunction' => 'AND',
+ 'bar',
+ 'baz',
+ '#negation' => TRUE,
+ ],
+ ];
+
+ $this->processor->preprocessSearchQuery($query);
+
+ $expected = [
+ '#conjunction' => 'OR',
+ '&foo',
+ [
+ '#conjunction' => 'AND',
+ '&bar',
+ '#negation' => TRUE,
+ ],
+ ];
+ $this->assertEquals($expected, $query->getKeys(), 'Search keys were correctly preprocessed.');
+ }
+
+ /**
+ * Tests whether preprocessing search conditions works correctly.
+ */
+ public function testProcessConditions() {
+ $query = \Drupal::getContainer()
+ ->get('search_api.query_helper')
+ ->createQuery($this->index);
+ $query->addCondition('text_field', 'foo');
+ $query->addCondition('text_field', ['foo', 'bar'], 'IN');
+ $query->addCondition('string_field', NULL, '<>');
+ $query->addCondition('integer_field', 'bar');
+
+ $this->processor->preprocessSearchQuery($query);
+
+ $expected = [
+ new Condition('text_field', '*foo'),
+ new Condition('text_field', ['*foo', '*bar'], 'IN'),
+ new Condition('string_field', 'undefined', '<>'),
+ new Condition('integer_field', 'bar'),
+ ];
+ $this->assertEquals($expected, $query->getConditionGroup()->getConditions(), 'Conditions were preprocessed correctly.');
+ }
+
+ /**
+ * Tests whether preprocessing nested search conditions works correctly.
+ */
+ public function testProcessConditionsNestedConditions() {
+ $query = \Drupal::getContainer()
+ ->get('search_api.query_helper')
+ ->createQuery($this->index);
+ $conditions = $query->createConditionGroup();
+ $conditions->addCondition('text_field', 'foo');
+ $conditions->addCondition('text_field', ['foo', 'bar'], 'IN');
+ $conditions->addCondition('string_field', NULL, '<>');
+ $conditions->addCondition('integer_field', 'bar');
+ $query->addConditionGroup($conditions);
+
+ $this->processor->preprocessSearchQuery($query);
+
+ $expected = [
+ new Condition('text_field', '*foo'),
+ new Condition('text_field', ['*foo', '*bar'], 'IN'),
+ new Condition('string_field', 'undefined', '<>'),
+ new Condition('integer_field', 'bar'),
+ ];
+ $this->assertEquals($expected, $query->getConditionGroup()->getConditions()[0]->getConditions(), 'Conditions were preprocessed correctly.');
+ }
+
+ /**
+ * Tests whether overriding processConditionValue() works correctly.
+ */
+ public function testProcessConditionValueOverride() {
+ $override = function (&$value) {
+ if (isset($value)) {
+ $value = '';
+ }
+ };
+ $this->processor->setMethodOverride('processConditionValue', $override);
+
+ $query = \Drupal::getContainer()
+ ->get('search_api.query_helper')
+ ->createQuery($this->index);
+ $query->addCondition('text_field', 'foo');
+ $query->addCondition('string_field', NULL, '<>');
+ $query->addCondition('integer_field', 'bar');
+
+ $this->processor->preprocessSearchQuery($query);
+
+ $expected = [
+ new Condition('string_field', NULL, '<>'),
+ new Condition('integer_field', 'bar'),
+ ];
+ $this->assertEquals($expected, array_merge($query->getConditionGroup()->getConditions()), 'Conditions were preprocessed correctly.');
+ }
+
+ /**
+ * Tests whether overriding processConditionValue() works correctly.
+ */
+ public function testProcessConditionValueArrayHandling() {
+ $override = function (&$value) {
+ $length = strlen($value);
+ if ($length == 2) {
+ $value = '';
+ }
+ elseif ($length == 3) {
+ $value .= '*';
+ }
+ };
+ $this->processor->setMethodOverride('process', $override);
+
+ $query = \Drupal::getContainer()
+ ->get('search_api.query_helper')
+ ->createQuery($this->index);
+ $query->addCondition('text_field', ['a', 'b'], 'NOT IN');
+ $query->addCondition('text_field', ['a', 'bo'], 'IN');
+ $query->addCondition('text_field', ['ab', 'bo'], 'NOT IN');
+ $query->addCondition('text_field', ['a', 'bo'], 'BETWEEN');
+ $query->addCondition('text_field', ['ab', 'bo'], 'NOT BETWEEN');
+ $query->addCondition('text_field', ['a', 'bar'], 'IN');
+ $query->addCondition('text_field', ['abo', 'baz'], 'BETWEEN');
+
+ $this->processor->preprocessSearchQuery($query);
+
+ $expected = [
+ new Condition('text_field', ['a', 'b'], 'NOT IN'),
+ new Condition('text_field', ['a'], 'IN'),
+ new Condition('text_field', ['a', 'bo'], 'BETWEEN'),
+ new Condition('text_field', ['ab', 'bo'], 'NOT BETWEEN'),
+ new Condition('text_field', ['a', 'bar*'], 'IN'),
+ new Condition('text_field', ['abo*', 'baz*'], 'BETWEEN'),
+ ];
+ $this->assertEquals($expected, array_merge($query->getConditionGroup()->getConditions()), 'Conditions were preprocessed correctly.');
+ }
+
+ /**
+ * Returns an array with one test item suitable for this test case.
+ *
+ * @param string[]|null $types
+ * (optional) The types of fields to create. Defaults to using "text",
+ * "string", "integer" and "float".
+ *
+ * @return \Drupal\search_api\Item\ItemInterface[]
+ * An array containing one item.
+ */
+ protected function getTestItem($types = NULL) {
+ if ($types === NULL) {
+ $types = ['text', 'string', 'integer', 'float'];
+ }
+
+ $fields = [];
+ foreach ($types as $type) {
+ $field_id = "{$type}_field";
+ $fields[$field_id] = [
+ 'type' => $type,
+ 'values' => [
+ "$field_id value 1",
+ "$field_id value 2",
+ ],
+ ];
+ }
+ return $this->createItems($this->index, 1, $fields);
+ }
+
+ /**
+ * Asserts that the given fields have been correctly processed.
+ *
+ * @param \Drupal\search_api\Item\ItemInterface[] $items
+ * An array containing one item.
+ * @param string[] $processed_fields
+ * The fields which should be processed.
+ * @param string $prefix
+ * (optional) The prefix that processed fields receive.
+ */
+ protected function assertFieldsProcessed(array $items, array $processed_fields, $prefix = "*") {
+ $processed_fields = array_fill_keys($processed_fields, TRUE);
+ foreach ($items as $item) {
+ foreach ($item->getFields() as $field_id => $field) {
+ if (!empty($processed_fields[$field_id])) {
+ $expected = [
+ "$prefix$field_id value 1",
+ "$prefix$field_id value 2",
+ ];
+ }
+ else {
+ $expected = [
+ "$field_id value 1",
+ "$field_id value 2",
+ ];
+ }
+ $this->assertEquals($expected, $field->getValues(), "Field $field_id is correct.");
+ }
+ }
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Unit/Processor/HighlightTest.php b/sites/all/modules/contrib/search/search_api/tests/src/Unit/Processor/HighlightTest.php
new file mode 100644
index 000000000..02d31b158
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Unit/Processor/HighlightTest.php
@@ -0,0 +1,1020 @@
+setUpMockContainer();
+
+ $this->processor = new Highlight([], 'highlight', []);
+
+ $this->index = $this->getMock(IndexInterface::class);
+ $this->index->expects($this->any())
+ ->method('getFulltextFields')
+ ->willReturn(['body', 'title']);
+ $this->processor->setIndex($this->index);
+
+ /** @var \Drupal\Core\StringTranslation\TranslationInterface $translation */
+ $translation = $this->getStringTranslationStub();
+ $this->processor->setStringTranslation($translation);
+ }
+
+ /**
+ * Tests whether the processor handles field ID changes correctly.
+ */
+ public function testFieldRenaming() {
+ $configuration['exclude_fields'] = ['body', 'title'];
+ $this->processor->setConfiguration($configuration);
+
+ $this->index->method('getFieldRenames')
+ ->willReturn([
+ 'title' => 'foobar',
+ ]);
+
+ $this->processor->preIndexSave();
+
+ $fields = $this->processor->getConfiguration()['exclude_fields'];
+ sort($fields);
+ $this->assertEquals(['body', 'foobar'], $fields);
+ }
+
+ /**
+ * Tests postprocessing with an empty result set.
+ */
+ public function testPostprocessSearchResultsWithEmptyResult() {
+ $query = $this->getMock(QueryInterface::class);
+
+ $results = $this->getMockBuilder(ResultSet::class)
+ ->setMethods(['getResultCount'])
+ ->setConstructorArgs([$query])
+ ->getMock();
+
+ $results->expects($this->once())
+ ->method('getResultCount')
+ ->will($this->returnValue(0));
+ $results->expects($this->never())
+ ->method('getQuery');
+ $results->expects($this->never())
+ ->method('getResultItems');
+ /** @var \Drupal\search_api\Query\ResultSet $results */
+
+ $this->processor->postprocessSearchResults($results);
+ }
+
+ /**
+ * Makes sure that queries with "basic" processing set are ignored.
+ */
+ public function testPostprocessBasicQuery() {
+ $query = $this->getMock(QueryInterface::class);
+ $query->expects($this->once())
+ ->method('getProcessingLevel')
+ ->willReturn(QueryInterface::PROCESSING_BASIC);
+
+ $results = $this->getMockBuilder(ResultSet::class)
+ ->setMethods(['getResultCount', 'getQuery'])
+ ->setConstructorArgs([$query])
+ ->getMock();
+
+ $results->expects($this->once())
+ ->method('getResultCount')
+ ->willReturn(1);
+ $results->expects($this->once())
+ ->method('getQuery')
+ ->will($this->returnValue($query));
+ $results->expects($this->never())
+ ->method('getResultItems');
+ /** @var \Drupal\search_api\Query\ResultSet $results */
+
+ $this->processor->postprocessSearchResults($results);
+ }
+
+ /**
+ * Tests postprocessing on a query without keywords.
+ */
+ public function testPostprocessSearchResultsWithoutKeywords() {
+ $query = $this->getMock(QueryInterface::class);
+ $query->expects($this->once())
+ ->method('getProcessingLevel')
+ ->willReturn(QueryInterface::PROCESSING_FULL);
+
+ $results = $this->getMockBuilder(ResultSet::class)
+ ->setMethods(['getResultCount', 'getQuery'])
+ ->setConstructorArgs([$query])
+ ->getMock();
+
+ $query->expects($this->once())
+ ->method('getOriginalKeys')
+ ->will($this->returnValue([]));
+
+ $results->expects($this->once())
+ ->method('getResultCount')
+ ->will($this->returnValue(1));
+ $results->expects($this->once())
+ ->method('getQuery')
+ ->will($this->returnValue($query));
+ $results->expects($this->never())
+ ->method('getResultItems');
+ /** @var \Drupal\search_api\Query\ResultSet $results */
+
+ $this->processor->postprocessSearchResults($results);
+ }
+
+ /**
+ * Tests field highlighting with a normal result set.
+ */
+ public function testPostprocessSearchResultsWithResults() {
+ $query = $this->getMock(QueryInterface::class);
+ $query->expects($this->once())
+ ->method('getProcessingLevel')
+ ->willReturn(QueryInterface::PROCESSING_FULL);
+ $query->expects($this->atLeastOnce())
+ ->method('getOriginalKeys')
+ ->will($this->returnValue('foo'));
+ /** @var \Drupal\search_api\Query\QueryInterface $query */
+
+ $field = $this->createTestField('body', 'entity:node/body');
+
+ $this->index->expects($this->atLeastOnce())
+ ->method('getFields')
+ ->will($this->returnValue(['body' => $field]));
+
+ $this->processor->setIndex($this->index);
+
+ $body_values = ['Some foo value'];
+ $fields = [
+ 'entity:node/body' => [
+ 'type' => 'text',
+ 'values' => $body_values,
+ ],
+ ];
+
+ $items = $this->createItems($this->index, 1, $fields);
+
+ $results = new ResultSet($query);
+ $results->setResultItems($items);
+ $results->setResultCount(1);
+
+ $this->processor->postprocessSearchResults($results);
+
+ $fields = $items[$this->itemIds[0]]->getExtraData('highlighted_fields');
+ $this->assertEquals('Some foo value', $fields['body'][0], 'Highlighting is correctly applied to body field.');
+ }
+
+ /**
+ * Tests changing the prefix and suffix used for highlighting.
+ */
+ public function testPostprocessSearchResultsWithChangedPrefixSuffix() {
+ $this->processor->setConfiguration([
+ 'prefix' => '',
+ 'suffix' => '',
+ ]);
+
+ $query = $this->getMock(QueryInterface::class);
+ $query->expects($this->once())
+ ->method('getProcessingLevel')
+ ->willReturn(QueryInterface::PROCESSING_FULL);
+ $query->expects($this->atLeastOnce())
+ ->method('getOriginalKeys')
+ ->will($this->returnValue(['#conjunction' => 'AND', 'foo']));
+ /** @var \Drupal\search_api\Query\QueryInterface $query */
+
+ $field = $this->createTestField('body', 'entity:node/body');
+
+ $this->index->expects($this->atLeastOnce())
+ ->method('getFields')
+ ->will($this->returnValue(['body' => $field]));
+
+ $this->processor->setIndex($this->index);
+
+ $body_values = ['Some foo value'];
+ $fields = [
+ 'entity:node/body' => [
+ 'type' => 'text',
+ 'values' => $body_values,
+ ],
+ ];
+
+ $items = $this->createItems($this->index, 1, $fields);
+
+ $results = new ResultSet($query);
+ $results->setResultItems($items);
+ $results->setResultCount(1);
+
+ $this->processor->postprocessSearchResults($results);
+
+ $fields = $items[$this->itemIds[0]]->getExtraData('highlighted_fields');
+ $this->assertEquals('Some foo value', $fields['body'][0], 'Highlighting is correctly applied');
+ }
+
+ /**
+ * Tests whether field highlighting can be disabled.
+ */
+ public function testPostprocessSearchResultsWithoutHighlight() {
+ $this->processor->setConfiguration(['highlight' => 'never']);
+
+ $query = $this->getMock(QueryInterface::class);
+ $query->expects($this->once())
+ ->method('getProcessingLevel')
+ ->willReturn(QueryInterface::PROCESSING_FULL);
+ $query->expects($this->atLeastOnce())
+ ->method('getOriginalKeys')
+ ->will($this->returnValue(['#conjunction' => 'AND', 'foo']));
+ /** @var \Drupal\search_api\Query\QueryInterface $query */
+
+ $field = $this->createTestField('body', 'entity:node/body');
+
+ $this->index->expects($this->atLeastOnce())
+ ->method('getFields')
+ ->will($this->returnValue(['body' => $field]));
+
+ $this->processor->setIndex($this->index);
+
+ $body_values = ['Some foo value'];
+ $fields = [
+ 'entity:node/body' => [
+ 'type' => 'text',
+ 'values' => $body_values,
+ ],
+ ];
+
+ $items = $this->createItems($this->index, 1, $fields);
+
+ $results = new ResultSet($query);
+ $results->setResultItems($items);
+ $results->setResultCount(1);
+
+ $this->processor->postprocessSearchResults($results);
+
+ $fields = $items[$this->itemIds[0]]->getExtraData('highlighted_fields');
+ $this->assertEmpty($fields, 'Highlighting is not applied when disabled.');
+ }
+
+ /**
+ * Tests highlighting of partial matches.
+ *
+ * @param string $text
+ * The text that should be highlighted.
+ * @param string $keywords
+ * The search keywords.
+ * @param string $highlighted
+ * The expected highlighted text.
+ *
+ * @dataProvider postprocessSearchResultsHighlightPartialDataProvider
+ */
+ public function testPostprocessSearchResultsHighlightPartial($text, $keywords, $highlighted) {
+ $this->processor->setConfiguration(['highlight_partial' => TRUE]);
+
+ $query = $this->getMock(QueryInterface::class);
+ $query->expects($this->once())
+ ->method('getProcessingLevel')
+ ->willReturn(QueryInterface::PROCESSING_FULL);
+ $query->expects($this->atLeastOnce())
+ ->method('getOriginalKeys')
+ ->will($this->returnValue(['#conjunction' => 'AND', $keywords]));
+ /** @var \Drupal\search_api\Query\QueryInterface $query */
+
+ $field = $this->createTestField('body', 'entity:node/body');
+
+ $this->index->expects($this->atLeastOnce())
+ ->method('getFields')
+ ->will($this->returnValue(['body' => $field]));
+
+ $this->processor->setIndex($this->index);
+
+ $fields = [
+ 'entity:node/body' => [
+ 'type' => 'text',
+ 'values' => [$text],
+ ],
+ ];
+
+ $items = $this->createItems($this->index, 1, $fields);
+
+ $results = new ResultSet($query);
+ $results->setResultItems($items);
+ $results->setResultCount(1);
+
+ $this->processor->postprocessSearchResults($results);
+
+ $fields = $items[$this->itemIds[0]]->getExtraData('highlighted_fields');
+ $this->assertEquals($highlighted, $fields['body'][0], 'Highlighting is correctly applied to a partial match.');
+ $excerpt = $items[$this->itemIds[0]]->getExcerpt();
+ $this->assertEquals("… $highlighted …", $excerpt, 'Highlighting is correctly applied to a partial match.');
+ }
+
+ /**
+ * Provides test data sets for testPostprocessSearchResultsHighlightPartial().
+ *
+ * @return array[]
+ * An array of argument arrays for
+ * testPostprocessSearchResultsHighlightPartial().
+ *
+ * @see \Drupal\Tests\search_api\Unit\Processor\HighlightTest::testPostprocessSearchResultsHighlightPartial()
+ */
+ public function postprocessSearchResultsHighlightPartialDataProvider() {
+ $data_sets = [
+ 'normal' => [
+ 'Some longwordtoshowpartialmatching value',
+ 'partial',
+ 'Some longwordtoshowpartialmatching value',
+ ],
+ ];
+
+ // Test multi-byte support only if this PHP installation actually contains
+ // the necessary function. Otherwise, we can't really be blamed for not
+ // supporting them.
+ if (function_exists('mb_stripos')) {
+ $data_sets['multi-byte'] = [
+ 'Alle Angaben ohne Gewähr.',
+ 'Ähr',
+ 'Alle Angaben ohne Gewähr.',
+ ];
+ }
+
+ return $data_sets;
+ }
+
+ /**
+ * Tests field highlighting when previous highlighting is present.
+ */
+ public function testPostprocessSearchResultsWithPreviousHighlighting() {
+ $query = $this->getMock(QueryInterface::class);
+ $query->expects($this->once())
+ ->method('getProcessingLevel')
+ ->willReturn(QueryInterface::PROCESSING_FULL);
+ $query->expects($this->atLeastOnce())
+ ->method('getOriginalKeys')
+ ->will($this->returnValue(['#conjunction' => 'AND', 'foo']));
+ /** @var \Drupal\search_api\Query\QueryInterface $query */
+
+ $field = $this->createTestField('body', 'entity:node/body');
+
+ $this->index->expects($this->atLeastOnce())
+ ->method('getFields')
+ ->will($this->returnValue(['body' => $field]));
+
+ $this->processor->setIndex($this->index);
+
+ $body_values = ['Some foo value'];
+ $fields = [
+ 'entity:node/body' => [
+ 'type' => 'text',
+ 'values' => $body_values,
+ ],
+ ];
+
+ $items = $this->createItems($this->index, 1, $fields);
+
+ $results = new ResultSet($query);
+ $results->setResultItems($items);
+ $results->setResultCount(1);
+ $highlighted_fields["body_2"][0] = 'Old highlighting text';
+ $highlighted_fields["body_2"][1] = 'More highlighting text';
+ $items[$this->itemIds[0]]->setExtraData('highlighted_fields', $highlighted_fields);
+
+ $this->processor->postprocessSearchResults($results);
+
+ $fields = $items[$this->itemIds[0]]->getExtraData('highlighted_fields');
+ $this->assertEquals('Some foo value', $fields['body'][0], 'Highlighting correctly applied to body field.');
+ $this->assertEquals('Old highlighting text', $fields["body_2"][0], 'Old highlighting data is preserved.');
+ $this->assertEquals('More highlighting text', $fields["body_2"][1], 'Old highlighting data is preserved.');
+ }
+
+ /**
+ * Tests whether highlighting works on a longer text.
+ */
+ public function testPostprocessSearchResultsExcerpt() {
+ $query = $this->getMock(QueryInterface::class);
+ $query->expects($this->once())
+ ->method('getProcessingLevel')
+ ->willReturn(QueryInterface::PROCESSING_FULL);
+ $query->expects($this->atLeastOnce())
+ ->method('getOriginalKeys')
+ ->will($this->returnValue(['#conjunction' => 'AND', 'congue']));
+ /** @var \Drupal\search_api\Query\QueryInterface $query */
+
+ $field = $this->createTestField('body', 'entity:node/body');
+
+ $this->index->expects($this->atLeastOnce())
+ ->method('getFields')
+ ->will($this->returnValue(['body' => $field]));
+
+ $this->processor->setIndex($this->index);
+
+ $body_values = [$this->getFieldBody()];
+ $fields = [
+ 'entity:node/body' => [
+ 'type' => 'text',
+ 'values' => $body_values,
+ ],
+ ];
+
+ $items = $this->createItems($this->index, 1, $fields);
+
+ $results = new ResultSet($query);
+ $results->setResultItems($items);
+ $results->setResultCount(1);
+
+ $this->processor->postprocessSearchResults($results);
+
+ $output = $results->getResultItems();
+ $excerpt = $output[$this->itemIds[0]]->getExcerpt();
+ $correct_output = '… tristique, ligula sit amet condimentum dapibus, lorem nunc congue velit, et dictum augue leo sodales augue. Maecenas …';
+ $this->assertEquals($correct_output, $excerpt, 'Excerpt was added.');
+ }
+
+ /**
+ * Tests whether highlighting works on a longer text matching near the end.
+ */
+ public function testPostprocessSearchResultsExerptMatchNearEnd() {
+ $query = $this->getMock(QueryInterface::class);
+ $query->expects($this->once())
+ ->method('getProcessingLevel')
+ ->willReturn(QueryInterface::PROCESSING_FULL);
+ $query->expects($this->atLeastOnce())
+ ->method('getOriginalKeys')
+ ->will($this->returnValue(['#conjunction' => 'AND', 'diam']));
+ /** @var \Drupal\search_api\Query\QueryInterface $query */
+
+ $field = $this->createTestField('body', 'entity:node/body');
+
+ $this->index->expects($this->atLeastOnce())
+ ->method('getFields')
+ ->will($this->returnValue(['body' => $field]));
+
+ $this->processor->setIndex($this->index);
+
+ $body_values = [$this->getFieldBody()];
+ $fields = [
+ 'entity:node/body' => [
+ 'type' => 'text',
+ 'values' => $body_values,
+ ],
+ ];
+
+ $items = $this->createItems($this->index, 1, $fields);
+
+ $results = new ResultSet($query);
+ $results->setResultItems($items);
+ $results->setResultCount(1);
+
+ $this->processor->postprocessSearchResults($results);
+
+ $output = $results->getResultItems();
+ $excerpt = $output[$this->itemIds[0]]->getExcerpt();
+ $correct_output = '… Fusce in mauris eu leo fermentum feugiat. Proin varius diam ante, non eleifend ipsum luctus sed. …';
+ $this->assertEquals($correct_output, $excerpt, 'Excerpt was added.');
+ }
+
+ /**
+ * Tests whether highlighting works with a changed excerpt length.
+ */
+ public function testPostprocessSearchResultsWithChangedExcerptLength() {
+ $this->processor->setConfiguration(['excerpt_length' => 64]);
+
+ $query = $this->getMock(QueryInterface::class);
+ $query->expects($this->once())
+ ->method('getProcessingLevel')
+ ->willReturn(QueryInterface::PROCESSING_FULL);
+ $query->expects($this->atLeastOnce())
+ ->method('getOriginalKeys')
+ ->will($this->returnValue('congue'));
+ /** @var \Drupal\search_api\Query\QueryInterface $query */
+
+ $field = $this->createTestField('body', 'entity:node/body');
+
+ $this->index->expects($this->atLeastOnce())
+ ->method('getFields')
+ ->will($this->returnValue(['body' => $field]));
+
+ $this->processor->setIndex($this->index);
+
+ $body_values = [$this->getFieldBody()];
+ $fields = [
+ 'entity:node/body' => [
+ 'type' => 'text',
+ 'values' => $body_values,
+ ],
+ ];
+
+ $items = $this->createItems($this->index, 1, $fields);
+
+ $results = new ResultSet($query);
+ $results->setResultItems($items);
+ $results->setResultCount(1);
+
+ $this->processor->postprocessSearchResults($results);
+
+ $output = $results->getResultItems();
+ $excerpt = $output[$this->itemIds[0]]->getExcerpt();
+ $correct_output = '… dapibus, lorem nunc congue velit, et dictum augue …';
+ $this->assertEquals($correct_output, $excerpt, 'Excerpt has correct reduced length.');
+ }
+
+ /**
+ * Tests whether adding an excerpt can be successfully disabled.
+ */
+ public function testPostprocessSearchResultsWithoutExcerpt() {
+ $this->processor->setConfiguration(['excerpt' => FALSE]);
+
+ $query = $this->getMock(QueryInterface::class);
+ $query->expects($this->once())
+ ->method('getProcessingLevel')
+ ->willReturn(QueryInterface::PROCESSING_FULL);
+ $query->expects($this->atLeastOnce())
+ ->method('getOriginalKeys')
+ ->will($this->returnValue(['#conjunction' => 'AND', 'congue']));
+ /** @var \Drupal\search_api\Query\QueryInterface $query */
+
+ $field = $this->createTestField('body', 'entity:node/body');
+
+ $this->index->expects($this->atLeastOnce())
+ ->method('getFields')
+ ->will($this->returnValue(['body' => $field]));
+
+ $this->processor->setIndex($this->index);
+
+ $body_values = [$this->getFieldBody()];
+ $fields = [
+ 'entity:node/body' => [
+ 'type' => 'text',
+ 'values' => $body_values,
+ ],
+ ];
+
+ $items = $this->createItems($this->index, 1, $fields);
+
+ $results = new ResultSet($query);
+ $results->setResultItems($items);
+ $results->setResultCount(1);
+
+ $this->processor->postprocessSearchResults($results);
+
+ $excerpt = $items[$this->itemIds[0]]->getExcerpt();
+
+ $this->assertEmpty($excerpt, 'No excerpt added when disabled.');
+ }
+
+ /**
+ * Tests whether highlighting works on a longer text.
+ */
+ public function testPostprocessSearchResultsWithComplexKeys() {
+ $keys = [
+ '#conjunction' => 'AND',
+ [
+ '#conjunction' => 'OR',
+ 'foo',
+ 'bar',
+ ],
+ 'baz',
+ [
+ '#conjunction' => 'OR',
+ '#negation' => TRUE,
+ 'text',
+ 'will',
+ ],
+ ];
+ $query = $this->getMock(QueryInterface::class);
+ $query->expects($this->once())
+ ->method('getProcessingLevel')
+ ->willReturn(QueryInterface::PROCESSING_FULL);
+ $query->expects($this->atLeastOnce())
+ ->method('getOriginalKeys')
+ ->will($this->returnValue($keys));
+ /** @var \Drupal\search_api\Query\QueryInterface $query */
+
+ $body_field = $this->createTestField('body', 'entity:node/body');
+
+ $this->index->expects($this->atLeastOnce())
+ ->method('getFields')
+ ->will($this->returnValue(['body' => $body_field]));
+
+ $this->processor->setIndex($this->index);
+
+ $fields = [
+ 'entity:node/body' => [
+ 'type' => 'text',
+ 'values' => [
+ 'This foo text bar will get baz riddled with <strong> tags.',
+ ],
+ ],
+ ];
+
+ $items = $this->createItems($this->index, 1, $fields);
+
+ $results = new ResultSet($query);
+ $results->setResultItems($items);
+ $results->setResultCount(1);
+
+ $this->processor->postprocessSearchResults($results);
+
+ $fields = $items[$this->itemIds[0]]->getExtraData('highlighted_fields');
+ $this->assertEquals('This foo text bar will get baz riddled with <strong> tags.', $fields['body'][0], 'Highlighting is correctly applied when keys are complex.');
+ $correct_output = '… This foo text bar will get baz riddled with <strong> tags. …';
+ $excerpt = $items[$this->itemIds[0]]->getExcerpt();
+ $this->assertEquals($correct_output, $excerpt, 'Excerpt was added.');
+ }
+
+ /**
+ * Tests field highlighting and excerpts for two fields.
+ */
+ public function testPostprocessSearchResultsWithTwoFields() {
+ $query = $this->getMock(QueryInterface::class);
+ $query->expects($this->once())
+ ->method('getProcessingLevel')
+ ->willReturn(QueryInterface::PROCESSING_FULL);
+ $query->expects($this->atLeastOnce())
+ ->method('getOriginalKeys')
+ ->will($this->returnValue(['#conjunction' => 'AND', 'foo']));
+ /** @var \Drupal\search_api\Query\QueryInterface $query */
+
+ $body_field = $this->createTestField('body', 'entity:node/body');
+ $title_field = $this->createTestField('title', 'title');
+
+ $this->index->expects($this->atLeastOnce())
+ ->method('getFields')
+ ->will($this->returnValue([
+ 'body' => $body_field,
+ 'title' => $title_field,
+ ]));
+
+ $this->processor->setIndex($this->index);
+
+ $body_values = ['Some foo value', 'foo bar'];
+ $title_values = ['Title foo'];
+ $fields = [
+ 'entity:node/body' => [
+ 'type' => 'text',
+ 'values' => $body_values,
+ ],
+ 'title' => [
+ 'type' => 'text',
+ 'values' => $title_values,
+ ],
+ ];
+
+ $items = $this->createItems($this->index, 1, $fields);
+
+ $results = new ResultSet($query);
+ $results->setResultItems($items);
+ $results->setResultCount(1);
+
+ $this->processor->postprocessSearchResults($results);
+
+ $fields = $items[$this->itemIds[0]]->getExtraData('highlighted_fields');
+ $this->assertEquals('Some foo value', $fields['body'][0], 'Highlighting is correctly applied to first body field value.');
+ $this->assertEquals('foo bar', $fields['body'][1], 'Highlighting is correctly applied to second body field value.');
+ $this->assertEquals('Title foo', $fields['title'][0], 'Highlighting is correctly applied to title field.');
+
+ $excerpt = $items[$this->itemIds[0]]->getExcerpt();
+ $this->assertContains('Some foo value', $excerpt);
+ $this->assertContains('foo bar', $excerpt);
+ $this->assertContains('Title foo', $excerpt);
+ $this->assertEquals(4, substr_count($excerpt, '…'));
+ }
+
+ /**
+ * Tests field highlighting and excerpts with two items.
+ */
+ public function testPostprocessSearchResultsWithTwoItems() {
+ $query = $this->getMock(QueryInterface::class);
+ $query->expects($this->once())
+ ->method('getProcessingLevel')
+ ->willReturn(QueryInterface::PROCESSING_FULL);
+ $query->expects($this->atLeastOnce())
+ ->method('getOriginalKeys')
+ ->will($this->returnValue(['#conjunction' => 'OR', 'foo']));
+ /** @var \Drupal\search_api\Query\QueryInterface $query */
+
+ $body_field = $this->createTestField('body', 'entity:node/body');
+
+ $this->index->expects($this->atLeastOnce())
+ ->method('getFields')
+ ->will($this->returnValue(['body' => $body_field]));
+
+ $this->processor->setIndex($this->index);
+
+ $body_values = ['Some foo value', 'foo bar'];
+ $fields = [
+ 'entity:node/body' => [
+ 'type' => 'text',
+ 'values' => $body_values,
+ ],
+ ];
+
+ $items = $this->createItems($this->index, 2, $fields);
+
+ $items[$this->itemIds[1]]->getField('body')
+ ->setValues(['The second item also contains foo in its body.']);
+
+ $results = new ResultSet($query);
+ $results->setResultItems($items);
+ $results->setResultCount(1);
+
+ $this->processor->postprocessSearchResults($results);
+
+ $fields = $items[$this->itemIds[0]]->getExtraData('highlighted_fields');
+ $this->assertEquals('Some foo value', $fields['body'][0], 'Highlighting is correctly applied to first body field value.');
+ $this->assertEquals('foo bar', $fields['body'][1], 'Highlighting is correctly applied to second body field value.');
+
+ $fields = $items[$this->itemIds[1]]->getExtraData('highlighted_fields');
+ $this->assertEquals('The second item also contains foo in its body.', $fields['body'][0], 'Highlighting is correctly applied to second item.');
+
+ $excerpt1 = '… Some foo value … foo bar …';
+ $excerpt2 = '… The second item also contains foo in its body. …';
+ $this->assertEquals($excerpt1, $items[$this->itemIds[0]]->getExcerpt(), 'Correct excerpt created from two text fields.');
+ $this->assertEquals($excerpt2, $items[$this->itemIds[1]]->getExcerpt(), 'Correct excerpt created for second item.');
+ }
+
+ /**
+ * Tests excerpts with some fields excluded.
+ */
+ public function testExcerptExcludeFields() {
+ $query = $this->getMock(QueryInterface::class);
+ $query->expects($this->once())
+ ->method('getProcessingLevel')
+ ->willReturn(QueryInterface::PROCESSING_FULL);
+ $query->expects($this->atLeastOnce())
+ ->method('getOriginalKeys')
+ ->will($this->returnValue(['#conjunction' => 'AND', 'foo']));
+ /** @var \Drupal\search_api\Query\QueryInterface $query */
+
+ $body_field = $this->createTestField('body', 'entity:node/body');
+ $title_field = $this->createTestField('title', 'title');
+
+ $this->index->expects($this->atLeastOnce())
+ ->method('getFields')
+ ->will($this->returnValue([
+ 'body' => $body_field,
+ 'title' => $title_field,
+ ]));
+
+ $this->processor->setIndex($this->index);
+
+ $this->processor->setConfiguration([
+ 'exclude_fields' => ['title'],
+ ]);
+
+ $body_values = ['Some foo value', 'foo bar'];
+ $title_values = ['Title foo'];
+ $fields = [
+ 'entity:node/body' => [
+ 'type' => 'text',
+ 'values' => $body_values,
+ ],
+ 'title' => [
+ 'type' => 'text',
+ 'values' => $title_values,
+ ],
+ ];
+
+ $items = $this->createItems($this->index, 1, $fields);
+
+ $results = new ResultSet($query);
+ $results->setResultItems($items);
+ $results->setResultCount(1);
+
+ $this->processor->postprocessSearchResults($results);
+
+ $fields = $items[$this->itemIds[0]]->getExtraData('highlighted_fields');
+ $this->assertEquals('Some foo value', $fields['body'][0], 'Highlighting is correctly applied to first body field value.');
+ $this->assertEquals('foo bar', $fields['body'][1], 'Highlighting is correctly applied to second body field value.');
+ $this->assertEquals('Title foo', $fields['title'][0], 'Highlighting is correctly applied to title field.');
+
+ $excerpt = '… Some foo value … foo bar …';
+ $this->assertEquals($excerpt, $items[$this->itemIds[0]]->getExcerpt(), 'Correct excerpt created ignoring title field.');
+ }
+
+ /**
+ * Tests that field extraction in the processor works correctly.
+ */
+ public function testFieldExtraction() {
+ /** @var \Drupal\Tests\search_api\Unit\TestComplexDataInterface|\PHPUnit_Framework_MockObject_MockObject $object */
+ $object = $this->getMock(TestComplexDataInterface::class);
+ $bar_foo_property = $this->getMock(TypedDataInterface::class);
+ $bar_foo_property->method('getValue')
+ ->willReturn('value3 foo');
+ $bar_foo_property->method('getDataDefinition')
+ ->willReturn(new DataDefinition());
+ $bar_property = $this->getMock(TestComplexDataInterface::class);
+ $bar_property->method('get')
+ ->willReturnMap([
+ ['foo', $bar_foo_property],
+ ]);
+ $bar_property->method('getProperties')
+ ->willReturn([
+ 'foo' => TRUE,
+ ]);
+ $foobar_property = $this->getMock(TypedDataInterface::class);
+ $foobar_property->method('getValue')
+ ->willReturn('wrong_value2 foo');
+ $foobar_property->method('getDataDefinition')
+ ->willReturn(new DataDefinition());
+ $object->method('get')
+ ->willReturnMap([
+ ['bar', $bar_property],
+ ['foobar', $foobar_property],
+ ]);
+ $object->method('getProperties')
+ ->willReturn([
+ 'bar' => TRUE,
+ 'foobar' => TRUE,
+ ]);
+
+ $this->index->method('getFields')
+ ->willReturn([
+ 'field1' => $this->createTestField('field1', 'entity:test1/bar:foo'),
+ 'field2' => $this->createTestField('field2', 'entity:test2/foobar'),
+ 'field3' => $this->createTestField('field3', 'foo'),
+ 'field4' => $this->createTestField('field4', 'baz', FALSE),
+ 'field5' => $this->createTestField('field5', 'entity:test1/foobar'),
+ ]);
+ $this->index->method('getPropertyDefinitions')
+ ->willReturnMap([
+ [
+ NULL,
+ [
+ 'foo' => new ProcessorProperty([
+ 'processor_id' => 'processor1',
+ ]),
+ ],
+ ],
+ [
+ 'entity:test1',
+ [
+ 'bar' => new DataDefinition(),
+ 'foobar' => new DataDefinition(),
+ ],
+ ],
+ ]);
+ $processor_mock = $this->getMock(ProcessorInterface::class);
+ $processor_mock->method('addFieldValues')
+ ->willReturnCallback(function (ItemInterface $item) {
+ foreach ($item->getFields(FALSE) as $field) {
+ if ($field->getCombinedPropertyPath() == 'foo') {
+ $field->setValues(['value4 foo', 'value5 foo']);
+ }
+ }
+ });
+ $this->index->method('getProcessorsByStage')
+ ->willReturnMap([
+ [
+ ProcessorInterface::STAGE_ADD_PROPERTIES,
+ [],
+ [
+ 'aggregated_field' => $this->processor,
+ 'processor1' => $processor_mock,
+ ],
+ ],
+ ]);
+ $this->processor->setIndex($this->index);
+
+ /** @var \Drupal\search_api\Datasource\DatasourceInterface|\PHPUnit_Framework_MockObject_MockObject $datasource */
+ $datasource = $this->getMock(DatasourceInterface::class);
+ $datasource->method('getPluginId')
+ ->willReturn('entity:test1');
+
+ $item = \Drupal::getContainer()
+ ->get('search_api.fields_helper')
+ ->createItem($this->index, 'id', $datasource);
+ $item->setOriginalObject($object);
+ $field = $this->createTestField('field4', 'baz')
+ ->addValue('wrong_value1 foo');
+ $item->setField('field4', $field);
+ $field = $this->createTestField('field5', 'entity:test1/foobar')
+ ->addValue('value1 foo')
+ ->addValue('value2 foo');
+ $item->setField('field5', $field);
+
+ $this->processor->setConfiguration(['excerpt' => FALSE]);
+ /** @var \Drupal\search_api\Query\QueryInterface|\PHPUnit_Framework_MockObject_MockObject $query */
+ $query = $this->getMock(QueryInterface::class);
+ $query->method('getOriginalKeys')
+ ->willReturn('foo');
+ $query->expects($this->once())
+ ->method('getProcessingLevel')
+ ->willReturn(QueryInterface::PROCESSING_FULL);
+ $results = new ResultSet($query);
+ $results->setResultCount(1)
+ ->setResultItems([$item]);
+ $this->processor->postprocessSearchResults($results);
+
+ $expected = [
+ 'field1' => [
+ 'value3 foo',
+ ],
+ 'field3' => [
+ 'value4 foo',
+ 'value5 foo',
+ ],
+ 'field5' => [
+ 'value1 foo',
+ 'value2 foo',
+ ],
+ ];
+ $fields = $item->getExtraData('highlighted_fields');
+ $this->assertEquals($expected, $fields);
+ }
+
+ /**
+ * Creates a field object for testing.
+ *
+ * @param string $id
+ * The field ID to set.
+ * @param string $combined_property_path
+ * The combined property path of the field.
+ * @param bool $text
+ * (optional) Whether the field should be a fulltext field or not.
+ *
+ * @return \Drupal\search_api\Item\FieldInterface
+ * A field object.
+ */
+ protected function createTestField($id, $combined_property_path, $text = TRUE) {
+ $field = new Field($this->index, $id);
+ list ($datasource_id, $property_path) = Utility::splitCombinedId($combined_property_path);
+ $field->setDatasourceId($datasource_id);
+ $field->setPropertyPath($property_path);
+ $field->setType($text ? 'text' : 'string');
+
+ return $field;
+ }
+
+ /**
+ * Returns a long text to use for highlighting tests.
+ *
+ * @return string
+ * A Lorem Ipsum text.
+ */
+ protected function getFieldBody() {
+ return 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris dictum ultricies sapien id consequat.
+Fusce tristique erat at dui ultricies, eu rhoncus odio rutrum. Praesent viverra mollis mauris a cursus.
+Curabitur at condimentum orci. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.
+Praesent suscipit massa non pretium volutpat. Suspendisse id lacus facilisis, fringilla mauris vitae, tincidunt turpis.
+Proin a euismod libero. Nam aliquet neque nulla, nec placerat libero accumsan id. Quisque sit amet consequat lacus.
+Donec mauris erat, iaculis id nisl nec, dapibus posuere lectus. Sed ultrices libero id elit volutpat sagittis.
+Donec a tortor ullamcorper, tempus lectus at, ultrices felis. Nam nibh magna, dictum in massa ut, ornare venenatis enim.
+Phasellus enim massa, condimentum eu sem vel, consectetur fermentum erat. Cras porttitor ut dolor interdum vehicula.
+Vestibulum erat arcu, placerat quis gravida quis, venenatis vel magna. Pellentesque pellentesque lacus ut feugiat auctor.
+Mauris libero magna, dictum in fermentum nec, blandit non augue.
+Morbi sed viverra libero.Phasellus sem velit, sollicitudin in felis lacinia, suscipit auctor dolor.
+Praesent dignissim dolor sed lobortis mattis.
+Ut tristique, ligula sit amet condimentum dapibus, lorem nunc congue velit, et dictum augue leo sodales augue.
+Maecenas eget mi ac massa sagittis malesuada. Fusce ac purus vel ipsum imperdiet vulputate.
+Mauris vestibulum sapien sit amet elementum tincidunt. Aenean sollicitudin tortor pulvinar ante commodo sagittis.
+Integer in nisi consequat, elementum felis in, consequat purus. Maecenas blandit ipsum id tellus accumsan, sit amet venenatis orci vestibulum.
+Ut id erat venenatis, vehicula mi eget, gravida odio. Etiam dapibus purus in massa condimentum, vitae lobortis est aliquam.
+Morbi tristique velit et sem varius rhoncus. In tincidunt sagittis libero. Integer interdum sit amet sem sit amet sodales.
+Donec sit amet arcu sit amet leo tristique dignissim vel ut enim. Nulla faucibus lacus eu adipiscing semper. Sed ut sodales erat.
+Sed mauris purus, tempor non eleifend et, mollis ut lacus. Etiam interdum velit justo, nec imperdiet nunc pulvinar sit amet.
+Sed eu lacus eget augue laoreet vehicula id sed sem. Maecenas at condimentum massa, et pretium nulla. Aliquam sed nibh velit.
+Quisque turpis lacus, sodales nec malesuada nec, commodo non purus.
+Cras pellentesque, lectus ut imperdiet euismod, purus sem convallis tortor, ut fermentum elit nulla et quam.
+Mauris luctus mattis enim non accumsan. Sed consequat sapien lorem, in ultricies orci posuere nec.
+Fusce in mauris eu leo fermentum feugiat. Proin varius diam ante, non eleifend ipsum luctus sed.';
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Unit/Processor/HtmlFilterTest.php b/sites/all/modules/contrib/search/search_api/tests/src/Unit/Processor/HtmlFilterTest.php
new file mode 100644
index 000000000..3984528d0
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Unit/Processor/HtmlFilterTest.php
@@ -0,0 +1,283 @@
+setUpMockContainer();
+
+ $this->processor = new HtmlFilter([], 'html_filter', []);
+ }
+
+ /**
+ * Tests preprocessing field values with "title" settings.
+ *
+ * @param string $passed_value
+ * The value that should be passed into process().
+ * @param string $expected_value
+ * The expected processed value.
+ * @param bool $title_config
+ * The value to set for the processor's "title" setting.
+ *
+ * @dataProvider titleConfigurationDataProvider
+ */
+ public function testTitleConfiguration($passed_value, $expected_value, $title_config) {
+ $configuration = [
+ 'tags' => [],
+ 'title' => $title_config,
+ 'alt' => FALSE,
+ ];
+ $this->processor->setConfiguration($configuration);
+ $type = 'text';
+ $this->invokeMethod('processFieldValue', [&$passed_value, $type]);
+ $this->assertEquals($expected_value, $passed_value);
+ }
+
+ /**
+ * Data provider for testTitleConfiguration().
+ *
+ * @return array
+ * An array of argument arrays for testTitleConfiguration().
+ */
+ public function titleConfigurationDataProvider() {
+ return [
+ ['word', 'word', FALSE],
+ ['word', 'word', TRUE],
+ ['word
', 'word', TRUE],
+ ['word
', 'TITLE word', TRUE],
+ ['word
', 'word', FALSE],
+ ['word
', 'word', TRUE],
+ ['word', 'TITLE word', TRUE],
+ ];
+ }
+
+ /**
+ * Tests preprocessing field values with "alt" settings.
+ *
+ * @param string $passed_value
+ * The value that should be passed into process().
+ * @param mixed $expected_value
+ * The expected processed value.
+ * @param bool $alt_config
+ * The value to set for the processor's "alt" setting.
+ *
+ * @dataProvider altConfigurationDataProvider
+ */
+ public function testAltConfiguration($passed_value, $expected_value, $alt_config) {
+ $configuration = [
+ 'tags' => ['img' => '2'],
+ 'title' => FALSE,
+ 'alt' => $alt_config,
+ ];
+ $this->processor->setConfiguration($configuration);
+ $type = 'text';
+ $this->invokeMethod('processFieldValue', [&$passed_value, $type]);
+ $this->assertEquals($expected_value, $passed_value);
+ }
+
+ /**
+ * Data provider method for testAltConfiguration().
+ *
+ * @return array
+ * An array of argument arrays for testAltConfiguration().
+ */
+ public function altConfigurationDataProvider() {
+ return [
+ ['word', [Utility::createTextToken('word')], FALSE],
+ ['word', [Utility::createTextToken('word')], TRUE],
+ [
+ '

word',
+ [Utility::createTextToken('word')],
+ TRUE,
+ ],
+ [
+ '
![ALT]()
word',
+ [
+ Utility::createTextToken('ALT', 2),
+ Utility::createTextToken('word'),
+ ],
+ TRUE,
+ ],
+ [
+ '
![ALT]()
word',
+ [Utility::createTextToken('word')],
+ FALSE,
+ ],
+ [
+ '
![]()
word',
+ [Utility::createTextToken('word')],
+ TRUE,
+ ],
+ [
+ '

word ',
+ [
+ Utility::createTextToken('ALT', 2),
+ Utility::createTextToken('word'),
+ ],
+ TRUE,
+ ],
+ // Test fault tolerance.
+ [
+ 'a < b',
+ [
+ Utility::createTextToken('a < b'),
+ ],
+ TRUE,
+ ],
+ ];
+ }
+
+ /**
+ * Tests preprocessing field values with "alt" settings.
+ *
+ * @param string $passed_value
+ * The value that should be passed into process().
+ * @param mixed $expected_value
+ * The expected processed value.
+ * @param float[] $tags_config
+ * The value to set for the processor's "tags" setting.
+ *
+ * @dataProvider tagConfigurationDataProvider
+ */
+ public function testTagConfiguration($passed_value, $expected_value, array $tags_config) {
+ $configuration = [
+ 'tags' => $tags_config,
+ 'title' => TRUE,
+ 'alt' => TRUE,
+ ];
+ $this->processor->setConfiguration($configuration);
+ $type = 'text';
+ $this->invokeMethod('processFieldValue', [&$passed_value, $type]);
+ $this->assertEquals($expected_value, $passed_value);
+ }
+
+ /**
+ * Data provider method for testTagConfiguration().
+ *
+ * @return array
+ * An array of argument arrays for testTagConfiguration().
+ */
+ public function tagConfigurationDataProvider() {
+ $complex_test = [
+ '
Foo Bar Baz
+
+
Bla Bla Bla. Important: Bla.
+

+
This is hidden',
+ [
+ Utility::createTextToken('Foo Bar', 3.0),
+ Utility::createTextToken('Baz', 4.5),
+ Utility::createTextToken('Bla Bla Bla.', 1.0),
+ Utility::createTextToken('Foobar Important:', 2.0),
+ Utility::createTextToken('Bla.', 1.0),
+ Utility::createTextToken('Some picture', 0.5),
+ ],
+ [
+ 'em' => 1.5,
+ 'strong' => 2.0,
+ 'h2' => 3.0,
+ 'img' => 0.5,
+ 'span' => 0,
+ ],
+ ];
+ $tags_config = ['h2' => '2'];
+ return [
+ ['h2word', 'h2word', []],
+ ['h2word', [Utility::createTextToken('h2word')], $tags_config],
+ [
+ 'foo bar
h2word
',
+ [
+ Utility::createTextToken('foo bar'),
+ Utility::createTextToken('h2word', 2.0),
+ ],
+ $tags_config,
+ ],
+ [
+ 'foo bar
h2word
',
+ [
+ Utility::createTextToken('foo bar'),
+ Utility::createTextToken('h2word', 2.0),
+ ],
+ $tags_config,
+ ],
+ [
+ '
word
',
+ [Utility::createTextToken('word', 2)],
+ ['div' => 2],
+ ],
+ $complex_test,
+ ];
+ }
+
+ /**
+ * Tests whether strings are correctly handled.
+ *
+ * String field handling should be completely independent of configuration.
+ *
+ * @param array $config
+ * The configuration to set on the processor.
+ *
+ * @dataProvider stringProcessingDataProvider
+ */
+ public function testStringProcessing(array $config) {
+ $this->processor->setConfiguration($config);
+
+ $passed_value = '
Foo Bar Baz
+
+
Bla Bla Bla. Important: Bla.
+

+
This is hidden';
+ $expected_value = preg_replace('/\s+/', ' ', strip_tags($passed_value));
+
+ $type = 'string';
+ $this->invokeMethod('processFieldValue', [&$passed_value, $type]);
+ $this->assertEquals($expected_value, $passed_value);
+ }
+
+ /**
+ * Provides a few sets of HTML filter configuration.
+ *
+ * @return array
+ * An array of argument arrays for testStringProcessing(), where each array
+ * contains a HTML filter configuration as the only value.
+ */
+ public function stringProcessingDataProvider() {
+ $configs = [];
+ $configs[] = [[]];
+ $config['tags'] = [
+ 'h2' => 2.0,
+ 'span' => 4.0,
+ 'strong' => 1.5,
+ 'p' => 0,
+ ];
+ $configs[] = [$config];
+ $config['title'] = TRUE;
+ $configs[] = [$config];
+ $config['alt'] = TRUE;
+ $configs[] = [$config];
+ unset($config['tags']);
+ $configs[] = [$config];
+ return $configs;
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Unit/Processor/IgnoreCaseTest.php b/sites/all/modules/contrib/search/search_api/tests/src/Unit/Processor/IgnoreCaseTest.php
new file mode 100644
index 000000000..71faece81
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Unit/Processor/IgnoreCaseTest.php
@@ -0,0 +1,57 @@
+processor = new IgnoreCase([], 'string', []);
+ }
+
+ /**
+ * Tests the process() method.
+ *
+ * @param string $passed_value
+ * The value that should be passed into process().
+ * @param string $expected_value
+ * The expected processed value.
+ *
+ * @dataProvider processDataProvider
+ */
+ public function testProcess($passed_value, $expected_value) {
+ $this->invokeMethod('process', [&$passed_value]);
+ $this->assertEquals($passed_value, $expected_value);
+ }
+
+ /**
+ * Provides sets of arguments for testProcess().
+ *
+ * @return array[]
+ * Arrays of arguments for testProcess().
+ */
+ public function processDataProvider() {
+ return [
+ ['Foo bar', 'foo bar'],
+ ['foo Bar', 'foo bar'],
+ ['Foo Bar', 'foo bar'],
+ ['Foo bar BaZ, ÄÖÜÀÁ<>»«.', 'foo bar baz, äöüàá<>»«.'],
+ ];
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Unit/Processor/IgnoreCharacterTest.php b/sites/all/modules/contrib/search/search_api/tests/src/Unit/Processor/IgnoreCharacterTest.php
new file mode 100644
index 000000000..51bb9cf8f
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Unit/Processor/IgnoreCharacterTest.php
@@ -0,0 +1,139 @@
+processor = new IgnoreCharacters(['ignorable' => ''], 'ignore_character', []);
+ }
+
+ /**
+ * Tests preprocessing with different ignorable character sets.
+ *
+ * @param string $passed_value
+ * The value that should be passed into process().
+ * @param string $expected_value
+ * The expected processed value.
+ * @param string[] $character_classes
+ * The "character_sets" setting to set on the processor.
+ *
+ * @dataProvider ignoreCharacterSetsDataProvider
+ */
+ public function testIgnoreCharacterSets($passed_value, $expected_value, array $character_classes) {
+ $this->processor->setConfiguration(['strip' => ['character_sets' => $character_classes]]);
+ $this->invokeMethod('process', [&$passed_value, 'text']);
+ $this->assertEquals($expected_value, $passed_value);
+ }
+
+ /**
+ * Data provider for testValueConfiguration().
+ */
+ public function ignoreCharacterSetsDataProvider() {
+ return [
+ ['word_s', 'words', ['Pc' => 'Pc']],
+ ['word⁔s', 'words', ['Pc' => 'Pc']],
+
+ ['word〜s', 'words', ['Pd' => 'Pd']],
+ ['w–ord⸗s', 'words', ['Pd' => 'Pd']],
+
+ ['word⌉s', 'words', ['Pe' => 'Pe']],
+ ['word⦊s〕', 'words', ['Pe' => 'Pe']],
+
+ ['word»s', 'words', ['Pf' => 'Pf']],
+ ['word⸍s', 'words', ['Pf' => 'Pf']],
+
+ ['word⸂s', 'words', ['Pi' => 'Pi']],
+ ['w«ord⸉s', 'words', ['Pi' => 'Pi']],
+
+ ['words%', 'words', ['Po' => 'Po']],
+ ['wo*rd/s', 'words', ['Po' => 'Po']],
+
+ ['word༺s', 'words', ['Ps' => 'Ps']],
+ ['w❮ord⌈s', 'words', ['Ps' => 'Ps']],
+
+ ['word៛s', 'words', ['Sc' => 'Sc']],
+ ['wo₥rd₦s', 'words', ['Sc' => 'Sc']],
+
+ ['w˓ords', 'words', ['Sk' => 'Sk']],
+ ['wo˘rd˳s', 'words', ['Sk' => 'Sk']],
+
+ ['word×s', 'words', ['Sm' => 'Sm']],
+ ['wo±rd؈s', 'words', ['Sm' => 'Sm']],
+
+ ['wo᧧rds', 'words', ['So' => 'So']],
+ ['w᧶ord᧲s', 'words', ['So' => 'So']],
+
+ ["wor\x0Ads", 'words', ['Cc' => 'Cc']],
+ ["wo\x0Crds", 'words', ['Cc' => 'Cc']],
+
+ ['words', 'words', ['Cf' => 'Cf']],
+ ['words', 'words', ['Cf' => 'Cf']],
+
+ ['words', 'words', ['Co' => 'Co']],
+ ['words', 'words', ['Co' => 'Co']],
+
+ ['wordॊs', 'words', ['Mc' => 'Mc']],
+ ['worौdংs', 'words', ['Mc' => 'Mc']],
+
+ ['wo⃞rds', 'words', ['Me' => 'Me']],
+ ['wor⃤⃟ds', 'words', ['Me' => 'Me']],
+
+ ['woྰrds', 'words', ['Mn' => 'Mn']],
+ ['worྵdྶs', 'words', ['Mn' => 'Mn']],
+
+ ['woྰrds', 'words', ['Mn' => 'Mn', 'Pd' => 'Pd', 'Pe' => 'Pe']],
+ ['worྵdྶs', 'words', ['Mn' => 'Mn', 'Pd' => 'Pd', 'Pe' => 'Pe']],
+ ];
+ }
+
+ /**
+ * Tests preprocessing with the "Ignorable characters" setting.
+ *
+ * @param string $passed_value
+ * The value that should be passed into process().
+ * @param string $expected_value
+ * The expected processed value.
+ * @param string $ignorable
+ * The "ignorable" setting to set on the processor.
+ *
+ * @dataProvider ignorableCharactersDataProvider
+ */
+ public function testIgnorableCharacters($passed_value, $expected_value, $ignorable) {
+ $this->processor->setConfiguration(['ignorable' => $ignorable, 'strip' => ['character_sets' => []]]);
+ $this->invokeMethod('process', [&$passed_value, 'text']);
+ $this->assertEquals($expected_value, $passed_value);
+ }
+
+ /**
+ * Provides sets of test parameters for testIgnorableCharacters().
+ *
+ * @return array
+ * Sets of arguments for testIgnorableCharacters().
+ */
+ public function ignorableCharactersDataProvider() {
+ return [
+ ['abcde', 'ace', '[bd]'],
+ [['abcde', 'abcdef'], ['ace', 'ace'], '[bdf]'],
+ ["ab.c'de", "a.'de", '[b-c]'],
+ ['foo 13$%& (bar)[93]', 'foo $%& (bar)[]', '\d'],
+ ];
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Unit/Processor/ProcessorTestTrait.php b/sites/all/modules/contrib/search/search_api/tests/src/Unit/Processor/ProcessorTestTrait.php
new file mode 100644
index 000000000..58cab130e
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Unit/Processor/ProcessorTestTrait.php
@@ -0,0 +1,35 @@
+processor));
+ $method = $class->getMethod($method_name);
+ $method->setAccessible(TRUE);
+ return $method->invokeArgs($this->processor, $args);
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Unit/Processor/RoleFilterTest.php b/sites/all/modules/contrib/search/search_api/tests/src/Unit/Processor/RoleFilterTest.php
new file mode 100644
index 000000000..0961001cb
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Unit/Processor/RoleFilterTest.php
@@ -0,0 +1,124 @@
+setUpMockContainer();
+
+ $this->processor = new RoleFilter([], 'role_filter', []);
+
+ /** @var \Drupal\search_api\IndexInterface $index */
+ $index = $this->getMock('Drupal\search_api\IndexInterface');
+
+ $node_datasource = $this->getMock(DatasourceInterface::class);
+ $node_datasource->expects($this->any())
+ ->method('getEntityTypeId')
+ ->will($this->returnValue('node'));
+ /** @var \Drupal\search_api\Datasource\DatasourceInterface $node_datasource */
+ $user_datasource = $this->getMock(DatasourceInterface::class);
+ $user_datasource->expects($this->any())
+ ->method('getEntityTypeId')
+ ->will($this->returnValue('user'));
+ /** @var \Drupal\search_api\Datasource\DatasourceInterface $user_datasource */
+
+ $fields_helper = \Drupal::getContainer()->get('search_api.fields_helper');
+ $item = $fields_helper->createItem($index, Utility::createCombinedId('entity:node', '1:en'), $node_datasource);
+ $node = $this->getMockBuilder(TestNodeInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ /** @var \Drupal\node\NodeInterface $node */
+ $item->setOriginalObject(EntityAdapter::createFromEntity($node));
+ $this->items[$item->getId()] = $item;
+
+ $item = $fields_helper->createItem($index, Utility::createCombinedId('entity:user', '1:en'), $user_datasource);
+ $account1 = $this->getMockBuilder(TestUserInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $account1->expects($this->any())
+ ->method('getRoles')
+ ->will($this->returnValue(['authenticated' => 'authenticated', 'editor' => 'editor']));
+ /** @var \Drupal\user\UserInterface $account1 */
+ $item->setOriginalObject(EntityAdapter::createFromEntity($account1));
+ $this->items[$item->getId()] = $item;
+
+ $item = $fields_helper->createItem($index, Utility::createCombinedId('entity:user', '2:en'), $user_datasource);
+ $account2 = $this->getMockBuilder(TestUserInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $account2->expects($this->any())
+ ->method('getRoles')
+ ->will($this->returnValue(['authenticated' => 'authenticated']));
+ /** @var \Drupal\user\UserInterface $account2 */
+ $item->setOriginalObject(EntityAdapter::createFromEntity($account2));
+ $this->items[$item->getId()] = $item;
+ }
+
+ /**
+ * Tests preprocessing search items with an inclusive filter.
+ */
+ public function testFilterInclusive() {
+ $configuration['roles'] = ['authenticated' => 'authenticated'];
+ $configuration['default'] = 0;
+ $this->processor->setConfiguration($configuration);
+
+ $this->processor->alterIndexedItems($this->items);
+
+ $this->assertTrue(!empty($this->items[Utility::createCombinedId('entity:user', '1:en')]), 'User with two roles was not removed.');
+ $this->assertTrue(!empty($this->items[Utility::createCombinedId('entity:user', '2:en')]), 'User with only the authenticated role was not removed.');
+ $this->assertTrue(!empty($this->items[Utility::createCombinedId('entity:node', '1:en')]), 'Node item was not removed.');
+ }
+
+ /**
+ * Tests preprocessing search items with an exclusive filter.
+ */
+ public function testFilterExclusive() {
+ $configuration['roles'] = ['editor' => 'editor'];
+ $configuration['default'] = 1;
+ $this->processor->setConfiguration($configuration);
+
+ $this->processor->alterIndexedItems($this->items);
+
+ $this->assertTrue(empty($this->items[Utility::createCombinedId('entity:user', '1:en')]), 'User with editor role was successfully removed.');
+ $this->assertTrue(!empty($this->items[Utility::createCombinedId('entity:user', '2:en')]), 'User without the editor role was not removed.');
+ $this->assertTrue(!empty($this->items[Utility::createCombinedId('entity:node', '1:en')]), 'Node item was not removed.');
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Unit/Processor/StemmerTest.php b/sites/all/modules/contrib/search/search_api/tests/src/Unit/Processor/StemmerTest.php
new file mode 100644
index 000000000..9e727cbd9
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Unit/Processor/StemmerTest.php
@@ -0,0 +1,240 @@
+setUpMockContainer();
+
+ $this->processor = new Stemmer([], 'string', []);
+ }
+
+ /**
+ * Tests the preprocessIndexItems() method.
+ *
+ * @covers ::preprocessIndexItems
+ */
+ public function testPreprocessIndexItems() {
+ $index = $this->getMock(IndexInterface::class);
+
+ $item_en = $this->getMockBuilder(ItemInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $item_en->method('getLanguage')->willReturn('en');
+ $field_en = new Field($index, 'foo');
+ $field_en->setType('text');
+ $field_en->setValues([
+ new TextValue('ties'),
+ ]);
+ $item_en->method('getFields')->willReturn(['foo' => $field_en]);
+
+ $item_de = $this->getMockBuilder(ItemInterface::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+ $item_de->method('getLanguage')->willReturn('de');
+ $field_de = new Field($index, 'foo');
+ $field_de->setType('text');
+ $field_de->setValues([
+ new TextValue('ties'),
+ ]);
+ $item_de->method('getFields')->willReturn(['foo' => $field_de]);
+
+ $items = [$item_en, $item_de];
+ $this->processor->preprocessIndexItems($items);
+
+ /** @var \Drupal\search_api\Plugin\search_api\data_type\value\TextValueInterface $value */
+ $value = $field_en->getValues()[0];
+ $this->assertEquals('tie', $value->toText());
+ $value = $field_de->getValues()[0];
+ $this->assertEquals('ties', $value->toText());
+ }
+
+ /**
+ * Tests the preprocessSearchQuery() method.
+ *
+ * @param string[]|null $languages
+ * The languages to set for the query.
+ * @param bool $should_process
+ * Whether keys are expected to be processed by the processor or not.
+ *
+ * @covers ::preprocessSearchQuery
+ *
+ * @dataProvider preprocessSearchQueryDataProvider
+ */
+ public function testPreprocessSearchQuery(array $languages = NULL, $should_process) {
+ /** @var \Drupal\search_api\Query\QueryInterface|\PHPUnit_Framework_MockObject_MockObject $query */
+ $query = $this->getMock(QueryInterface::class);
+ $query->method('getLanguages')->willReturn($languages);
+ // Unfortunately, returning a reference (as getKeys() has to do for
+ // processing to work) doesn't seem to be possible with a mock object. But
+ // since the only code we really want to test is the language check, using
+ // an exception works just as well, and is quite simple.
+ $query->method('getKeys')->willThrowException(new SearchApiException());
+
+ try {
+ $this->processor->preprocessSearchQuery($query);
+ $this->assertFalse($should_process, "Keys weren't processed but should have been.");
+ }
+ catch (SearchApiException $e) {
+ $this->assertTrue($should_process, "Keys were processed but shouldn't have been.");
+ }
+ }
+
+ /**
+ * Provides sets of arguments for testPreprocessSearchQuery().
+ *
+ * @return array[]
+ * Arrays of arguments for testPreprocessSearchQuery().
+ */
+ public function preprocessSearchQueryDataProvider() {
+ return [
+ 'language-less query' => [NULL, TRUE],
+ 'English query' => [['en'], TRUE],
+ 'Non-English query' => [['de'], FALSE],
+ 'Multilingual query (including English)' => [['en', 'fr', 'es'], TRUE],
+ 'Multilingual query (not including English)' => [['de', 'it'], FALSE],
+ ];
+ }
+
+ /**
+ * Tests the process() method.
+ *
+ * @param string $passed_value
+ * The value that should be passed into process().
+ * @param string $expected_value
+ * The expected processed value.
+ *
+ * @covers ::process
+ *
+ * @dataProvider processDataProvider
+ */
+ public function testProcess($passed_value, $expected_value) {
+ $this->invokeMethod('process', [&$passed_value]);
+ $this->assertEquals($passed_value, $expected_value);
+ }
+
+ /**
+ * Provides sets of arguments for testProcess().
+ *
+ * @return array[]
+ * Arrays of arguments for testProcess().
+ */
+ public function processDataProvider() {
+ return [
+ ['Yo', 'yo'],
+ ['ties', 'tie'],
+ ['cries', 'cri'],
+ ['exceed', 'exceed'],
+ ['consign', 'consign'],
+ ['consigned', 'consign'],
+ ['consigning', 'consign'],
+ ['consignment', 'consign'],
+ ['consist', 'consist'],
+ ['consisted', 'consist'],
+ ['consistency', 'consist'],
+ ['consistent', 'consist'],
+ ['consistently', 'consist'],
+ ['consisting', 'consist'],
+ ['consists', 'consist'],
+ ['consolation', 'consol'],
+ ['consolations', 'consol'],
+ ['consolatory', 'consolatori'],
+ ['console', 'consol'],
+ ['consoled', 'consol'],
+ ['consoles', 'consol'],
+ ['consolidate', 'consolid'],
+ ['consolidated', 'consolid'],
+ ['consolidating', 'consolid'],
+ ['consoling', 'consol'],
+ ['consolingly', 'consol'],
+ ['consols', 'consol'],
+ ['consonant', 'conson'],
+ ['consort', 'consort'],
+ ['consorted', 'consort'],
+ ['consorting', 'consort'],
+ ['conspicuous', 'conspicu'],
+ ['conspicuously', 'conspicu'],
+ ['conspiracy', 'conspiraci'],
+ ['conspirator', 'conspir'],
+ ['conspirators', 'conspir'],
+ ['conspire', 'conspir'],
+ ['conspired', 'conspir'],
+ ['conspiring', 'conspir'],
+ ['constable', 'constabl'],
+ ['constables', 'constabl'],
+ ['constance', 'constanc'],
+ ['constancy', 'constanc'],
+ ['constant', 'constant'],
+ ['knack', 'knack'],
+ ['knackeries', 'knackeri'],
+ ['knacks', 'knack'],
+ ['knag', 'knag'],
+ ['knave', 'knave'],
+ ['knaves', 'knave'],
+ ['knavish', 'knavish'],
+ ['kneaded', 'knead'],
+ ['kneading', 'knead'],
+ ['knee', 'knee'],
+ ['kneel', 'kneel'],
+ ['kneeled', 'kneel'],
+ ['kneeling', 'kneel'],
+ ['kneels', 'kneel'],
+ ['knees', 'knee'],
+ ['knell', 'knell'],
+ ['knelt', 'knelt'],
+ ['knew', 'knew'],
+ ['knick', 'knick'],
+ ['knif', 'knif'],
+ ['knife', 'knife'],
+ ['knight', 'knight'],
+ ['knightly', 'knight'],
+ ['knights', 'knight'],
+ ['knit', 'knit'],
+ ['knits', 'knit'],
+ ['knitted', 'knit'],
+ ['knitting', 'knit'],
+ ['knives', 'knive'],
+ ['knob', 'knob'],
+ ['knobs', 'knob'],
+ ['knock', 'knock'],
+ ['knocked', 'knock'],
+ ['knocker', 'knocker'],
+ ['knockers', 'knocker'],
+ ['knocking', 'knock'],
+ ['knocks', 'knock'],
+ ['knopp', 'knopp'],
+ ['knot', 'knot'],
+ ['knots', 'knot'],
+ // This can happen when Tokenizer is off during indexing, or when
+ // preprocessing a search query with quoted keywords.
+ [" \tExtra spaces \rappeared \n", 'extra space appear'],
+ ["\tspaced-out \r\n", 'space out'],
+ ];
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Unit/Processor/StopwordsTest.php b/sites/all/modules/contrib/search/search_api/tests/src/Unit/Processor/StopwordsTest.php
new file mode 100644
index 000000000..46b9e2fce
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Unit/Processor/StopwordsTest.php
@@ -0,0 +1,117 @@
+setUpMockContainer();
+ $this->processor = new Stopwords([], 'stopwords', []);;
+ }
+
+ /**
+ * Tests the process() method of the Stopwords processor.
+ *
+ * @param string $passed_value
+ * The string that should be passed to process().
+ * @param string $expected_value
+ * The expected altered string.
+ * @param string[] $stopwords
+ * The stopwords with which to configure the test processor.
+ *
+ * @dataProvider processDataProvider
+ */
+ public function testProcess($passed_value, $expected_value, array $stopwords) {
+ $this->processor->setConfiguration(['stopwords' => $stopwords]);
+ $this->invokeMethod('process', [&$passed_value]);
+ $this->assertEquals($expected_value, $passed_value);
+ }
+
+ /**
+ * Data provider for testStopwords().
+ *
+ * Processor checks for exact case, and tokenized content.
+ */
+ public function processDataProvider() {
+ return [
+ [
+ 'or',
+ '',
+ ['or'],
+ ],
+ [
+ 'orb',
+ 'orb',
+ ['or'],
+ ],
+ [
+ 'for',
+ 'for',
+ ['or'],
+ ],
+ [
+ 'ordor',
+ 'ordor',
+ ['or'],
+ ],
+ [
+ 'ÄÖÜÀÁ<>»«û',
+ 'ÄÖÜÀÁ<>»«û',
+ ['stopword1', 'ÄÖÜÀÁ<>»«', 'stopword3'],
+ ],
+ [
+ 'ÄÖÜÀÁ',
+ '',
+ ['stopword1', 'ÄÖÜÀÁ', 'stopword3'],
+ ],
+ [
+ 'ÄÖÜÀÁ stopword1',
+ 'ÄÖÜÀÁ stopword1',
+ ['stopword1', 'ÄÖÜÀÁ', 'stopword3'],
+ ],
+ ];
+ }
+
+ /**
+ * Tests the processor's preprocessSearchQuery() method.
+ */
+ public function testPreprocessSearchQuery() {
+ $index = $this->getMock('Drupal\search_api\IndexInterface');
+ $index->expects($this->any())
+ ->method('status')
+ ->will($this->returnValue(TRUE));
+ /** @var \Drupal\search_api\IndexInterface $index */
+
+ $this->processor->setIndex($index);
+ $query = \Drupal::getContainer()
+ ->get('search_api.query_helper')
+ ->createQuery($index);
+ $keys = ['#conjunction' => 'AND', 'foo', 'bar', 'bar foo'];
+ $query->keys($keys);
+
+ $configuration = ['stopwords' => ['foobar', 'bar', 'barfoo']];
+ $this->processor->setConfiguration($configuration);
+ $this->processor->preprocessSearchQuery($query);
+ unset($keys[1]);
+ $this->assertEquals($keys, $query->getKeys());
+
+ $this->assertEquals(['bar'], $query->getResults()->getIgnoredSearchKeys());
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Unit/Processor/TestFieldsProcessorPlugin.php b/sites/all/modules/contrib/search/search_api/tests/src/Unit/Processor/TestFieldsProcessorPlugin.php
new file mode 100644
index 000000000..33a1a5313
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Unit/Processor/TestFieldsProcessorPlugin.php
@@ -0,0 +1,135 @@
+setTokens($tokens);
+ return $return;
+ }
+
+ /**
+ * Overrides a method in this processor.
+ *
+ * @param string $method
+ * The name of the method to override.
+ * @param callable|null $override
+ * The new code of the method, or NULL to use the default.
+ */
+ public function setMethodOverride($method, callable $override = NULL) {
+ $this->methodOverrides[$method] = $override;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function testField($name, FieldInterface $field) {
+ if (isset($this->methodOverrides[__FUNCTION__])) {
+ return $this->methodOverrides[__FUNCTION__]($name, $field);
+ }
+ return parent::testField($name, $field);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function testType($type) {
+ if (isset($this->methodOverrides[__FUNCTION__])) {
+ return $this->methodOverrides[__FUNCTION__]($type);
+ }
+ return parent::testType($type);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function processFieldValue(&$value, $type) {
+ if (isset($this->methodOverrides[__FUNCTION__])) {
+ $this->methodOverrides[__FUNCTION__]($value, $type);
+ return;
+ }
+ parent::processFieldValue($value, $type);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function processKey(&$value) {
+ if (isset($this->methodOverrides[__FUNCTION__])) {
+ $this->methodOverrides[__FUNCTION__]($value);
+ return;
+ }
+ parent::processKey($value);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function processConditionValue(&$value) {
+ if (isset($this->methodOverrides[__FUNCTION__])) {
+ $this->methodOverrides[__FUNCTION__]($value);
+ return;
+ }
+ parent::processConditionValue($value);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function process(&$value) {
+ if (isset($this->methodOverrides[__FUNCTION__])) {
+ $this->methodOverrides[__FUNCTION__]($value);
+ return;
+ }
+ if (isset($value)) {
+ $value = "*$value";
+ }
+ else {
+ $value = 'undefined';
+ }
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Unit/Processor/TestItemsTrait.php b/sites/all/modules/contrib/search/search_api/tests/src/Unit/Processor/TestItemsTrait.php
new file mode 100644
index 000000000..8b1910fc4
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Unit/Processor/TestItemsTrait.php
@@ -0,0 +1,155 @@
+itemIds[0] = $itemId = Utility::createCombinedId('entity:node', '1:en');
+ $item = new Item($index, $itemId);
+ $field = new Field($index, $fieldId);
+ $field->setType($fieldType);
+ $field->addValue($fieldValue);
+ $item->setField($fieldId, $field);
+ $item->setFieldsExtracted(TRUE);
+
+ return [$itemId => $item];
+ }
+
+ /**
+ * Creates a certain number of test items.
+ *
+ * @param \Drupal\search_api\IndexInterface $index
+ * The index that should be used for the items.
+ * @param int $count
+ * The number of items to create.
+ * @param array[] $fields
+ * The fields to create on the items, with keys being combined property
+ * paths and values being arrays with properties to set on the field.
+ * @param \Drupal\Core\TypedData\ComplexDataInterface|null $object
+ * (optional) The object to set on each item as the "original object".
+ * @param array|null $datasource_ids
+ * (optional) An array of datasource IDs to use for the items, in that order
+ * (starting again from the front if necessary).
+ *
+ * @return \Drupal\search_api\Item\ItemInterface[]
+ * An array containing the requested test items.
+ */
+ public function createItems(IndexInterface $index, $count, array $fields, ComplexDataInterface $object = NULL, array $datasource_ids = ['entity:node']) {
+ $datasource_count = count($datasource_ids);
+ $items = [];
+ for ($i = 0; $i < $count; ++$i) {
+ $datasource_id = $datasource_ids[$i % $datasource_count];
+ $this->itemIds[$i] = $item_id = Utility::createCombinedId($datasource_id, ($i + 1) . ':en');
+ $item = new Item($index, $item_id);
+ if (isset($object)) {
+ $item->setOriginalObject($object);
+ }
+ foreach ($fields as $combined_property_path => $field_info) {
+ list($field_info['datasource_id'], $field_info['property_path']) = Utility::splitCombinedId($combined_property_path);
+ // Only add fields of the right datasource.
+ if (isset($field_info['datasource_id']) && $field_info['datasource_id'] != $datasource_id) {
+ continue;
+ }
+ $fields_helper = \Drupal::getContainer()
+ ->get('search_api.fields_helper');
+ $field_id = $fields_helper->getNewFieldId($index, $field_info['property_path']);
+ $field = $fields_helper->createField($index, $field_id, $field_info);
+ $item->setField($field_id, $field);
+ }
+ $item->setFieldsExtracted(TRUE);
+ $items[$item_id] = $item;
+ }
+ return $items;
+ }
+
+ /**
+ * Adds a container with several mock services commonly needed by our tests.
+ */
+ protected function setUpMockContainer() {
+ /** @var \Drupal\Tests\UnitTestCase|\Drupal\Tests\search_api\Unit\Processor\TestItemsTrait $this */
+ $dataTypeManager = $this->getMockBuilder('Drupal\search_api\DataType\DataTypePluginManager')
+ ->disableOriginalConstructor()
+ ->getMock();
+ $dataTypeManager->method('getInstances')
+ ->will($this->returnValue([]));
+
+ $moduleHandler = $this->getMockBuilder('Drupal\Core\Extension\ModuleHandlerInterface')
+ ->disableOriginalConstructor()
+ ->getMock();
+ $dataTypeHelper = new DataTypeHelper($moduleHandler, $dataTypeManager);
+
+ $entityTypeManager = $this->getMockBuilder('Drupal\Core\Entity\EntityTypeManagerInterface')
+ ->disableOriginalConstructor()
+ ->getMock();
+ $entityFieldManager = $this->getMockBuilder('Drupal\Core\Entity\EntityFieldManagerInterface')
+ ->disableOriginalConstructor()
+ ->getMock();
+ $entityBundleInfo = $this->getMockBuilder('Drupal\Core\Entity\EntityTypeBundleInfoInterface')
+ ->disableOriginalConstructor()
+ ->getMock();
+ $fieldsHelper = new FieldsHelper($entityTypeManager, $entityFieldManager, $entityBundleInfo, $dataTypeHelper);
+
+ $queryHelper = $this->getMock('Drupal\search_api\Utility\QueryHelperInterface');
+ $queryHelper->method('createQuery')
+ ->willReturnCallback(function (IndexInterface $index, array $options = []) {
+ return Query::create($index, $options);
+ });
+ $queryHelper->method('getResults')
+ ->will($this->returnValue([]));
+
+ $this->container = new ContainerBuilder();
+ $this->container->set('plugin.manager.search_api.data_type', $dataTypeManager);
+ $this->container->set('search_api.data_type_helper', $dataTypeHelper);
+ $this->container->set('search_api.fields_helper', $fieldsHelper);
+ $this->container->set('search_api.query_helper', $queryHelper);
+ \Drupal::setContainer($this->container);
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Unit/Processor/TokenizerTest.php b/sites/all/modules/contrib/search/search_api/tests/src/Unit/Processor/TokenizerTest.php
new file mode 100644
index 000000000..dcd2dd909
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Unit/Processor/TokenizerTest.php
@@ -0,0 +1,390 @@
+processor = new Tokenizer([], 'tokenizer', []);
+ }
+
+ /**
+ * Tests the processFieldValue() method.
+ *
+ * @param string $passed_value
+ * The field value passed to the processor's processFieldValue() method.
+ * @param string $expected_value
+ * The expected preprocessed value.
+ * @param array $config
+ * (optional) Configuration to override the processor's defaults.
+ *
+ * @dataProvider textDataProvider
+ */
+ public function testProcessFieldValue($passed_value, $expected_value, array $config = []) {
+ if ($config) {
+ $this->processor->setConfiguration($config);
+ }
+ $type = 'text';
+ $this->invokeMethod('processFieldValue', [&$passed_value, $type]);
+ $this->assertEquals($expected_value, $passed_value);
+ }
+
+ /**
+ * Provides test data for testValueConfiguration().
+ *
+ * @return array
+ * Arrays of parameters for testProcessFieldValue(), each containing (in
+ * this order):
+ * - The field value passed to the processor's processFieldValue() method.
+ * - The expected preprocessed value.
+ * - (optional) Configuration to override the processor's defaults.
+ */
+ public function textDataProvider() {
+ $word_token = Utility::createTextToken('word');
+ return [
+ // Test some simple cases.
+ ['word', [$word_token]],
+ ['word word', [$word_token, $word_token]],
+ // Test whether the default splits on special characters, too.
+ ['words!word', [Utility::createTextToken('words'), $word_token]],
+ ['words$word', [Utility::createTextToken('words'), $word_token]],
+ // Test whether overriding the default works and is case-insensitive.
+ [
+ 'wordXwordxword',
+ [$word_token, Utility::createTextToken('wordxword')],
+ ['spaces' => 'X'],
+ ],
+ [
+ 'word3word!word',
+ [$word_token, Utility::createTextToken('word!word')],
+ ['spaces' => '\d'],
+ ],
+ [
+ 'wordXwordRword',
+ [$word_token, $word_token, $word_token],
+ ['spaces' => 'R-Z'],
+ ],
+ [
+ 'wordXwordRword',
+ [$word_token, $word_token, $word_token],
+ ['spaces' => 'R-TW-Z'],
+ ],
+ [
+ 'wordXword word',
+ [$word_token, $word_token, $word_token],
+ ['spaces' => 'R-Z'],
+ ],
+ // Test whether minimum word size works.
+ [
+ 'wordSwo',
+ [$word_token],
+ ['spaces' => 'R-Z'],
+ ],
+ [
+ 'wordSwo',
+ [$word_token, Utility::createTextToken('wo')],
+ ['spaces' => 'R-Z', 'minimum_word_size' => 2],
+ ],
+ [
+ 'word w',
+ [$word_token],
+ ['minimum_word_size' => 2],
+ ],
+ [
+ 'word w',
+ [$word_token, Utility::createTextToken('w')],
+ ['minimum_word_size' => 1],
+ ],
+ [
+ 'word wordword',
+ [],
+ ['minimum_word_size' => 10],
+ ],
+ ];
+ }
+
+ /**
+ * Tests that the simplifyText() method handles CJK characters properly.
+ *
+ * The simplifyText() method does special things with numbers, symbols and
+ * punctuation. So we only test that CJK characters that are not in these
+ * character classes are tokenized properly. See PREG_CLASS_CJK for more
+ * information.
+ */
+ public function testCjkSupport() {
+ $this->invokeMethod('prepare');
+
+ // Create a string of CJK characters from various character ranges in
+ // the Unicode tables. $starts contains the starts of the character ranges,
+ // $ends the ends.
+ $starts = [
+ 'CJK unified' => 0x4e00,
+ 'CJK Ext A' => 0x3400,
+ 'CJK Compat' => 0xf900,
+ 'Hangul Jamo' => 0x1100,
+ 'Hangul Ext A' => 0xa960,
+ 'Hangul Ext B' => 0xd7b0,
+ 'Hangul Compat' => 0x3131,
+ 'Half non-punct 1' => 0xff21,
+ 'Half non-punct 2' => 0xff41,
+ 'Half non-punct 3' => 0xff66,
+ 'Hangul Syllables' => 0xac00,
+ 'Hiragana' => 0x3040,
+ 'Katakana' => 0x30a1,
+ 'Katakana Ext' => 0x31f0,
+ 'CJK Reserve 1' => 0x20000,
+ 'CJK Reserve 2' => 0x30000,
+ 'Bomofo' => 0x3100,
+ 'Bomofo Ext' => 0x31a0,
+ 'Lisu' => 0xa4d0,
+ 'Yi' => 0xa000,
+ ];
+ $ends = [
+ 'CJK unified' => 0x9fcf,
+ 'CJK Ext A' => 0x4dbf,
+ 'CJK Compat' => 0xfaff,
+ 'Hangul Jamo' => 0x11ff,
+ 'Hangul Ext A' => 0xa97f,
+ 'Hangul Ext B' => 0xd7ff,
+ 'Hangul Compat' => 0x318e,
+ 'Half non-punct 1' => 0xff3a,
+ 'Half non-punct 2' => 0xff5a,
+ 'Half non-punct 3' => 0xffdc,
+ 'Hangul Syllables' => 0xd7af,
+ 'Hiragana' => 0x309f,
+ 'Katakana' => 0x30ff,
+ 'Katakana Ext' => 0x31ff,
+ 'CJK Reserve 1' => 0x2fffd,
+ 'CJK Reserve 2' => 0x3fffd,
+ 'Bomofo' => 0x312f,
+ 'Bomofo Ext' => 0x31b7,
+ 'Lisu' => 0xa4fd,
+ 'Yi' => 0xa48f,
+ ];
+
+ // Generate characters consisting of starts, midpoints, and ends.
+ $chars = [];
+ foreach ($starts as $key => $value) {
+ $chars[] = self::codepointToUtf8($starts[$key]);
+ $mid = round(0.5 * ($starts[$key] + $ends[$key]));
+ $chars[] = self::codepointToUtf8($mid);
+ $chars[] = self::codepointToUtf8($ends[$key]);
+ }
+
+ // Merge into a single string and tokenize.
+ $text = implode('', $chars);
+ $simplified_text = $this->invokeMethod('simplifyText', [$text]);
+
+ // Prepare the expected return value, which consists of all the 3-grams in
+ // the original string, separated by spaces.
+ $expected = '';
+ for ($i = 2; $i < count($chars); ++$i) {
+ $expected .= $chars[$i - 2];
+ $expected .= $chars[$i - 1];
+ $expected .= $chars[$i];
+ $expected .= ' ';
+ }
+ $expected = trim($expected);
+
+ // Verify that the output matches what we expect.
+ $this->assertEquals($expected, $simplified_text, 'CJK tokenizer worked on all supplied CJK characters');
+
+ // Verify that disabling the "overlap_cjk" setting works as expected.
+ $this->processor->setConfiguration(['overlap_cjk' => FALSE]);
+ $this->invokeMethod('prepare');
+ $simplified_text = $this->invokeMethod('simplifyText', [$text]);
+ $this->assertEquals($text, $simplified_text, 'CJK tokenizing is successfully disabled');
+ }
+
+ /**
+ * Verifies that strings of non-CJK characters are not tokenized.
+ *
+ * This is just a sanity check – it verifies that strings of letters are
+ * not tokenized.
+ */
+ public function testNoTokenizer() {
+ // Set the minimum word size to 1 (to split all CJK characters).
+ $this->processor->setConfiguration(['minimum_word_size' => 1]);
+ $this->invokeMethod('prepare');
+
+ $letters = 'abcdefghijklmnopqrstuvwxyz';
+ $out = $this->invokeMethod('simplifyText', [$letters]);
+
+ $this->assertEquals($letters, $out, 'Latin letters are not CJK tokenized');
+ }
+
+ /**
+ * Converts a Unicode code point to a UTF-8 string.
+ *
+ * The PHP function "chr()" only works for ASCII characters up to character
+ * 255. This function converts a number to the corresponding unicode
+ * character. Adapted from functions supplied in comments on several functions
+ * on php.net.
+ *
+ * @param int $num
+ * A Unicode code point.
+ *
+ * @return string
+ * A UTF-8 string containing the character corresponding to that code point.
+ */
+ protected static function codepointToUtf8($num) {
+ if ($num < 128) {
+ return chr($num);
+ }
+
+ if ($num < 2048) {
+ return chr(($num >> 6) + 192) . chr(($num & 63) + 128);
+ }
+
+ if ($num < 65536) {
+ return chr(($num >> 12) + 224) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
+ }
+
+ if ($num < 2097152) {
+ return chr(($num >> 18) + 240) . chr((($num >> 12) & 63) + 128) . chr((($num >> 6) & 63) + 128) . chr(($num & 63) + 128);
+ }
+
+ return '';
+ }
+
+ /**
+ * Tests that all Unicode characters simplify correctly.
+ *
+ * This test uses a Drupal core search file that was constructed so that the
+ * even lines are boundary characters, and the odd lines are valid word
+ * characters. (It was generated as a sequence of all the Unicode characters,
+ * and then the boundary characters (punctuation, spaces, etc.) were split
+ * off into their own lines). So the even-numbered lines should simplify to
+ * nothing, and the odd-numbered lines we need to split into shorter chunks
+ * and verify that simplification doesn't lose any characters.
+ *
+ * @see \Drupal\search\Tests\SearchSimplifyTest::testSearchSimplifyUnicode()
+ */
+ public function testSearchSimplifyUnicode() {
+ // Set the minimum word size to 1 (to split all CJK characters).
+ $this->processor->setConfiguration(['minimum_word_size' => 1]);
+ $this->invokeMethod('prepare');
+
+ $input = file_get_contents($this->root . '/core/modules/search/tests/UnicodeTest.txt');
+ $basestrings = explode(chr(10), $input);
+ $strings = [];
+ foreach ($basestrings as $key => $string) {
+ if ($key % 2) {
+ // Even line, should be removed by simplifyText().
+ $simplified = $this->invokeMethod('simplifyText', [$string]);
+ $this->assertEquals('', $simplified, "Line $key is excluded from the index");
+ }
+ else {
+ // Odd line, should be word characters (which might be expanded, but
+ // never removed). Split this into 30-character chunks, so we don't run
+ // into limits of truncation.
+ $start = 0;
+ while ($start < Unicode::strlen($string)) {
+ $newstr = Unicode::substr($string, $start, 30);
+ // Special case: leading zeros are removed from numeric strings,
+ // and there's one string in this file that is numbers starting with
+ // zero, so prepend a 1 on that string.
+ if (preg_match('/^[0-9]+$/', $newstr)) {
+ $newstr = '1' . $newstr;
+ }
+ $strings[] = $newstr;
+ $start += 30;
+ }
+ }
+ }
+ foreach ($strings as $key => $string) {
+ $simplified = $this->invokeMethod('simplifyText', [$string]);
+ $this->assertGreaterThanOrEqual(Unicode::strlen($string), Unicode::strlen($simplified), "Nothing is removed from string $key.");
+ }
+
+ // Test the low-numbered ASCII control characters separately. They are not
+ // in the text file because they are problematic for diff, especially \0.
+ $string = '';
+ for ($i = 0; $i < 32; $i++) {
+ $string .= chr($i);
+ }
+ $this->assertEquals('', $this->invokeMethod('simplifyText', [$string]), 'Text simplification works for ASCII control characters.');
+ }
+
+ /**
+ * Tests whether punctuation is treated correctly.
+ *
+ * @param string $passed_value
+ * The string passed to simplifyText().
+ * @param string $expected_value
+ * The expected return value.
+ * @param string $message
+ * The message to display for the assertion.
+ *
+ * @dataProvider searchSimplifyPunctuationProvider
+ */
+ public function testSearchSimplifyPunctuation($passed_value, $expected_value, $message) {
+ // Set the minimum word size to 1 (to split all CJK characters).
+ $this->processor->setConfiguration(['minimum_word_size' => 1]);
+ $this->invokeMethod('prepare');
+
+ $out = $this->invokeMethod('simplifyText', [$passed_value]);
+ $this->assertEquals($expected_value, $out, $message);
+ }
+
+ /**
+ * Provides test data for testSearchSimplifyPunctuation().
+ *
+ * @return array
+ * Arrays of parameters for testSearchSimplifyPunctuation(), each containing
+ * (in this order):
+ * - The string passed to simplifyText().
+ * - The expected return value.
+ * - The message to display for the assertion.
+ */
+ public function searchSimplifyPunctuationProvider() {
+ $cases = [
+ [
+ '20.03/94-28,876',
+ '20039428876',
+ 'Punctuation removed from numbers',
+ ],
+ [
+ 'great...drupal--module',
+ 'great drupal module',
+ 'Multiple dot and dashes are word boundaries',
+ ],
+ [
+ 'very_great-drupal.module',
+ 'verygreatdrupalmodule',
+ 'Single dot, dash, underscore are removed',
+ ],
+ [
+ 'regular,punctuation;word',
+ 'regular punctuation word',
+ 'Punctuation is a word boundary',
+ ],
+ [
+ 'Äußerung français repülőtér',
+ 'Äußerung français repülőtér',
+ 'Umlauts and accented characters are not treated as word boundaries',
+ ],
+ ];
+ return $cases;
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Unit/Processor/TransliterationTest.php b/sites/all/modules/contrib/search/search_api/tests/src/Unit/Processor/TransliterationTest.php
new file mode 100644
index 000000000..63bcbaaa1
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Unit/Processor/TransliterationTest.php
@@ -0,0 +1,83 @@
+index = $this->getMock('Drupal\search_api\IndexInterface');
+
+ $this->setUpMockContainer();
+ $this->processor = new Transliteration([], 'transliteration', []);
+ $this->processor->setLangcode('en');
+
+ $transliterator = $this->getMock('\Drupal\Component\Transliteration\TransliterationInterface');
+ $transliterate = function ($string, $langcode = 'en', $unknown_character = '?', $max_length = NULL) {
+ return "translit-$string-$langcode$unknown_character$max_length";
+ };
+ $transliterator->expects($this->any())
+ ->method('transliterate')
+ ->will($this->returnCallback($transliterate));
+ /** @var \Drupal\Component\Transliteration\TransliterationInterface $transliterator */
+ $this->processor->setTransliterator($transliterator);
+ }
+
+ /**
+ * Tests that integers are not affected.
+ */
+ public function testTransliterationWithInteger() {
+ $field_value = 5;
+ /** @var \Drupal\search_api\Item\FieldInterface $field */
+ $items = $this->createSingleFieldItem($this->index, 'int', $field_value, $field);
+ $this->processor->preprocessIndexItems($items);
+ $this->assertEquals([$field_value], $field->getValues(), 'Integer not affected by transliteration.');
+ }
+
+ /**
+ * Tests that floating point numbers are not affected.
+ */
+ public function testTransliterationWithDouble() {
+ $field_value = 3.14;
+ /** @var \Drupal\search_api\Item\FieldInterface $field */
+ $items = $this->createSingleFieldItem($this->index, 'double', $field_value, $field);
+ $this->processor->preprocessIndexItems($items);
+ $this->assertEquals([$field_value], $field->getValues(), 'Floating point number not affected by transliteration.');
+ }
+
+ /**
+ * Tests that strings are affected.
+ */
+ public function testTransliterationWithString() {
+ $field_value = 'test_string';
+ /** @var \Drupal\search_api\Item\FieldInterface $field */
+ $items = $this->createSingleFieldItem($this->index, 'string', $field_value, $field);
+ $this->processor->preprocessIndexItems($items);
+ $expected_value = "translit-$field_value-en?";
+ $this->assertEquals([$expected_value], $field->getValues(), 'Strings are correctly transliterated.');
+ }
+
+}
diff --git a/sites/all/modules/contrib/search/search_api/tests/src/Unit/TestComplexDataInterface.php b/sites/all/modules/contrib/search/search_api/tests/src/Unit/TestComplexDataInterface.php
new file mode 100644
index 000000000..003a696e6
--- /dev/null
+++ b/sites/all/modules/contrib/search/search_api/tests/src/Unit/TestComplexDataInterface.php
@@ -0,0 +1,13 @@
+' . t('About') . '';
+ $output .= '
' . t('Edlp search module') . '
';
+ return $output;
+
+ default:
+ }
+}
+
+/**
+ * Implements hook_theme().
+ */
+function edlp_search_theme($existing, $type, $theme, $path) {
+ return array(
+ 'edlp_search_search_form' => array(
+ 'file' => 'includes/edlp_search_search_form.inc',
+ 'variables' => array(
+ 'form' => null,
+ ),
+ ),
+ );
+}
diff --git a/sites/all/modules/figli/edlp_search/edlp_search.routing.yml b/sites/all/modules/figli/edlp_search/edlp_search.routing.yml
new file mode 100644
index 000000000..46518a569
--- /dev/null
+++ b/sites/all/modules/figli/edlp_search/edlp_search.routing.yml
@@ -0,0 +1,40 @@
+
+edlp_search.edlp_search_controller_searchForm:
+ path: '/edlp_search/searchform'
+ defaults:
+ _controller: '\Drupal\edlp_search\Controller\EdlpSearchController::searchForm'
+ _title: 'Search Form'
+ requirements:
+ _permission: 'access content'
+
+edlp_search.edlp_search_controller_searchForm_ajax:
+ path: '/edlp_search/searchform/ajax'
+ defaults:
+ _controller: '\Drupal\edlp_search\Controller\EdlpSearchController::searchFormJson'
+ _title: 'Search Form'
+ requirements:
+ _permission: 'access content'
+
+edlp_search.edlp_search_controller_searchResults:
+ path: '/edlp_search/searchresults'
+ defaults:
+ _controller: '\Drupal\edlp_search\Controller\EdlpSearchController::searchResults'
+ _title: 'Search Results'
+ requirements:
+ _permission: 'access content'
+
+edlp_search.edlp_search_controller_searchResults_ajax:
+ path: '/edlp_search/searchresults/ajax'
+ defaults:
+ _controller: '\Drupal\edlp_search\Controller\EdlpSearchController::searchResultsJson'
+ _title: 'Search Results'
+ requirements:
+ _permission: 'access content'
+
+edlp_search.edlp_search_form:
+ path: '/edlp_search/form/search'
+ defaults:
+ _form: '\Drupal\edlp_search\Form\EdlpSearchForm'
+ _title: 'EdlpSearchForm'
+ requirements:
+ _access: 'TRUE'
diff --git a/sites/all/modules/figli/edlp_search/includes/edlp_search_search_form.inc b/sites/all/modules/figli/edlp_search/includes/edlp_search_search_form.inc
new file mode 100644
index 000000000..1c00b8de9
--- /dev/null
+++ b/sites/all/modules/figli/edlp_search/includes/edlp_search_search_form.inc
@@ -0,0 +1,10 @@
+getViewBuilder($vars['entity_type']);
+ // $vars['content'] = $view_builder->view($vars['entity'], $vars['view_mode']);
+}
diff --git a/sites/all/modules/figli/edlp_search/src/Controller/EdlpSearchController.php b/sites/all/modules/figli/edlp_search/src/Controller/EdlpSearchController.php
new file mode 100644
index 000000000..e6ada123b
--- /dev/null
+++ b/sites/all/modules/figli/edlp_search/src/Controller/EdlpSearchController.php
@@ -0,0 +1,99 @@
+formBuilder = $formBuilder;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->get('form_builder')
+ );
+ }
+
+
+ private function getSearchForm(){
+ $this->form = $this->formBuilder->getForm('Drupal\edlp_search\Form\EdlpSearchForm');
+ }
+
+ private function buildRenderable(){
+ $this->getSearchForm();
+ $this->renderable = array(
+ "#theme" => "edlp_search_search_form",
+ '#form' => $this->form
+ );
+ }
+
+ /**
+ * Searchform.
+ *
+ * @return string
+ * Return Hello string.
+ */
+ public function searchForm() {
+ $this->buildRenderable();
+
+ return $this->renderable;
+ }
+
+ public function searchFormJson(){
+ $this->buildRenderable();
+
+ $rendered = render($this->renderable);
+
+ $response = new JsonResponse();
+ $response->setData([
+ 'rendered'=> $rendered,
+ ]);
+ return $response;
+
+ }
+
+ /**
+ * Searchresults.
+ *
+ * @return string
+ * Return Hello string.
+ */
+ public function searchResults() {
+ return [
+ '#type' => 'markup',
+ '#markup' => $this->t('Implement method: searchResults')
+ ];
+ }
+ public function searchResultsJson(){
+ $response = new JsonResponse();
+ $response->setData([
+ 'test'=>'search results',
+ // 'rendered'=> $rendered,
+ ]);
+ return $response;
+ }
+}
diff --git a/sites/all/modules/figli/edlp_search/src/Form/EdlpSearchForm.php b/sites/all/modules/figli/edlp_search/src/Form/EdlpSearchForm.php
new file mode 100644
index 000000000..630bffb30
--- /dev/null
+++ b/sites/all/modules/figli/edlp_search/src/Form/EdlpSearchForm.php
@@ -0,0 +1,94 @@
+ 'search',
+ // '#title' => $this->t('Keys'),
+ '#maxlength' => 64,
+ '#size' => 15,
+ '#weight' => '0',
+ ];
+
+ $this->getEntries();
+
+ $form['entries'] = [
+ '#type' => 'checkboxes',
+ '#title' => $this->t('Entries'),
+ '#options' => $this->entries,
+ '#weight' => '1',
+ ];
+
+ $form['search'] = [
+ '#type' => 'submit',
+ '#title' => $this->t('Search'),
+ '#value' => $this->t('Search'),
+ '#weight' => '99',
+ ];
+
+ return $form;
+ }
+
+ private function getEntries(){
+ $query = \Drupal::entityQuery('taxonomy_term')
+ ->condition('vid', 'entrees');
+
+ $tids = $query->execute();
+ $terms = entity_load_multiple('taxonomy_term', $tids);
+ // dsm($terms);
+ foreach ($terms as $term) {
+ // dpm($term->toArray());
+ $tid = $term->id();
+ $name = $term->getName();
+
+ // remove masqué
+ $sid = WorkflowManager::getCurrentStateId($term, 'field_workflow');
+ if($sid == 'generique_masque') continue;
+
+ $entries[$tid] = $name;
+ }
+
+ $this->entries = $entries;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function validateForm(array &$form, FormStateInterface $form_state) {
+ parent::validateForm($form, $form_state);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state) {
+ // Display result.
+ foreach ($form_state->getValues() as $key => $value) {
+ drupal_set_message($key . ': ' . $value);
+ }
+
+ }
+
+}
diff --git a/sites/all/modules/figli/edlp_search/src/Plugin/Block/EdlpSearchLinkBlock.php b/sites/all/modules/figli/edlp_search/src/Plugin/Block/EdlpSearchLinkBlock.php
new file mode 100644
index 000000000..55dcbad9e
--- /dev/null
+++ b/sites/all/modules/figli/edlp_search/src/Plugin/Block/EdlpSearchLinkBlock.php
@@ -0,0 +1,38 @@
+ TRUE]);
+ $build['edlp_search_link_block'] = array(
+ '#title' => "Search",
+ '#type' => 'link',
+ '#url' => $url,
+ '#options'=>array(
+ 'attributes' => array(
+ 'data-drupal-link-system-path' => $url->getInternalPath(),
+ 'class' => array('ajax-link'),
+ )
+ )
+ );
+ return $build;
+ }
+
+}
diff --git a/sites/all/modules/figli/edlp_search/templates/edlp-search-search-form.html.twig b/sites/all/modules/figli/edlp_search/templates/edlp-search-search-form.html.twig
new file mode 100644
index 000000000..f9a859ae9
--- /dev/null
+++ b/sites/all/modules/figli/edlp_search/templates/edlp-search-search-form.html.twig
@@ -0,0 +1 @@
+{{ form }}
diff --git a/sites/all/themes/custom/edlptheme/assets/dist/styles/app.min.css b/sites/all/themes/custom/edlptheme/assets/dist/styles/app.min.css
index ccff9fb4f..23d5d5755 100644
--- a/sites/all/themes/custom/edlptheme/assets/dist/styles/app.min.css
+++ b/sites/all/themes/custom/edlptheme/assets/dist/styles/app.min.css
@@ -1752,7 +1752,7 @@ footer {
overflow: hidden;
text-align: center; }
footer > .wrapper .region-footer-right {
- width: 12%;
+ min-width: 12%;
text-align: right; }
footer nav.block-menu {
display: inline-block; }
@@ -2026,7 +2026,8 @@ footer {
width: 350px;
opacity: 1; }
footer #block-studiolinkblock {
- pointer-events: all; }
+ pointer-events: all;
+ margin-left: 0.5em; }
footer #block-studiolinkblock a {
display: block;
width: 20px;
@@ -2116,6 +2117,26 @@ footer {
height: 200px;
opacity: 1;
pointer-events: auto; }
+ footer #block-edlpsearchlinkblock {
+ pointer-events: all;
+ margin-left: 0.5em; }
+ footer #block-edlpsearchlinkblock a {
+ display: block;
+ width: 20px;
+ height: 20px;
+ text-indent: 40px;
+ margin: 0;
+ overflow: hidden;
+ -webkit-mask-image: url(../img/search.svg);
+ mask-image: url(../img/search.svg);
+ -webkit-mask-size: contain;
+ mask-size: contain;
+ background-color: #000;
+ -webkit-transition: background-color 0.3s ease-in-out;
+ transition: background-color 0.3s ease-in-out;
+ cursor: pointer; }
+ footer #block-edlpsearchlinkblock a.is-active {
+ background-color: red; }
.node-popup .inner {
position: relative; }
diff --git a/sites/all/themes/custom/edlptheme/assets/styles/app.scss b/sites/all/themes/custom/edlptheme/assets/styles/app.scss
index 69f4a36a9..82f49f0ba 100644
--- a/sites/all/themes/custom/edlptheme/assets/styles/app.scss
+++ b/sites/all/themes/custom/edlptheme/assets/styles/app.scss
@@ -752,7 +752,7 @@ footer{
}
.region-footer-right{
// float:right;
- width:12%;
+ min-width:12%;
// min-width: 50px;
text-align: right;
}
@@ -1041,28 +1041,8 @@ footer{
}
}
- $icons_w:20px;
- #block-studiolinkblock{
- pointer-events: all;
- a{
- $wh:$icons_w;
- display: block;
- width:$wh; height:$wh;
- text-indent: $wh*2;
- margin: 0;
- overflow: hidden;
- mask-image: url(../img/studio.svg);
- mask-size: contain;
- background-color: #000;
- transition: background-color 0.3s ease-in-out;
- cursor: pointer;
- &.is-active{
- background-color: red;
- }
- }
- }
-
- .block.random-player{
+ @mixin iconlinkblock($bgimgurl) {
+ $icons_w:20px;
pointer-events: all;
margin-left: 0.5em;
a{
@@ -1072,7 +1052,7 @@ footer{
text-indent: $wh*2;
margin: 0;
overflow: hidden;
- mask-image: url(../img/random.svg);
+ mask-image: $bgimgurl;
mask-size: contain;
background-color: #000;
transition: background-color 0.3s ease-in-out;
@@ -1082,6 +1062,13 @@ footer{
}
}
}
+ #block-studiolinkblock{
+ @include iconlinkblock(url(../img/studio.svg));
+ }
+
+ .block.random-player{
+ @include iconlinkblock(url(../img/random.svg));
+ }
#block-userlogin{
pointer-events: all;
@@ -1149,6 +1136,11 @@ footer{
}
}
+
+ #block-edlpsearchlinkblock{
+ @include iconlinkblock(url(../img/search.svg));
+ }
+
}
// _ _ _ ___
diff --git a/sites/all/themes/custom/edlptheme/templates/content/edlp-search-search-form.html.twig b/sites/all/themes/custom/edlptheme/templates/content/edlp-search-search-form.html.twig
new file mode 100644
index 000000000..0f7145593
--- /dev/null
+++ b/sites/all/themes/custom/edlptheme/templates/content/edlp-search-search-form.html.twig
@@ -0,0 +1,7 @@
+
diff --git a/sites/default/config/sync/block.block.edlpsearchlinkblock.yml b/sites/default/config/sync/block.block.edlpsearchlinkblock.yml
new file mode 100644
index 000000000..72ff0244c
--- /dev/null
+++ b/sites/default/config/sync/block.block.edlpsearchlinkblock.yml
@@ -0,0 +1,20 @@
+uuid: b330b3dc-45a6-4563-b460-9c6955142e1b
+langcode: fr
+status: true
+dependencies:
+ module:
+ - edlp_search
+ theme:
+ - edlptheme
+id: edlpsearchlinkblock
+theme: edlptheme
+region: footer_right
+weight: 0
+provider: null
+plugin: edlp_search_link_block
+settings:
+ id: edlp_search_link_block
+ label: 'Edlp search link block'
+ provider: edlp_search
+ label_display: '0'
+visibility: { }
diff --git a/sites/default/config/sync/block.block.studiolinkblock.yml b/sites/default/config/sync/block.block.studiolinkblock.yml
index d063c1082..8ce96a5b8 100644
--- a/sites/default/config/sync/block.block.studiolinkblock.yml
+++ b/sites/default/config/sync/block.block.studiolinkblock.yml
@@ -9,7 +9,7 @@ dependencies:
id: studiolinkblock
theme: edlptheme
region: footer_right
-weight: 0
+weight: -6
provider: null
plugin: edlp_studio_link_block
settings:
diff --git a/sites/default/config/sync/block.block.userlogin.yml b/sites/default/config/sync/block.block.userlogin.yml
index f7be0b5cf..b818b1ce8 100644
--- a/sites/default/config/sync/block.block.userlogin.yml
+++ b/sites/default/config/sync/block.block.userlogin.yml
@@ -10,7 +10,7 @@ dependencies:
id: userlogin
theme: edlptheme
region: footer_right
-weight: 0
+weight: -5
provider: null
plugin: user_login_block
settings:
diff --git a/sites/default/config/sync/core.entity_view_display.node.enregistrement.default.yml b/sites/default/config/sync/core.entity_view_display.node.enregistrement.default.yml
index 50127a85b..a7cdbeeaf 100644
--- a/sites/default/config/sync/core.entity_view_display.node.enregistrement.default.yml
+++ b/sites/default/config/sync/core.entity_view_display.node.enregistrement.default.yml
@@ -16,8 +16,6 @@ dependencies:
- field.field.node.enregistrement.field_workflow
- node.type.enregistrement
module:
- - audiofield
- - options
- text
- user
id: node.enregistrement.default
@@ -25,109 +23,36 @@ targetEntityType: node
bundle: enregistrement
mode: default
content:
- body:
- label: above
- type: text_default
- weight: 101
- settings: { }
- third_party_settings: { }
- region: content
content_moderation_control:
weight: -20
region: content
settings: { }
third_party_settings: { }
- field_collectionneurs:
- weight: 106
- label: above
- settings:
- link: true
- third_party_settings: { }
- type: entity_reference_label
- region: content
field_description:
- weight: 109
- label: above
+ weight: 1
+ label: hidden
settings: { }
third_party_settings: { }
type: text_default
region: content
field_entrees:
- weight: 103
- label: above
- settings:
- link: true
- third_party_settings: { }
type: entity_reference_label
+ weight: 0
region: content
- field_genres:
- weight: 104
- label: above
+ label: hidden
settings:
- link: true
- third_party_settings: { }
- type: entity_reference_label
- region: content
- field_langues:
- weight: 105
- label: above
- settings:
- link: true
- third_party_settings: { }
- type: entity_reference_label
- region: content
- field_locuteurs:
- weight: 107
- label: above
- settings:
- link: true
- third_party_settings: { }
- type: entity_reference_label
- region: content
- field_son:
- weight: 108
- label: above
- settings:
- audio_player: default_mp3_player
- audio_player_jplayer_theme: none
- audio_player_wavesurfer_combine_files: false
- audio_player_wordpress_combine_files: false
- audio_player_wordpress_animation: true
- audio_player_soundmanager_theme: default
- audio_player_initial_volume: 8
- audio_player_autoplay: false
- download_link: false
- audio_player_lazyload: false
- download_button: false
- third_party_settings: { }
- type: audiofield_audioplayer
- region: content
- field_transcript_trad:
- weight: 112
- label: above
- settings: { }
- third_party_settings: { }
- type: text_default
- region: content
- field_transcript_vo:
- weight: 111
- label: above
- settings: { }
- third_party_settings: { }
- type: text_default
- region: content
- field_workflow:
- weight: 110
- label: above
- settings: { }
- third_party_settings: { }
- type: list_default
- region: content
- links:
- weight: 100
- region: content
- settings: { }
+ link: false
third_party_settings: { }
hidden:
+ body: true
chutier_actions: true
+ field_collectionneurs: true
+ field_genres: true
+ field_langues: true
+ field_locuteurs: true
+ field_son: true
+ field_transcript_trad: true
+ field_transcript_vo: true
+ field_workflow: true
langcode: true
+ links: true
diff --git a/sites/default/config/sync/core.entity_view_display.node.enregistrement.search_index.yml b/sites/default/config/sync/core.entity_view_display.node.enregistrement.search_index.yml
new file mode 100644
index 000000000..bc78af9bd
--- /dev/null
+++ b/sites/default/config/sync/core.entity_view_display.node.enregistrement.search_index.yml
@@ -0,0 +1,59 @@
+uuid: 07cdb5f0-25e5-443d-838b-0c681108f189
+langcode: fr
+status: true
+dependencies:
+ config:
+ - core.entity_view_mode.node.search_index
+ - field.field.node.enregistrement.body
+ - field.field.node.enregistrement.field_collectionneurs
+ - field.field.node.enregistrement.field_description
+ - field.field.node.enregistrement.field_entrees
+ - field.field.node.enregistrement.field_genres
+ - field.field.node.enregistrement.field_langues
+ - field.field.node.enregistrement.field_locuteurs
+ - field.field.node.enregistrement.field_son
+ - field.field.node.enregistrement.field_transcript_trad
+ - field.field.node.enregistrement.field_transcript_vo
+ - field.field.node.enregistrement.field_workflow
+ - node.type.enregistrement
+ module:
+ - text
+ - user
+id: node.enregistrement.search_index
+targetEntityType: node
+bundle: enregistrement
+mode: search_index
+content:
+ content_moderation_control:
+ weight: -20
+ region: content
+ settings: { }
+ third_party_settings: { }
+ field_description:
+ weight: 1
+ label: hidden
+ settings: { }
+ third_party_settings: { }
+ type: text_default
+ region: content
+ field_entrees:
+ weight: 0
+ label: hidden
+ settings:
+ link: false
+ third_party_settings: { }
+ type: entity_reference_label
+ region: content
+hidden:
+ body: true
+ chutier_actions: true
+ field_collectionneurs: true
+ field_genres: true
+ field_langues: true
+ field_locuteurs: true
+ field_son: true
+ field_transcript_trad: true
+ field_transcript_vo: true
+ field_workflow: true
+ langcode: true
+ links: true
diff --git a/sites/default/config/sync/core.extension.yml b/sites/default/config/sync/core.extension.yml
index f86818653..bd1ff4379 100644
--- a/sites/default/config/sync/core.extension.yml
+++ b/sites/default/config/sync/core.extension.yml
@@ -35,6 +35,7 @@ module:
edlp_home: 0
edlp_migrate: 0
edlp_productions: 0
+ edlp_search: 0
edlp_studio: 0
entity: 0
entity_reference: 0
@@ -67,6 +68,8 @@ module:
profile: 0
redis: 0
responsive_image: 0
+ search_api: 0
+ search_api_db: 0
system: 0
taxonomy: 0
text: 0
diff --git a/sites/default/config/sync/language/en/views.view.frontpage.yml b/sites/default/config/sync/language/en/views.view.frontpage.yml
index a266c298f..86118ee76 100644
--- a/sites/default/config/sync/language/en/views.view.frontpage.yml
+++ b/sites/default/config/sync/language/en/views.view.frontpage.yml
@@ -5,7 +5,7 @@ display:
display_options:
empty:
area_text_custom:
- content: 'No front page content has been created yet.'
+ content: 'No front page content has been created yet.
Follow the
User Guide to start building your site.'
title:
title: 'Welcome to [site:name]'
exposed_form:
diff --git a/sites/default/config/sync/search_api.index.collection.yml b/sites/default/config/sync/search_api.index.collection.yml
new file mode 100644
index 000000000..6514832a6
--- /dev/null
+++ b/sites/default/config/sync/search_api.index.collection.yml
@@ -0,0 +1,223 @@
+uuid: cf2ecdd8-6c34-445d-b67d-c73fe20fd65c
+langcode: fr
+status: true
+dependencies:
+ config:
+ - field.storage.node.body
+ - field.storage.node.field_description
+ - field.storage.node.field_transcript_trad
+ - field.storage.node.field_transcript_vo
+ - field.storage.node.field_entrees
+ - search_api.server.edlp_db
+ module:
+ - taxonomy
+ - search_api
+ - node
+id: collection
+name: Collection
+description: ''
+read_only: false
+field_settings:
+ node_grants:
+ label: 'Node access information'
+ property_path: search_api_node_grants
+ type: string
+ indexed_locked: true
+ type_locked: true
+ hidden: true
+ body:
+ label: Article
+ datasource_id: 'entity:node'
+ property_path: body
+ type: text
+ dependencies:
+ config:
+ - field.storage.node.body
+ title:
+ label: Title
+ datasource_id: 'entity:node'
+ property_path: title
+ type: text
+ boost: !!float 3
+ field_description:
+ label: Description
+ datasource_id: 'entity:node'
+ property_path: field_description
+ type: text
+ boost: !!float 2
+ dependencies:
+ config:
+ - field.storage.node.field_description
+ field_transcript_trad:
+ label: 'Transcription (Traduction)'
+ datasource_id: 'entity:node'
+ property_path: field_transcript_trad
+ type: text
+ boost: 0.8
+ dependencies:
+ config:
+ - field.storage.node.field_transcript_trad
+ field_transcript_vo:
+ label: 'Transcription (Version Originale)'
+ datasource_id: 'entity:node'
+ property_path: field_transcript_vo
+ type: text
+ boost: !!float 8
+ dependencies:
+ config:
+ - field.storage.node.field_transcript_vo
+ status:
+ label: status
+ datasource_id: 'entity:node'
+ property_path: status
+ type: boolean
+ indexed_locked: true
+ type_locked: true
+ uid:
+ label: uid
+ datasource_id: 'entity:node'
+ property_path: uid
+ type: integer
+ indexed_locked: true
+ type_locked: true
+ name:
+ label: 'Entrée(s) » Taxonomy term » Name'
+ datasource_id: 'entity:node'
+ property_path: 'field_entrees:entity:name'
+ type: text
+ dependencies:
+ config:
+ - field.storage.node.field_entrees
+ module:
+ - taxonomy
+datasource_settings:
+ 'entity:node':
+ bundles:
+ default: false
+ selected:
+ - enregistrement
+ languages:
+ default: true
+ selected: { }
+processor_settings:
+ entity_status: { }
+ rendered_item: { }
+ tokenizer:
+ all_fields: true
+ fields:
+ - body
+ - title
+ - field_description
+ - field_transcript_trad
+ - field_transcript_vo
+ - name
+ spaces: ''
+ overlap_cjk: 1
+ minimum_word_size: '3'
+ weights:
+ preprocess_index: -6
+ preprocess_query: -6
+ aggregated_field: { }
+ add_url: { }
+ html_filter:
+ all_fields: false
+ fields:
+ - body
+ - field_description
+ - field_transcript_trad
+ - field_transcript_vo
+ title: true
+ alt: true
+ tags:
+ h1: 5
+ h2: 3
+ h3: 2
+ strong: 2
+ b: 2
+ em: 1
+ u: 1
+ weights:
+ preprocess_index: -15
+ preprocess_query: -15
+ stopwords:
+ all_fields: true
+ fields:
+ - body
+ - title
+ - field_description
+ - field_transcript_trad
+ - field_transcript_vo
+ - name
+ 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
+ weights:
+ preprocess_index: -5
+ preprocess_query: -2
+ ignorecase:
+ all_fields: true
+ fields:
+ - body
+ - title
+ - field_description
+ - field_transcript_trad
+ - field_transcript_vo
+ - name
+ weights:
+ preprocess_index: -20
+ preprocess_query: -20
+ transliteration:
+ all_fields: true
+ fields:
+ - body
+ - title
+ - field_description
+ - field_transcript_trad
+ - field_transcript_vo
+ - name
+ weights:
+ preprocess_index: -20
+ preprocess_query: -20
+ content_access:
+ weights:
+ preprocess_query: -30
+tracker_settings:
+ default:
+ indexing_order: fifo
+options:
+ index_directly: true
+ cron_limit: 50
+server: edlp_db
diff --git a/sites/default/config/sync/search_api.server.edlp_db.yml b/sites/default/config/sync/search_api.server.edlp_db.yml
new file mode 100644
index 000000000..0e87a5fab
--- /dev/null
+++ b/sites/default/config/sync/search_api.server.edlp_db.yml
@@ -0,0 +1,17 @@
+uuid: 6358442e-ab23-47e0-8366-ebdb27b49d32
+langcode: fr
+status: true
+dependencies:
+ module:
+ - search_api_db
+id: edlp_db
+name: 'edlp DB'
+description: ''
+backend: search_api_db
+backend_config:
+ database: 'default:default'
+ min_chars: 3
+ partial_matches: true
+ autocomplete:
+ suggest_suffix: true
+ suggest_words: true
diff --git a/sites/default/config/sync/search_api.settings.yml b/sites/default/config/sync/search_api.settings.yml
new file mode 100644
index 000000000..65799ee06
--- /dev/null
+++ b/sites/default/config/sync/search_api.settings.yml
@@ -0,0 +1,7 @@
+default_cron_limit: 50
+cron_worker_runtime: 15
+default_tracker: default
+tracking_page_size: 100
+_core:
+ default_config_hash: n7m4vlCPoB3_1C7l13LKYsifmLur4QR71mOD7S_5hSE
+langcode: fr
diff --git a/sites/default/config/sync/search_api_db.settings.yml b/sites/default/config/sync/search_api_db.settings.yml
new file mode 100644
index 000000000..03836ad7d
--- /dev/null
+++ b/sites/default/config/sync/search_api_db.settings.yml
@@ -0,0 +1,4 @@
+autocomplete_max_occurrences: 0.9
+_core:
+ default_config_hash: 1oDrVmUfNY3gp0cRBV5bSXj1OdMCinSoSCM5qeZdLNU
+langcode: fr
diff --git a/sites/default/config/sync/user.role.anonymous.yml b/sites/default/config/sync/user.role.anonymous.yml
index c2c069d42..a8c782a10 100644
--- a/sites/default/config/sync/user.role.anonymous.yml
+++ b/sites/default/config/sync/user.role.anonymous.yml
@@ -15,3 +15,4 @@ permissions:
- 'create corpus_documents workflow_transition'
- 'create generique workflow_transition'
- 'view published fil entities'
+ - 'view search api pages'
diff --git a/sites/default/config/sync/user.role.authenticated.yml b/sites/default/config/sync/user.role.authenticated.yml
index 59083827e..4de88227b 100644
--- a/sites/default/config/sync/user.role.authenticated.yml
+++ b/sites/default/config/sync/user.role.authenticated.yml
@@ -28,3 +28,4 @@ permissions:
- 'view own unpublished fil entities'
- 'view published composition entities'
- 'view published fil entities'
+ - 'view search api pages'
diff --git a/sites/default/config/sync/user.role.root.yml b/sites/default/config/sync/user.role.root.yml
index cd544c491..050f34152 100644
--- a/sites/default/config/sync/user.role.root.yml
+++ b/sites/default/config/sync/user.role.root.yml
@@ -45,6 +45,7 @@ permissions:
- 'administer display modes'
- 'administer domain aliases'
- 'administer domains'
+ - 'administer facets'
- 'administer fil entities'
- 'administer filters'
- 'administer footer menu items'
@@ -69,6 +70,8 @@ permissions:
- 'administer profile form display'
- 'administer profile types'
- 'administer responsive images'
+ - 'administer search_api'
+ - 'administer search_api_page'
- 'administer site configuration'
- 'administer software updates'
- 'administer studio'
diff --git a/sites/default/config/sync/views.view.content.yml b/sites/default/config/sync/views.view.content.yml
index e23452d7d..dc80651e0 100644
--- a/sites/default/config/sync/views.view.content.yml
+++ b/sites/default/config/sync/views.view.content.yml
@@ -946,7 +946,7 @@ display:
path: admin/content/production
menu:
type: tab
- title: Production
+ title: Content
description: ''
expanded: false
parent: 'menu_link_content:8b5fed48-c008-4041-9bda-06f997582175'