PluginProbe
Elementor Website Builder – more than just a page builder / 3.28.0-dev2
Elementor Website Builder – more than just a page builder v3.28.0-dev2
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
elementor / modules / wp-rest / classes / wp-post.php

wp-post.php in Elementor Website Builder – more than just a page builder 3.28.0-dev2, at modules/wp-rest/classes/wp-post.php

283 lines 8.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Elementor\Modules\WpRest\Classes;
4
5 use Elementor\Core\Isolation\Wordpress_Adapter;
6 use Elementor\Core\Isolation\Wordpress_Adapter_Interface;
7 use Elementor\Core\Utils\Collection;
8 use Elementor\Modules\GlobalClasses\Utils\Error_Builder;
9
10 if ( ! defined( 'ABSPATH' ) ) {
11 exit; // Exit if accessed directly.
12 }
13
14 class WP_Post {
15 const MAX_COUNT = 100;
16 const NAMESPACE = 'elementor/v1';
17 const ENDPOINT = 'post';
18
19 const EXCLUDED_POST_TYPES_KEY = 'excluded_post_types';
20 const TERM_KEY = 'term';
21 const KEYS_FORMAT_MAP_KEY = 'keys_format_map';
22 const MAX_COUNT_KEY = 'max_count';
23 const NONCE_KEY = 'x_wp_nonce';
24
25 private ?Wordpress_Adapter_Interface $wp_adapter = null;
26
27 public function __construct( ?Wordpress_Adapter_Interface $wp_adapter = null ) {
28 $this->wp_adapter = $wp_adapter ?? new Wordpress_Adapter();
29 }
30
31 public function register( bool $override_existing_endpoints = false ): void {
32 register_rest_route( self::NAMESPACE, self::ENDPOINT, [
33 [
34 'methods' => \WP_REST_Server::READABLE,
35 'permission_callback' => fn ( \WP_REST_Request $request ) => $this->validate_access_permission( $request ),
36 'args' => $this->get_args(),
37 'sanitize_callback' => 'esc_attr',
38 'callback' => fn ( \WP_REST_Request $request ) => $this->route_wrapper( fn() => $this->get_posts( $request ) ),
39 ],
40 ], $override_existing_endpoints );
41 }
42
43 /**
44 * Builds the query parameters for the REST request.
45 *
46 * @param $args array{
47 * excluded_post_types: array,
48 * keys_format_map: array,
49 * max_count: int,
50 * } The query parameters
51 * @return array The query parameters.
52 */
53 public static function build_query_params( array $args ): array {
54 $allowed_keys = [ self::EXCLUDED_POST_TYPES_KEY, self::KEYS_FORMAT_MAP_KEY, self::MAX_COUNT_KEY ];
55 $keys_to_encode = [ self::EXCLUDED_POST_TYPES_KEY, self::KEYS_FORMAT_MAP_KEY ];
56
57 $params = [];
58
59 foreach ( $args as $key => $value ) {
60 if ( ! in_array( $key, $allowed_keys, true ) || ! isset( $value ) ) {
61 continue;
62 }
63
64 if ( ! in_array( $key, $keys_to_encode, true ) ) {
65 $params[ $key ] = $value;
66 continue;
67 }
68
69 $params[ $key ] = wp_json_encode( $value );
70 }
71
72 return $params;
73 }
74
75 private function validate_access_permission( $request ): bool {
76 $nonce = $request->get_header( self::NONCE_KEY );
77
78 return current_user_can( 'edit_posts' ) && wp_verify_nonce( $nonce, 'wp_rest' );
79 }
80
81 /**
82 * Alters the SQL search query to filter by post title or ID.
83 *
84 * @param string $search_term The original search query.
85 * @param \WP_Query $wp_query The WP_Query instance.
86 * @return string Modified search query.
87 */
88 private function customize_search( string $search_term, \WP_Query $wp_query ) {
89 $term = $wp_query->get( 'search_term' ) ?? '';
90 $is_custom_search = $wp_query->get( 'custom_search' ) ?? false;
91
92 if ( $is_custom_search && ! empty( $term ) ) {
93 $search_term .= ' AND (';
94 $search_term .= "post_title LIKE '%" . esc_sql( $term ) . "%' ";
95 $search_term .= "OR ID LIKE '%" . esc_sql( $term ) . "%')";
96 }
97
98 return $search_term;
99 }
100
101 /**
102 * Wraps the route callback with try/catch to handle exceptions.
103 *
104 * @param callable $cb The route callback.
105 * @return \WP_REST_Response | \WP_Error
106 */
107 private function route_wrapper( callable $cb ) {
108 try {
109 $response = $cb();
110 } catch ( \Exception $e ) {
111 return Error_Builder::make( $e->getCode() )
112 ->set_message( $e->getMessage() )
113 ->build();
114 }
115
116 return $response;
117 }
118
119 /**
120 * Fetches posts based on the search term, formats them based on the keys format map, and returns them.
121 *
122 * @param \WP_REST_Request $request
123 * @return \WP_REST_Response
124 */
125 private function get_posts( \WP_REST_Request $request ) {
126 $params = $request->get_params();
127 $term = $params[ self::TERM_KEY ];
128
129 if ( empty( $term ) ) {
130 return new \WP_REST_Response( [
131 'success' => true,
132 'data' => [
133 'value' => [],
134 ],
135 ], 200 );
136 }
137
138 $excluded_types = $params[ self::EXCLUDED_POST_TYPES_KEY ];
139 $keys_format_map = $params[ self::KEYS_FORMAT_MAP_KEY ];
140 $requested_count = $params[ self::MAX_COUNT_KEY ] ?? 0;
141 $validated_count = max( $requested_count, 1 );
142 $max_count = min( $validated_count, self::MAX_COUNT );
143 $post_types = new Collection( $this->wp_adapter->get_post_types( [ 'public' => true ], 'object' ) );
144
145 $post_types = $post_types->filter( function ( $post_type ) use ( $excluded_types ) {
146 return ! in_array( $post_type->name, $excluded_types, true );
147 } );
148
149 $post_type_slugs = $post_types->map( function ( $post_type ) {
150 return $post_type->name;
151 } );
152
153 $this->add_filter_to_customize_query();
154
155 $posts = new Collection( $this->wp_adapter->get_posts( [
156 'post_type' => $post_type_slugs->all(),
157 'numberposts' => $max_count,
158 'suppress_filters' => false,
159 'custom_search' => true,
160 'search_term' => $term,
161 ] ) );
162
163 $this->remove_filter_to_customize_query();
164
165 return new \WP_REST_Response( [
166 'success' => true,
167 'data' => [
168 'value' => $posts
169 ->map( function ( $post ) use ( $keys_format_map, $post_types ) {
170 $post_object = (array) $post;
171
172 if ( isset( $post_object['post_type'] ) ) {
173 $post_object['post_type'] = $post_types->get( ( $post_object['post_type'] ) )->label;
174 }
175
176 return $this->translate_keys( $post_object, $keys_format_map );
177 } )
178 ->all(),
179 ],
180 ], 200 );
181 }
182
183 /**
184 * Hooks into the flow of wordpress's get_post querying.
185 *
186 * @return void
187 */
188 private function add_filter_to_customize_query() {
189 add_filter( 'posts_search', fn ( $search_term, $wp_query ) => $this->customize_search( $search_term, $wp_query ), 10, 2 );
190 }
191
192 /**
193 * Hooks out of the flow of wordpress's get_post querying.
194 *
195 * @return void
196 */
197 private function remove_filter_to_customize_query() {
198 remove_filter( 'posts_search', fn ( $search_term, $wp_query ) => $this->customize_search( $search_term, $wp_query ), 10, 2 );
199 }
200
201 /**
202 * Arguments for registering an endpoint.
203 *
204 * @return array
205 */
206 private function get_args() {
207 return [
208 self::EXCLUDED_POST_TYPES_KEY => [
209 'description' => 'Post type to exclude',
210 'type' => [ 'array', 'string' ],
211 'required' => false,
212 'default' => [ 'e-floating-buttons', 'e-landing-page', 'elementor_library', 'attachment' ],
213 'sanitize_callback' => fn ( ...$args ) => $this->sanitize_string_array( ...$args ),
214 ],
215 self::TERM_KEY => [
216 'description' => 'Posts to search',
217 'type' => 'string',
218 'required' => false,
219 'default' => '',
220 'sanitize_callback' => 'sanitize_text_field',
221 ],
222 self::KEYS_FORMAT_MAP_KEY => [
223 'description' => 'Specify keys to extract and convert, i.e. ["key_1" => "new_key_1"].',
224 'type' => [ 'array', 'string' ],
225 'required' => false,
226 'default' => [],
227 'sanitize_callback' => fn ( ...$args ) => $this->sanitize_string_array( ...$args ),
228 ],
229 self::MAX_COUNT_KEY => [
230 'description' => 'Max count of returned items',
231 'type' => 'number',
232 'required' => false,
233 'default' => self::MAX_COUNT,
234 ],
235 ];
236 }
237
238 /**
239 * Sanitizes an array of strings.
240 * Ensures the input is an array, converts it if necessary,
241 * and applies `sanitize_text_field` to each element.
242 *
243 * @param Array<string>|string $input The input data, expected to be an array or JSON-encoded string.
244 * @return array The sanitized array of strings.
245 */
246 private function sanitize_string_array( $input ) {
247 if ( ! is_array( $input ) ) {
248 $input = json_decode( sanitize_text_field( $input ) ) ?? [];
249 }
250
251 $array = new Collection( json_decode( json_encode( $input ), true ) );
252
253 return $array
254 ->map( 'sanitize_text_field' )
255 ->all();
256 }
257
258 /**
259 * Replaces array keys based on a dictionary mapping.
260 *
261 * @param array $item The input array with original keys.
262 * @param array $dictionary An associative array mapping old keys to new keys.
263 * @return array The array with translated keys.
264 */
265 private function translate_keys( array $item, array $dictionary ): array {
266 if ( empty( $dictionary ) ) {
267 return $item;
268 }
269
270 $replaced = [];
271
272 foreach ( $item as $key => $value ) {
273 if ( ! isset( $dictionary[ $key ] ) ) {
274 continue;
275 }
276
277 $replaced[ $dictionary[ $key ] ] = $value;
278 }
279
280 return $replaced;
281 }
282 }
283