| @@ -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(); |