PluginProbe
ElasticPress / 5.0.0
ElasticPress v5.0.0
5.3.5 5.3.4 3.6.5 3.6.6 4.0.0 4.0.1 4.1.0 4.2.0 4.2.1 4.2.2 4.3.0 4.3.1 4.4.0 4.4.1 4.5.0 4.5.1 4.5.2 4.6.0 4.6.1 4.7.0 4.7.1 4.7.2 5.0.0 5.0.1 5.0.2 All 108 releases
elasticpress / includes / classes / Feature / WooCommerce / OrdersAutosuggest.php

OrdersAutosuggest.php in ElasticPress 5.0.0, at includes/classes/Feature/WooCommerce/OrdersAutosuggest.php

552 lines 15.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * WooCommerce Orders Feature
4 *
5 * @since 4.5.0
6 * @package elasticpress
7 */
8
9 namespace ElasticPress\Feature\WooCommerce;
10
11 use ElasticPress\Elasticsearch;
12 use ElasticPress\Indexables;
13 use ElasticPress\REST;
14 use ElasticPress\Utils;
15
16 if ( ! defined( 'ABSPATH' ) ) {
17 exit; // Exit if accessed directly.
18 }
19
20 /**
21 * WooCommerce OrdersAutosuggest Feature
22 */
23 class OrdersAutosuggest {
24 /**
25 * The name of the index.
26 *
27 * @var string
28 */
29 protected $index;
30
31 /**
32 * The search template.
33 *
34 * @var string
35 */
36 protected $search_template;
37
38 /**
39 * Initialize feature.
40 *
41 * @return void
42 */
43 public function __construct() {
44 $this->index = Indexables::factory()->get( 'post' )->get_index_name();
45 }
46
47 /**
48 * Setup feature functionality.
49 *
50 * @return void
51 */
52 public function setup() {
53 add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_assets' ] );
54 add_filter( 'ep_after_update_feature', [ $this, 'after_update_feature' ], 10, 3 );
55 add_filter( 'ep_after_sync_index', [ $this, 'epio_save_search_template' ] );
56 add_filter( 'ep_saved_weighting_configuration', [ $this, 'epio_save_search_template' ] );
57 add_filter( 'ep_indexable_post_status', [ $this, 'post_statuses' ] );
58 add_filter( 'ep_indexable_post_types', [ $this, 'post_types' ] );
59 add_action( 'rest_api_init', [ $this, 'rest_api_init' ] );
60 add_filter( 'ep_post_sync_args', [ $this, 'filter_term_suggest' ], 10 );
61 add_filter( 'ep_post_mapping', [ $this, 'mapping' ] );
62 add_action( 'ep_woocommerce_shop_order_search_fields', [ $this, 'set_search_fields' ], 10, 2 );
63 add_filter( 'ep_index_posts_args', [ $this, 'maybe_query_password_protected_posts' ] );
64 add_filter( 'posts_where', [ $this, 'maybe_set_posts_where' ], 10, 2 );
65 }
66
67 /**
68 * Un-setup feature functionality.
69 *
70 * @since 5.0.0
71 */
72 public function tear_down() {
73 remove_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_assets' ] );
74 remove_filter( 'ep_after_update_feature', [ $this, 'after_update_feature' ] );
75 remove_filter( 'ep_after_sync_index', [ $this, 'epio_save_search_template' ] );
76 remove_filter( 'ep_saved_weighting_configuration', [ $this, 'epio_save_search_template' ] );
77 remove_filter( 'ep_indexable_post_status', [ $this, 'post_statuses' ] );
78 remove_filter( 'ep_indexable_post_types', [ $this, 'post_types' ] );
79 remove_action( 'rest_api_init', [ $this, 'rest_api_init' ] );
80 remove_filter( 'ep_post_sync_args', [ $this, 'filter_term_suggest' ] );
81 remove_filter( 'ep_post_mapping', [ $this, 'mapping' ] );
82 remove_action( 'ep_woocommerce_shop_order_search_fields', [ $this, 'set_search_fields' ] );
83 remove_filter( 'ep_index_posts_args', [ $this, 'maybe_query_password_protected_posts' ] );
84 remove_filter( 'posts_where', [ $this, 'maybe_set_posts_where' ] );
85 }
86
87 /**
88 * Get the endpoint for WooCommerce Orders search.
89 *
90 * @return string WooCommerce orders search endpoint.
91 */
92 public function get_search_endpoint() {
93 /**
94 * Filters the WooCommerce Orders search endpoint.
95 *
96 * @since 4.5.0
97 * @hook ep_woocommerce_order_search_endpoint
98 * @param {string} $endpoint Endpoint path.
99 * @param {string} $index Elasticsearch index.
100 */
101 return apply_filters( 'ep_woocommerce_order_search_endpoint', "api/v1/search/orders/{$this->index}", $this->index );
102 }
103
104 /**
105 * Get the endpoint for the WooCommerce Orders search template.
106 *
107 * @return string WooCommerce Orders search template endpoint.
108 */
109 public function get_template_endpoint() {
110 /**
111 * Filters the WooCommerce Orders search template API endpoint.
112 *
113 * @since 4.5.0
114 * @hook ep_woocommerce_order_search_template_endpoint
115 * @param {string} $endpoint Endpoint path.
116 * @param {string} $index Elasticsearch index.
117 * @returns {string} Search template API endpoint.
118 */
119 return apply_filters( 'ep_woocommerce_order_search_template_endpoint', "api/v1/search/orders/{$this->index}/template", $this->index );
120 }
121
122 /**
123 * Registers the API endpoint to get a token.
124 *
125 * @return void
126 */
127 public function rest_api_init() {
128 $controller = new REST\Token();
129 $controller->register_routes();
130 }
131
132 /**
133 * Enqueue admin assets.
134 *
135 * @param string $hook_suffix The current admin page.
136 */
137 public function enqueue_admin_assets( $hook_suffix ) {
138 if ( ! in_array( $hook_suffix, [ 'edit.php', 'woocommerce_page_wc-orders' ], true ) ) {
139 return;
140 }
141
142 if ( 'edit.php' === $hook_suffix ) {
143 if ( ! isset( $_GET['post_type'] ) || 'shop_order' !== $_GET['post_type'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
144 return;
145 }
146 }
147
148 wp_enqueue_style(
149 'elasticpress-woocommerce-order-search',
150 EP_URL . 'dist/css/woocommerce-order-search-styles.css',
151 Utils\get_asset_info( 'woocommerce-order-search-styles', 'dependencies' ),
152 Utils\get_asset_info( 'woocommerce-order-search-styles', 'version' )
153 );
154
155 wp_enqueue_script(
156 'elasticpress-woocommerce-order-search',
157 EP_URL . 'dist/js/woocommerce-order-search-script.js',
158 Utils\get_asset_info( 'woocommerce-order-search-script', 'dependencies' ),
159 Utils\get_asset_info( 'woocommerce-order-search-script', 'version' ),
160 true
161 );
162
163 wp_set_script_translations( 'elasticpress-woocommerce-order-search', 'elasticpress' );
164
165 $api_endpoint = $this->get_search_endpoint();
166 $api_host = Utils\get_host();
167
168 wp_localize_script(
169 'elasticpress-woocommerce-order-search',
170 'epWooCommerceOrderSearch',
171 array(
172 'adminUrl' => admin_url( 'post.php' ),
173 'apiEndpoint' => $api_endpoint,
174 'apiHost' => ( 0 !== strpos( $api_endpoint, 'http' ) ) ? trailingslashit( esc_url_raw( $api_host ) ) : '',
175 'argsSchema' => $this->get_args_schema(),
176 'credentialsApiUrl' => rest_url( 'elasticpress/v1/token' ),
177 'credentialsNonce' => wp_create_nonce( 'wp_rest' ),
178 'dateFormat' => wc_date_format(),
179 'statusLabels' => wc_get_order_statuses(),
180 'timeFormat' => wc_time_format(),
181 'requestIdBase' => Utils\get_request_id_base(),
182 )
183 );
184 }
185
186 /**
187 * Save or delete the search template on ElasticPress.io based on whether
188 * the WooCommerce feature is being activated or deactivated.
189 *
190 * @param string $feature Feature slug
191 * @param array $settings Feature settings
192 * @param array $data Feature activation data
193 *
194 * @return void
195 */
196 public function after_update_feature( $feature, $settings, $data ) {
197 if ( 'woocommerce' !== $feature ) {
198 return;
199 }
200
201 if ( true === $data['active'] ) {
202 $this->epio_save_search_template();
203 } else {
204 $this->epio_delete_search_template();
205 }
206 }
207
208 /**
209 * Save the search template to ElasticPress.io.
210 *
211 * @return void
212 */
213 public function epio_save_search_template() {
214 $endpoint = $this->get_template_endpoint();
215 $template = $this->get_search_template();
216
217 Elasticsearch::factory()->remote_request(
218 $endpoint,
219 [
220 'blocking' => false,
221 'body' => $template,
222 'method' => 'PUT',
223 ]
224 );
225
226 /**
227 * Fires after the request is sent the search template API endpoint.
228 *
229 * @since 4.5.0
230 * @hook ep_woocommerce_order_search_template_saved
231 * @param {string} $template The search template (JSON).
232 * @param {string} $index Index name.
233 */
234 do_action( 'ep_woocommerce_order_search_template_saved', $template, $this->index );
235 }
236
237 /**
238 * Delete the search template from ElasticPress.io.
239 *
240 * @return void
241 */
242 public function epio_delete_search_template() {
243 $endpoint = $this->get_template_endpoint();
244
245 Elasticsearch::factory()->remote_request(
246 $endpoint,
247 [
248 'blocking' => false,
249 'method' => 'DELETE',
250 ]
251 );
252
253 /**
254 * Fires after the request is sent the search template API endpoint.
255 *
256 * @since 4.5.0
257 * @hook ep_woocommerce_order_search_template_deleted
258 * @param {string} $index Index name.
259 */
260 do_action( 'ep_woocommerce_order_search_template_deleted', $this->index );
261 }
262
263 /**
264 * Get the saved search template from ElasticPress.io.
265 *
266 * @return string|WP_Error Search template if found, WP_Error on error.
267 */
268 public function epio_get_search_template() {
269 $endpoint = $this->get_template_endpoint();
270 $request = Elasticsearch::factory()->remote_request( $endpoint );
271
272 if ( is_wp_error( $request ) ) {
273 return $request;
274 }
275
276 $response = wp_remote_retrieve_body( $request );
277
278 return $response;
279 }
280
281 /**
282 * Generate a search template.
283 *
284 * A search template is the JSON for an Elasticsearch query with a
285 * placeholder search term. The template is sent to ElasticPress.io where
286 * it's used to make Elasticsearch queries using search terms sent from
287 * the front end.
288 *
289 * @return string The search template as JSON.
290 */
291 public function get_search_template() {
292 $order_statuses = wc_get_order_statuses();
293
294 add_filter( 'ep_bypass_exclusion_from_search', '__return_true', 10 );
295 add_filter( 'ep_intercept_remote_request', '__return_true' );
296 add_filter( 'ep_do_intercept_request', [ $this, 'intercept_search_request' ], 10, 4 );
297 add_filter( 'ep_is_integrated_request', [ $this, 'is_integrated_request' ], 10, 2 );
298
299 $query = new \WP_Query(
300 array(
301 'ep_integrate' => true,
302 'ep_order_search_template' => true,
303 'post_status' => array_keys( $order_statuses ),
304 'post_type' => 'shop_order',
305 's' => '{{ep_placeholder}}',
306 )
307 );
308
309 remove_filter( 'ep_bypass_exclusion_from_search', '__return_true', 10 );
310 remove_filter( 'ep_intercept_remote_request', '__return_true' );
311 remove_filter( 'ep_do_intercept_request', [ $this, 'intercept_search_request' ], 10 );
312 remove_filter( 'ep_is_integrated_request', [ $this, 'is_integrated_request' ], 10 );
313
314 return $this->search_template;
315 }
316
317 /**
318 * Return true if a given feature is supported by WooCommerce Orders.
319 *
320 * Applied as a filter on Utils\is_integrated_request() so that features
321 * are enabled for the query that is used to generate the search template,
322 * regardless of the request type. This avoids the need to send a request
323 * to the front end.
324 *
325 * @param bool $is_integrated Whether queries for the request will be
326 * integrated.
327 * @param string $context Context for the original check. Usually the
328 * slug of the feature doing the check.
329 * @return bool True if the check is for a feature supported by WooCommerce
330 * Order search.
331 */
332 public function is_integrated_request( $is_integrated, $context ) {
333 $supported_contexts = [
334 'search',
335 'woocommerce',
336 ];
337
338 return in_array( $context, $supported_contexts, true );
339 }
340
341 /**
342 * Store intercepted request body and return request result.
343 *
344 * @param object $response Response
345 * @param array $query Query
346 * @param array $args WP_Query argument array
347 * @param int $failures Count of failures in request loop
348 * @return object $response Response
349 */
350 public function intercept_search_request( $response, $query = [], $args = [], $failures = 0 ) {
351 $this->search_template = $query['args']['body'];
352
353 return wp_remote_request( $query['url'], $args );
354 }
355
356 /**
357 * Get schema for search args.
358 *
359 * @return array Search args schema.
360 */
361 public function get_args_schema() {
362 $args = array(
363 'customer' => array(
364 'type' => 'number',
365 ),
366 'm' => array(
367 'type' => 'string',
368 ),
369 'offset' => array(
370 'type' => 'number',
371 'default' => 0,
372 ),
373 'per_page' => array(
374 'type' => 'number',
375 'default' => 6,
376 ),
377 'search' => array(
378 'type' => 'string',
379 'default' => '',
380 ),
381 );
382
383 return $args;
384 }
385
386 /**
387 * Index shop orders.
388 *
389 * @param array $post_types Indexable post types.
390 * @return array Indexable post types.
391 */
392 public function post_types( $post_types ) {
393 $post_types['shop_order'] = 'shop_order';
394
395 return $post_types;
396 }
397
398 /**
399 * Index order statuses.
400 *
401 * @param array $post_statuses Indexable post statuses.
402 * @return array Indexable post statuses.
403 */
404 public function post_statuses( $post_statuses ) {
405 $order_statuses = wc_get_order_statuses();
406
407 return array_unique( array_merge( $post_statuses, array_keys( $order_statuses ) ) );
408 }
409
410 /**
411 * Add term suggestions to be indexed
412 *
413 * @param array $post_args Array of ES args.
414 * @return array
415 */
416 public function filter_term_suggest( $post_args ) {
417 if ( empty( $post_args['post_type'] ) || 'shop_order' !== $post_args['post_type'] ) {
418 return $post_args;
419 }
420
421 if ( empty( $post_args['meta'] ) ) {
422 return $post_args;
423 }
424
425 /**
426 * Add the order number as a meta (text) field, so we can freely search on it.
427 */
428 $order_id = $post_args['ID'];
429 if ( function_exists( 'wc_get_order' ) ) {
430 $order = wc_get_order( $post_args['ID'] );
431 if ( $order && is_a( $order, 'WC_Order' ) && method_exists( $order, 'get_order_number' ) ) {
432 $order_id = $order->get_order_number();
433 }
434 }
435
436 $post_args['meta']['order_number'] = [
437 [
438 'raw' => $order_id,
439 'value' => $order_id,
440 ],
441 ];
442
443 $suggest = [];
444
445 $fields_to_ngram = [
446 '_billing_email',
447 '_billing_last_name',
448 '_billing_first_name',
449 ];
450
451 foreach ( $fields_to_ngram as $field_to_ngram ) {
452 if ( ! empty( $post_args['meta'][ $field_to_ngram ] )
453 && ! empty( $post_args['meta'][ $field_to_ngram ][0] )
454 && ! empty( $post_args['meta'][ $field_to_ngram ][0]['value'] ) ) {
455 $suggest[] = $post_args['meta'][ $field_to_ngram ][0]['value'];
456 }
457 }
458
459 if ( ! empty( $suggest ) ) {
460 $post_args['term_suggest'] = $suggest;
461 }
462
463 return $post_args;
464 }
465
466 /**
467 * Add mapping for suggest fields
468 *
469 * @param array $mapping ES mapping.
470 * @return array
471 */
472 public function mapping( $mapping ) {
473 $post_indexable = Indexables::factory()->get( 'post' );
474
475 $mapping = $post_indexable->add_ngram_analyzer( $mapping );
476 $mapping = $post_indexable->add_term_suggest_field( $mapping );
477
478 return $mapping;
479 }
480
481 /**
482 * Set the search_fields parameter in the search template.
483 *
484 * @param array $search_fields Current search fields
485 * @param \WP_Query $query Query being executed
486 * @return array New search fields
487 */
488 public function set_search_fields( array $search_fields, \WP_Query $query ) : array {
489 $is_orders_search_template = (bool) $query->get( 'ep_order_search_template' );
490
491 if ( $is_orders_search_template ) {
492 $search_fields = [
493 'meta.order_number.value',
494 'term_suggest',
495 'meta' => [
496 '_billing_email',
497 '_billing_last_name',
498 '_billing_first_name',
499 ],
500 ];
501 }
502
503 return $search_fields;
504 }
505
506 /**
507 * Allow password protected to be indexed.
508 *
509 * If Protected Content is enabled, do nothing. Otherwise, allow pw protected posts to be indexed.
510 * The feature restricts it back in maybe_set_posts_where()
511 *
512 * @see maybe_set_posts_where()
513 * @param array $args WP_Query args
514 * @return array
515 */
516 public function maybe_query_password_protected_posts( $args ) {
517 // Password protected posts are already being indexed, no need to do anything.
518 if ( isset( $args['has_password'] ) && is_null( $args['has_password'] ) ) {
519 return $args;
520 }
521
522 /**
523 * Set a flag in the query but allow it to index all password protected posts for now,
524 * so WP does not inject its own where clause.
525 */
526 $args['ep_orders_has_password'] = true;
527 $args['has_password'] = null;
528
529 return $args;
530 }
531
532 /**
533 * Restrict password protected posts back but allow orders.
534 *
535 * @see maybe_query_password_protected_posts
536 * @param string $where Current where clause
537 * @param WP_Query $query WP_Query
538 * @return string
539 */
540 public function maybe_set_posts_where( $where, $query ) {
541 global $wpdb;
542
543 if ( ! $query->get( 'ep_orders_has_password' ) ) {
544 return $where;
545 }
546
547 $where .= " AND ( {$wpdb->posts}.post_password = '' OR {$wpdb->posts}.post_type = 'shop_order' )";
548
549 return $where;
550 }
551 }
552