PluginProbe
ElasticPress / 4.7.1
ElasticPress v4.7.1
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 4.7.1, at includes/classes/Feature/Documents/Documents.php

521 lines 15.1 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\Feature as Feature;
11 use ElasticPress\Elasticsearch as Elasticsearch;
12 use ElasticPress\FeatureRequirementsStatus as FeatureRequirementsStatus;
13 use ElasticPress\Indexables as Indexables;
14 use ElasticPress\Utils as 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->title = esc_html__( 'Documents', 'elasticpress' );
29
30 $this->summary = __( 'Indexes text inside of popular file types, and adds those files types to search results.', 'elasticpress' );
31
32 $this->docs_url = __( 'https://elasticpress.zendesk.com/hc/en-us/articles/360050447492-Configuring-ElasticPress-via-the-Plugin-Dashboard#documents', 'elasticpress' );
33
34 $this->requires_install_reindex = false;
35
36 parent::__construct();
37 }
38
39 /**
40 * Setup feature filters
41 *
42 * @since 2.3
43 */
44 public function setup() {
45 add_filter( 'ep_search_fields', [ $this, 'search_fields' ] );
46 add_filter( 'ep_index_request_path', [ $this, 'index_request_path' ], 999, 3 );
47 add_filter( 'ep_post_sync_args', [ $this, 'post_sync_args' ], 999, 2 );
48 add_filter( 'ep_indexable_post_status', [ $this, 'indexable_post_status' ], 999, 1 );
49 add_filter( 'ep_bulk_index_request_path', [ $this, 'bulk_index_request_path' ], 999, 3 );
50 add_filter( 'pre_get_posts', [ $this, 'setup_document_search' ] );
51 add_filter( 'ep_post_mapping', [ $this, 'attachments_mapping' ] );
52 add_action( 'ep_cli_put_mapping', [ $this, 'create_pipeline' ] );
53 add_action( 'ep_dashboard_put_mapping', [ $this, 'create_pipeline' ] );
54 add_filter( 'ep_indexable_post_types', [ $this, 'index_attachment_post_type' ] );
55 add_filter( 'ep_searchable_post_types', [ $this, 'search_attachment_post_type' ] );
56
57 // Autosuggest Compatibility
58 add_filter( 'ep_autosuggest_options', [ $this, 'filter_autosuggest_options' ] );
59 add_filter( 'ep_term_suggest_post_status', [ $this, 'filter_autosuggest_post_status' ] );
60
61 add_filter( 'ep_weighting_fields_for_post_type', [ $this, 'filter_weightable_fields_for_post_type' ], 10, 2 );
62 add_filter( 'ep_weighting_default_post_type_weights', [ $this, 'filter_attachment_post_type_weights' ], 10, 2 );
63 }
64
65 /**
66 * Add attachment post type to be searched. We used to search these by default.
67 *
68 * @param array $post_types List of indexable post types
69 * @since 2.6
70 * @return array
71 */
72 public function search_attachment_post_type( $post_types ) {
73 $post_types['attachment'] = 'attachment';
74
75 return $post_types;
76 }
77
78 /**
79 * Add attachment post type to be indexed. We used to index these by default.
80 *
81 * @param array $post_types List of indexable post types
82 * @since 2.6
83 * @return array
84 */
85 public function index_attachment_post_type( $post_types ) {
86 $post_types['attachment'] = 'attachment';
87
88 return $post_types;
89 }
90
91 /**
92 * Add attachments mapping
93 *
94 * @param array $mapping Mapping to add to.
95 * @since 2.3
96 * @return array
97 */
98 public function attachments_mapping( $mapping ) {
99 if ( version_compare( (string) Elasticsearch::factory()->get_elasticsearch_version(), '7.0', '<' ) ) {
100 $mapping['mappings']['post']['properties']['attachments'] = array(
101 'type' => 'object',
102 );
103 } else {
104 $mapping['mappings']['properties']['attachments'] = array(
105 'type' => 'object',
106 );
107 }
108
109 return $mapping;
110 }
111
112 /**
113 * This is some complex logic to handle the front end search query. If we have a search query,
114 * add the attachment post type to post_type and inherit to post_status. If post_status is not set,
115 * we assume publish/inherit is wanted. post_type should always be set. We also add allowed mime types.
116 * If mime types are already set, append.
117 *
118 * @param WP_Query $query WP_Query to modify to search.
119 * @since 2.3
120 */
121 public function setup_document_search( $query ) {
122 if ( ! Utils\is_integrated_request( $this->slug, [ 'public', 'ajax' ] ) ) {
123 return;
124 }
125
126 $s = $query->get( 's', false );
127
128 if ( empty( $s ) ) {
129 return;
130 }
131
132 $post_status = $query->get( 'post_status', [] );
133 $post_type = $query->get( 'post_type', [] );
134 $mime_types = $query->get( 'post_mime_type', [] );
135
136 if ( ! empty( $post_type ) ) {
137 if ( 'any' !== $post_type ) {
138 if ( is_string( $post_type ) ) {
139 $post_type = explode( ' ', $post_type );
140 $post_type[] = 'attachment';
141
142 $query->set( 'post_type', array_unique( $post_type ) );
143 }
144 }
145 }
146
147 if ( empty( $post_status ) ) {
148 $post_status = array_values(
149 get_post_stati(
150 [
151 'public' => true,
152 'exclude_from_search' => false,
153 ]
154 )
155 );
156
157 // Add inherit for documents
158 $post_status[] = 'inherit';
159 } else {
160 if ( is_string( $post_status ) ) {
161 $post_status = explode( ' ', $post_status );
162 }
163
164 $post_status[] = 'inherit';
165 }
166
167 $query->set( 'post_status', array_unique( $post_status ) );
168
169 if ( ! empty( $mime_types ) && is_string( $mime_types ) ) {
170 $mime_types = explode( ' ', $mime_types );
171 }
172
173 $mime_types = array_merge( $mime_types, $this->get_allowed_ingest_mime_types() );
174 $mime_types[] = ''; // This let's us query non-attachments as well as attachments.
175
176 $query->set( 'post_mime_type', array_unique( array_values( $mime_types ) ) );
177 }
178
179 /**
180 * Change Elasticsearch request path if processing attachment
181 *
182 * @param string $path Path to request.
183 * @param array $post Post array.
184 * @param string $type Type of document
185 * @since 2.6
186 * @return string
187 */
188 public function index_request_path( $path, $post, $type ) {
189 if ( 'post' !== $type ) {
190 return $path;
191 }
192
193 if ( 'attachment' === $post['post_type'] ) {
194 if ( ! empty( $post['attachments'][0]['data'] ) && isset( $post['post_mime_type'] ) && in_array( $post['post_mime_type'], $this->get_allowed_ingest_mime_types(), true ) ) {
195 $index = Indexables::factory()->get( 'post' )->get_index_name();
196
197 /**
198 * Filter documents pipeline ID
199 *
200 * @hook ep_documents_pipeline_id
201 * @param {string} $id Pipeline ID
202 * @return {string} new ID
203 */
204 $pipeline_id = apply_filters( 'ep_documents_pipeline_id', Indexables::factory()->get( 'post' )->get_index_name() . '-attachment' );
205
206 if ( version_compare( (string) Elasticsearch::factory()->get_elasticsearch_version(), '7.0', '<' ) ) {
207 $path = trailingslashit( $index ) . 'post/' . $post['ID'] . '?pipeline=' . $pipeline_id;
208 } else {
209 $path = trailingslashit( $index ) . '_doc/' . $post['ID'] . '?pipeline=' . $pipeline_id;
210 }
211 }
212 }
213
214 return $path;
215 }
216
217 /**
218 * Add attachment data in post sync args
219 *
220 * @param array $post_args Post arguments to be synced.
221 * @param int $post_id Post id.
222 * @since 2.3
223 * @return mixed
224 */
225 public function post_sync_args( $post_args, $post_id ) {
226 global $wp_filesystem;
227
228 require_once ABSPATH . 'wp-admin/includes/file.php';
229
230 $post_args['attachments'] = [];
231
232 /**
233 * Filters the arguments passed to WP_Filesystem()
234 *
235 * @hook ep_filesystem_args
236 * @param {boolean} False (default value)
237 * @return {array|false} Array of args, or false if none
238 */
239 $filesystem_args = apply_filters( 'ep_filesystem_args', false );
240
241 if ( ! WP_Filesystem( $filesystem_args ) ) {
242 return $post_args;
243 }
244
245 $allowed_ingest_mime_types = $this->get_allowed_ingest_mime_types();
246
247 if ( 'attachment' === get_post_type( $post_id ) && in_array( get_post_mime_type( $post_id ), $allowed_ingest_mime_types, true ) ) {
248 $file_name = get_attached_file( $post_id );
249 $exist = $wp_filesystem->exists( $file_name, false, 'f' );
250 if ( $exist ) {
251 $file_content = $wp_filesystem->get_contents( $file_name );
252
253 $post_args['attachments'][] = array(
254 // phpcs:disable
255 'data' => base64_encode( $file_content ),
256 // phpcs:enable
257 );
258 }
259 }
260
261 return $post_args;
262 }
263
264 /**
265 * Add attachment field for search
266 *
267 * @param array $search_fields Search fields.
268 * @since 2.3
269 * @return array
270 */
271 public function search_fields( $search_fields ) {
272 if ( ! is_array( $search_fields ) ) {
273 return $search_fields;
274 }
275 $search_fields[] = 'attachments.attachment.content';
276 return $search_fields;
277 }
278
279 /**
280 * Add "inherit" post status for indexable post status
281 *
282 * @param array $statuses Array of post statuses.
283 * @since 2.3
284 * @return array
285 */
286 public function indexable_post_status( $statuses ) {
287 if ( ! array_search( 'inherit', $statuses, true ) ) {
288 $statuses[] = 'inherit';
289 }
290
291 return $statuses;
292 }
293
294 /**
295 * Set attachment pipeline in Elaticsearch request path for bulk index
296 *
297 * @param string $path Existing request path.
298 * @param string $body JSON to index.
299 * @param string $type Type of documents.
300 * @since 2.6
301 * @return string
302 */
303 public function bulk_index_request_path( $path, $body, $type ) {
304 if ( 'post' !== $type ) {
305 return $path;
306 }
307
308 return add_query_arg(
309 array(
310 /**
311 * Filter documents pipeline ID
312 *
313 * @hook ep_documents_pipeline_id
314 * @param {string} $id Pipeline ID
315 * @return {string} new ID
316 */
317 'pipeline' => apply_filters( 'ep_documents_pipeline_id', Indexables::factory()->get( 'post' )->get_index_name() . '-attachment' ),
318 ),
319 $path
320 );
321 }
322
323 /**
324 * Determine Documents feature requirement status
325 *
326 * @since 2.3
327 * @return mixed
328 */
329 public function requirements_status() {
330 $status = new FeatureRequirementsStatus( 1 );
331
332 if ( empty( Elasticsearch::factory()->get_elasticsearch_version( false ) ) ) {
333 return $status;
334 }
335
336 $plugins = Elasticsearch::factory()->get_elasticsearch_plugins();
337
338 $status->message = [];
339
340 // Ingest attachment plugin is required for this feature.
341 if ( empty( $plugins ) || empty( $plugins['ingest-attachment'] ) ) {
342 $status->code = 2;
343 $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' );
344 } else {
345 $status->code = 1;
346 $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' );
347 }
348
349 return $status;
350 }
351
352 /**
353 * Output feature box long
354 *
355 * @since 2.3
356 */
357 public function output_feature_box_long() {
358 ?>
359 <p><?php esc_html_e( '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.', 'elasticpress' ); ?></p>
360 <?php
361 }
362
363 /**
364 * Make sure to create pipeline after activation
365 *
366 * @since 2.6
367 */
368 public function post_activation() {
369 $this->create_pipeline();
370 }
371
372 /**
373 * Put attachment pipeline
374 *
375 * @since 2.3
376 */
377 public function create_pipeline() {
378 $args = array(
379 'description' => 'Extract attachment information',
380 'processors' => array(
381 array(
382 'foreach' => array(
383 'field' => 'attachments',
384 'processor' => array(
385 'attachment' => array(
386 'target_field' => '_ingest._value.attachment',
387 'field' => '_ingest._value.data',
388 'ignore_missing' => true,
389 'indexed_chars' => -1,
390 ),
391 ),
392 ),
393 ),
394 array(
395 'foreach' => array(
396 'field' => 'attachments',
397 'processor' => array(
398 'remove' => array(
399 'field' => '_ingest._value.data',
400 ),
401 ),
402 ),
403 ),
404 ),
405 );
406
407 /**
408 * Filter documents pipeline ID
409 *
410 * @hook ep_documents_pipeline_id
411 * @param {string} $id Pipeline ID
412 * @return {string} new ID
413 */
414 Elasticsearch::factory()->create_pipeline( apply_filters( 'ep_documents_pipeline_id', Indexables::factory()->get( 'post' )->get_index_name() . '-attachment' ), $args );
415 }
416
417 /**
418 * Get allowed mime types for feature
419 *
420 * @since 2.3
421 * @return array
422 */
423 public function get_allowed_ingest_mime_types() {
424 /**
425 * Filter allowed mime types for documents
426 *
427 * @hook ep_allowed_documents_ingest_mime_types
428 * @param {array} $mime_types Allowed mime types
429 * @return {array} New types
430 */
431 return apply_filters(
432 'ep_allowed_documents_ingest_mime_types',
433 array(
434 'pdf' => 'application/pdf',
435 'ppt' => 'application/vnd.ms-powerpoint',
436 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
437 'xls' => 'application/vnd.ms-excel',
438 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
439 'doc' => 'application/msword',
440 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
441 )
442 );
443 }
444
445 /**
446 * Filters autosuggest options to add the mime type filters
447 *
448 * @param array $options Current autosuggest options
449 *
450 * @return array
451 */
452 public function filter_autosuggest_options( $options ) {
453 $mime_types = isset( $options['mimeTypes'] ) && is_array( $options['mimeTypes'] ) ? $options['mimeTypes'] : array();
454
455 $mime_types = array_merge( $mime_types, $this->get_allowed_ingest_mime_types(), [ '' ] ); // Empty type matches any other post type without mime type set
456
457 $options['mimeTypes'] = $mime_types;
458
459 return $options;
460 }
461
462 /**
463 * Adds the "inherit" post status to allowed post statuses for autosuggest searches
464 *
465 * @param array $post_statuses Current post statuses
466 *
467 * @return array
468 */
469 public function filter_autosuggest_post_status( $post_statuses ) {
470 $post_statuses[] = 'inherit';
471
472 return $post_statuses;
473 }
474
475 /**
476 * Filters the weightable fields for attachments.
477 *
478 * Adds the document content field and changes the post_content and post_excerpt labels to "Description" and "Caption"
479 *
480 * @param array $fields Current weightable fields
481 * @param string $post_type The post type the weightable fields apply to
482 *
483 * @return array Final weightable fields for post type
484 */
485 public function filter_weightable_fields_for_post_type( $fields, $post_type ) {
486 if ( 'attachment' === $post_type ) {
487 // Updates labels for description and caption
488 // @todo this might need to move to Protected Content if attachments are enabled there
489 $fields['attributes']['children']['post_content']['label'] = __( 'Description', 'elasticpress' );
490 $fields['attributes']['children']['post_excerpt']['label'] = __( 'Caption', 'elasticpress' );
491
492 // Adds new field
493 $fields['attributes']['children']['attachments.attachment.content'] = [
494 'key' => 'attachments.attachment.content',
495 'label' => __( 'Document Content', 'elasticpress' ),
496 ];
497 }
498
499 return $fields;
500 }
501
502 /**
503 * Filters the default weight values to add attachment-specific weights
504 *
505 * @param array $weights Current weight settings
506 * @param string $post_type The post type the weights apply to
507 *
508 * @return array Final weights
509 */
510 public function filter_attachment_post_type_weights( $weights, $post_type ) {
511 if ( 'attachment' === $post_type ) {
512 $weights['attachments.attachment.content'] = [
513 'enabled' => true,
514 'weight' => 0,
515 ];
516 }
517
518 return $weights;
519 }
520 }
521