PluginProbe
ElasticPress / 5.3.5
ElasticPress v5.3.5
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 / Documents / Documents.php

Documents.php in ElasticPress 5.3.5, at includes/classes/Feature/Documents/Documents.php

602 lines 17.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Documents feature
4 *
5 * @package elasticpress
6 */
7
8 namespace ElasticPress\Feature\Documents;
9
10 use ElasticPress\Elasticsearch;
11 use ElasticPress\Feature;
12 use ElasticPress\FeatureRequirementsStatus;
13 use ElasticPress\Indexables;
14 use ElasticPress\Utils;
15
16 /**
17 * Documents feature class.
18 */
19 class Documents extends Feature {
20 /**
21 * Initialize feature setting it's config
22 *
23 * @since 2.6
24 */
25 public function __construct() {
26 $this->slug = 'documents';
27
28 $this->group = 'indexing-options';
29
30 $this->requires_install_reindex = false;
31
32 parent::__construct();
33 }
34
35 /**
36 * Sets i18n strings.
37 *
38 * @return void
39 * @since 5.2.0
40 */
41 public function set_i18n_strings(): void {
42 $this->title = esc_html__( 'Documents', 'elasticpress' );
43
44 $this->summary = '<p>' . __( 'Website search results will include popular document file types, using file names as well as their content. Supported file types include: ppt, pptx, doc, docx, xls, xlsx, pdf, csv, txt.', 'elasticpress' ) . '</p>';
45
46 $this->docs_url = __( 'https://www.elasticpress.io/resources/articles/configuring-elasticpress-via-the-plugin-dashboard/#documents', 'elasticpress' );
47 }
48
49 /**
50 * Setup feature filters
51 *
52 * @since 2.3
53 */
54 public function setup() {
55 add_filter( 'ep_search_fields', [ $this, 'search_fields' ] );
56 add_filter( 'ep_index_request_path', [ $this, 'index_request_path' ], 999, 3 );
57 add_filter( 'ep_post_sync_args', [ $this, 'post_sync_args' ], 999, 2 );
58 add_filter( 'ep_indexable_post_status', [ $this, 'indexable_post_status' ], 999, 1 );
59 add_filter( 'ep_bulk_index_request_path', [ $this, 'bulk_index_request_path' ], 999, 3 );
60 add_filter( 'pre_get_posts', [ $this, 'setup_document_search' ] );
61 add_filter( 'ep_post_mapping', [ $this, 'attachments_mapping' ] );
62 add_action( 'ep_cli_put_mapping', [ $this, 'create_pipeline' ] );
63 add_action( 'ep_dashboard_put_mapping', [ $this, 'create_pipeline' ] );
64 add_filter( 'ep_indexable_post_types', [ $this, 'index_attachment_post_type' ] );
65 add_filter( 'ep_searchable_post_types', [ $this, 'search_attachment_post_type' ] );
66
67 // Autosuggest Compatibility
68 add_filter( 'ep_autosuggest_options', [ $this, 'filter_autosuggest_options' ] );
69 add_filter( 'ep_term_suggest_post_status', [ $this, 'filter_autosuggest_post_status' ] );
70
71 add_filter( 'ep_weighting_fields_for_post_type', [ $this, 'filter_weightable_fields_for_post_type' ], 10, 2 );
72 add_filter( 'ep_weighting_default_post_type_weights', [ $this, 'filter_attachment_post_type_weights' ], 10, 2 );
73
74 add_filter( 'ep_ajax_wp_query_integration', [ $this, 'maybe_enable_ajax_wp_query_integration' ] );
75 }
76
77 /**
78 * Add attachment post type to be searched. We used to search these by default.
79 *
80 * @param array $post_types List of indexable post types
81 * @since 2.6
82 * @return array
83 */
84 public function search_attachment_post_type( $post_types ) {
85 $post_types['attachment'] = 'attachment';
86
87 return $post_types;
88 }
89
90 /**
91 * Add attachment post type to be indexed. We used to index these by default.
92 *
93 * @param array $post_types List of indexable post types
94 * @since 2.6
95 * @return array
96 */
97 public function index_attachment_post_type( $post_types ) {
98 $post_types['attachment'] = 'attachment';
99
100 return $post_types;
101 }
102
103 /**
104 * Add attachments mapping
105 *
106 * @param array $mapping Mapping to add to.
107 * @since 2.3
108 * @return array
109 */
110 public function attachments_mapping( $mapping ) {
111 if ( version_compare( (string) Elasticsearch::factory()->get_elasticsearch_version(), '7.0', '<' ) ) {
112 $mapping['mappings']['post']['properties']['attachments'] = array(
113 'type' => 'object',
114 );
115 } else {
116 $mapping['mappings']['properties']['attachments'] = array(
117 'type' => 'object',
118 );
119 }
120
121 return $mapping;
122 }
123
124 /**
125 * Handle the search query
126 *
127 * @param WP_Query $query WP_Query to modify to search.
128 * @since 2.3
129 */
130 public function setup_document_search( $query ) {
131 if ( ! Utils\is_integrated_request( $this->slug, [ 'public', 'ajax' ] ) ) {
132 return;
133 }
134
135 // If not a search, return.
136 $s = $query->get( 's', false );
137 if ( empty( $s ) ) {
138 return;
139 }
140
141 // Return if attachments are not involved in the query.
142 // If post_type is empty, attachments will be included automatically.
143 $post_type = (array) $query->get( 'post_type', [] );
144 if ( ! empty( $post_type ) && ! in_array( 'attachment', $post_type, true ) ) {
145 return;
146 }
147
148 $this->maybe_set_post_status( $query );
149 $this->maybe_set_mime_type( $query );
150 }
151
152 /**
153 * Change Elasticsearch request path if processing attachment
154 *
155 * @param string $path Path to request.
156 * @param array $post Post array.
157 * @param string $type Type of document
158 * @since 2.6
159 * @return string
160 */
161 public function index_request_path( $path, $post, $type ) {
162 if ( 'post' !== $type ) {
163 return $path;
164 }
165
166 if ( 'attachment' !== $post['post_type'] ) {
167 return $path;
168 }
169
170 if ( empty( $post['attachments'][0]['data'] ) || ! isset( $post['post_mime_type'] ) || ! in_array( $post['post_mime_type'], $this->get_allowed_ingest_mime_types(), true ) ) {
171 return $path;
172 }
173
174 $index = Indexables::factory()->get( 'post' )->get_index_name();
175
176 /**
177 * Filter documents pipeline ID
178 *
179 * @hook ep_documents_pipeline_id
180 * @param {string} $id Pipeline ID
181 * @return {string} new ID
182 */
183 $pipeline_id = apply_filters( 'ep_documents_pipeline_id', Indexables::factory()->get( 'post' )->get_index_name() . '-attachment' );
184
185 if ( version_compare( (string) Elasticsearch::factory()->get_elasticsearch_version(), '7.0', '<' ) ) {
186 $path = trailingslashit( $index ) . 'post/' . $post['ID'] . '?pipeline=' . $pipeline_id;
187 } else {
188 $path = trailingslashit( $index ) . '_doc/' . $post['ID'] . '?pipeline=' . $pipeline_id;
189 }
190
191 return $path;
192 }
193
194 /**
195 * Add attachment data in post sync args
196 *
197 * @param array $post_args Post arguments to be synced.
198 * @param int $post_id Post id.
199 * @since 2.3
200 * @return mixed
201 */
202 public function post_sync_args( $post_args, $post_id ) {
203 global $wp_filesystem;
204
205 require_once ABSPATH . 'wp-admin/includes/file.php';
206
207 $post_args['attachments'] = [];
208
209 /**
210 * Filters the arguments passed to WP_Filesystem()
211 *
212 * @hook ep_filesystem_args
213 * @param {boolean} False (default value)
214 * @return {array|false} Array of args, or false if none
215 */
216 $filesystem_args = apply_filters( 'ep_filesystem_args', false );
217
218 if ( ! WP_Filesystem( $filesystem_args ) ) {
219 return $post_args;
220 }
221
222 $allowed_ingest_mime_types = $this->get_allowed_ingest_mime_types();
223
224 if ( 'attachment' === get_post_type( $post_id ) && in_array( get_post_mime_type( $post_id ), $allowed_ingest_mime_types, true ) ) {
225 $file_name = get_attached_file( $post_id );
226 $exist = $wp_filesystem->exists( $file_name, false, 'f' );
227 if ( $exist ) {
228 $file_content = $wp_filesystem->get_contents( $file_name );
229
230 $post_args['attachments'][] = array(
231 // phpcs:disable
232 'data' => base64_encode( $file_content ),
233 // phpcs:enable
234 );
235 }
236 }
237
238 return $post_args;
239 }
240
241 /**
242 * Add attachment field for search
243 *
244 * @param array $search_fields Search fields.
245 * @since 2.3
246 * @return array
247 */
248 public function search_fields( $search_fields ) {
249 if ( ! is_array( $search_fields ) ) {
250 return $search_fields;
251 }
252 $search_fields[] = 'attachments.attachment.content';
253 return $search_fields;
254 }
255
256 /**
257 * Add "inherit" post status for indexable post status
258 *
259 * @param array $statuses Array of post statuses.
260 * @since 2.3
261 * @return array
262 */
263 public function indexable_post_status( $statuses ) {
264 if ( ! array_search( 'inherit', $statuses, true ) ) {
265 $statuses[] = 'inherit';
266 }
267
268 return $statuses;
269 }
270
271 /**
272 * Set attachment pipeline in Elaticsearch request path for bulk index
273 *
274 * @param string $path Existing request path.
275 * @param string $body JSON to index.
276 * @param string $type Type of documents.
277 * @since 2.6
278 * @return string
279 */
280 public function bulk_index_request_path( $path, $body, $type ) {
281 if ( 'post' !== $type ) {
282 return $path;
283 }
284
285 return add_query_arg(
286 array(
287 /**
288 * Filter documents pipeline ID
289 *
290 * @hook ep_documents_pipeline_id
291 * @param {string} $id Pipeline ID
292 * @return {string} new ID
293 */
294 'pipeline' => apply_filters( 'ep_documents_pipeline_id', Indexables::factory()->get( 'post' )->get_index_name() . '-attachment' ),
295 ),
296 $path
297 );
298 }
299
300 /**
301 * Determine Documents feature requirement status
302 *
303 * @since 2.3
304 * @return mixed
305 */
306 public function requirements_status() {
307 $status = new FeatureRequirementsStatus( 1, null, $this );
308
309 if ( empty( Elasticsearch::factory()->get_elasticsearch_version( false ) ) ) {
310 return $status;
311 }
312
313 $plugins = Elasticsearch::factory()->get_elasticsearch_plugins();
314
315 $status->message = [];
316
317 // Ingest attachment plugin is required for this feature.
318 if ( empty( $plugins ) || empty( $plugins['ingest-attachment'] ) ) {
319 $status->code = 2;
320 $status->message[] = __( 'The <a href="https://www.elastic.co/guide/en/elasticsearch/plugins/master/ingest-attachment.html">Ingest Attachment plugin</a> for Elasticsearch is not installed. To get the most out of ElasticPress, without the hassle of Elasticsearch management, check out <a href="https://elasticpress.io">ElasticPress.io</a> hosting.', 'elasticpress' );
321 } else {
322 $status->code = 1;
323 $status->message[] = __( 'This feature modifies the default user experience for your visitors by adding popular document file types to search results. All supported documents (PDFs and Microsoft Office) uploaded to your media library will appear in search results.', 'elasticpress' );
324 }
325
326 return $status;
327 }
328
329 /**
330 * Make sure to create pipeline after activation
331 *
332 * @since 2.6
333 */
334 public function post_activation() {
335 $this->create_pipeline();
336 }
337
338 /**
339 * Put attachment pipeline
340 *
341 * @since 2.3
342 */
343 public function create_pipeline() {
344 $args = array(
345 'description' => 'Extract attachment information',
346 'processors' => array(
347 array(
348 'foreach' => array(
349 'field' => 'attachments',
350 'processor' => array(
351 'attachment' => array(
352 'target_field' => '_ingest._value.attachment',
353 'field' => '_ingest._value.data',
354 'ignore_missing' => true,
355 'indexed_chars' => -1,
356 ),
357 ),
358 ),
359 ),
360 array(
361 'foreach' => array(
362 'field' => 'attachments',
363 'processor' => array(
364 'remove' => array(
365 'field' => '_ingest._value.data',
366 ),
367 ),
368 ),
369 ),
370 ),
371 );
372
373 /**
374 * Filter documents pipeline ID
375 *
376 * @hook ep_documents_pipeline_id
377 * @param {string} $id Pipeline ID
378 * @return {string} new ID
379 */
380 Elasticsearch::factory()->create_pipeline( apply_filters( 'ep_documents_pipeline_id', Indexables::factory()->get( 'post' )->get_index_name() . '-attachment' ), $args );
381 }
382
383 /**
384 * Get allowed mime types for feature
385 *
386 * @since 2.3
387 * @return array
388 */
389 public function get_allowed_ingest_mime_types() {
390 /**
391 * Filter allowed mime types for documents
392 *
393 * @hook ep_allowed_documents_ingest_mime_types
394 * @param {array} $mime_types Allowed mime types
395 * @return {array} New types
396 */
397 return apply_filters(
398 'ep_allowed_documents_ingest_mime_types',
399 array(
400 'pdf' => 'application/pdf',
401 'ppt' => 'application/vnd.ms-powerpoint',
402 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
403 'xls' => 'application/vnd.ms-excel',
404 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
405 'doc' => 'application/msword',
406 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
407 'csv' => 'text/csv',
408 'txt' => 'text/plain',
409 )
410 );
411 }
412
413 /**
414 * Filters autosuggest options to add the mime type filters
415 *
416 * @param array $options Current autosuggest options
417 *
418 * @return array
419 */
420 public function filter_autosuggest_options( $options ) {
421 $mime_types = isset( $options['mimeTypes'] ) && is_array( $options['mimeTypes'] ) ? $options['mimeTypes'] : array();
422
423 $mime_types = array_merge( $mime_types, $this->get_allowed_ingest_mime_types(), [ '' ] ); // Empty type matches any other post type without mime type set
424
425 $options['mimeTypes'] = $mime_types;
426
427 return $options;
428 }
429
430 /**
431 * Adds the "inherit" post status to allowed post statuses for autosuggest searches
432 *
433 * @param array $post_statuses Current post statuses
434 *
435 * @return array
436 */
437 public function filter_autosuggest_post_status( $post_statuses ) {
438 $post_statuses[] = 'inherit';
439
440 return $post_statuses;
441 }
442
443 /**
444 * Filters the weightable fields for attachments.
445 *
446 * Adds the document content field and changes the post_content and post_excerpt labels to "Description" and "Caption"
447 *
448 * @param array $fields Current weightable fields
449 * @param string $post_type The post type the weightable fields apply to
450 *
451 * @return array Final weightable fields for post type
452 */
453 public function filter_weightable_fields_for_post_type( $fields, $post_type ) {
454 if ( 'attachment' === $post_type ) {
455 // Updates labels for description and caption
456 // @todo this might need to move to Protected Content if attachments are enabled there
457 $fields['attributes']['children']['post_content']['label'] = __( 'Description', 'elasticpress' );
458 $fields['attributes']['children']['post_excerpt']['label'] = __( 'Caption', 'elasticpress' );
459
460 // Adds new field
461 $fields['attributes']['children']['attachments.attachment.content'] = [
462 'key' => 'attachments.attachment.content',
463 'label' => __( 'Document Content', 'elasticpress' ),
464 ];
465 }
466
467 return $fields;
468 }
469
470 /**
471 * Filters the default weight values to add attachment-specific weights
472 *
473 * @param array $weights Current weight settings
474 * @param string $post_type The post type the weights apply to
475 *
476 * @return array Final weights
477 */
478 public function filter_attachment_post_type_weights( $weights, $post_type ) {
479 if ( 'attachment' === $post_type ) {
480 $weights['attachments.attachment.content'] = [
481 'enabled' => true,
482 'weight' => 0,
483 ];
484 }
485
486 return $weights;
487 }
488
489 /**
490 * Enable integration if we are in the media library admin ajax search
491 *
492 * @param bool $integrate Whether it should be integrated or not
493 * @return bool
494 */
495 public function maybe_enable_ajax_wp_query_integration( $integrate ) {
496 return ( $this->is_admin_ajax_search() && $this->is_media_library_ajax_enabled() ) ? true : $integrate;
497 }
498
499 /**
500 * If post_status is not set, we assume publish/inherit is wanted.
501 *
502 * @param WP_Query $query WP_Query to modify to search.
503 * @return void
504 */
505 protected function maybe_set_post_status( $query ) {
506 $post_status = $query->get( 'post_status', [] );
507
508 if ( empty( $post_status ) ) {
509 $post_status = array_values(
510 get_post_stati(
511 [
512 'public' => true,
513 'exclude_from_search' => false,
514 ]
515 )
516 );
517
518 // Add inherit for documents
519 $post_status[] = 'inherit';
520 } else {
521 if ( is_string( $post_status ) ) {
522 $post_status = explode( ' ', $post_status );
523 }
524
525 $post_status[] = 'inherit';
526 }
527
528 $query->set( 'post_status', array_unique( $post_status ) );
529 }
530
531 /**
532 * Add allowed mime types. If mime types are already set, append.
533 *
534 * @param WP_Query $query WP_Query to modify to search.
535 * @return void
536 */
537 protected function maybe_set_mime_type( $query ) {
538 /**
539 * Mime types
540 *
541 * By default, we do not restrict results by mime types in the Media Library AJAX search,
542 * otherwise images, and SVGs, for example, will not be returned.
543 */
544 $should_set_mime_types = ! $this->is_admin_ajax_search() || ! $this->is_media_library_ajax_enabled();
545
546 /**
547 * Filter whether mime type restriction should be applied to the current WP Query
548 *
549 * @since 5.1.0
550 * @hook ep_documents_wp_query_set_mime_types
551 * @param {bool} $should_set Whether to restrict this query with mime types or not
552 * @param {WP_Query} $query WP Query object
553 * @return {bool} New value
554 */
555 $should_set_mime_types = apply_filters( 'ep_documents_wp_query_set_mime_types', $should_set_mime_types, $query );
556
557 if ( ! $should_set_mime_types ) {
558 return;
559 }
560
561 // Set mime types
562 $mime_types = $query->get( 'post_mime_type', [] );
563
564 if ( ! empty( $mime_types ) && is_string( $mime_types ) ) {
565 $mime_types = explode( ' ', $mime_types );
566 }
567
568 $mime_types = array_merge( $mime_types, $this->get_allowed_ingest_mime_types() );
569 $mime_types[] = ''; // This let's us query non-attachments as well as attachments.
570
571 $query->set( 'post_mime_type', array_unique( array_values( $mime_types ) ) );
572 }
573
574 /**
575 * Whether the feature should work on the Media Library admin ajax request
576 *
577 * @return boolean
578 */
579 protected function is_media_library_ajax_enabled() {
580 $protected_content = \ElasticPress\Features::factory()->get_registered_feature( 'protected_content' );
581
582 /**
583 * Filter whether the feature should work on the Media Library admin ajax request
584 *
585 * @since 5.1.0
586 * @hook ep_documents_media_library_ajax_enabled
587 * @param {bool} $enabled Whether to integrate or not
588 * @return {bool} New value
589 */
590 return apply_filters( 'ep_documents_media_library_ajax_enabled', $protected_content->is_active() );
591 }
592
593 /**
594 * Whether we are in the admin ajax search request for the media library
595 *
596 * @return boolean
597 */
598 protected function is_admin_ajax_search() {
599 return wp_doing_ajax() && isset( $_REQUEST['action'] ) && 'query-attachments' === $_REQUEST['action']; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
600 }
601 }
602