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 +133 -19 4.1.1 → 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
@@ -280,9 +369,9 @@
280 369 $placeholder_image_src = Utils::get_placeholder_image_src();
281 370 $image_src = wp_get_attachment_image_src($image_id, $size);
282 371
283 372 if (!$image_src) {
284 - printf('<img class="upk-img" src="%1$s" alt="%2$s">', esc_url($placeholder_image_src), esc_html(get_the_title()));
373 + printf('<img class="upk-img" src="%1$s" alt="%2$s">', esc_url($placeholder_image_src), esc_attr(get_the_title()));
285 374 } else {
286 375 print(wp_get_attachment_image(
287 376 $image_id,
288 377 $size,
@@ -288,9 +377,9 @@
288 377 $size,
289 378 false,
290 379 [
291 380 'class' => 'upk-img',
292 - 'alt' => esc_html(get_the_title())
381 + 'alt' => esc_attr(get_the_title())
293 382 ]
294 383 ));
295 384 }
296 385 }
@@ -308,9 +397,9 @@
308 397 } else {
309 398 $image_src = $image_src[0];
310 399 }
311 400 ?>
312 - <img class="upk-img" src="<?php echo esc_url($image_src); ?>" alt="<?php echo esc_html(get_the_title()); ?>">
401 + <img class="upk-img" src="<?php echo esc_url($image_src); ?>" alt="<?php echo esc_attr(get_the_title()); ?>">
313 402 <?php
314 403 }
315 404
316 405 function render_title($widget_name) {
@@ -317,16 +406,20 @@
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', '');
412 + $title = get_the_title();
322 413 printf(
323 - '<%1$s class="upk-title"><a href="%2$s" title="%3$s" class="title-animation-%4$s" aria-label="%3$s">%3$s</a></%1$s>',
324 - esc_attr(Utils::get_valid_html_tag($settings['title_tags'])),
325 - esc_url( get_permalink() ),
326 - esc_html( get_the_title() ),
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>',
415 + esc_attr(Utils::get_valid_html_tag($settings['title_tags'])),
416 + esc_url( get_permalink() ),
417 + esc_html( $title ),
327 418 esc_attr($settings['title_style']),
419 + esc_attr( $title )
328 420 );
421 + // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- established hook name relied on across the plugin family; renaming would break integration.
329 422 apply_filters('upk/' . $widget_name . '/after/title', '');
330 423 }
331 424
332 425
@@ -336,9 +429,9 @@
336 429 return;
337 430 }
338 431 ?>
339 432 <div class="upk-category">
340 - <?php echo upk_get_category($this->get_settings('posts_source')); ?>
433 + <?php echo wp_kses_post( upk_get_category($this->get_settings('posts_source')) ); ?>
341 434 </div>
342 435 <?php
343 436 }
344 437
@@ -346,8 +439,9 @@
346 439 $settings = $this->get_settings_for_display();
347 440 if (!$this->get_settings('show_date')) {
348 441 return;
349 442 }
443 + $meta_separator = isset( $settings['meta_separator'] ) ? $settings['meta_separator'] : '.';
350 444 ?>
351 445 <div class="upk-date">
352 446 <?php if ($settings['human_diff_time'] == 'yes') {
353 447 echo esc_html( ultimate_post_kit_post_time_diff( ($settings['human_diff_time_short'] == 'yes') ? 'short' : '' ) );
@@ -356,9 +450,9 @@
356 450 } ?>
357 451 </div>
358 452
359 453 <?php if ($settings['show_time']) : ?>
360 - <div class="upk-post-time">
454 + <div class="upk-post-time" data-separator="<?php echo esc_attr( $meta_separator ); ?>">
361 455 <i class="upk-icon-clock" aria-hidden="true"></i>
362 456 <?php echo esc_html( get_the_time() ); ?>
363 457 </div>
364 458 <?php endif; ?>
@@ -379,13 +473,33 @@
379 473 <?php
380 474 if (has_excerpt()) {
381 475 the_excerpt();
382 476 } else {
383 - echo ultimate_post_kit_custom_excerpt($excerpt_length, $strip_shortcode, $ellipsis);
477 + echo wp_kses_post( ultimate_post_kit_custom_excerpt($excerpt_length, $strip_shortcode, $ellipsis) );
384 478 }
385 479 ?>
386 480 </div>
387 481 <?php
482 + }
483 +
484 + /**
485 + * Localized comment count with label. Echo with esc_html().
486 + *
487 + * @param int $post_id Post ID, or 0 for current post in The Loop.
488 + * @return string
489 + */
490 + protected function upk_get_formatted_comments_count( $post_id = 0 ) {
491 + return Utils::get_formatted_comments_count( $post_id );
492 + }
493 +
494 + /**
495 + * Localized comment count number only. Echo with esc_html().
496 + *
497 + * @param int $post_id Post ID, or 0 for current post in The Loop.
498 + * @return string
499 + */
500 + protected function upk_get_localized_comment_count( $post_id = 0 ) {
501 + return Utils::get_localized_comment_count( $post_id );
388 502 }
389 503
390 504 function render_post_format() {
391 505 $settings = $this->get_settings_for_display();