diff --git a/inc/namespace.php b/inc/namespace.php index f327cb5..7c71d97 100644 --- a/inc/namespace.php +++ b/inc/namespace.php @@ -21,6 +21,7 @@ use WP_REST_Response; use WP_REST_Server; use WP_User; +use WP_User_Query; use function Asset_Loader\enqueue_asset; @@ -45,6 +46,7 @@ function bootstrap() : void { add_action( 'rest_api_init', __NAMESPACE__ . '\\register_rest_api_fields' ); add_action( 'enqueue_block_editor_assets', __NAMESPACE__ . '\\enqueue_assets' ); add_action( 'pre_get_posts', __NAMESPACE__ . '\\action_pre_get_posts', 9999 ); + add_action( 'pre_user_query', __NAMESPACE__ . '\\action_reference_pre_user_query', 9999 ); add_action( 'wp', __NAMESPACE__ . '\\action_wp' ); add_action( 'wp_insert_post', [ $insert_post_handler, 'action_wp_insert_post' ], 10, 3 ); @@ -687,6 +689,72 @@ function action_pre_get_posts( WP_Query $query ) : void { }, 999, 2 ); } +/** + * Handle has_published_posts where clause to include guest authors. + * + * @param WP_User_Query $query Current instance of WP_User_Query (passed by reference). + */ +function action_reference_pre_user_query( WP_User_Query $query ) : void { + global $wpdb; + + // Author sitemap queries check for this meta key. + if ( ! is_array( $query->get( 'has_published_posts' ) ) ) { + return; + } + + $blog_id = $query->get( 'blog_id' ) ?: 0; + $post_types = $query->get( 'has_published_posts' ); + $supported_post_types = array_intersect( + get_supported_post_types(), + $post_types + ); + $unsupported_post_types = array_diff( + $post_types, + $supported_post_types + ); + + foreach ( $post_types as &$post_type ) { + $post_type = $wpdb->prepare( '%s', $post_type ); + } + foreach ( $supported_post_types as &$post_type ) { + $post_type = $wpdb->prepare( '%s', $post_type ); + } + foreach ( $unsupported_post_types as &$post_type ) { + $post_type = $wpdb->prepare( '%s', $post_type ); + } + + $prefix = $wpdb->get_blog_prefix( $blog_id ); + $posts_table = $prefix . 'posts'; + $term_relationships_table = $prefix . 'term_relationships'; + $term_taxonomy_table = $prefix . 'term_taxonomy'; + $terms_table = $prefix . 'terms'; + + // Rebuild the has_published_posts part of the query. + $sql = " $wpdb->users.ID IN ( SELECT DISTINCT $posts_table.post_author FROM $posts_table WHERE $posts_table.post_status = 'publish' AND $posts_table.post_type IN ( " . implode( ', ', $post_types ) . ' ) )'; + + // Non-authorship author query. + $unsupported_sql = empty( $unsupported_post_types ) + ? '1=0' + : str_replace( implode( ', ', $post_types ), implode( ', ', $unsupported_post_types ), $sql ); + + // Attributed author query. + $supported_sql = empty( $supported_post_types ) + ? '1=0' + : " $wpdb->users.ID IN ( + SELECT DISTINCT CAST( t.slug AS SIGNED ) + FROM {$posts_table} p + LEFT JOIN {$term_relationships_table} tr ON p.ID = tr.object_id + LEFT JOIN {$term_taxonomy_table} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id + LEFT JOIN {$terms_table} t ON tt.term_id = t.term_id + WHERE p.post_type IN ( " . implode( ', ', $supported_post_types ) . " ) + AND p.post_status = 'publish' + )"; + + $query_with_guests = " ( ( $unsupported_sql ) OR ( $supported_sql ) )"; + + $query->query_where = str_replace( $sql, $query_with_guests, $query->query_where ); +} + /** * Filters the list of recipients for comment moderation emails. * diff --git a/tests/phpunit/includes/bootstrap.php b/tests/phpunit/includes/bootstrap.php index e804f73..5c9bc5d 100644 --- a/tests/phpunit/includes/bootstrap.php +++ b/tests/phpunit/includes/bootstrap.php @@ -27,6 +27,13 @@ require_once $_plugin_dir . '/plugin.php'; } ); +tests_add_filter( 'init', function() : void { + register_post_type( 'test_cpt_no_author', [ + 'public' => true, + 'supports' => [ 'title', 'editor' ], + ] ); +} ); + require_once $_tests_dir . '/includes/bootstrap.php'; require_once __DIR__ . '/testcase.php'; require_once __DIR__ . '/email-testcase.php'; diff --git a/tests/phpunit/test-wp-user-query.php b/tests/phpunit/test-wp-user-query.php new file mode 100644 index 0000000..6dfe6a9 --- /dev/null +++ b/tests/phpunit/test-wp-user-query.php @@ -0,0 +1,145 @@ +post; + + // Attributed to Editor, owned by Admin. + $factory->create_and_get( [ + 'post_author' => self::$users['admin']->ID, + 'post_status' => 'publish', + POSTS_PARAM => [ + self::$users['editor']->ID, + ], + ] ); + + // Attributed to Editor, owned by nobody. + $factory->create_and_get( [ + 'post_status' => 'publish', + POSTS_PARAM => [ + self::$users['editor']->ID, + ], + ] ); + + // Attributed to Guest, owned by Author. + $factory->create_and_get( [ + 'post_author' => self::$users['author']->ID, + 'post_status' => 'publish', + POSTS_PARAM => [ + self::$users[ GUEST_ROLE ]->ID, + ], + ] ); + + // Attributed to Guest, owned by Admin. + $factory->create_and_get( [ + 'post_author' => self::$users['admin']->ID, + 'post_status' => 'publish', + POSTS_PARAM => [ + self::$users[ GUEST_ROLE ]->ID, + ], + ] ); + + // Owned by Author. + $factory->create_and_get( [ + 'post_author' => self::$users['author']->ID, + 'post_status' => 'publish', + 'post_type' => 'test_cpt_no_author', + ] ); + + // Owned by Admin. + $factory->create_and_get( [ + 'post_author' => self::$users['admin']->ID, + 'post_status' => 'publish', + 'post_type' => 'test_cpt_no_author', + ] ); + + $common_args = [ + 'fields' => 'ID', + 'orderby' => 'ID', + 'order' => 'ASC', + ]; + + // Queries for attributed published post types. + $test_args = [ + 'has_published_posts' => [ 'post' ], + ]; + + foreach ( $test_args as $test_key => $test_value ) { + $args = array_merge( $common_args, [ + $test_key => $test_value, + ] ); + + $query = new WP_User_Query( $args ); + $users = (array) $query->get_results(); + $users = array_map( 'absint', $users ); + + $this->assertSame( + [ self::$users['editor']->ID, self::$users[ GUEST_ROLE ]->ID ], + $users, + "User IDs for attributed {$test_key} query are incorrect." + ); + } + + // Queries for non attributed published post types. + $test_args = [ + 'has_published_posts' => [ 'test_cpt_no_author' ], + ]; + + foreach ( $test_args as $test_key => $test_value ) { + $args = array_merge( $common_args, [ + $test_key => $test_value, + ] ); + + $query = new WP_User_Query( $args ); + $users = (array) $query->get_results(); + $users = array_map( 'absint', $users ); + + $this->assertSame( + [ self::$users['admin']->ID, self::$users['author']->ID ], + $users, + "User IDs for non attributed {$test_key} query are incorrect." + ); + } + + // Queries for non attributed published post types. + $test_args = [ + 'has_published_posts' => [ 'post', 'test_cpt_no_author' ], + ]; + + foreach ( $test_args as $test_key => $test_value ) { + $args = array_merge( $common_args, [ + $test_key => $test_value, + ] ); + + $query = new WP_User_Query( $args ); + $users = (array) $query->get_results(); + $users = array_map( 'absint', $users ); + + $this->assertSame( + [ + self::$users['admin']->ID, + self::$users['editor']->ID, + self::$users['author']->ID, + self::$users[ GUEST_ROLE ]->ID, + ], + $users, + "User IDs for combined attributed and non-attributed {$test_key} query are incorrect." + ); + } + } + +}