PluginProbe
ElasticPress / 5.0.1
ElasticPress v5.0.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 / ProtectedContent / ProtectedContent.php

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

431 lines 11.8 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->title = esc_html__( 'Protected Content', 'elasticpress' );
34
35 $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>' .
36 '<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>';
37
38 $this->docs_url = __( 'https://elasticpress.zendesk.com/hc/en-us/articles/360050447492-Configuring-ElasticPress-via-the-Plugin-Dashboard#protected-content', 'elasticpress' );
39
40 $this->requires_install_reindex = true;
41
42 $this->available_during_installation = true;
43
44 parent::__construct();
45 }
46
47 /**
48 * Setup all feature filters
49 *
50 * @since 2.1
51 */
52 public function setup() {
53 add_filter( 'ep_indexable_post_status', [ $this, 'get_statuses' ] );
54 add_filter( 'ep_indexable_post_types', [ $this, 'post_types' ], 10, 1 );
55 add_filter( 'ep_post_formatted_args', [ $this, 'exclude_protected_posts' ], 10, 2 );
56 add_filter( 'ep_index_posts_args', [ $this, 'query_password_protected_posts' ] );
57 add_filter( 'ep_post_sync_args', [ $this, 'include_post_password' ], 10, 2 );
58 add_filter( 'ep_post_sync_args', [ $this, 'remove_fields_from_password_protected' ], 11, 2 );
59 add_filter( 'ep_search_post_return_args', [ $this, 'return_post_password' ] );
60 add_filter( 'ep_skip_autosave_sync', '__return_false' );
61 add_filter( 'ep_pre_kill_sync_for_password_protected', [ $this, 'sync_password_protected' ], 10, 2 );
62
63 if ( is_admin() ) {
64 add_filter( 'ep_admin_wp_query_integration', '__return_true' );
65 add_action( 'pre_get_posts', [ $this, 'integrate' ] );
66 add_filter( 'ep_post_query_db_args', [ $this, 'query_password_protected_posts' ] );
67 }
68
69 if ( Features::factory()->get_registered_feature( 'comments' )->is_active() ) {
70 add_filter( 'ep_indexable_comment_status', [ $this, 'get_comment_statuses' ] );
71 add_action( 'pre_get_comments', [ $this, 'integrate_comments_query' ] );
72 }
73 }
74
75 /**
76 * Index all post types
77 *
78 * @param array $post_types Existing post types.
79 * @since 2.2
80 * @return array
81 */
82 public function post_types( $post_types ) {
83 // Let's get non public post types first
84 $pc_post_types = get_post_types( array( 'public' => false ) );
85
86 $ignored_post_types = [
87 'custom_css',
88 'customize_changeset',
89 'ep-synonym',
90 'ep-pointer',
91 'nav_menu_item',
92 'oembed_cache',
93 'revision',
94 'user_request',
95 'wp_block',
96 'wp_global_styles',
97 'wp_navigation',
98 'wp_template',
99 'wp_template_part',
100 ];
101
102 foreach ( $ignored_post_types as $ignored_post_type ) {
103 unset( $pc_post_types[ $ignored_post_type ] );
104 }
105
106 // By default, attachments are not indexed, we have to make sure they are included (Could already be included by documents feature).
107 $post_types['attachment'] = 'attachment';
108
109 // Merge non public post types with any pre-filtered post_type
110 return array_merge( $post_types, $pc_post_types );
111 }
112
113 /**
114 * Integrate EP into proper queries
115 *
116 * @param WP_Query $query WP Query
117 * @since 2.1
118 */
119 public function integrate( $query ) {
120 if ( ! Utils\is_integrated_request( $this->slug, [ 'admin' ] ) ) {
121 return;
122 }
123
124 // Lets make sure this doesn't interfere with the CLI
125 if ( defined( 'WP_CLI' ) && WP_CLI ) {
126 return;
127 }
128
129 if ( ! $query->is_main_query() ) {
130 return;
131 }
132
133 /**
134 * We limit to these post types to not conflict with other features like WooCommerce
135 *
136 * @since 2.1
137 * @var array
138 */
139 $post_types = array(
140 'post' => 'post',
141 'attachment' => 'attachment',
142 );
143
144 /**
145 * Filter protected content supported post types. For backwards compatibility.
146 *
147 * @hook ep_admin_supported_post_types
148 * @param {array} $post_types Post types
149 * @return {array} New post types
150 */
151 $supported_post_types = apply_filters( 'ep_admin_supported_post_types', $post_types );
152
153 /**
154 * Filter protected content supported post types.
155 *
156 * @hook ep_pc_supported_post_types
157 * @param {array} $supported_post_types Supported post types
158 * @return {array} New post types
159 */
160 $supported_post_types = apply_filters( 'ep_pc_supported_post_types', $supported_post_types );
161
162 $post_type = $query->get( 'post_type' );
163
164 if ( empty( $post_type ) ) {
165 $post_type = 'post';
166 }
167
168 if ( is_array( $post_type ) ) {
169 foreach ( $post_type as $pt ) {
170 if ( empty( $supported_post_types[ $pt ] ) ) {
171 return;
172 }
173 }
174
175 $query->set( 'ep_integrate', true );
176 } else {
177 if ( ! empty( $supported_post_types[ $post_type ] ) ) {
178 $query->set( 'ep_integrate', true );
179 }
180 }
181
182 /**
183 * Remove articles weighting by date in admin.
184 *
185 * @since 3.0
186 */
187 $search_feature = Features::factory()->get_registered_feature( 'search' );
188
189 remove_filter( 'ep_formatted_args', [ $search_feature, 'weight_recent' ], 10 );
190 }
191
192 /**
193 * Query all posts with and without password for indexing.
194 *
195 * @since 4.0.0
196 *
197 * @param array $args Database arguments
198 * @return array
199 */
200 public function query_password_protected_posts( $args ) {
201 $args['has_password'] = null;
202
203 return $args;
204 }
205
206 /**
207 * Include post password when indexing.
208 *
209 * @since 4.0.0
210 *
211 * @param array $post_args Post arguments
212 * @param int $post_id Post ID
213 * @return array
214 */
215 public function include_post_password( $post_args, $post_id ) {
216 $post = get_post( $post_id );
217
218 // Assign null value so we can use the EXISTS filter.
219 $post_args['post_password'] = ! empty( $post->post_password ) ? $post->post_password : null;
220
221 return $post_args;
222 }
223
224 /**
225 * Prevent some fields in password protected posts from being indexed.
226 *
227 * As some solutions publicly expose full post contents, this method prevents password
228 * protected posts to have their full content and their meta fields indexed. Developers
229 * wanting to bypass this behavior can use the `ep_pc_skip_post_content_cleanup` filter.
230 *
231 * @param array $post_args Post arguments
232 * @param int $post_id Post ID
233 * @return array
234 */
235 public function remove_fields_from_password_protected( $post_args, $post_id ) {
236 if ( empty( $post_args['post_password'] ) ) {
237 return $post_args;
238 }
239
240 /**
241 * Filter to skip the password protected content clean up.
242 *
243 * @hook ep_pc_skip_post_content_cleanup
244 * @since 4.0.0, 4.2.0 added $post_args and $post_id
245 * @param {bool} $skip Whether the password protected content should have their content, and meta removed
246 * @param {array} $post_args Post arguments
247 * @param {int} $post_id Post ID
248 * @return {bool}
249 */
250 if ( apply_filters( 'ep_pc_skip_post_content_cleanup', false, $post_args, $post_id ) ) {
251 return $post_args;
252 }
253
254 $fields_to_remove = [
255 'post_content_filtered',
256 'post_content',
257 'meta',
258 'thumbnail',
259 'post_content_plain',
260 'price_html',
261 ];
262
263 foreach ( $fields_to_remove as $field ) {
264 if ( ! empty( $post_args[ $field ] ) ) {
265 if ( is_array( $post_args[ $field ] ) ) {
266 $post_args[ $field ] = [];
267 } else {
268 $post_args[ $field ] = '';
269 }
270 }
271 }
272
273 return $post_args;
274 }
275
276 /**
277 * Exclude protected post from the frontend queries.
278 *
279 * @since 4.0.0
280 *
281 * @param array $formatted_args Formatted Elasticsearch query
282 * @param array $args Query variables
283 * @return array
284 */
285 public function exclude_protected_posts( $formatted_args, $args ) {
286 if ( empty( $args['has_password'] ) ) {
287 /**
288 * Filter to exclude protected posts from search.
289 *
290 * @hook ep_exclude_password_protected_from_search
291 * @since 4.0.0
292 * @param {bool} $exclude Exclude post from search.
293 * @return {bool}
294 */
295 if ( ( ! is_user_logged_in() && ! empty( $args['s'] ) ) || apply_filters( 'ep_exclude_password_protected_from_search', false ) ) {
296 $formatted_args['post_filter']['bool']['must_not'][] = array(
297 'exists' => array(
298 'field' => 'post_password',
299 ),
300 );
301 }
302 }
303
304 return $formatted_args;
305 }
306
307 /**
308 * Add post_password to post object properties set after query
309 *
310 * @since 4.0.0
311 *
312 * @param array $properties Post properties
313 * @return array
314 */
315 public function return_post_password( $properties ) {
316 $properties[] = 'post_password';
317 return $properties;
318 }
319
320 /**
321 * Integrate EP into comment queries
322 *
323 * @param WP_Comment_Query $comment_query WP Comment Query
324 * @since 3.6.0
325 */
326 public function integrate_comments_query( $comment_query ) {
327 if ( ! Utils\is_integrated_request( $this->slug, [ 'admin' ] ) ) {
328 return;
329 }
330
331 // Lets make sure this doesn't interfere with the CLI
332 if ( defined( 'WP_CLI' ) && WP_CLI ) {
333 return;
334 }
335
336 $comment_types = array( 'comment', 'review' );
337
338 /**
339 * Filter protected content supported comment types.
340 *
341 * @hook ep_pc_supported_comment_types
342 * @since 3.6.0
343 * @param {array} $comment_types Comment types
344 * @return {array} New comment types
345 */
346 $supported_comment_types = apply_filters( 'ep_pc_supported_comment_types', $comment_types );
347
348 $comment_type = $comment_query->query_vars['type'];
349
350 if ( is_array( $comment_type ) ) {
351 foreach ( $comment_type as $comment_type_value ) {
352 if ( ! in_array( $comment_type_value, $supported_comment_types, true ) ) {
353 return;
354 }
355 }
356
357 $comment_query->query_vars['ep_integrate'] = true;
358 } else {
359 if ( in_array( $comment_type, $supported_comment_types, true ) ) {
360 $comment_query->query_vars['ep_integrate'] = true;
361 }
362 }
363
364 }
365
366 /**
367 * Output feature box long
368 *
369 * @since 2.1
370 */
371 public function output_feature_box_long() {
372 ?>
373 <p><?php echo wp_kses_post( __( 'Securely indexes 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. <em>We recommend using a secured Elasticsearch setup, such as ElasticPress.io, to prevent potential exposure of content not intended for the public.</em>', 'elasticpress' ) ); ?></p>
374 <?php
375 }
376
377 /**
378 * Fetches all post statuses we need to index
379 *
380 * @since 2.1
381 * @param array $statuses Post statuses array
382 * @return array
383 */
384 public function get_statuses( $statuses ) {
385 $post_statuses = get_post_stati();
386
387 unset( $post_statuses['auto-draft'] );
388
389 return array_unique( array_merge( $statuses, array_values( $post_statuses ) ) );
390 }
391
392 /**
393 * Fetches all comment statuses we need to index
394 *
395 * @since 3.6.0
396 * @param array $comment_statuses Post statuses array
397 * @return array
398 */
399 public function get_comment_statuses( $comment_statuses ) {
400 return [ 'all' ];
401 }
402
403 /**
404 * Determine feature reqs status
405 *
406 * @since 2.2
407 * @return FeatureRequirementsStatus
408 */
409 public function requirements_status() {
410 $status = new FeatureRequirementsStatus( 1 );
411
412 if ( ! Utils\is_epio() ) {
413 $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' );
414 }
415
416 return $status;
417 }
418
419 /**
420 * Bypass the default check for password protected posts.
421 *
422 * @since 4.6.0
423 * @param null|bool $new_skip Short-circuit flag
424 * @param bool $skip Current value of $skip
425 * @return bool
426 */
427 public function sync_password_protected( $new_skip, bool $skip ) : bool {
428 return $skip;
429 }
430 }
431