├── .distignore
├── .editorconfig
├── .github
└── workflows
│ ├── coding-standards.yml
│ └── unit-tests.yml
├── .gitignore
├── .phpcs.xml
├── .phpcs
└── .gitkeep
├── Gruntfile.js
├── README.md
├── README.txt
├── assets
├── admin.css
└── admin.js
├── bin
└── wp-cli.php
├── composer.json
├── composer.lock
├── lib
├── class-sp-admin.php
├── class-sp-api.php
├── class-sp-compat.php
├── class-sp-config.php
├── class-sp-cron.php
├── class-sp-debug.php
├── class-sp-heartbeat.php
├── class-sp-indexable.php
├── class-sp-integration.php
├── class-sp-post.php
├── class-sp-search-suggest.php
├── class-sp-search.php
├── class-sp-singleton.php
├── class-sp-sync-manager.php
├── class-sp-sync-meta.php
├── class-sp-wp-search.php
├── functions.php
└── globals.php
├── multisite.xml
├── package.json
├── phpunit.xml.dist
├── searchpress.php
└── tests
├── bootstrap.php
├── class-searchpress-unit-test-case.php
├── test-admin.php
├── test-api.php
├── test-faceting.php
├── test-functions.php
├── test-general.php
├── test-heartbeat.php
├── test-indexing.php
├── test-integration.php
├── test-mapping-postmeta.php
├── test-mapping.php
├── test-post.php
├── test-search-suggest.php
├── test-searching.php
└── test-sync-meta.php
/.distignore:
--------------------------------------------------------------------------------
1 | # A set of files you probably don't want in your WordPress.org distribution
2 | .distignore
3 | .editorconfig
4 | .git
5 | .gitignore
6 | .travis.yml
7 | .DS_Store
8 | .idea
9 | Thumbs.db
10 | bin
11 | composer.json
12 | composer.lock
13 | Gruntfile.js
14 | package.json
15 | multisite.xml
16 | phpunit.xml
17 | phpunit.xml.dist
18 | phpcs.xml
19 | phpcs.xml.dist
20 | README.md
21 | wp-cli.local.yml
22 | tests
23 | vendor
24 | node_modules
25 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # This file is for unifying the coding style for different editors and IDEs
2 | # editorconfig.org
3 |
4 | # WordPress Coding Standards
5 | # https://make.wordpress.org/core/handbook/coding-standards/
6 |
7 | root = true
8 |
9 | [*]
10 | charset = utf-8
11 | end_of_line = lf
12 | insert_final_newline = true
13 | trim_trailing_whitespace = true
14 | indent_style = tab
15 | indent_size = 4
16 |
17 | [{.jshintrc,*.json,*.yml}]
18 | indent_style = space
19 | indent_size = 2
20 |
21 | [{*.txt,wp-config-sample.php}]
22 | end_of_line = crlf
23 |
--------------------------------------------------------------------------------
/.github/workflows/coding-standards.yml:
--------------------------------------------------------------------------------
1 | name: Coding Standards
2 |
3 | on: pull_request
4 |
5 | jobs:
6 | phpcs:
7 | name: PHPCS
8 | runs-on: ubuntu-latest
9 | strategy:
10 | fail-fast: true
11 | matrix:
12 | php: ['8.2']
13 |
14 | steps:
15 | - name: Cancel previous runs of this workflow (pull requests only)
16 | if: ${{ github.event_name == 'pull_request' }}
17 | uses: styfle/cancel-workflow-action@0.5.0
18 | with:
19 | access_token: ${{ github.token }}
20 |
21 | - name: Checkout code
22 | uses: actions/checkout@v2
23 |
24 | - name: Setup PHP
25 | uses: shivammathur/setup-php@v2
26 | with:
27 | php-version: ${{ matrix.php }}
28 | tools: composer:v2
29 | coverage: none
30 |
31 | - name: Log information
32 | run: |
33 | echo "$GITHUB_REF"
34 | echo "$GITHUB_EVENT_NAME"
35 | git --version
36 | php --version
37 | composer --version
38 |
39 | - name: Validate Composer
40 | run: composer validate --strict
41 |
42 | - name: Install dependencies
43 | uses: ramsey/composer-install@v1
44 | with:
45 | composer-options: "--ignore-platform-reqs"
46 |
47 | - name: Run PHPCS
48 | run: composer phpcs
49 |
--------------------------------------------------------------------------------
/.github/workflows/unit-tests.yml:
--------------------------------------------------------------------------------
1 | name: Unit Tests
2 |
3 | on: pull_request
4 |
5 | jobs:
6 | tests:
7 | name: "WP: ${{ matrix.wp_version }} - PHP: ${{ matrix.php }} - ES: ${{ matrix.es_version }} (MU: ${{ matrix.multisite }})"
8 | runs-on: ubuntu-latest
9 | strategy:
10 | fail-fast: false # do not fail fast, let all the failing tests fail.
11 | matrix:
12 | php: [8.2]
13 | es_version: [7.17.5, 8.10.2]
14 | multisite: [0]
15 | wp_version: ["latest"]
16 | include:
17 | - php: '7.4'
18 | es_version: '8.10.2'
19 | multisite: 0
20 | wp_version: '5.9.3'
21 | - php: '8.0'
22 | es_version: '6.8.23'
23 | multisite: 1
24 | wp_version: '5.9.3'
25 | env:
26 | CACHEDIR: /tmp/test-cache
27 | WP_CORE_DIR: /tmp/wordpress/
28 | WP_TESTS_DIR: /tmp/wordpress-tests-lib
29 | WP_VERSION: ${{ matrix.wp_version }}
30 | WP_MULTISITE: ${{ matrix.multisite }}
31 | services:
32 | mysql:
33 | image: mysql:5.7
34 | env:
35 | MYSQL_ALLOW_EMPTY_PASSWORD: yes
36 | ports:
37 | - 3306:3306
38 | options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
39 |
40 | steps:
41 | - name: Cancel previous runs of this workflow (pull requests only)
42 | if: ${{ github.event_name == 'pull_request' }}
43 | uses: styfle/cancel-workflow-action@0.5.0
44 | with:
45 | access_token: ${{ github.token }}
46 |
47 | - name: Check out code
48 | uses: actions/checkout@v2
49 |
50 | - name: Configure sysctl limits
51 | run: |
52 | sudo swapoff -a
53 | sudo sysctl -w vm.swappiness=1
54 | sudo sysctl -w fs.file-max=262144
55 | sudo sysctl -w vm.max_map_count=262144
56 |
57 | - name: Set up Elasticsearch
58 | uses: elastic/elastic-github-actions/elasticsearch@master
59 | with:
60 | stack-version: ${{ matrix.es_version }}
61 | security-enabled: false
62 |
63 | - name: Set up PHP
64 | uses: shivammathur/setup-php@v2
65 | with:
66 | php-version: ${{ matrix.php }}
67 | extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, gd
68 | tools: composer:v2
69 | coverage: none
70 |
71 | - name: Install dependencies
72 | uses: ramsey/composer-install@v1
73 | with:
74 | composer-options: "--ignore-platform-reqs"
75 |
76 | - name: Log information
77 | run: |
78 | echo "$GITHUB_REF"
79 | echo "$GITHUB_EVENT_NAME"
80 | git --version
81 | php --version
82 | composer --version
83 |
84 | - name: Set up WordPress
85 | run: |
86 | sudo apt-get update && sudo apt-get install subversion
87 | bash <(curl -s "https://raw.githubusercontent.com/wp-cli/sample-plugin/master/bin/install-wp-tests.sh") wordpress_test root '' 127.0.0.1 ${{ matrix.wp_version }}
88 | rm -rf "${WP_CORE_DIR}wp-content/plugins"
89 | mkdir -p "${WP_CORE_DIR}wp-content/plugins/searchpress"
90 | rsync -a --exclude=.git . "${WP_CORE_DIR}wp-content/plugins/searchpress"
91 |
92 | - name: Run tests
93 | run: |
94 | cd ${WP_CORE_DIR}wp-content/plugins/searchpress
95 | composer phpunit
96 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Build files
2 | report
3 | node_modules
4 | vendor
5 |
6 | # Log files
7 | *.log
8 |
9 | # Cache files
10 | .phpcs/*.json
11 | .phpunit.result.cache
12 |
13 | # Ignore temporary OS files
14 | .DS_Store
15 | .DS_Store?
16 | .Spotlight-V100
17 | .Trashes
18 | ehthumbs.db
19 | Thumbs.db
20 | .thumbsdb
21 |
22 | # IDE files
23 | *.code-workspace
24 | .idea
25 | .vscode
26 |
--------------------------------------------------------------------------------
/.phpcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | PHP_CodeSniffer standard for SearchPress.
4 |
5 |
6 |
7 |
8 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | */**/tests/
46 | */node_modules/*
47 | */vendor/*
48 | */bin/*
49 | .phpcs/
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/.phpcs/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alleyinteractive/searchpress/f849fe5332c390a4727609be53099866c7610969/.phpcs/.gitkeep
--------------------------------------------------------------------------------
/Gruntfile.js:
--------------------------------------------------------------------------------
1 | module.exports = function( grunt ) {
2 |
3 | 'use strict';
4 | var banner = '/**\n * <%= pkg.homepage %>\n * Copyright (c) <%= grunt.template.today("yyyy") %>\n * This file is generated automatically. Do not edit.\n */\n';
5 | // Project configuration
6 | grunt.initConfig( {
7 |
8 | pkg: grunt.file.readJSON( 'package.json' ),
9 |
10 | addtextdomain: {
11 | options: {
12 | textdomain: 'searchpress',
13 | },
14 | target: {
15 | files: {
16 | src: [ '*.php', '**/*.php', '!node_modules/**', '!php-tests/**', '!bin/**' ]
17 | }
18 | }
19 | },
20 |
21 | wp_readme_to_markdown: {
22 | your_target: {
23 | files: {
24 | 'README.md': 'readme.txt'
25 | }
26 | },
27 | },
28 |
29 | makepot: {
30 | target: {
31 | options: {
32 | domainPath: '/languages',
33 | mainFile: 'searchpress.php',
34 | exclude: [ 'tests', 'node_modules', 'vendor', 'bin' ],
35 | potFilename: 'searchpress.pot',
36 | potHeaders: {
37 | poedit: true,
38 | 'x-poedit-keywordslist': true
39 | },
40 | type: 'wp-plugin',
41 | updateTimestamp: true
42 | }
43 | }
44 | },
45 | } );
46 |
47 | grunt.loadNpmTasks( 'grunt-wp-i18n' );
48 | grunt.loadNpmTasks( 'grunt-wp-readme-to-markdown' );
49 | grunt.registerTask( 'i18n', ['addtextdomain', 'makepot'] );
50 | grunt.registerTask( 'readme', ['wp_readme_to_markdown'] );
51 |
52 | grunt.util.linefeed = '\n';
53 |
54 | };
55 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | SearchPress
2 | ===========
3 |
4 | 
5 |
6 | Elasticsearch integration for WordPress.
7 |
8 |
9 | Release Information
10 | -------------------
11 |
12 | Each stable release gets tagged and you can [download releases from GitHub](https://github.com/alleyinteractive/searchpress/releases). `master` points to the latest stable release at any given time.
13 |
14 | ### Pre-Release Versions
15 |
16 | Pre-release development happens against `release/` branches. Once a release is ready-to-ship, it is merged into `master` and tagged.
17 |
18 | ### Backwards Compatibility & Breaking Changes
19 |
20 | We try to maintain backwards compatibility as much as possible. It's possible that releases will need to contain breaking changes from time to time, especially as Elasticsearch itself is a constantly changing product. Breaking changes will be detailed in release notes.
21 |
22 | SearchPress has a thorough battery of unit and integration tests to help add compatibility with each new Elasticsearch release, without compromising compatibility with older releases.
23 |
24 | Prerequisites
25 | -------------
26 |
27 | * [Elasticsearch](https://www.elastic.co/elasticsearch): 6.8+
28 | * PHP: 7.4+
29 | * WordPress: 5.9+
30 |
31 |
32 | Setup
33 | -----
34 |
35 | 1. Upload to the `/wp-content/plugins/` directory
36 | 2. Activate the plugin through the 'Plugins' menu in WordPress
37 | 3. You'll be prompted to add your Elasticsearch endpoint and to index your posts
38 | * SearchPress provides a WP-CLI command for faster indexing on large sites
39 | 4. Once indexing is complete, you're good to go!
40 |
41 |
42 | Indexing Post Meta
43 | ------------------
44 |
45 | In early versions of SearchPress, SearchPress would index almost all post meta with the post. Starting in the 0.4 release, SearchPress only indexes the post meta that it is explicitly told to index. Further, it only indexes post meta in the _data types_ that a site's developer plans to use. The principal reason behind this change is performance, and to prevent ["mappings explosion"](https://www.elastic.co/guide/en/elasticsearch/reference/master/mapping.html#mapping-limit-settings).
46 |
47 | Data type casting will only be attempted for a key if the opt-in callback specifies that type for the key in question (see example below for the full list of possible types). However, the data type will still only be indexed if the type casting is successful. For example, attempting to index the meta value `"WordPress"` as a `long` would fail, since it is not a numeric value. This failure is silent, for better or worse, but type casting is overall quite forgiving.
48 |
49 | If a meta key is allowed to be indexed, the meta value will _always_ be indexed as an unanalyzed string (`post_meta.*.raw`) and that type need not be specified. This is primarily for compatibility with [ES_WP_Query](https://github.com/alleyinteractive/es-wp-query), which depends on that key in `EXISTS` queries, among others.
50 |
51 | ### How to index post meta
52 |
53 | ```php
54 | add_filter(
55 | 'sp_post_allowed_meta',
56 | function( $allowed_meta ) {
57 | // Tell SearchPress to index 'some_meta_key' post meta when encountered.
58 | $allowed_meta['some_meta_key'] = [
59 | 'value', // Index as an analyzed string.
60 | 'boolean', // Index as a boolean value.
61 | 'long', // Index as a "long" (integer).
62 | 'double', // Index as a "double" (floating point number).
63 | 'date', // Index as a GMT date-only value in the format Y-m-d.
64 | 'datetime', // Index as a GMT datetime value in the format Y-m-d H:i:s.
65 | 'time', // Index as a GMT time-only value in the format H:i:s.
66 | ];
67 | return $allowed_meta;
68 | }
69 | );
70 | ```
71 |
72 | Changelog
73 | ---------
74 |
75 | ### 0.6
76 |
77 | * **POSSIBLE BREAKING CHANGE** Refactors `SP_Heartbeat` to store the last time the beat was checked as well as the last time the beat was verified. `SP_Heartbeat::get_last_beat()` previously returned an int, but now returns an array.
78 | * Includes assorted improvements for IDE static analysis.
79 |
80 | ### 0.5.1
81 |
82 | * Adds `sp_api_request_url` filter to filter the API url.
83 | * Fixes indexing error when using SP_DEBUG
84 | * Fixes possible fatal where 'sp' is set as a query_var but is not an array
85 | * Fixes CLI display of page size when page size is greater than 999
86 |
87 | ### 0.5
88 |
89 | * **POSSIBLE BREAKING CHANGE**: Moves SearchPress integration to the `posts_pre_query`.
90 | * Adds UI for authentication
91 | * Trims trailing slashes from ES host value
92 | * Adds the `sp_search_uri` filter
93 | * Addresses phpcs issues
94 | * Disables flush via UI
95 | * Adjusts empty-string search integrations
96 | * Filters the log message for the error
97 | * Adds support for pasing an array of terms in SP_WP_Search
98 | * Cleans up inline documentation
99 | * Adds the `sp_request_response` action hook to the `SP_API` class
100 |
101 | ### 0.4.3
102 |
103 | * Manually check for queried taxonomy terms when building facets
104 | * Adds support for Github Actions
105 | * Tests action for PHPCS
106 | * Tests action for unit tests
107 | * Removes double underscore for single to fix PHP warning
108 | * Updates phpunit
109 | * Adds support for ES 8.x
110 | * Adds support for PHP 8.1, 8.2
111 |
112 | ### 0.4.2
113 |
114 | * CLI improvements
115 | * Improves PHPDoc
116 | * Adds more CLI command examples
117 | * Adds more standards from https://make.wordpress.org/cli/handbook/guides/commands-cookbook/
118 | * Uses WP_CLI::log instead of WP_CLI::line
119 | * The debug command checks for the existence of a valid post and adds a warning for SP_DEBUG and SAVEQUERIES
120 | * Uses WP_CLI\Utils\get_flag_value to get flag values
121 | * Uses quote instead of double quotes
122 | * Adds support for --post-types argument
123 |
124 | ### 0.4.1
125 |
126 | * Updates grunt packages to latest versions
127 | * Documents deprecated/removed filters in 0.4.0
128 | * Improves handling of indexing batch with no indexable posts
129 | * Adds filter `sp_post_index_path` for single post paths
130 | * Adds filter `sp_bulk_index_path` for bulk index paths
131 |
132 | ### 0.4
133 |
134 | * **CRITICAL BREAKING CHANGE:** Post meta indexing is now opt-in. See README for more information.
135 | * **POTENTIAL BREAKING CHANGE:** Removes `sp_post_indexable_meta` filter
136 | * Removes `sp_post_ignored_postmeta` filter
137 | * Adds support for ES 5.x, 6.x, 7.x
138 | * Fixes indexing bug with parentless attachments
139 | * Fixes a bug with bulk syncing attachments
140 | * Improves flexibility for custom indexing of posts
141 | * Improves facet lists to exclude current selections
142 | * Adds option in the admin to index content without flushing
143 | * Fixes bug with cached list of post types to sync
144 | * Fixes conflicts with Advanced Post Cache and CLI-based cron runners
145 | * Adds completion suggester API for search-as-you-type functionality
146 | * Fixes bug with SSL cert verification
147 | * Overhaul of phpunit testing environment for performance
148 | * General coding standards cleanup
149 |
150 |
151 | ### 0.3
152 |
153 | * Adds heartbeat to monitor Elasticsearch
154 | * Improves capabilities handling for admin settings
155 | * Adds a status tab to admin page
156 | * Improves test coverage for heartbeat and admin settings
157 | * Fixes bug with post type facet field
158 | * Allows multiple post IDs to be passed to cli index command
159 | * Locally cache API host to improve external referencing to it
160 | * Fixes edge case bugs with indexing, e.g. with long meta strings
161 | * Improves indexed/searched post types and statuses handling
162 | * Tests across a wider range of ES versions using CI
163 | * Stores/checks mapping version
164 | * General code improvements
165 |
166 |
167 | ### 0.2
168 |
169 | * Adds unit testing
170 | * Significant updates to mapping *(breaking change)*
171 | * Enforce data types when indexing
172 | * Adds helper functions
173 | * Adds support for ES 1.0+ *(breaking change)*
174 | * Refactors search *(breaking change)*
175 | * Removes SP_Config::unserialize_meta()
176 | * Adds Heartbeat to automatically disable the integration if ES goes away
177 | * Update to latest WP Coding Standards
178 | * Assorted bug fixes
179 |
180 |
181 | ### 0.1
182 |
183 | * First release!
184 |
--------------------------------------------------------------------------------
/README.txt:
--------------------------------------------------------------------------------
1 | === SearchPress ===
2 | Contributors: mboynes, alleyinteractive
3 | Tags: search, elasticsearch, faceted search, performance
4 | Requires at least: 5.9
5 | Requires PHP: 7.4
6 | Tested up to: 6.4.3
7 | Stable tag: 0.5
8 | License: GPLv2 or later
9 | License URI: http://www.gnu.org/licenses/gpl-2.0.html
10 |
11 | Elasticsearch plugin for WordPress.
12 |
13 | == Description ==
14 |
15 | This plugin indexes your content in Elasticsearch, replaces WordPress' core search
16 | with Elasticsearch, and provides an API for custom queries against the Elasticsearch
17 | server.
18 |
19 | == Prerequisites ==
20 |
21 | * Elasticsearch: 6.8+
22 | * PHP: 7.4+
23 | * WordPress: 5.9+
24 |
25 | == Installation ==
26 |
27 | 1. Upload to the `/wp-content/plugins/` directory
28 | 1. Activate the plugin through the 'Plugins' menu in WordPress
29 | 1. You'll be prompted to add your elasticsearch endpoint and to index your posts.
30 |
--------------------------------------------------------------------------------
/assets/admin.css:
--------------------------------------------------------------------------------
1 | .tools_page_searchpress .tab-content {
2 | display: none;
3 | }
4 | .tools_page_searchpress div.progress {
5 | position: relative;
6 | height: 50px;
7 | border: 2px solid #111;
8 | background: #333;
9 | margin: 9px 0 18px;
10 | }
11 | .tools_page_searchpress div.progress-bar {
12 | background: #0074a2;
13 | position: absolute;
14 | left: 0;
15 | top: 0;
16 | height: 50px;
17 | z-index: 1;
18 | }
19 | .tools_page_searchpress div.progress-text {
20 | color: white;
21 | text-shadow: 1px 1px 0 #333;
22 | line-height: 50px;
23 | text-align: center;
24 | position: absolute;
25 | width: 100%;
26 | z-index: 2;
27 | }
28 | .tools_page_searchpress h3 input {
29 | vertical-align: baseline !important;
30 | margin-left: 9px !important;
31 | }
32 | .tools_page_searchpress p {
33 | width: 500px;
34 | max-width: 100%;
35 | }
36 | .tools_page_searchpress span.explanation {
37 | display:block;
38 | font-size: 90%;
39 | padding-left: 24px;
40 | font-style: italic;
41 | }
42 | .tools_page_searchpress div.host-index {
43 | display: flex;
44 | }
45 | .tools_page_searchpress div.host-index p {
46 | width: 300px;
47 | margin-top: 0;
48 | }
49 | .tools_page_searchpress div.host-index input {
50 | width: 100%;
51 | }
52 | .tools_page_searchpress div.host-index span {
53 | padding: 2em 1em 0;
54 | }
55 | .tools_page_searchpress .auth-options label {
56 | display: block;
57 | margin-bottom: 3px;
58 | }
59 | #searchpress-stats {
60 | margin: 20px 0;
61 | }
62 | #searchpress-stats td, th {
63 | padding: 5px 10px;
64 | text-align: center;
65 | }
66 | #searchpress-stats tbody td {
67 | font-size: 3em;
68 | }
69 | #searchpress-stats .status-ok {
70 | color: #46b450;
71 | }
72 | #searchpress-stats .status-alert {
73 | color: #ffb900;
74 | }
75 | #searchpress-stats .status-shutdown {
76 | color: #dc3232;
77 | }
78 | #searchpress-stats .status-never {
79 | color: #ffb900;
80 | }
81 | #searchpress-stats .status-inactive {
82 | color: #aaa;
83 | }
84 | #searchpress-stats tfoot th {
85 | color: #888;
86 | font-size: 1em;
87 | white-space: nowrap;
88 | }
89 | .tools_page_searchpress [role='tooltip'] {
90 | display: block !important;
91 | }
92 |
--------------------------------------------------------------------------------
/assets/admin.js:
--------------------------------------------------------------------------------
1 | jQuery( function( $ ) {
2 | $( 'a.nav-tab' ).click( function( e ) {
3 | e.preventDefault();
4 | $( '.nav-tab-active' ).removeClass( 'nav-tab-active' );
5 | $( '.tab-content' ).hide();
6 | $( $( this ).attr( 'href' ) ).fadeIn();
7 | $( this ).addClass( 'nav-tab-active' );
8 | } );
9 | $( 'a.nav-tab-active' ).click();
10 |
11 | $('.auth-options input').click( function () {
12 | var show = $( this ).data( 'show' );
13 | if ( show ) {
14 | $( '.sp-auth-options:not(' + show + ')' ).hide();
15 | $( show ).show();
16 | } else {
17 | $( '.sp-auth-options' ).hide();
18 | }
19 | } );
20 | $('.auth-options input:checked').click();
21 |
22 | var progress_total = $( '.progress-bar' ).data( 'total' ) - 0
23 | , progress_processed = $( '.progress-bar' ).data( 'processed' ) - 0;
24 |
25 | setInterval( function() {
26 | jQuery.get( ajaxurl, { action : 'sp_sync_status', t : new Date().getTime() }, function( data ) {
27 | if ( data.processed ) {
28 | if ( data.processed == 'complete' ) {
29 | jQuery( '#sync-processed' ).text( progress_total );
30 | jQuery( '.progress-bar' ).animate( { width: '100%' }, 1000, 'swing', function() {
31 | document.location = searchpress.admin_url + '&complete=1';
32 | } );
33 | } else if ( data.processed > progress_processed ) {
34 | var new_width = Math.round( data.processed / progress_total * 100 );
35 | progress_processed = data.processed;
36 | jQuery( '#sync-processed' ).text( data.processed );
37 | jQuery( '.progress-bar' ).animate( { width: new_width + '%' }, 1000 );
38 | }
39 | }
40 | }, 'json' );
41 | }, 10000 );
42 | } );
43 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "alleyinteractive/searchpress",
3 | "description": "Elasticsearch integration for WordPress.",
4 | "type": "wordpress-plugin",
5 | "keywords": [
6 | "wordpress",
7 | "plugin",
8 | "search",
9 | "searchpress",
10 | "faceted search",
11 | "performance"
12 | ],
13 | "license": "GPL-2.0-or-later",
14 | "authors": [
15 | {
16 | "name": "Alley Interactive",
17 | "email": "noreply@alley.co"
18 | }
19 | ],
20 | "require-dev": {
21 | "alleyinteractive/alley-coding-standards": "^0.3.0",
22 | "php": ">=7.4",
23 | "yoast/phpunit-polyfills": "^1.0"
24 | },
25 | "scripts": {
26 | "phpcbf": "phpcbf .",
27 | "phpcs": "phpcs . --basepath=.",
28 | "phpunit": "phpunit",
29 | "setup": [
30 | "composer install"
31 | ]
32 | },
33 | "config": {
34 | "allow-plugins": {
35 | "dealerdirect/phpcodesniffer-composer-installer": true,
36 | "alleyinteractive/composer-wordpress-autoloader": true
37 | },
38 | "sort-packages": true
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/lib/class-sp-compat.php:
--------------------------------------------------------------------------------
1 | do_index_loop();
26 | if ( SP_Sync_Meta()->running ) {
27 | $this->schedule_reindex();
28 | }
29 | }
30 |
31 | /**
32 | * Schedule the next indexing iteration.
33 | */
34 | public function schedule_reindex() {
35 | if ( ! wp_next_scheduled( 'sp_reindex' ) ) {
36 | wp_schedule_single_event( time() + 5, 'sp_reindex' );
37 | }
38 | }
39 |
40 | /**
41 | * Cancel the indexing process.
42 | */
43 | public function cancel_reindex() {
44 | wp_clear_scheduled_hook( 'sp_reindex' );
45 | SP_Sync_Meta()->stop( 'save' );
46 | }
47 | }
48 |
49 | /**
50 | * Initializes and returns the SP_Cron instance.
51 | *
52 | * @return SP_Singleton The initialized SP_Cron instance.
53 | */
54 | function SP_Cron() { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.FunctionNameInvalid
55 | return SP_Cron::instance();
56 | }
57 | add_action( 'after_setup_theme', 'SP_Cron', 20 );
58 |
--------------------------------------------------------------------------------
/lib/class-sp-debug.php:
--------------------------------------------------------------------------------
1 | log( new WP_Error( 'debug', "SP_Debug.$action (@" . self::split() . ") : $value" ) );
53 | }
54 |
55 | /**
56 | * Determines if the current request is running in a CLI context.
57 | *
58 | * @access public
59 | * @return bool True if the current request is CLI, false if not.
60 | */
61 | public static function is_cli() {
62 | if ( null === self::$is_cli ) {
63 | self::$is_cli = ( defined( 'WP_CLI' ) && WP_CLI && ! wp_doing_cron() );
64 | }
65 | return self::$is_cli;
66 | }
67 |
68 | /**
69 | * Gets a split (number of seconds between now and when the script started).
70 | *
71 | * @return string The split value.
72 | */
73 | public static function split() {
74 | return number_format( microtime( true ) - self::$timer_start, 2 ) . 's';
75 | }
76 |
77 | /**
78 | * Triggers the sp_debug action to include the data before indexing.
79 | *
80 | * @param object $data The data passed to sp_post_pre_index.
81 | */
82 | public static function debug_sp_post_pre_index( $data ) {
83 | do_action( 'sp_debug', '[SP_Post] Post JSON', wp_json_encode( $data ) );
84 | return $data;
85 | }
86 | }
87 | add_action( 'sp_debug', array( 'SP_Debug', 'debug' ), 10, 2 );
88 | add_filter( 'sp_post_pre_index', array( 'SP_Debug', 'debug_sp_post_pre_index' ), 999 );
89 |
90 | SP_Debug::init();
91 |
--------------------------------------------------------------------------------
/lib/class-sp-heartbeat.php:
--------------------------------------------------------------------------------
1 |
17 | */
18 | public array $healthy_statuses = [ 'yellow', 'green' ];
19 |
20 | /**
21 | * Store the intervals at which the heartbeat gets scheduled.
22 | *
23 | * @var array
24 | */
25 | public array $intervals = [];
26 |
27 | /**
28 | * Store the thresholds that we compare against our last heartbeat.
29 | *
30 | * @var array
31 | */
32 | public array $thresholds = [];
33 |
34 | /**
35 | * The action that the cron fires.
36 | *
37 | * @access protected
38 | * @var string
39 | */
40 | protected string $cron_event = 'sp_heartbeat';
41 |
42 | /**
43 | * Cached result of the heartbeat check.
44 | *
45 | * @var null|'invalid'|'red'|'yellow'|'green'
46 | */
47 | public ?string $beat_result;
48 |
49 | /**
50 | * Cached result of the time of last heartbeat.
51 | *
52 | * @var array<{ queried: int, verified: int }>
53 | */
54 | protected array $last_beat;
55 |
56 | /**
57 | * Set up the singleton.
58 | *
59 | * @codeCoverageIgnore
60 | */
61 | public function setup(): void {
62 | $this->intervals = [
63 | 'heartbeat' => 5 * MINUTE_IN_SECONDS,
64 | 'increase' => MINUTE_IN_SECONDS,
65 | 'stale' => 10 * MINUTE_IN_SECONDS,
66 | ];
67 |
68 | $this->thresholds = [
69 | 'alert' => 8 * MINUTE_IN_SECONDS,
70 | 'notify' => 15 * MINUTE_IN_SECONDS,
71 | 'shutdown' => 15 * MINUTE_IN_SECONDS,
72 | ];
73 |
74 | $this->maybe_schedule_cron();
75 | add_filter( 'sp_ready', [ $this, 'is_ready' ] );
76 | add_action( $this->cron_event, [ $this, 'check_beat' ] );
77 | }
78 |
79 | /**
80 | * Check the status from Elasticsearch.
81 | *
82 | * @param bool $force Optional. If true, bypasses local cache and re-checks the heartbeat from ES.
83 | * @return bool true on success or false on failure.
84 | */
85 | public function check_beat( bool $force = false ): bool {
86 | // Ensure we only check the beat once per request.
87 | $checked = false;
88 | if ( $force || ! isset( $this->beat_result ) ) {
89 | $health = SP_API()->cluster_health();
90 | $this->beat_result = ! empty( $health->status ) ? $health->status : 'invalid';
91 | $checked = true;
92 | }
93 |
94 | // Verify the beat is healthy.
95 | $has_healthy_beat = in_array( $this->beat_result, $this->healthy_statuses, true );
96 |
97 | if ( $checked ) {
98 | if ( $has_healthy_beat ) {
99 | $this->record_pulse();
100 | } else {
101 | $this->record_pulse( $this->last_seen() );
102 | $this->call_nurse();
103 | }
104 | }
105 |
106 | return $has_healthy_beat;
107 | }
108 |
109 | /**
110 | * Get the last recorded beat.
111 | *
112 | * @param bool $force Optional. If true, bypass the local cache and get the value from the option.
113 | * @return array<{queried: int, verified: int}> The times the last time the beat was queried and verified.
114 | */
115 | public function get_last_beat( bool $force = false ): array {
116 | if ( $force || ! isset( $this->last_beat ) ) {
117 | $beat = get_option( 'sp_heartbeat' );
118 | if ( is_array( $beat ) ) {
119 | $this->last_beat = $beat;
120 | } elseif ( is_numeric( $beat ) ) {
121 | // The heartbeat needs to be migrated from the old format.
122 | $this->record_pulse( $beat, $beat );
123 | } else {
124 | // No heartbeat, zero out the heartbeat.
125 | $this->last_beat = [
126 | 'queried' => 0,
127 | 'verified' => 0,
128 | ];
129 | }
130 | }
131 | return $this->last_beat;
132 | }
133 |
134 | /**
135 | * Get the last time the heartbeat was verified.
136 | *
137 | * @return int
138 | */
139 | public function last_seen(): int {
140 | return $this->get_last_beat()['verified'];
141 | }
142 |
143 | /**
144 | * Record that we missed a beat. Presently, this means rescheduling the cron
145 | * at the 'increase' checkin rate.
146 | */
147 | protected function call_nurse(): void {
148 | $this->reschedule_cron( 'increase' );
149 | }
150 |
151 | /**
152 | * Check if the heartbeat is below the given threshold.
153 | *
154 | * @param string $threshold One of SP_Heartbeat::thresholds.
155 | * @param int|null $compared_to Optional. Time to which to compare. Defaults to now.
156 | * @return bool
157 | */
158 | protected function is_heartbeat_below_threshold( string $threshold, ?int $compared_to = null ): bool {
159 | return ( $compared_to ?? time() ) - $this->last_seen() < $this->thresholds[ $threshold ];
160 | }
161 |
162 | /**
163 | * Check if the heartbeat is stale (has not been checked recently).
164 | *
165 | * @return bool
166 | */
167 | protected function is_heartbeat_stale(): bool {
168 | return time() - $this->get_last_beat()['queried'] > $this->intervals['stale'];
169 | }
170 |
171 | /**
172 | * Check if SearchPress has a pulse within the provided threshold.
173 | *
174 | * @param string $threshold Optional. One of SP_Heartbeat::thresholds. Defaults to 'shutdown'.
175 | * @return bool
176 | */
177 | public function has_pulse( string $threshold = 'shutdown' ): bool {
178 | if ( $this->is_heartbeat_below_threshold( $threshold ) ) {
179 | return true;
180 | } elseif ( is_admin() ) {
181 | // There's no heartbeat, but this is an admin request, so query it now.
182 | $this->check_beat();
183 | return $this->is_heartbeat_below_threshold( $threshold );
184 | } elseif ( $this->is_heartbeat_stale() ) {
185 | // If the heartbeat is stale, check the last known status.
186 | return $this->is_heartbeat_below_threshold( $threshold, $this->get_last_beat()['queried'] );
187 | }
188 |
189 | return false;
190 | }
191 |
192 | /**
193 | * Is SearchPress ready for requests? This is added to the `sp_ready`
194 | * filter.
195 | *
196 | * @param bool|null $ready Value passed from other methods.
197 | * @return bool
198 | */
199 | public function is_ready( ?bool $ready ): bool {
200 | if ( false === $ready ) {
201 | return false;
202 | }
203 |
204 | return SP_Config()->active() && $this->has_pulse();
205 | }
206 |
207 | /**
208 | * Record a successful heartbeat.
209 | *
210 | * @param int|null $verified Optional. The time of the last successful heartbeat response.
211 | * @param int|null $queried Optional. The time of the last heartbeat request.
212 | */
213 | public function record_pulse( ?int $verified = null, ?int $queried = null ): void {
214 | $this->last_beat = [
215 | 'queried' => $queried ?? time(),
216 | 'verified' => $verified ?? time(),
217 | ];
218 | update_option( 'sp_heartbeat', $this->last_beat );
219 | $this->reschedule_cron();
220 | }
221 |
222 | /**
223 | * If no heartbeat is scheduled, schedule the default one.
224 | *
225 | * @codeCoverageIgnore
226 | */
227 | protected function maybe_schedule_cron(): void {
228 | if ( ! wp_next_scheduled( $this->cron_event ) ) {
229 | wp_schedule_single_event( time() + $this->intervals['heartbeat'], $this->cron_event );
230 | }
231 | }
232 |
233 | /**
234 | * Reschedules the cron event at the given interval from now.
235 | *
236 | * @param string $interval Time from now to schedule the heartbeat.
237 | * possible values are in SP_Heartbeat::intervals.
238 | */
239 | protected function reschedule_cron( string $interval = 'heartbeat' ): void {
240 | wp_clear_scheduled_hook( $this->cron_event );
241 | wp_schedule_single_event( time() + $this->intervals[ $interval ], $this->cron_event );
242 | }
243 |
244 | /**
245 | * Get the current heartbeat status.
246 | *
247 | * @return 'never'|'alert'|'shutdown'|'ok'|'stale'
248 | */
249 | public function get_status(): string {
250 | if ( ! $this->last_seen() ) {
251 | return 'never';
252 | } elseif ( $this->is_heartbeat_stale() ) {
253 | return 'stale';
254 | } elseif ( $this->is_heartbeat_below_threshold( 'alert' ) ) {
255 | return 'ok';
256 | } elseif ( $this->is_heartbeat_below_threshold( 'shutdown' ) ) {
257 | return 'alert';
258 | } else {
259 | return 'shutdown';
260 | }
261 | }
262 | }
263 |
264 | /**
265 | * Initializes and returns the instance of the SP_Heartbeat class.
266 | *
267 | * @return SP_Heartbeat The initialized instance of the SP_Heartbeat class.
268 | */
269 | function SP_Heartbeat(): SP_Heartbeat { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.FunctionNameInvalid
270 | return SP_Heartbeat::instance();
271 | }
272 | add_action( 'after_setup_theme', 'SP_Heartbeat', 20 );
273 |
--------------------------------------------------------------------------------
/lib/class-sp-indexable.php:
--------------------------------------------------------------------------------
1 | isset( $value ) ? self::limit_string( (string) $value ) : null,
58 | );
59 |
60 | if ( isset( $types['value'] ) ) {
61 | $return['value'] = isset( $value )
62 | ? self::limit_word_length( (string) $value )
63 | : null;
64 | }
65 | if ( isset( $types['long'] ) && is_numeric( $value ) && $value <= PHP_INT_MAX ) {
66 | $return['long'] = (int) $value;
67 | if ( ! is_finite( $return['long'] ) ) {
68 | unset( $return['long'] );
69 | }
70 | }
71 | if ( isset( $types['double'] ) && is_numeric( $value ) ) {
72 | $return['double'] = (float) $value;
73 | if ( ! is_finite( $return['double'] ) ) {
74 | unset( $return['double'] );
75 | }
76 | }
77 | if ( isset( $types['boolean'] ) ) {
78 | // Correct boolean values.
79 | if ( is_string( $value ) && 'false' === strtolower( $value ) ) {
80 | $return['boolean'] = false;
81 | } else {
82 | $return['boolean'] = (bool) $value;
83 | }
84 | }
85 | if (
86 | ( isset( $types['date'] ) || isset( $types['datetime'] ) || isset( $types['time'] ) )
87 | && strlen( (string) $value ) <= 255 // Limit date/time strings to 255 chars for performance.
88 | ) {
89 | $time = false;
90 | $int = (int) $value;
91 |
92 | // Check to see if this is a timestamp.
93 | if ( (string) $int === (string) $value ) {
94 | $time = $int;
95 | } elseif ( ! is_numeric( $value ) ) {
96 | $time = strtotime( (string) $value );
97 | }
98 |
99 | if ( false !== $time ) {
100 | if ( isset( $types['date'] ) ) {
101 | $return['date'] = gmdate( 'Y-m-d', $time );
102 | }
103 | if ( isset( $types['datetime'] ) ) {
104 | $return['datetime'] = gmdate( 'Y-m-d H:i:s', $time );
105 | }
106 | if ( isset( $types['time'] ) ) {
107 | $return['time'] = gmdate( 'H:i:s', $time );
108 | }
109 | }
110 | }
111 |
112 | return $return;
113 | }
114 |
115 | /**
116 | * Parse out the properties of a date.
117 | *
118 | * @param string $date A date, expected to be in mysql format.
119 | * @return array The parsed date.
120 | */
121 | public static function get_date( $date ) {
122 | if ( empty( $date ) || '0000-00-00 00:00:00' === $date ) {
123 | return null;
124 | }
125 |
126 | $ts = strtotime( $date );
127 | return array(
128 | 'date' => strval( $date ),
129 | 'year' => intval( gmdate( 'Y', $ts ) ),
130 | 'month' => intval( gmdate( 'm', $ts ) ),
131 | 'day' => intval( gmdate( 'd', $ts ) ),
132 | 'hour' => intval( gmdate( 'H', $ts ) ),
133 | 'minute' => intval( gmdate( 'i', $ts ) ),
134 | 'second' => intval( gmdate( 's', $ts ) ),
135 | 'week' => intval( gmdate( 'W', $ts ) ),
136 | 'day_of_week' => intval( gmdate( 'N', $ts ) ),
137 | 'day_of_year' => intval( gmdate( 'z', $ts ) ),
138 | 'seconds_from_day' => intval( mktime( gmdate( 'H', $ts ), gmdate( 'i', $ts ), gmdate( 's', $ts ), 1, 1, 1970 ) ),
139 | 'seconds_from_hour' => intval( mktime( 0, gmdate( 'i', $ts ), gmdate( 's', $ts ), 1, 1, 1970 ) ),
140 | );
141 | }
142 |
143 | /**
144 | * Limit a string in length.
145 | *
146 | * It's important to keep some strings limited in length, because ES will
147 | * choke on tokens greater than 32k.
148 | *
149 | * @static
150 | *
151 | * @param string $string String to (maybe) truncate.
152 | * @param integer $length Length to which to truncate. Defaults to
153 | * SP_Indexable::$token_size_limit.
154 | * @return string
155 | */
156 | public static function limit_string( $string, $length = null ) {
157 | if ( is_null( $length ) ) {
158 | $length = self::$token_size_limit;
159 | }
160 |
161 | return mb_substr( $string, 0, $length );
162 | }
163 |
164 | /**
165 | * Limit word length in a string.
166 | *
167 | * As noted in {@see limit_string()}, ES chokes on tokens longer than 32k.
168 | * This method helps keep words under that limit, which should rarely come
169 | * up. If a string does have a word that is great than {$length}, then the
170 | * entire string is modified to insert a space between words every {$length}
171 | * characters (or sooner, to only insert at word boundaries). Modifying the
172 | * string is not ideal, but this is a significant edge case.
173 | *
174 | * @static
175 | *
176 | * @param string $string String to limit.
177 | * @param integer $length Max token length of tokens. Defaults to
178 | * SP_Indexable::$token_size_limit.
179 | * @return string
180 | */
181 | public static function limit_word_length( $string, $length = null ) {
182 | if ( is_null( $length ) ) {
183 | $length = self::$token_size_limit;
184 | }
185 |
186 | // Only modify the string if it's going to cause issues.
187 | if ( preg_match( '/[^\s]{' . absint( $length ) . '}/', $string ) ) {
188 | return wordwrap( $string, $length, ' ', true );
189 | }
190 |
191 | return $string;
192 | }
193 | }
194 |
--------------------------------------------------------------------------------
/lib/class-sp-integration.php:
--------------------------------------------------------------------------------
1 | init_hooks();
73 | }
74 | }
75 |
76 | /**
77 | * Initializes action and filter hooks used by SearchPress.
78 | *
79 | * @access public
80 | */
81 | public function init_hooks() {
82 | add_filter( 'posts_pre_query', [ $this, 'filter__posts_pre_query' ], 10, 2 );
83 |
84 | // Add our custom query var for advanced searches.
85 | add_filter( 'query_vars', [ $this, 'query_vars' ] );
86 |
87 | // Force the search template if ?sp[force]=1.
88 | add_action( 'parse_query', [ $this, 'force_search_template' ], 5 );
89 | }
90 |
91 | /**
92 | * Removes SearchPress hooks when SearchPress should not be used.
93 | *
94 | * @access public
95 | */
96 | public function remove_hooks() {
97 | remove_filter( 'posts_pre_query', [ $this, 'filter__posts_pre_query' ] );
98 | remove_filter( 'query_vars', [ $this, 'query_vars' ] );
99 | remove_action( 'parse_query', [ $this, 'force_search_template' ], 5 );
100 | }
101 |
102 | /**
103 | * Filter the 'posts_pre_query' action to replace the entire query with the results
104 | * returned by SearchPress.
105 | *
106 | * @param array|null $posts Array of posts, defaults to null.
107 | * @param \WP_Query $query Query object.
108 | * @return array|null
109 | */
110 | public function filter__posts_pre_query( $posts, $query ) {
111 | $should_auto_integrate = $query->is_main_query() && $query->is_search();
112 | if (
113 | /**
114 | * Filters whether a query should automatically be integrated with SearchPress.
115 | *
116 | * @param bool $should_auto_integrate Whether the query should be auto-integrated with SearchPress. This
117 | * defaults to true if the query is the main search query.
118 | * @param WP_Query $query The query object.
119 | */
120 | ! apply_filters( 'sp_integrate_query', $should_auto_integrate, $query )
121 | ) {
122 | return $posts;
123 | }
124 |
125 | // If we put in a phony search term, remove it now.
126 | if ( '1441f19754335ca4638bfdf1aea00c6d' === $query->get( 's' ) ) {
127 | $query->set( 's', '' );
128 | }
129 |
130 | // Force the query to advertise as a search query.
131 | $query->is_search = true;
132 |
133 | $es_wp_query_args = $this->build_es_request( $query );
134 |
135 | // Convert the WP-style args into ES args.
136 | $this->search_obj = new SP_WP_Search( $es_wp_query_args );
137 | $results = $this->search_obj->get_results( 'hits' );
138 |
139 | // Total number of results for paging purposes.
140 | $this->found_posts = $this->search_obj->get_results( 'total' );
141 | $query->found_posts = $this->found_posts;
142 |
143 | // Calculate the maximum number of pages.
144 | $query->max_num_pages = ceil( $this->found_posts / (int) $query->get( 'posts_per_page' ) );
145 |
146 | // Modify the 'query' to mention SearchPress was involved.
147 | $query->request = 'SELECT * FROM {$wpdb->posts} WHERE 1=0 /* SearchPress search results */';
148 |
149 | if ( empty( $results ) ) {
150 | return [];
151 | }
152 |
153 | /**
154 | * Allow the entire SearchPress result to be overridden.
155 | *
156 | * @param WP_Post[]|null $results Query results.
157 | * @param SP_WP_Search $search Search object.
158 | * @param WP_Query WP Query object.
159 | */
160 | $pre_search_results = apply_filters( 'sp_pre_search_results', null, $this->search_obj, $query );
161 | if ( null !== $pre_search_results ) {
162 | return $pre_search_results;
163 | }
164 |
165 | // Get the post IDs of the results.
166 | $post_ids = $this->search_obj->pluck_field();
167 | $post_ids = array_filter( array_map( 'absint', $post_ids ) );
168 | $posts = array_filter( array_map( 'get_post', $post_ids ) );
169 | return $posts;
170 | }
171 |
172 |
173 | /**
174 | * Add a query var for holding advanced search fields.
175 | *
176 | * @param array $qv Query variables to be filtered.
177 | * @return array The filtered list of query variables.
178 | */
179 | public function query_vars( $qv ) {
180 | $qv[] = 'sp';
181 | return $qv;
182 | }
183 |
184 |
185 | /**
186 | * Set a faceted search as a search (and thus force the search template). A hook for the parse_query action.
187 | *
188 | * @param object $wp_query The current WP_Query. Passed by reference and modified if necessary.
189 | */
190 | public function force_search_template( &$wp_query ) {
191 | if ( ! $wp_query->is_main_query() ) {
192 | return;
193 | }
194 |
195 | // Load our sp query string variable.
196 | $this->sp = get_query_var( 'sp' );
197 |
198 | // If this is a search, but not a keyword search, we have to fake it.
199 | if ( ! $wp_query->is_search() && isset( $this->sp['force'] ) && 1 === intval( $this->sp['force'] ) ) {
200 | // First, we'll set the search string to something phony.
201 | $wp_query->set( 's', '1441f19754335ca4638bfdf1aea00c6d' );
202 | $wp_query->is_search = true;
203 | $wp_query->is_home = false;
204 | }
205 | }
206 |
207 | /**
208 | * Given a query object, build the variables needed for an Elasticsearch
209 | * request.
210 | *
211 | * @param WP_Query $query The query to use when building the ES query.
212 | * @access protected
213 | * @return array The ES query to execute.
214 | */
215 | protected function build_es_request( $query ) {
216 | $page = ( $query->get( 'paged' ) ) ? absint( $query->get( 'paged' ) ) : 1;
217 |
218 | // Start building the WP-style search query args.
219 | // They'll be translated to ES format args later.
220 | $es_wp_query_args = [
221 | 'query' => $query->get( 's' ),
222 | 'posts_per_page' => $query->get( 'posts_per_page' ),
223 | 'paged' => $page,
224 | ];
225 |
226 | $query_vars = $this->parse_query( $query );
227 |
228 | // Set taxonomy terms.
229 | if ( ! empty( $query_vars['terms'] ) ) {
230 | $es_wp_query_args['terms'] = $query_vars['terms'];
231 | }
232 |
233 | // Set post types.
234 | if ( ! empty( $query_vars['post_type'] ) ) {
235 | $es_wp_query_args['post_type'] = $query_vars['post_type'];
236 | }
237 |
238 | // Set date range.
239 | if ( $query->get( 'year' ) ) {
240 | if ( $query->get( 'monthnum' ) ) {
241 | // Padding.
242 | $date_monthnum = sprintf( '%02d', $query->get( 'monthnum' ) );
243 |
244 | if ( $query->get( 'day' ) ) {
245 | // Padding.
246 | $date_day = sprintf( '%02d', $query->get( 'day' ) );
247 |
248 | $date_start = $query->get( 'year' ) . '-' . $date_monthnum . '-' . $date_day . ' 00:00:00';
249 | $date_end = $query->get( 'year' ) . '-' . $date_monthnum . '-' . $date_day . ' 23:59:59';
250 | } else {
251 | $days_in_month = gmdate( 't', mktime( 0, 0, 0, $query->get( 'monthnum' ), 14, $query->get( 'year' ) ) ); // 14 = middle of the month so no chance of DST issues
252 |
253 | $date_start = $query->get( 'year' ) . '-' . $date_monthnum . '-01 00:00:00';
254 | $date_end = $query->get( 'year' ) . '-' . $date_monthnum . '-' . $days_in_month . ' 23:59:59';
255 | }
256 | } else {
257 | $date_start = $query->get( 'year' ) . '-01-01 00:00:00';
258 | $date_end = $query->get( 'year' ) . '-12-31 23:59:59';
259 | }
260 |
261 | $es_wp_query_args['date_range'] = [
262 | 'gte' => $date_start,
263 | 'lte' => $date_end,
264 | ];
265 | }
266 |
267 | // Advanced search fields.
268 | if ( ! empty( $this->sp ) ) {
269 | // Date from and to.
270 | if ( ! empty( $this->sp['f'] ) ) {
271 | $gte = strtotime( $this->sp['f'] );
272 | if ( false !== $gte ) {
273 | $es_wp_query_args['date_range']['gte'] = gmdate( 'Y-m-d 00:00:00', $gte );
274 | }
275 | }
276 | if ( ! empty( $this->sp['t'] ) ) {
277 | $lte = strtotime( $this->sp['t'] );
278 | if ( false !== $lte ) {
279 | $es_wp_query_args['date_range']['lte'] = gmdate( 'Y-m-d 23:59:59', $lte );
280 | }
281 | }
282 | }
283 |
284 | if ( ! empty( $es_wp_query_args['date_range'] ) && empty( $es_wp_query_args['date_range']['field'] ) ) {
285 | $es_wp_query_args['date_range']['field'] = 'post_date';
286 | }
287 |
288 | /** Ordering */
289 | // Set results sorting.
290 | $orderby = $query->get( 'orderby' );
291 | if ( ! empty( $orderby ) ) {
292 | if ( in_array( $orderby, [ 'date', 'relevance' ], true ) ) {
293 | $es_wp_query_args['orderby'] = $orderby;
294 | }
295 | }
296 |
297 | // Set sort ordering.
298 | $order = strtolower( $query->get( 'order' ) );
299 | if ( ! empty( $order ) ) {
300 | if ( in_array( $order, [ 'asc', 'desc' ], true ) ) {
301 | $es_wp_query_args['order'] = $order;
302 | }
303 | }
304 |
305 | // Facets.
306 | if ( ! empty( $this->facets ) ) {
307 | $es_wp_query_args['facets'] = $this->facets;
308 | }
309 |
310 | $es_wp_query_args['fields'] = [ 'post_id' ];
311 |
312 | return $es_wp_query_args;
313 | }
314 |
315 | /**
316 | * Gets a list of valid taxonomy query variables, optionally filtering by
317 | * a provided query.
318 | *
319 | * @param WP_Query|bool $query Optional. The query to filter by. Defaults to false.
320 | * @access protected
321 | * @return array An array of valid taxonomy query variables.
322 | */
323 | protected function get_valid_taxonomy_query_vars( $query = false ) {
324 | $taxonomies = get_taxonomies( [ 'public' => true ], 'objects' );
325 | $query_vars = wp_list_pluck( $taxonomies, 'query_var' );
326 | if ( $query ) {
327 | $return = [];
328 | foreach ( $query->query as $qv => $value ) {
329 | if ( in_array( $qv, $query_vars, true ) ) {
330 | $taxonomy = array_search( $qv, $query_vars, true );
331 | $return[ $taxonomy ] = $value;
332 | }
333 | }
334 | return $return;
335 | }
336 | return $query_vars;
337 | }
338 |
339 | /**
340 | * Parses query to be used with SearchPress.
341 | *
342 | * @param WP_Query $query The query to be parsed.
343 | * @access protected
344 | * @return array The parsed query to be executed against Elasticsearch.
345 | */
346 | protected function parse_query( $query ) {
347 | $vars = [];
348 |
349 | // Taxonomy filters.
350 | $terms = $this->get_valid_taxonomy_query_vars( $query );
351 | if ( ! empty( $terms ) ) {
352 | $vars['terms'] = $terms;
353 | }
354 |
355 | // Post type filters.
356 | $indexed_post_types = SP_Config()->sync_post_types();
357 |
358 | // phpcs:disable WordPress.Security.NonceVerification.Recommended
359 | if ( $query->get( 'post_type' ) && 'any' !== $query->get( 'post_type' ) ) {
360 | $post_types = (array) $query->get( 'post_type' );
361 | } elseif ( ! empty( $_GET['post_type'] ) ) {
362 | $post_types = explode( ',', sanitize_text_field( wp_unslash( $_GET['post_type'] ) ) );
363 | } else {
364 | $post_types = false;
365 | }
366 | // phpcs:enable
367 |
368 | $vars['post_type'] = [];
369 |
370 | // Validate post types, making sure they exist and are indexed.
371 | if ( $post_types ) {
372 | foreach ( (array) $post_types as $post_type ) {
373 | if ( in_array( $post_type, $indexed_post_types, true ) ) {
374 | $vars['post_type'][] = $post_type;
375 | }
376 | }
377 | }
378 |
379 | if ( empty( $vars['post_type'] ) ) {
380 | $vars['post_type'] = sp_searchable_post_types();
381 | }
382 |
383 | return $vars;
384 | }
385 | }
386 |
387 | /**
388 | * Returns an initialized instance of the SP_Integration class.
389 | *
390 | * @return SP_Integration An initialized instance of the SP_Integration class.
391 | */
392 | function SP_Integration() { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.FunctionNameInvalid
393 | return SP_Integration::instance();
394 | }
395 | add_action( 'after_setup_theme', 'SP_Integration', 30 ); // Must init after SP_Heartbeat.
396 |
--------------------------------------------------------------------------------
/lib/class-sp-post.php:
--------------------------------------------------------------------------------
1 | id = $post->ID;
37 | $this->fill( $post );
38 | }
39 |
40 |
41 | /**
42 | * Use magic methods to make the normal post properties available in
43 | * OOP style accessing.
44 | *
45 | * @param string $property The property name to update.
46 | * @param mixed $value The value to set.
47 | */
48 | public function __set( $property, $value ) {
49 | $this->data[ $property ] = $value;
50 | }
51 |
52 | /**
53 | * Use magic methods to make the normal post properties available in
54 | * OOP style accessing.
55 | *
56 | * @param string $property The property name to look up.
57 | * @return mixed The property value if set, null if not.
58 | */
59 | public function __get( $property ) {
60 | // Let the post ID be accessed either way.
61 | if ( 'ID' === $property ) {
62 | $property = 'post_id';
63 | }
64 |
65 | return isset( $this->data[ $property ] ) ? $this->data[ $property ] : null;
66 | }
67 |
68 |
69 | /**
70 | * Populate this object with all of the post's properties.
71 | *
72 | * @param WP_Post $post The post object to use when populating properties.
73 | * @access public
74 | */
75 | public function fill( $post ) {
76 | do_action( 'sp_debug', '[SP_Post] Populating Post' );
77 | $apply_filters = apply_filters( 'sp_post_index_filtered_data', false );
78 |
79 | $this->data['post_id'] = intval( $post->ID );
80 | // We're storing the login here instead of user ID, as that's more flexible.
81 | $this->data['post_author'] = $this->get_user( $post->post_author );
82 | $this->data['post_date'] = $this->get_date( $post->post_date );
83 | $this->data['post_date_gmt'] = $this->get_date( $post->post_date_gmt );
84 | $this->data['post_modified'] = $this->get_date( $post->post_modified );
85 | $this->data['post_modified_gmt'] = $this->get_date( $post->post_modified_gmt );
86 | $this->data['post_title'] = self::limit_string( $apply_filters ? strval( get_the_title( $post->ID ) ) : strval( $post->post_title ) );
87 | $this->data['post_excerpt'] = self::limit_word_length( strval( $post->post_excerpt ) );
88 | $this->data['post_content'] = self::limit_word_length( $apply_filters ? strval( str_replace( ']]>', ']]>', apply_filters( 'the_content', $post->post_content ) ) ) : strval( $post->post_content ) ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
89 | $this->data['post_status'] = self::limit_string( strval( $post->post_status ) );
90 | $this->data['post_name'] = self::limit_string( strval( $post->post_name ) );
91 | $this->data['post_parent'] = intval( $post->post_parent );
92 | $this->data['parent_status'] = $post->post_parent ? get_post_status( $post->post_parent ) : '';
93 | $this->data['post_type'] = self::limit_string( strval( $post->post_type ) );
94 | $this->data['post_mime_type'] = self::limit_string( strval( $post->post_mime_type ) );
95 | $this->data['post_password'] = self::limit_string( strval( $post->post_password ) );
96 | $this->data['menu_order'] = intval( $post->menu_order );
97 | $this->data['permalink'] = self::limit_string( strval( esc_url_raw( get_permalink( $post->ID ) ) ) );
98 |
99 | $this->data['terms'] = $this->get_terms( $post );
100 | $this->data['post_meta'] = $this->get_meta( $post->ID );
101 |
102 | // If a date field is empty, kill it to avoid indexing errors.
103 | foreach ( array( 'post_date', 'post_date_gmt', 'post_modified', 'post_modified_gmt' ) as $field ) {
104 | if ( empty( $this->data[ $field ] ) ) {
105 | unset( $this->data[ $field ] );
106 | }
107 | }
108 |
109 | // If post status is inherit, but there's no parent status, index the
110 | // parent status as 'publish'. This is a bit hacky, but required for
111 | // proper indexing and searching.
112 | if ( 'inherit' === $this->data['post_status'] && empty( $this->data['parent_status'] ) ) {
113 | $this->data['parent_status'] = 'publish';
114 | }
115 | }
116 |
117 | /**
118 | * Get post meta for a given post ID.
119 | * Some post meta is removed (you can filter it), and serialized data gets unserialized
120 | *
121 | * @param int $post_id The ID of the post for which to retrieve meta.
122 | * @access public
123 | * @return array 'meta_key' => array( value 1, value 2... ).
124 | */
125 | public static function get_meta( $post_id ) {
126 | /**
127 | * List which post meta should be indexed and the data types it should
128 | * be cast to. Example:
129 | *
130 | * [
131 | * 'my_meta_key' => [
132 | * 'value',
133 | * 'boolean',
134 | * 'long',
135 | * 'double',
136 | * 'date',
137 | * 'datetime',
138 | * 'time',
139 | * ]
140 | * ]
141 | *
142 | * Indexing all meta, as well as unnecessarily indexing additional
143 | * properties of each key, can inflate an index mapping and create
144 | * significant performance issues in Elasticsearch. For further reading,
145 | * {@see https://www.elastic.co/guide/en/elasticsearch/reference/master/mapping.html#mapping-limit-settings}.
146 | *
147 | * @param array $allowed_meta Array of allowed meta keys => data types.
148 | * See above example.
149 | * @param int $post_id ID of post currently being indexed.
150 | */
151 | $allowed_meta = apply_filters(
152 | 'sp_post_allowed_meta',
153 | array(),
154 | $post_id
155 | );
156 |
157 | $meta = array_intersect_key(
158 | (array) get_post_meta( $post_id ),
159 | (array) $allowed_meta
160 | );
161 |
162 | foreach ( $meta as $key => &$values ) {
163 | foreach ( $values as &$value ) {
164 | $value = self::cast_meta_types( $value, $allowed_meta[ $key ] );
165 | }
166 | $values = array_filter( $values );
167 | if ( empty( $values ) ) {
168 | unset( $meta[ $key ] );
169 | }
170 | }
171 |
172 | do_action( 'sp_debug', '[SP_Post] Compiled Meta', $meta );
173 |
174 | /**
175 | * Filter the final array of processed meta to be indexed. This includes
176 | * all the tokenized values.
177 | *
178 | * @param array $meta Processed meta to be index.
179 | * @param int $post_id ID of post currently being indexed.
180 | */
181 | return apply_filters( 'sp_post_indexed_meta', $meta, $post_id );
182 | }
183 |
184 |
185 | /**
186 | * Get all terms across all taxonomies for a given post
187 | *
188 | * @param WP_Post $post The post object to query for terms.
189 | * @access public
190 | * @return array The list of terms for the post.
191 | */
192 | public static function get_terms( $post ) {
193 | if ( ! wp_doing_cron() && defined( 'WP_CLI' ) && WP_CLI ) {
194 | return self::get_terms_efficiently( $post );
195 | }
196 |
197 | $object_terms = array();
198 | $taxonomies = get_object_taxonomies( $post->post_type );
199 | foreach ( $taxonomies as $taxonomy ) {
200 | $these_terms = get_the_terms( $post->ID, $taxonomy );
201 | if ( $these_terms && ! is_wp_error( $these_terms ) ) {
202 | $object_terms = array_merge( $object_terms, $these_terms );
203 | }
204 | }
205 |
206 | if ( empty( $object_terms ) ) {
207 | return;
208 | }
209 |
210 | $terms = array();
211 | foreach ( (array) $object_terms as $term ) {
212 | $terms[ $term->taxonomy ][] = array(
213 | 'term_id' => intval( $term->term_id ),
214 | 'slug' => self::limit_string( strval( $term->slug ) ),
215 | 'name' => self::limit_string( strval( $term->name ) ),
216 | 'parent' => intval( $term->parent ),
217 | );
218 | }
219 |
220 | do_action( 'sp_debug', '[SP_Post] Compiled Terms', $terms );
221 | return $terms;
222 | }
223 |
224 |
225 | /**
226 | * Does the same thing as get_terms but in 1 query instead of
227 | * + 1. Only used in WP-CLI commands.
228 | *
229 | * @codeCoverageIgnore
230 | *
231 | * @param WP_Post $post The post to query for terms.
232 | * @access private
233 | * @return array Terms to index.
234 | */
235 | private static function get_terms_efficiently( $post ) {
236 | global $wpdb;
237 |
238 | $taxonomies = get_object_taxonomies( $post->post_type );
239 | if ( empty( $taxonomies ) ) {
240 | return array();
241 | }
242 |
243 | $query = "SELECT t.*, tt.* FROM {$wpdb->terms} AS t INNER JOIN {$wpdb->term_taxonomy} AS tt ON tt.term_id = t.term_id INNER JOIN {$wpdb->term_relationships} AS tr ON tr.term_taxonomy_id = tt.term_taxonomy_id WHERE tt.taxonomy IN (" . implode( ', ', array_fill( 0, count( $taxonomies ), '%s' ) ) . ') AND tr.object_id = %d ORDER BY t.term_id';
244 |
245 | $params = array_merge( array( $query ), $taxonomies, array( $post->ID ) );
246 |
247 | // phpcs:ignore WordPress.DB
248 | $object_terms = $wpdb->get_results( call_user_func_array( array( $wpdb, 'prepare' ), $params ) );
249 |
250 | if ( ! $object_terms || is_wp_error( $object_terms ) ) {
251 | return array();
252 | }
253 |
254 | $terms = array();
255 | foreach ( (array) $object_terms as $term ) {
256 | $terms[ $term->taxonomy ][] = array(
257 | 'term_id' => intval( $term->term_id ),
258 | 'slug' => strval( $term->slug ),
259 | 'name' => strval( $term->name ),
260 | 'parent' => intval( $term->parent ),
261 | );
262 | }
263 |
264 | do_action( 'sp_debug', '[SP_Post] Compiled Terms Efficiently', $terms );
265 | return $terms;
266 | }
267 |
268 |
269 | /**
270 | * Get information about a post author.
271 | *
272 | * @param int $user_id The user ID to look up.
273 | * @access public
274 | * @return array An array of information about the user.
275 | */
276 | public function get_user( $user_id ) {
277 | if ( ! empty( SP_Sync_Manager()->users[ $user_id ] ) ) {
278 | return SP_Sync_Manager()->users[ $user_id ];
279 | }
280 |
281 | $user = get_userdata( $user_id );
282 | if ( $user instanceof WP_User ) {
283 | $data = array(
284 | 'user_id' => intval( $user_id ),
285 | 'login' => self::limit_string( strval( $user->user_login ) ),
286 | 'display_name' => self::limit_string( strval( $user->display_name ) ),
287 | 'user_nicename' => self::limit_string( strval( $user->user_nicename ) ),
288 | );
289 | } else {
290 | $data = array(
291 | 'user_id' => intval( $user_id ),
292 | 'login' => '',
293 | 'display_name' => '',
294 | 'user_nicename' => '',
295 | );
296 | }
297 | SP_Sync_Manager()->users[ $user_id ] = $data;
298 | do_action( 'sp_debug', '[SP_Post] Compiled User', $data );
299 |
300 | return $data;
301 | }
302 |
303 | /**
304 | * Return this object as JSON
305 | *
306 | * @return string
307 | */
308 | public function to_json() {
309 | /**
310 | * Filter the data prior to indexing. If you need to modify the data sent
311 | * to Elasticsearch, this is likely the best filter to use.
312 | *
313 | * @param array $data Data to be sent to Elasticsearch.
314 | * @param \SP_Post $this This object.
315 | */
316 | return wp_json_encode( apply_filters( 'sp_post_pre_index', $this->data, $this ) );
317 | }
318 |
319 | /**
320 | * Determines whether the current post should be indexed or not.
321 | *
322 | * @access public
323 | * @return bool True if the post should be indexed, false if not.
324 | */
325 | public function should_be_indexed() {
326 | // Check post type.
327 | if ( ! in_array( $this->data['post_type'], SP_Config()->sync_post_types(), true ) ) {
328 | return false;
329 | }
330 |
331 | // Check post status.
332 | if ( 'inherit' === $this->data['post_status'] && ! empty( $this->data['parent_status'] ) ) {
333 | $post_status = $this->data['parent_status'];
334 | } else {
335 | $post_status = $this->data['post_status'];
336 | }
337 | if ( ! in_array( $post_status, SP_Config()->sync_statuses(), true ) ) {
338 | return false;
339 | }
340 |
341 | return apply_filters( 'sp_post_should_be_indexed', true, $this );
342 | }
343 |
344 | /**
345 | * Helper to determine if this post is "searchable". That is, is its
346 | * `post_type` in `sp_searchable_post_types()` and is its `post_status` in
347 | * `sp_searchable_post_statuses()`.
348 | *
349 | * @return boolean true if yes, false if no.
350 | */
351 | public function is_searchable() {
352 | return (
353 | in_array( $this->data['post_type'], sp_searchable_post_types(), true )
354 | && in_array( $this->data['post_status'], sp_searchable_post_statuses(), true )
355 | );
356 | }
357 | }
358 |
--------------------------------------------------------------------------------
/lib/class-sp-search-suggest.php:
--------------------------------------------------------------------------------
1 | get_doc_type();
33 | if ( isset( $mapping['mappings'][ $doc_type ] ) ) {
34 | $props =& $mapping['mappings'][ $doc_type ]['properties'];
35 | } else {
36 | $props =& $mapping['mappings']['properties'];
37 | }
38 |
39 | $props['search_suggest'] = array(
40 | 'type' => 'completion',
41 | );
42 |
43 | return $mapping;
44 | }
45 |
46 | /**
47 | * Update the mapping version to tell SP we modified the map.
48 | *
49 | * @param float $version Mapping version.
50 | * @return float
51 | */
52 | public function sp_map_version( $version ) {
53 | // Add a randomish number to the version.
54 | return $version + 0.073;
55 | }
56 |
57 | /**
58 | * Builds the post data for search suggest.
59 | *
60 | * @param array $data `sp_post_pre_index` data.
61 | * @param SP_Post $sp_post Post being indexed, as an SP_Post.
62 | * @return array Search suggest data.
63 | */
64 | public function sp_post_pre_index( $data, $sp_post ) {
65 | /**
66 | * Filter if the post should be searchable using search suggest.
67 | *
68 | * By default, this assumes that search suggest would be used on the
69 | * frontend, so a post must meet the criteria to be considered "public".
70 | * That is, its post type and post status must exist within the
71 | * `sp_searchable_post_types()` and `sp_searchable_post_statuses()`
72 | * arrays, respectively.
73 | *
74 | * If you're using search suggest in the admin, you should either
75 | * always return true for this filter so that private post types and
76 | * statuses show in the suggestion results, or add a second search
77 | * suggest index with permissions-based access.
78 | *
79 | * @param bool $is_searchable Is this post searchable and thus
80 | * should be added to the search suggest
81 | * data?
82 | * @param array $data sp_post_pre_index data.
83 | * @param \SP_Post $sp_post The \SP_Post object.
84 | */
85 | if ( apply_filters( 'sp_search_suggest_post_is_searchable', $sp_post->is_searchable(), $data, $sp_post ) ) {
86 | /**
87 | * Filters the search suggest data (fields/content).
88 | *
89 | * To filter any other characteristics of search suggest, use the
90 | * `sp_post_pre_index` filter.
91 | *
92 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/2.4/search-suggesters-completion.html
93 | *
94 | * @param array $search_suggest_data Array of data for search suggesters
95 | * completion. By default, this just
96 | * includes the post_title.
97 | * @param array $data sp_post_pre_index data.
98 | * @param \SP_Post $sp_post The \SP_Post object.
99 | */
100 | $data['search_suggest'] = array(
101 | 'input' => apply_filters(
102 | 'sp_search_suggest_data',
103 | array(
104 | $data['post_title'],
105 | ),
106 | $data,
107 | $sp_post
108 | ),
109 | );
110 | }
111 |
112 | return $data;
113 | }
114 |
115 | /**
116 | * Register the REST API routes.
117 | */
118 | public function register_rest_routes() {
119 | register_rest_route(
120 | SP_Config()->namespace,
121 | '/suggest/(?P.+)',
122 | array(
123 | array(
124 | 'methods' => WP_REST_Server::READABLE,
125 | 'callback' => array( $this, 'rest_response' ),
126 | 'permission_callback' => '__return_true',
127 | ),
128 | 'schema' => array( $this, 'rest_schema' ),
129 | )
130 | );
131 | }
132 |
133 | /**
134 | * Generate the REST schema for the search suggestions endpoint.
135 | *
136 | * @return array Endpoint schema data.
137 | */
138 | public function rest_schema() {
139 | return array(
140 | '$schema' => 'http://json-schema.org/draft-04/schema#',
141 | 'title' => __( 'Search suggestions', 'searchpress' ),
142 | 'type' => 'array',
143 | 'items' => array(
144 | 'type' => 'object',
145 | 'properties' => array(
146 | 'text' => array(
147 | 'type' => 'string',
148 | 'description' => __( 'Matching text excerpt.', 'searchpress' ),
149 | ),
150 | '_score' => array(
151 | 'type' => 'number',
152 | 'description' => __( 'Calculated match score of the search result.', 'searchpress' ),
153 | ),
154 | '_source' => array(
155 | 'type' => 'object',
156 | 'properties' => array(
157 | 'post_title' => array(
158 | 'type' => 'string',
159 | 'description' => __( 'Title of the search result.', 'searchpress' ),
160 | ),
161 | 'post_id' => array(
162 | 'type' => 'integer',
163 | 'description' => __( 'ID of the search result.', 'searchpress' ),
164 | ),
165 | 'permalink' => array(
166 | 'type' => 'string',
167 | 'description' => __( 'Permalink to the search result.', 'searchpress' ),
168 | ),
169 | ),
170 | ),
171 | ),
172 | ),
173 | );
174 | }
175 |
176 | /**
177 | * Send API response for REST endpoint.
178 | *
179 | * @param WP_REST_Request $request REST request data.
180 | * @return WP_REST_Response
181 | */
182 | public function rest_response( $request ) {
183 | // Sanitize the request arguments.
184 | $fragment = sanitize_text_field( wp_unslash( $request['fragment'] ) );
185 |
186 | // Query search suggest against the fragment.
187 | $data = $this->get_suggestions( $fragment );
188 |
189 | // Send the response.
190 | return rest_ensure_response( $data );
191 | }
192 |
193 | /**
194 | * Query Elasticsearch for search suggestions.
195 | *
196 | * @param string $fragment Search fragment.
197 | * @return array
198 | */
199 | public function get_suggestions( $fragment ) {
200 | /**
201 | * Filter the raw search suggest query.
202 | *
203 | * @param array Search suggest query.
204 | */
205 | $request = apply_filters(
206 | 'sp_search_suggest_query',
207 | array(
208 | 'suggest' => array(
209 | 'search_suggestions' => array(
210 | 'prefix' => $fragment,
211 | 'completion' => array(
212 | 'field' => 'search_suggest',
213 | ),
214 | ),
215 | ),
216 | '_source' => array(
217 | 'post_id',
218 | 'post_title',
219 | 'permalink',
220 | ),
221 | )
222 | );
223 | $results = SP_API()->search( wp_json_encode( $request ), array( 'output' => ARRAY_A ) );
224 |
225 | $options = ! empty( $results['suggest']['search_suggestions'][0]['options'] )
226 | ? $results['suggest']['search_suggestions'][0]['options']
227 | : array();
228 |
229 | // Remove some data that could be considered sensitive.
230 | $options = array_map(
231 | function( $option ) {
232 | unset( $option['_index'], $option['_type'], $option['_id'] );
233 | return $option;
234 | },
235 | $options
236 | );
237 |
238 | /**
239 | * Filter the raw search suggest options.
240 | *
241 | * @param array $options Search suggest options.
242 | * @param array $results Search suggest raw results.
243 | * @param string $fragment Search fragment producing the results.
244 | */
245 | return apply_filters(
246 | 'sp_search_suggest_results',
247 | $options,
248 | $results,
249 | $fragment
250 | );
251 | }
252 | }
253 |
254 | /**
255 | * Optionally setup search suggest. This runs after the theme and plugins have
256 | * all been setup.
257 | */
258 | function sp_maybe_enable_search_suggest() {
259 | /**
260 | * Checks if search suggestions are enabled. If true, adds the config to
261 | * the mapping. If you'd like to edit it, use the `sp_config_mapping`
262 | * filter.
263 | *
264 | * Note that this will only work on ES 5.0 or later.
265 | *
266 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/7.6/search-suggesters.html#completion-suggester
267 | *
268 | * @param boolean $enabled Enabled if true, disabled if false. Defaults
269 | * to false.
270 | */
271 | if ( apply_filters( 'sp_enable_search_suggest', false ) ) {
272 | // Initialize the singleton.
273 | SP_Search_Suggest::instance();
274 | }
275 | }
276 | add_action( 'after_setup_theme', 'sp_maybe_enable_search_suggest', 100 );
277 |
--------------------------------------------------------------------------------
/lib/class-sp-search.php:
--------------------------------------------------------------------------------
1 | search( $es_args );
51 | }
52 | }
53 |
54 | /**
55 | * Perform an Elasticsearch query.
56 | *
57 | * @param array $es_args Elsticsearch query DSL as a PHP array.
58 | * @return array Raw response from the ES server, parsed by json_Decode.
59 | */
60 | public function search( $es_args ) {
61 | $this->es_args = apply_filters( 'sp_search_query_args', $es_args );
62 | $this->search_results = SP_API()->search( wp_json_encode( $this->es_args ), array( 'output' => ARRAY_A ) );
63 | return $this->search_results;
64 | }
65 |
66 | /**
67 | * Get the results of the current object's query.
68 | *
69 | * @param string $return Optional. The data you want to receive. Options are:
70 | * raw: Default. The full raw response.
71 | * hits: Just the document data (response.hits.hits).
72 | * total: The total number of results found (response.hits.total).
73 | * facets: Just the facet data (response.facets).
74 | * @return mixed Depends on what you've asked to return.
75 | */
76 | public function get_results( $return = 'raw' ) {
77 | switch ( $return ) {
78 | case 'hits':
79 | return ( ! empty( $this->search_results['hits']['hits'] ) ) ? $this->search_results['hits']['hits'] : array();
80 |
81 | case 'total':
82 | if ( isset( $this->search_results['hits']['total']['value'] ) ) {
83 | return (int) $this->search_results['hits']['total']['value'];
84 | }
85 | return ( ! empty( $this->search_results['hits']['total'] ) ) ? intval( $this->search_results['hits']['total'] ) : 0;
86 |
87 | case 'facets':
88 | return ( ! empty( $this->search_results['aggregations'] ) ) ? $this->search_results['aggregations'] : array();
89 |
90 | default:
91 | return $this->search_results;
92 | }
93 | }
94 |
95 | /**
96 | * Pluck a certain field out of an ES response.
97 | *
98 | * @see sp_results_pluck
99 | *
100 | * @param int|string $field A field from the retuls to place instead of the entire object.
101 | * @param bool $as_single Return as single (true) or an array (false). Defaults to true.
102 | * @return array
103 | */
104 | public function pluck_field( $field = null, $as_single = true ) {
105 | if ( ! $field ) {
106 | $field = reset( $this->es_args['_source'] );
107 | }
108 |
109 | return sp_results_pluck( $this->search_results, $field, $as_single );
110 | }
111 |
112 | /**
113 | * Get the posts for this search.
114 | *
115 | * @return array array of WP_Post objects, as with get_posts.
116 | */
117 | public function get_posts() {
118 | if ( isset( $this->posts ) ) {
119 | return $this->posts;
120 | }
121 |
122 | if ( 0 === $this->get_results( 'total' ) ) {
123 | $this->posts = array();
124 | } else {
125 | $ids = $this->pluck_field( 'post_id' );
126 | $this->posts = get_posts( // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.get_posts_get_posts
127 | array(
128 | 'post_type' => array_values( get_post_types() ),
129 | 'post_status' => array_values( get_post_stati() ),
130 | 'posts_per_page' => count( $ids ), // phpcs:ignore WordPress.WP.PostsPerPage.posts_per_page_posts_per_page
131 | 'post__in' => $ids,
132 | 'orderby' => 'post__in',
133 | 'order' => 'ASC',
134 | )
135 | );
136 | }
137 |
138 | return $this->posts;
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/lib/class-sp-singleton.php:
--------------------------------------------------------------------------------
1 |
18 | */
19 | private static array $instances;
20 |
21 | /**
22 | * Ensure singletons can't be instantiated outside the `instance()` method.
23 | */
24 | private function __construct() {
25 | // Don't do anything, needs to be initialized via instance() method.
26 | }
27 |
28 | /**
29 | * Get an instance of the class.
30 | *
31 | * @return Instance
32 | */
33 | public static function instance() {
34 | $class_name = get_called_class();
35 | if ( ! isset( self::$instances[ $class_name ] ) ) {
36 | self::$instances[ $class_name ] = new static();
37 | self::$instances[ $class_name ]->setup();
38 | }
39 | return self::$instances[ $class_name ];
40 | }
41 |
42 | /**
43 | * Sets up the singleton.
44 | */
45 | public function setup() {
46 | // Silence is golden.
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/lib/class-sp-sync-manager.php:
--------------------------------------------------------------------------------
1 | index_post( $post );
47 |
48 | if ( is_wp_error( $response ) && 'unindexable-post' === $response->get_error_code() ) {
49 | // If the post should not be indexed, ensure it's not in the index already.
50 | // @todo This is excessive, figure out a better way around it.
51 | $this->delete_post( $post_id );
52 | do_action( 'sp_debug', "[SP_Sync_Manager] Post {$post_id} is not indexable", $response );
53 | return;
54 | }
55 |
56 | if ( ! $this->parse_error( $response, array( 200, 201 ) ) ) {
57 | do_action( 'sp_debug', "[SP_Sync_Manager] Indexed Post {$post_id}", $response );
58 | } else {
59 | do_action( 'sp_debug', "[SP_Sync_Manager] Error Indexing Post {$post_id}", $response );
60 | }
61 | }
62 |
63 | /**
64 | * Delete a post from the ES index.
65 | *
66 | * @param int $post_id The post ID of the post to delete.
67 | * @access public
68 | */
69 | public function delete_post( $post_id ) {
70 | $response = SP_API()->delete_post( $post_id );
71 |
72 | // We're OK with 404 responses here because a post might not be in the index.
73 | if ( ! $this->parse_error( $response, array( 200, 404 ) ) ) {
74 | do_action( 'sp_debug', '[SP_Sync_Manager] Deleted Post', $response );
75 | } else {
76 | do_action( 'sp_debug', '[SP_Sync_Manager] Error Deleting Post', $response );
77 | }
78 | }
79 |
80 | /**
81 | * Parse any errors found in a single-post ES response.
82 | *
83 | * @param object $response SP_API response.
84 | * @param array $allowed_codes Allowed HTTP status codes. Default is array( 200 ).
85 | * @return bool True if errors are found, false if successful.
86 | */
87 | protected function parse_error( $response, $allowed_codes = array( 200 ) ) {
88 | if ( is_wp_error( $response ) ) {
89 | SP_Sync_Meta()->log( new WP_Error( 'error', gmdate( '[Y-m-d H:i:s] ' ) . $response->get_error_message(), $response->get_error_data() ) );
90 | } elseif ( ! empty( $response->error ) ) {
91 | if ( isset( $response->error->message, $response->error->data ) ) {
92 | SP_Sync_Meta()->log( new WP_Error( 'error', gmdate( '[Y-m-d H:i:s] ' ) . $response->error->message, $response->error->data ) );
93 | } elseif ( isset( $response->error->reason ) ) {
94 | SP_Sync_Meta()->log( new WP_Error( 'error', gmdate( '[Y-m-d H:i:s] ' ) . $response->error->reason ) );
95 | } else {
96 | SP_Sync_Meta()->log( new WP_Error( 'error', gmdate( '[Y-m-d H:i:s] ' ) . wp_json_encode( $response->error ) ) );
97 | }
98 | } elseif ( ! in_array( intval( SP_API()->last_request['response_code'] ), $allowed_codes, true ) ) {
99 | // translators: date, status code, JSON-encoded last request object.
100 | SP_Sync_Meta()->log( new WP_Error( 'error', sprintf( __( '[%1$s] Elasticsearch response failed! Status code %2$d; %3$s', 'searchpress' ), gmdate( 'Y-m-d H:i:s' ), SP_API()->last_request['response_code'], wp_json_encode( SP_API()->last_request ) ) ) );
101 | } elseif ( ! is_object( $response ) ) {
102 | // translators: date, JSON-encoded API response.
103 | SP_Sync_Meta()->log( new WP_Error( 'error', sprintf( __( '[%1$s] Unexpected response from Elasticsearch: %2$s', 'searchpress' ), gmdate( 'Y-m-d H:i:s' ), wp_json_encode( $response ) ) ) );
104 | } else {
105 | return false;
106 | }
107 | return true;
108 | }
109 |
110 | /**
111 | * Get all the posts in a given range.
112 | *
113 | * @param int $start Starting value to use for 'offset' parameter in query.
114 | * @param int $limit Limit value to use for 'posts_per_page' parameter in query.
115 | * @return array Array of found posts.
116 | */
117 | public function get_range( $start, $limit ) {
118 | return $this->get_posts(
119 | array(
120 | 'offset' => $start,
121 | 'posts_per_page' => $limit,
122 | )
123 | );
124 | }
125 |
126 | /**
127 | * Get posts to loop through
128 | *
129 | * @param array $args Arguments passed to get_posts.
130 | * @access public
131 | * @return array
132 | */
133 | public function get_posts( $args = array() ) {
134 | $args = wp_parse_args(
135 | $args,
136 | array(
137 | 'post_status' => null,
138 | 'post_type' => null,
139 | 'orderby' => 'ID',
140 | 'order' => 'ASC',
141 | 'suppress_filters' => true, // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.SuppressFiltersTrue
142 | 'ignore_sticky_posts' => true,
143 | )
144 | );
145 |
146 | if ( empty( $args['post_type'] ) ) {
147 | $args['post_type'] = SP_Config()->sync_post_types();
148 | }
149 |
150 | if ( empty( $args['post_status'] ) ) {
151 | $args['post_status'] = SP_Config()->sync_statuses();
152 | }
153 |
154 | $args = apply_filters( 'searchpress_index_loop_args', $args );
155 |
156 | $query = new WP_Query();
157 | $posts = $query->query( $args );
158 |
159 | do_action( 'sp_debug', '[SP_Sync_Manager] Queried Posts', $args );
160 |
161 | $this->published_posts = $query->found_posts;
162 |
163 | $indexed_posts = array();
164 | foreach ( $posts as $post ) {
165 | $indexed_posts[ $post->ID ] = new SP_Post( $post );
166 | }
167 | do_action( 'sp_debug', '[SP_Sync_Manager] Converted Posts' );
168 |
169 | return $indexed_posts;
170 | }
171 |
172 | /**
173 | * Do an indexing loop. This is the meat of the process.
174 | *
175 | * @return bool
176 | */
177 | public function do_index_loop() {
178 | /**
179 | * Action hook that fires before the index loop starts.
180 | *
181 | * Provides an opportunity to unhook actions that are incompatible with
182 | * the index loop.
183 | *
184 | * @since 0.3.0
185 | */
186 | do_action( 'sp_pre_index_loop' );
187 |
188 | $sync_meta = SP_Sync_Meta();
189 |
190 | $start = $sync_meta->page * $sync_meta->bulk;
191 | do_action( 'sp_debug', '[SP_Sync_Manager] Getting Range' );
192 | $posts = $this->get_range( $start, $sync_meta->bulk );
193 | // Reload the sync meta to ensure it hasn't been canceled while we were getting those posts.
194 | $sync_meta->reload();
195 |
196 | if ( ! $posts || is_wp_error( $posts ) || ! $sync_meta->running ) {
197 | return false;
198 | }
199 |
200 | $response = SP_API()->index_posts( $posts );
201 | do_action( 'sp_debug', sprintf( '[SP_Sync_Manager] Indexed %d Posts', count( $posts ) ), $response );
202 |
203 | $sync_meta->reload();
204 | if ( ! $sync_meta->running ) {
205 | return false;
206 | }
207 |
208 | $sync_meta->processed += count( $posts );
209 |
210 | if ( 200 !== intval( SP_API()->last_request['response_code'] ) ) {
211 | // Should probably throw an error here or something.
212 | $sync_meta->log( new WP_Error( 'error', __( 'ES response failed', 'searchpress' ), SP_API()->last_request ) );
213 | $sync_meta->save();
214 | $this->cancel_reindex();
215 | return false;
216 | } elseif ( ! is_object( $response ) || ! isset( $response->items ) || ! is_array( $response->items ) ) {
217 | $sync_meta->log( new WP_Error( 'error', __( 'Error indexing data', 'searchpress' ), $response ) );
218 | $sync_meta->save();
219 | $this->cancel_reindex();
220 | return false;
221 | } else {
222 | foreach ( $response->items as $post ) {
223 | // Status should be 200 or 201, depending on if we're updating or creating respectively.
224 | if ( ! isset( $post->index->status ) ) {
225 | // translators: post ID, JSON-encoded API response.
226 | $sync_meta->log( new WP_Error( 'warning', sprintf( __( 'Error indexing post %1$s; Response: %2$s', 'searchpress' ), $post->index->_id, wp_json_encode( $post ) ), $post ) );
227 | } elseif ( ! in_array( intval( $post->index->status ), array( 200, 201 ), true ) ) {
228 | // translators: post ID, HTTP response code.
229 | $sync_meta->log( new WP_Error( 'warning', sprintf( __( 'Error indexing post %1$s; HTTP response code: %2$s', 'searchpress' ), $post->index->_id, $post->index->status ), $post ) );
230 | } else {
231 | $sync_meta->success++;
232 | }
233 | }
234 | }
235 | $total_pages = ceil( $this->published_posts / $sync_meta->bulk );
236 | $sync_meta->page++;
237 |
238 | if ( wp_doing_cron() || ! defined( 'WP_CLI' ) || ! WP_CLI ) {
239 | if ( $sync_meta->processed >= $sync_meta->total || $sync_meta->page > $total_pages ) {
240 | SP_Config()->update_settings( array( 'active' => true ) );
241 | $this->cancel_reindex();
242 | } else {
243 | $sync_meta->save();
244 | }
245 | }
246 | return true;
247 | }
248 |
249 | /**
250 | * Initialize a cron reindexing.
251 | */
252 | public function do_cron_reindex() {
253 | SP_Sync_Meta()->start();
254 | SP_Sync_Meta()->total = $this->count_posts();
255 | SP_Sync_Meta()->save();
256 | SP_Cron()->schedule_reindex();
257 | }
258 |
259 | /**
260 | * Cancel reindexing.
261 | */
262 | public function cancel_reindex() {
263 | SP_Cron()->cancel_reindex();
264 | }
265 |
266 | /**
267 | * Count the posts to index.
268 | *
269 | * @param array $args WP_Query args used for counting.
270 | * @return int Total number of posts to index.
271 | */
272 | public function count_posts( $args = array() ) {
273 | if ( false === $this->published_posts ) {
274 | $args = wp_parse_args(
275 | $args,
276 | array(
277 | 'post_type' => null,
278 | 'post_status' => null,
279 | 'posts_per_page' => 1,
280 | )
281 | );
282 | if ( empty( $args['post_type'] ) ) {
283 | $args['post_type'] = SP_Config()->sync_post_types();
284 | }
285 | if ( empty( $args['post_status'] ) ) {
286 | $args['post_status'] = SP_Config()->sync_statuses();
287 | }
288 |
289 | $args = apply_filters( 'searchpress_index_count_args', $args );
290 |
291 | $query = new WP_Query( $args );
292 | $this->published_posts = intval( $query->found_posts );
293 | }
294 | return $this->published_posts;
295 | }
296 |
297 | /**
298 | * Get the number of posts indexed in Elasticsearch.
299 | *
300 | * @return int
301 | */
302 | public function count_posts_indexed() {
303 | $count = SP_API()->get( SP_API()->get_api_endpoint( '_count' ) );
304 | return ! empty( $count->count ) ? intval( $count->count ) : 0;
305 | }
306 | }
307 |
308 | /**
309 | * Returns an initialized instance of the SP_Sync_Manager class.
310 | *
311 | * @return SP_Sync_Manager An initialized instance of the SP_Sync_Manager class.
312 | */
313 | function SP_Sync_Manager() { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.FunctionNameInvalid
314 | return SP_Sync_Manager::instance();
315 | }
316 |
317 | /**
318 | * SP_Sync_Manager only gets instantiated when necessary, so we register these
319 | * hooks outside of the class.
320 | */
321 | if ( SP_Config()->active() ) {
322 | sp_add_sync_hooks();
323 | }
324 |
--------------------------------------------------------------------------------
/lib/class-sp-sync-meta.php:
--------------------------------------------------------------------------------
1 | init();
46 |
47 | if ( $this->is_cli() ) {
48 | return;
49 | }
50 |
51 | $sync_meta = get_option( 'sp_sync_meta' );
52 | if ( ! empty( $sync_meta ) && is_array( $sync_meta ) ) {
53 | foreach ( $sync_meta as $key => $value ) {
54 | $this->data[ $key ] = $value;
55 | }
56 | }
57 | }
58 |
59 | /**
60 | * Set the data defaults.
61 | */
62 | private function init() {
63 | $this->data = array(
64 | 'running' => false, // Is the sync currently running?
65 | 'started' => 0,
66 | 'finished' => 0,
67 | 'bulk' => 500,
68 | 'page' => 0,
69 | 'total' => 0,
70 | 'processed' => 0,
71 | 'success' => 0,
72 | 'messages' => array(),
73 | );
74 | }
75 |
76 | /**
77 | * Trigger that the sync has started.
78 | *
79 | * @param string $save Optional. Should we immediately save the meta?
80 | * Defaults to false.
81 | */
82 | public function start( $save = null ) {
83 | $this->init();
84 | $this->data['running'] = true;
85 | $this->data['started'] = time();
86 | if ( 'save' === $save ) {
87 | $this->save();
88 | }
89 | }
90 |
91 | /**
92 | * Trigger that the sync has stopped.
93 | *
94 | * @param string $save Optional. Should we immediately save the meta?
95 | * Defaults to false.
96 | */
97 | public function stop( $save = null ) {
98 | $this->data['running'] = false;
99 | $this->data['finished'] = time();
100 | if ( 'save' === $save ) {
101 | $this->save();
102 | }
103 | }
104 |
105 | /**
106 | * Save the options.
107 | */
108 | public function save() {
109 | if ( $this->is_cli() ) {
110 | return;
111 | }
112 |
113 | update_option( 'sp_sync_meta', $this->data, false );
114 | }
115 |
116 | /**
117 | * Delete the options.
118 | */
119 | public function delete() {
120 | delete_option( 'sp_sync_meta' );
121 | $this->init();
122 | }
123 |
124 | /**
125 | * Reload the options and be sure it's not from cache.
126 | */
127 | public function reload() {
128 | if ( $this->is_cli() ) {
129 | return;
130 | }
131 |
132 | wp_cache_delete( 'sp_sync_meta', 'options' );
133 | $this->setup();
134 | }
135 |
136 | /**
137 | * Reset the sync meta back to defaults.
138 | *
139 | * @param string $save Whether we should save the reset data.
140 | */
141 | public function reset( $save = null ) {
142 | $this->init();
143 | if ( 'save' === $save ) {
144 | $this->save();
145 | }
146 | }
147 |
148 | /**
149 | * Log a message about the syncing process.
150 | *
151 | * @param WP_Error $error While the message may not be an "error" per se,
152 | * this uses WP_Error to keep organized.
153 | */
154 | public function log( WP_Error $error ) {
155 | if ( ! wp_doing_cron() && defined( 'WP_CLI' ) && WP_CLI ) {
156 | $method = $error->get_error_code();
157 | if ( ! in_array( $method, array( 'success', 'warning', 'error' ), true ) ) {
158 | $method = 'line';
159 | }
160 | $message = $error->get_error_data() ? $error->get_error_message() . '; Data: ' . wp_json_encode( $error->get_error_data() ) : $error->get_error_message();
161 | call_user_func( array( 'WP_CLI', $method ), $message );
162 | $this->data['messages'][ $error->get_error_code() ][] = $message;
163 | } else {
164 | /**
165 | * Filter the log message for the error.
166 | *
167 | * @param string $message Log message
168 | * @param WP_Error $error Error instance.
169 | */
170 | $this->data['messages'][ $error->get_error_code() ][] = apply_filters( 'sp_log_message', $error->get_error_message(), $error );
171 |
172 | set_transient( $this->error_transient, true );
173 | if ( ! $this->data['running'] ) {
174 | $this->save();
175 | }
176 | }
177 | }
178 |
179 | /**
180 | * Clear the messages log.
181 | */
182 | public function clear_log() {
183 | $this->data['messages'] = array();
184 | $this->save();
185 | }
186 |
187 | /**
188 | * Get one of the sync meta properties.
189 | *
190 | * @param string $name Sync meta key.
191 | * @return mixed
192 | */
193 | public function __get( $name ) {
194 | if ( isset( $this->data[ $name ] ) ) {
195 | return $this->data[ $name ];
196 | } else {
197 | return new WP_Error( 'invalid', __( 'Invalid property', 'searchpress' ) );
198 | }
199 | }
200 |
201 | /**
202 | * Set one of the sync meta properties.
203 | *
204 | * @param string $name Sync meta key.
205 | * @param mixed $value Sync meta value.
206 | */
207 | public function __set( $name, $value ) {
208 | if ( isset( $this->data[ $name ] ) ) {
209 | $this->data[ $name ] = $value;
210 | }
211 | }
212 |
213 | /**
214 | * Overloaded isset.
215 | *
216 | * @param string $name Sync meta key.
217 | * @return boolean If the key exists or not.
218 | */
219 | public function __isset( $name ) {
220 | return isset( $this->data[ $name ] );
221 | }
222 |
223 | /**
224 | * Are we using WP_CLI right now?
225 | *
226 | * @access protected
227 | *
228 | * @return bool
229 | */
230 | protected function is_cli() {
231 | return ( defined( 'WP_CLI' ) && WP_CLI && ! wp_doing_cron() );
232 | }
233 |
234 | /**
235 | * Do we have non-full-sync errors?
236 | *
237 | * @return boolean True if we do, false if we don't.
238 | */
239 | public function has_errors() {
240 | return (bool) get_transient( $this->error_transient );
241 | }
242 |
243 | /**
244 | * Clear the "We have errors" flag.
245 | */
246 | public function clear_error_notice() {
247 | delete_transient( $this->error_transient );
248 | }
249 | }
250 |
251 | /**
252 | * Returns an initialized instance of the SP_Sync_Meta class.
253 | *
254 | * @return SP_Sync_Meta An initialized instance of the SP_Sync_Meta class.
255 | */
256 | function SP_Sync_Meta() { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.FunctionNameInvalid
257 | return SP_Sync_Meta::instance();
258 | }
259 |
--------------------------------------------------------------------------------
/lib/functions.php:
--------------------------------------------------------------------------------
1 | $value ) {
27 | if ( empty( $value['_source'] ) ) {
28 | $return[ $key ] = array();
29 | } elseif ( 1 === count( $parts ) ) {
30 | if ( array_key_exists( $field, $value['_source'] ) ) {
31 | $return[ $key ] = (array) $value['_source'][ $field ];
32 | }
33 | } else {
34 | $return[ $key ] = (array) sp_get_array_value_by_path( $value['_source'], $parts );
35 | }
36 |
37 | // If the result was empty, remove it.
38 | if ( array() === $return[ $key ] ) {
39 | unset( $return[ $key ] );
40 | } elseif ( $as_single ) {
41 | $return[ $key ] = reset( $return[ $key ] );
42 | }
43 | }
44 |
45 | return $return;
46 | }
47 |
48 | /**
49 | * Recursively get an deep array value by a "path" (array of keys). This helper
50 | * function helps to collect values from an ES _source response.
51 | *
52 | * This function is easier to illustrate than explain. Given an array
53 | * `[ 'grand' => [ 'parent' => [ 'child' => 1 ] ] ]`, passing the `$path`...
54 | *
55 | * `[ 'grand' ]` yields `[ 'parent' => [ 'child' => 1 ] ]`
56 | * `[ 'grand', 'parent' ]` yields `[ 'child' => 1 ]`
57 | * `[ 'grand', 'parent', 'child' ]` yields `1`
58 | *
59 | * If one of the depths is a numeric array, it will be mapped for the remaining
60 | * path components. In other words, given the an array
61 | * `[ 'parent' => [ [ 'child' => 1 ], [ 'child' => 2 ] ] ]`, passing the `$path`
62 | * `[ 'parent', 'child' ]` yields `[ 1, 2 ]`. This feature does not work with
63 | * multiple depths of numeric arrays.
64 | *
65 | * @param array $array Multi-dimensional array.
66 | * @param array $path Single-dimensional array of array keys.
67 | * @return mixed
68 | */
69 | function sp_get_array_value_by_path( $array, $path = array() ) {
70 | if ( isset( $array[0] ) ) {
71 | return array_map( 'sp_get_array_value_by_path', $array, array_fill( 0, count( $array ), $path ) );
72 | } elseif ( ! empty( $path ) ) {
73 | $part = array_shift( $path );
74 | if ( array_key_exists( $part, $array ) ) {
75 | $array = $array[ $part ];
76 | } else {
77 | return array();
78 | }
79 | }
80 | return empty( $path ) ? $array : sp_get_array_value_by_path( $array, $path );
81 | }
82 |
83 | /**
84 | * Get a list of all searchable post types.
85 | *
86 | * @param bool $reload Optional. Force reload the post types from the cached
87 | * static variable. This is helpful for automated tests.
88 | * @return array Array of post types with 'exclude_from_search' => false.
89 | */
90 | function sp_searchable_post_types( $reload = false ) {
91 | static $post_types;
92 | if ( empty( $post_types ) || $reload ) {
93 | $post_types = array_values( get_post_types( array( 'exclude_from_search' => false ) ) );
94 |
95 | /**
96 | * Filter the *searchable* post types. Also {@see SP_Config::sync_post_types()}
97 | * and the `sp_config_sync_post_types` filter to filter the post types that
98 | * SearchPress indexes in Elasticsearch.
99 | *
100 | * @param array $post_types Post type slugs.
101 | */
102 | $post_types = apply_filters( 'sp_searchable_post_types', $post_types );
103 |
104 | // If we haven't hit `wp_loaded` yet, we don't want to cache the post
105 | // types in the static variable, since not all post types may have been
106 | // registered yet.
107 | if ( ! did_action( 'wp_loaded' ) ) {
108 | $uncached_post_types = $post_types;
109 | $post_types = null;
110 | return $uncached_post_types;
111 | }
112 | }
113 | return $post_types;
114 | }
115 |
116 | /**
117 | * Get a list of all searchable post statuses.
118 | *
119 | * @param bool $reload Optional. Force reload the post statuses from the cached
120 | * static variable. This is helpful for automated tests.
121 | * @return array Array of post statuses. Defaults to 'public' => true.
122 | */
123 | function sp_searchable_post_statuses( $reload = false ) {
124 | static $post_statuses;
125 | if ( empty( $post_statuses ) || $reload ) {
126 | // Start with the statuses that SearchPress syncs, since we can't search
127 | // on anything that isn't in there.
128 | $post_statuses = SP_Config()->sync_statuses();
129 |
130 | // Collect post statuses we don't want to search and exclude them.
131 | $exclude = array_values(
132 | get_post_stati(
133 | array(
134 | 'internal' => true,
135 | 'exclude_from_search' => true,
136 | 'private' => true,
137 | 'protected' => true,
138 | ),
139 | 'names',
140 | 'or'
141 | )
142 | );
143 | $post_statuses = array_values( array_diff( $post_statuses, $exclude ) );
144 |
145 | /**
146 | * Filter the *searchable* post statuses. Also {@see SP_Config::sync_statuses()}
147 | * and the `sp_config_sync_post_statuses` filter to filter the post statuses that
148 | * SearchPress indexes in Elasticsearch.
149 | *
150 | * @param array $post_statuses Post statuses.
151 | */
152 | $post_statuses = apply_filters( 'sp_searchable_post_statuses', $post_statuses );
153 |
154 | // If we haven't hit `wp_loaded` yet, we don't want to cache the post
155 | // statuses in the static variable, since not all post statuses may have
156 | // been registered yet.
157 | if ( ! did_action( 'wp_loaded' ) ) {
158 | $uncached_post_statuses = $post_statuses;
159 | $post_statuses = null;
160 | return $uncached_post_statuses;
161 | }
162 | }
163 | return $post_statuses;
164 | }
165 |
166 | /**
167 | * Run a search through SearchPress using Elasticsearch syntax.
168 | *
169 | * @see SP_Search::search()
170 | *
171 | * @param array $es_args PHP array of ES arguments.
172 | * @param bool $raw_result Whether to return the raw result or a post list.
173 | * @return array Search results.
174 | */
175 | function sp_search( $es_args, $raw_result = false ) {
176 | $s = new SP_Search( $es_args );
177 | return $raw_result ? $s->get_results() : $s->get_posts();
178 | }
179 |
180 | /**
181 | * Run a search through SearchPress using WP-friendly syntax.
182 | *
183 | * @see SP_WP_Search
184 | *
185 | * @param array $wp_args PHP array of search arguments.
186 | * @param bool $raw_result Whether to return the raw result or a post list.
187 | * @return array Search results.
188 | */
189 | function sp_wp_search( $wp_args, $raw_result = false ) {
190 | $s = new SP_WP_Search( $wp_args );
191 | return $raw_result ? $s->get_results() : $s->get_posts();
192 | }
193 |
194 | /**
195 | * To be used with the sp_cluster_health_uri filter, force SP to check the
196 | * global cluster cluster health instead of the index's health. This is helpful
197 | * when the index doesn't exist yet.
198 | *
199 | * @return string Always returns '/_cluster/health'.
200 | */
201 | function sp_global_cluster_health() {
202 | return '/_cluster/health';
203 | }
204 |
205 | /**
206 | * Compare an Elasticsearch version against the one in use. This is a convenient
207 | * wrapper for `version_compare()`, setting the second argument to the current
208 | * version of Elasticsearch.
209 | *
210 | * For example, to see if the current version of Elasticsearch is 5.x, you would
211 | * call `sp_es_version_compare( '5.0' )`.
212 | *
213 | * @param string $version Version number.
214 | * @param string $compare Optional. Test for a particular relationship. Default
215 | * is `>=`.
216 | * @return bool|null Null on failure, bool on success.
217 | */
218 | function sp_es_version_compare( $version, $compare = '>=' ) {
219 | return version_compare( SP_Config()->get_es_version(), $version, $compare );
220 | }
221 |
222 | /**
223 | * Make a remote request.
224 | *
225 | * This is separated out as its own function in order to filter the callable
226 | * which is used to make the request. This pattern allows you to replace or
227 | * wrap the request to wp_remote_request() as needed. The filtered callable is
228 | * immediately invoked.
229 | *
230 | * @param string $url ES endpoint URL.
231 | * @param array $request_params Optional. Request arguments. Default empty
232 | * array.
233 | * @return WP_Error|array The response or WP_Error on failure.
234 | */
235 | function sp_remote_request( $url, $request_params = array() ) {
236 | /**
237 | * Filter the callable used to make API requests to ES.
238 | *
239 | * @param callable $callable Request callable. Should be compatible
240 | * with wp_remote_request.
241 | * @param string $url ES endpoint URL.
242 | * @param array $request_params Optional. Request arguments. Default
243 | * empty array.
244 | */
245 | $callable = apply_filters(
246 | 'sp_remote_request',
247 | 'wp_remote_request',
248 | $url,
249 | $request_params
250 | );
251 |
252 | // Revert back to wp_remote_request if something went awry.
253 | if ( ! is_callable( $callable ) ) {
254 | $callable = 'wp_remote_request';
255 | }
256 |
257 | return call_user_func( $callable, $url, $request_params );
258 | }
259 |
260 | /**
261 | * Add the syncing actions for post changes.
262 | */
263 | function sp_add_sync_hooks() {
264 | add_action( 'save_post', array( SP_Sync_Manager(), 'sync_post' ) );
265 | add_action( 'edit_attachment', array( SP_Sync_Manager(), 'sync_post' ) );
266 | add_action( 'add_attachment', array( SP_Sync_Manager(), 'sync_post' ) );
267 | add_action( 'deleted_post', array( SP_Sync_Manager(), 'delete_post' ) );
268 | add_action( 'trashed_post', array( SP_Sync_Manager(), 'delete_post' ) );
269 | }
270 |
271 | /**
272 | * Remove the syncing actions for post changes.
273 | */
274 | function sp_remove_sync_hooks() {
275 | remove_action( 'save_post', array( SP_Sync_Manager(), 'sync_post' ) );
276 | remove_action( 'edit_attachment', array( SP_Sync_Manager(), 'sync_post' ) );
277 | remove_action( 'add_attachment', array( SP_Sync_Manager(), 'sync_post' ) );
278 | remove_action( 'deleted_post', array( SP_Sync_Manager(), 'delete_post' ) );
279 | remove_action( 'trashed_post', array( SP_Sync_Manager(), 'delete_post' ) );
280 | }
281 |
--------------------------------------------------------------------------------
/lib/globals.php:
--------------------------------------------------------------------------------
1 | flush didn't respond with 200.
19 | define( 'SP_ERROR_FLUSH_FAIL', '100' );
20 |
21 | // The function SP_Heartbeat->check_beat failed.
22 | define( 'SP_ERROR_NO_BEAT', '101' );
23 |
--------------------------------------------------------------------------------
/multisite.xml:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 | ./tests/
15 |
16 |
17 |
18 |
19 | ./
20 |
21 | ./tests/
22 | ./bin/
23 | ./lib/class-sp-debug.php
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "searchpress",
3 | "version": "0.5.1",
4 | "main": "Gruntfile.js",
5 | "author": "Matthew Boynes"
6 | }
7 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 | ./tests/
12 |
13 |
14 |
15 |
16 | ./
17 |
18 | ./tests/
19 | ./bin/
20 | ./lib/class-sp-debug.php
21 | ./lib/class-sp-singleton.php
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/searchpress.php:
--------------------------------------------------------------------------------
1 | $host,
22 | 'must_init' => false,
23 | 'active' => true,
24 | ) );
25 | require dirname( __FILE__ ) . '/../searchpress.php';
26 |
27 | SP_API()->index = 'searchpress-tests';
28 |
29 | // Make sure ES is running and responding
30 | $tries = 5;
31 | $sleep = 3;
32 | do {
33 | $response = wp_remote_get( $host );
34 | if ( 200 === wp_remote_retrieve_response_code( $response ) ) {
35 | $body = json_decode( wp_remote_retrieve_body( $response ), true );
36 | if ( ! empty( $body['version']['number'] ) ) {
37 | printf( "Elasticsearch is up and running, using version %s.\n", $body['version']['number'] );
38 | }
39 | break;
40 | } else {
41 | printf( "\nInvalid response from ES (%s), sleeping %d seconds and trying again...\n", wp_remote_retrieve_response_code( $response ), $sleep );
42 | sleep( $sleep );
43 | }
44 | } while ( --$tries );
45 |
46 | // If we didn't end with a 200 status code, exit
47 | sp_tests_verify_response_code( $response );
48 |
49 | sp_index_flush_data();
50 |
51 | $i = 0;
52 | while ( ! ( $beat = SP_Heartbeat()->check_beat( true ) ) && $i++ < 5 ) {
53 | echo "\nHeartbeat failed, sleeping 2 seconds and trying again...\n";
54 | sleep( 2 );
55 | }
56 | if ( ! $beat && ! SP_Heartbeat()->check_beat( true ) ) {
57 | echo "\nCould not find a heartbeat!";
58 | exit( 1 );
59 | }
60 | }
61 | tests_add_filter( 'muplugins_loaded', 'sp_manually_load_plugin' );
62 |
63 | function sp_remove_index() {
64 | SP_Config()->flush();
65 | }
66 | tests_add_filter( 'shutdown', 'sp_remove_index' );
67 |
68 | function sp_index_flush_data() {
69 | SP_Config()->flush();
70 |
71 | // Attempt to create the mapping.
72 | $response = SP_Config()->create_mapping();
73 |
74 | if ( ! empty( $response->error ) ) {
75 | echo "Could not create the mapping!\n";
76 |
77 | // Output error data.
78 | if ( ! empty( $response->error->code ) ) {
79 | printf( "Error code `%d`\n", $response->error->code );
80 | } elseif ( ! empty( $response->status ) ) {
81 | printf( "Error code `%d`\n", $response->status );
82 | }
83 | if ( ! empty( $response->error->message ) ) {
84 | printf( "Error message `%s`\n", $response->error->message );
85 | } elseif ( ! empty( $response->error->reason ) && ! empty( $response->error->type ) ) {
86 | printf( "Error: %s\n%s\n", $response->error->type, $response->error->reason );
87 | }
88 | exit( 1 );
89 | }
90 |
91 | SP_API()->post( '_refresh' );
92 | }
93 |
94 | function sp_tests_verify_response_code( $response ) {
95 | if ( '200' != wp_remote_retrieve_response_code( $response ) ) {
96 | printf( "Could not index posts!\nResponse code %s\n", wp_remote_retrieve_response_code( $response ) );
97 | if ( is_wp_error( $response ) ) {
98 | printf( "Message: %s\n", $response->get_error_message() );
99 | }
100 | exit( 1 );
101 | }
102 | }
103 |
104 | require $_tests_dir . '/includes/bootstrap.php';
105 |
106 | // Load a reusable test case.
107 | require_once( dirname( __FILE__ ) . '/class-searchpress-unit-test-case.php' );
108 |
--------------------------------------------------------------------------------
/tests/class-searchpress-unit-test-case.php:
--------------------------------------------------------------------------------
1 | setup();
11 | wp_clear_scheduled_hook( 'sp_heartbeat' );
12 |
13 | // Don't auto-sync posts to ES.
14 | sp_remove_sync_hooks();
15 | }
16 |
17 | public static function tearDownAfterClass(): void {
18 | SP_Sync_Meta()->reset( 'save' );
19 | SP_Sync_Manager()->published_posts = false;
20 | sp_index_flush_data();
21 |
22 | SP_Heartbeat()->record_pulse();
23 | wp_clear_scheduled_hook( 'sp_reindex' );
24 | wp_clear_scheduled_hook( 'sp_heartbeat' );
25 |
26 | parent::tearDownAfterClass();
27 | }
28 |
29 | public function setUp(): void {
30 | parent::setUp();
31 | self::$sp_settings = SP_Config()->get_settings();
32 | }
33 |
34 | public function tearDown(): void {
35 | $this->reset_post_types();
36 | $this->reset_taxonomies();
37 | $this->reset_post_statuses();
38 | SP_Config()->update_settings( self::$sp_settings );
39 | SP_Config()->post_types = null;
40 | SP_Config()->post_statuses = null;
41 | sp_searchable_post_types( true );
42 | sp_searchable_post_statuses( true );
43 |
44 | parent::tearDown();
45 | }
46 |
47 | /**
48 | * Given a set of sp_wp_search arguments, execute a search and return only
49 | * the requested field's data for each result.
50 | *
51 | * @param array|string $args {@see \SP_WP_Search::wp_to_es_args()}.
52 | * @param string $field Field to return.
53 | * @return array
54 | */
55 | function search_and_get_field( $args, $field = 'post_name' ) {
56 | $args = wp_parse_args( $args, array(
57 | 'fields' => $field
58 | ) );
59 | $posts = sp_wp_search( $args, true );
60 | return sp_results_pluck( $posts, $field );
61 | }
62 |
63 | /**
64 | * Force Elasticsearch to refresh its index to make content changes
65 | * available to search.
66 | *
67 | * Without refreshing the index, inserting content then immediately
68 | * searching for it might (and almost certainly will) not return the
69 | * content.
70 | *
71 | * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-refresh.html
72 | */
73 | protected static function refresh_index() {
74 | SP_API()->post( '_refresh' );
75 | }
76 |
77 | /**
78 | * Index one or more posts in Elasticsearch and refresh the index.
79 | *
80 | * @param mixed $posts Can be a post ID, WP_Post object, SP_Post object, or
81 | * an array of any of the above.
82 | */
83 | protected static function index( $posts ) {
84 | $posts = is_array( $posts ) ? $posts : [ $posts ];
85 | SP_API()->index_posts( $posts );
86 | self::refresh_index();
87 | }
88 |
89 | /**
90 | * Create an assortment of sample content.
91 | *
92 | * While some of this content may not be used, it adds enough noise to the
93 | * system to help catch issues that carefully crafted datasets can miss. At
94 | * least, that's the story I tell myself when it catches a false positive
95 | * for me.
96 | */
97 | protected static function create_sample_content() {
98 | $cat_a = self::factory()->term->create( array( 'taxonomy' => 'category', 'name' => 'cat-a' ) );
99 | $cat_b = self::factory()->term->create( array( 'taxonomy' => 'category', 'name' => 'cat-b' ) );
100 | $cat_c = self::factory()->term->create( array( 'taxonomy' => 'category', 'name' => 'cat-c' ) );
101 |
102 | $posts_to_index = array(
103 | self::factory()->post->create( array( 'post_title' => 'tag-נ', 'tags_input' => array( 'tag-נ' ), 'post_date' => '2008-11-01 00:00:00' ) ),
104 | self::factory()->post->create( array( 'post_title' => 'cats-a-b-c', 'post_date' => '2008-12-01 00:00:00', 'post_category' => array( $cat_a, $cat_b, $cat_c ), 'menu_order' => 10 ) ),
105 | self::factory()->post->create( array( 'post_title' => 'cats-a-and-b', 'post_date' => '2009-01-01 00:00:00', 'post_category' => array( $cat_a, $cat_b ) ) ),
106 | self::factory()->post->create( array( 'post_title' => 'cats-b-and-c', 'post_date' => '2009-02-01 00:00:00', 'post_category' => array( $cat_b, $cat_c ) ) ),
107 | self::factory()->post->create( array( 'post_title' => 'cats-a-and-c', 'post_date' => '2009-03-01 00:00:00', 'post_category' => array( $cat_a, $cat_c ) ) ),
108 | self::factory()->post->create( array( 'post_title' => 'cat-a', 'post_date' => '2009-04-01 00:00:00', 'post_category' => array( $cat_a ), 'menu_order' => 6 ) ),
109 | self::factory()->post->create( array( 'post_title' => 'cat-b', 'post_date' => '2009-05-01 00:00:00', 'post_category' => array( $cat_b ) ) ),
110 | self::factory()->post->create( array( 'post_title' => 'cat-c', 'post_date' => '2009-06-01 00:00:00', 'post_category' => array( $cat_c ) ) ),
111 | self::factory()->post->create( array( 'post_title' => 'lorem-ipsum', 'post_date' => '2009-07-01 00:00:00', 'menu_order' => 2 ) ),
112 | self::factory()->post->create( array( 'post_title' => 'comment-test', 'post_date' => '2009-08-01 00:00:00' ) ),
113 | self::factory()->post->create( array( 'post_title' => 'one-trackback', 'post_date' => '2009-09-01 00:00:00' ) ),
114 | self::factory()->post->create( array( 'post_title' => 'many-trackbacks', 'post_date' => '2009-10-01 00:00:00' ) ),
115 | self::factory()->post->create( array( 'post_title' => 'no-comments', 'post_date' => '2009-10-02 00:00:00' ) ),
116 | self::factory()->post->create( array( 'post_title' => 'one-comment', 'post_date' => '2009-11-01 00:00:00' ) ),
117 | self::factory()->post->create( array( 'post_title' => 'contributor-post-approved', 'post_date' => '2009-12-01 00:00:00' ) ),
118 | self::factory()->post->create( array( 'post_title' => 'embedded-video', 'post_date' => '2010-01-01 00:00:00' ) ),
119 | self::factory()->post->create( array( 'post_title' => 'simple-markup-test', 'post_date' => '2010-02-01 00:00:00' ) ),
120 | self::factory()->post->create( array( 'post_title' => 'raw-html-code', 'post_date' => '2010-03-01 00:00:00' ) ),
121 | self::factory()->post->create( array( 'post_title' => 'tags-a-b-c', 'tags_input' => array( 'tag-a', 'tag-b', 'tag-c' ), 'post_date' => '2010-04-01 00:00:00' ) ),
122 | self::factory()->post->create( array( 'post_title' => 'tag-a', 'tags_input' => array( 'tag-a' ), 'post_date' => '2010-05-01 00:00:00' ) ),
123 | self::factory()->post->create( array( 'post_title' => 'tag-b', 'tags_input' => array( 'tag-b' ), 'post_date' => '2010-06-01 00:00:00' ) ),
124 | self::factory()->post->create( array( 'post_title' => 'tag-c', 'tags_input' => array( 'tag-c' ), 'post_date' => '2010-07-01 00:00:00' ) ),
125 | self::factory()->post->create( array( 'post_title' => 'tags-a-and-b', 'tags_input' => array( 'tag-a', 'tag-b' ), 'post_date' => '2010-08-01 00:00:00' ) ),
126 | self::factory()->post->create( array( 'post_title' => 'tags-b-and-c', 'tags_input' => array( 'tag-b', 'tag-c' ), 'post_date' => '2010-09-01 00:00:00' ) ),
127 | self::factory()->post->create( array( 'post_title' => 'tags-a-and-c', 'tags_input' => array( 'tag-a', 'tag-c' ), 'post_date' => '2010-10-01 00:00:00' ) ),
128 | );
129 |
130 | // Update a few posts' modified dates for sorting tests.
131 | self::set_post_modified_date( $posts_to_index[1], '2020-01-02 03:04:03' );
132 | self::set_post_modified_date( $posts_to_index[5], '2020-01-02 03:04:02' );
133 | self::set_post_modified_date( $posts_to_index[8], '2020-01-02 03:04:01' );
134 |
135 | $parent_one = self::factory()->post->create( array( 'post_title' => 'parent-one', 'post_date' => '2007-01-01 00:00:01' ) );
136 | $parent_two = self::factory()->post->create( array( 'post_title' => 'parent-two', 'post_date' => '2007-01-01 00:00:02' ) );
137 | $posts_to_index[] = $parent_one;
138 | $posts_to_index[] = $parent_two;
139 | $posts_to_index[] = self::factory()->post->create( array( 'post_title' => 'parent-three', 'post_date' => '2007-01-01 00:00:03' ) );
140 |
141 | $posts_to_index[] = self::factory()->post->create( array( 'post_title' => 'child-one', 'post_parent' => $parent_one, 'post_date' => '2007-01-01 00:00:04' ) );
142 | $posts_to_index[] = self::factory()->post->create( array( 'post_title' => 'child-two', 'post_parent' => $parent_one, 'post_date' => '2007-01-01 00:00:05' ) );
143 | $posts_to_index[] = self::factory()->post->create( array( 'post_title' => 'child-three', 'post_parent' => $parent_two, 'post_date' => '2007-01-01 00:00:06' ) );
144 | $posts_to_index[] = self::factory()->post->create( array( 'post_title' => 'child-four', 'post_parent' => $parent_two, 'post_date' => '2007-01-01 00:00:07' ) );
145 |
146 | self::index( $posts_to_index );
147 | }
148 |
149 | /**
150 | * Set a post's post_modified date.
151 | *
152 | * WordPress core doesn't provide a way to manually set a post's
153 | * post_modified date.
154 | *
155 | * @see https://core.trac.wordpress.org/ticket/36595
156 | *
157 | * @param int $ID Post ID.
158 | * @param string $post_modified Datetime string in the format Y-m-d H:i:s.
159 | */
160 | protected static function set_post_modified_date( $ID, $post_modified ) {
161 | global $wpdb;
162 | $wpdb->update(
163 | $wpdb->posts,
164 | compact( 'post_modified' ),
165 | compact( 'ID' )
166 | );
167 | clean_post_cache( $ID );
168 | }
169 |
170 | /**
171 | * Fakes a cron job.
172 | */
173 | protected function fake_cron() {
174 | $crons = _get_cron_array();
175 | if ( ! is_array( $crons ) ) {
176 | return;
177 | }
178 |
179 | foreach ( $crons as $timestamp => $cronhooks ) {
180 | if ( ! is_array( $cronhooks ) ) {
181 | continue;
182 | }
183 |
184 | foreach ( $cronhooks as $hook => $keys ) {
185 | if ( substr( $hook, 0, 3 ) !== 'sp_' ) {
186 | continue; // only run our own jobs.
187 | }
188 |
189 | foreach ( $keys as $k => $v ) {
190 | $schedule = $v['schedule'];
191 |
192 | if ( $schedule != false ) {
193 | $new_args = array( $timestamp, $schedule, $hook, $v['args'] );
194 | call_user_func_array( 'wp_reschedule_event', $new_args );
195 | }
196 |
197 | wp_unschedule_event( $timestamp, $hook, $v['args'] );
198 | do_action_ref_array( $hook, $v['args'] );
199 | }
200 | }
201 | }
202 | }
203 |
204 | /**
205 | * Is the current version of WordPress at least ... ?
206 | *
207 | * @param float $min_version Minimum version required, e.g. 3.9.
208 | * @return bool True if it is, false if it isn't.
209 | */
210 | protected function is_wp_at_least( $min_version ) {
211 | global $wp_version;
212 | return floatval( $wp_version ) >= $min_version;
213 | }
214 | }
215 |
--------------------------------------------------------------------------------
/tests/test-api.php:
--------------------------------------------------------------------------------
1 | post->create( array( 'post_title' => 'lorem-ipsum', 'post_date' => '2009-07-01 00:00:00' ) );
13 | self::index( self::$post_id );
14 | }
15 |
16 | function test_api_get() {
17 | $response = SP_API()->get( SP_API()->get_api_endpoint( '_doc', self::$post_id ) );
18 | $this->assertEquals( 'GET', SP_API()->last_request['params']['method'] );
19 | $this->assertEquals( '200', wp_remote_retrieve_response_code( SP_API()->last_request['response'] ) );
20 | $this->assertEquals( self::$post_id, $response->_source->post_id );
21 |
22 | SP_API()->get( SP_API()->get_api_endpoint( '_doc', 'foo' ) );
23 | $this->assertEquals( 'GET', SP_API()->last_request['params']['method'] );
24 | $this->assertEquals( '404', wp_remote_retrieve_response_code( SP_API()->last_request['response'] ) );
25 | }
26 |
27 | function test_api_post() {
28 | $response = SP_API()->post( SP_API()->get_api_endpoint( '_search' ), '{"query":{"match_all":{}}}' );
29 | $this->assertEquals( 'POST', SP_API()->last_request['params']['method'] );
30 | $this->assertEquals( '200', wp_remote_retrieve_response_code( SP_API()->last_request['response'] ) );
31 | $this->assertEquals( self::$post_id, $response->hits->hits[0]->_source->post_id );
32 | }
33 |
34 | function test_api_put() {
35 | SP_API()->get( SP_API()->get_api_endpoint( '_doc', '123456' ) );
36 | $this->assertEquals( '404', wp_remote_retrieve_response_code( SP_API()->last_request['response'] ) );
37 |
38 | SP_API()->put( SP_API()->get_api_endpoint( '_doc', '123456' ), '{"post_id":123456}' );
39 | $this->assertEquals( 'PUT', SP_API()->last_request['params']['method'] );
40 | $this->assertEquals( '201', wp_remote_retrieve_response_code( SP_API()->last_request['response'] ) );
41 |
42 | $response = SP_API()->get( SP_API()->get_api_endpoint( '_doc', '123456' ) );
43 | $this->assertEquals( 123456, $response->_source->post_id );
44 | }
45 |
46 | function test_api_delete() {
47 | SP_API()->put( SP_API()->get_api_endpoint( '_doc', '223456' ), '{"post_id":223456}' );
48 | $this->assertEquals( '201', wp_remote_retrieve_response_code( SP_API()->last_request['response'] ) );
49 | $response = SP_API()->get( SP_API()->get_api_endpoint( '_doc', '223456' ) );
50 | $this->assertEquals( 223456, $response->_source->post_id );
51 |
52 | SP_API()->delete( SP_API()->get_api_endpoint( '_doc', '223456' ) );
53 | $this->assertEquals( '200', wp_remote_retrieve_response_code( SP_API()->last_request['response'] ) );
54 |
55 | SP_API()->delete( SP_API()->get_api_endpoint( '_doc', '223456' ) );
56 | $this->assertEquals( '404', wp_remote_retrieve_response_code( SP_API()->last_request['response'] ) );
57 | }
58 |
59 | function test_api_error() {
60 | $response = json_decode( SP_API()->request( 'http://asdf.jkl;/some/bad/url' ) );
61 | $this->assertNotEmpty( $response->error );
62 | }
63 |
64 | public function test_version() {
65 | $this->assertMatchesRegularExpression( '/^\d+\.\d+\.\d+/', SP_API()->version() );
66 | }
67 |
68 | function test_wrapping_sp_remote_request() {
69 | $pre = '';
70 | $post = '';
71 | $method = null;
72 | $request_time = 0;
73 | add_filter( 'sp_remote_request', function( $callable ) use ( &$pre, &$post, &$method, &$request_time ) {
74 | return function( $url, $request_args ) use ( $callable, &$pre, &$post, &$method, &$request_time ) {
75 | $start = microtime( true );
76 |
77 | $pre = 'before request';
78 | $method = $request_args['method'];
79 | $response = call_user_func( $callable, $url, $request_args );
80 | $post = 'after request';
81 |
82 | $request_time = microtime( true ) - $start;
83 | return $response;
84 | };
85 | } );
86 |
87 | SP_API()->put( SP_API()->get_api_endpoint( '_doc', '123456' ), '{"post_id":123456}' );
88 |
89 | $this->assertSame( 'PUT', $method );
90 | $this->assertSame( 'before request', $pre );
91 | $this->assertSame( 'after request', $post );
92 | $this->assertGreaterThan( 0, $request_time );
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/tests/test-faceting.php:
--------------------------------------------------------------------------------
1 | user->create( array( 'user_login' => 'author_a', 'user_pass' => rand_str(), 'role' => 'author' ) );
14 | $posts_to_index[] = self::factory()->post->create( array( 'post_title' => rand_str(), 'post_author' => $author_a, 'post_date' => '2007-01-04 00:00:00' ) );
15 | $posts_to_index[] = self::factory()->post->create( array( 'post_title' => rand_str(), 'post_author' => $author_a, 'post_date' => '2007-01-05 00:00:00' ) );
16 |
17 | $author_b = self::factory()->user->create( array( 'user_login' => 'author_b', 'user_pass' => rand_str(), 'role' => 'author' ) );
18 | $posts_to_index[] = self::factory()->post->create( array( 'post_title' => rand_str(), 'post_author' => $author_b, 'post_date' => '2007-01-03 00:00:00' ) );
19 |
20 | $cat_a = self::factory()->term->create( array( 'taxonomy' => 'category', 'name' => 'cat-a' ) );
21 | $cat_b = self::factory()->term->create( array( 'taxonomy' => 'category', 'name' => 'cat-b' ) );
22 | $cat_c = self::factory()->term->create( array( 'taxonomy' => 'category', 'name' => 'cat-c' ) );
23 |
24 | $posts_to_index[] = self::factory()->post->create( array( 'post_title' => 'cats-a-b-c', 'post_date' => '2008-12-01 00:00:00', 'post_category' => array( $cat_a, $cat_b, $cat_c ) ) );
25 | $posts_to_index[] = self::factory()->post->create( array( 'post_title' => 'cats-a-and-b', 'post_date' => '2009-01-01 00:00:00', 'post_category' => array( $cat_a, $cat_b ) ) );
26 | $posts_to_index[] = self::factory()->post->create( array( 'post_title' => 'cats-b-and-c', 'post_date' => '2009-02-01 00:00:00', 'post_category' => array( $cat_b, $cat_c ) ) );
27 | $posts_to_index[] = self::factory()->post->create( array( 'post_title' => 'cats-a-and-c', 'post_date' => '2009-03-01 00:00:00', 'post_category' => array( $cat_a, $cat_c ) ) );
28 | $posts_to_index[] = self::factory()->post->create( array( 'post_title' => 'cat-a', 'post_date' => '2009-04-01 00:00:00', 'post_category' => array( $cat_a ) ) );
29 | $posts_to_index[] = self::factory()->post->create( array( 'post_title' => 'cat-b', 'post_date' => '2009-05-01 00:00:00', 'post_category' => array( $cat_b ) ) );
30 | $posts_to_index[] = self::factory()->post->create( array( 'post_title' => 'cat-c', 'post_date' => '2009-06-01 00:00:00', 'post_category' => array( $cat_c ) ) );
31 | $posts_to_index[] = self::factory()->post->create( array( 'post_title' => 'lorem-ipsum', 'post_date' => '2009-07-01 00:00:00', 'post_category' => array( $cat_a ) ) );
32 | $posts_to_index[] = self::factory()->post->create( array( 'post_title' => 'comment-test', 'post_date' => '2009-08-01 00:00:00', 'post_category' => array( $cat_a ) ) );
33 | $posts_to_index[] = self::factory()->post->create( array( 'post_title' => 'one-trackback', 'post_date' => '2009-09-01 00:00:00', 'post_category' => array( $cat_b ) ) );
34 | $posts_to_index[] = self::factory()->post->create( array( 'post_title' => 'many-trackbacks', 'post_date' => '2009-10-01 00:00:00' ) );
35 | $posts_to_index[] = self::factory()->post->create( array( 'post_title' => 'no-comments', 'post_date' => '2009-10-02 00:00:00' ) );
36 | $posts_to_index[] = self::factory()->post->create( array( 'post_title' => 'one-comment', 'post_date' => '2009-11-01 00:00:00' ) );
37 | $posts_to_index[] = self::factory()->post->create( array( 'post_title' => 'contributor-post-approved', 'post_date' => '2009-12-01 00:00:00' ) );
38 | $posts_to_index[] = self::factory()->post->create( array( 'post_title' => 'embedded-video', 'post_date' => '2010-01-01 00:00:00' ) );
39 | $posts_to_index[] = self::factory()->post->create( array( 'post_title' => 'simple-markup-test', 'post_date' => '2010-02-01 00:00:00' ) );
40 | $posts_to_index[] = self::factory()->post->create( array( 'post_title' => 'raw-html-code', 'post_date' => '2010-03-01 00:00:00' ) );
41 | $posts_to_index[] = self::factory()->post->create( array( 'post_title' => 'tags-a-b-c', 'tags_input' => array( 'tag-a', 'tag-b', 'tag-c' ), 'post_date' => '2010-04-01 00:00:00' ) );
42 | $posts_to_index[] = self::factory()->post->create( array( 'post_title' => 'tag-a', 'tags_input' => array( 'tag-a' ), 'post_date' => '2010-05-01 00:00:00' ) );
43 | $posts_to_index[] = self::factory()->post->create( array( 'post_title' => 'tag-a-2', 'tags_input' => array( 'tag-a' ), 'post_date' => '2010-05-01 00:00:01' ) );
44 | $posts_to_index[] = self::factory()->post->create( array( 'post_title' => 'tag-a-3', 'tags_input' => array( 'tag-a' ), 'post_date' => '2010-05-01 00:00:02' ) );
45 | $posts_to_index[] = self::factory()->post->create( array( 'post_title' => 'tag-b', 'tags_input' => array( 'tag-b' ), 'post_date' => '2010-06-01 00:00:00' ) );
46 | $posts_to_index[] = self::factory()->post->create( array( 'post_title' => 'tag-b-2', 'tags_input' => array( 'tag-b' ), 'post_date' => '2010-06-01 00:00:01' ) );
47 | $posts_to_index[] = self::factory()->post->create( array( 'post_title' => 'tag-c', 'tags_input' => array( 'tag-c' ), 'post_date' => '2010-07-01 00:00:00' ) );
48 | $posts_to_index[] = self::factory()->post->create( array( 'post_title' => 'tags-a-and-b', 'tags_input' => array( 'tag-a', 'tag-b' ), 'post_date' => '2010-08-01 00:00:00' ) );
49 | $posts_to_index[] = self::factory()->post->create( array( 'post_title' => 'tags-b-and-c', 'tags_input' => array( 'tag-b', 'tag-c' ), 'post_date' => '2010-09-01 00:00:00' ) );
50 | $posts_to_index[] = self::factory()->post->create( array( 'post_title' => 'tags-a-and-c', 'tags_input' => array( 'tag-a', 'tag-c' ), 'post_date' => '2010-10-01 00:00:00' ) );
51 |
52 | $parent_one = self::factory()->post->create( array( 'post_title' => 'parent-one', 'post_type' => 'page', 'post_date' => '2007-01-01 00:00:01' ) );
53 | $parent_two = self::factory()->post->create( array( 'post_title' => 'parent-two', 'post_type' => 'page', 'post_date' => '2007-01-01 00:00:02' ) );
54 | $posts_to_index[] = $parent_one;
55 | $posts_to_index[] = $parent_two;
56 | $posts_to_index[] = self::factory()->post->create( array( 'post_title' => 'parent-three', 'post_type' => 'page', 'post_date' => '2007-01-01 00:00:03' ) );
57 | $posts_to_index[] = self::factory()->post->create( array( 'post_title' => 'child-one', 'post_parent' => $parent_one, 'post_type' => 'page', 'post_date' => '2007-01-01 00:00:04' ) );
58 | $posts_to_index[] = self::factory()->post->create( array( 'post_title' => 'child-two', 'post_parent' => $parent_one, 'post_type' => 'page', 'post_date' => '2007-01-01 00:00:05' ) );
59 | $posts_to_index[] = self::factory()->post->create( array( 'post_title' => 'child-three', 'post_parent' => $parent_two, 'post_type' => 'page', 'post_date' => '2007-01-01 00:00:06' ) );
60 | $posts_to_index[] = self::factory()->post->create( array( 'post_title' => 'child-four', 'post_parent' => $parent_two, 'post_type' => 'page', 'post_date' => '2007-01-01 00:00:07' ) );
61 |
62 | self::index( $posts_to_index );
63 | }
64 |
65 | function test_faceting() {
66 | $s = new SP_WP_Search( array(
67 | 'post_type' => array( 'post', 'page' ),
68 | 'posts_per_page' => 0,
69 | 'facets' => array(
70 | 'Tag' => array( 'type' => 'taxonomy', 'taxonomy' => 'post_tag', 'count' => 10 ),
71 | 'Post Type' => array( 'type' => 'post_type', 'count' => 10 ),
72 | 'Author' => array( 'type' => 'author', 'count' => 10 ),
73 | 'Histogram' => array( 'type' => 'date_histogram', 'interval' => 'year', 'count' => 10 ),
74 | ),
75 | ) );
76 | $facets = $s->get_results( 'facets' );
77 |
78 | $this->assertNotEmpty( $facets );
79 | $this->assertNotEmpty( $facets['Tag']['buckets'] );
80 | $this->assertNotEmpty( $facets['Post Type']['buckets'] );
81 | $this->assertNotEmpty( $facets['Author']['buckets'] );
82 | $this->assertNotEmpty( $facets['Histogram']['buckets'] );
83 | }
84 |
85 | function test_parsed_data() {
86 | $s = new SP_WP_Search( array(
87 | 'post_type' => array( 'post', 'page' ),
88 | 'posts_per_page' => 0,
89 | 'facets' => array(
90 | 'Tag' => array( 'type' => 'taxonomy', 'taxonomy' => 'post_tag', 'count' => 10 ),
91 | 'Post Type' => array( 'type' => 'post_type', 'count' => 10 ),
92 | ),
93 | ) );
94 | $facet_data = $s->get_facet_data();
95 |
96 | // Tags
97 | $this->assertEquals(
98 | array( 'tag-a', 'tag-b', 'tag-c' ),
99 | wp_list_pluck( $facet_data['Tag']['items'], 'name' )
100 | );
101 | $this->assertEquals(
102 | array( 6, 5, 4 ),
103 | wp_list_pluck( $facet_data['Tag']['items'], 'count' )
104 | );
105 | $this->assertEquals(
106 | array( array( 'tag' => 'tag-a' ), array( 'tag' => 'tag-b' ), array( 'tag' => 'tag-c' ) ),
107 | wp_list_pluck( $facet_data['Tag']['items'], 'query_vars' )
108 | );
109 |
110 | // Post Type
111 | $this->assertEquals( 'Post', $facet_data['Post Type']['items'][0]['name'] );
112 | $this->assertEquals( 'Page', $facet_data['Post Type']['items'][1]['name'] );
113 | $this->assertEquals( 30, $facet_data['Post Type']['items'][0]['count'] );
114 | $this->assertEquals( 7, $facet_data['Post Type']['items'][1]['count'] );
115 | $this->assertEquals( array( 'post_type' => 'post' ), $facet_data['Post Type']['items'][0]['query_vars'] );
116 | $this->assertEquals( array( 'post_type' => 'page' ), $facet_data['Post Type']['items'][1]['query_vars'] );
117 | }
118 |
119 | public function test_post_type() {
120 | $label = rand_str();
121 | register_post_type( 'custom-post-type', array(
122 | 'public' => true,
123 | 'labels' => array(
124 | 'singular_name' => $label,
125 | ),
126 | ) );
127 | SP_Config()->post_types = null;
128 | sp_searchable_post_types( true );
129 |
130 | $posts_to_index = array(
131 | self::factory()->post->create( array( 'post_title' => 'first lorem', 'post_date' => '2010-01-01 00:00:00', 'post_type' => 'custom-post-type' ) ),
132 | self::factory()->post->create( array( 'post_title' => 'second lorem', 'post_date' => '2010-02-01 00:00:00', 'post_type' => 'custom-post-type' ) ),
133 | );
134 | self::index( $posts_to_index );
135 |
136 | $s = new SP_WP_Search( array(
137 | 'post_type' => array( 'post', 'page', 'custom-post-type' ),
138 | 'posts_per_page' => 0,
139 | 'facets' => array(
140 | 'Post Type' => array( 'type' => 'post_type', 'count' => 10 ),
141 | ),
142 | ) );
143 | $facet_data = $s->get_facet_data();
144 |
145 | $this->assertCount( 3, $facet_data['Post Type']['items'] );
146 | $this->assertEquals( $label, $facet_data['Post Type']['items'][2]['name'] );
147 | $this->assertEquals( 2, $facet_data['Post Type']['items'][2]['count'] );
148 | $this->assertEquals( array( 'post_type' => 'custom-post-type' ), $facet_data['Post Type']['items'][2]['query_vars'] );
149 | }
150 |
151 | function test_tax_query_var() {
152 | $s = new SP_WP_Search( array(
153 | 'post_type' => array( 'post', 'page' ),
154 | 'posts_per_page' => 0,
155 | 'facets' => array(
156 | 'Category' => array( 'type' => 'taxonomy', 'taxonomy' => 'category', 'count' => 10 ),
157 | ),
158 | ) );
159 | $facet_data = $s->get_facet_data();
160 |
161 | $this->assertEquals(
162 | array( array( 'category_name' => 'uncategorized' ), array( 'category_name' => 'cat-a' ), array( 'category_name' => 'cat-b' ), array( 'category_name' => 'cat-c' ) ),
163 | wp_list_pluck( $facet_data['Category']['items'], 'query_vars' )
164 | );
165 | }
166 |
167 | function test_histograms() {
168 | $s = new SP_WP_Search( array(
169 | 'post_type' => array( 'post', 'page' ),
170 | 'posts_per_page' => 0,
171 | 'facets' => array(
172 | 'Year' => array( 'type' => 'date_histogram', 'interval' => 'year', 'count' => 10 ),
173 | 'Month' => array( 'type' => 'date_histogram', 'interval' => 'month', 'count' => 10 ),
174 | 'Day' => array( 'type' => 'date_histogram', 'interval' => 'day', 'field' => 'post_modified', 'count' => 10 ),
175 | ),
176 | ) );
177 | $facet_data = $s->get_facet_data();
178 |
179 | $this->assertEquals( '2007', $facet_data['Year']['items'][0]['name'] );
180 | $this->assertEquals( '2008', $facet_data['Year']['items'][1]['name'] );
181 | $this->assertEquals( '2009', $facet_data['Year']['items'][2]['name'] );
182 | $this->assertEquals( '2010', $facet_data['Year']['items'][3]['name'] );
183 | $this->assertEquals( 10, $facet_data['Year']['items'][0]['count'] );
184 | $this->assertEquals( 1, $facet_data['Year']['items'][1]['count'] );
185 | $this->assertEquals( 13, $facet_data['Year']['items'][2]['count'] );
186 | $this->assertEquals( 13, $facet_data['Year']['items'][3]['count'] );
187 | $this->assertEquals( array( 'year' => '2007' ), $facet_data['Year']['items'][0]['query_vars'] );
188 | $this->assertEquals( array( 'year' => '2008' ), $facet_data['Year']['items'][1]['query_vars'] );
189 | $this->assertEquals( array( 'year' => '2009' ), $facet_data['Year']['items'][2]['query_vars'] );
190 | $this->assertEquals( array( 'year' => '2010' ), $facet_data['Year']['items'][3]['query_vars'] );
191 |
192 | $this->assertEquals( 'January 2007', $facet_data['Month']['items'][0]['name'] );
193 | $this->assertEquals( 10, $facet_data['Month']['items'][0]['count'] );
194 | $this->assertEquals( array( 'year' => '2007', 'monthnum' => 1 ), $facet_data['Month']['items'][0]['query_vars'] );
195 |
196 | $this->assertEquals( 'January 1, 2007', $facet_data['Day']['items'][0]['name'] );
197 | $this->assertEquals( 7, $facet_data['Day']['items'][0]['count'] );
198 | $this->assertEquals( array( 'year' => '2007', 'monthnum' => 1, 'day' => 1 ), $facet_data['Day']['items'][0]['query_vars'] );
199 | }
200 |
201 | function test_author_facets() {
202 | $s = new SP_WP_Search( array(
203 | 'post_type' => array( 'post', 'page' ),
204 | 'posts_per_page' => 0,
205 | 'facets' => array(
206 | 'Author' => array( 'type' => 'author', 'count' => 10 ),
207 | ),
208 | ) );
209 | $facet_data = $s->get_facet_data();
210 |
211 | $this->assertEquals( 'author_a', $facet_data['Author']['items'][0]['name'] );
212 | $this->assertEquals( 2, $facet_data['Author']['items'][0]['count'] );
213 |
214 | $this->assertEquals( 'author_b', $facet_data['Author']['items'][1]['name'] );
215 | $this->assertEquals( 1, $facet_data['Author']['items'][1]['count'] );
216 | }
217 |
218 | function test_facet_by_taxonomy() {
219 | // Fake a taxonomy query to WP_Query so the query vars are set properly.
220 | global $wp_query;
221 | $wp_query->parse_query(
222 | [
223 | 'post_type' => ['post', 'page'],
224 | 'tax_query' => [
225 | [
226 | 'taxonomy' => 'category',
227 | 'field' => 'slug',
228 | 'terms' => ['cat-a'],
229 | ],
230 | [
231 | 'taxonomy' => 'category',
232 | 'field' => 'slug',
233 | 'terms' => ['cat-b'],
234 | ],
235 | [
236 | 'taxonomy' => 'category',
237 | 'field' => 'slug',
238 | 'terms' => ['cat-c'],
239 | ],
240 | ],
241 | ]
242 | );
243 |
244 | $s = new SP_WP_Search( array(
245 | 'post_type' => array( 'post', 'page' ),
246 | 'posts_per_page' => 0,
247 | 'facets' => array(
248 | 'Category' => array( 'type' => 'taxonomy', 'taxonomy' => 'category', 'count' => 10 ),
249 | ),
250 | ) );
251 | $facet_data = $s->get_facet_data(
252 | [
253 | 'exclude_current' => false,
254 | 'join_existing_terms' => false,
255 | ]
256 | );
257 |
258 | $this->assertEquals(
259 | array( false, true, true, true ),
260 | wp_list_pluck( $facet_data['Category']['items'], 'selected' )
261 | );
262 | }
263 | }
264 |
--------------------------------------------------------------------------------
/tests/test-functions.php:
--------------------------------------------------------------------------------
1 | array( 'parent' => array( 'child' => 1 ) ) ),
17 | array( 'grand', 'parent', 'child' ),
18 | 1,
19 | ),
20 | array(
21 | array( 'grand' => array( 'parent' => array( 'child' => 1 ) ) ),
22 | array( 'grand', 'parent' ),
23 | array( 'child' => 1 ),
24 | ),
25 | array(
26 | array( 'grand' => array( 'parent' => array( 'child' => 1 ) ) ),
27 | array( 'grand' ),
28 | array( 'parent' => array( 'child' => 1 ) ),
29 | ),
30 | array(
31 | array( 'grand' => array( 'parent' => array( 'child' => 1 ) ) ),
32 | array(),
33 | array( 'grand' => array( 'parent' => array( 'child' => 1 ) ) ),
34 | ),
35 |
36 | // Introduce numeric arrays
37 | array(
38 | array( 'child' => array( 7, 8, 9 ) ),
39 | array( 'child' ),
40 | array( 7, 8, 9 )
41 | ),
42 | array(
43 | array(
44 | 'parent' => array(
45 | array( 'child' => 7 ),
46 | array( 'child' => 8 ),
47 | array( 'child' => 9 ),
48 | ),
49 | ),
50 | array( 'parent', 'child' ),
51 | array( 7, 8, 9 ),
52 | ),
53 | array(
54 | array(
55 | array( 'parent' => array( 'child' => 7 ) ),
56 | array( 'parent' => array( 'child' => 8 ) ),
57 | array( 'parent' => array( 'child' => 9 ) ),
58 | ),
59 | array( 'parent', 'child' ),
60 | array( 7, 8, 9 ),
61 | ),
62 | );
63 | }
64 |
65 | /**
66 | * @dataProvider data_sp_get_array_value_by_path
67 | *
68 | * @param array $array Array to crawl.
69 | * @param array $path Path to take when crawling.
70 | * @param mixed $expected Expected results.
71 | */
72 | public function test_sp_get_array_value_by_path( $array, $path, $expected ) {
73 | $this->assertSame( $expected, sp_get_array_value_by_path( $array, $path ) );
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/tests/test-general.php:
--------------------------------------------------------------------------------
1 | post->create( array( 'post_title' => 'lorem-ipsum', 'post_date' => '2009-07-01 00:00:00' ) ),
13 | self::factory()->post->create( array( 'post_title' => 'comment-test', 'post_date' => '2009-08-01 00:00:00' ) ),
14 | self::factory()->post->create( array( 'post_title' => 'one-trackback', 'post_date' => '2009-09-01 00:00:00' ) ),
15 | self::factory()->post->create( array( 'post_title' => 'many-trackbacks', 'post_date' => '2009-10-01 00:00:00' ) ),
16 | self::factory()->post->create( array( 'post_title' => 'no-comments', 'post_date' => '2009-10-02 00:00:00' ) ),
17 | self::factory()->post->create( array( 'post_title' => 'one-comment', 'post_date' => '2009-11-01 00:00:00' ) ),
18 | self::factory()->post->create( array( 'post_title' => 'contributor-post-approved', 'post_date' => '2009-12-01 00:00:00' ) ),
19 | self::factory()->post->create( array( 'post_title' => 'embedded-video', 'post_date' => '2010-01-01 00:00:00' ) ),
20 | self::factory()->post->create( array( 'post_title' => 'simple-markup-test', 'post_date' => '2010-02-01 00:00:00' ) ),
21 | self::factory()->post->create( array( 'post_title' => 'raw-html-code', 'post_date' => '2010-03-01 00:00:00' ) ),
22 | )
23 | );
24 | }
25 |
26 | function test_search_activation() {
27 | SP_Config()->update_settings( array( 'active' => false ) );
28 | SP_Integration()->remove_hooks();
29 |
30 | $this->go_to( '/?s=trackback' );
31 | $this->assertEquals( get_query_var( 's' ), 'trackback' );
32 | $this->assertEquals( false, strpos( $GLOBALS['wp_query']->request, 'SearchPress' ) );
33 |
34 | SP_Config()->update_settings( array( 'active' => true ) );
35 | SP_Integration()->init_hooks();
36 | $this->go_to( '/?s=trackback' );
37 | $this->assertEquals( get_query_var( 's' ), 'trackback' );
38 | $this->assertStringContainsString( 'SearchPress', $GLOBALS['wp_query']->request );
39 | }
40 |
41 | function test_settings() {
42 | $host = getenv( 'SEARCHPRESS_HOST' );
43 | if ( empty( $host ) ) {
44 | $host = 'http://localhost:9200';
45 | }
46 | SP_Config()->settings = false;
47 | delete_option( 'sp_settings' );
48 |
49 | $this->assertEquals( 'http://localhost:9200', SP_Config()->host() );
50 | $this->assertTrue( SP_Config()->must_init() );
51 | $this->assertFalse( SP_Config()->active() );
52 |
53 | SP_Config()->update_settings( array( 'active' => true, 'must_init' => false, 'host' => $host ) );
54 | $this->assertEquals( $host, SP_Config()->host() );
55 | $this->assertFalse( SP_Config()->must_init() );
56 | $this->assertTrue( SP_Config()->active() );
57 |
58 | SP_Config()->settings = false;
59 | delete_option( 'sp_settings' );
60 | SP_Config()->update_settings( array( 'active' => true, 'must_init' => false, 'host' => $host ) );
61 | $this->assertEquals( $host, SP_Config()->host() );
62 | $this->assertFalse( SP_Config()->must_init() );
63 | $this->assertTrue( SP_Config()->active() );
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/tests/test-heartbeat.php:
--------------------------------------------------------------------------------
1 | check_beat( true );
10 | $this->assertTrue( $beat_result );
11 | $this->assertTrue( wp_next_scheduled( 'sp_heartbeat' ) > 0 );
12 |
13 | wp_clear_scheduled_hook( 'sp_heartbeat' );
14 | $this->assertFalse( wp_next_scheduled( 'sp_heartbeat' ) );
15 | wp_schedule_single_event( time(), 'sp_heartbeat', array( true ) );
16 |
17 | // Run the cron
18 | $pre_cron = time();
19 | $this->fake_cron();
20 | $cron_time = time() - $pre_cron;
21 |
22 | $this->assertTrue( wp_next_scheduled( 'sp_heartbeat' ) > 0 );
23 |
24 | // The beat should have run and scheduled the next checkup. Here we make
25 | // sure that the next event is scheduled in one heartbeat interval from
26 | // now, while also accounting for however long the cron took.
27 | $this->assertGreaterThanOrEqual(
28 | time() + SP_Heartbeat()->intervals['heartbeat'] - $cron_time,
29 | wp_next_scheduled( 'sp_heartbeat' )
30 | );
31 | }
32 |
33 | function test_heartbeat_reschedules_after_query() {
34 | wp_schedule_single_event( time(), 'sp_heartbeat' );
35 | $next_scheduled = wp_next_scheduled( 'sp_heartbeat' );
36 | SP_Heartbeat()->record_pulse();
37 | $this->assertGreaterThan( $next_scheduled, wp_next_scheduled( 'sp_heartbeat' ) );
38 | }
39 |
40 | function test_heartbeat_increases_frequency_after_error() {
41 | SP_Heartbeat()->record_pulse();
42 | $was_scheduled = wp_next_scheduled( 'sp_heartbeat' );
43 |
44 | // Change the host, re-check the heartbeat, and set the host back
45 | $host = SP_Config()->get_setting( 'host' );
46 | SP_Config()->update_settings( array( 'host' => 'http://asdftestblog1.files.wordpress.com' ) );
47 | $beat_result = SP_Heartbeat()->check_beat( true );
48 | SP_Config()->update_settings( array( 'host' => $host ) );
49 |
50 | $this->assertFalse( $beat_result );
51 | $this->assertLessThan( $was_scheduled, wp_next_scheduled( 'sp_heartbeat' ) );
52 | }
53 |
54 | function test_heartbeat_decreases_frequency_after_error_resolved() {
55 | SP_Heartbeat()->record_pulse();
56 | $was_scheduled = wp_next_scheduled( 'sp_heartbeat' );
57 |
58 | // Change the host, re-check the heartbeat, and set the host back
59 | $host = SP_Config()->get_setting( 'host' );
60 |
61 | SP_Config()->update_settings( array( 'host' => 'http://asdftestblog1.files.wordpress.com' ) );
62 | $beat_result = SP_Heartbeat()->check_beat( true );
63 | SP_Config()->update_settings( array( 'host' => $host ) );
64 |
65 | $this->assertFalse( $beat_result );
66 | $increase_scheduled = wp_next_scheduled( 'sp_heartbeat' );
67 | $this->assertLessThan( $was_scheduled, $increase_scheduled );
68 |
69 | // The heartbeat should be working again, so we'll make sure that
70 | // checking the beat will reschedule the next check normally.
71 | $beat_result = SP_Heartbeat()->check_beat( true );
72 | $this->assertTrue( $beat_result );
73 | $this->assertGreaterThanOrEqual(
74 | $increase_scheduled + SP_Heartbeat()->intervals['heartbeat'] - SP_Heartbeat()->intervals['increase'],
75 | wp_next_scheduled( 'sp_heartbeat' )
76 | );
77 | }
78 |
79 | function test_heartbeat_status_escalation() {
80 | // Change the host, re-check the heartbeat, and set the host back
81 | $host = SP_Config()->get_setting( 'host' );
82 | SP_Config()->update_settings( array( 'host' => 'http://asdftestblog1.files.wordpress.com' ) );
83 |
84 | SP_Heartbeat()->record_pulse( time() - SP_Heartbeat()->thresholds['alert'], time() );
85 | $alert_status = SP_Heartbeat()->get_status();
86 | $has_pulse_alert = SP_Heartbeat()->has_pulse();
87 |
88 | SP_Heartbeat()->record_pulse( time() - SP_Heartbeat()->thresholds['shutdown'], time() );
89 | $shutdown_status = SP_Heartbeat()->get_status();
90 | $has_pulse_shutdown = SP_Heartbeat()->has_pulse();
91 |
92 | SP_Config()->update_settings( array( 'host' => $host ) );
93 |
94 | $beat_result = SP_Heartbeat()->check_beat( true );
95 | SP_Heartbeat()->get_last_beat( true );
96 | $ok_status = SP_Heartbeat()->get_status();
97 | $has_pulse_ok = SP_Heartbeat()->has_pulse();
98 |
99 | $this->assertEquals( 'alert', $alert_status );
100 | $this->assertTrue( $has_pulse_alert );
101 |
102 | $this->assertEquals( 'shutdown', $shutdown_status );
103 | $this->assertFalse( $has_pulse_shutdown );
104 |
105 | $this->assertEquals( 'ok', $ok_status );
106 | $this->assertTrue( $has_pulse_ok );
107 |
108 | $this->assertTrue( $beat_result );
109 | }
110 |
111 | function test_searchpress_readiness() {
112 | $host = SP_Config()->get_setting( 'host' );
113 | SP_Config()->update_settings( array( 'host' => 'http://asdftestblog1.files.wordpress.com' ) );
114 |
115 | SP_Heartbeat()->record_pulse( time() - SP_Heartbeat()->thresholds['shutdown'], time() );
116 | $shutdown_ready = apply_filters( 'sp_ready', null );
117 |
118 | SP_Config()->update_settings( array( 'host' => $host ) );
119 |
120 | SP_Heartbeat()->check_beat( true );
121 | SP_Heartbeat()->get_last_beat( true );
122 | $ok_ready = apply_filters( 'sp_ready', null );
123 |
124 | $this->assertFalse( $shutdown_ready );
125 | $this->assertTrue( $ok_ready );
126 | }
127 |
128 | public function test_ready_override() {
129 | $this->assertTrue( SP_Heartbeat()->is_ready( null ) );
130 | $this->assertFalse( SP_Heartbeat()->is_ready( false ) );
131 | }
132 |
133 | public function test_heartbeat_migration() {
134 | // Heartbeat option was originally just a timestamp.
135 | update_option( 'sp_heartbeat', 1700000000 );
136 | $last_beat = SP_Heartbeat()->get_last_beat( true );
137 |
138 | $this->assertIsArray( $last_beat );
139 | $this->assertSame( $last_beat, get_option( 'sp_heartbeat' ) );
140 | $this->assertSame( 1700000000, $last_beat['queried'] );
141 | $this->assertSame( 1700000000, $last_beat['verified'] );
142 | }
143 |
144 | /**
145 | * @group stale-heartbeat
146 | */
147 | public function test_stale_heartbeat_rechecks_in_non_admin_screen() {
148 | wp_set_current_user( $this->factory->user->create( [ 'role' => 'administrator' ] ) );
149 | $this->assertFalse( is_admin() );
150 |
151 | $time = time() - SP_Heartbeat()->thresholds['shutdown'] - 10;
152 | SP_Heartbeat()->record_pulse( $time, $time );
153 | $this->assertSame( 'stale', SP_Heartbeat()->get_status() );
154 |
155 | SP_Heartbeat()->beat_result = null;
156 | $this->assertTrue( apply_filters( 'sp_ready', null ) );
157 | $this->assertSame( 'stale', SP_Heartbeat()->get_status() );
158 | }
159 | }
160 |
--------------------------------------------------------------------------------
/tests/test-integration.php:
--------------------------------------------------------------------------------
1 | term->create( array( 'taxonomy' => 'category', 'name' => 'cat-demo' ) );
14 | $tag = self::factory()->term->create( array( 'taxonomy' => 'post_tag', 'name' => 'tag-demo' ) );
15 |
16 | register_post_type( 'cpt', array( 'public' => true ) );
17 | SP_Config()->post_types = null;
18 | sp_searchable_post_types( true );
19 |
20 | self::index(
21 | array(
22 | self::factory()->post->create( array( 'post_title' => 'lorem-ipsum', 'post_date' => '2009-07-01 00:00:00', 'post_category' => array( $cat ) ) ),
23 | self::factory()->post->create( array( 'post_title' => 'comment-test', 'post_date' => '2009-08-01 00:00:00', 'post_category' => array( $cat ) ) ),
24 | self::factory()->post->create( array( 'post_title' => 'one-trackback', 'post_date' => '2009-09-01 00:00:00', 'post_category' => array( $cat ) ) ),
25 | self::factory()->post->create( array( 'post_title' => 'many-trackbacks', 'post_date' => '2009-10-01 00:00:00', 'post_category' => array( $cat ) ) ),
26 | self::factory()->post->create( array( 'post_title' => 'no-comments', 'post_date' => '2009-10-02 00:00:00' ) ),
27 |
28 | self::factory()->post->create( array( 'post_title' => 'one-comment', 'post_date' => '2009-11-01 00:00:00', 'tags_input' => array( $tag ) ) ),
29 | self::factory()->post->create( array( 'post_title' => 'contributor-post-approved', 'post_date' => '2009-12-01 00:00:00' ) ),
30 | self::factory()->post->create( array( 'post_title' => 'many-comments', 'post_date' => '2010-01-01 00:00:00' ) ),
31 | self::factory()->post->create( array( 'post_title' => 'simple-markup-test', 'post_date' => '2010-02-01 00:00:00', 'tags_input' => array( $tag ) ) ),
32 | self::factory()->post->create( array( 'post_title' => 'raw-html-code', 'post_date' => '2010-03-01 00:00:00', 'tags_input' => array( $tag ) ) ),
33 |
34 | self::factory()->post->create( array( 'post_title' => 'cpt', 'post_date' => '2010-01-01 00:00:00', 'post_type' => 'cpt' ) ),
35 | self::factory()->post->create( array( 'post_title' => 'lorem-cpt', 'post_date' => '2010-01-01 00:00:00', 'post_type' => 'cpt' ) ),
36 | )
37 | );
38 | }
39 |
40 | public function setUp(): void {
41 | parent::setUp();
42 |
43 | register_post_type( 'cpt', array( 'public' => true ) );
44 | }
45 |
46 | function test_search_auto_integration() {
47 | $this->go_to( '/?s=trackback&orderby=date' );
48 | $this->assertEquals( get_query_var( 's' ), 'trackback' );
49 | $this->assertTrue( is_search() );
50 |
51 | $this->assertStringContainsString( 'SearchPress', $GLOBALS['wp_query']->request );
52 | $this->assertEquals(
53 | array(
54 | 'many-trackbacks',
55 | 'one-trackback',
56 | ),
57 | wp_list_pluck( $GLOBALS['wp_query']->posts, 'post_name' )
58 | );
59 | }
60 |
61 | function test_sp_query_arg() {
62 | $this->go_to( '/?sp[force]=1' );
63 |
64 | $this->assertEquals( get_query_var( 'sp' ), array( 'force' => '1' ) );
65 | $this->assertTrue( is_search() );
66 | $this->assertFalse( is_home() );
67 | }
68 |
69 | function test_no_results() {
70 | $this->go_to( '/?s=cucumbers' );
71 | $this->assertEquals( get_query_var( 's' ), 'cucumbers' );
72 | $this->assertTrue( is_search() );
73 | $this->assertStringContainsString( 'SearchPress', $GLOBALS['wp_query']->request );
74 | $this->assertEquals( 0, $GLOBALS['wp_query']->found_posts );
75 | }
76 |
77 | function test_date_results() {
78 | $this->go_to( '/?s=test&year=2010' );
79 | $this->assertEquals( get_query_var( 'year' ), '2010' );
80 | $this->assertEmpty( get_query_var( 'monthnum' ) );
81 | $this->assertEmpty( get_query_var( 'day' ) );
82 | $this->assertStringContainsString( 'SearchPress', $GLOBALS['wp_query']->request );
83 | $this->assertEquals(
84 | array( 'simple-markup-test' ),
85 | wp_list_pluck( $GLOBALS['wp_query']->posts, 'post_name' )
86 | );
87 |
88 | $this->go_to( '/?s=test&year=2009&monthnum=8' );
89 | $this->assertEquals( get_query_var( 'year' ), '2009' );
90 | $this->assertEquals( get_query_var( 'monthnum' ), '8' );
91 | $this->assertEmpty( get_query_var( 'day' ) );
92 | $this->assertStringContainsString( 'SearchPress', $GLOBALS['wp_query']->request );
93 | $this->assertEquals(
94 | array( 'comment-test' ),
95 | wp_list_pluck( $GLOBALS['wp_query']->posts, 'post_name' )
96 | );
97 |
98 | $this->go_to( '/?s=comment&year=2009&monthnum=11&day=1' );
99 | $this->assertEquals( get_query_var( 'year' ), '2009' );
100 | $this->assertEquals( get_query_var( 'monthnum' ), '11' );
101 | $this->assertEquals( get_query_var( 'day' ), '1' );
102 | $this->assertStringContainsString( 'SearchPress', $GLOBALS['wp_query']->request );
103 | $this->assertEquals(
104 | array( 'one-comment' ),
105 | wp_list_pluck( $GLOBALS['wp_query']->posts, 'post_name' )
106 | );
107 | }
108 |
109 | function test_sp_date_range() {
110 | $this->go_to( '/?s=comment&sp[f]=2009-10-14&sp[t]=2009-12-31' );
111 | $this->assertStringContainsString( 'SearchPress', $GLOBALS['wp_query']->request );
112 | $this->assertEquals(
113 | array( 'one-comment' ),
114 | wp_list_pluck( $GLOBALS['wp_query']->posts, 'post_name' )
115 | );
116 | }
117 |
118 | function test_terms() {
119 | $this->go_to( '/?s=comment&category_name=cat-demo' );
120 | $this->assertEquals( get_query_var( 'category_name' ), 'cat-demo' );
121 | $this->assertStringContainsString( 'SearchPress', $GLOBALS['wp_query']->request );
122 | $this->assertEquals(
123 | array( 'comment-test' ),
124 | wp_list_pluck( $GLOBALS['wp_query']->posts, 'post_name' )
125 | );
126 |
127 | $this->go_to( '/?s=comment&tag=tag-demo' );
128 | $this->assertEquals( get_query_var( 'tag' ), 'tag-demo' );
129 | $this->assertStringContainsString( 'SearchPress', $GLOBALS['wp_query']->request );
130 | $this->assertEquals(
131 | array( 'one-comment' ),
132 | wp_list_pluck( $GLOBALS['wp_query']->posts, 'post_name' )
133 | );
134 | }
135 |
136 | function test_post_types() {
137 | $this->go_to( '/?s=lorem&post_type=cpt' );
138 | $this->assertEquals( get_query_var( 'post_type' ), 'cpt' );
139 | $this->assertStringContainsString( 'SearchPress', $GLOBALS['wp_query']->request );
140 | $this->assertEquals(
141 | array( 'lorem-cpt' ),
142 | wp_list_pluck( $GLOBALS['wp_query']->posts, 'post_name' )
143 | );
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/tests/test-mapping-postmeta.php:
--------------------------------------------------------------------------------
1 | post->create();
15 | }
16 |
17 | public function setUp(): void {
18 | parent::setUp();
19 |
20 | add_filter(
21 | 'sp_post_allowed_meta',
22 | function() {
23 | return array(
24 | 'mapping_postmeta_test' => array( 'value', 'boolean', 'long', 'double', 'date', 'datetime', 'time' ),
25 | 'long_string_test' => array( 'value' ),
26 | );
27 | }
28 | );
29 | }
30 |
31 | public function tearDown(): void {
32 | delete_post_meta( self::$demo_post_id, 'mapping_postmeta_test' );
33 | delete_post_meta( self::$demo_post_id, 'long_string_test' );
34 |
35 | parent::tearDown();
36 | }
37 |
38 | function meta_sample_data() {
39 | // $value, $boolean, $long, $double, $datetime
40 | return array(
41 | array( 'mnducnrvnfh', true, null, null, null ), // Randomish string.
42 | array( 'To be or not to be', true, null, null, null ), // Only stopwords.
43 | array( 1, true, 1, 1, '1970-01-01 00:00:01' ),
44 | array( -123, true, -123, -123, '1969-12-31 23:57:57' ),
45 | array( 0, false, 0, 0, '1970-01-01 00:00:00' ),
46 | array( '1', true, 1, 1, '1970-01-01 00:00:01' ),
47 | array( '0', false, 0, 0, '1970-01-01 00:00:00' ),
48 | array( 1.1, true, 1, 1.1, null ),
49 | array( -1.1, true, -1, -1.1, null ),
50 | array( 0.0, false, 0, 0, '1970-01-01 00:00:00' ),
51 | array( 0.01, true, 0, 0.01, null ),
52 | array( 0.9999999, true, 0, 0.9999999, null ),
53 | array( '', false, null, null, null ),
54 | array( null, false, null, null, null ),
55 | array( array( 'foo' => array( 'bar' => array( 'bat' => true ) ) ), true, null, null, null ),
56 | array( '2015-01-01', true, null, null, '2015-01-01 00:00:00' ),
57 | array( '1/2/2015', true, null, null, '2015-01-02 00:00:00' ),
58 | array( 'Jan 3rd 2030', true, null, null, '2030-01-03 00:00:00' ),
59 | array( 1442600000, true, 1442600000, 1442600000, '2015-09-18 18:13:20' ),
60 | array( 1234567, true, 1234567, 1234567, '1970-01-15 06:56:07' ),
61 | array( '1442600000', true, 1442600000, 1442600000, '2015-09-18 18:13:20' ),
62 | array( 1442600000.0001, true, 1442600000, 1442600000.0001, null ),
63 | array( '2015-01-04T15:19:21-05:00', true, null, null, '2015-01-04 20:19:21' ), // Note the timezone.
64 | array( '18:13:20', true, null, null, gmdate( 'Y-m-d' ) . ' 18:13:20' ),
65 | array( '14e7647469', true, null, null, null ), // Hash that is technically a (huge) number.
66 | );
67 | }
68 |
69 | /**
70 | * @dataProvider meta_sample_data
71 | */
72 | function test_mapping_post_meta( $value, $boolean, $long, $double, $datetime ) {
73 | update_post_meta( self::$demo_post_id, 'mapping_postmeta_test', $value );
74 | self::index( self::$demo_post_id );
75 |
76 | if ( null === $value ) {
77 | $string = array( null );
78 | } elseif ( is_array( $value ) ) {
79 | $string = array( serialize( $value ) );
80 | } else {
81 | $string = array( strval( $value ) );
82 | }
83 |
84 | // Test the various meta mappings. Ideally, these each would be their
85 | // own test, but this is considerably faster.
86 | $this->assertSame( $string, $this->search_and_get_field( array(), 'post_meta.mapping_postmeta_test.value' ), 'Checking meta.value' );
87 | $this->assertSame( $string, $this->search_and_get_field( array(), 'post_meta.mapping_postmeta_test.raw' ), 'Checking meta.raw' );
88 | $this->assertSame( array( $boolean ), $this->search_and_get_field( array(), 'post_meta.mapping_postmeta_test.boolean' ), 'Checking meta.boolean' );
89 |
90 | if ( isset( $long ) ) {
91 | $this->assertSame( array( $long ), $this->search_and_get_field( array(), 'post_meta.mapping_postmeta_test.long' ), 'Checking meta.long' );
92 | } else {
93 | $this->assertSame( array(), $this->search_and_get_field( array(), 'post_meta.mapping_postmeta_test.long' ), 'Checking that meta.long is missing' );
94 | }
95 |
96 | if ( isset( $double ) ) {
97 | $this->assertSame( array( $double ), $this->search_and_get_field( array(), 'post_meta.mapping_postmeta_test.double' ), 'Checking meta.double' );
98 | } else {
99 | $this->assertSame( array(), $this->search_and_get_field( array(), 'post_meta.mapping_postmeta_test.double' ), 'Checking that meta.double is missing' );
100 | }
101 |
102 | if ( isset( $datetime ) ) {
103 | list( $date, $time ) = explode( ' ', $datetime );
104 | $this->assertSame( array( $datetime ), $this->search_and_get_field( array(), 'post_meta.mapping_postmeta_test.datetime' ), 'Checking meta.datetime' );
105 | } else {
106 | $this->assertSame( array(), $this->search_and_get_field( array(), 'post_meta.mapping_postmeta_test.datetime' ), 'Checking that meta.datetime is missing' );
107 | }
108 | }
109 |
110 | public function long_string_data() {
111 | return array(
112 | // $string, $should_truncate_indexed, $should_truncate_raw
113 | array( str_repeat( 'a', 1000 ), false, false ),
114 | array( str_repeat( 'a', 50000 ), true, true ),
115 | array( trim( str_repeat( 'test ', 200 ) ), false, false ),
116 | array( trim( str_repeat( 'test ', 10000 ) ), false, true ),
117 | );
118 | }
119 |
120 | /**
121 | * @dataProvider long_string_data
122 | */
123 | public function test_long_strings( $string, $should_truncate_indexed, $should_truncate_raw ) {
124 | self::factory()->post->update_object( self::$demo_post_id, array( 'post_content' => $string ) );
125 | update_post_meta( self::$demo_post_id, 'long_string_test', $string );
126 | self::index( self::$demo_post_id );
127 |
128 | // These fields are not analyzed
129 | if ( $should_truncate_raw ) {
130 | $meta_raw = $this->search_and_get_field( array(), 'post_meta.long_string_test.raw' );
131 | $this->assertNotSame( array( $string ), $meta_raw, 'Checking meta.raw' );
132 | $this->assertStringContainsString( $meta_raw[0], $string );
133 | } else {
134 | $this->assertSame( array( $string ), $this->search_and_get_field( array(), 'post_meta.long_string_test.raw' ), 'Checking meta.raw' );
135 | }
136 |
137 | // These fields are analyzed
138 | if ( $should_truncate_indexed ) {
139 | $this->assertNotSame( array( $string ), $this->search_and_get_field( array(), 'post_content' ), 'Checking post_content' );
140 | $this->assertNotSame( array( $string ), $this->search_and_get_field( array(), 'post_meta.long_string_test.value' ), 'Checking meta.value' );
141 | } else {
142 | $this->assertSame( array( $string ), $this->search_and_get_field( array(), 'post_content' ), 'Checking post_content' );
143 | $this->assertSame( array( $string ), $this->search_and_get_field( array(), 'post_meta.long_string_test.value' ), 'Checking meta.value' );
144 | }
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/tests/test-mapping.php:
--------------------------------------------------------------------------------
1 | array( 'value' ),
64 | 'test_long' => array( 'long' ),
65 | 'test_double' => array( 'double' ),
66 | 'test_boolean_true' => array( 'boolean' ),
67 | 'test_boolean_false' => array( 'boolean' ),
68 | 'test_date' => array( 'date', 'datetime', 'time' ),
69 | );
70 | }
71 | );
72 |
73 | self::$demo_user = array(
74 | 'user_login' => 'author1',
75 | 'user_nicename' => 'author-nicename',
76 | 'user_pass' => rand_str(),
77 | 'role' => 'author',
78 | 'display_name' => 'Michael Scott',
79 | );
80 | self::$demo_user_id = self::factory()->user->create( self::$demo_user );
81 |
82 | self::$demo_term = array(
83 | 'taxonomy' => 'category',
84 | 'name' => 'cat-a',
85 | 'slug' => 'cat-a',
86 | );
87 | self::$demo_term_id = self::factory()->term->create( self::$demo_term );
88 |
89 | self::$demo_dates = array(
90 | 'post_date' => array( 'date' => '2013-02-28 01:23:45' ),
91 | 'post_date_gmt' => array( 'date' => '2013-02-28 05:23:45' ),
92 | 'post_modified' => array( 'date' => '2013-02-28 01:23:45' ),
93 | 'post_modified_gmt' => array( 'date' => '2013-02-28 05:23:45' ),
94 | );
95 | foreach ( self::$demo_dates as &$date ) {
96 | $ts = strtotime( $date['date'] );
97 | $date = array(
98 | 'date' => strval( $date['date'] ),
99 | 'year' => intval( gmdate( 'Y', $ts ) ),
100 | 'month' => intval( gmdate( 'm', $ts ) ),
101 | 'day' => intval( gmdate( 'd', $ts ) ),
102 | 'hour' => intval( gmdate( 'H', $ts ) ),
103 | 'minute' => intval( gmdate( 'i', $ts ) ),
104 | 'second' => intval( gmdate( 's', $ts ) ),
105 | 'week' => intval( gmdate( 'W', $ts ) ),
106 | 'day_of_week' => intval( gmdate( 'N', $ts ) ),
107 | 'day_of_year' => intval( gmdate( 'z', $ts ) ),
108 | 'seconds_from_day' => intval( mktime( gmdate( 'H', $ts ), gmdate( 'i', $ts ), gmdate( 's', $ts ), 1, 1, 1970 ) ),
109 | 'seconds_from_hour' => intval( mktime( 0, gmdate( 'i', $ts ), gmdate( 's', $ts ), 1, 1, 1970 ) ),
110 | );
111 | }
112 |
113 | self::$demo_post = array(
114 | 'post_author' => self::$demo_user_id,
115 | 'post_date' => self::$demo_dates['post_date']['date'],
116 | 'post_date_gmt' => self::$demo_dates['post_date_gmt']['date'],
117 | 'post_content' => 'Welcome to Local WordPress Dev Sites. This is your first post. Edit or delete it, then start blogging!',
118 | 'post_title' => 'Hello world!',
119 | 'post_excerpt' => 'Lorem ipsum dolor sit amet',
120 | 'post_status' => 'publish',
121 | 'post_password' => 'foobar',
122 | 'post_name' => 'hello-world',
123 | 'post_parent' => 123,
124 | 'menu_order' => 456,
125 | 'post_type' => 'post',
126 | 'post_mime_type' => 'image/jpeg',
127 | 'post_category' => array( self::$demo_term_id ),
128 | );
129 | self::$demo_post_id = self::factory()->post->create( self::$demo_post );
130 | add_post_meta( self::$demo_post_id, 'test_string', 'foo' );
131 | add_post_meta( self::$demo_post_id, 'test_long', '123' );
132 | add_post_meta( self::$demo_post_id, 'test_double', '123.456' );
133 | add_post_meta( self::$demo_post_id, 'test_boolean_true', 'true' );
134 | add_post_meta( self::$demo_post_id, 'test_boolean_false', 'false' );
135 | add_post_meta( self::$demo_post_id, 'test_date', '2012-03-14 03:14:15' );
136 | self::index( self::$demo_post_id );
137 | }
138 |
139 | function _field_mapping_test( $field ) {
140 | $this->assertSame(
141 | array( self::$demo_post[ $field ] ),
142 | $this->search_and_get_field( array(), $field )
143 | );
144 | }
145 |
146 | function _date_field_mapping_test( $field ) {
147 | $this->assertSame(
148 | array( self::$demo_dates[ $field ]['date'] ),
149 | $this->search_and_get_field( array(), $field . '.date' )
150 | );
151 |
152 | $this->assertSame(
153 | array( self::$demo_dates[ $field ]['year'] ),
154 | $this->search_and_get_field( array(), $field . '.year' )
155 | );
156 |
157 | $this->assertSame(
158 | array( self::$demo_dates[ $field ]['month'] ),
159 | $this->search_and_get_field( array(), $field . '.month' )
160 | );
161 |
162 | $this->assertSame(
163 | array( self::$demo_dates[ $field ]['day'] ),
164 | $this->search_and_get_field( array(), $field . '.day' )
165 | );
166 |
167 | $this->assertSame(
168 | array( self::$demo_dates[ $field ]['hour'] ),
169 | $this->search_and_get_field( array(), $field . '.hour' )
170 | );
171 |
172 | $this->assertSame(
173 | array( self::$demo_dates[ $field ]['minute'] ),
174 | $this->search_and_get_field( array(), $field . '.minute' )
175 | );
176 |
177 | $this->assertSame(
178 | array( self::$demo_dates[ $field ]['second'] ),
179 | $this->search_and_get_field( array(), $field . '.second' )
180 | );
181 |
182 | $this->assertSame(
183 | array( self::$demo_dates[ $field ]['week'] ),
184 | $this->search_and_get_field( array(), $field . '.week' )
185 | );
186 |
187 | $this->assertSame(
188 | array( self::$demo_dates[ $field ]['day_of_week'] ),
189 | $this->search_and_get_field( array(), $field . '.day_of_week' )
190 | );
191 |
192 | $this->assertSame(
193 | array( self::$demo_dates[ $field ]['day_of_year'] ),
194 | $this->search_and_get_field( array(), $field . '.day_of_year' )
195 | );
196 |
197 | $this->assertSame(
198 | array( self::$demo_dates[ $field ]['seconds_from_day'] ),
199 | $this->search_and_get_field( array(), $field . '.seconds_from_day' )
200 | );
201 |
202 | $this->assertSame(
203 | array( self::$demo_dates[ $field ]['seconds_from_hour'] ),
204 | $this->search_and_get_field( array(), $field . '.seconds_from_hour' )
205 | );
206 | }
207 |
208 |
209 | function test_mapping_field_post_content() {
210 | $this->_field_mapping_test( 'post_content' );
211 | }
212 |
213 | function test_mapping_field_post_excerpt() {
214 | $this->_field_mapping_test( 'post_excerpt' );
215 | }
216 |
217 | function test_mapping_field_post_mime_type() {
218 | $this->_field_mapping_test( 'post_mime_type' );
219 | }
220 |
221 | function test_mapping_field_post_name() {
222 | $this->_field_mapping_test( 'post_name' );
223 | }
224 |
225 | function test_mapping_field_post_parent() {
226 | $this->_field_mapping_test( 'post_parent' );
227 | }
228 |
229 | function test_mapping_field_post_password() {
230 | $this->_field_mapping_test( 'post_password' );
231 | }
232 |
233 | function test_mapping_field_menu_order() {
234 | $this->_field_mapping_test( 'menu_order' );
235 | }
236 |
237 | function test_mapping_field_post_status() {
238 | $this->_field_mapping_test( 'post_status' );
239 | }
240 |
241 | function test_mapping_field_post_title() {
242 | $this->_field_mapping_test( 'post_title' );
243 | }
244 |
245 | function test_mapping_field_post_type() {
246 | $this->_field_mapping_test( 'post_type' );
247 | }
248 |
249 |
250 | function test_mapping_field_post_date() {
251 | $this->_date_field_mapping_test( 'post_date' );
252 | }
253 |
254 | function test_mapping_field_post_date_gmt() {
255 | $this->_date_field_mapping_test( 'post_date_gmt' );
256 | }
257 |
258 | function test_mapping_field_post_modified() {
259 | $this->_date_field_mapping_test( 'post_modified' );
260 | }
261 |
262 | function test_mapping_field_post_modified_gmt() {
263 | $this->_date_field_mapping_test( 'post_modified_gmt' );
264 | }
265 |
266 |
267 | function test_mapped_field_permalink() {
268 | $this->assertSame(
269 | array( get_permalink( self::$demo_post_id ) ),
270 | $this->search_and_get_field( array(), 'permalink' )
271 | );
272 | }
273 |
274 | function test_mapped_field_post_id() {
275 | $this->assertSame(
276 | array( self::$demo_post_id ),
277 | $this->search_and_get_field( array(), 'post_id' )
278 | );
279 | }
280 |
281 | function test_mapping_field_post_author() {
282 | $this->assertSame(
283 | array( self::$demo_user_id ),
284 | $this->search_and_get_field( array(), 'post_author.user_id' )
285 | );
286 |
287 | $this->assertSame(
288 | array( self::$demo_user['user_login'] ),
289 | $this->search_and_get_field( array(), 'post_author.login' )
290 | );
291 |
292 | $this->assertSame(
293 | array( self::$demo_user['display_name'] ),
294 | $this->search_and_get_field( array(), 'post_author.display_name' )
295 | );
296 |
297 | $this->assertSame(
298 | array( self::$demo_user['user_nicename'] ),
299 | $this->search_and_get_field( array(), 'post_author.user_nicename' )
300 | );
301 | }
302 |
303 |
304 | function test_mapping_field_post_meta() {
305 | $this->assertSame(
306 | array( 'foo' ),
307 | $this->search_and_get_field( array(), 'post_meta.test_string.raw' )
308 | );
309 |
310 | $this->assertSame(
311 | array( 123 ),
312 | $this->search_and_get_field( array(), 'post_meta.test_long.long' )
313 | );
314 |
315 | $this->assertSame(
316 | array( 123.456 ),
317 | $this->search_and_get_field( array(), 'post_meta.test_double.double' )
318 | );
319 |
320 | $this->assertSame(
321 | array( true ),
322 | $this->search_and_get_field( array(), 'post_meta.test_boolean_true.boolean' )
323 | );
324 |
325 | $this->assertSame(
326 | array( false ),
327 | $this->search_and_get_field( array(), 'post_meta.test_boolean_false.boolean' )
328 | );
329 |
330 | $this->assertSame(
331 | array( '2012-03-14 03:14:15' ),
332 | $this->search_and_get_field( array(), 'post_meta.test_date.datetime' )
333 | );
334 | }
335 |
336 | function test_mapping_field_terms() {
337 | $this->assertSame(
338 | array( self::$demo_term['name'] ),
339 | $this->search_and_get_field( array(), 'terms.category.name' )
340 | );
341 |
342 | $this->assertSame(
343 | array( self::$demo_term['slug'] ),
344 | $this->search_and_get_field( array(), 'terms.category.slug' )
345 | );
346 |
347 | $this->assertSame(
348 | array( 0 ),
349 | $this->search_and_get_field( array(), 'terms.category.parent' )
350 | );
351 |
352 | $this->assertSame(
353 | array( self::$demo_term_id ),
354 | $this->search_and_get_field( array(), 'terms.category.term_id' )
355 | );
356 | }
357 |
358 | }
359 |
--------------------------------------------------------------------------------
/tests/test-post.php:
--------------------------------------------------------------------------------
1 | [ 'value' ],
17 | '_test_key_2' => [ 'long', 'double' ],
18 | '_test_key_3' => [ 'date', 'datetime' ], // Invalid.
19 | );
20 | }
21 | );
22 |
23 | $cat_a = self::factory()->term->create( array( 'taxonomy' => 'category', 'name' => 'cat-a' ) );
24 | $cat_b = self::factory()->term->create( array( 'taxonomy' => 'category', 'name' => 'cat-b' ) );
25 |
26 | $post_id = self::factory()->post->create( array(
27 | 'post_title' => 'lorem-ipsum',
28 | 'post_date' => '2009-07-01 00:00:00',
29 | 'tags_input' => array( 'tag-a', 'tag-b' ),
30 | 'post_category' => array( $cat_a, $cat_b ),
31 | ) );
32 | update_post_meta( $post_id, '_test_key_1', 'test meta string' );
33 | update_post_meta( $post_id, '_test_key_2', 721.8 );
34 | update_post_meta( $post_id, '_test_key_3', array( 'foo' => array( 'bar' => array( 'bat' => true ) ) ) );
35 |
36 | $post = get_post( $post_id );
37 | self::$sp_post = new SP_Post( $post );
38 | }
39 |
40 | function test_getting_attributes() {
41 | $this->assertEquals( 'lorem-ipsum', self::$sp_post->post_name );
42 | $meta = self::$sp_post->post_meta;
43 | $this->assertCount( 1, $meta['_test_key_1'] );
44 | $this->assertCount( 2, $meta['_test_key_1'][0] );
45 | $this->assertCount( 1, $meta['_test_key_2'] );
46 | $this->assertCount( 3, $meta['_test_key_2'][0] );
47 | $this->assertCount( 1, $meta['_test_key_3'] );
48 | $this->assertCount( 1, $meta['_test_key_3'][0] );
49 | $this->assertEquals( 'test meta string', $meta['_test_key_1'][0]['value'] );
50 | $this->assertEquals( 721, $meta['_test_key_2'][0]['long'] );
51 | $this->assertEquals( 721.8, $meta['_test_key_2'][0]['double'] );
52 | $this->assertEquals( '721.8', $meta['_test_key_2'][0]['raw'] );
53 | $this->assertEquals( 'a:1:{s:3:"foo";a:1:{s:3:"bar";a:1:{s:3:"bat";b:1;}}}', $meta['_test_key_3'][0]['raw'] );
54 | $this->assertEquals( 'cat-a', self::$sp_post->terms['category'][0]['slug'] );
55 | }
56 |
57 | function test_setting_attributes() {
58 | self::$sp_post->post_name = 'new-name';
59 | $this->assertEquals( 'new-name', self::$sp_post->post_name );
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/tests/test-search-suggest.php:
--------------------------------------------------------------------------------
1 | setup();
28 | self::$hooked = true;
29 |
30 | // Unfortunately this gets run an extra time on these tests.
31 | sp_index_flush_data();
32 |
33 | self::$matching_post = self::factory()->post->create_and_get( array( 'post_title' => 'testing suggestions' ) );
34 | self::$unmatching_post = self::factory()->post->create_and_get( array( 'post_title' => 'blah blah blah' ) );
35 | self::index( array( self::$matching_post, self::$unmatching_post ) );
36 | }
37 |
38 | public function setUp(): void {
39 | parent::setUp();
40 |
41 | if ( ! self::$hooked ) {
42 | SP_Search_Suggest::instance()->setup();
43 | }
44 | }
45 |
46 | public function tearDown(): void {
47 | self::$hooked = false;
48 |
49 | parent::tearDown();
50 | }
51 |
52 | protected function make_rest_request( $method, $uri ) {
53 | // Mock REST API.
54 | global $wp_rest_server;
55 | $wp_rest_server = new Spy_REST_Server();
56 | do_action( 'rest_api_init' );
57 |
58 | // Build the API request.
59 | $suggest_url = sprintf( '/%s/%s', SP_Config()->namespace, $uri );
60 | $request = new WP_REST_Request( $method, $suggest_url );
61 |
62 | // Dispatch the request.
63 | return rest_get_server()->dispatch( $request );
64 | }
65 |
66 | /**
67 | * @test
68 | */
69 | public function it_should_find_matching_post_using_suggest_api() {
70 | if ( sp_es_version_compare( '5.0', '<' ) ) {
71 | $this->markTestSkipped( 'Search suggest is only available on ES 5.0+' );
72 | }
73 |
74 | $suggestions = SP_Search_Suggest::instance()->get_suggestions( 'test' );
75 | $this->assertCount( 1, $suggestions );
76 | $this->assertSame(
77 | self::$matching_post->post_title,
78 | $suggestions[0]['_source']['post_title']
79 | );
80 | }
81 |
82 | /**
83 | * @test
84 | */
85 | public function it_should_have_a_rest_endpoint() {
86 | if ( sp_es_version_compare( '5.0', '<' ) ) {
87 | $this->markTestSkipped( 'Search suggest is only available on ES 5.0+' );
88 | }
89 |
90 | $response = $this->make_rest_request( 'GET', 'suggest/test' );
91 |
92 | // Assert the request was successful.
93 | $this->assertNotWPError( $response );
94 | $this->assertInstanceOf( '\WP_REST_Response', $response );
95 | $this->assertEquals( 200, $response->get_status() );
96 |
97 | // Confirm the response data.
98 | $data = $response->get_data();
99 | $this->assertCount( 1, $data );
100 | $this->assertSame(
101 | self::$matching_post->post_title,
102 | $data[0]['_source']['post_title']
103 | );
104 | }
105 |
106 | /**
107 | * @test
108 | */
109 | public function it_should_have_a_rest_schema() {
110 | if ( sp_es_version_compare( '5.0', '<' ) ) {
111 | $this->markTestSkipped( 'Search suggest is only available on ES 5.0+' );
112 | }
113 |
114 | $response = $this->make_rest_request( 'OPTIONS', 'suggest/test' );
115 |
116 | // Assert the request was successful.
117 | $this->assertNotWPError( $response );
118 | $this->assertInstanceOf( '\WP_REST_Response', $response );
119 | $this->assertEquals( 200, $response->get_status() );
120 |
121 | // Confirm the response data.
122 | $data = $response->get_data();
123 |
124 | // Pick one field and confirm that it's present.
125 | $this->assertSame(
126 | 'string',
127 | $data['schema']['items']['properties']['_source']['properties']['post_title']['type']
128 | );
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/tests/test-searching.php:
--------------------------------------------------------------------------------
1 | assertEquals(
15 | array(
16 | 'tags-a-and-c',
17 | 'tags-b-and-c',
18 | 'tags-a-and-b',
19 | 'tag-c',
20 | 'tag-b',
21 | 'tag-a',
22 | 'tags-a-b-c',
23 | 'raw-html-code',
24 | 'simple-markup-test',
25 | 'embedded-video',
26 | ),
27 | $this->search_and_get_field( array() )
28 | );
29 | }
30 |
31 | function test_query_tag_a() {
32 | $this->assertEquals(
33 | array(
34 | 'tags-a-and-c',
35 | 'tags-a-and-b',
36 | 'tag-a',
37 | 'tags-a-b-c',
38 | ),
39 | $this->search_and_get_field( array( 'terms' => array( 'post_tag' => 'tag-a' ) ) )
40 | );
41 | }
42 |
43 | function test_query_tag_b() {
44 | $this->assertEquals(
45 | array(
46 | 'tags-b-and-c',
47 | 'tags-a-and-b',
48 | 'tag-b',
49 | 'tags-a-b-c',
50 | ),
51 | $this->search_and_get_field( array( 'terms' => array( 'post_tag' => 'tag-b' ) ) )
52 | );
53 | }
54 |
55 | function test_query_tag_nun() {
56 | $this->assertEquals(
57 | array( 'tag-%d7%a0' ),
58 | $this->search_and_get_field( array( 'terms' => array( 'post_tag' => 'tag-נ' ) ) )
59 | );
60 | }
61 |
62 | function test_query_tags_union() {
63 | $this->assertEquals(
64 | array(
65 | 'tags-a-and-c',
66 | 'tags-b-and-c',
67 | 'tags-a-and-b',
68 | 'tag-c',
69 | 'tag-b',
70 | 'tags-a-b-c',
71 | ),
72 | $this->search_and_get_field( array( 'terms' => array( 'post_tag' => 'tag-b,tag-c' ) ) )
73 | );
74 | }
75 |
76 | function test_query_tags_intersection() {
77 | $this->assertEquals(
78 | array(
79 | 'tags-a-and-c',
80 | 'tags-a-b-c',
81 | ),
82 | $this->search_and_get_field( array( 'terms' => array( 'post_tag' => 'tag-a+tag-c' ) ) )
83 | );
84 | }
85 |
86 | function test_query_category_name() {
87 | $this->assertEquals(
88 | array(
89 | 'cat-a',
90 | 'cats-a-and-c',
91 | 'cats-a-and-b',
92 | 'cats-a-b-c',
93 | ),
94 | $this->search_and_get_field( array( 'terms' => array( 'category' => 'cat-a' ) ) )
95 | );
96 | }
97 |
98 | public function data_for_query_sorting() {
99 | return array(
100 | array(
101 | array(
102 | 'tags-a-and-c',
103 | 'tags-b-and-c',
104 | 'tags-a-and-b',
105 | 'tag-c',
106 | 'tag-b',
107 | 'tag-a',
108 | 'tags-a-b-c',
109 | 'raw-html-code',
110 | 'simple-markup-test',
111 | 'embedded-video',
112 | ),
113 | array( 'orderby' => 'date', 'order' => 'desc' ),
114 | 'orderby => date desc',
115 | ),
116 | array(
117 | array(
118 | 'parent-one',
119 | 'parent-two',
120 | 'parent-three',
121 | 'child-one',
122 | 'child-two',
123 | 'child-three',
124 | 'child-four',
125 | 'tag-%d7%a0',
126 | 'cats-a-b-c',
127 | 'cats-a-and-b',
128 | ),
129 | array( 'orderby' => 'date', 'order' => 'asc' ),
130 | 'orderby => date asc',
131 | ),
132 | array(
133 | array(
134 | 'tag-%d7%a0',
135 | 'cats-a-b-c',
136 | 'cats-a-and-b',
137 | 'cats-b-and-c',
138 | 'cats-a-and-c',
139 | 'cat-a',
140 | 'cat-b',
141 | 'cat-c',
142 | 'lorem-ipsum',
143 | 'comment-test',
144 | ),
145 | array( 'orderby' => 'id', 'order' => 'asc' ),
146 | 'orderby => id asc',
147 | ),
148 | array(
149 | array( 'cats-a-b-c', 'cat-a', 'lorem-ipsum' ),
150 | array( 'orderby' => 'modified', 'order' => 'desc', 'posts_per_page' => 3 ),
151 | 'orderby => modified desc',
152 | ),
153 | array(
154 | array( 'cats-a-b-c', 'cat-a', 'lorem-ipsum' ),
155 | array( 'orderby' => 'menu_order', 'order' => 'desc', 'posts_per_page' => 3 ),
156 | 'orderby => menu_order desc',
157 | ),
158 | array(
159 | array(
160 | 'child-four',
161 | 'child-three',
162 | 'child-two',
163 | 'child-one',
164 | ),
165 | array( 'orderby' => array( 'parent' => 'desc', 'date' => 'desc' ), 'posts_per_page' => 4 ),
166 | 'orderby => parent desc, date desc',
167 | ),
168 | array(
169 | array( 'cat-a', 'cat-b', 'cat-c' ),
170 | array( 'orderby' => 'name', 'order' => 'asc', 'posts_per_page' => 3 ),
171 | 'orderby => name asc',
172 | ),
173 | array(
174 | array( 'cat-a', 'cat-b', 'cat-c' ),
175 | array( 'orderby' => 'title', 'order' => 'asc', 'posts_per_page' => 3 ),
176 | 'orderby => title asc',
177 | ),
178 | array(
179 | array( 'cat-a', 'cat-b', 'cat-c' ),
180 | array( 'orderby' => array( 'title' => 'asc' ), 'posts_per_page' => 3 ),
181 | 'orderby => title asc',
182 | ),
183 | array(
184 | array( 'tags-b-and-c', 'tags-a-b-c', 'tags-a-and-c' ),
185 | array( 'orderby' => array( 'title' => 'desc' ), 'posts_per_page' => 3 ),
186 | 'orderby => title desc',
187 | ),
188 | array(
189 | array( 'child-three', 'child-four', 'child-one', 'child-two' ),
190 | array( 'orderby' => array( 'parent' => 'desc', 'date' => 'asc' ), 'posts_per_page' => 4 ),
191 | 'orderby => parent desc, date asc',
192 | ),
193 | );
194 | }
195 |
196 | /**
197 | * @dataProvider data_for_query_sorting
198 | */
199 | function test_query_sorting( $expected, $params, $message ) {
200 | $this->assertEquals( $expected, $this->search_and_get_field( $params ), $message );
201 | }
202 |
203 | function test_invalid_sorting() {
204 | $es_args = SP_WP_Search::wp_to_es_args( array( 'orderby' => 'modified', 'order' => 'desc' ) );
205 | $this->assertEquals( 'desc', $es_args['sort'][0]['post_modified.date'], 'Verify es_args["sort"] exists' );
206 |
207 | $es_args = SP_WP_Search::wp_to_es_args( array( 'orderby' => 'modified_gmt' ) );
208 | $this->assertTrue( empty( $es_args['sort'] ), 'Verify es_args["sort"] exists' );
209 | }
210 |
211 | function test_query_posts_per_page() {
212 | $this->assertEquals(
213 | array(
214 | 'tags-a-and-c',
215 | 'tags-b-and-c',
216 | 'tags-a-and-b',
217 | 'tag-c',
218 | 'tag-b',
219 | ),
220 | $this->search_and_get_field( array( 'posts_per_page' => 5 ) )
221 | );
222 | }
223 |
224 | function test_query_offset() {
225 | $this->assertEquals(
226 | array(
227 | 'tags-a-and-b',
228 | 'tag-c',
229 | 'tag-b',
230 | 'tag-a',
231 | 'tags-a-b-c',
232 | 'raw-html-code',
233 | 'simple-markup-test',
234 | 'embedded-video',
235 | 'contributor-post-approved',
236 | 'one-comment',
237 | ),
238 | $this->search_and_get_field( array( 'offset' => 2 ) )
239 | );
240 | }
241 |
242 | function test_query_paged() {
243 | $this->assertEquals(
244 | array(
245 | 'contributor-post-approved',
246 | 'one-comment',
247 | 'no-comments',
248 | 'many-trackbacks',
249 | 'one-trackback',
250 | 'comment-test',
251 | 'lorem-ipsum',
252 | 'cat-c',
253 | 'cat-b',
254 | 'cat-a',
255 | ),
256 | $this->search_and_get_field( array( 'paged' => 2 ) )
257 | );
258 | }
259 |
260 | function test_query_paged_and_posts_per_page() {
261 | $this->assertEquals(
262 | array(
263 | 'no-comments',
264 | 'many-trackbacks',
265 | 'one-trackback',
266 | 'comment-test',
267 | ),
268 | $this->search_and_get_field( array( 'paged' => 4, 'posts_per_page' => 4 ) )
269 | );
270 | }
271 |
272 | /**
273 | * Skipping this test until https://core.trac.wordpress.org/ticket/18897 is
274 | * resolved.
275 | */
276 | function test_query_offset_and_paged() {
277 | $this->markTestSkipped();
278 | $this->assertEquals(
279 | array(
280 | 'many-trackbacks',
281 | 'one-trackback',
282 | 'comment-test',
283 | 'lorem-ipsum',
284 | 'cat-c',
285 | 'cat-b',
286 | 'cat-a',
287 | 'cats-a-and-c',
288 | 'cats-b-and-c',
289 | 'cats-a-and-b',
290 | ),
291 | $this->search_and_get_field( array( 'paged' => 2, 'offset' => 3 ) )
292 | );
293 | }
294 |
295 | function test_exlude_from_search_empty() {
296 | global $wp_post_types;
297 | foreach ( array_keys( $wp_post_types ) as $slug ) {
298 | $wp_post_types[ $slug ]->exclude_from_search = true;
299 | }
300 | SP_Config()->post_types = null;
301 | sp_searchable_post_types( true );
302 |
303 | $this->assertEmpty( $this->search_and_get_field( array( 'post_type' => 'any' ) ) );
304 |
305 | foreach ( array_keys( $wp_post_types ) as $slug ) {
306 | $wp_post_types[ $slug ]->exclude_from_search = false;
307 | }
308 | SP_Config()->post_types = null;
309 | sp_searchable_post_types( true );
310 |
311 | $this->assertNotEmpty( $this->search_and_get_field( array( 'post_type' => 'any' ) ) );
312 | }
313 |
314 | function test_query_post_type() {
315 | $this->assertEmpty( $this->search_and_get_field( array( 'post_type' => 'page' ) ) );
316 | $this->assertNotEmpty( $this->search_and_get_field( array( 'post_type' => 'post' ) ) );
317 | }
318 |
319 | function test_basic_search() {
320 | $expected = array(
321 | 'cat-c',
322 | 'cat-b',
323 | 'cat-a',
324 | 'cats-a-and-c',
325 | 'cats-b-and-c',
326 | 'cats-a-and-b',
327 | 'cats-a-b-c',
328 | );
329 |
330 | $this->assertEquals(
331 | $expected,
332 | $this->search_and_get_field( array( 'query' => 'cat', 'orderby' => 'date' ) )
333 | );
334 | $this->assertEquals(
335 | $expected,
336 | $this->search_and_get_field( array( 'query' => 'cats', 'orderby' => 'date' ) )
337 | );
338 |
339 | $this->assertEquals(
340 | array(
341 | 'many-trackbacks',
342 | 'one-trackback',
343 | ),
344 | $this->search_and_get_field( array( 'query' => 'trackback', 'orderby' => 'date' ) )
345 | );
346 | }
347 |
348 | function test_query_date_ranges() {
349 | $this->assertEquals(
350 | array(
351 | 'contributor-post-approved',
352 | 'one-comment',
353 | 'no-comments',
354 | 'many-trackbacks',
355 | 'one-trackback',
356 | 'comment-test',
357 | 'lorem-ipsum',
358 | 'cat-c',
359 | 'cat-b',
360 | 'cat-a',
361 | ),
362 | $this->search_and_get_field( array(
363 | 'date_range' => array( 'field' => 'post_date', 'gte' => '2009-01-01', 'lt' => '2010-01-01' )
364 | ) )
365 | );
366 |
367 | $this->assertEquals(
368 | array(
369 | 'contributor-post-approved',
370 | 'one-comment',
371 | ),
372 | $this->search_and_get_field( array(
373 | 'date_range' => array( 'field' => 'post_date', 'gt' => '2009-10-02', 'lte' => '2009-12-01 00:00:00' )
374 | ) )
375 | );
376 |
377 | $this->assertEquals(
378 | array(
379 | 'one-comment',
380 | ),
381 | $this->search_and_get_field( array(
382 | 'date_range' => array( 'field' => 'post_date', 'gte' => '2009-11-01 00:00:00', 'lt' => '2009-12-01 00:00:00' )
383 | ) )
384 | );
385 |
386 | $this->assertEquals(
387 | array(
388 | 'parent-one',
389 | ),
390 | $this->search_and_get_field( array(
391 | 'date_range' => array( 'lt' => '2007-01-01 00:00:02' )
392 | ) )
393 | );
394 | }
395 |
396 | function test_search_get_posts() {
397 | $db_posts = get_posts( 'tag=tag-a&order=id&order=asc' );
398 | $sp_posts = sp_wp_search( array( 'terms' => array( 'post_tag' => 'tag-a' ), 'orderby' => 'id', 'order' => 'asc' ) );
399 |
400 | $this->assertEquals( $db_posts, $sp_posts );
401 | $this->assertInstanceOf( '\WP_Post', reset( $sp_posts ) );
402 | $this->assertEquals( 'tags-a-b-c', reset( $sp_posts )->post_title );
403 | }
404 |
405 | function test_raw_es() {
406 | $posts = sp_search( array(
407 | 'query' => array(
408 | 'match_all' => new stdClass(),
409 | ),
410 | '_source' => array( 'post_id' ),
411 | 'size' => 1,
412 | 'from' => 1,
413 | 'sort' => array(
414 | 'post_name.raw' => 'asc',
415 | ),
416 | ) );
417 | $this->assertEquals( 'cat-b', $posts[0]->post_name );
418 |
419 | $s = new SP_Search( array(
420 | 'query' => array(
421 | 'match_all' => new stdClass
422 | ),
423 | '_source' => array( 'post_id' ),
424 | 'size' => 1,
425 | 'from' => 2,
426 | 'sort' => array(
427 | 'post_name.raw' => 'asc'
428 | )
429 | ) );
430 | $posts = $s->get_posts();
431 | $this->assertEquals( 'cat-c', $posts[0]->post_name );
432 |
433 | // Test running it again
434 | $posts = $s->get_posts();
435 | $this->assertEquals( 'cat-c', $posts[0]->post_name );
436 |
437 | // Verify emptiness
438 | $s = new SP_Search( array(
439 | 'query' => array(
440 | 'term' => array(
441 | 'post_name.raw' => array(
442 | 'value' => 'cucumbers',
443 | )
444 | )
445 | ),
446 | '_source' => array( 'post_id' ),
447 | 'size' => 1,
448 | 'from' => 0,
449 | 'sort' => array(
450 | 'post_name.raw' => 'asc',
451 | )
452 | ) );
453 | $this->assertEmpty( $s->get_posts() );
454 | }
455 |
456 | function test_query_author_vars() {
457 | $author_1 = self::factory()->user->create( array( 'user_login' => 'author1', 'user_pass' => rand_str(), 'role' => 'author' ) );
458 | $post_1 = self::factory()->post->create( array( 'post_title' => rand_str(), 'post_author' => $author_1, 'post_date' => '2006-01-04 00:00:00' ) );
459 |
460 | $author_2 = self::factory()->user->create( array( 'user_login' => 'author2', 'user_pass' => rand_str(), 'role' => 'author' ) );
461 | $post_2 = self::factory()->post->create( array( 'post_title' => rand_str(), 'post_author' => $author_2, 'post_date' => '2006-01-03 00:00:00' ) );
462 |
463 | $author_3 = self::factory()->user->create( array( 'user_login' => 'author3', 'user_pass' => rand_str(), 'role' => 'author' ) );
464 | $post_3 = self::factory()->post->create( array( 'post_title' => rand_str(), 'post_author' => $author_3, 'post_date' => '2006-01-02 00:00:00' ) );
465 |
466 | $author_4 = self::factory()->user->create( array( 'user_login' => 'author4', 'user_pass' => rand_str(), 'role' => 'author' ) );
467 | $post_4 = self::factory()->post->create( array( 'post_title' => rand_str(), 'post_author' => $author_4, 'post_date' => '2006-01-01 00:00:00' ) );
468 |
469 | // Index the posts.
470 | self::index( array( $post_1, $post_2, $post_3, $post_4 ) );
471 |
472 | $this->assertEquals(
473 | array( $author_2 ),
474 | $this->search_and_get_field( array( 'author' => $author_2 ), 'post_author.user_id' )
475 | );
476 |
477 | $this->assertEquals(
478 | array( $author_1, $author_2, $author_3, $author_4 ),
479 | $this->search_and_get_field( array(
480 | 'author' => array( $author_1, $author_2, $author_3, $author_4 )
481 | ), 'post_author.user_id' )
482 | );
483 |
484 | $this->assertEquals(
485 | array( $author_3 ),
486 | $this->search_and_get_field( array( 'author_name' => 'author3' ), 'post_author.user_id' )
487 | );
488 |
489 | $this->assertEquals(
490 | array( $author_1, $author_2, $author_3, $author_4 ),
491 | $this->search_and_get_field( array(
492 | 'author_name' => array( 'author1', 'author2', 'author3', 'author4' )
493 | ), 'post_author.user_id' )
494 | );
495 |
496 | $this->assertEquals(
497 | array( $author_4, $author_3, $author_2, $author_1 ),
498 | $this->search_and_get_field( array(
499 | 'author_name' => array( 'author1', 'author2', 'author3', 'author4' ),
500 | 'orderby' => 'author'
501 | ), 'post_author.user_id' )
502 | );
503 | }
504 | }
505 |
--------------------------------------------------------------------------------
/tests/test-sync-meta.php:
--------------------------------------------------------------------------------
1 | assertFalse( SP_Sync_Meta()->running );
10 | SP_Sync_Meta()->start();
11 | $this->assertTrue( SP_Sync_Meta()->running );
12 | $this->assertGreaterThan( 0, SP_Sync_Meta()->started );
13 | }
14 |
15 | function test_sync_meta_reset() {
16 | SP_Sync_Meta()->start();
17 | $this->assertTrue( SP_Sync_Meta()->running );
18 |
19 | SP_Sync_Meta()->reset();
20 | $this->assertFalse( SP_Sync_Meta()->running );
21 | }
22 |
23 | function test_sync_meta_storage() {
24 | delete_option( 'sp_sync_meta' );
25 | $meta = get_option( 'sp_sync_meta', null );
26 | $this->assertNull( $meta );
27 |
28 | SP_Sync_Meta()->start( 'save' );
29 | $meta = get_option( 'sp_sync_meta', null );
30 | $this->assertTrue( $meta['running'] );
31 | }
32 |
33 | function test_sync_meta_reset_save() {
34 | SP_Sync_Meta()->start( 'save' );
35 | $meta = get_option( 'sp_sync_meta', null );
36 | $this->assertTrue( $meta['running'] );
37 |
38 | SP_Sync_Meta()->reset( 'save' );
39 | $meta = get_option( 'sp_sync_meta', null );
40 | $this->assertFalse( $meta['running'] );
41 | }
42 |
43 | function test_sync_meta_deleting() {
44 | SP_Sync_Meta()->start( 'save' );
45 | $meta = get_option( 'sp_sync_meta', null );
46 | $this->assertTrue( SP_Sync_Meta()->running );
47 | $this->assertTrue( $meta['running'] );
48 |
49 | SP_Sync_Meta()->delete();
50 | $this->assertFalse( SP_Sync_Meta()->running );
51 | $meta = get_option( 'sp_sync_meta', null );
52 | $this->assertNull( $meta );
53 | }
54 |
55 | function test_sync_meta_magic_set() {
56 | SP_Sync_Meta()->reset();
57 | $this->assertFalse( SP_Sync_Meta()->running );
58 | SP_Sync_Meta()->running = true;
59 | $this->assertTrue( SP_Sync_Meta()->running );
60 | }
61 |
62 | function test_sync_meta_magic_isset() {
63 | SP_Sync_Meta()->start( 'save' );
64 | $meta = get_option( 'sp_sync_meta', null );
65 | $this->assertEmpty( $meta['messages'] );
66 | $this->assertTrue( isset( $meta['messages'] ) );
67 | $this->assertEmpty( SP_Sync_Meta()->messages );
68 | $this->assertTrue( isset( SP_Sync_Meta()->messages ) );
69 | }
70 |
71 | function test_sync_meta_invalid_property() {
72 | SP_Sync_Meta()->reset();
73 |
74 | // Getting
75 | $this->assertFalse( SP_Sync_Meta()->running );
76 | $this->assertInstanceOf( 'WP_Error', SP_Sync_Meta()->foo );
77 |
78 | // Setting
79 | SP_Sync_Meta()->running = true;
80 | $this->assertTrue( SP_Sync_Meta()->running );
81 | SP_Sync_Meta()->foo = 'bar';
82 | $this->assertInstanceOf( 'WP_Error', SP_Sync_Meta()->foo );
83 | }
84 |
85 | function test_sync_meta_logging() {
86 | $message = rand_str();
87 | SP_Sync_Meta()->log( new WP_Error( 'error', $message ) );
88 | $this->assertEquals( $message, SP_Sync_Meta()->messages['error'][0] );
89 | SP_Sync_Meta()->clear_log();
90 | $this->assertEmpty( SP_Sync_Meta()->messages );
91 | }
92 |
93 | function test_sync_meta_error_notice() {
94 | SP_Sync_Meta()->running = false;
95 | $message = rand_str();
96 | SP_Sync_Meta()->log( new WP_Error( 'error', $message ) );
97 | $this->assertEquals( $message, SP_Sync_Meta()->messages['error'][0] );
98 | $this->assertTrue( SP_Sync_Meta()->has_errors() );
99 | SP_Sync_Meta()->clear_error_notice();
100 | $this->assertFalse( SP_Sync_Meta()->has_errors() );
101 | }
102 | }
103 |
--------------------------------------------------------------------------------