| @@ -39,10 +39,12 @@ | ||
| 39 | 39 | add_action( 'wp_ajax_nopriv_ultp_pagination', array( $this, 'ultp_pagination_callback' ) ); // Page Number AJAX Call Logout User . |
| 40 | 40 | |
| 41 | 41 | add_action( 'wp_ajax_ultp_share_count', array( $this, 'ultp_share_count_callback' ) ); // share Count save . |
| 42 | 42 | add_action( 'wp_ajax_nopriv_ultp_share_count', array( $this, 'ultp_share_count_callback' ) ); // share Count save . |
| 43 | - add_action( 'wp_ajax_ultp_get_nonce', array( $this, 'ultp_get_nonce_callback' ) ); // Nonce Generating Callback | |
| 44 | - add_action( 'wp_ajax_nopriv_ultp_get_nonce', array( $this, 'ultp_get_nonce_callback' ) ); // Nonce Generating Callback | |
| 43 | + // Public by design — see ultp_get_nonce_callback() for why, and for what | |
| 44 | + // 'ultp-public-nonce' does and does not protect. | |
| 45 | + add_action( 'wp_ajax_ultp_get_nonce', array( $this, 'ultp_get_nonce_callback' ) ); | |
| 46 | + add_action( 'wp_ajax_nopriv_ultp_get_nonce', array( $this, 'ultp_get_nonce_callback' ) ); | |
| 45 | 47 | } |
| 46 | 48 | |
| 47 | 49 | /** |
| 48 | 50 | * Require Blocks |
| @@ -227,9 +229,9 @@ | ||
| 227 | 229 | ); |
| 228 | 230 | $value['attrs']['queryExclude'] = wp_json_encode( $queryArr ); |
| 229 | 231 | } |
| 230 | 232 | $attr = array_merge( $attr, $value['attrs'] ); |
| 231 | - echo $objectBlock->content($attr, true); //phpcs:ignore | |
| 233 | + echo $objectBlock->content( $attr, true ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Block render output; each block escapes its own markup. | |
| 232 | 234 | die(); |
| 233 | 235 | } |
| 234 | 236 | } |
| 235 | 237 | if ( ! empty( $value['innerBlocks'] ) ) { |
| @@ -246,9 +248,9 @@ | ||
| 246 | 248 | * |
| 247 | 249 | * @return STRING The AJAX response. |
| 248 | 250 | */ |
| 249 | 251 | public function ultp_next_prev_callback() { |
| 250 | - if ( ! ( isset( $_REQUEST['wpnonce'] ) && wp_verify_nonce( sanitize_key( wp_unslash( $_REQUEST['wpnonce'] ) ), 'ultp-nonce' ) ) ) { | |
| 252 | + if ( ! $this->verify_public_nonce() ) { | |
| 251 | 253 | return; |
| 252 | 254 | } |
| 253 | 255 | |
| 254 | 256 | $paged = isset( $_POST['paged'] ) ? sanitize_text_field( $_POST['paged'] ) : ''; |
| @@ -340,9 +342,9 @@ | ||
| 340 | 342 | * |
| 341 | 343 | * @return STRING The AJAX response. |
| 342 | 344 | */ |
| 343 | 345 | public function ultp_filter_callback() { |
| 344 | - if ( ! ( isset( $_REQUEST['wpnonce'] ) && wp_verify_nonce( sanitize_key( wp_unslash( $_REQUEST['wpnonce'] ) ), 'ultp-nonce' ) ) ) { | |
| 346 | + if ( ! $this->verify_public_nonce() ) { | |
| 345 | 347 | return; |
| 346 | 348 | } |
| 347 | 349 | |
| 348 | 350 | $taxtype = isset( $_POST['taxtype'] ) ? sanitize_text_field( $_POST['taxtype'] ) : ''; |
| @@ -380,9 +382,9 @@ | ||
| 380 | 382 | * |
| 381 | 383 | * @return STRING The AJAX response. |
| 382 | 384 | */ |
| 383 | 385 | public function ultp_adv_filter_callback() { |
| 384 | - if ( ! ( isset( $_REQUEST['wpnonce'] ) && wp_verify_nonce( sanitize_key( wp_unslash( $_REQUEST['wpnonce'] ) ), 'ultp-nonce' ) ) ) { | |
| 386 | + if ( ! $this->verify_public_nonce() ) { | |
| 385 | 387 | return; |
| 386 | 388 | } |
| 387 | 389 | |
| 388 | 390 | $blockId = isset( $_POST['blockId'] ) ? sanitize_text_field( $_POST['blockId'] ) : ''; |
| @@ -438,9 +440,9 @@ | ||
| 438 | 440 | * |
| 439 | 441 | * @return STRING The AJAX response. |
| 440 | 442 | */ |
| 441 | 443 | public function ultp_pagination_callback() { |
| 442 | - if ( ! ( isset( $_REQUEST['wpnonce'] ) && wp_verify_nonce( sanitize_key( wp_unslash( $_REQUEST['wpnonce'] ) ), 'ultp-nonce' ) ) ) { | |
| 444 | + if ( ! $this->verify_public_nonce() ) { | |
| 443 | 445 | return; |
| 444 | 446 | } |
| 445 | 447 | |
| 446 | 448 | $paged = isset( $_POST['paged'] ) ? sanitize_text_field( $_POST['paged'] ) : ''; |
| @@ -511,9 +513,9 @@ | ||
| 511 | 513 | * |
| 512 | 514 | * @return void |
| 513 | 515 | */ |
| 514 | 516 | public function ultp_share_count_callback() { |
| 515 | - if ( ! ( isset( $_REQUEST['wpnonce'] ) && wp_verify_nonce( sanitize_key( wp_unslash( $_REQUEST['wpnonce'] ) ), 'ultp-nonce' ) ) ) { | |
| 517 | + if ( ! $this->verify_public_nonce() ) { | |
| 516 | 518 | return; |
| 517 | 519 | } |
| 518 | 520 | |
| 519 | 521 | $post_id = isset( $_POST['postId'] ) ? absint( $_POST['postId'] ) : 0; |
| @@ -661,13 +663,15 @@ | ||
| 661 | 663 | $filter_attributes['queryQuick'] = $adv_filter_data['adv_sort']; |
| 662 | 664 | $filter_attributes['queryOrderBy'] = |
| 663 | 665 | ! empty( $adv_filter_data['orderby'] ) ? $adv_filter_data['orderby'] : ( $value['attrs']['queryOrderBy'] ?? '' ); |
| 664 | 666 | } |
| 665 | - | |
| 667 | + $pagination = $this->pagination_for_filter( $attr, $postId, $blockRaw, $filter_attributes ); | |
| 666 | 668 | $toReturn = array( |
| 667 | 669 | 'blocks' => $objectBlock->content( $attr, true ), |
| 668 | 670 | 'notFound' => isset( $attr['notFoundMessage'] ) ? $attr['notFoundMessage'] : '', |
| 669 | - 'pagination' => $this->pagination_for_filter( $attr, $postId, $blockRaw, $filter_attributes ), | |
| 671 | + 'pagination' => $pagination['pagination_content'], | |
| 672 | + 'data_attrs' => $pagination['data_attrs'], | |
| 673 | + 'data_all_attrs' => $pagination['data_attrs'], | |
| 670 | 674 | 'paginationType' => $attr['paginationType'], |
| 671 | 675 | 'paginationShow' => $attr['paginationShow'], |
| 672 | 676 | ); |
| 673 | 677 | } |
| @@ -723,15 +727,81 @@ | ||
| 723 | 727 | |
| 724 | 728 | $wraper_after .= '</div>'; |
| 725 | 729 | } |
| 726 | 730 | wp_reset_query(); |
| 731 | + | |
| 732 | + // queryQuick | |
| 733 | + return array( | |
| 734 | + 'pagination_content' => $wraper_after, | |
| 735 | + 'data_attrs' => wp_json_encode( array( | |
| 736 | + 'paged' => 1, | |
| 737 | + 'blockid' => $attr['blockId'], | |
| 738 | + 'postid' => $postId, | |
| 739 | + 'pages' => $pageNum, | |
| 740 | + 'blockname' => $blockRaw, | |
| 741 | + 'filter_value' => isset( $filter_attributes['queryTaxValue'] ) ? $filter_attributes['queryTaxValue'] : '', | |
| 742 | + 'filter_type' => isset( $filter_attributes['queryTax'] ) ? $filter_attributes['queryTax'] : '', | |
| 743 | + 'filter_author' => isset( $filter_attributes['queryAuthor'] ) ? $filter_attributes['queryAuthor'] : '', | |
| 744 | + 'filter_order' => isset( $filter_attributes['queryOrder'] ) ? $filter_attributes['queryOrder'] : 'DESC', | |
| 745 | + 'filter_orderby' => isset( $filter_attributes['queryOrderBy'] ) ? $filter_attributes['queryOrderBy'] : '', | |
| 746 | + 'filter_search' => isset( $filter_attributes['querySearch'] ) ? $filter_attributes['querySearch'] : '', | |
| 747 | + 'filter_queryQuick' => isset( $filter_attributes['queryQuick'] ) ? $filter_attributes['queryQuick'] : '', | |
| 748 | + 'filter_for' => 'ultp-block-' . $attr['blockId'], | |
| 749 | + )), | |
| 750 | + ); | |
| 751 | + } | |
| 727 | 752 | |
| 728 | - return $wraper_after; | |
| 753 | + /** | |
| 754 | + * Verify the front-end AJAX nonce, or end the request. | |
| 755 | + * | |
| 756 | + * Fails with a machine-readable 403 rather than the bare `return` these | |
| 757 | + * handlers used to do. A bare return lets admin-ajax emit "0" at HTTP 200, | |
| 758 | + * which the client cannot tell apart from a real response — ultp_next_prev's | |
| 759 | + * success handler treated that "0" as markup and rendered it into the block. | |
| 760 | + * | |
| 761 | + * The 'ultp_invalid_nonce' code is a contract with assets/js/ultp.js: on | |
| 762 | + * seeing it the client discards its cached nonce, fetches a fresh one and | |
| 763 | + * retries once. A nonce legitimately expires while a tab is open (WordPress | |
| 764 | + * nonces last 12-24h, tabs last longer), so this is a routine condition and | |
| 765 | + * not an attack signal. See ultp_get_nonce_callback() for what this nonce | |
| 766 | + * does and does not protect. | |
| 767 | + * | |
| 768 | + * @since v.5.1.0 | |
| 769 | + * | |
| 770 | + * @return BOOLEAN True when the nonce is valid; otherwise the request ends. | |
| 771 | + */ | |
| 772 | + private function verify_public_nonce() { | |
| 773 | + if ( isset( $_REQUEST['wpnonce'] ) && wp_verify_nonce( sanitize_key( wp_unslash( $_REQUEST['wpnonce'] ) ), 'ultp-public-nonce' ) ) { | |
| 774 | + return true; | |
| 775 | + } | |
| 776 | + | |
| 777 | + wp_send_json_error( | |
| 778 | + array( | |
| 779 | + 'code' => 'ultp_invalid_nonce', | |
| 780 | + 'message' => __( 'Security token expired. Please try again.', 'ultimate-post' ), | |
| 781 | + ), | |
| 782 | + 403 | |
| 783 | + ); | |
| 784 | + | |
| 785 | + return false; | |
| 729 | 786 | } |
| 730 | 787 | |
| 731 | 788 | /** |
| 732 | - * Nonce Generation Callback | |
| 789 | + * Vends the front-end AJAX nonce. | |
| 733 | 790 | * |
| 791 | + * Public by design: full-page caching makes a nonce embedded in HTML stale for | |
| 792 | + * most visitors, so front-end blocks fetch one at runtime instead. | |
| 793 | + * | |
| 794 | + * SECURITY: because this endpoint is public, 'ultp-public-nonce' is NOT a CSRF | |
| 795 | + * control on the front end — anyone can request one. It is a request-shape | |
| 796 | + * check only. Any nopriv handler that changes state MUST carry its own | |
| 797 | + * authorisation; see ultp_share_count_callback() for the pattern (post status, | |
| 798 | + * capability and rate-limit checks, none of which depend on the nonce). | |
| 799 | + * | |
| 800 | + * This endpoint vends the PUBLIC nonce only. Admin routes verify the separate | |
| 801 | + * 'ultp-admin-nonce', which is never vended here and is always paired with a | |
| 802 | + * capability check. Do not merge the two. | |
| 803 | + * | |
| 734 | 804 | * @since v.5.0.6 |
| 735 | 805 | * |
| 736 | 806 | * @return STRING The AJAX response. |
| 737 | 807 | */ |
| @@ -738,9 +808,9 @@ | ||
| 738 | 808 | public function ultp_get_nonce_callback() { |
| 739 | 809 | nocache_headers(); |
| 740 | 810 | wp_send_json_success( |
| 741 | 811 | array( |
| 742 | - 'nonce' => wp_create_nonce( 'ultp-nonce' ), | |
| 812 | + 'nonce' => wp_create_nonce( 'ultp-public-nonce' ), | |
| 743 | 813 | ) |
| 744 | 814 | ); |
| 745 | 815 | } |
| 746 | 816 | } |