| @@ -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 | |
| @@ -581,8 +572,13 @@ | ||
| 581 | 572 | * |
| 582 | 573 | * @return string |
| 583 | 574 | */ |
| 584 | 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 | + | |
| 585 | 581 | $posts = $this->get_posts_contain_form( $form ); |
| 586 | 582 | |
| 587 | 583 | if ( ! $posts ) { |
| 588 | 584 | return '<span class="frm-forms-list-embeds-zero">0</span>'; |
| @@ -602,8 +598,27 @@ | ||
| 602 | 598 | ); |
| 603 | 599 | } |
| 604 | 600 | |
| 605 | 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 | + /** | |
| 606 | 621 | * Gets posts or pages that contain the form shortcode. |
| 607 | 622 | * |
| 608 | 623 | * @since 6.32 |
| 609 | 624 | * |
| @@ -611,52 +626,114 @@ | ||
| 611 | 626 | * |
| 612 | 627 | * @return array |
| 613 | 628 | */ |
| 614 | 629 | private function get_posts_contain_form( $form ) { |
| 615 | - $cached_posts = get_transient( self::$embed_posts_transient_name ); | |
| 630 | + $cached_posts = FrmFormEmbedsHelper::get_cached_posts(); | |
| 616 | 631 | |
| 617 | - if ( isset( $cached_posts[ $form->id ] ) && is_array( $cached_posts[ $form->id ] ) ) { | |
| 618 | - 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 ); | |
| 619 | 635 | } |
| 620 | 636 | |
| 621 | - $posts = $this->query_posts_contain_form( $form ); | |
| 622 | - | |
| 623 | - if ( ! is_array( $posts ) ) { | |
| 637 | + if ( ! isset( $cached_posts[ $form->id ] ) || ! is_array( $cached_posts[ $form->id ] ) ) { | |
| 624 | 638 | return array(); |
| 625 | 639 | } |
| 626 | 640 | |
| 627 | - foreach ( $posts as $post ) { | |
| 628 | - if ( ! property_exists( $post, 'permalink' ) ) { | |
| 629 | - $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; | |
| 630 | 664 | } |
| 631 | 665 | |
| 632 | - if ( ! property_exists( $post, 'edit_link' ) ) { | |
| 633 | - $post->edit_link = get_edit_post_link( $post->ID ); | |
| 634 | - } | |
| 666 | + $search_map[ $form_id ] = $this->get_search_strings_for_form( $form_id ); | |
| 667 | + $forms[ $form_id ] = $form_to_scan; | |
| 668 | + } | |
| 635 | 669 | |
| 636 | - // Ensure post_name is not null or the string "null" | |
| 637 | - if ( ! isset( $post->post_name ) ) { | |
| 638 | - $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; | |
| 639 | 705 | } |
| 706 | + } | |
| 640 | 707 | |
| 641 | - // Ensure post_title is not null or the string "null" | |
| 642 | - if ( ! isset( $post->post_title ) ) { | |
| 643 | - $post->post_title = ''; | |
| 644 | - } | |
| 708 | + return $forms; | |
| 709 | + } | |
| 645 | 710 | |
| 646 | - if ( '' === $post->post_title ) { | |
| 647 | - $post->post_title = __( '(no title)', 'formidable' ); | |
| 648 | - } | |
| 649 | - }//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' ) ); | |
| 650 | 729 | |
| 651 | - if ( ! is_array( $cached_posts ) ) { | |
| 652 | - $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; | |
| 653 | 733 | } |
| 654 | 734 | |
| 655 | - $cached_posts[ $form->id ] = $posts; | |
| 656 | - set_transient( self::$embed_posts_transient_name, $cached_posts, DAY_IN_SECONDS ); | |
| 657 | - | |
| 658 | - return $posts; | |
| 735 | + return $filtered_posts; | |
| 659 | 736 | } |
| 660 | 737 | |
| 661 | 738 | /** |
| 662 | 739 | * Gets search strings for a form inside a post. |
| @@ -680,91 +757,73 @@ | ||
| 680 | 757 | * |
| 681 | 758 | * @return string[] |
| 682 | 759 | */ |
| 683 | 760 | protected function get_base_search_strings_for_form( $form_id ) { |
| 684 | - return array( | |
| 685 | - '[formidable id=' . $form_id . ']', | |
| 686 | - '[formidable id=' . $form_id . ' ', | |
| 687 | - '[formidable id="' . $form_id . '"', | |
| 688 | - "[formidable id='" . $form_id . "'", | |
| 761 | + $strings = array( | |
| 689 | 762 | '<!-- wp:formidable/simple-form {"formId":"' . $form_id . '"', |
| 690 | 763 | ); |
| 691 | - } | |
| 692 | 764 | |
| 693 | - /** | |
| 694 | - * Queries for posts that contain the form shortcode. | |
| 695 | - * | |
| 696 | - * @param stdClass $form Form object. | |
| 697 | - * | |
| 698 | - * @return array | |
| 699 | - */ | |
| 700 | - private function query_posts_contain_form( $form ) { | |
| 701 | - $form_id = $form->id; | |
| 702 | - global $wpdb; | |
| 703 | - $query_strings = $this->get_search_strings_for_form( $form_id ); | |
| 704 | - $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 ); | |
| 705 | 769 | |
| 706 | - foreach ( $query_strings as $query_string ) { | |
| 707 | - $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; | |
| 708 | 772 | } |
| 709 | 773 | |
| 710 | - $like_where = implode( ' OR ', $like_where ); | |
| 711 | - $where = "post_type IN ('post', 'page') AND ($like_where)"; | |
| 712 | - | |
| 713 | - // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared | |
| 714 | - $posts = $wpdb->get_results( "SELECT ID,post_title,post_name FROM $wpdb->posts WHERE $where" ); | |
| 715 | - | |
| 716 | - if ( ! is_array( $posts ) ) { | |
| 717 | - 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 | + } | |
| 718 | 781 | } |
| 719 | 782 | |
| 720 | - /** | |
| 721 | - * @since 6.32 | |
| 722 | - * | |
| 723 | - * @param stdClass[] $posts | |
| 724 | - * @param array $args | |
| 725 | - */ | |
| 726 | - $filtered_posts = apply_filters( 'frm_get_posts_contain_form', $posts, compact( 'form' ) ); | |
| 727 | - | |
| 728 | - if ( ! is_array( $filtered_posts ) ) { | |
| 729 | - _doing_it_wrong( 'frm_get_posts_contain_form', 'Filter should return an array.', '6.32' ); | |
| 730 | - return $posts; | |
| 731 | - } | |
| 732 | - | |
| 733 | - return $filtered_posts; | |
| 783 | + return $strings; | |
| 734 | 784 | } |
| 735 | 785 | |
| 736 | 786 | /** |
| 737 | - * Maybe clear the embed posts transient. | |
| 787 | + * Maybe clear the embed posts transient when a post is inserted. | |
| 738 | 788 | * |
| 739 | 789 | * @since 6.32 |
| 740 | 790 | * |
| 741 | 791 | * @param int $post_id Post ID. |
| 742 | 792 | * @param WP_Post $post Post object. |
| 793 | + * @param bool $update True when an existing post was updated rather than created. | |
| 743 | 794 | * |
| 744 | 795 | * @return void |
| 745 | 796 | */ |
| 746 | - public static function maybe_clear_embed_posts_transient( $post_id, $post ) { | |
| 747 | - if ( str_contains( $post->post_content, '[formidable ' ) || str_contains( $post->post_content, '<!-- wp:formidable/simple-form ' ) ) { | |
| 748 | - // New post contains the form shortcode, so clear the embed posts transient. | |
| 749 | - delete_transient( self::$embed_posts_transient_name ); | |
| 750 | - return; | |
| 751 | - } | |
| 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 | + } | |
| 752 | 800 | |
| 753 | - $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 | + } | |
| 754 | 815 | |
| 755 | - if ( ! is_array( $cached_posts ) ) { | |
| 756 | - return; | |
| 757 | - } | |
| 758 | - | |
| 759 | - // If the new post data of a cached post doesn't contain the Formidable forms, clear the transient. | |
| 760 | - foreach ( $cached_posts as $posts ) { | |
| 761 | - foreach ( $posts as $post_data ) { | |
| 762 | - if ( intval( $post_data->ID ) === intval( $post_id ) ) { | |
| 763 | - // This post contains the form shortcode before updating, so clear the embed posts transient. | |
| 764 | - delete_transient( self::$embed_posts_transient_name ); | |
| 765 | - return; | |
| 766 | - } | |
| 767 | - } | |
| 768 | - } | |
| 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 ); | |
| 769 | 828 | } |
| 770 | 829 | } |