Source: includes/classes/Feature/ProtectedContent/ProtectedContent.php

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