| @@ -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( |
| @@ -31,11 +67,26 @@ | ||
| 31 | 67 | function query_args() { |
| 32 | 68 | // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified in the load-more AJAX handler (check_ajax_referer 'upk-site') before this runs. |
| 33 | 69 | if ( isset( $_POST['settings'] ) && is_array( $_POST['settings'] ) ) { |
| 34 | 70 | // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified in the load-more AJAX handler (check_ajax_referer 'upk-site') before this runs. |
| 35 | - extract( map_deep( wp_unslash( $_POST['settings'] ), 'sanitize_text_field' ) ); | |
| 71 | + $request_settings = map_deep( wp_unslash( $_POST['settings'] ), 'sanitize_text_field' ); | |
| 72 | + | |
| 73 | + unset( $request_settings['this'], $request_settings['GLOBALS'] ); | |
| 74 | + | |
| 75 | + extract( $request_settings, EXTR_SKIP ); | |
| 36 | 76 | } |
| 37 | 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 | + | |
| 38 | 89 | // This handler is reachable unauthenticated (wp_ajax_nopriv_*). Clamp the |
| 39 | 90 | // page size to a sane positive maximum so a request cannot ask for -1 |
| 40 | 91 | // ("all posts") or a huge value and turn load-more into a DoS amplifier. |
| 41 | 92 | // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce verified in the load-more AJAX handler (check_ajax_referer 'upk-site') before this runs. |
| @@ -121,8 +172,10 @@ | ||
| 121 | 172 | } |
| 122 | 173 | |
| 123 | 174 | $exclude_by = isset($posts_exclude_by) ? $posts_exclude_by : []; |
| 124 | 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 ] ); | |
| 125 | 178 | $include_users = []; |
| 126 | 179 | $exclude_users = []; |
| 127 | 180 | // print_r($exclude_by); |
| 128 | 181 | /** |
| @@ -145,9 +198,19 @@ | ||
| 145 | 198 | * Set Including Manually |
| 146 | 199 | */ |
| 147 | 200 | $selected_ids = $posts_selected_ids; |
| 148 | 201 | $selected_ids = wp_parse_id_list($selected_ids); |
| 149 | - $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 | + ); | |
| 150 | 213 | if (!empty($selected_ids)) { |
| 151 | 214 | $args['post__in'] = $selected_ids; |
| 152 | 215 | } |
| 153 | 216 | $args['ignore_sticky_posts'] = 1; |
| @@ -165,10 +228,10 @@ | ||
| 165 | 228 | $related_post_id = is_singular() && (0 !== $post_id) ? $post_id : null; |
| 166 | 229 | $args['post_type'] = get_post_type($related_post_id); |
| 167 | 230 | |
| 168 | 231 | // $include_by = $this->getGroupControlQueryParamBy('include'); |
| 169 | - if (in_array('authors', $include_by)) { | |
| 170 | - $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); | |
| 171 | 234 | } else { |
| 172 | 235 | $args['author__in'] = get_post_field('post_author', $related_post_id); |
| 173 | 236 | } |
| 174 | 237 | |
| @@ -266,8 +329,12 @@ | ||
| 266 | 329 | // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query -- Elementor widget query built from user-configured controls; expected behaviour. |
| 267 | 330 | $args['tax_query'] = $terms_query; |
| 268 | 331 | $args['tax_query']['relation'] = 'AND'; |
| 269 | 332 | } |
| 333 | + } | |
| 334 | + | |
| 335 | + if ( empty( $args['post_status'] ) || 'publish' !== $args['post_status'] ) { | |
| 336 | + $args['post_status'] = 'publish'; | |
| 270 | 337 | } |
| 271 | 338 | |
| 272 | 339 | $ajaxposts = new \WP_Query($args); |
| 273 | 340 | return $ajaxposts; |