| @@ -1,5 +1,8 @@ | ||
| 1 | 1 | <?php |
| 2 | +// phpcs:ignore | |
| 3 | +// @codingStandardsIgnoreFile | |
| 4 | +// @ignoreWarnings | |
| 2 | 5 | /** |
| 3 | 6 | * Require All Blocks and handle block ajax action |
| 4 | 7 | * |
| 5 | 8 | * @package ULTP\Blocks |
| @@ -6,9 +9,8 @@ | ||
| 6 | 9 | * @since 4.1.11 |
| 7 | 10 | */ |
| 8 | 11 | |
| 9 | 12 | namespace ULTP; |
| 10 | - | |
| 11 | 13 | defined( 'ABSPATH' ) || exit; |
| 12 | 14 | |
| 13 | 15 | /** |
| 14 | 16 | * Blocks class. |
| @@ -37,10 +39,12 @@ | ||
| 37 | 39 | add_action( 'wp_ajax_nopriv_ultp_pagination', array( $this, 'ultp_pagination_callback' ) ); // Page Number AJAX Call Logout User . |
| 38 | 40 | |
| 39 | 41 | add_action( 'wp_ajax_ultp_share_count', array( $this, 'ultp_share_count_callback' ) ); // share Count save . |
| 40 | 42 | add_action( 'wp_ajax_nopriv_ultp_share_count', array( $this, 'ultp_share_count_callback' ) ); // share Count save . |
| 41 | - add_action( 'wp_ajax_ultp_get_nonce', array( $this, 'ultp_get_nonce_callback' ) ); // Nonce Generating Callback | |
| 42 | - 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' ) ); | |
| 43 | 47 | } |
| 44 | 48 | |
| 45 | 49 | /** |
| 46 | 50 | * Require Blocks |
| @@ -225,9 +229,9 @@ | ||
| 225 | 229 | ); |
| 226 | 230 | $value['attrs']['queryExclude'] = wp_json_encode( $queryArr ); |
| 227 | 231 | } |
| 228 | 232 | $attr = array_merge( $attr, $value['attrs'] ); |
| 229 | - 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. | |
| 230 | 234 | die(); |
| 231 | 235 | } |
| 232 | 236 | } |
| 233 | 237 | if ( ! empty( $value['innerBlocks'] ) ) { |
| @@ -244,9 +248,9 @@ | ||
| 244 | 248 | * |
| 245 | 249 | * @return STRING The AJAX response. |
| 246 | 250 | */ |
| 247 | 251 | public function ultp_next_prev_callback() { |
| 248 | - if ( ! ( isset( $_REQUEST['wpnonce'] ) && wp_verify_nonce( sanitize_key( wp_unslash( $_REQUEST['wpnonce'] ) ), 'ultp-nonce' ) ) ) { | |
| 252 | + if ( ! $this->verify_public_nonce() ) { | |
| 249 | 253 | return; |
| 250 | 254 | } |
| 251 | 255 | |
| 252 | 256 | $paged = isset( $_POST['paged'] ) ? sanitize_text_field( $_POST['paged'] ) : ''; |
| @@ -338,9 +342,9 @@ | ||
| 338 | 342 | * |
| 339 | 343 | * @return STRING The AJAX response. |
| 340 | 344 | */ |
| 341 | 345 | public function ultp_filter_callback() { |
| 342 | - if ( ! ( isset( $_REQUEST['wpnonce'] ) && wp_verify_nonce( sanitize_key( wp_unslash( $_REQUEST['wpnonce'] ) ), 'ultp-nonce' ) ) ) { | |
| 346 | + if ( ! $this->verify_public_nonce() ) { | |
| 343 | 347 | return; |
| 344 | 348 | } |
| 345 | 349 | |
| 346 | 350 | $taxtype = isset( $_POST['taxtype'] ) ? sanitize_text_field( $_POST['taxtype'] ) : ''; |
| @@ -378,9 +382,9 @@ | ||
| 378 | 382 | * |
| 379 | 383 | * @return STRING The AJAX response. |
| 380 | 384 | */ |
| 381 | 385 | public function ultp_adv_filter_callback() { |
| 382 | - if ( ! ( isset( $_REQUEST['wpnonce'] ) && wp_verify_nonce( sanitize_key( wp_unslash( $_REQUEST['wpnonce'] ) ), 'ultp-nonce' ) ) ) { | |
| 386 | + if ( ! $this->verify_public_nonce() ) { | |
| 383 | 387 | return; |
| 384 | 388 | } |
| 385 | 389 | |
| 386 | 390 | $blockId = isset( $_POST['blockId'] ) ? sanitize_text_field( $_POST['blockId'] ) : ''; |
| @@ -436,9 +440,9 @@ | ||
| 436 | 440 | * |
| 437 | 441 | * @return STRING The AJAX response. |
| 438 | 442 | */ |
| 439 | 443 | public function ultp_pagination_callback() { |
| 440 | - if ( ! ( isset( $_REQUEST['wpnonce'] ) && wp_verify_nonce( sanitize_key( wp_unslash( $_REQUEST['wpnonce'] ) ), 'ultp-nonce' ) ) ) { | |
| 444 | + if ( ! $this->verify_public_nonce() ) { | |
| 441 | 445 | return; |
| 442 | 446 | } |
| 443 | 447 | |
| 444 | 448 | $paged = isset( $_POST['paged'] ) ? sanitize_text_field( $_POST['paged'] ) : ''; |
| @@ -509,9 +513,9 @@ | ||
| 509 | 513 | * |
| 510 | 514 | * @return void |
| 511 | 515 | */ |
| 512 | 516 | public function ultp_share_count_callback() { |
| 513 | - if ( ! ( isset( $_REQUEST['wpnonce'] ) && wp_verify_nonce( sanitize_key( wp_unslash( $_REQUEST['wpnonce'] ) ), 'ultp-nonce' ) ) ) { | |
| 517 | + if ( ! $this->verify_public_nonce() ) { | |
| 514 | 518 | return; |
| 515 | 519 | } |
| 516 | 520 | |
| 517 | 521 | $post_id = isset( $_POST['postId'] ) ? absint( $_POST['postId'] ) : 0; |
| @@ -592,8 +596,19 @@ | ||
| 592 | 596 | * @return ARRAY |
| 593 | 597 | */ |
| 594 | 598 | public function filter_content_return( $blocks, $blockId, $blockRaw, $blockName, $taxtype, $taxonomy, $postId, &$toReturn, $widgetBlockId = '', $adv_filter_data = array(), $ultp_uniqueIds = array(), $ultp_current_unique_posts = array() ) { |
| 595 | 599 | foreach ( $blocks as $key => $value ) { |
| 600 | + if ( ( $value['blockName'] ?? '' ) === 'core/shortcode' ) { | |
| 601 | + if ( preg_match( '/id="([^"]+)"/', $value['innerHTML'] ?? '', $matches ) ) { | |
| 602 | + $template_post = get_post( $matches[1] ); | |
| 603 | + if ( $template_post && ! empty( $template_post->post_content ) ) { | |
| 604 | + $template_blocks = parse_blocks( $template_post->post_content ); | |
| 605 | + // recurse into the expanded blocks instead | |
| 606 | + $this->filter_content_return( $template_blocks, $blockId, $blockRaw, $blockName, $taxtype, $taxonomy, $postId, $toReturn, $widgetBlockId, $adv_filter_data, $ultp_uniqueIds, $ultp_current_unique_posts ); | |
| 607 | + continue; // skip the shortcode block itself | |
| 608 | + } | |
| 609 | + } | |
| 610 | + } | |
| 596 | 611 | if ( $blockName == $value['blockName'] ) { |
| 597 | 612 | if ( $value['attrs']['blockId'] == $blockId ) { |
| 598 | 613 | $objName = str_replace( ' ', '_', ucwords( str_replace( array( 'ultimate-post/', '-' ), array( '', ' ' ), $blockName ) ) ); |
| 599 | 614 | $new_obj = '\ULTP\blocks\\' . $objName; |
| @@ -648,13 +663,15 @@ | ||
| 648 | 663 | $filter_attributes['queryQuick'] = $adv_filter_data['adv_sort']; |
| 649 | 664 | $filter_attributes['queryOrderBy'] = |
| 650 | 665 | ! empty( $adv_filter_data['orderby'] ) ? $adv_filter_data['orderby'] : ( $value['attrs']['queryOrderBy'] ?? '' ); |
| 651 | 666 | } |
| 652 | - | |
| 667 | + $pagination = $this->pagination_for_filter( $attr, $postId, $blockRaw, $filter_attributes ); | |
| 653 | 668 | $toReturn = array( |
| 654 | 669 | 'blocks' => $objectBlock->content( $attr, true ), |
| 655 | 670 | 'notFound' => isset( $attr['notFoundMessage'] ) ? $attr['notFoundMessage'] : '', |
| 656 | - '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'], | |
| 657 | 674 | 'paginationType' => $attr['paginationType'], |
| 658 | 675 | 'paginationShow' => $attr['paginationShow'], |
| 659 | 676 | ); |
| 660 | 677 | } |
| @@ -710,15 +727,81 @@ | ||
| 710 | 727 | |
| 711 | 728 | $wraper_after .= '</div>'; |
| 712 | 729 | } |
| 713 | 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 | + } | |
| 714 | 752 | |
| 715 | - 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; | |
| 716 | 786 | } |
| 717 | 787 | |
| 718 | 788 | /** |
| 719 | - * Nonce Generation Callback | |
| 789 | + * Vends the front-end AJAX nonce. | |
| 720 | 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 | + * | |
| 721 | 804 | * @since v.5.0.6 |
| 722 | 805 | * |
| 723 | 806 | * @return STRING The AJAX response. |
| 724 | 807 | */ |
| @@ -725,9 +808,9 @@ | ||
| 725 | 808 | public function ultp_get_nonce_callback() { |
| 726 | 809 | nocache_headers(); |
| 727 | 810 | wp_send_json_success( |
| 728 | 811 | array( |
| 729 | - 'nonce' => wp_create_nonce( 'ultp-nonce' ), | |
| 812 | + 'nonce' => wp_create_nonce( 'ultp-public-nonce' ), | |
| 730 | 813 | ) |
| 731 | 814 | ); |
| 732 | 815 | } |
| 733 | 816 | } |