diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 3b67c6c..934c1bb 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -57,23 +57,22 @@ jobs: run: | composer config -g cache-dir "${{ env.COMPOSER_CACHE }}" - name: Prepare composer cache - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: ${{ env.COMPOSER_CACHE }} key: composer-${{ env.COMPOSER_VERSION }}-${{ hashFiles('**/composer.lock') }} restore-keys: | composer-${{ env.COMPOSER_VERSION }}- + - name: Set PHP version uses: shivammathur/setup-php@v2 with: - php-version: '7.4' + php-version: '8.0' + tools: cs2pr coverage: none - name: composer install run: composer install - name: PHPCS check - uses: chekalsky/phpcs-action@v1 - with: - enable_warnings: true - phpcs_bin_path: './vendor/bin/phpcs . --runtime-set testVersion 7.0-' \ No newline at end of file + run: './vendor/bin/phpcs . -q --report=checkstyle --runtime-set testVersion 7.0- | cs2pr' \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a8a2d4f..28439fd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -36,6 +36,11 @@ jobs: sudo sysctl -w fs.file-max=262144 sudo sysctl -w vm.max_map_count=262144 + - name: Setup Elasticsearch + uses: getong/elasticsearch-action@v1.2 + with: + elasticsearch version: '7.5.0' + - name: Set standard 10up cache directories run: | composer config -g cache-dir "${{ env.COMPOSER_CACHE }}" diff --git a/composer.json b/composer.json index bfcefd7..e01b755 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,7 @@ } ], "require": { - "php": ">=7.0" + "php": "^7.0|^8.0" }, "autoload": { "psr-4": { @@ -20,17 +20,17 @@ } }, "require-dev": { - "phpunit/phpunit": "^9.5", - "10up/wp_mock": "^0.5.0", + "phpunit/phpunit": "^9", + "10up/wp_mock": "dev-trunk", "10up/phpcs-composer": "dev-master", - "10up/elasticpress": "*", + "10up/elasticpress": "dev-develop", "yoast/phpunit-polyfills": "^1.0" }, "scripts": { "lint": "phpcs elasticpresslabs.php includes --runtime-set testVersion 7.0-", "lint-fix": "phpcbf elasticpresslabs.php includes", "test": "phpunit", - "setup-local-tests": "bash bin/install-wp-tests.sh epl_wp_test root password mysql trunk true" + "setup-local-tests": "bash bin/install-wp-tests.sh epl_wp_test root password 127.0.0.1 latest true" }, "config": { "allow-plugins": { diff --git a/composer.lock b/composer.lock index a7e38f1..eb8d50a 100644 --- a/composer.lock +++ b/composer.lock @@ -4,21 +4,21 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a049a5c1d2d49305f3b5035bef435da3", + "content-hash": "bc356f2f343787994f5f4f5d5dbbb831", "packages": [], "packages-dev": [ { "name": "10up/elasticpress", - "version": "4.3.1", + "version": "dev-develop", "source": { "type": "git", "url": "https://github.com/10up/ElasticPress.git", - "reference": "e52164f2bc1fc749c0d7755ef2992c7b2f65ed4d" + "reference": "ce6b43138066ea6d69f39def05214072a8e300da" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/10up/ElasticPress/zipball/e52164f2bc1fc749c0d7755ef2992c7b2f65ed4d", - "reference": "e52164f2bc1fc749c0d7755ef2992c7b2f65ed4d", + "url": "https://api.github.com/repos/10up/ElasticPress/zipball/ce6b43138066ea6d69f39def05214072a8e300da", + "reference": "ce6b43138066ea6d69f39def05214072a8e300da", "shasum": "" }, "require": { @@ -27,10 +27,11 @@ "require-dev": { "10up/phpcs-composer": "dev-master", "phpcompatibility/phpcompatibility-wp": "*", - "phpunit/phpunit": "^7", + "phpunit/phpunit": "^9.5", "wpackagist-plugin/woocommerce": "*", - "yoast/phpunit-polyfills": "1.x-dev" + "yoast/phpunit-polyfills": "^1.0" }, + "default-branch": true, "type": "wordpress-plugin", "extra": { "installer-paths": { @@ -51,12 +52,12 @@ }, { "name": "10up", - "homepage": "http://10up.com" + "homepage": "https://10up.com" }, { "name": "Aaron Holbrook", "email": "aaron@10up.com", - "homepage": "http://aaronjholbrook.com" + "homepage": "https://aaronjholbrook.com" } ], "description": "Supercharge WordPress with Elasticsearch.", @@ -69,9 +70,9 @@ ], "support": { "issues": "https://github.com/10up/ElasticPress/issues", - "source": "https://github.com/10up/ElasticPress/tree/4.3.1" + "source": "https://github.com/10up/ElasticPress/tree/develop" }, - "time": "2022-09-27T17:03:44+00:00" + "time": "2023-02-20T12:38:37+00:00" }, { "name": "10up/phpcs-composer", @@ -79,15 +80,16 @@ "source": { "type": "git", "url": "https://github.com/10up/phpcs-composer.git", - "reference": "a3b05c0dafbb4a5df8b47f845074157c096e84a6" + "reference": "5f9ac994db1af9924a71b0f5663fe7f6bd59c812" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/10up/phpcs-composer/zipball/a3b05c0dafbb4a5df8b47f845074157c096e84a6", - "reference": "a3b05c0dafbb4a5df8b47f845074157c096e84a6", + "url": "https://api.github.com/repos/10up/phpcs-composer/zipball/5f9ac994db1af9924a71b0f5663fe7f6bd59c812", + "reference": "5f9ac994db1af9924a71b0f5663fe7f6bd59c812", "shasum": "" }, "require": { + "automattic/vipwpcs": "^2.3", "dealerdirect/phpcodesniffer-composer-installer": "*", "phpcompatibility/phpcompatibility-wp": "^2", "squizlabs/php_codesniffer": "3.7.1", @@ -109,20 +111,20 @@ "issues": "https://github.com/10up/phpcs-composer/issues", "source": "https://github.com/10up/phpcs-composer/tree/master" }, - "time": "2022-11-03T18:34:24+00:00" + "time": "2022-11-18T18:13:03+00:00" }, { "name": "10up/wp_mock", - "version": "0.5.0", + "version": "dev-trunk", "source": { "type": "git", "url": "https://github.com/10up/wp_mock.git", - "reference": "5cd57c63b1a946301ce0d7aaaa37cdc25805c163" + "reference": "4a460429a2df1cf0f43e61bdd52afce02e068e94" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/10up/wp_mock/zipball/5cd57c63b1a946301ce0d7aaaa37cdc25805c163", - "reference": "5cd57c63b1a946301ce0d7aaaa37cdc25805c163", + "url": "https://api.github.com/repos/10up/wp_mock/zipball/4a460429a2df1cf0f43e61bdd52afce02e068e94", + "reference": "4a460429a2df1cf0f43e61bdd52afce02e068e94", "shasum": "" }, "require": { @@ -133,7 +135,15 @@ }, "require-dev": { "behat/behat": "^v3.11.0", + "dealerdirect/phpcodesniffer-composer-installer": "^0.7.2", + "friendsofphp/php-cs-fixer": "^3.4", "php-coveralls/php-coveralls": "^v2.5.3", + "php-stubs/wordpress-globals": "^0.2.0", + "php-stubs/wordpress-stubs": "^6.0", + "phpcompatibility/php-compatibility": "^9.3", + "phpstan/phpstan": "^1.9", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-phpunit": "^1.2", "sebastian/comparator": "^4.0.8", "sempro/phpunit-pretty-print": "^1.4" }, @@ -153,22 +163,22 @@ "description": "A mocking library to take the pain out of unit testing for WordPress", "support": { "issues": "https://github.com/10up/wp_mock/issues", - "source": "https://github.com/10up/wp_mock/tree/0.5.0" + "source": "https://github.com/10up/wp_mock/tree/trunk" }, - "time": "2022-11-01T03:01:40+00:00" + "time": "2023-01-16T03:31:12+00:00" }, { "name": "antecedent/patchwork", - "version": "2.1.21", + "version": "2.1.25", "source": { "type": "git", "url": "https://github.com/antecedent/patchwork.git", - "reference": "25c1fa0cd9a6e6d0d13863d8df8f050b6733f16d" + "reference": "17314e042d45e0dacb0a494c2d1ef50e7621136a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/antecedent/patchwork/zipball/25c1fa0cd9a6e6d0d13863d8df8f050b6733f16d", - "reference": "25c1fa0cd9a6e6d0d13863d8df8f050b6733f16d", + "url": "https://api.github.com/repos/antecedent/patchwork/zipball/17314e042d45e0dacb0a494c2d1ef50e7621136a", + "reference": "17314e042d45e0dacb0a494c2d1ef50e7621136a", "shasum": "" }, "require": { @@ -201,9 +211,61 @@ ], "support": { "issues": "https://github.com/antecedent/patchwork/issues", - "source": "https://github.com/antecedent/patchwork/tree/2.1.21" + "source": "https://github.com/antecedent/patchwork/tree/2.1.25" }, - "time": "2022-02-07T07:28:34+00:00" + "time": "2023-02-19T12:51:24+00:00" + }, + { + "name": "automattic/vipwpcs", + "version": "2.3.3", + "source": { + "type": "git", + "url": "https://github.com/Automattic/VIP-Coding-Standards.git", + "reference": "6cd0a6a82bc0ac988dbf9d6a7c2e293dc8ac640b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Automattic/VIP-Coding-Standards/zipball/6cd0a6a82bc0ac988dbf9d6a7c2e293dc8ac640b", + "reference": "6cd0a6a82bc0ac988dbf9d6a7c2e293dc8ac640b", + "shasum": "" + }, + "require": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.4.1 || ^0.5 || ^0.6.2 || ^0.7", + "php": ">=5.4", + "sirbrillig/phpcs-variable-analysis": "^2.11.1", + "squizlabs/php_codesniffer": "^3.5.5", + "wp-coding-standards/wpcs": "^2.3" + }, + "require-dev": { + "php-parallel-lint/php-console-highlighter": "^0.5", + "php-parallel-lint/php-parallel-lint": "^1.0", + "phpcompatibility/php-compatibility": "^9", + "phpcsstandards/phpcsdevtools": "^1.0", + "phpunit/phpunit": "^4 || ^5 || ^6 || ^7" + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Contributors", + "homepage": "https://github.com/Automattic/VIP-Coding-Standards/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer rules (sniffs) to enforce WordPress VIP minimum coding conventions", + "keywords": [ + "phpcs", + "standards", + "wordpress" + ], + "support": { + "issues": "https://github.com/Automattic/VIP-Coding-Standards/issues", + "source": "https://github.com/Automattic/VIP-Coding-Standards", + "wiki": "https://github.com/Automattic/VIP-Coding-Standards/wiki" + }, + "time": "2021-09-29T16:20:23+00:00" }, { "name": "dealerdirect/phpcodesniffer-composer-installer", @@ -282,30 +344,30 @@ }, { "name": "doctrine/instantiator", - "version": "1.4.1", + "version": "1.5.0", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "10dcfce151b967d20fde1b34ae6640712c3891bc" + "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/10dcfce151b967d20fde1b34ae6640712c3891bc", - "reference": "10dcfce151b967d20fde1b34ae6640712c3891bc", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/0a0fa9780f5d4e507415a065172d26a98d02047b", + "reference": "0a0fa9780f5d4e507415a065172d26a98d02047b", "shasum": "" }, "require": { "php": "^7.1 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^9", + "doctrine/coding-standard": "^9 || ^11", "ext-pdo": "*", "ext-phar": "*", "phpbench/phpbench": "^0.16 || ^1", "phpstan/phpstan": "^1.4", "phpstan/phpstan-phpunit": "^1", "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", - "vimeo/psalm": "^4.22" + "vimeo/psalm": "^4.30 || ^5.4" }, "type": "library", "autoload": { @@ -332,7 +394,7 @@ ], "support": { "issues": "https://github.com/doctrine/instantiator/issues", - "source": "https://github.com/doctrine/instantiator/tree/1.4.1" + "source": "https://github.com/doctrine/instantiator/tree/1.5.0" }, "funding": [ { @@ -348,7 +410,7 @@ "type": "tidelift" } ], - "time": "2022-03-03T08:28:38+00:00" + "time": "2022-12-30T00:15:36+00:00" }, { "name": "hamcrest/hamcrest-php", @@ -534,16 +596,16 @@ }, { "name": "nikic/php-parser", - "version": "v4.15.1", + "version": "v4.15.3", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "0ef6c55a3f47f89d7a374e6f835197a0b5fcf900" + "reference": "570e980a201d8ed0236b0a62ddf2c9cbb2034039" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/0ef6c55a3f47f89d7a374e6f835197a0b5fcf900", - "reference": "0ef6c55a3f47f89d7a374e6f835197a0b5fcf900", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/570e980a201d8ed0236b0a62ddf2c9cbb2034039", + "reference": "570e980a201d8ed0236b0a62ddf2c9cbb2034039", "shasum": "" }, "require": { @@ -584,9 +646,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.1" + "source": "https://github.com/nikic/PHP-Parser/tree/v4.15.3" }, - "time": "2022-09-04T07:30:47+00:00" + "time": "2023-01-16T22:05:37+00:00" }, { "name": "phar-io/manifest", @@ -875,16 +937,16 @@ }, { "name": "phpunit/php-code-coverage", - "version": "9.2.18", + "version": "9.2.24", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "12fddc491826940cf9b7e88ad9664cf51f0f6d0a" + "reference": "2cf940ebc6355a9d430462811b5aaa308b174bed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/12fddc491826940cf9b7e88ad9664cf51f0f6d0a", - "reference": "12fddc491826940cf9b7e88ad9664cf51f0f6d0a", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2cf940ebc6355a9d430462811b5aaa308b174bed", + "reference": "2cf940ebc6355a9d430462811b5aaa308b174bed", "shasum": "" }, "require": { @@ -940,7 +1002,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.18" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.24" }, "funding": [ { @@ -948,7 +1010,7 @@ "type": "github" } ], - "time": "2022-10-27T13:35:33+00:00" + "time": "2023-01-26T08:26:55+00:00" }, { "name": "phpunit/php-file-iterator", @@ -1193,20 +1255,20 @@ }, { "name": "phpunit/phpunit", - "version": "9.5.26", + "version": "9.6.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "851867efcbb6a1b992ec515c71cdcf20d895e9d2" + "reference": "e7b1615e3e887d6c719121c6d4a44b0ab9645555" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/851867efcbb6a1b992ec515c71cdcf20d895e9d2", - "reference": "851867efcbb6a1b992ec515c71cdcf20d895e9d2", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/e7b1615e3e887d6c719121c6d4a44b0ab9645555", + "reference": "e7b1615e3e887d6c719121c6d4a44b0ab9645555", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.3.1", + "doctrine/instantiator": "^1.3.1 || ^2", "ext-dom": "*", "ext-json": "*", "ext-libxml": "*", @@ -1244,7 +1306,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "9.5-dev" + "dev-master": "9.6-dev" } }, "autoload": { @@ -1275,7 +1337,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.26" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.3" }, "funding": [ { @@ -1291,7 +1353,7 @@ "type": "tidelift" } ], - "time": "2022-10-28T06:00:21+00:00" + "time": "2023-02-04T13:37:15+00:00" }, { "name": "sebastian/cli-parser", @@ -1659,16 +1721,16 @@ }, { "name": "sebastian/environment", - "version": "5.1.4", + "version": "5.1.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "1b5dff7bb151a4db11d49d90e5408e4e938270f7" + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/1b5dff7bb151a4db11d49d90e5408e4e938270f7", - "reference": "1b5dff7bb151a4db11d49d90e5408e4e938270f7", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", "shasum": "" }, "require": { @@ -1710,7 +1772,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", - "source": "https://github.com/sebastianbergmann/environment/tree/5.1.4" + "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" }, "funding": [ { @@ -1718,7 +1780,7 @@ "type": "github" } ], - "time": "2022-04-03T09:37:03+00:00" + "time": "2023-02-03T06:03:51+00:00" }, { "name": "sebastian/exporter", @@ -2032,16 +2094,16 @@ }, { "name": "sebastian/recursion-context", - "version": "4.0.4", + "version": "4.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172" + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/cd9d8cf3c5804de4341c283ed787f099f5506172", - "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", "shasum": "" }, "require": { @@ -2080,10 +2142,10 @@ } ], "description": "Provides functionality to recursively process PHP variables", - "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "homepage": "https://github.com/sebastianbergmann/recursion-context", "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.4" + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5" }, "funding": [ { @@ -2091,7 +2153,7 @@ "type": "github" } ], - "time": "2020-10-26T13:17:30+00:00" + "time": "2023-02-03T06:07:39+00:00" }, { "name": "sebastian/resource-operations", @@ -2150,16 +2212,16 @@ }, { "name": "sebastian/type", - "version": "3.2.0", + "version": "3.2.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "fb3fe09c5f0bae6bc27ef3ce933a1e0ed9464b6e" + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/fb3fe09c5f0bae6bc27ef3ce933a1e0ed9464b6e", - "reference": "fb3fe09c5f0bae6bc27ef3ce933a1e0ed9464b6e", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", "shasum": "" }, "require": { @@ -2194,7 +2256,7 @@ "homepage": "https://github.com/sebastianbergmann/type", "support": { "issues": "https://github.com/sebastianbergmann/type/issues", - "source": "https://github.com/sebastianbergmann/type/tree/3.2.0" + "source": "https://github.com/sebastianbergmann/type/tree/3.2.1" }, "funding": [ { @@ -2202,7 +2264,7 @@ "type": "github" } ], - "time": "2022-09-12T14:47:03+00:00" + "time": "2023-02-03T06:13:03+00:00" }, { "name": "sebastian/version", @@ -2257,6 +2319,64 @@ ], "time": "2020-09-28T06:39:44+00:00" }, + { + "name": "sirbrillig/phpcs-variable-analysis", + "version": "v2.11.10", + "source": { + "type": "git", + "url": "https://github.com/sirbrillig/phpcs-variable-analysis.git", + "reference": "0f25a3766f26df91d6bdda0c8931303fc85499d7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sirbrillig/phpcs-variable-analysis/zipball/0f25a3766f26df91d6bdda0c8931303fc85499d7", + "reference": "0f25a3766f26df91d6bdda0c8931303fc85499d7", + "shasum": "" + }, + "require": { + "php": ">=5.4.0", + "squizlabs/php_codesniffer": "^3.5.6" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.7 || ^1.0", + "phpcsstandards/phpcsdevcs": "^1.1", + "phpstan/phpstan": "^1.7", + "phpunit/phpunit": "^4.8.36 || ^5.7.21 || ^6.5 || ^7.0 || ^8.0 || ^9.0", + "sirbrillig/phpcs-import-detection": "^1.1", + "vimeo/psalm": "^0.2 || ^0.3 || ^1.1 || ^4.24 || ^5.0@beta" + }, + "type": "phpcodesniffer-standard", + "autoload": { + "psr-4": { + "VariableAnalysis\\": "VariableAnalysis/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Sam Graham", + "email": "php-codesniffer-variableanalysis@illusori.co.uk" + }, + { + "name": "Payton Swick", + "email": "payton@foolord.com" + } + ], + "description": "A PHPCS sniff to detect problems with variables.", + "keywords": [ + "phpcs", + "static analysis" + ], + "support": { + "issues": "https://github.com/sirbrillig/phpcs-variable-analysis/issues", + "source": "https://github.com/sirbrillig/phpcs-variable-analysis", + "wiki": "https://github.com/sirbrillig/phpcs-variable-analysis/wiki" + }, + "time": "2023-01-05T18:45:16+00:00" + }, { "name": "squizlabs/php_codesniffer", "version": "3.7.1", @@ -2416,16 +2536,16 @@ }, { "name": "yoast/phpunit-polyfills", - "version": "1.0.3", + "version": "1.0.4", "source": { "type": "git", "url": "https://github.com/Yoast/PHPUnit-Polyfills.git", - "reference": "5ea3536428944955f969bc764bbe09738e151ada" + "reference": "3c621ff5429d2b1ff96dc5808ad6cde99d31ea4c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Yoast/PHPUnit-Polyfills/zipball/5ea3536428944955f969bc764bbe09738e151ada", - "reference": "5ea3536428944955f969bc764bbe09738e151ada", + "url": "https://api.github.com/repos/Yoast/PHPUnit-Polyfills/zipball/3c621ff5429d2b1ff96dc5808ad6cde99d31ea4c", + "reference": "3c621ff5429d2b1ff96dc5808ad6cde99d31ea4c", "shasum": "" }, "require": { @@ -2433,7 +2553,7 @@ "phpunit/phpunit": "^4.8.36 || ^5.7.21 || ^6.0 || ^7.0 || ^8.0 || ^9.0" }, "require-dev": { - "yoast/yoastcs": "^2.2.0" + "yoast/yoastcs": "^2.2.1" }, "type": "library", "extra": { @@ -2473,18 +2593,20 @@ "issues": "https://github.com/Yoast/PHPUnit-Polyfills/issues", "source": "https://github.com/Yoast/PHPUnit-Polyfills" }, - "time": "2021-11-23T01:37:03+00:00" + "time": "2022-11-16T09:07:52+00:00" } ], "aliases": [], "minimum-stability": "stable", "stability-flags": { - "10up/phpcs-composer": 20 + "10up/wp_mock": 20, + "10up/phpcs-composer": 20, + "10up/elasticpress": 20 }, "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": ">=7.0" + "php": "^7.0|^8.0" }, "platform-dev": [], "plugin-api-version": "2.3.0" diff --git a/includes/classes/Feature/ElasticPressLabs.php b/includes/classes/Feature/ElasticPressLabs.php index 674057e..2296170 100644 --- a/includes/classes/Feature/ElasticPressLabs.php +++ b/includes/classes/Feature/ElasticPressLabs.php @@ -176,7 +176,7 @@ private function register_subfeatures() { continue; } - require ELASTICPRESS_LABS_INC . 'classes/Feature/' . basename( $filename ); + require_once ELASTICPRESS_LABS_INC . 'classes/Feature/' . basename( $filename ); $class_name = 'ElasticPressLabs\Feature\\' . basename( $filename, '.php' ); diff --git a/includes/classes/Feature/Users.php b/includes/classes/Feature/Users.php new file mode 100644 index 0000000..2c1bbe7 --- /dev/null +++ b/includes/classes/Feature/Users.php @@ -0,0 +1,95 @@ +slug = 'users'; + + $this->title = esc_html__( 'Users', 'elasticpress' ); + + $this->summary = __( 'Improve user search relevancy and query performance.', 'elasticpress' ); + + $this->docs_url = __( 'https://elasticpress.zendesk.com/hc/en-us/articles/360050447492-Configuring-ElasticPress-via-the-Plugin-Dashboard#users', 'elasticpress' ); + + $this->requires_install_reindex = true; + + Indexables::factory()->register( new \ElasticPressLabs\Indexable\User\User(), false ); + + parent::__construct(); + } + + /** + * Hook search functionality + */ + public function setup() { + Indexables::factory()->activate( 'user' ); + + add_action( 'init', [ $this, 'search_setup' ] ); + } + + /** + * Setup feature on each page load + */ + public function search_setup() { + add_filter( 'ep_elasticpress_enabled', [ $this, 'integrate_search_queries' ], 10, 2 ); + } + + /** + * Output feature box long text + */ + public function output_feature_box_long() { + ?> +
+ + query_vars['ep_integrate'] ) && ! filter_var( $query->query_vars['ep_integrate'], FILTER_VALIDATE_BOOLEAN ) ) { + $enabled = false; + } elseif ( ! empty( $query->query_vars['search'] ) ) { + $enabled = true; + } + + return $enabled; + } + + /** + * Determine feature reqs status + * + * @return FeatureRequirementsStatus + */ + public function requirements_status() { + $status = new FeatureRequirementsStatus( 1 ); + + return $status; + } +} diff --git a/includes/classes/Indexable/User/QueryIntegration.php b/includes/classes/Indexable/User/QueryIntegration.php new file mode 100644 index 0000000..aae6d56 --- /dev/null +++ b/includes/classes/Indexable/User/QueryIntegration.php @@ -0,0 +1,207 @@ +is_full_reindexing() is not available at this point yet, so using the IndexHelper version of it. + if ( \ElasticPress\IndexHelper::factory()->is_full_reindexing( $indexable_slug ) ) { + return; + } + + add_filter( 'users_pre_query', [ $this, 'maybe_filter_query' ], 10, 2 ); + + // Add header + add_action( 'pre_get_users', array( $this, 'action_pre_get_users' ), 5 ); + } + + /** + * If WP_User_Query meets certain conditions, query results from ES + * + * @param array $results Users array. + * @param WP_User_Query $query Current query. + * @return array + */ + public function maybe_filter_query( $results, WP_User_Query $query ) { + $user_indexable = Indexables::factory()->get( 'user' ); + + /** + * Filter to skip user query integration + * + * @hook ep_skip_user_query_integration + * @param {bool} $skip True meanas skip query + * @param {WP_User_Query} $query User query + * @since 2.1.0 + * @return {boolean} New value + */ + if ( ! $user_indexable->elasticpress_enabled( $query ) || apply_filters( 'ep_skip_user_query_integration', false, $query ) ) { + return $results; + } + + /** + * Filter cached user query users + * + * @hook ep_wp_query_search_cached_users + * @param {array} $users Array of users + * @param {WP_User_Query} $query User query + * @since 2.1.0 + * @return {array} New users + */ + $new_users = apply_filters( 'ep_wp_query_search_cached_users', null, $query ); + + if ( null === $new_users ) { + $formatted_args = $user_indexable->format_args( $query->query_vars, $query ); + + $ep_query = $user_indexable->query_es( $formatted_args, $query->query_vars, null, $query ); + + if ( false === $ep_query ) { + return $results; + } + + /** + * WP_User_Query does not let us set this property: + * + * $query->elasticsearch_success = true; + */ + $query->query_vars['elasticsearch_success'] = true; + + $fields = $query->get( 'fields' ); + $new_users = []; + + if ( in_array( $fields, [ 'all', 'all_with_meta' ], true ) ) { + foreach ( $ep_query['documents'] as $document ) { + $new_users[] = $document['ID']; + } + } elseif ( is_array( $fields ) ) { + // WP_User_Query returns a stdClass. + foreach ( $ep_query['documents'] as $document ) { + + $user = new \stdClass(); + $user->elasticsearch = true; // Super useful for debugging. + + foreach ( $fields as $field ) { + if ( 'id' === $field ) { + $field = 'ID'; + } + $user->$field = $document[ $field ]; + } + + $new_users[] = $user; + } + } elseif ( is_string( $fields ) && ! empty( $fields ) ) { + foreach ( $ep_query['documents'] as $document ) { + $new_users[] = $document[ $fields ]; + } + } else { + $new_users = $this->format_hits_as_users( $ep_query['documents'] ); + } + } + + $query->total_users = is_array( $ep_query['found_documents'] ) ? $ep_query['found_documents']['value'] : $ep_query['found_documents']; // 7.0+ have this as an array rather than int; + + return $new_users; + } + + /** + * Format the ES hits/results as WP_User objects. + * + * @param array $users The users that should be formatted. + * @return array + */ + protected function format_hits_as_users( $users ) { + $new_users = []; + + foreach ( $users as $user_array ) { + $user = new \stdClass(); + + /** + * Filter arguments inserted into user object after search + * + * @hook ep_search_user_return_args + * @param {array} $args Array of arguments + * @since 2.1.0 + * @return {array} New arguments + */ + $user_return_args = apply_filters( + 'ep_search_user_return_args', + [ + 'ID', + 'user_login', + 'user_nicename', + 'user_email', + 'user_url', + 'user_registered', + 'user_status', + 'display_name', + 'spam', + 'deleted', + 'terms', + 'meta', + ] + ); + + foreach ( $user_return_args as $key ) { + if ( isset( $user_array[ $key ] ) ) { + $user->$key = $user_array[ $key ]; + } + } + + $user->elasticsearch = true; // Super useful for debugging. + + $new_users[] = $user; + } + + return $new_users; + } + + /** + * Disables cache_results, adds header. + * + * @param WP_User_Query $query User query + */ + public function action_pre_get_users( $query ) { + /** + * Filter to skip user query integration + * + * @hook ep_skip_user_query_integration + * @param {bool} $skip True meanas skip query + * @param {WP_User_Query} $query User query + * @since 2.1.0 + * @return {boolean} New value + */ + if ( ! Indexables::factory()->get( 'user' )->elasticpress_enabled( $query ) || apply_filters( 'ep_skip_user_query_integration', false, $query ) ) { + return; + } + + if ( ! headers_sent() ) { + /** + * Manually setting a header as $wp_query isn't yet initialized + * when we call: add_filter('wp_headers', 'filter_wp_headers'); + */ + header( 'X-ElasticPress-Search: true' ); + } + } +} diff --git a/includes/classes/Indexable/User/SyncManager.php b/includes/classes/Indexable/User/SyncManager.php new file mode 100644 index 0000000..0bb4537 --- /dev/null +++ b/includes/classes/Indexable/User/SyncManager.php @@ -0,0 +1,122 @@ +get_elasticsearch_version() ) { + return; + } + + add_action( 'delete_user', [ $this, 'action_delete_user' ] ); + add_action( 'wpmu_delete_user', [ $this, 'action_delete_user' ] ); + add_action( 'profile_update', [ $this, 'action_sync_on_update' ] ); + add_action( 'user_register', [ $this, 'action_sync_on_update' ] ); + add_action( 'updated_user_meta', [ $this, 'action_queue_meta_sync' ], 10, 4 ); + add_action( 'added_user_meta', [ $this, 'action_queue_meta_sync' ], 10, 4 ); + add_action( 'deleted_user_meta', [ $this, 'action_queue_meta_sync' ], 10, 4 ); + + // @todo Handle deleted meta + } + + /** + * Dummy implementation of site unsetup method (for now) + */ + public function tear_down() { + } + + /** + * When whitelisted meta is updated/added/deleted, queue the object for reindex + * + * @param int $meta_id Meta id. + * @param int|array $object_id Object id. + * @param string $meta_key Meta key. + * @param string $meta_value Meta value. + */ + public function action_queue_meta_sync( $meta_id, $object_id, $meta_key, $meta_value ) { + if ( $this->kill_sync() ) { + return; + } + + $indexable = Indexables::factory()->get( 'user' ); + + $this->add_to_queue( $object_id ); + } + + /** + * Delete ES user when WP user is deleted + * + * @param int $user_id User ID + */ + public function action_delete_user( $user_id ) { + if ( $this->kill_sync() ) { + return; + } + + if ( ! current_user_can( 'edit_user', $user_id ) ) { + return; + } + + Indexables::factory()->get( 'user' )->delete( $user_id, false ); + } + + /** + * Sync ES index with what happened to the user being saved + * + * @param int $user_id User id. + */ + public function action_sync_on_update( $user_id ) { + if ( $this->kill_sync() ) { + return; + } + + if ( ! current_user_can( 'edit_user', $user_id ) ) { + return; + } + + /** + * Filter whether to kill sync for a particular user + * + * @hook ep_user_sync_kill + * @param {bool} $kill True means dont sync + * @param {int} $user_id User ID + * @since 2.1.0 + * @return {bool} New kill value + */ + if ( apply_filters( 'ep_user_sync_kill', false, $user_id ) ) { + return; + } + + /** + * Fires before adding user to sync queue + * + * @hook ep_sync_user_on_transition + * @param {int} $user_id User ID + * @since 2.1.0 + */ + do_action( 'ep_sync_user_on_transition', $user_id ); + + $this->add_to_queue( $user_id ); + } +} diff --git a/includes/classes/Indexable/User/User.php b/includes/classes/Indexable/User/User.php new file mode 100644 index 0000000..9daff7d --- /dev/null +++ b/includes/classes/Indexable/User/User.php @@ -0,0 +1,880 @@ +labels = [ + 'plural' => esc_html__( 'Users', 'elasticpress' ), + 'singular' => esc_html__( 'User', 'elasticpress' ), + ]; + } + + /** + * Instantiate the indexable SyncManager and QueryIntegration, the main responsibles for the WP integration. + * + * @return void + */ + public function setup() { + $this->sync_manager = new SyncManager( $this->slug ); + $this->query_integration = new QueryIntegration( $this->slug ); + } + + /** + * Format query vars into ES query + * + * @param array $query_vars WP_User_Query args. + * @param WP_User_Query $query User query object + * @return array + */ + public function format_args( $query_vars, $query ) { + global $wpdb; + + /** + * Handle `number` query var + */ + if ( ! empty( $query_vars['number'] ) ) { + $number = (int) $query_vars['number']; + + // ES have a maximum size allowed so we have to convert "-1" to a maximum size. + if ( -1 === $number ) { + /** + * Filter max result size if set to -1 + * + * @hook ep_max_results_window + * @since 2.1.0 + * @param {int} $window Max result window + * @return {int} New window + */ + $number = apply_filters( 'ep_max_results_window', 10000 ); + } + } else { + /** This filter is documented above */ + $number = apply_filters( 'ep_max_results_window', 10000 ); + } + + $formatted_args = [ + 'from' => 0, + 'size' => $number, + ]; + + $filter = [ + 'bool' => [ + 'must' => [], + ], + ]; + + $use_filters = false; + + /** + * Support `blog_id` query arg + */ + $blog_id = false; + if ( isset( $query_vars['blog_id'] ) ) { + $blog_id = (int) $query_vars['blog_id']; + } + + /** + * Support `role` query arg + */ + if ( ! empty( $blog_id ) ) { + // If a blog id is set, we will apply at least one filter for roles. + $use_filters = true; + + // If there are no specific roles named, make sure the user is a member of the site. + if ( empty( $query_vars['role'] ) && empty( $query_vars['role__in'] ) && empty( $query_vars['role__not_in'] ) ) { + $filter['bool']['must'][] = array( + 'exists' => array( + 'field' => 'capabilities.' . $blog_id . '.roles', + ), + ); + /** + * EP versions prior to 4.1.0 set non-existent roles as `0`. + */ + $filter['bool']['must_not'][] = array( + 'term' => array( + 'capabilities.' . $blog_id . '.roles' => 0, + ), + ); + } elseif ( ! empty( $query_vars['role'] ) ) { + $roles = (array) $query_vars['role']; + + foreach ( $roles as $role ) { + $filter['bool']['must'][] = array( + 'terms' => array( + 'capabilities.' . $blog_id . '.roles' => [ + strtolower( $role ), + ], + ), + ); + } + } else { + if ( ! empty( $query_vars['role__in'] ) ) { + $roles_in = (array) $query_vars['role__in']; + + $roles_in = array_map( 'strtolower', $roles_in ); + + $filter['bool']['must'][] = array( + 'terms' => array( + 'capabilities.' . $blog_id . '.roles' => $roles_in, + ), + ); + } + + if ( ! empty( $query_vars['role__not_in'] ) ) { + $roles_not_in = (array) $query_vars['role__not_in']; + + foreach ( $roles_not_in as $role ) { + $filter['bool']['must_not'][] = array( + 'terms' => array( + 'capabilities.' . $blog_id . '.roles' => [ + strtolower( $role ), + ], + ), + ); + } + } + } + } + + $meta_queries = []; + + /** + * Support `meta_key`, `meta_value`, and `meta_compare` + */ + if ( ! empty( $query_vars['meta_key'] ) ) { + $meta_query_array = [ + 'key' => $query_vars['meta_key'], + ]; + + if ( isset( $query_vars['meta_value'] ) ) { + $meta_query_array['value'] = $query_vars['meta_value']; + } + + if ( isset( $query_vars['meta_compare'] ) ) { + $meta_query_array['compare'] = $query_vars['meta_compare']; + } + + $meta_queries[] = $meta_query_array; + } + + /** + * 'meta_query' arg support. + */ + if ( ! empty( $query_vars['meta_query'] ) ) { + $meta_queries = array_merge( $meta_queries, $query_vars['meta_query'] ); + } + + if ( ! empty( $meta_queries ) ) { + $filter['bool']['must'][] = $this->build_meta_query( $meta_queries ); + + $use_filters = true; + } + + /** + * Support `fields` query var. + */ + if ( isset( $query_vars['fields'] ) && 'all' !== $query_vars['fields'] && 'all_with_meta' !== $query_vars['fields'] ) { + $fields = (array) $query_vars['fields']; + $id_position = array_search( 'id', $fields, true ); + if ( false !== $id_position ) { + $fields[ $id_position ] = 'ID'; + } + $formatted_args['_source'] = [ + 'includes' => $fields, + ]; + } + + /** + * Support `nicename` query var + */ + if ( ! empty( $query_vars['nicename'] ) ) { + $filter['bool']['must'][] = array( + 'terms' => array( + 'user_nicename' => [ + $query_vars['nicename'], + ], + ), + ); + + $use_filters = true; + } + + /** + * Support `nicename` query var + */ + if ( ! empty( $query_vars['nicename__not_in'] ) ) { + $filter['bool']['must'][] = [ + 'bool' => [ + 'must_not' => [ + [ + 'terms' => [ + 'user_nicename' => (array) $query_vars['nicename__not_in'], + ], + ], + ], + ], + ]; + + $use_filters = true; + } + + /** + * Support `nicename__in` query var + */ + if ( ! empty( $query_vars['nicename__in'] ) ) { + $filter['bool']['must'][] = array( + 'terms' => array( + 'user_nicename' => (array) $query_vars['nicename__in'], + ), + ); + + $use_filters = true; + } + + /** + * Support `login` query var + */ + if ( ! empty( $query_vars['login'] ) ) { + $filter['bool']['must'][] = array( + 'terms' => array( + 'user_login' => [ + $query_vars['login'], + ], + ), + ); + + $use_filters = true; + } + + /** + * Support `login__in` query var + */ + if ( ! empty( $query_vars['login__in'] ) ) { + $filter['bool']['must'][] = array( + 'terms' => array( + 'user_login' => (array) $query_vars['login__in'], + ), + ); + + $use_filters = true; + } + + /** + * Support `login__not_in` query var + */ + if ( ! empty( $query_vars['login__not_in'] ) ) { + $filter['bool']['must'][] = [ + 'bool' => [ + 'must_not' => [ + [ + 'terms' => [ + 'user_login' => (array) $query_vars['login__not_in'], + ], + ], + ], + ], + ]; + + $use_filters = true; + } + + /** + * Handle `offset` and `paged` query vars. Paged takes priority if both are set. + */ + if ( isset( $query_vars['offset'] ) ) { + $formatted_args['from'] = (int) $query_vars['offset']; + } + + if ( isset( $query_vars['paged'] ) && $query_vars['paged'] > 1 ) { + $formatted_args['from'] = $number * ( $query_vars['paged'] - 1 ); + } + + /** + * Support `include` parameter + */ + if ( ! empty( $query_vars['include'] ) ) { + $filter['bool']['must'][] = [ + 'bool' => [ + 'must' => [ + 'terms' => [ + 'ID' => array_values( (array) $query_vars['include'] ), + ], + ], + ], + ]; + + $use_filters = true; + } + + /** + * Support `exclude` parameter + */ + if ( ! empty( $query_vars['exclude'] ) ) { + $filter['bool']['must'][] = [ + 'bool' => [ + 'must_not' => [ + 'terms' => [ + 'ID' => array_values( (array) $query_vars['exclude'] ), + ], + ], + ], + ]; + + $use_filters = true; + } + + /** + * Need to support a few more params + * + * @todo Support the following parameters: + * + * $who + * $has_published_posts + */ + + /** + * Handle `search` query_var + */ + if ( ! empty( $query_vars['search'] ) ) { + + /** + * Remove *'s from beginning and end of user search string' + * + * @hook ep_user_search_remove_wildcards + * @param {boolean} $remove True to remove + * @param {array} $query Current query + * @param {array} $query_vars Query variables + * @since 2.1.0 + * @return {boolean} + */ + if ( apply_filters( 'ep_user_search_remove_wildcards', true, $query, $query_vars ) ) { + $query_vars['search'] = trim( $query_vars['search'], '*' ); + } + + $search_fields = ( ! empty( $query_vars['search_columns'] ) ) ? $query_vars['search_columns'] : []; + + if ( ! empty( $query_vars['search_fields'] ) ) { + $search_fields = array_merge( $search_fields, $query_vars['search_fields'] ); + } + + /** + * Handle `search_fields` query var and `search_columns`. search_columns is a bit too + * simplistic for our needs since we want to be able to search meta too. We just merge + * search columns into search_fields. search_fields overwrites search_columns. + */ + if ( ! empty( $search_fields ) ) { + $prepared_search_fields = []; + + // WP_User_Query uses shortened column names so we need to expand those. + if ( ! empty( $search_fields['login'] ) ) { + $prepared_search_fields['user_login'] = $search_fields['login']; + + unset( $search_fields['login'] ); + } + + if ( ! empty( $search_fields['url'] ) ) { + $prepared_search_fields['user_url'] = $search_fields['url']; + + unset( $search_fields['url'] ); + } + + if ( ! empty( $search_fields['nicename'] ) ) { + $prepared_search_fields['user_nicename'] = $search_fields['nicename']; + + unset( $search_fields['nicename'] ); + } + + if ( ! empty( $search_fields['email'] ) ) { + $prepared_search_fields['user_email'] = $search_fields['email']; + + unset( $search_fields['email'] ); + } + + if ( ! empty( $search_fields['meta'] ) ) { + $metas = (array) $search_fields['meta']; + + foreach ( $metas as $meta ) { + $prepared_search_fields[] = 'meta.' . $meta . '.value'; + } + + unset( $search_fields['meta'] ); + } + + $prepared_search_fields = array_merge( $search_fields, $prepared_search_fields ); + } else { + $prepared_search_fields = [ + 'user_login', + 'user_nicename', + 'display_name', + 'user_url', + 'user_email', + 'meta.first_name', + 'meta.last_name', + 'meta.nickname', + ]; + } + + /** + * Filter search fields in user query + * + * @hook ep_user_search_fields + * @param {array} $prepared_search_fields Prepared search fields + * @param {array} $query_vars Query variables + * @since 2.1.0 + * @return {array} Search fields + */ + $prepared_search_fields = apply_filters( 'ep_user_search_fields', $prepared_search_fields, $query_vars ); + + $search_algorithm = $this->get_search_algorithm( $query_vars['search'], $prepared_search_fields, $query_vars ); + $formatted_args['query'] = $search_algorithm->get_query( 'user', $query_vars['search'], $prepared_search_fields, $query_vars ); + } else { + $formatted_args['query']['match_all'] = [ + 'boost' => 1, + ]; + } + + if ( $use_filters ) { + $formatted_args['post_filter'] = $filter; + } + + /** + * Handle order and orderby + */ + if ( ! empty( $query_vars['order'] ) ) { + $order = trim( strtolower( $query_vars['order'] ) ); + } else { + $order = 'desc'; + } + + if ( empty( $query_vars['orderby'] ) && ( ! isset( $query_vars['search'] ) || '' === $query_vars['search'] ) ) { + $query_vars['orderby'] = 'user_login'; + } + + // Set sort type. + if ( ! empty( $query_vars['orderby'] ) ) { + $formatted_args['sort'] = $this->parse_orderby( $query_vars['orderby'], $order, $query_vars ); + } else { + // Default sort is to use the score (based on relevance). + $formatted_args['sort'] = array( + array( + '_score' => array( + 'order' => $order, + ), + ), + ); + } + + /** + * Filter formatted Elasticsearch user query (entire query) + * + * @hook ep_user_formatted_args_query + * @param {array} $formatted_args Formatted Elasticsearch query + * @param {array} $query_vars Query variables + * @param {array} $query Query part + * @since 2.1.0 + * @return {array} New query + */ + return apply_filters( 'ep_user_formatted_args', $formatted_args, $query_vars, $query ); + } + + /** + * Convert the alias to a properly-prefixed sort value. + * + * @param string $orderby Orderby query var + * @param string $default_order Order direction + * @param array $query_vars Query vars + * @return array + */ + public function parse_orderby( $orderby, $default_order, $query_vars ) { + /** + * More params to support + * + * @todo Need to support: + * + * include + * login__in + * nicename__in + * post_count + */ + + if ( ! is_array( $orderby ) ) { + $orderby = explode( ' ', $orderby ); + } + + $from_to = [ + 'relevance' => '_score', + 'user_login' => 'user_login.raw', + 'login' => 'user_login.raw', + 'id' => 'ID', + 'display_name' => 'display_name.sortable', + 'name' => 'display_name.sortable', + 'nicename' => 'user_nicename.raw', + 'user_nicename' => 'user_nicename.raw', + 'user_email' => 'user_email.raw', + 'email' => 'user_email.raw', + 'user_url' => 'user_url.raw', + 'url' => 'user_url.raw', + 'registered' => 'user_registered', + ]; + + $sort = []; + + if ( empty( $orderby ) ) { + return $sort; + } + + $unsupported_clauses = [ 'rand', 'include', 'login__in', 'nicename__in', 'post_count' ]; + + foreach ( $orderby as $key => $value ) { + if ( is_string( $key ) ) { + $orderby_clause = $key; + $order = $value; + } else { + $orderby_clause = $value; + $order = $default_order; + } + + if ( empty( $orderby_clause ) || in_array( $orderby_clause, $unsupported_clauses, true ) ) { + continue; + } + + if ( in_array( $orderby_clause, [ 'meta_value', 'meta_value_num' ], true ) ) { + if ( empty( $args['meta_key'] ) ) { + continue; + } else { + $from_to['meta_value'] = 'meta.' . $args['meta_key'] . '.raw'; + $from_to['meta_value_num'] = 'meta.' . $args['meta_key'] . '.long'; + } + } + + $orderby_clause = $from_to[ $orderby_clause ] ?? $orderby_clause; + + $sort[] = array( + $orderby_clause => array( + 'order' => $order, + ), + ); + } + + return $sort; + } + + /** + * Query DB for users + * + * @param array $args Query arguments + * @return array + */ + public function query_db( $args ) { + global $wpdb; + + $defaults = [ + 'number' => 350, + 'offset' => 0, + 'orderby' => 'ID', + 'order' => 'desc', + ]; + + if ( isset( $args['per_page'] ) ) { + $args['number'] = $args['per_page']; + } + + /** + * Filter query database arguments for user indexable + * + * @hook ep_user_query_db_args + * @param {array} $args Database query arguments + * @since 2.1.0 + * @return {array} New arguments + */ + $args = apply_filters( 'ep_user_query_db_args', wp_parse_args( $args, $defaults ) ); + + $args['order'] = trim( strtolower( $args['order'] ) ); + + if ( ! in_array( $args['order'], [ 'asc', 'desc' ], true ) ) { + $args['order'] = 'desc'; + } + + $orderby_args = sanitize_sql_orderby( "{$args['orderby']} {$args['order']}" ); + $orderby = $orderby_args ? sprintf( 'ORDER BY %s', $orderby_args ) : ''; + + /** + * WP_User_Query doesn't let us get users across all blogs easily. This is the best + * way to do that. + */ + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared + $objects = $wpdb->get_results( $wpdb->prepare( "SELECT SQL_CALC_FOUND_ROWS ID FROM {$wpdb->users} {$orderby} LIMIT %d, %d", (int) $args['offset'], (int) $args['number'] ) ); + + return [ + 'objects' => $objects, + 'total_objects' => ( 0 === count( $objects ) ) ? 0 : (int) $wpdb->get_var( 'SELECT FOUND_ROWS()' ), + ]; + } + + /** + * Generate the mapping array + * + * @return array + */ + public function generate_mapping() { + $es_version = Elasticsearch::factory()->get_elasticsearch_version(); + if ( empty( $es_version ) ) { + /** + * Filter fallback Elasticsearch version + * + * @since 2.1.0 + * @hook ep_fallback_elasticsearch_version + * @param {string} $version Fall back Elasticsearch version + * @return {string} New version + */ + $es_version = apply_filters( 'ep_fallback_elasticsearch_version', '2.0' ); + } + + $mapping_file = 'initial.php'; + + if ( version_compare( $es_version, '5.0', '<' ) ) { + $mapping_file = 'pre-5-0.php'; + } elseif ( version_compare( $es_version, '7.0', '>=' ) ) { + $mapping_file = '7-0.php'; + } + + /** + * Filter user indexable mapping file + * + * @hook ep_user_mapping_file + * @param {string} $file Path to file + * @since 2.1.0 + * @return {string} New file path + */ + $mapping = require apply_filters( 'ep_user_mapping_file', __DIR__ . '/../../../mappings/user/' . $mapping_file ); + + /** + * Filter user indexable mapping + * + * @hook ep_user_mapping + * @param {array} $mapping Mapping + * @since 2.1.0 + * @return {array} New mapping + */ + $mapping = apply_filters( 'ep_user_mapping', $mapping ); + + return $mapping; + } + + /** + * Prepare a user document for indexing + * + * @param int $user_id User id + * @return array + */ + public function prepare_document( $user_id ) { + $user = get_user_by( 'ID', $user_id ); + + if ( empty( $user ) ) { + return false; + } + + $user_args = [ + 'ID' => $user_id, + 'user_login' => $user->user_login, + 'user_email' => $user->user_email, + 'user_nicename' => $user->user_nicename, + 'spam' => $user->spam, + 'deleted' => $user->spam, + 'user_status' => $user->user_status, + 'display_name' => $user->display_name, + 'user_registered' => $user->user_registered, + 'user_url' => $user->user_url, + 'capabilities' => $this->prepare_capabilities( $user_id ), + 'meta' => $this->prepare_meta_types( $this->prepare_meta( $user_id ) ), + ]; + + /** + * Filter prepared user document before index + * + * @hook ep_user_sync_args + * @param {array} $user_args Document + * @param {int} $user_id User ID + * @since 2.1.0 + * @return {array} New document + */ + $user_args = apply_filters( 'ep_user_sync_args', $user_args, $user_id ); + + return $user_args; + } + + /** + * Prepare capabilities for indexing + * + * @param int $user_id User ID + * @return array + */ + public function prepare_capabilities( $user_id ) { + global $wpdb; + + if ( defined( 'EP_IS_NETWORK' ) && EP_IS_NETWORK ) { + $sites = Utils\get_sites(); + } else { + $sites = [ + [ + 'blog_id' => (int) get_current_blog_id(), + ], + ]; + } + + $prepared_roles = []; + + foreach ( $sites as $site ) { + $roles = (array) get_user_meta( $user_id, $wpdb->get_blog_prefix( $site['blog_id'] ) . 'capabilities', true ); + + if ( ! empty( $roles ) ) { + $prepared_roles[ (int) $site['blog_id'] ] = [ + 'roles' => array_keys( (array) $roles ), + ]; + } + } + + return $prepared_roles; + } + + /** + * Prepare meta to send to ES + * + * @param int $user_id User id + * @return array + */ + public function prepare_meta( $user_id ) { + /** + * Filter pre-prepare meta for a user + * + * @hook ep_prepare_user_meta_data + * @since 2.1.0 + * @param {array} $meta Meta data + * @param {int} $user_id User ID + * @return {array} New meta + */ + $meta = apply_filters( 'ep_prepare_user_meta_data', (array) get_user_meta( $user_id ), $user_id ); + + if ( empty( $meta ) ) { + /** + * Filter final list of prepared user meta. + * + * @hook ep_prepared_user_meta + * @param {array} $prepared_meta Prepared meta + * @param {integer} $user_id User ID + * @since 2.1.0 + * @return {array} Prepared meta + */ + return apply_filters( 'ep_prepared_user_meta', [], $user_id ); + } + + $prepared_meta = []; + + /** + * Filter indexable private meta for users + * + * @hook ep_prepare_user_meta_allowed_protected_keys + * @param {array} $meta Meta keys + * @param {int} $user_id User ID + * @since 2.1.0 + * @return {array} New meta array + */ + $allowed_protected_keys = apply_filters( 'ep_prepare_user_meta_allowed_protected_keys', [], $user_id ); + + /** + * Filter out excluded indexable public meta keys for users + * + * @hook ep_prepare_user_meta_excluded_public_keys + * @param {array} $meta Meta keys + * @param {int} $user_id User ID + * @since 2.1.0 + * @return {array} New meta array + */ + $excluded_public_keys = apply_filters( + 'ep_prepare_user_meta_excluded_public_keys', + [ + 'session_tokens', + ], + $user_id + ); + + foreach ( $meta as $key => $value ) { + + $allow_index = false; + + if ( is_protected_meta( $key ) ) { + + if ( true === $allowed_protected_keys || in_array( $key, $allowed_protected_keys, true ) ) { + $allow_index = true; + } + } else { + + if ( true !== $excluded_public_keys && ! in_array( $key, $excluded_public_keys, true ) ) { + $allow_index = true; + } + } + + /** + * Filter whether to whitelist a specific user meta key + * + * @hookep_prepare_user_meta_whitelist_key + * @param {bool} $index True to force index + * @param {string} $key User meta key + * @param {int} $user_id User ID + * @since 2.1.0 + * @return {bool} New index value + */ + if ( true === $allow_index || apply_filters( 'ep_prepare_user_meta_whitelist_key', false, $key, $user_id ) ) { + $prepared_meta[ $key ] = maybe_unserialize( $value ); + } + } + + /** + * Filter final list of prepared user meta. + * + * @hook ep_prepared_user_meta + * @param {array} $prepared_meta Prepared meta + * @param {integer} $user_id User ID + * @since 2.1.0 + * @return {array} Prepared meta + */ + return apply_filters( 'ep_prepared_user_meta', $prepared_meta, $user_id ); + } +} diff --git a/includes/functions/core.php b/includes/functions/core.php index 28a4f75..432db6b 100644 --- a/includes/functions/core.php +++ b/includes/functions/core.php @@ -30,6 +30,8 @@ function setup() { add_action( 'plugins_loaded', $n( 'maybe_load_features' ) ); + add_filter( 'ep_user_register_feature', '__return_false' ); + do_action( 'elasticpress_labs_loaded' ); } diff --git a/includes/mappings/user/7-0.php b/includes/mappings/user/7-0.php new file mode 100644 index 0000000..bd1d69c --- /dev/null +++ b/includes/mappings/user/7-0.php @@ -0,0 +1,286 @@ + array( + /** + * Filter number of Elasticsearch shards to use in indices + * + * @since 2.1.0 + * @hook ep_default_index_number_of_shards + * @param {int} $shards Number of shards + * @return {int} New number + */ + 'index.number_of_shards' => apply_filters( 'ep_default_index_number_of_shards', 5 ), + /** + * Filter number of Elasticsearch replicas to use in indices + * + * @since 2.1.0 + * @hook ep_default_index_number_of_replicas + * @param {int} $replicas Number of replicas + * @return {int} New number + */ + 'index.number_of_replicas' => apply_filters( 'ep_default_index_number_of_replicas', 1 ), + /** + * Filter Elasticsearch total field limit for users + * + * @since 2.1.0 + * @hook ep_user_total_field_limit + * @param {int} $number Number of fields + * @return {int} New number + */ + 'index.mapping.total_fields.limit' => apply_filters( 'ep_user_total_field_limit', 5000 ), + /** + * Filter Elasticsearch maximum shingle difference + * + * @since 2.1.0 + * @hook ep_max_shingle_diff + * @param {int} $number Max difference + * @return {int} New number + */ + 'index.max_shingle_diff' => apply_filters( 'ep_max_shingle_diff', 8 ), + /** + * Filter Elasticsearch max result window for users + * + * @since 2.1.0 + * @hook ep_user_max_result_window + * @param {int} $number Size of result window + * @return {int} New number + */ + 'index.max_result_window' => apply_filters( 'ep_user_max_result_window', 1000000 ), + /** + * Filter whether Elasticsearch ignores malformed fields or not. + * + * @since 2.1.0 + * @hook ep_ignore_malformed + * @param {bool} $ignore True for ignore + * @return {bool} New value + */ + 'index.mapping.ignore_malformed' => apply_filters( 'ep_ignore_malformed', true ), + 'analysis' => array( + 'analyzer' => array( + 'default' => array( + 'tokenizer' => 'standard', + 'filter' => array( 'ewp_word_delimiter', 'lowercase', 'stop', 'ewp_snowball' ), + /** + * Filter Elasticsearch default language in mapping + * + * @since 2.1.0 + * @hook ep_analyzer_language + * @param {string} $lang Default language + * @param {string} $lang_context Language context + * @return {string} New language + */ + 'language' => apply_filters( 'ep_analyzer_language', 'english', 'analyzer_default' ), + ), + 'shingle_analyzer' => array( + 'type' => 'custom', + 'tokenizer' => 'standard', + 'filter' => array( 'lowercase', 'shingle_filter' ), + ), + 'ewp_lowercase' => array( + 'type' => 'custom', + 'tokenizer' => 'keyword', + 'filter' => array( 'lowercase' ), + ), + ), + 'filter' => array( + 'shingle_filter' => array( + 'type' => 'shingle', + 'min_shingle_size' => 2, + 'max_shingle_size' => 5, + ), + 'ewp_word_delimiter' => array( + 'type' => 'word_delimiter_graph', + 'preserve_original' => true, + ), + 'ewp_snowball' => array( + 'type' => 'snowball', + /** + * Filter Elasticsearch default language in mapping + * + * @since 2.1.0 + * @hook ep_analyzer_language + * @param {string} $lang Default language + * @param {string} $lang_context Language context + * @return {string} New language + */ + 'language' => apply_filters( 'ep_analyzer_language', 'english', 'filter_ewp_snowball' ), + ), + 'edge_ngram' => array( + 'side' => 'front', + 'max_gram' => 10, + 'min_gram' => 3, + 'type' => 'edge_ngram', + ), + ), + 'normalizer' => array( + 'lowerasciinormalizer' => array( + 'type' => 'custom', + 'filter' => array( 'lowercase', 'asciifolding' ), + ), + ), + ), + ), + 'mappings' => array( + 'date_detection' => false, + 'dynamic_templates' => array( + array( + 'template_meta_types' => array( + 'path_match' => 'meta.*', + 'mapping' => array( + 'type' => 'object', + 'properties' => array( + 'value' => array( + 'type' => 'text', + 'fields' => array( + 'sortable' => array( + 'type' => 'keyword', + 'ignore_above' => 10922, + 'normalizer' => 'lowerasciinormalizer', + ), + 'raw' => array( + 'type' => 'keyword', + 'ignore_above' => 10922, + ), + ), + ), + 'raw' => array( /* Left for backwards compat */ + 'type' => 'keyword', + 'ignore_above' => 10922, + ), + 'long' => array( + 'type' => 'long', + ), + 'double' => array( + 'type' => 'double', + ), + 'boolean' => array( + 'type' => 'boolean', + ), + 'date' => array( + 'type' => 'date', + 'format' => 'yyyy-MM-dd', + ), + 'datetime' => array( + 'type' => 'date', + 'format' => 'yyyy-MM-dd HH:mm:ss', + ), + 'time' => array( + 'type' => 'date', + 'format' => 'HH:mm:ss', + ), + ), + ), + ), + ), + array( + 'template_capabilities' => array( + 'path_match' => 'capabilities.*', + 'mapping' => array( + 'type' => 'object', + 'properties' => array( + 'roles' => array( + 'type' => 'keyword', + ), + ), + ), + ), + ), + ), + 'properties' => array( + 'ID' => array( + 'type' => 'long', + ), + 'user_registered' => array( + 'type' => 'date', + 'format' => 'yyyy-MM-dd HH:mm:ss', + ), + 'user_nicename' => array( + 'type' => 'text', + 'fields' => array( + 'user_nicename' => array( + 'type' => 'text', + ), + 'raw' => array( + 'type' => 'keyword', + 'ignore_above' => 10922, + ), + ), + ), + 'user_login' => array( + 'type' => 'text', + 'fields' => array( + 'user_login' => array( + 'type' => 'text', + ), + 'raw' => array( + 'type' => 'keyword', + 'ignore_above' => 10922, + ), + ), + ), + 'display_name' => array( + 'type' => 'text', + 'fields' => array( + 'raw' => array( + 'type' => 'keyword', + 'ignore_above' => 10922, + ), + 'sortable' => array( + 'type' => 'keyword', + 'normalizer' => 'lowerasciinormalizer', + ), + ), + ), + 'user_email' => array( + 'type' => 'text', + 'fields' => array( + 'user_email' => array( + 'type' => 'text', + ), + 'raw' => array( + 'type' => 'keyword', + 'ignore_above' => 10922, + ), + ), + ), + 'capabilities' => array( + 'type' => 'object', + ), + 'user_url' => array( + 'type' => 'text', + 'fields' => array( + 'user_url' => array( + 'type' => 'text', + ), + 'raw' => array( + 'type' => 'keyword', + 'ignore_above' => 10922, + ), + ), + ), + 'status' => array( + 'type' => 'long', + ), + 'spam' => array( + 'type' => 'long', + ), + 'deleted' => array( + 'type' => 'long', + ), + 'meta' => array( + 'type' => 'object', + ), + ), + ), +); diff --git a/includes/mappings/user/initial.php b/includes/mappings/user/initial.php new file mode 100644 index 0000000..6e25a54 --- /dev/null +++ b/includes/mappings/user/initial.php @@ -0,0 +1,276 @@ + array( + /** + * Filter number of Elasticsearch shards to use in indices + * + * @since 2.1.0 + * @hook ep_default_index_number_of_shards + * @param {int} $shards Number of shards + * @return {int} New number + */ + 'index.number_of_shards' => apply_filters( 'ep_default_index_number_of_shards', 5 ), + /** + * Filter number of Elasticsearch replicas to use in indices + * + * @since 2.1.0 + * @hook ep_default_index_number_of_replicas + * @param {int} $replicas Number of replicas + * @return {int} New number + */ + 'index.number_of_replicas' => apply_filters( 'ep_default_index_number_of_replicas', 1 ), + /** + * Filter Elasticsearch total field limit for users + * + * @since 2.1.0 + * @hook ep_user_total_field_limit + * @param {int} $number Number of fields + * @return {int} New number + */ + 'index.mapping.total_fields.limit' => apply_filters( 'ep_user_total_field_limit', 5000 ), + /** + * Filter Elasticsearch max result window for users + * + * @since 2.1.0 + * @hook ep_user_max_result_window + * @param {int} $number Size of result window + * @return {int} New number + */ + 'index.max_result_window' => apply_filters( 'ep_user_max_result_window', 1000000 ), + 'analysis' => array( + 'analyzer' => array( + 'default' => array( + 'tokenizer' => 'standard', + 'filter' => array( 'standard', 'ewp_word_delimiter', 'lowercase', 'stop', 'ewp_snowball' ), + /** + * Filter Elasticsearch default language in mapping + * + * @since 2.1.0 + * @hook ep_analyzer_language + * @param {string} $lang Default language + * @param {string} $lang_context Language context + * @return {string} New language + */ + 'language' => apply_filters( 'ep_analyzer_language', 'english', 'analyzer_default' ), + ), + 'shingle_analyzer' => array( + 'type' => 'custom', + 'tokenizer' => 'standard', + 'filter' => array( 'lowercase', 'shingle_filter' ), + ), + 'ewp_lowercase' => array( + 'type' => 'custom', + 'tokenizer' => 'keyword', + 'filter' => array( 'lowercase' ), + ), + ), + 'filter' => array( + 'shingle_filter' => array( + 'type' => 'shingle', + 'min_shingle_size' => 2, + 'max_shingle_size' => 5, + ), + 'ewp_word_delimiter' => array( + 'type' => 'word_delimiter', + 'preserve_original' => true, + ), + 'ewp_snowball' => array( + 'type' => 'snowball', + /** + * Filter Elasticsearch default language in mapping + * + * @since 2.1.0 + * @hook ep_analyzer_language + * @param {string} $lang Default language + * @param {string} $lang_context Language context + * @return {string} New language + */ + 'language' => apply_filters( 'ep_analyzer_language', 'english', 'filter_ewp_snowball' ), + ), + 'edge_ngram' => array( + 'side' => 'front', + 'max_gram' => 10, + 'min_gram' => 3, + 'type' => 'edgeNGram', + ), + ), + 'normalizer' => array( + 'lowerasciinormalizer' => array( + 'type' => 'custom', + 'filter' => array( 'lowercase', 'asciifolding' ), + ), + ), + ), + ), + 'mappings' => array( + 'user' => array( + 'date_detection' => false, + 'dynamic_templates' => array( + array( + 'template_meta_types' => array( + 'path_match' => 'meta.*', + 'mapping' => array( + 'type' => 'object', + 'path' => 'full', + 'properties' => array( + 'value' => array( + 'type' => 'text', + 'fields' => array( + 'sortable' => array( + 'type' => 'keyword', + 'ignore_above' => 10922, + 'normalizer' => 'lowerasciinormalizer', + ), + 'raw' => array( + 'type' => 'keyword', + 'ignore_above' => 10922, + ), + ), + ), + 'raw' => array( /* Left for backwards compat */ + 'type' => 'keyword', + 'ignore_above' => 10922, + ), + 'long' => array( + 'type' => 'long', + ), + 'double' => array( + 'type' => 'double', + ), + 'boolean' => array( + 'type' => 'boolean', + ), + 'date' => array( + 'type' => 'date', + 'format' => 'yyyy-MM-dd', + ), + 'datetime' => array( + 'type' => 'date', + 'format' => 'yyyy-MM-dd HH:mm:ss', + ), + 'time' => array( + 'type' => 'date', + 'format' => 'HH:mm:ss', + ), + ), + ), + ), + ), + array( + 'template_capabilities' => array( + 'path_match' => 'capabilities.*', + 'mapping' => array( + 'type' => 'object', + 'path' => 'full', + 'properties' => array( + 'roles' => array( + 'type' => 'keyword', + ), + ), + ), + ), + ), + ), + '_all' => array( + 'analyzer' => 'simple', + ), + 'properties' => array( + 'ID' => array( + 'type' => 'long', + ), + 'user_registered' => array( + 'type' => 'date', + 'format' => 'YYYY-MM-dd HH:mm:ss', + ), + 'user_nicename' => array( + 'type' => 'text', + 'fields' => array( + 'user_nicename' => array( + 'type' => 'text', + ), + 'raw' => array( + 'type' => 'keyword', + 'ignore_above' => 10922, + ), + ), + ), + 'user_login' => array( + 'type' => 'text', + 'fields' => array( + 'user_login' => array( + 'type' => 'text', + ), + 'raw' => array( + 'type' => 'keyword', + 'ignore_above' => 10922, + ), + ), + ), + 'display_name' => array( + 'type' => 'text', + 'fields' => array( + 'raw' => array( + 'type' => 'keyword', + 'ignore_above' => 10922, + ), + 'sortable' => array( + 'type' => 'keyword', + 'ignore_above' => 10922, + 'normalizer' => 'lowerasciinormalizer', + ), + ), + ), + 'user_email' => array( + 'type' => 'text', + 'fields' => array( + 'user_email' => array( + 'type' => 'text', + ), + 'raw' => array( + 'type' => 'keyword', + 'ignore_above' => 10922, + ), + ), + ), + 'capabilities' => array( + 'type' => 'object', + ), + 'user_url' => array( + 'type' => 'text', + 'fields' => array( + 'user_url' => array( + 'type' => 'text', + ), + 'raw' => array( + 'type' => 'keyword', + 'ignore_above' => 10922, + ), + ), + ), + 'status' => array( + 'type' => 'long', + ), + 'spam' => array( + 'type' => 'long', + ), + 'deleted' => array( + 'type' => 'long', + ), + 'meta' => array( + 'type' => 'object', + ), + ), + ), + ), +); diff --git a/includes/mappings/user/pre-5-0.php b/includes/mappings/user/pre-5-0.php new file mode 100644 index 0000000..a691238 --- /dev/null +++ b/includes/mappings/user/pre-5-0.php @@ -0,0 +1,277 @@ + array( + /** + * Filter number of Elasticsearch shards to use in indices + * + * @since 2.1.0 + * @hook ep_default_index_number_of_shards + * @param {int} $shards Number of shards + * @return {int} New number + */ + 'index.number_of_shards' => apply_filters( 'ep_default_index_number_of_shards', 5 ), + /** + * Filter number of Elasticsearch replicas to use in indices + * + * @since 2.1.0 + * @hook ep_default_index_number_of_replicas + * @param {int} $replicas Number of replicas + * @return {int} New number + */ + 'index.number_of_replicas' => apply_filters( 'ep_default_index_number_of_replicas', 1 ), + /** + * Filter Elasticsearch total field limit for users + * + * @since 2.1.0 + * @hook ep_user_total_field_limit + * @param {int} $number Number of fields + * @return {int} New number + */ + 'index.mapping.total_fields.limit' => apply_filters( 'ep_user_total_field_limit', 5000 ), + /** + * Filter Elasticsearch max result window for users + * + * @since 2.1.0 + * @hook ep_user_max_result_window + * @param {int} $number Size of result window + * @return {int} New number + */ + 'index.max_result_window' => apply_filters( 'ep_user_max_result_window', 1000000 ), + 'analysis' => array( + 'analyzer' => array( + 'default' => array( + 'tokenizer' => 'standard', + 'filter' => array( 'standard', 'ewp_word_delimiter', 'lowercase', 'stop', 'ewp_snowball' ), + /** + * Filter Elasticsearch default language in mapping + * + * @since 2.1.0 + * @hook ep_analyzer_language + * @param {string} $lang Default language + * @param {string} $lang_context Language context + * @return {string} New language + */ + 'language' => apply_filters( 'ep_analyzer_language', 'english', 'analyzer_default' ), + ), + 'shingle_analyzer' => array( + 'type' => 'custom', + 'tokenizer' => 'standard', + 'filter' => array( 'lowercase', 'shingle_filter' ), + ), + 'ewp_lowercase' => array( + 'type' => 'custom', + 'tokenizer' => 'keyword', + 'filter' => array( 'lowercase' ), + ), + ), + 'filter' => array( + 'shingle_filter' => array( + 'type' => 'shingle', + 'min_shingle_size' => 2, + 'max_shingle_size' => 5, + ), + 'ewp_word_delimiter' => array( + 'type' => 'word_delimiter', + 'preserve_original' => true, + ), + 'ewp_snowball' => array( + 'type' => 'snowball', + /** + * Filter Elasticsearch default language in mapping + * + * @since 2.1.0 + * @hook ep_analyzer_language + * @param {string} $lang Default language + * @param {string} $lang_context Language context + * @return {string} New language + */ + 'language' => apply_filters( 'ep_analyzer_language', 'english', 'filter_ewp_snowball' ), + ), + 'edge_ngram' => array( + 'side' => 'front', + 'max_gram' => 10, + 'min_gram' => 3, + 'type' => 'edgeNGram', + ), + ), + 'normalizer' => array( + 'lowerasciinormalizer' => array( + 'type' => 'custom', + 'filter' => array( 'lowercase', 'asciifolding' ), + ), + ), + ), + ), + 'mappings' => array( + 'user' => array( + 'date_detection' => false, + 'dynamic_templates' => array( + array( + 'template_meta_types' => array( + 'path_match' => 'meta.*', + 'mapping' => array( + 'type' => 'object', + 'path' => 'full', + 'properties' => array( + 'value' => array( + 'type' => 'string', + 'fields' => array( + 'sortable' => array( + 'type' => 'string', + 'ignore_above' => 10922, + 'normalizer' => 'lowerasciinormalizer', + ), + 'raw' => array( + 'type' => 'string', + 'ignore_above' => 10922, + ), + ), + ), + 'raw' => array( /* Left for backwards compat */ + 'type' => 'string', + 'ignore_above' => 10922, + ), + 'long' => array( + 'type' => 'long', + ), + 'double' => array( + 'type' => 'double', + ), + 'boolean' => array( + 'type' => 'boolean', + ), + 'date' => array( + 'type' => 'date', + 'format' => 'yyyy-MM-dd', + ), + 'datetime' => array( + 'type' => 'date', + 'format' => 'yyyy-MM-dd HH:mm:ss', + ), + 'time' => array( + 'type' => 'date', + 'format' => 'HH:mm:ss', + ), + ), + ), + ), + ), + array( + 'template_capabilities' => array( + 'path_match' => 'capabilities.*', + 'mapping' => array( + 'type' => 'object', + 'path' => 'full', + 'properties' => array( + 'roles' => array( + 'type' => 'string', + ), + ), + ), + ), + ), + ), + '_all' => array( + 'analyzer' => 'simple', + ), + 'properties' => array( + 'ID' => array( + 'type' => 'long', + ), + 'user_registered' => array( + 'type' => 'date', + 'format' => 'YYYY-MM-dd HH:mm:ss', + ), + 'user_nicename' => array( + 'type' => 'string', + 'fields' => array( + 'user_nicename' => array( + 'type' => 'string', + ), + 'raw' => array( + 'type' => 'string', + 'ignore_above' => 10922, + ), + ), + ), + 'user_login' => array( + 'type' => 'string', + 'fields' => array( + 'user_login' => array( + 'type' => 'string', + ), + 'raw' => array( + 'type' => 'string', + 'ignore_above' => 10922, + ), + ), + ), + 'display_name' => array( + 'type' => 'string', + 'fields' => array( + 'raw' => array( + 'type' => 'string', + 'index' => 'not_analyzed', + 'ignore_above' => 10922, + ), + 'sortable' => array( + 'type' => 'string', + 'index' => 'not_analyzed', + 'analyzer' => 'ewp_lowercase', + ), + ), + ), + 'user_email' => array( + 'type' => 'string', + 'fields' => array( + 'user_email' => array( + 'type' => 'string', + ), + 'raw' => array( + 'type' => 'string', + 'ignore_above' => 10922, + ), + ), + ), + 'capabilities' => array( + 'type' => 'object', + ), + 'user_url' => array( + 'type' => 'string', + 'fields' => array( + 'user_url' => array( + 'type' => 'string', + ), + 'raw' => array( + 'type' => 'string', + 'ignore_above' => 10922, + ), + ), + ), + 'status' => array( + 'type' => 'long', + ), + 'spam' => array( + 'type' => 'long', + ), + 'deleted' => array( + 'type' => 'long', + ), + 'meta' => array( + 'type' => 'object', + ), + ), + ), + ), +); diff --git a/package-lock.json b/package-lock.json index 896701b..b1f1276 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11768,9 +11768,9 @@ "dev": true }, "node_modules/json5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", - "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, "bin": { "json5": "lib/cli.js" @@ -12176,9 +12176,9 @@ } }, "node_modules/loader-utils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz", - "integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", "dev": true, "dependencies": { "big.js": "^5.2.2", @@ -17298,9 +17298,9 @@ } }, "node_modules/tsconfig-paths/node_modules/json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", "dev": true, "dependencies": { "minimist": "^1.2.0" @@ -27070,9 +27070,9 @@ "dev": true }, "json5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", - "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true }, "jsx-ast-utils": { @@ -27354,9 +27354,9 @@ "dev": true }, "loader-utils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz", - "integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", "dev": true, "requires": { "big.js": "^5.2.2", @@ -31021,9 +31021,9 @@ }, "dependencies": { "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", "dev": true, "requires": { "minimist": "^1.2.0" diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 744a36d..fd28f90 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -1,4 +1,13 @@ [], + ]; +} +tests_add_filter( 'translations_api', __NAMESPACE__ . '\skip_translations_api' ); + require_once $_tests_dir . '/includes/functions.php'; require_once $_tests_dir . '/includes/bootstrap.php'; +require_once __DIR__ . '/phpunit/BaseTestCase.php'; +require_once __DIR__ . '/phpunit/factory/UserFactory.php'; \ No newline at end of file diff --git a/tests/phpunit/BaseTestCase.php b/tests/phpunit/BaseTestCase.php new file mode 100644 index 0000000..b760dcc --- /dev/null +++ b/tests/phpunit/BaseTestCase.php @@ -0,0 +1,39 @@ +setup_factory(); + parent::set_up(); + } + + /** + * Setup factory + */ + protected function setup_factory() { + $this->ep_factory = new \stdClass(); + $this->ep_factory->user = new UserFactory(); + } +} diff --git a/tests/phpunit/factory/UserFactory.php b/tests/phpunit/factory/UserFactory.php new file mode 100644 index 0000000..f786812 --- /dev/null +++ b/tests/phpunit/factory/UserFactory.php @@ -0,0 +1,37 @@ +get( 'user' )->index( $user_id, true ); + return $user_id; + } +} diff --git a/tests/phpunit/feature/TestBooleanSearchOperators.php b/tests/phpunit/feature/TestBooleanSearchOperators.php index 5861eef..dc2fd2d 100644 --- a/tests/phpunit/feature/TestBooleanSearchOperators.php +++ b/tests/phpunit/feature/TestBooleanSearchOperators.php @@ -21,7 +21,7 @@ class TestBooleanSearchOperators extends \WP_UnitTestCase { * * @since 1.2.0 */ - public function setUp() { + public function set_up() { $instance = new ElasticPressLabs\Feature\BooleanSearchOperators(); \ElasticPress\Features::factory()->register_feature($instance); } @@ -58,7 +58,7 @@ public function testBoxSummary() { $this->get_feature()->output_feature_box_summary(); $output = ob_get_clean(); - $this->assertContains( 'Allow boolean operators in search queries', $output ); + $this->assertStringContainsString( 'Allow boolean operators in search queries', $output ); } /** @@ -71,7 +71,7 @@ public function testBoxLong() { $this->get_feature()->output_feature_box_long(); $output = ob_get_clean(); - $this->assertContains( 'Allows users to search using the following boolean operators:', $output ); + $this->assertStringContainsString( 'Allows users to search using the following boolean operators:', $output ); } /** diff --git a/tests/phpunit/feature/TestCoAuthorsPlus.php b/tests/phpunit/feature/TestCoAuthorsPlus.php index dec78db..6b77247 100644 --- a/tests/phpunit/feature/TestCoAuthorsPlus.php +++ b/tests/phpunit/feature/TestCoAuthorsPlus.php @@ -21,7 +21,7 @@ class TestCoAuthorsPlus extends \WP_UnitTestCase { * * @since 1.1.0 */ - public function setUp() { + public function set_up() { $instance = new ElasticPressLabs\Feature\CoAuthorsPlus(); \ElasticPress\Features::factory()->register_feature($instance); } @@ -74,7 +74,7 @@ public function testBoxSummary() { $this->get_feature()->output_feature_box_summary(); $output = ob_get_clean(); - $this->assertContains( 'Add support for the Co-Authors Plus plugin in the Admin Post List screen by Author name', $output ); + $this->assertStringContainsString( 'Add support for the Co-Authors Plus plugin in the Admin Post List screen by Author name', $output ); } /** @@ -87,7 +87,7 @@ public function testBoxLong() { $this->get_feature()->output_feature_box_long(); $output = ob_get_clean(); - $this->assertContains( 'If using the Co-Authors Plus plugin and the Protected Content feature, enable this feature to visit the Admin Post List screen by Author namewp-admin/edit.php?author_name=<name>
and see correct results.', $output );
+ $this->assertStringContainsString( 'If using the Co-Authors Plus plugin and the Protected Content feature, enable this feature to visit the Admin Post List screen by Author name wp-admin/edit.php?author_name=<name>
and see correct results.', $output );
}
/**
diff --git a/tests/phpunit/feature/TestMetaKeyPattern.php b/tests/phpunit/feature/TestMetaKeyPattern.php
index 9d64f29..29ce746 100644
--- a/tests/phpunit/feature/TestMetaKeyPattern.php
+++ b/tests/phpunit/feature/TestMetaKeyPattern.php
@@ -22,7 +22,7 @@ class TestMetaKeyPattern extends \WP_UnitTestCase {
*
* @since 3.5
*/
- public function setUp() {
+ public function set_up() {
$instance = new ElasticPressLabs\Feature\MetaKeyPattern();
\ElasticPress\Features::factory()->register_feature($instance);
}
@@ -51,7 +51,7 @@ public function testBoxSummary() {
$this->get_feature()->output_feature_box_summary();
$output = ob_get_clean();
- $this->assertContains( 'Include or exclude meta key patterns.', $output );
+ $this->assertStringContainsString( 'Include or exclude meta key patterns.', $output );
}
public function testBoxLong() {
@@ -59,7 +59,7 @@ public function testBoxLong() {
$this->get_feature()->output_feature_box_long();
$output = ob_get_clean();
- $this->assertContains( 'This feature will give you the most control over the metadata indexed.', $output );
+ $this->assertStringContainsString( 'This feature will give you the most control over the metadata indexed.', $output );
}
public function testOutputFeatureBoxSettings() {
@@ -67,8 +67,8 @@ public function testOutputFeatureBoxSettings() {
$this->get_feature()->output_feature_box_settings();
$output = ob_get_clean();
- $this->assertContains( 'Allow patterns', $output );
- $this->assertContains( 'Deny patterns', $output );
+ $this->assertStringContainsString( 'Allow patterns', $output );
+ $this->assertStringContainsString( 'Deny patterns', $output );
}
public function testIsMatch() {
diff --git a/tests/phpunit/indexables/TestUser.php b/tests/phpunit/indexables/TestUser.php
new file mode 100644
index 0000000..7720711
--- /dev/null
+++ b/tests/phpunit/indexables/TestUser.php
@@ -0,0 +1,1550 @@
+suppress_errors();
+
+ \ElasticPress\register_indexable_posts();
+
+ $instance = new \ElasticPressLabs\Feature\Users();
+ ElasticPress\Features::factory()->register_feature( $instance );
+
+ ElasticPress\Features::factory()->activate_feature( 'users' );
+ ElasticPress\Features::factory()->setup_features();
+
+ ElasticPress\Indexables::factory()->get( 'user' )->delete_index();
+ ElasticPress\Indexables::factory()->get( 'user' )->put_mapping();
+
+ ElasticPress\Indexables::factory()->get( 'user' )->sync_manager->sync_queue = [];
+
+ $admin_id = $this->factory->user->create(
+ [
+ 'role' => 'administrator',
+ 'user_login' => 'test_admin',
+ 'first_name' => 'Mike',
+ 'last_name' => 'Mickey',
+ 'display_name' => 'mikey',
+ 'user_email' => 'mikey@gmail.com',
+ 'user_nicename' => 'mike',
+ 'user_url' => 'http://abc.com',
+ ]
+ );
+
+ grant_super_admin( $admin_id );
+
+ wp_set_current_user( $admin_id );
+
+ // Need to call this since it's hooked to init
+ ElasticPress\Features::factory()->get_registered_feature( 'users' )->search_setup();
+ }
+
+ /**
+ * Get User feature
+ *
+ * @return ElasticPress\Feature\Users
+ */
+ protected function get_feature() {
+ return ElasticPress\Features::factory()->get_registered_feature( 'users' );
+ }
+
+ /**
+ * Create and index users for testing
+ */
+ public function createAndIndexUsers() {
+ ElasticPress\Indexables::factory()->get( 'user' )->sync_manager->add_to_queue( 1 );
+
+ ElasticPress\Indexables::factory()->get( 'user' )->bulk_index( array_keys( ElasticPress\Indexables::factory()->get( 'user' )->sync_manager->sync_queue ) );
+
+ $user_1 = $this->ep_factory->user->create(
+ [
+ 'user_login' => 'user1-author',
+ 'role' => 'author',
+ 'first_name' => 'Dave',
+ 'last_name' => 'Smith',
+ 'display_name' => 'dave',
+ 'user_email' => 'dave@gmail.com',
+ 'user_url' => 'http://bac.com',
+ 'meta_input' => [
+ 'user_1_key' => 'value1',
+ 'user_num' => 5,
+ 'long_key' => 'here is a text field',
+ ],
+ ],
+ );
+
+ $user_2 = $this->ep_factory->user->create(
+ [
+ 'user_login' => 'user2-contributor',
+ 'role' => 'contributor',
+ 'first_name' => 'Zoey',
+ 'last_name' => 'Johnson',
+ 'display_name' => 'Zoey',
+ 'user_email' => 'zoey@gmail.com',
+ 'user_url' => 'http://google.com',
+ 'meta_input' => [
+ 'user_2_key' => 'value2',
+ ],
+ ]
+ );
+
+ $user_3 = $this->ep_factory->user->create(
+ [
+ 'user_login' => 'user3-editor',
+ 'role' => 'editor',
+ 'first_name' => 'Joe',
+ 'last_name' => 'Doe',
+ 'display_name' => 'joe',
+ 'user_email' => 'joe@gmail.com',
+ 'user_url' => 'http://cab.com',
+ 'meta_input' => [
+ 'user_3_key' => 'value3',
+ 'user_num' => 5,
+ ],
+ ]
+ );
+
+ ElasticPress\Elasticsearch::factory()->refresh_indices();
+
+ return [ $user_1, $user_2, $user_3 ];
+ }
+
+ /**
+ * Clean up after each test. Reset our mocks
+ */
+ public function tear_down() {
+ parent::tear_down();
+
+ $this->fired_actions = array();
+ }
+
+ /**
+ * Test a simple user sync
+ *
+ * @group user
+ */
+ public function testUserSync() {
+ add_action(
+ 'ep_sync_user_on_transition',
+ function() {
+ $this->fired_actions['ep_sync_user_on_transition'] = true;
+ }
+ );
+
+ ElasticPress\Indexables::factory()->get( 'user' )->sync_manager->sync_queue = [];
+
+ $user_id = $this->factory->user->create( array( 'role' => 'administrator' ) );
+
+ $this->assertEquals( 1, count( ElasticPress\Indexables::factory()->get( 'user' )->sync_manager->sync_queue ) );
+
+ ElasticPress\Indexables::factory()->get( 'user' )->index( $user_id );
+
+ ElasticPress\Elasticsearch::factory()->refresh_indices();
+
+ $this->assertTrue( ! empty( $this->fired_actions['ep_sync_user_on_transition'] ) );
+
+ $user = ElasticPress\Indexables::factory()->get( 'user' )->get( $user_id );
+ $this->assertTrue( ! empty( $user ) );
+ }
+
+ /**
+ * Test a simple user sync with meta
+ *
+ * @group user
+ */
+ public function testUserSyncMeta() {
+ $user_id = $this->factory->user->create( array( 'role' => 'administrator' ) );
+
+ update_user_meta( $user_id, 'new_meta', 'test' );
+
+ ElasticPress\Indexables::factory()->get( 'user' )->index( $user_id );
+
+ ElasticPress\Elasticsearch::factory()->refresh_indices();
+
+ $user = ElasticPress\Indexables::factory()->get( 'user' )->get( $user_id );
+
+ $this->assertEquals( 'test', $user['meta']['new_meta'][0]['value'] );
+ }
+
+ /**
+ * Test a simple user sync on meta update
+ *
+ * @group user
+ */
+ public function testUserSyncOnMetaUpdate() {
+ $user_id = $this->factory->user->create( array( 'role' => 'administrator' ) );
+
+ ElasticPress\Indexables::factory()->get( 'user' )->sync_manager->sync_queue = [];
+
+ update_user_meta( $user_id, 'test_key', true );
+
+ $this->assertEquals( 1, count( ElasticPress\Indexables::factory()->get( 'user' )->sync_manager->sync_queue ) );
+ $this->assertTrue( ! empty( ElasticPress\Indexables::factory()->get( 'user' )->sync_manager->add_to_queue( $user_id ) ) );
+ }
+
+ /**
+ * Test user sync kill. Note we can't actually check Elasticsearch here due to how the
+ * code is structured.
+ *
+ * @group user
+ */
+ public function testUserSyncKill() {
+ $created_user_id = $this->factory->user->create( array( 'role' => 'administrator' ) );
+
+ add_action(
+ 'ep_sync_user_on_transition',
+ function() {
+ $this->fired_actions['ep_sync_user_on_transition'] = true;
+ }
+ );
+
+ add_filter(
+ 'ep_user_sync_kill',
+ function( $kill, $user_id ) use ( $created_user_id ) {
+ if ( $created_user_id === $user_id ) {
+ return true;
+ }
+
+ return $kill;
+ },
+ 10,
+ 2
+ );
+
+ ElasticPress\Indexables::factory()->get( 'user' )->sync_manager->action_sync_on_update( $created_user_id );
+
+ $this->assertTrue( empty( $this->fired_actions['ep_sync_user_on_transition'] ) );
+ }
+
+ /**
+ * Test a basic user query with and without ElasticPress
+ *
+ * @group user
+ */
+ public function testBasicUserQuery() {
+ $this->createAndIndexUsers();
+
+ // First try without ES and make sure everything is right.
+ $user_query = new \WP_User_Query(
+ [
+ 'number' => 10,
+ ]
+ );
+
+ $this->assertArrayNotHasKey( 'elasticsearch_success', $user_query->query_vars );
+ $this->assertEquals( 5, count( $user_query->results ) );
+ $this->assertEquals( 5, $user_query->total_users );
+
+ // Now try with Elasticsearch.
+ $user_query = new \WP_User_Query(
+ [
+ 'ep_integrate' => true,
+ 'number' => 10,
+ ]
+ );
+
+ $this->assertTrue( $user_query->query_vars['elasticsearch_success'] );
+ $this->assertEquals( 5, count( $user_query->results ) );
+ $this->assertEquals( 5, $user_query->total_users );
+ }
+
+ /**
+ * Test user query number parameter
+ *
+ * @group user
+ */
+ public function testUserQueryNumber() {
+ $this->createAndIndexUsers();
+
+ $user_query = new \WP_User_Query(
+ [
+ 'ep_integrate' => true,
+ 'number' => 1,
+ ]
+ );
+
+ $this->assertEquals( 1, count( $user_query->results ) );
+ $this->assertEquals( 5, $user_query->total_users );
+
+ $this->ep_factory->user->create_many( 15 );
+ ElasticPress\Elasticsearch::factory()->refresh_indices();
+
+ $user_query = new \WP_User_Query(
+ [
+ 'ep_integrate' => true,
+ ]
+ );
+
+ $this->assertTrue( $user_query->query_vars['elasticsearch_success'] );
+ $this->assertEquals( 20, count( $user_query->results ) );
+ $this->assertEquals( 20, $user_query->total_users );
+ }
+
+ /**
+ * Test user query number parameter
+ *
+ * @group user
+ */
+ public function testUserQueryOffset() {
+ $this->createAndIndexUsers();
+
+ $user_query = new \WP_User_Query(
+ [
+ 'ep_integrate' => true,
+ 'number' => 1,
+ ]
+ );
+
+ $first_user = $user_query->results[0];
+
+ $user_query = new \WP_User_Query(
+ [
+ 'ep_integrate' => true,
+ 'number' => 1,
+ 'offset' => 1,
+ ]
+ );
+
+ $this->assertTrue( $user_query->query_vars['elasticsearch_success'] );
+ $this->assertNotEquals( $first_user->ID, $user_query->results[0]->ID );
+ }
+
+ /**
+ * Test user query paged parameter
+ *
+ * @group user
+ */
+ public function testUserQueryPaged() {
+ $this->createAndIndexUsers();
+
+ $user_query = new \WP_User_Query(
+ [
+ 'ep_integrate' => true,
+ 'number' => 1,
+ ]
+ );
+
+ $first_user = $user_query->results[0];
+
+ $user_query = new \WP_User_Query(
+ [
+ 'ep_integrate' => true,
+ 'number' => 1,
+ 'paged' => 2,
+ ]
+ );
+
+ $this->assertTrue( $user_query->query_vars['elasticsearch_success'] );
+ $this->assertNotEquals( $first_user->ID, $user_query->results[0]->ID );
+ }
+
+ /**
+ * Test user query role paramter
+ *
+ * @group user
+ */
+ public function testUserQueryRole() {
+ $this->createAndIndexUsers();
+
+ $user_query = new \WP_User_Query(
+ [
+ 'ep_integrate' => true,
+ 'role' => 'editor',
+ ]
+ );
+
+ $this->assertTrue( $user_query->query_vars['elasticsearch_success'] );
+ $this->assertEquals( 1, $user_query->total_users );
+ $this->assertTrue( in_array( 'editor', $user_query->results[0]->roles, true ) );
+ }
+
+ /**
+ * Test user query include parameter
+ *
+ * @group user
+ */
+ public function testUserInclude() {
+ $this->createAndIndexUsers();
+
+ $user_query = new \WP_User_Query(
+ [
+ 'ep_integrate' => true,
+ 'include' => [ 1 ],
+ ]
+ );
+
+ $this->assertTrue( $user_query->query_vars['elasticsearch_success'] );
+ $this->assertEquals( 1, $user_query->total_users );
+ $this->assertEquals( 1, $user_query->results[0]->ID );
+ }
+
+ /**
+ * Test user query exclude parameter
+ *
+ * @group user
+ */
+ public function testUserExclude() {
+ $this->createAndIndexUsers();
+
+ $user_query = new \WP_User_Query(
+ [
+ 'ep_integrate' => true,
+ 'exclude' => [ 1 ],
+ ]
+ );
+
+ $this->assertTrue( $user_query->query_vars['elasticsearch_success'] );
+ $this->assertEquals( 4, $user_query->total_users );
+ }
+
+ /**
+ * Test user query login parameter
+ *
+ * @group user
+ */
+ public function testUserQueryLogin() {
+ $this->createAndIndexUsers();
+
+ $user_query = new \WP_User_Query(
+ [
+ 'ep_integrate' => true,
+ 'login' => 'test_admin',
+ ]
+ );
+
+ $this->assertTrue( $user_query->query_vars['elasticsearch_success'] );
+ $this->assertEquals( 1, $user_query->total_users );
+ $this->assertEquals( 'test_admin', $user_query->results[0]->user_login );
+ }
+
+ /**
+ * Test user query login__in paramter
+ *
+ * @group user
+ */
+ public function testUserQueryLoginIn() {
+ $this->createAndIndexUsers();
+
+ $user_query = new \WP_User_Query(
+ [
+ 'ep_integrate' => true,
+ 'login__in' => [ 'test_admin' ],
+ ]
+ );
+
+ $this->assertTrue( $user_query->query_vars['elasticsearch_success'] );
+ $this->assertEquals( 1, $user_query->total_users );
+ $this->assertEquals( 'test_admin', $user_query->results[0]->user_login );
+ }
+
+ /**
+ * Test user query login__not_in paramter
+ *
+ * @group user
+ */
+ public function testUserQueryLoginNotIn() {
+ $this->createAndIndexUsers();
+
+ $user_query = new \WP_User_Query(
+ [
+ 'ep_integrate' => true,
+ 'login__not_in' => [ 'test_admin' ],
+ ]
+ );
+
+ $this->assertTrue( $user_query->query_vars['elasticsearch_success'] );
+ $this->assertEquals( 4, $user_query->total_users );
+ }
+
+ /**
+ * Test user query nicename parameter
+ *
+ * @group user
+ */
+ public function testUserQueryNicename() {
+ $this->createAndIndexUsers();
+
+ $user_query = new \WP_User_Query(
+ [
+ 'ep_integrate' => true,
+ 'nicename' => 'mike',
+ ]
+ );
+
+ $this->assertTrue( $user_query->query_vars['elasticsearch_success'] );
+ $this->assertEquals( 1, $user_query->total_users );
+ $this->assertEquals( 'mike', $user_query->results[0]->user_nicename );
+ }
+
+ /**
+ * Test user query nicename__in parameter
+ *
+ * @group user
+ */
+ public function testUserQueryNicenameIn() {
+ $this->createAndIndexUsers();
+
+ $user_query = new \WP_User_Query(
+ [
+ 'ep_integrate' => true,
+ 'nicename__in' => [ 'mike' ],
+ ]
+ );
+
+ $this->assertTrue( $user_query->query_vars['elasticsearch_success'] );
+ $this->assertEquals( 1, $user_query->total_users );
+ $this->assertEquals( 'mike', $user_query->results[0]->user_nicename );
+ }
+
+ /**
+ * Test user query nicename__in parameter
+ *
+ * @group user
+ */
+ public function testUserQueryNicenameNotIn() {
+ $this->createAndIndexUsers();
+
+ $user_query = new \WP_User_Query(
+ [
+ 'ep_integrate' => true,
+ 'nicename__not_in' => [ 'mike' ],
+ ]
+ );
+
+ $this->assertTrue( $user_query->query_vars['elasticsearch_success'] );
+ $this->assertEquals( 4, $user_query->total_users );
+ }
+
+ /**
+ * Test user query role__not_in paramter
+ *
+ * @group user
+ */
+ public function testUserQueryRoleNotIn() {
+ $this->createAndIndexUsers();
+
+ $user_query = new \WP_User_Query(
+ [
+ 'ep_integrate' => true,
+ 'role__not_in' => [ 'editor' ],
+ ]
+ );
+
+ $this->assertTrue( $user_query->query_vars['elasticsearch_success'] );
+
+ foreach ( $user_query->results as $user ) {
+ $this->assertFalse( in_array( 'editor', $user_query->results[0]->roles, true ) );
+ }
+ }
+
+ /**
+ * Test user query role__in paramter
+ *
+ * @group user
+ */
+ public function testUserQueryRoleIn() {
+ $this->createAndIndexUsers();
+
+ $user_query = new \WP_User_Query(
+ [
+ 'ep_integrate' => true,
+ 'role__in' => [
+ 'editor',
+ 'author',
+ ],
+ ]
+ );
+
+ $this->assertTrue( $user_query->query_vars['elasticsearch_success'] );
+
+ foreach ( $user_query->results as $user ) {
+ $this->assertTrue( ( in_array( 'editor', $user_query->results[0]->roles, true ) || in_array( 'author', $user_query->results[0]->roles, true ) ) );
+ }
+ }
+
+ /**
+ * Test user query orderby paramter where we are ordering by display name
+ *
+ * @group user
+ */
+ public function testUserQueryOrderbyDisplayName() {
+ $users_id = $this->createAndIndexUsers();
+
+ $user_query = new \WP_User_Query(
+ [
+ 'ep_integrate' => true,
+ 'orderby' => 'display_name',
+ ]
+ );
+
+ $this->assertTrue( $user_query->query_vars['elasticsearch_success'] );
+
+ $users_id_fetched = wp_list_pluck( $user_query->results, 'ID' );
+
+ $this->assertCount( 5, $user_query->results );
+
+ foreach ( $users_id as $user_id ) {
+ $this->assertContains( $user_id, $users_id_fetched );
+ }
+
+ $users_display_name_fetched = wp_list_pluck( $user_query->results, 'display_name' );
+
+ $this->assertEquals( 'admin', $users_display_name_fetched[0] );
+ $this->assertEquals( 'Zoey', $users_display_name_fetched[4] );
+
+ }
+
+ /**
+ * Test order by display_name in format_args().
+ *
+ * We should not use a text/string field to sort
+ * in Elasticsearch.
+ *
+ * @group user
+ */
+ public function testFormatArgsOrderByDisplayName() {
+ $user = new \ElasticPress\Indexable\User\User();
+
+ $user_query = new \WP_User_Query();
+
+ $args = $user->format_args(
+ [
+ 'orderby' => 'display_name',
+ ],
+ $user_query
+ );
+
+ $this->assertArrayHasKey( 'display_name.sortable', $args['sort'][0] );
+ $this->assertArrayNotHasKey( 'display_name', $args['sort'][0] );
+
+ $args = $user->format_args(
+ [
+ 'orderby' => 'name',
+ ],
+ $user_query
+ );
+
+ $this->assertArrayHasKey( 'display_name.sortable', $args['sort'][0] );
+ $this->assertArrayNotHasKey( 'display_name', $args['sort'][0] );
+ }
+
+ /**
+ * Test user query orderby paramter where we are ordering by user_nicename
+ *
+ * @group user
+ */
+ public function testUserQueryOrderbyUserNicename() {
+ $users_id = $this->createAndIndexUsers();
+
+ $user_query = new \WP_User_Query(
+ [
+ 'ep_integrate' => true,
+ 'orderby' => 'user_nicename',
+ ]
+ );
+
+ $this->assertTrue( $user_query->query_vars['elasticsearch_success'] );
+
+ $users_id_fetched = wp_list_pluck( $user_query->results, 'ID' );
+
+ $this->assertCount( 5, $user_query->results );
+
+ foreach ( $users_id as $user_id ) {
+ $this->assertContains( $user_id, $users_id_fetched );
+ }
+
+ $users_display_name_fetched = wp_list_pluck( $user_query->results, 'display_name' );
+
+ // Check if 'admin' is the first user
+ $this->assertEquals( 'admin', $users_display_name_fetched[0] );
+ }
+
+ /**
+ * Test order by user_nicename in format_args().
+ *
+ * We should not use a text/string field to sort
+ * in Elasticsearch.
+ *
+ * @return void * @group user
+ */
+ public function testFormatArgsOrderByUserNicename() {
+ $user = new \ElasticPress\Indexable\User\User();
+
+ $user_query = new \WP_User_Query();
+
+ $args = $user->format_args(
+ [
+ 'orderby' => 'user_nicename',
+ ],
+ $user_query
+ );
+
+ $this->assertArrayHasKey( 'user_nicename.raw', $args['sort'][0] );
+ $this->assertArrayNotHasKey( 'user_nicename', $args['sort'][0] );
+
+ $args = $user->format_args(
+ [
+ 'orderby' => 'nicename',
+ ],
+ $user_query
+ );
+
+ $this->assertArrayHasKey( 'user_nicename.raw', $args['sort'][0] );
+ $this->assertArrayNotHasKey( 'user_nicename', $args['sort'][0] );
+ }
+
+ /**
+ * Test user query orderby parameter where we are ordering by user_email
+ *
+ * @group user
+ */
+ public function testUserQueryOrderbyUserEmail() {
+ $users_id = $this->createAndIndexUsers();
+
+ $user_query = new \WP_User_Query(
+ [
+ 'ep_integrate' => true,
+ 'orderby' => 'user_email',
+ ]
+ );
+
+ $this->assertTrue( $user_query->query_vars['elasticsearch_success'] );
+
+ $users_id_fetched = wp_list_pluck( $user_query->results, 'ID' );
+
+ $this->assertCount( 5, $user_query->results );
+
+ foreach ( $users_id as $user_id ) {
+ $this->assertContains( $user_id, $users_id_fetched );
+ }
+
+ $users_display_name_fetched = wp_list_pluck( $user_query->results, 'display_name' );
+
+ // Check if 'admin' is the first user
+ $this->assertEquals( 'admin', $users_display_name_fetched[0] );
+ }
+
+ /**
+ * Test order by user_email in format_args().
+ *
+ * We should not use a text/string field to sort
+ * in Elasticsearch.
+ *
+ * @return void
+ * @group user
+ */
+ public function testFormatArgsOrderByUserEmail() {
+ $user = new \ElasticPress\Indexable\User\User();
+
+ $user_query = new \WP_User_Query();
+
+ $args = $user->format_args(
+ [
+ 'orderby' => 'user_email',
+ ],
+ $user_query
+ );
+
+ $this->assertArrayHasKey( 'user_email.raw', $args['sort'][0] );
+ $this->assertArrayNotHasKey( 'user_email', $args['sort'][0] );
+
+ $args = $user->format_args(
+ [
+ 'orderby' => 'user_email',
+ ],
+ $user_query
+ );
+
+ $this->assertArrayHasKey( 'user_email.raw', $args['sort'][0] );
+ $this->assertArrayNotHasKey( 'user_email', $args['sort'][0] );
+ }
+
+ /**
+ * Test user query orderby parameter where we are ordering by user_url
+ *
+ * @group user
+ */
+ public function testUserQueryOrderbyUserUrl() {
+ $users_id = $this->createAndIndexUsers();
+
+ $user_query = new \WP_User_Query(
+ [
+ 'ep_integrate' => true,
+ 'orderby' => 'user_url',
+ ]
+ );
+
+ $this->assertTrue( $user_query->query_vars['elasticsearch_success'] );
+
+ $users_id_fetched = wp_list_pluck( $user_query->results, 'ID' );
+
+ $this->assertCount( 5, $user_query->results );
+
+ foreach ( $users_id as $user_id ) {
+ $this->assertContains( $user_id, $users_id_fetched );
+ }
+
+ $users_display_name_fetched = wp_list_pluck( $user_query->results, 'display_name' );
+
+ $this->assertEquals( 'mikey', $users_display_name_fetched[0] );
+ }
+
+ /**
+ * Test order by user_url in format_args().
+ *
+ * We should not use a text/string field to sort
+ * in Elasticsearch.
+ *
+ * @return void
+ * @group user
+ */
+ public function testFormatArgsOrderByUserUrl() {
+ $user = new \ElasticPress\Indexable\User\User();
+
+ $user_query = new \WP_User_Query();
+
+ $args = $user->format_args(
+ [
+ 'orderby' => 'user_url',
+ ],
+ $user_query
+ );
+
+ $this->assertArrayHasKey( 'user_url.raw', $args['sort'][0] );
+ $this->assertArrayNotHasKey( 'user_url', $args['sort'][0] );
+
+ $args = $user->format_args(
+ [
+ 'orderby' => 'user_url',
+ ],
+ $user_query
+ );
+
+ $this->assertArrayHasKey( 'user_url.raw', $args['sort'][0] );
+ $this->assertArrayNotHasKey( 'user_url', $args['sort'][0] );
+ }
+
+ /**
+ * Test user query orderby paramter where we are ordering by ID
+ *
+ * @group user
+ */
+ public function testUserQueryOrderbyID() {
+ $this->createAndIndexUsers();
+
+ $user_query = new \WP_User_Query(
+ [
+ 'ep_integrate' => true,
+ 'orderby' => 'ID',
+ ]
+ );
+
+ $this->assertTrue( $user_query->query_vars['elasticsearch_success'] );
+
+ foreach ( $user_query->results as $key => $user ) {
+ if ( ! empty( $user_query->results[ $key - 1 ] ) ) {
+ $this->assertTrue( $user_query->results[ $key - 1 ]->ID < $user->ID );
+ }
+ }
+ }
+
+ /**
+ * Test user query orderby paramter where we are ordering by email
+ *
+ * @group user
+ */
+ public function testUserQueryOrderbyEmail() {
+ $this->createAndIndexUsers();
+
+ $user_query = new \WP_User_Query(
+ [
+ 'ep_integrate' => true,
+ 'orderby' => 'email',
+ ]
+ );
+
+ $this->assertTrue( $user_query->query_vars['elasticsearch_success'] );
+
+ foreach ( $user_query->results as $key => $user ) {
+ if ( ! empty( $user_query->results[ $key - 1 ] ) ) {
+ $this->assertTrue( strcasecmp( $user_query->results[ $key - 1 ]->user_email, $user->user_email ) < 0 );
+ }
+ }
+ }
+
+ /**
+ * Test user query order parameter where we are ordering by ID descending
+ *
+ * @group user
+ */
+ public function testUserQueryOrderDesc() {
+ $this->createAndIndexUsers();
+
+ $user_query = new \WP_User_Query(
+ [
+ 'ep_integrate' => true,
+ 'orderby' => 'ID',
+ 'order' => 'desc',
+ ]
+ );
+
+ $this->assertTrue( $user_query->query_vars['elasticsearch_success'] );
+
+ foreach ( $user_query->results as $key => $user ) {
+ if ( ! empty( $user_query->results[ $key - 1 ] ) ) {
+ $this->assertTrue( $user_query->results[ $key - 1 ]->ID > $user->ID );
+ }
+ }
+ }
+
+ /**
+ * Test meta query with simple args
+ *
+ */
+ public function testUserMetaQuerySimple() {
+ $this->createAndIndexUsers();
+
+ // Value does not exist so should return nothing
+ $user_query = new \WP_User_Query(
+ [
+ 'ep_integrate' => true,
+ 'meta_key' => 'user_1_key',
+ 'meta_value' => 'value5',
+ ]
+ );
+
+ $this->assertTrue( $user_query->query_vars['elasticsearch_success'] );
+ $this->assertEquals( 0, $user_query->total_users );
+
+ // This value exists
+ $user_query = new \WP_User_Query(
+ [
+ 'ep_integrate' => true,
+ 'meta_key' => 'user_1_key',
+ 'meta_value' => 'value1',
+ ]
+ );
+
+ $this->assertEquals( 1, $user_query->total_users );
+ $this->assertEquals( 'value1', get_user_meta( $user_query->results[0]->ID, 'user_1_key', true ) );
+ }
+
+ /**
+ * Test meta query with simple args and meta_compare does not equal
+ *
+ */
+ public function testUserMetaQuerySimpleCompare() {
+ $this->createAndIndexUsers();
+
+ $user_query = new \WP_User_Query(
+ [
+ 'ep_integrate' => true,
+ 'meta_key' => 'user_1_key',
+ 'meta_value' => 'value1',
+ 'meta_compare' => '!=',
+ ]
+ );
+
+ $this->assertTrue( $user_query->query_vars['elasticsearch_success'] );
+ $this->assertEquals( 4, $user_query->total_users );
+ }
+
+ /**
+ * Test meta query with no compare
+ *
+ */
+ public function testUserMetaQueryNoCompare() {
+ $this->createAndIndexUsers();
+
+ $user_query = new \WP_User_Query(
+ [
+ 'ep_integrate' => true,
+ 'meta_query' => [
+ [
+ 'key' => 'user_1_key',
+ 'value' => 'value1',
+ ],
+ ],
+ ]
+ );
+
+ $this->assertTrue( $user_query->query_vars['elasticsearch_success'] );
+ $this->assertEquals( 1, $user_query->total_users );
+ $this->assertEquals( 'value1', get_user_meta( $user_query->results[0]->ID, 'user_1_key', true ) );
+ }
+
+ /**
+ * Test meta query compare equals
+ *
+ */
+ public function testUserMetaQueryCompareEquals() {
+ $this->createAndIndexUsers();
+
+ $user_query = new \WP_User_Query(
+ [
+ 'ep_integrate' => true,
+ 'meta_query' => [
+ [
+ 'key' => 'user_2_key',
+ 'value' => 'value2',
+ 'compare' => '=',
+ ],
+ ],
+ ]
+ );
+
+ $this->assertTrue( $user_query->query_vars['elasticsearch_success'] );
+ $this->assertEquals( 1, $user_query->total_users );
+ $this->assertEquals( 'value2', get_user_meta( $user_query->results[0]->ID, 'user_2_key', true ) );
+ }
+
+ /**
+ * Test meta query with multiple statements
+ *
+ */
+ public function testUserMetaQueryMulti() {
+ $this->createAndIndexUsers();
+
+ $user_query = new \WP_User_Query(
+ [
+ 'ep_integrate' => true,
+ 'meta_query' => [
+ [
+ 'key' => 'user_num',
+ 'value' => 5,
+ ],
+ [
+ 'key' => 'user_1_key',
+ 'value' => 'value1',
+ ],
+ ],
+ ]
+ );
+
+ $this->assertTrue( $user_query->query_vars['elasticsearch_success'] );
+ $this->assertEquals( 1, $user_query->total_users );
+ $this->assertEquals( 'value1', get_user_meta( $user_query->results[0]->ID, 'user_1_key', true ) );
+ }
+
+ /**
+ * Test meta query with multiple statements and relation OR
+ *
+ */
+ public function testUserMetaQueryMultiRelationOr() {
+ $this->createAndIndexUsers();
+
+ $user_query = new \WP_User_Query(
+ [
+ 'ep_integrate' => true,
+ 'meta_query' => [
+ [
+ 'key' => 'user_num',
+ 'value' => 5,
+ ],
+ [
+ 'key' => 'user_1_key',
+ 'value' => 'value1',
+ ],
+ 'relation' => 'or',
+ ],
+ ]
+ );
+
+ $this->assertTrue( $user_query->query_vars['elasticsearch_success'] );
+ $this->assertEquals( 2, $user_query->total_users );
+ }
+
+ /**
+ * Test meta query with multiple statements and relation AND
+ *
+ */
+ public function testUserMetaQueryMultiRelationAnd() {
+ $this->createAndIndexUsers();
+
+ $user_query = new \WP_User_Query(
+ [
+ 'ep_integrate' => true,
+ 'meta_query' => [
+ [
+ 'key' => 'user_num',
+ 'value' => 5,
+ ],
+ [
+ 'key' => 'user_1_key',
+ 'value' => 'value1',
+ ],
+ 'relation' => 'and',
+ ],
+ ]
+ );
+
+ $this->assertTrue( $user_query->query_vars['elasticsearch_success'] );
+ $this->assertEquals( 1, $user_query->total_users );
+ }
+
+ /**
+ * Test basic user search
+ *
+ */
+ public function testBasicUserSearch() {
+ $this->createAndIndexUsers();
+
+ $user_query = new \WP_User_Query(
+ [
+ 'search' => 'joe',
+ ]
+ );
+
+ $this->assertTrue( $user_query->query_vars['elasticsearch_success'] );
+ $this->assertEquals( 1, $user_query->total_users );
+ $this->assertEquals( 'user3-editor', $user_query->results[0]->user_login );
+ }
+
+ /**
+ * Test basic user search via user login
+ *
+ */
+ public function testBasicUserSearchUserLogin() {
+ $this->createAndIndexUsers();
+
+ $user_query = new \WP_User_Query(
+ [
+ 'search' => 'joe',
+ ]
+ );
+
+ $this->assertTrue( $user_query->query_vars['elasticsearch_success'] );
+ $this->assertEquals( 1, $user_query->total_users );
+ $this->assertEquals( 'user3-editor', $user_query->results[0]->user_login );
+ }
+
+ /**
+ * Test basic user search via user url
+ *
+ */
+ public function testBasicUserSearchUserUrl() {
+ $this->createAndIndexUsers();
+
+ $user_query = new \WP_User_Query(
+ [
+ 'search' => 'http://google.com',
+ 'search_fields' => [
+ 'user_url.raw',
+ ],
+ ]
+ );
+
+ $this->assertTrue( $user_query->query_vars['elasticsearch_success'] );
+ $this->assertEquals( 1, $user_query->total_users );
+ $this->assertEquals( 'user2-contributor', $user_query->results[0]->user_login );
+ }
+
+ /**
+ * Test basic user search via meta
+ *
+ */
+ public function testBasicUserSearchMeta() {
+ $this->createAndIndexUsers();
+
+ $user_query = new \WP_User_Query(
+ [
+ 'search' => 'test field',
+ 'search_fields' => [
+ 'meta.long_key.value',
+ ],
+ ]
+ );
+
+ $this->assertTrue( $user_query->query_vars['elasticsearch_success'] );
+ $this->assertEquals( 1, $user_query->total_users );
+ $this->assertEquals( 'user1-author', $user_query->results[0]->user_login );
+ }
+
+ /**
+ * Tests a single field in the fields parameters for user queries.
+ */
+ public function testSingleUserFieldQuery() {
+ $this->createAndIndexUsers();
+
+ // First, get the IDs of the users.
+ $user_query = new \WP_User_Query(
+ [
+ 'number' => 5,
+ 'fields' => 'ID',
+ ]
+ );
+
+ $this->assertEquals( 5, count( $user_query->results ) );
+
+ // This returns an array of strings, while EP returns ints.
+ $user_ids = array_map( 'absint', $user_query->results );
+
+ // Run the same query against EP to verify we're only getting
+ // user IDs.
+ $user_query = new \WP_User_Query(
+ [
+ 'ep_integrate' => true,
+ 'number' => 5,
+ 'fields' => 'ID',
+ ]
+ );
+
+ $ep_user_ids = array_map( 'absint', $user_query->results );
+
+ $this->assertSame( $user_ids, $ep_user_ids );
+ }
+
+ /**
+ * Tests multiple fields in the fields parameters for user queries.
+ */
+ public function testMultipleUserFieldsQuery() {
+ $this->createAndIndexUsers();
+
+ $count = 5;
+
+ // First, get the IDs of the users.
+ $user_query = new \WP_User_Query(
+ [
+ 'number' => $count,
+ 'fields' => [ 'ID', 'display_name' ],
+ ]
+ );
+
+ $users = $user_query->results;
+
+ $this->assertEquals( $count, count( $users ) );
+
+ // Run the same query against EP to verify we're getting classes
+ // with properties.
+ $user_query = new \WP_User_Query(
+ [
+ 'ep_integrate' => true,
+ 'number' => $count,
+ 'fields' => [ 'ID', 'display_name' ],
+ ]
+ );
+
+ $this->assertTrue( $user_query->query_vars['elasticsearch_success'] );
+
+ $ep_users = $user_query->results;
+
+ $this->assertEquals( 5, count( $users ) );
+
+ for ( $i = 0; $i < 5; $i++ ) {
+ $this->assertSame( absint( $users[ $i ]->ID ), absint( $ep_users[ $i ]->ID ) );
+ $this->assertSame( $users[ $i ]->display_name, $ep_users[ $i ]->display_name );
+ }
+ }
+
+ /**
+ * Test integration with User Queries.
+ */
+ public function testIntegrateSearchQueries() {
+ $this->assertTrue( $this->get_feature()->integrate_search_queries( true, null ) );
+ $this->assertFalse( $this->get_feature()->integrate_search_queries( false, null ) );
+
+ $query = new \WP_User_Query(
+ [
+ 'ep_integrate' => false,
+ ]
+ );
+
+ $this->assertFalse( $this->get_feature()->integrate_search_queries( true, $query ) );
+
+ $query = new \WP_User_Query(
+ [
+ 'ep_integrate' => 0,
+ ]
+ );
+
+ $this->assertFalse( $this->get_feature()->integrate_search_queries( true, $query ) );
+
+ $query = new \WP_User_Query(
+ [
+ 'ep_integrate' => 'false',
+ ]
+ );
+
+ $this->assertFalse( $this->get_feature()->integrate_search_queries( true, $query ) );
+
+ $query = new \WP_User_Query(
+ [
+ 'search' => 'user',
+ ]
+ );
+
+ $this->assertTrue( $this->get_feature()->integrate_search_queries( false, $query ) );
+ }
+
+ /**
+ * Test users that does not belong to any blog.
+ *
+ */
+ public function testUserSearchLimitedToOneBlog() {
+ // This user does not belong to any blog.
+ $this->ep_factory->user->create(
+ [
+ 'user_login' => 'users-and-blogs-1',
+ 'role' => '',
+ 'first_name' => 'No Blog',
+ 'last_name' => 'User',
+ 'user_email' => 'no-blog@test.com',
+ 'user_url' => 'http://domain.test',
+ ]
+ );
+ $this->ep_factory->user->create(
+ [
+ 'user_login' => 'users-and-blogs-2',
+ 'role' => 'contributor',
+ 'first_name' => 'Blog',
+ 'last_name' => 'User',
+ 'user_email' => 'blog@test.com',
+ 'user_url' => 'http://domain.test',
+ ]
+ );
+
+ ElasticPress\Elasticsearch::factory()->refresh_indices();
+
+ // Here `blog_id` defaults to `get_current_blog_id()`.
+ $query = new \WP_User_Query(
+ [
+ 'search' => 'users-and-blogs',
+ ]
+ );
+
+ $this->assertTrue( $this->get_feature()->integrate_search_queries( false, $query ) );
+ $this->assertEquals( 1, $query->total_users );
+ $this->assertTrue( $query->query_vars['elasticsearch_success'] );
+
+ // Search accross all blogs.
+ $query = new \WP_User_Query(
+ [
+ 'search' => 'users-and-blogs',
+ 'blog_id' => 0,
+ ]
+ );
+
+ $this->assertTrue( $this->get_feature()->integrate_search_queries( false, $query ) );
+ $this->assertEquals( 2, $query->total_users );
+ $this->assertTrue( $query->query_vars['elasticsearch_success'] );
+ }
+
+ /**
+ * Test user query search by user login.
+ *
+ */
+ public function testUserQueryUserLogin() {
+ $this->createAndIndexUsers();
+
+ $user_query = new \WP_User_Query(
+ [
+ 'search' => 'contributor',
+ 'search_columns' => [ 'user_login' ],
+ ]
+ );
+
+ ElasticPress\Elasticsearch::factory()->refresh_indices();
+
+ $this->assertEquals( 1, $user_query->total_users );
+ $this->assertEquals( 'user2-contributor', $user_query->results[0]->user_login );
+ $this->assertTrue( $user_query->query_vars['elasticsearch_success'] );
+ }
+
+ /**
+ * Test user query search by user nicename.
+ *
+ */
+ public function testUserQueryUserNiceName() {
+ $this->createAndIndexUsers();
+
+ $user_query = new \WP_User_Query(
+ [
+ 'search' => 'mike',
+ 'search_columns' => [ 'user_nicename' ],
+ ]
+ );
+
+ ElasticPress\Elasticsearch::factory()->refresh_indices();
+
+ $this->assertEquals( 1, $user_query->total_users );
+ $this->assertEquals( 'test_admin', $user_query->results[0]->user_login );
+ $this->assertTrue( $user_query->query_vars['elasticsearch_success'] );
+ }
+
+ /**
+ * Test user query default orderby set to asc.
+ *
+ */
+ public function testUserQueryDefaultOrderBy() {
+ $this->createAndIndexUsers();
+
+ $expected_user_order = [
+ 'admin',
+ 'test_admin',
+ 'user1-author',
+ 'user2-contributor',
+ 'user3-editor',
+ ];
+
+ ElasticPress\Elasticsearch::factory()->refresh_indices();
+
+ $user_query = new \WP_User_Query(
+ [
+ 'ep_integrate' => true,
+ 'orderby' => '',
+ ]
+ );
+
+ $user_order = array();
+ foreach ( $user_query->results as $user ) {
+ $user_order[] = $user->user_login;
+ }
+
+ $this->assertTrue( $user_query->query_vars['elasticsearch_success'] );
+ $this->assertEquals( $expected_user_order, $user_order );
+ }
+
+ /**
+ * Test default order set to the score when orderby is set to empty
+ *
+ */
+ public function testUserQueryDefaultOrder() {
+ $this->createAndIndexUsers();
+
+ ElasticPress\Elasticsearch::factory()->refresh_indices();
+
+ add_action(
+ 'pre_http_request',
+ function( $preempt, $parsed_args, $url ) {
+ $body = json_decode( $parsed_args['body'], true );
+
+ $this->assertNotEmpty( $body['sort'][0]['_score'] );
+
+ return $preempt;
+ },
+ 10,
+ 3
+ );
+
+ $user_query = new \WP_User_Query(
+ [
+ 'orderby' => '',
+ 'search' => 'user',
+ ]
+ );
+
+ $this->assertTrue( $user_query->query_vars['elasticsearch_success'] );
+ }
+
+ /**
+ * Test protected meta does not index.
+ *
+ */
+ public function testProtectedMetaNotIndex() {
+
+ $user_id = $this->factory->user->create(
+ [
+ 'meta_input' => array(
+ '_phone_number' => '1234567890',
+ ),
+ ]
+ );
+
+ $user = new \ElasticPress\Indexable\User\User();
+
+ $user_args = $user->prepare_document( $user_id );
+
+ $this->assertTrue( empty( $user_args['meta']['_phone_number'] ) );
+ }
+
+ /**
+ * Test whitelisted meta does index.
+ *
+ */
+ public function testProtectedWhiteListMetaIndex() {
+
+ add_filter(
+ 'ep_prepare_user_meta_allowed_protected_keys',
+ function( $meta_keys ) {
+ $meta_keys[] = '_phone_number';
+
+ return $meta_keys;
+ }
+ );
+
+ $user_id = $this->factory->user->create(
+ [
+ 'meta_input' => array(
+ '_phone_number' => '1234567890',
+ ),
+ ]
+ );
+
+ $user = new \ElasticPress\Indexable\User\User();
+ $user_args = $user->prepare_document( $user_id );
+
+ $this->assertEquals( $user_args['meta']['_phone_number'][0]['value'], '1234567890' );
+ }
+
+ /**
+ * Test query_db() function.
+ *
+ */
+ public function testQueryDb() {
+
+ $this->createAndIndexUsers();
+ $user_1 = $this->factory->user->create();
+ $user_2 = $this->factory->user->create();
+
+ ElasticPress\Elasticsearch::factory()->refresh_indices();
+
+ $user = new \ElasticPress\Indexable\User\User();
+
+ // Test the first loop of the indexing.
+ $results = $user->query_db(
+ [
+ 'per_page' => 1,
+ ]
+ );
+
+ $this->assertCount( 1, $results['objects'] );
+ $this->assertEquals( 7, $results['total_objects'] );
+ $this->assertEquals( $user_2, $results['objects'][0]->ID );
+
+ // Test the second loop of the indexing.
+ $results = $user->query_db(
+ [
+ 'per_page' => 1,
+ 'offset' => 1,
+ ]
+ );
+
+ $this->assertCount( 1, $results['objects'] );
+ $this->assertEquals( 7, $results['total_objects'] );
+ $this->assertEquals( $user_1, $results['objects'][0]->ID );
+ }
+
+}