PluginProbe
Ultimate Post Kit – Elementor Post Grid, Post Carousel, Post Slider & Blog Layout Widgets / 4.5.6
Ultimate Post Kit – Elementor Post Grid, Post Carousel, Post Slider & Blog Layout Widgets v4.5.6
4.5.6 4.5.5 4.5.4 4.2.1 4.2.2 4.2.3 4.5.0 4.5.2 4.5.3 4.2.0 4.1.18 4.1.17 4.1.16 4.1.15 4.1.14 4.1.13 4.1.12 4.1.11 4.1.10 4.1.9 4.1.8 4.0.9 4.1.0 4.1.1 4.1.2 All 148 releases
← All changes | traits/global-widget-functions.php +100 -9 4.2.3 → 4.5.6 View file →
@@ -8,8 +8,44 @@
8 8
9 9 trait Global_Widget_Functions {
10 10
11 11 /**
12 + * Default display flags for the load-more AJAX handlers.
13 + *
14 + * The widgets always emit every one of these keys into their data-settings
15 + * payload, so this never changes rendering for a real request. It only matters
16 + * for the unauthenticated wp_ajax_nopriv_* endpoints, where a partial payload
17 + * would otherwise make the render loop read undefined array keys and emit a
18 + * PHP warning per missing key. Values mirror the widgets' own defaults.
19 + *
20 + * @return array<string, string>
21 + */
22 + protected function loadmore_display_defaults() {
23 + return [
24 + 'show_title' => 'yes',
25 + 'title_tags' => 'h3',
26 + 'title_style' => 'underline',
27 + 'show_author' => 'yes',
28 + 'show_author_name' => 'yes',
29 + 'show_author_avatar' => 'yes',
30 + 'show_date' => 'yes',
31 + 'show_time' => 'no',
32 + 'show_category' => 'yes',
33 + 'show_excerpt' => 'yes',
34 + 'show_readmore' => 'yes',
35 + 'readmore_type' => '',
36 + 'show_comments' => 'no',
37 + 'show_image' => 'yes',
38 + 'show_post_format' => 'no',
39 + 'show_reading_time' => 'no',
40 + 'show_counter_number' => 'no',
41 + 'human_diff_time' => 'no',
42 + 'upk_link_new_tab' => 'no',
43 + 'meta_separator' => '//',
44 + ];
45 + }
46 +
47 + /**
12 48 * Render Ajax Qery Posts
13 49 */
14 50 function mapGroupControlQuery($term_ids = []) {
15 51 $terms = get_terms(
@@ -28,18 +64,46 @@
28 64
29 65 return $tax_terms_map;
30 66 }
31 67 function query_args() {
32 - extract($_POST['settings']);
68 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified in the load-more AJAX handler (check_ajax_referer 'upk-site') before this runs.
69 + if ( isset( $_POST['settings'] ) && is_array( $_POST['settings'] ) ) {
70 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified in the load-more AJAX handler (check_ajax_referer 'upk-site') before this runs.
71 + $request_settings = map_deep( wp_unslash( $_POST['settings'] ), 'sanitize_text_field' );
33 72
73 + unset( $request_settings['this'], $request_settings['GLOBALS'] );
74 +
75 + extract( $request_settings, EXTR_SKIP );
76 + }
77 +
78 + // extract() only defines what the request actually sent, and this handler is
79 + // reachable unauthenticated, so any omitted key would leave the matching
80 + // variable undefined. The three below are consumed unconditionally further
81 + // down (orderby/order in $args, and $posts_select_date in the date query),
82 + // unlike their siblings which are isset()-guarded at the point of use.
83 + // Seed them so a partial request cannot emit PHP warnings or push a null
84 + // into WP_Query.
85 + $posts_orderby = isset( $posts_orderby ) ? $posts_orderby : 'date';
86 + $posts_order = isset( $posts_order ) ? $posts_order : 'DESC';
87 + $posts_select_date = isset( $posts_select_date ) ? $posts_select_date : '';
88 +
89 + // This handler is reachable unauthenticated (wp_ajax_nopriv_*). Clamp the
90 + // page size to a sane positive maximum so a request cannot ask for -1
91 + // ("all posts") or a huge value and turn load-more into a DoS amplifier.
92 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified in the load-more AJAX handler (check_ajax_referer 'upk-site') before this runs.
93 + $per_page = isset( $_POST['per_page'] ) ? absint( $_POST['per_page'] ) : 0;
94 + $per_page = max( 1, min( 100, $per_page ) );
95 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified in the load-more AJAX handler (check_ajax_referer 'upk-site') before this runs.
96 + $offset = isset( $_POST['offset'] ) ? absint( $_POST['offset'] ) : 0;
97 +
34 98 // setmeta args
35 99 $args = [
36 - 'posts_per_page' => $_POST['per_page'],
100 + 'posts_per_page' => $per_page,
37 101 'post_status' => 'publish',
38 102 'suppress_filters' => false,
39 103 'orderby' => $posts_orderby,
40 104 'order' => $posts_order,
41 - 'offset' => $_POST['offset'],
105 + 'offset' => $offset,
42 106 ];
43 107 /**
44 108 * set feature image
45 109 *
@@ -44,8 +108,9 @@
44 108 * set feature image
45 109 *
46 110 */
47 111 if (isset($posts_only_with_featured_image) && $posts_only_with_featured_image === 'yes') {
112 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Elementor widget query built from user-configured controls; expected behaviour.
48 113 $args['meta_query'] = [
49 114 [
50 115 'key' => '_thumbnail_id',
51 116 'compare' => 'EXISTS',
@@ -107,8 +172,10 @@
107 172 }
108 173
109 174 $exclude_by = isset($posts_exclude_by) ? $posts_exclude_by : [];
110 175 $include_by = isset($posts_include_by) ? $posts_include_by : [];
176 + $exclude_by = is_array($exclude_by) ? $exclude_by : ( '' === $exclude_by || null === $exclude_by ? [] : [ $exclude_by ] );
177 + $include_by = is_array($include_by) ? $include_by : ( '' === $include_by || null === $include_by ? [] : [ $include_by ] );
111 178 $include_users = [];
112 179 $exclude_users = [];
113 180 // print_r($exclude_by);
114 181 /**
@@ -116,8 +183,9 @@
116 183 */
117 184 if (!empty($exclude_by) && $posts_source === 'post' && $posts_ignore_sticky_posts === 'yes') {
118 185 $args['ignore_sticky_posts'] = true;
119 186 if (in_array('current_post', $exclude_by)) {
187 + // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in -- Elementor widget query built from user-configured controls; expected behaviour.
120 188 $args['post__not_in'] = [get_the_ID()];
121 189 }
122 190 }
123 191
@@ -130,9 +198,19 @@
130 198 * Set Including Manually
131 199 */
132 200 $selected_ids = $posts_selected_ids;
133 201 $selected_ids = wp_parse_id_list($selected_ids);
134 - $args['post_type'] = 'any';
202 +
203 + // Both $posts_source and $posts_selected_ids arrive from $_POST on the
204 + // wp_ajax_nopriv_* load-more handlers, so an anonymous caller can pick this
205 + // branch and name arbitrary IDs. 'any' only filters on exclude_from_search,
206 + // which is weaker than the allowlist the else branch below applies: a post
207 + // type registered public => true, publicly_queryable => false is rejected by
208 + // is_post_type_viewable() but still matched by 'any'. Route this branch
209 + // through the same allowlist so every path out of query_args() agrees.
210 + $args['post_type'] = ultimate_post_kit_sanitize_public_post_type(
211 + array_values( get_post_types( [ 'public' => true, 'exclude_from_search' => false ] ) )
212 + );
135 213 if (!empty($selected_ids)) {
136 214 $args['post__in'] = $selected_ids;
137 215 }
138 216 $args['ignore_sticky_posts'] = 1;
@@ -140,9 +218,9 @@
140 218 /**
141 219 * Make Current Query
142 220 */
143 221 $args = $GLOBALS['wp_query']->query_vars;
144 - $args = apply_filters('element_pack/query/get_query_args/current_query', $args);
222 + $args = apply_filters('ultimate_post_kit/query/get_query_args/current_query', $args);
145 223 } elseif ('_related_post_type' === $posts_source) {
146 224 /**
147 225 * Set Related Query
148 226 */
@@ -150,10 +228,10 @@
150 228 $related_post_id = is_singular() && (0 !== $post_id) ? $post_id : null;
151 229 $args['post_type'] = get_post_type($related_post_id);
152 230
153 231 // $include_by = $this->getGroupControlQueryParamBy('include');
154 - if (in_array('authors', $include_by)) {
155 - $args['author__in'] = wp_parse_id_list($settings['posts_include_author_ids']);
232 + if (in_array('authors', $include_by) && isset($posts_include_author_ids)) {
233 + $args['author__in'] = wp_parse_id_list($posts_include_author_ids);
156 234 } else {
157 235 $args['author__in'] = get_post_field('post_author', $related_post_id);
158 236 }
159 237
@@ -162,15 +240,20 @@
162 240 $args['author__not_in'] = wp_parse_id_list($posts_exclude_author_ids);
163 241 }
164 242
165 243 if (in_array('current_post', $exclude_by)) {
244 + // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in -- Elementor widget query built from user-configured controls; expected behaviour.
166 245 $args['post__not_in'] = [get_the_ID()];
167 246 }
168 247
169 248 $args['ignore_sticky_posts'] = 1;
170 - $args = apply_filters('element_pack/query/get_query_args/related_query', $args);
249 + $args = apply_filters('ultimate_post_kit/query/get_query_args/related_query', $args);
171 250 } else {
172 - $args['post_type'] = $posts_source;
251 + // This runs on wp_ajax_nopriv_* and $posts_source comes straight from $_POST,
252 + // so restrict it to post types this site already exposes to anonymous visitors.
253 + // Without this a request can enumerate published entries of post types that are
254 + // deliberately hidden from anonymous access (e.g. Elementor's elementor_library).
255 + $args['post_type'] = ultimate_post_kit_sanitize_public_post_type( $posts_source );
173 256 $current_post = [];
174 257
175 258 /**
176 259 * Set Taxonomy && Set Authors
@@ -194,8 +277,9 @@
194 277 $current_post[] = get_the_ID();
195 278 }
196 279 if (in_array('manual_selection', $exclude_by)) {
197 280 $exclude_ids = $posts_exclude_ids;
281 + // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in -- Elementor widget query built from user-configured controls; expected behaviour.
198 282 $args['post__not_in'] = array_merge($current_post, wp_parse_id_list($exclude_ids));
199 283 }
200 284 if (in_array('terms', $exclude_by)) {
201 285 $exclude_terms = wp_parse_id_list($posts_exclude_term_ids);
@@ -241,13 +325,18 @@
241 325 }
242 326
243 327
244 328 if (!empty($terms_query)) {
329 + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query -- Elementor widget query built from user-configured controls; expected behaviour.
245 330 $args['tax_query'] = $terms_query;
246 331 $args['tax_query']['relation'] = 'AND';
247 332 }
248 333 }
249 334
335 + if ( empty( $args['post_status'] ) || 'publish' !== $args['post_status'] ) {
336 + $args['post_status'] = 'publish';
337 + }
338 +
250 339 $ajaxposts = new \WP_Query($args);
251 340 return $ajaxposts;
252 341 }
253 342
@@ -317,8 +406,9 @@
317 406 $settings = $this->get_settings_for_display();
318 407 if (!$this->get_settings('show_title')) {
319 408 return;
320 409 }
410 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- established hook name relied on across the plugin family; renaming would break integration.
321 411 apply_filters('upk/' . $widget_name . '/before/title', '');
322 412 $title = get_the_title();
323 413 printf(
324 414 '<%1$s class="upk-title"><a href="%2$s" title="%5$s" class="title-animation-%4$s" aria-label="%5$s">%3$s</a></%1$s>',
@@ -327,8 +417,9 @@
327 417 esc_html( $title ),
328 418 esc_attr($settings['title_style']),
329 419 esc_attr( $title )
330 420 );
421 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- established hook name relied on across the plugin family; renaming would break integration.
331 422 apply_filters('upk/' . $widget_name . '/after/title', '');
332 423 }
333 424
334 425