PluginProbe
Elementor Website Builder – more than just a page builder / 3.32.0-dev3
Elementor Website Builder – more than just a page builder v3.32.0-dev3
4.3.0-beta2 4.3.0-beta1 4.2.4 4.2.3 4.2.2 4.2.1 4.2.0 4.1.5 4.2.0-beta2 4.2.0-dev2 4.2.0-beta1 4.1.4 4.1.3 4.1.2 4.1.1 4.1.0 4.1.0-beta3 4.1.0-dev3 4.0.9 4.1.0-beta2 4.1.0-dev2 4.0.8 4.1.0-beta1 4.1.0-dev1 4.0.7 All 451 releases
← All changes | modules/wp-rest/classes/post-query.php +166 -165 4.2.13.32.0-dev3 View file →
@@ -2,49 +2,88 @@
2 2
3 3 namespace Elementor\Modules\WpRest\Classes;
4 4
5 5 use Elementor\Core\Utils\Collection;
6 -use Elementor\Modules\WpRest\Base\Query as Base;
6 +use Elementor\Modules\GlobalClasses\Utils\Error_Builder;
7 7
8 8 if ( ! defined( 'ABSPATH' ) ) {
9 9 exit; // Exit if accessed directly.
10 10 }
11 11
12 -class Post_Query extends Base {
12 +class Post_Query {
13 + const MAX_RESPONSE_COUNT = 100;
14 + const NAMESPACE = 'elementor/v1';
13 15 const ENDPOINT = 'post';
14 - const SEARCH_FILTER_ACCEPTED_ARGS = 2;
15 - const DEFAULT_FORBIDDEN_POST_TYPES = [ 'e-floating-buttons', 'e-landing-page', 'elementor_library', 'attachment', 'revision', 'nav_menu_item', 'custom_css', 'customize_changeset' ];
16 - const SEARCH_IN_CONTENT_KEY = 'search_in_content';
17 - const ALLOWED_KEYS_CONVERSION_MAP = [
18 - 'ID' => 'id',
19 - 'post_title' => 'label',
20 - 'post_type' => 'groupLabel',
21 - ];
22 16
17 + const EXCLUDED_POST_TYPE_KEYS = 'excluded_post_types';
18 + const SEARCH_TERM_KEY = 'term';
19 + const POST_KEYS_CONVERSION_MAP = 'post_keys_conversion_map';
20 + const MAX_COUNT_KEY = 'max_count';
21 + const NONCE_KEY = 'x_wp_nonce';
22 +
23 + const FORBIDDEN_POST_TYPES = [ 'e-floating-buttons', 'e-landing-page', 'elementor_library', 'attachment' ];
24 +
25 + public function register( bool $override_existing_endpoints = false ): void {
26 + register_rest_route( self::NAMESPACE, self::ENDPOINT, [
27 + [
28 + 'methods' => \WP_REST_Server::READABLE,
29 + 'permission_callback' => fn ( \WP_REST_Request $request ) => $this->validate_access_permission( $request ),
30 + 'args' => $this->get_endpoint_registration_args(),
31 + 'sanitize_callback' => 'esc_attr',
32 + 'callback' => fn ( \WP_REST_Request $request ) => $this->route_wrapper( fn() => $this->get_posts( $request ) ),
33 + ],
34 + ], $override_existing_endpoints );
35 + }
36 +
23 37 /**
24 - * @param string $search_term The original search query.
38 + * @param $args array{
39 + * excluded_post_types: array,
40 + * post_keys_conversion_map: array,
41 + * max_count: int,
42 + * } The query parameters
43 + * @return array The query parameters.
44 + */
45 + public static function build_query_params( array $args ): array {
46 + $allowed_keys = [ self::EXCLUDED_POST_TYPE_KEYS, self::POST_KEYS_CONVERSION_MAP, self::MAX_COUNT_KEY ];
47 + $keys_to_encode = [ self::EXCLUDED_POST_TYPE_KEYS, self::POST_KEYS_CONVERSION_MAP ];
48 +
49 + $params = [];
50 +
51 + foreach ( $args as $key => $value ) {
52 + if ( ! in_array( $key, $allowed_keys, true ) || ! isset( $value ) ) {
53 + continue;
54 + }
55 +
56 + if ( ! in_array( $key, $keys_to_encode, true ) ) {
57 + $params[ $key ] = $value;
58 + continue;
59 + }
60 +
61 + $params[ $key ] = wp_json_encode( $value );
62 + }
63 +
64 + return $params;
65 + }
66 +
67 + private function validate_access_permission( $request ): bool {
68 + $nonce = $request->get_header( self::NONCE_KEY );
69 +
70 + return current_user_can( 'edit_posts' ) && wp_verify_nonce( $nonce, 'wp_rest' );
71 + }
72 +
73 + /**
74 + * @param string $search_term The original search query.
25 75 * @param \WP_Query $wp_query The WP_Query instance.
26 76 * @return string Modified search query.
27 77 */
28 - public function customize_post_query( string $search_term, \WP_Query $wp_query ) {
78 + public function customize_search( string $search_term, \WP_Query $wp_query ) {
29 79 $term = $wp_query->get( 'search_term' ) ?? '';
30 80 $is_custom_search = $wp_query->get( 'custom_search' ) ?? false;
31 81
32 82 if ( $is_custom_search && ! empty( $term ) ) {
33 - $escaped = esc_sql( $term );
34 - $search_in_content = $wp_query->get( self::SEARCH_IN_CONTENT_KEY ) ?? false;
35 83 $search_term .= ' AND (';
36 - $search_term .= "post_title LIKE '%{$escaped}%'";
37 - if ( $search_in_content ) {
38 - $search_term .= " OR post_content LIKE '%{$escaped}%'";
39 - $search_term .= " OR post_excerpt LIKE '%{$escaped}%'";
40 - }
41 - if ( ctype_digit( $term ) ) {
42 - $search_term .= ' OR ID = ' . intval( $term );
43 - } else {
44 - $search_term .= " OR ID LIKE '%{$escaped}%'";
45 - }
46 - $search_term .= ')';
84 + $search_term .= "post_title LIKE '%" . esc_sql( $term ) . "%' ";
85 + $search_term .= "OR ID LIKE '%" . esc_sql( $term ) . "%')";
47 86 }
48 87
49 88 return $search_term;
50 89 }
@@ -49,78 +88,78 @@
49 88 return $search_term;
50 89 }
51 90
52 91 /**
92 + * @param callable $cb The route callback.
93 + * @return \WP_REST_Response | \WP_Error
94 + */
95 + private function route_wrapper( callable $cb ) {
96 + try {
97 + $response = $cb();
98 + } catch ( \Exception $e ) {
99 + return Error_Builder::make( $e->getCode() )
100 + ->set_message( $e->getMessage() )
101 + ->build();
102 + }
103 +
104 + return $response;
105 + }
106 +
107 + /**
53 108 * @param \WP_REST_Request $request
54 109 * @return \WP_REST_Response
55 110 */
56 - protected function get( \WP_REST_Request $request ) {
111 + private function get_posts( \WP_REST_Request $request ) {
57 112 $params = $request->get_params();
58 113 $term = trim( $params[ self::SEARCH_TERM_KEY ] ?? '' );
59 114
60 - $keys_format_map = $this->filter_keys_conversion_map(
61 - $params[ self::KEYS_CONVERSION_MAP_KEY ] ?? self::ALLOWED_KEYS_CONVERSION_MAP,
62 - self::ALLOWED_KEYS_CONVERSION_MAP
63 - );
64 - $requested_count = $params[ self::ITEMS_COUNT_KEY ] ?? 0;
115 + if ( empty( $term ) ) {
116 + return new \WP_REST_Response( [
117 + 'success' => true,
118 + 'data' => [
119 + 'value' => [],
120 + ],
121 + ], 200 );
122 + }
123 +
124 + $excluded_types = array_merge( self::FORBIDDEN_POST_TYPES, $params[ self::EXCLUDED_POST_TYPE_KEYS ] ?? [] );
125 + $keys_format_map = $params[ self::POST_KEYS_CONVERSION_MAP ];
126 + $requested_count = $params[ self::MAX_COUNT_KEY ] ?? 0;
65 127 $validated_count = max( $requested_count, 1 );
66 - $post_count = min( $validated_count, self::MAX_RESPONSE_COUNT );
67 - $is_public_only = $params[ self::IS_PUBLIC_KEY ] ?? true;
68 - $post_types = $this->get_post_types_from_params( $request );
128 + $max_count = min( $validated_count, self::MAX_RESPONSE_COUNT );
129 + $post_types = new Collection( get_post_types( [ 'public' => true ], 'object' ) );
69 130
70 - $query_args = [
71 - 'post_type' => array_keys( $post_types ),
72 - 'numberposts' => $post_count,
73 - 'suppress_filters' => false,
74 - 'custom_search' => true,
75 - 'post_status' => $is_public_only ? 'publish' : 'any',
76 - 'orderby' => 'modified',
77 - 'order' => 'DESC',
78 - ];
131 + $post_types = $post_types->filter( function ( $post_type ) use ( $excluded_types ) {
132 + return ! in_array( $post_type->name, $excluded_types, true );
133 + } );
79 134
80 - if ( ! empty( $term ) ) {
81 - $query_args['search_term'] = $term;
82 - $query_args[ self::SEARCH_IN_CONTENT_KEY ] = $params[ self::SEARCH_IN_CONTENT_KEY ] ?? false;
83 - }
135 + $post_type_slugs = $post_types->map( function ( $post_type ) {
136 + return $post_type->name;
137 + } );
84 138
85 - if ( ! empty( $params[ self::META_QUERY_KEY ] ) && is_array( $params[ self::META_QUERY_KEY ] ) ) {
86 - $query_args['meta_query'] = $params[ self::META_QUERY_KEY ];
87 - }
139 + $this->add_filter_to_customize_query();
88 140
89 - if ( ! empty( $params[ self::TAX_QUERY_KEY ] ) && is_array( $params[ self::TAX_QUERY_KEY ] ) ) {
90 - $query_args['tax_query'] = $params[ self::TAX_QUERY_KEY ];
91 - }
141 + $posts = new Collection( get_posts( [
142 + 'post_type' => $post_type_slugs->all(),
143 + 'numberposts' => $max_count,
144 + 'suppress_filters' => false,
145 + 'custom_search' => true,
146 + 'search_term' => $term,
147 + ] ) );
92 148
93 - $this->add_filter_to_customize_query();
94 - $posts = new Collection( get_posts( $query_args ) );
95 149 $this->remove_filter_to_customize_query();
96 150
97 - $post_type_labels = ( new Collection( $post_types ) )
98 - ->map( function ( $pt ) {
99 - return $pt->label;
100 - } )
101 - ->all();
102 -
103 151 return new \WP_REST_Response( [
104 152 'success' => true,
105 153 'data' => [
106 154 'value' => $posts
107 - ->filter( function ( $post ) {
108 - return current_user_can( 'read_post', $post->ID );
109 - } )
110 - ->map( function ( $post ) use ( $keys_format_map, $post_type_labels ) {
111 - $post_type_label = $post->post_type;
155 + ->map( function ( $post ) use ( $keys_format_map, $post_types ) {
156 + $post_object = (array) $post;
112 157
113 - if ( isset( $post_type_labels[ $post->post_type ] ) ) {
114 - $post_type_label = $post_type_labels[ $post->post_type ];
158 + if ( isset( $post_object['post_type'] ) ) {
159 + $post_object['post_type'] = $post_types->get( ( $post_object['post_type'] ) )->label;
115 160 }
116 161
117 - $post_object = [
118 - 'ID' => $post->ID,
119 - 'post_title' => $post->post_title,
120 - 'post_type' => $post_type_label,
121 - ];
122 -
123 162 return $this->translate_keys( $post_object, $keys_format_map );
124 163 } )
125 164 ->all(),
126 165 ],
@@ -130,12 +169,12 @@
130 169 /**
131 170 * @return void
132 171 */
133 172 private function add_filter_to_customize_query() {
134 - $priority = self::SEARCH_FILTER_PRIORITY;
135 - $accepted_args = self::SEARCH_FILTER_ACCEPTED_ARGS;
173 + $priority = 10;
174 + $accepted_args = 2;
136 175
137 - add_filter( 'posts_search', [ $this, 'customize_post_query' ], $priority, $accepted_args );
176 + add_filter( 'posts_search', [ $this, 'customize_search' ], $priority, $accepted_args );
138 177 }
139 178
140 179 /**
141 180 * @return void
@@ -140,33 +179,25 @@
140 179 /**
141 180 * @return void
142 181 */
143 182 private function remove_filter_to_customize_query() {
144 - $priority = self::SEARCH_FILTER_PRIORITY;
145 - $accepted_args = self::SEARCH_FILTER_ACCEPTED_ARGS;
183 + $priority = 10;
184 + $accepted_args = 2;
146 185
147 - remove_filter( 'posts_search', [ $this, 'customize_post_query' ], $priority, $accepted_args );
186 + remove_filter( 'posts_search', [ $this, 'customize_search' ], $priority, $accepted_args );
148 187 }
149 188
150 - protected function permission_check( \WP_REST_Request $request ): bool {
151 - return current_user_can( 'edit_posts' );
152 - }
153 -
154 - protected function get_endpoint_registration_args(): array {
189 + /**
190 + * @return array
191 + */
192 + private function get_endpoint_registration_args() {
155 193 return [
156 - self::INCLUDED_TYPE_KEY => [
157 - 'description' => 'Included post types',
158 - 'type' => 'array',
159 - 'required' => false,
160 - 'default' => null,
161 - 'sanitize_callback' => fn ( ...$args ) => self::sanitize_string_array( ...$args ),
162 - ],
163 - self::EXCLUDED_TYPE_KEY => [
194 + self::EXCLUDED_POST_TYPE_KEYS => [
164 195 'description' => 'Post type to exclude',
165 - 'type' => 'array',
196 + 'type' => [ 'array', 'string' ],
166 197 'required' => false,
167 - 'default' => self::DEFAULT_FORBIDDEN_POST_TYPES,
168 - 'sanitize_callback' => fn ( ...$args ) => self::sanitize_string_array( ...$args ),
198 + 'default' => self::FORBIDDEN_POST_TYPES,
199 + 'sanitize_callback' => fn ( ...$args ) => $this->sanitize_string_array( ...$args ),
169 200 ],
170 201 self::SEARCH_TERM_KEY => [
171 202 'description' => 'Posts to search',
172 203 'type' => 'string',
@@ -173,89 +204,59 @@
173 204 'required' => false,
174 205 'default' => '',
175 206 'sanitize_callback' => 'sanitize_text_field',
176 207 ],
177 - self::KEYS_CONVERSION_MAP_KEY => [
208 + self::POST_KEYS_CONVERSION_MAP => [
178 209 'description' => 'Specify keys to extract and convert, i.e. ["key_1" => "new_key_1"].',
179 - 'type' => 'array',
210 + 'type' => [ 'array', 'string' ],
180 211 'required' => false,
181 - 'default' => [
182 - 'ID' => 'id',
183 - 'post_title' => 'label',
184 - 'post_type' => 'groupLabel',
185 - ],
186 - 'sanitize_callback' => fn ( ...$args ) => self::sanitize_string_array( ...$args ),
212 + 'default' => [],
213 + 'sanitize_callback' => fn ( ...$args ) => $this->sanitize_string_array( ...$args ),
187 214 ],
188 - self::ITEMS_COUNT_KEY => [
189 - 'description' => 'Posts per page',
190 - 'type' => 'integer',
215 + self::MAX_COUNT_KEY => [
216 + 'description' => 'Max count of returned items',
217 + 'type' => 'number',
191 218 'required' => false,
192 219 'default' => self::MAX_RESPONSE_COUNT,
193 220 ],
194 - self::IS_PUBLIC_KEY => [
195 - 'description' => 'Whether to include only public post types',
196 - 'type' => 'boolean',
197 - 'required' => false,
198 - 'default' => true,
199 - ],
200 - self::META_QUERY_KEY => [
201 - 'description' => 'WP_Query meta_query array',
202 - 'type' => 'array',
203 - 'required' => false,
204 - 'default' => null,
205 - 'sanitize_callback' => fn ( ...$args ) => self::sanitize_string_array( ...$args ),
206 - ],
207 - self::TAX_QUERY_KEY => [
208 - 'description' => 'WP_Query tax_query array',
209 - 'type' => 'array',
210 - 'required' => false,
211 - 'default' => null,
212 - 'sanitize_callback' => fn ( ...$args ) => self::sanitize_string_array( ...$args ),
213 - ],
214 - self::SEARCH_IN_CONTENT_KEY => [
215 - 'description' => 'Whether to search within post content and excerpt in addition to title',
216 - 'type' => 'boolean',
217 - 'required' => false,
218 - 'default' => false,
219 - ],
220 221 ];
221 222 }
222 223
223 - protected static function get_allowed_param_keys(): array {
224 - return [
225 - self::EXCLUDED_TYPE_KEY,
226 - self::INCLUDED_TYPE_KEY,
227 - self::KEYS_CONVERSION_MAP_KEY,
228 - self::META_QUERY_KEY,
229 - self::TAX_QUERY_KEY,
230 - self::IS_PUBLIC_KEY,
231 - self::ITEMS_COUNT_KEY,
232 - self::SEARCH_IN_CONTENT_KEY,
233 - ];
224 + /**
225 + * @param Array<string>|string $input The input data, expected to be an array or JSON-encoded string.
226 + * @return array The sanitized array of strings.
227 + */
228 + private function sanitize_string_array( $input ) {
229 + if ( ! is_array( $input ) ) {
230 + $input = json_decode( sanitize_text_field( $input ) ) ?? [];
231 + }
232 +
233 + $array = new Collection( json_decode( json_encode( $input ), true ) );
234 +
235 + return $array
236 + ->map( 'sanitize_text_field' )
237 + ->all();
234 238 }
235 239
236 - protected static function get_keys_to_encode(): array {
237 - return [
238 - self::EXCLUDED_TYPE_KEY,
239 - self::INCLUDED_TYPE_KEY,
240 - self::KEYS_CONVERSION_MAP_KEY,
241 - self::META_QUERY_KEY,
242 - self::TAX_QUERY_KEY,
243 - ];
244 - }
240 + /**
241 + * @param array $item The input array with original keys.
242 + * @param array $dictionary An associative array mapping old keys to new keys.
243 + * @return array The array with translated keys.
244 + */
245 + private function translate_keys( array $item, array $dictionary ): array {
246 + if ( empty( $dictionary ) ) {
247 + return $item;
248 + }
245 249
246 - private function get_post_types_from_params( \WP_REST_Request $request ) {
247 - $included_types = $request->get_param( self::INCLUDED_TYPE_KEY );
248 - $excluded_types = $request->get_param( self::EXCLUDED_TYPE_KEY );
249 - $post_type_query_args = [
250 - 'public' => true,
251 - ];
250 + $replaced = [];
252 251
253 - $post_types = get_post_types( $post_type_query_args, 'objects' );
252 + foreach ( $item as $key => $value ) {
253 + if ( ! isset( $dictionary[ $key ] ) ) {
254 + continue;
255 + }
254 256
255 - return Collection::make( $post_types )
256 - ->filter( function ( $slug, $post_type ) use ( $included_types, $excluded_types ) {
257 - return ( empty( $included_types ) || in_array( $post_type, $included_types ) ) &&
258 - ( empty( $excluded_types ) || ! in_array( $post_type, $excluded_types ) );
259 - } )->all();
257 + $replaced[ $dictionary[ $key ] ] = $value;
258 + }
259 +
260 + return $replaced;
260 261 }
261 262 }