defaults = array( 'title' => __( 'Recent Posts', 'advance-widget-pack' ), 'post_count' => '5', 'show_image' => '0', 'show_date' => '0', 'show_author' => '0', 'show_category' => '0', 'show_comment_number' => '0', 'show_excerpt' => '0', ); $widget_ops = array( 'classname' => 'awp_recent_posts awp_widget', 'description' => __( 'Widget to display recent posts.', 'advance-widget-pack' ), ); $control_ops = array( 'id_base' => 'awp_recent_posts', 'width' => 200, 'height' => 250, ); parent::__construct( 'awp_recent_posts', __( 'AWP Recent Posts', 'advance-widget-pack' ), $widget_ops, $control_ops ); } public function widget( $args, $instance ) { $instance = wp_parse_args( (array) $instance, $this->defaults ); $title = apply_filters( 'widget_title', $instance['title'] ); $post_count = absint( $instance['post_count'] ); echo $args['before_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- sidebar-provided wrapper markup, trusted per WP core convention. if ( ! empty( $title ) ) { echo $args['before_title'] . esc_html( $title ) . $args['after_title']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- title text is esc_html()'d; before/after_title are sidebar-provided wrapper markup. } $post_ids = self::get_post_ids( $post_count ); if ( $post_ids ) { awp_is_rendering( true ); echo ''; awp_is_rendering( false ); } echo $args['after_widget']; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- sidebar-provided wrapper markup, trusted per WP core convention. } /** * Fetches the recent-post ID list for the given count, using the shared * transient cache. Used by both the widget and the Recent Posts block. * * The 1-50 clamp is applied here rather than in the callers because this * method is the single choke point every path reaches: the widget, the * block's render.php, and any direct static call. block.json's * "type": "number" is an editor-side hint only — WordPress passes block * attributes through uncoerced, so a string "999999" arrives verbatim and * absint() alone would let it straight into posts_per_page. Clamping here * also bounds the transient key space below, since the key is built from * the clamped value. * * @param int $post_count Number of posts to fetch; clamped to 1-50. * @return int[] Post IDs. */ public static function get_post_ids( $post_count ) { $post_count = max( 1, min( 50, absint( $post_count ) ) ); $cache_key = 'awp_recent_posts_' . awp_cache_generation() . '_' . $post_count; $post_ids = get_transient( $cache_key ); if ( false === $post_ids ) { $query = new WP_Query( array( 'posts_per_page' => $post_count, 'post_type' => 'post', 'post_status' => 'publish', 'order' => 'DESC', 'no_found_rows' => true, 'update_post_meta_cache' => false, 'update_post_term_cache' => false, ) ); $post_ids = wp_list_pluck( $query->posts, 'ID' ); set_transient( $cache_key, $post_ids, 5 * MINUTE_IN_SECONDS ); } return $post_ids; } /** * Renders a single post row inside a