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 / ProtectedContent / ProtectedContent.php

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

577 lines 16.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * ElasticPress Protected Content feature
4 *
5 * @since 2.2
6 * @package elasticpress
7 */
8
9 namespace ElasticPress\Feature\ProtectedContent;
10
11 use ElasticPress\Feature;
12 use ElasticPress\FeatureRequirementsStatus;
13 use ElasticPress\Features;
14 use ElasticPress\Utils;
15
16 if ( ! defined( 'ABSPATH' ) ) {
17 exit; // Exit if accessed directly.
18 }
19
20 /**
21 * Protected content feature
22 */
23 class ProtectedContent extends Feature {
24
25 /**
26 * Initialize feature setting its config
27 *
28 * @since 3.0
29 */
30 public function __construct() {
31 $this->slug = 'protected_content';
32
33 $this->group = 'indexing-options';
34
35 $this->requires_install_reindex = true;
36
37 $this->available_during_installation = true;
38
39 parent::__construct();
40 }
41
42 /**
43 * Sets i18n strings.
44 *
45 * @return void
46 * @since 5.2.0
47 */
48 public function set_i18n_strings(): void {
49 $this->title = esc_html__( 'Protected Content', 'elasticpress' );
50
51 $this->summary = '<p>' . __( 'Syncs unpublished content — including private, draft, and scheduled posts — improving load times in places like the administrative dashboard where WordPress needs to include protected content in a query.', 'elasticpress' ) . '</p>' .
52 '<p><em>' . __( 'We recommend using a secured Elasticsearch setup, such as ElasticPress.io, to prevent potential exposure of content not intended for the public.', 'elasticpress' ) . '</em></p>';
53
54 $this->docs_url = __( 'https://www.elasticpress.io/resources/articles/configuring-elasticpress-via-the-plugin-dashboard/#protected-content', 'elasticpress' );
55 }
56
57 /**
58 * Setup all feature filters
59 *
60 * @since 2.1
61 */
62 public function setup() {
63 add_filter( 'ep_indexable_post_status', [ $this, 'get_statuses' ] );
64 add_filter( 'ep_indexable_post_types', [ $this, 'post_types' ], 10, 1 );
65 add_filter( 'ep_post_formatted_args', [ $this, 'exclude_protected_posts' ], 10, 2 );
66 add_filter( 'ep_index_posts_args', [ $this, 'query_password_protected_posts' ] );
67 add_filter( 'ep_post_sync_args', [ $this, 'include_post_password' ], 10, 2 );
68 add_filter( 'ep_post_sync_args', [ $this, 'remove_fields_from_password_protected' ], 11, 2 );
69 add_filter( 'ep_search_post_return_args', [ $this, 'return_post_password' ] );
70 add_filter( 'ep_skip_autosave_sync', '__return_false' );
71 add_filter( 'ep_pre_kill_sync_for_password_protected', [ $this, 'sync_password_protected' ], 10, 2 );
72
73 if ( is_admin() ) {
74 add_filter( 'ep_admin_wp_query_integration', '__return_true' );
75 add_action( 'pre_get_posts', [ $this, 'integrate' ] );
76 add_filter( 'ep_post_query_db_args', [ $this, 'query_password_protected_posts' ] );
77 add_filter( 'ep_set_sort', [ $this, 'maybe_change_sort' ] );
78 add_filter( 'ep_post_formatted_args', [ $this, 'filter_private_posts_for_current_user' ], 10, 2 );
79 }
80
81 if ( Features::factory()->get_registered_feature( 'comments' )->is_active() ) {
82 add_filter( 'ep_indexable_comment_status', [ $this, 'get_comment_statuses' ] );
83 add_action( 'pre_get_comments', [ $this, 'integrate_comments_query' ] );
84 }
85 }
86
87 /**
88 * Index all post types
89 *
90 * @param array $post_types Existing post types.
91 * @since 2.2
92 * @return array
93 */
94 public function post_types( $post_types ) {
95 // Let's get non public post types first
96 $pc_post_types = get_post_types( array( 'public' => false ) );
97
98 $ignored_post_types = [
99 'custom_css',
100 'customize_changeset',
101 'ep-synonym',
102 'ep-pointer',
103 'nav_menu_item',
104 'oembed_cache',
105 'revision',
106 'user_request',
107 'wp_block',
108 'wp_global_styles',
109 'wp_navigation',
110 'wp_template',
111 'wp_template_part',
112 ];
113
114 foreach ( $ignored_post_types as $ignored_post_type ) {
115 unset( $pc_post_types[ $ignored_post_type ] );
116 }
117
118 // By default, attachments are not indexed, we have to make sure they are included (Could already be included by documents feature).
119 $post_types['attachment'] = 'attachment';
120
121 // Merge non public post types with any pre-filtered post_type
122 return array_merge( $post_types, $pc_post_types );
123 }
124
125 /**
126 * Integrate EP into proper queries
127 *
128 * @param WP_Query $query WP Query
129 * @since 2.1
130 */
131 public function integrate( $query ) {
132 if ( ! Utils\is_integrated_request( $this->slug, [ 'admin' ] ) ) {
133 return;
134 }
135
136 // Lets make sure this doesn't interfere with the CLI
137 if ( defined( 'WP_CLI' ) && WP_CLI ) {
138 return;
139 }
140
141 if ( ! $query->is_main_query() ) {
142 return;
143 }
144
145 /**
146 * We limit to these post types to not conflict with other features like WooCommerce
147 *
148 * @since 2.1
149 * @var array
150 */
151 $post_types = array(
152 'post' => 'post',
153 'attachment' => 'attachment',
154 );
155
156 /**
157 * Filter protected content supported post types. For backwards compatibility.
158 *
159 * @hook ep_admin_supported_post_types
160 * @param {array} $post_types Post types
161 * @return {array} New post types
162 */
163 $supported_post_types = apply_filters( 'ep_admin_supported_post_types', $post_types );
164
165 /**
166 * Filter protected content supported post types.
167 *
168 * @hook ep_pc_supported_post_types
169 * @param {array} $supported_post_types Supported post types
170 * @return {array} New post types
171 */
172 $supported_post_types = apply_filters( 'ep_pc_supported_post_types', $supported_post_types );
173
174 $post_type = $query->get( 'post_type' );
175
176 if ( empty( $post_type ) ) {
177 $post_type = 'post';
178 }
179
180 if ( is_array( $post_type ) ) {
181 foreach ( $post_type as $pt ) {
182 if ( empty( $supported_post_types[ $pt ] ) ) {
183 return;
184 }
185 }
186
187 $query->set( 'ep_integrate', true );
188 } elseif ( ! empty( $supported_post_types[ $post_type ] ) ) {
189 $query->set( 'ep_integrate', true );
190 }
191
192 /**
193 * Remove articles weighting by date in admin.
194 *
195 * @since 3.0
196 */
197 $search_feature = Features::factory()->get_registered_feature( 'search' );
198
199 remove_filter( 'ep_formatted_args', [ $search_feature, 'weight_recent' ], 10 );
200 }
201
202 /**
203 * Query all posts with and without password for indexing.
204 *
205 * @since 4.0.0
206 *
207 * @param array $args Database arguments
208 * @return array
209 */
210 public function query_password_protected_posts( $args ) {
211 $args['has_password'] = null;
212
213 return $args;
214 }
215
216 /**
217 * Include post password when indexing.
218 *
219 * @since 4.0.0
220 *
221 * @param array $post_args Post arguments
222 * @param int $post_id Post ID
223 * @return array
224 */
225 public function include_post_password( $post_args, $post_id ) {
226 $post = get_post( $post_id );
227
228 // Assign null value so we can use the EXISTS filter.
229 $post_args['post_password'] = ! empty( $post->post_password ) ? $post->post_password : null;
230
231 return $post_args;
232 }
233
234 /**
235 * Prevent some fields in password protected posts from being indexed.
236 *
237 * As some solutions publicly expose full post contents, this method prevents password
238 * protected posts to have their full content and their meta fields indexed. Developers
239 * wanting to bypass this behavior can use the `ep_pc_skip_post_content_cleanup` filter.
240 *
241 * @param array $post_args Post arguments
242 * @param int $post_id Post ID
243 * @return array
244 */
245 public function remove_fields_from_password_protected( $post_args, $post_id ) {
246 if ( empty( $post_args['post_password'] ) ) {
247 return $post_args;
248 }
249
250 /**
251 * Filter to skip the password protected content clean up.
252 *
253 * @hook ep_pc_skip_post_content_cleanup
254 * @since 4.0.0, 4.2.0 added $post_args and $post_id
255 * @param {bool} $skip Whether the password protected content should have their content, and meta removed
256 * @param {array} $post_args Post arguments
257 * @param {int} $post_id Post ID
258 * @return {bool}
259 */
260 if ( apply_filters( 'ep_pc_skip_post_content_cleanup', false, $post_args, $post_id ) ) {
261 return $post_args;
262 }
263
264 $fields_to_remove = [
265 'post_content_filtered',
266 'post_content',
267 'meta',
268 'thumbnail',
269 'post_content_plain',
270 'price_html',
271 ];
272
273 foreach ( $fields_to_remove as $field ) {
274 if ( ! empty( $post_args[ $field ] ) ) {
275 if ( is_array( $post_args[ $field ] ) ) {
276 $post_args[ $field ] = [];
277 } else {
278 $post_args[ $field ] = '';
279 }
280 }
281 }
282
283 return $post_args;
284 }
285
286 /**
287 * Exclude protected post from the frontend queries.
288 *
289 * @since 4.0.0
290 *
291 * @param array $formatted_args Formatted Elasticsearch query
292 * @param array $args Query variables
293 * @return array
294 */
295 public function exclude_protected_posts( $formatted_args, $args ) {
296 if ( empty( $args['has_password'] ) ) {
297 /**
298 * Filter to exclude protected posts from search.
299 *
300 * @hook ep_exclude_password_protected_from_search
301 * @since 4.0.0
302 * @param {bool} $exclude Exclude post from search.
303 * @return {bool}
304 */
305 if ( ( ! is_user_logged_in() && ! empty( $args['s'] ) ) || apply_filters( 'ep_exclude_password_protected_from_search', false ) ) {
306 $formatted_args['post_filter']['bool']['must_not'][] = array(
307 'exists' => array(
308 'field' => 'post_password',
309 ),
310 );
311 }
312 }
313
314 return $formatted_args;
315 }
316
317 /**
318 * Add post_password to post object properties set after query
319 *
320 * @since 4.0.0
321 *
322 * @param array $properties Post properties
323 * @return array
324 */
325 public function return_post_password( $properties ) {
326 $properties[] = 'post_password';
327 return $properties;
328 }
329
330 /**
331 * Integrate EP into comment queries
332 *
333 * @param WP_Comment_Query $comment_query WP Comment Query
334 * @since 3.6.0
335 */
336 public function integrate_comments_query( $comment_query ) {
337 if ( ! Utils\is_integrated_request( $this->slug, [ 'admin' ] ) ) {
338 return;
339 }
340
341 // Lets make sure this doesn't interfere with the CLI
342 if ( defined( 'WP_CLI' ) && WP_CLI ) {
343 return;
344 }
345
346 $comment_types = array( 'comment', 'review' );
347
348 /**
349 * Filter protected content supported comment types.
350 *
351 * @hook ep_pc_supported_comment_types
352 * @since 3.6.0
353 * @param {array} $comment_types Comment types
354 * @return {array} New comment types
355 */
356 $supported_comment_types = apply_filters( 'ep_pc_supported_comment_types', $comment_types );
357
358 $comment_type = $comment_query->query_vars['type'];
359
360 if ( is_array( $comment_type ) ) {
361 foreach ( $comment_type as $comment_type_value ) {
362 if ( ! in_array( $comment_type_value, $supported_comment_types, true ) ) {
363 return;
364 }
365 }
366
367 $comment_query->query_vars['ep_integrate'] = true;
368 } elseif ( in_array( $comment_type, $supported_comment_types, true ) ) {
369 $comment_query->query_vars['ep_integrate'] = true;
370 }
371 }
372
373 /**
374 * Fetches all post statuses we need to index
375 *
376 * @since 2.1
377 * @param array $statuses Post statuses array
378 * @return array
379 */
380 public function get_statuses( $statuses ) {
381 $post_statuses = get_post_stati();
382
383 unset( $post_statuses['auto-draft'] );
384
385 return array_unique( array_merge( $statuses, array_values( $post_statuses ) ) );
386 }
387
388 /**
389 * Fetches all comment statuses we need to index
390 *
391 * @since 3.6.0
392 * @param array $comment_statuses Post statuses array
393 * @return array
394 */
395 public function get_comment_statuses( $comment_statuses ) {
396 return [ 'all' ];
397 }
398
399 /**
400 * Determine feature reqs status
401 *
402 * @since 2.2
403 * @return FeatureRequirementsStatus
404 */
405 public function requirements_status() {
406 $status = new FeatureRequirementsStatus( 1, null, $this );
407
408 if ( ! Utils\is_epio() ) {
409 $status->message = __( "You aren't using <a href='https://elasticpress.io'>ElasticPress.io</a> so we can't be sure your Elasticsearch instance is secure.", 'elasticpress' );
410 }
411
412 return $status;
413 }
414
415 /**
416 * Bypass the default check for password protected posts.
417 *
418 * @since 4.6.0
419 * @param null|bool $new_skip Short-circuit flag
420 * @param bool $skip Current value of $skip
421 * @return bool
422 */
423 public function sync_password_protected( $new_skip, bool $skip ): bool {
424 return $skip;
425 }
426
427 /**
428 * Maybe change the sort order for the WP Dashboard.
429 *
430 * If the admin user has enabled the setting to use the default WordPress sort order,
431 * we will change the sort order to (somewhat) match the default WP behavior.
432 *
433 * @since 5.1.4
434 *
435 * @param array $default_sort The previous value of the `ep_set_sort` filter
436 * @return array
437 */
438 public function maybe_change_sort( $default_sort ) {
439 if ( ! function_exists( '\get_current_screen' ) ) {
440 return $default_sort;
441 }
442
443 $screen = get_current_screen();
444 if ( empty( $screen ) || 'edit' !== $screen->base ) {
445 return $default_sort;
446 }
447
448 if ( ! $this->get_setting( 'use_default_wp_sort' ) ) {
449 return $default_sort;
450 }
451
452 return [
453 [ 'post_date' => [ 'order' => 'desc' ] ],
454 [ 'post_title.sortable' => [ 'order' => 'asc' ] ],
455 ];
456 }
457
458 /**
459 * Filter private posts for current user.
460 *
461 * Private statuses (including custom ones from `get_post_stati( [ 'private' => true ] )`)
462 * are never added to the all-authors clause. Users without `read_private_posts` can
463 * still match their own private posts via the author-restricted clause below. Merging
464 * a requested private status into the all-authors list would otherwise expose other
465 * authors' private posts.
466 *
467 * @param array $formatted_args Formatted Elasticsearch query
468 * @param array $args Query variables
469 *
470 * @return array
471 */
472 public function filter_private_posts_for_current_user( $formatted_args, $args ): array {
473 $post_statuses = ! empty( $args['post_status'] ) ? $args['post_status'] : [];
474 $post_statuses = is_string( $post_statuses ) ? explode( ',', $post_statuses ) : $post_statuses;
475 $post_types = ! empty( $args['post_type'] ) ? (array) $args['post_type'] : [];
476 $valid_post_types = array_filter( $post_types, 'post_type_exists' );
477 $private_statuses = get_post_stati( [ 'private' => true ] );
478
479 $base_statuses = array_merge(
480 get_post_stati( [ 'public' => true ] ),
481 get_post_stati(
482 [
483 'protected' => true,
484 'show_in_admin_all_list' => true,
485 ]
486 ),
487 $post_statuses
488 );
489 // Requested private statuses belong only in the capability or author clauses.
490 $base_statuses = array_unique( array_diff( $base_statuses, $private_statuses ) );
491
492 $post_types_with_capability = [];
493 $post_types_without_capability = [];
494
495 foreach ( $valid_post_types as $post_type ) {
496 $post_type_object = get_post_type_object( $post_type );
497
498 if ( empty( $post_type_object ) || empty( $post_type_object->cap->read_private_posts ) ) {
499 continue;
500 }
501
502 $read_private_cap = $post_type_object->cap->read_private_posts;
503
504 if ( current_user_can( $read_private_cap ) ) {
505 $post_types_with_capability[] = $post_type;
506 } else {
507 $post_types_without_capability[] = $post_type;
508 }
509 }
510
511 $should_clauses = [];
512
513 if ( ! empty( $post_types_with_capability ) ) {
514 $all_statuses = array_unique( array_merge( $base_statuses, $private_statuses ) );
515
516 $should_clauses[] = [
517 'bool' => [
518 'must' => [
519 [ 'terms' => [ 'post_type.raw' => array_values( $post_types_with_capability ) ] ],
520 [ 'terms' => [ 'post_status' => array_values( $all_statuses ) ] ],
521 ],
522 ],
523 ];
524 }
525
526 if ( ! empty( $post_types_without_capability ) ) {
527 $should_clauses[] = [
528 'bool' => [
529 'must' => [
530 [ 'terms' => [ 'post_type.raw' => array_values( $post_types_without_capability ) ] ],
531 [ 'terms' => [ 'post_status' => array_values( $base_statuses ) ] ],
532 ],
533 ],
534 ];
535
536 if ( ! empty( $private_statuses ) ) {
537 $should_clauses[] = [
538 'bool' => [
539 'must' => [
540 [ 'terms' => [ 'post_type.raw' => array_values( $post_types_without_capability ) ] ],
541 [ 'terms' => [ 'post_status' => array_values( $private_statuses ) ] ],
542 [ 'term' => [ 'post_author.id' => get_current_user_id() ] ],
543 ],
544 ],
545 ];
546 }
547 }
548
549 if ( ! empty( $should_clauses ) ) {
550 $formatted_args['post_filter']['bool']['should'] = array_merge(
551 $formatted_args['post_filter']['bool']['should'] ?? [],
552 $should_clauses
553 );
554 $formatted_args['post_filter']['bool']['minimum_should_match'] = 1;
555 }
556
557 return $formatted_args;
558 }
559
560 /**
561 * Set the `settings_schema` attribute
562 *
563 * @since 5.1.4
564 */
565 protected function set_settings_schema() {
566 $this->settings_schema = [
567 [
568 'default' => '0',
569 'key' => 'use_default_wp_sort',
570 'help' => __( 'Enable to use WordPress default sort for searches inside the WP Dashboard.', 'elasticpress' ),
571 'label' => __( 'Use default WordPress sort on the WP Dashboard', 'elasticpress' ),
572 'type' => 'checkbox',
573 ],
574 ];
575 }
576 }
577