| @@ -5,19 +5,10 @@ | ||
| 5 | 5 | |
| 6 | 6 | class FrmFormsListHelper extends FrmListHelper { |
| 7 | 7 | |
| 8 | 8 | /** |
| 9 | - * The transient name that stores data for which posts a form is embedded in. | |
| 10 | - * | |
| 11 | - * @since 6.32 | |
| 12 | - * | |
| 13 | 9 | * @var string |
| 14 | 10 | */ |
| 15 | - private static $embed_posts_transient_name = 'frm_posts_contain_form'; | |
| 16 | - | |
| 17 | - /** | |
| 18 | - * @var string | |
| 19 | - */ | |
| 20 | 11 | public $status = ''; |
| 21 | 12 | |
| 22 | 13 | public $total_items = 0; |
| 23 | 14 | |
| @@ -133,8 +124,32 @@ | ||
| 133 | 124 | /** |
| 134 | 125 | * @return void |
| 135 | 126 | */ |
| 136 | 127 | public function no_items() { |
| 128 | + $s = self::get_param( | |
| 129 | + array( | |
| 130 | + 'param' => 's', | |
| 131 | + 'sanitize' => 'sanitize_text_field', | |
| 132 | + ) | |
| 133 | + ); | |
| 134 | + | |
| 135 | + if ( $s !== '' ) { | |
| 136 | + $current_url = set_url_scheme( | |
| 137 | + 'http://' . FrmAppHelper::get_server_value( 'HTTP_HOST' ) . FrmAppHelper::get_server_value( 'REQUEST_URI' ) | |
| 138 | + ); | |
| 139 | + $clear_url = remove_query_arg( 's', $current_url ); | |
| 140 | + | |
| 141 | + echo '<p>'; | |
| 142 | + printf( | |
| 143 | + /* translators: %1$s: Start link HTML, %2$s: End link HTML */ | |
| 144 | + esc_html__( 'No forms match your search. %1$sClear search%2$s', 'formidable' ), | |
| 145 | + '<a href="' . esc_url( $clear_url ) . '">', | |
| 146 | + '</a>' | |
| 147 | + ); | |
| 148 | + echo '</p>'; | |
| 149 | + return; | |
| 150 | + } | |
| 151 | + | |
| 137 | 152 | if ( $this->status === 'trash' ) { |
| 138 | 153 | echo '<p>'; |
| 139 | 154 | esc_html_e( 'No forms found in the trash.', 'formidable' ); |
| 140 | 155 | // phpcs:disable Generic.WhiteSpace.ScopeIndent |
| @@ -557,8 +572,13 @@ | ||
| 557 | 572 | * |
| 558 | 573 | * @return string |
| 559 | 574 | */ |
| 560 | 575 | public function column_embeds( $form ) { |
| 576 | + if ( $this->column_is_hidden( 'embeds' ) ) { | |
| 577 | + // Locating embeds means scanning post_content, so skip it when the column is hidden. | |
| 578 | + return ''; | |
| 579 | + } | |
| 580 | + | |
| 561 | 581 | $posts = $this->get_posts_contain_form( $form ); |
| 562 | 582 | |
| 563 | 583 | if ( ! $posts ) { |
| 564 | 584 | return '<span class="frm-forms-list-embeds-zero">0</span>'; |
| @@ -578,8 +598,27 @@ | ||
| 578 | 598 | ); |
| 579 | 599 | } |
| 580 | 600 | |
| 581 | 601 | /** |
| 602 | + * Checks if a column is hidden with Screen Options. | |
| 603 | + * | |
| 604 | + * @since 6.35 | |
| 605 | + * | |
| 606 | + * @param string $column_name Column name. | |
| 607 | + * | |
| 608 | + * @return bool | |
| 609 | + */ | |
| 610 | + private function column_is_hidden( $column_name ) { | |
| 611 | + $column_info = $this->get_column_info(); | |
| 612 | + | |
| 613 | + if ( ! isset( $column_info[1] ) || ! is_array( $column_info[1] ) ) { | |
| 614 | + return false; | |
| 615 | + } | |
| 616 | + | |
| 617 | + return in_array( $column_name, $column_info[1], true ); | |
| 618 | + } | |
| 619 | + | |
| 620 | + /** | |
| 582 | 621 | * Gets posts or pages that contain the form shortcode. |
| 583 | 622 | * |
| 584 | 623 | * @since 6.32 |
| 585 | 624 | * |
| @@ -587,52 +626,114 @@ | ||
| 587 | 626 | * |
| 588 | 627 | * @return array |
| 589 | 628 | */ |
| 590 | 629 | private function get_posts_contain_form( $form ) { |
| 591 | - $cached_posts = get_transient( self::$embed_posts_transient_name ); | |
| 630 | + $cached_posts = FrmFormEmbedsHelper::get_cached_posts(); | |
| 592 | 631 | |
| 593 | - if ( isset( $cached_posts[ $form->id ] ) && is_array( $cached_posts[ $form->id ] ) ) { | |
| 594 | - return $cached_posts[ $form->id ]; | |
| 632 | + if ( ! isset( $cached_posts[ $form->id ] ) || ! is_array( $cached_posts[ $form->id ] ) ) { | |
| 633 | + // A single scan covers every form listed on this page, not just this one. | |
| 634 | + $cached_posts = $this->fill_embed_posts_cache( $form ); | |
| 595 | 635 | } |
| 596 | 636 | |
| 597 | - $posts = $this->query_posts_contain_form( $form ); | |
| 598 | - | |
| 599 | - if ( ! is_array( $posts ) ) { | |
| 637 | + if ( ! isset( $cached_posts[ $form->id ] ) || ! is_array( $cached_posts[ $form->id ] ) ) { | |
| 600 | 638 | return array(); |
| 601 | 639 | } |
| 602 | 640 | |
| 603 | - foreach ( $posts as $post ) { | |
| 604 | - if ( ! property_exists( $post, 'permalink' ) ) { | |
| 605 | - $post->permalink = get_permalink( $post->ID ); | |
| 641 | + // Links and title fallbacks are derived at render time, not stored. get_edit_post_link() | |
| 642 | + // depends on the current user, so caching it in a shared transient would hand one user's | |
| 643 | + // edit link to another, and a permalink cached now goes stale on any slug change. | |
| 644 | + return FrmFormEmbedsHelper::prepare_posts( $cached_posts[ $form->id ] ); | |
| 645 | + } | |
| 646 | + | |
| 647 | + /** | |
| 648 | + * Scans for embeds once and caches the result for every form listed on the current page. | |
| 649 | + * | |
| 650 | + * @since 6.35 | |
| 651 | + * | |
| 652 | + * @param stdClass $form The form whose column is currently rendering. | |
| 653 | + * | |
| 654 | + * @return array Embed posts keyed by form ID. | |
| 655 | + */ | |
| 656 | + private function fill_embed_posts_cache( $form ) { | |
| 657 | + $cached_posts = FrmFormEmbedsHelper::get_cached_posts(); | |
| 658 | + $search_map = array(); | |
| 659 | + $forms = array(); | |
| 660 | + | |
| 661 | + foreach ( $this->get_forms_to_scan( $form ) as $form_id => $form_to_scan ) { | |
| 662 | + if ( isset( $cached_posts[ $form_id ] ) && is_array( $cached_posts[ $form_id ] ) ) { | |
| 663 | + continue; | |
| 606 | 664 | } |
| 607 | 665 | |
| 608 | - if ( ! property_exists( $post, 'edit_link' ) ) { | |
| 609 | - $post->edit_link = get_edit_post_link( $post->ID ); | |
| 610 | - } | |
| 666 | + $search_map[ $form_id ] = $this->get_search_strings_for_form( $form_id ); | |
| 667 | + $forms[ $form_id ] = $form_to_scan; | |
| 668 | + } | |
| 611 | 669 | |
| 612 | - // Ensure post_name is not null or the string "null" | |
| 613 | - if ( ! isset( $post->post_name ) ) { | |
| 614 | - $post->post_name = ''; | |
| 670 | + if ( ! $forms ) { | |
| 671 | + return $cached_posts; | |
| 672 | + } | |
| 673 | + | |
| 674 | + $matched = FrmFormEmbedsHelper::match_candidate_posts( $search_map ); | |
| 675 | + | |
| 676 | + foreach ( $forms as $form_id => $form_to_scan ) { | |
| 677 | + $posts = $this->filter_embed_posts( $matched[ $form_id ], $form_to_scan ); | |
| 678 | + $cached_posts[ $form_id ] = FrmFormEmbedsHelper::slim_posts( $posts ); | |
| 679 | + } | |
| 680 | + | |
| 681 | + FrmFormEmbedsHelper::save_cached_posts( $cached_posts ); | |
| 682 | + | |
| 683 | + return $cached_posts; | |
| 684 | + } | |
| 685 | + | |
| 686 | + /** | |
| 687 | + * Gets every form rendered on the current list page, keyed by form ID. | |
| 688 | + * | |
| 689 | + * @since 6.35 | |
| 690 | + * | |
| 691 | + * @param stdClass $form The form whose column is currently rendering. | |
| 692 | + * | |
| 693 | + * @return array | |
| 694 | + */ | |
| 695 | + private function get_forms_to_scan( $form ) { | |
| 696 | + $forms = array( $form->id => $form ); | |
| 697 | + | |
| 698 | + if ( ! is_array( $this->items ) ) { | |
| 699 | + return $forms; | |
| 700 | + } | |
| 701 | + | |
| 702 | + foreach ( $this->items as $item ) { | |
| 703 | + if ( is_object( $item ) && isset( $item->id ) ) { | |
| 704 | + $forms[ $item->id ] = $item; | |
| 615 | 705 | } |
| 706 | + } | |
| 616 | 707 | |
| 617 | - // Ensure post_title is not null or the string "null" | |
| 618 | - if ( ! isset( $post->post_title ) ) { | |
| 619 | - $post->post_title = ''; | |
| 620 | - } | |
| 708 | + return $forms; | |
| 709 | + } | |
| 621 | 710 | |
| 622 | - if ( '' === $post->post_title ) { | |
| 623 | - $post->post_title = __( '(no title)', 'formidable' ); | |
| 624 | - } | |
| 625 | - }//end foreach | |
| 711 | + /** | |
| 712 | + * Applies the frm_get_posts_contain_form filter to a form's embed posts. | |
| 713 | + * | |
| 714 | + * @since 6.35 | |
| 715 | + * | |
| 716 | + * @param array $posts Posts that embed the form. | |
| 717 | + * @param stdClass $form Form object. | |
| 718 | + * | |
| 719 | + * @return array | |
| 720 | + */ | |
| 721 | + private function filter_embed_posts( $posts, $form ) { | |
| 722 | + /** | |
| 723 | + * @since 6.32 | |
| 724 | + * | |
| 725 | + * @param stdClass[] $posts | |
| 726 | + * @param array $args | |
| 727 | + */ | |
| 728 | + $filtered_posts = apply_filters( 'frm_get_posts_contain_form', $posts, compact( 'form' ) ); | |
| 626 | 729 | |
| 627 | - if ( ! is_array( $cached_posts ) ) { | |
| 628 | - $cached_posts = array(); | |
| 730 | + if ( ! is_array( $filtered_posts ) ) { | |
| 731 | + _doing_it_wrong( 'frm_get_posts_contain_form', 'Filter should return an array.', '6.32' ); | |
| 732 | + return $posts; | |
| 629 | 733 | } |
| 630 | 734 | |
| 631 | - $cached_posts[ $form->id ] = $posts; | |
| 632 | - set_transient( self::$embed_posts_transient_name, $cached_posts, DAY_IN_SECONDS ); | |
| 633 | - | |
| 634 | - return $posts; | |
| 735 | + return $filtered_posts; | |
| 635 | 736 | } |
| 636 | 737 | |
| 637 | 738 | /** |
| 638 | 739 | * Gets search strings for a form inside a post. |
| @@ -656,91 +757,73 @@ | ||
| 656 | 757 | * |
| 657 | 758 | * @return string[] |
| 658 | 759 | */ |
| 659 | 760 | protected function get_base_search_strings_for_form( $form_id ) { |
| 660 | - return array( | |
| 661 | - '[formidable id=' . $form_id . ']', | |
| 662 | - '[formidable id=' . $form_id . ' ', | |
| 663 | - '[formidable id="' . $form_id . '"', | |
| 664 | - "[formidable id='" . $form_id . "'", | |
| 761 | + $strings = array( | |
| 665 | 762 | '<!-- wp:formidable/simple-form {"formId":"' . $form_id . '"', |
| 666 | 763 | ); |
| 667 | - } | |
| 668 | 764 | |
| 669 | - /** | |
| 670 | - * Queries for posts that contain the form shortcode. | |
| 671 | - * | |
| 672 | - * @param stdClass $form Form object. | |
| 673 | - * | |
| 674 | - * @return array | |
| 675 | - */ | |
| 676 | - private function query_posts_contain_form( $form ) { | |
| 677 | - $form_id = $form->id; | |
| 678 | - global $wpdb; | |
| 679 | - $query_strings = $this->get_search_strings_for_form( $form_id ); | |
| 680 | - $like_where = array(); | |
| 765 | + // The shortcode renders a form from its key just as happily as from its ID, in either | |
| 766 | + // attribute, so all four spellings have to be searched or those pages never count. | |
| 767 | + $identifiers = array( $form_id ); | |
| 768 | + $form_key = FrmForm::get_key_by_id( $form_id ); | |
| 681 | 769 | |
| 682 | - foreach ( $query_strings as $query_string ) { | |
| 683 | - $like_where[] = $wpdb->remove_placeholder_escape( $wpdb->prepare( 'post_content LIKE %s', '%' . $query_string . '%' ) ); | |
| 770 | + if ( $form_key && $form_key !== (string) $form_id ) { | |
| 771 | + $identifiers[] = $form_key; | |
| 684 | 772 | } |
| 685 | 773 | |
| 686 | - $like_where = implode( ' OR ', $like_where ); | |
| 687 | - $where = "post_type IN ('post', 'page') AND ($like_where)"; | |
| 688 | - | |
| 689 | - // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared | |
| 690 | - $posts = $wpdb->get_results( "SELECT ID,post_title,post_name FROM $wpdb->posts WHERE $where" ); | |
| 691 | - | |
| 692 | - if ( ! is_array( $posts ) ) { | |
| 693 | - return array(); | |
| 774 | + foreach ( $identifiers as $identifier ) { | |
| 775 | + foreach ( array( 'id', 'key' ) as $att ) { | |
| 776 | + $strings[] = '[formidable ' . $att . '=' . $identifier . ']'; | |
| 777 | + $strings[] = '[formidable ' . $att . '=' . $identifier . ' '; | |
| 778 | + $strings[] = '[formidable ' . $att . '="' . $identifier . '"'; | |
| 779 | + $strings[] = '[formidable ' . $att . "='" . $identifier . "'"; | |
| 780 | + } | |
| 694 | 781 | } |
| 695 | 782 | |
| 696 | - /** | |
| 697 | - * @since 6.32 | |
| 698 | - * | |
| 699 | - * @param stdClass[] $posts | |
| 700 | - * @param array $args | |
| 701 | - */ | |
| 702 | - $filtered_posts = apply_filters( 'frm_get_posts_contain_form', $posts, compact( 'form' ) ); | |
| 703 | - | |
| 704 | - if ( ! is_array( $filtered_posts ) ) { | |
| 705 | - _doing_it_wrong( 'frm_get_posts_contain_form', 'Filter should return an array.', '6.32' ); | |
| 706 | - return $posts; | |
| 707 | - } | |
| 708 | - | |
| 709 | - return $filtered_posts; | |
| 783 | + return $strings; | |
| 710 | 784 | } |
| 711 | 785 | |
| 712 | 786 | /** |
| 713 | - * Maybe clear the embed posts transient. | |
| 787 | + * Maybe clear the embed posts transient when a post is inserted. | |
| 714 | 788 | * |
| 715 | 789 | * @since 6.32 |
| 716 | 790 | * |
| 717 | 791 | * @param int $post_id Post ID. |
| 718 | 792 | * @param WP_Post $post Post object. |
| 793 | + * @param bool $update True when an existing post was updated rather than created. | |
| 719 | 794 | * |
| 720 | 795 | * @return void |
| 721 | 796 | */ |
| 722 | - public static function maybe_clear_embed_posts_transient( $post_id, $post ) { | |
| 723 | - if ( str_contains( $post->post_content, '[formidable ' ) || str_contains( $post->post_content, '<!-- wp:formidable/simple-form ' ) ) { | |
| 724 | - // New post contains the form shortcode, so clear the embed posts transient. | |
| 725 | - delete_transient( self::$embed_posts_transient_name ); | |
| 726 | - return; | |
| 727 | - } | |
| 797 | + public static function maybe_clear_embed_posts_transient( $post_id, $post, $update = false ) { | |
| 798 | + FrmFormEmbedsHelper::maybe_clear_on_insert( $post_id, $post, $update ); | |
| 799 | + } | |
| 728 | 800 | |
| 729 | - $cached_posts = get_transient( self::$embed_posts_transient_name ); | |
| 801 | + /** | |
| 802 | + * Maybe clear the embed posts transient when a post is updated. | |
| 803 | + * | |
| 804 | + * @since 6.35 | |
| 805 | + * | |
| 806 | + * @param int $post_id Post ID. | |
| 807 | + * @param WP_Post $post_after Post object after the update. | |
| 808 | + * @param WP_Post $post_before Post object before the update. | |
| 809 | + * | |
| 810 | + * @return void | |
| 811 | + */ | |
| 812 | + public static function maybe_clear_embed_posts_transient_on_update( $post_id, $post_after, $post_before ) { | |
| 813 | + FrmFormEmbedsHelper::maybe_clear_on_update( $post_id, $post_after, $post_before ); | |
| 814 | + } | |
| 730 | 815 | |
| 731 | - if ( ! is_array( $cached_posts ) ) { | |
| 732 | - return; | |
| 733 | - } | |
| 734 | - | |
| 735 | - // If the new post data of a cached post doesn't contain the Formidable forms, clear the transient. | |
| 736 | - foreach ( $cached_posts as $posts ) { | |
| 737 | - foreach ( $posts as $post_data ) { | |
| 738 | - if ( intval( $post_data->ID ) === intval( $post_id ) ) { | |
| 739 | - // This post contains the form shortcode before updating, so clear the embed posts transient. | |
| 740 | - delete_transient( self::$embed_posts_transient_name ); | |
| 741 | - return; | |
| 742 | - } | |
| 743 | - } | |
| 744 | - } | |
| 816 | + /** | |
| 817 | + * Maybe clear the embed posts transient when a post is trashed, untrashed or deleted. | |
| 818 | + * | |
| 819 | + * @since 6.35 | |
| 820 | + * | |
| 821 | + * @param int $post_id Post ID. | |
| 822 | + * @param WP_Post|null $post Post object, when the hook provides one. | |
| 823 | + * | |
| 824 | + * @return void | |
| 825 | + */ | |
| 826 | + public static function clear_embed_posts_transient_for_post( $post_id, $post = null ) { | |
| 827 | + FrmFormEmbedsHelper::maybe_clear_for_post( $post_id, $post ); | |
| 745 | 828 | } |
| 746 | 829 | } |