From 4cfbf629ca231820cc692049be30ef588101e523 Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Mon, 1 Feb 2021 23:31:54 +0000 Subject: [PATCH] Posts, Post Types: Additional functions to check if a post is publicly viewable. Introduces `is_post_status_viewable()` as a sibling to `is_post_type_viewable()`. Internal and protected statuses are never considered viewable. For built in posts statuses the `public` attribute is checked, for custom statuses the `publicly_queryable` attribute is checked. Introduces `is_post_publicly_viewable()` for determining if an individual post can be viewed by logged out users. A post is considered viewable if both `is_post_status_viewable()` and `is_post_type_viewable()` return `true` for the post's attributes. Additionally modifies `is_post_type_viewable()` to return `false` if an unregistered post type is passed to the function to avoid attempting to access properties on a non-object. Props peterwilsoncc, SergeyBiryukov, whyisjake, TimothyBlynJacobs. Fixes #49380. git-svn-id: https://develop.svn.wordpress.org/trunk@50130 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/post.php | 58 ++++++ .../tests/post/isPostPubliclyViewable.php | 108 +++++++++++ .../tests/post/isPostStatusViewable.php | 171 ++++++++++++++++++ 3 files changed, 337 insertions(+) create mode 100644 tests/phpunit/tests/post/isPostPubliclyViewable.php create mode 100644 tests/phpunit/tests/post/isPostStatusViewable.php diff --git a/src/wp-includes/post.php b/src/wp-includes/post.php index d7f7b25652..e1997e24b8 100644 --- a/src/wp-includes/post.php +++ b/src/wp-includes/post.php @@ -2019,9 +2019,67 @@ function is_post_type_viewable( $post_type ) { } } + if ( ! is_object( $post_type ) ) { + return false; + } + return $post_type->publicly_queryable || ( $post_type->_builtin && $post_type->public ); } +/** + * Determine whether a post status is considered "viewable". + * + * For built-in post statuses such as publish and private, the 'public' value will be evaluted. + * For all others, the 'publicly_queryable' value will be used. + * + * @since 5.7.0 + * + * @param string|stdClass $post_status Post status name or object. + * @return bool Whether the post status should be considered viewable. + */ +function is_post_status_viewable( $post_status ) { + if ( is_scalar( $post_status ) ) { + $post_status = get_post_status_object( $post_status ); + if ( ! $post_status ) { + return false; + } + } + + if ( + ! is_object( $post_status ) || + $post_status->internal || + $post_status->protected + ) { + return false; + } + + return $post_status->publicly_queryable || ( $post_status->_builtin && $post_status->public ); +} + +/** + * Determine whether a post is publicly viewable. + * + * Posts are considered publicly viewable if both the post status and post type + * are viewable. + * + * @since 5.7.0 + * + * @param int|WP_Post|null $post Optional. Post ID or post object. Defaults to global $post. + * @return bool Whether the post is publicly viewable. + */ +function is_post_publicly_viewable( $post = null ) { + $post = get_post( $post ); + + if ( ! $post ) { + return false; + } + + $post_type = get_post_type( $post ); + $post_status = get_post_status( $post ); + + return is_post_type_viewable( $post_type ) && is_post_status_viewable( $post_status ); +} + /** * Retrieves an array of the latest posts, or posts matching the given criteria. * diff --git a/tests/phpunit/tests/post/isPostPubliclyViewable.php b/tests/phpunit/tests/post/isPostPubliclyViewable.php new file mode 100644 index 0000000000..9d5d781e88 --- /dev/null +++ b/tests/phpunit/tests/post/isPostPubliclyViewable.php @@ -0,0 +1,108 @@ +post->create( + array( + 'post_status' => $actual_status, + 'post_name' => "$post_status-post", + 'post_date' => $date, + 'post_type' => 'page', + ) + ); + } + + wp_trash_post( self::$parent_post_ids['trash'] ); + wp_delete_post( self::$parent_post_ids['delete'], true ); + } + + /** + * Unit tests for is_post_publicly_viewable(). + * + * @dataProvider data_is_post_publicly_viewable + * @ticket 49380 + * + * @param string $post_type The post type. + * @param string $post_status The post status. + * @param bool $expected The expected result of the function call. + * @param string $parent_key The parent key as set up in shared fixtures. + */ + public function test_is_post_publicly_viewable( $post_type, $post_status, $expected, $parent_key = '' ) { + $date = ''; + if ( 'future' === $post_status ) { + $date = strftime( '%Y-%m-%d %H:%M:%S', strtotime( '+1 year' ) ); + } + + $post_id = $this->factory()->post->create( + array( + 'post_type' => $post_type, + 'post_status' => $post_status, + 'post_parent' => $parent_key ? self::$parent_post_ids[ $parent_key ] : 0, + 'post_date' => $date, + ) + ); + + $this->assertSame( $expected, is_post_publicly_viewable( $post_id ) ); + } + + /** + * Data provider for test_is_post_publicly_viewable(). + * + * return array[] { + * @type string $post_type The post type. + * @type string $post_status The post status. + * @type bool $expected The expected result of the function call. + * @type string $parent_key The parent key as set up in shared fixtures. + * } + */ + public function data_is_post_publicly_viewable() { + return array( + array( 'post', 'publish', true ), + array( 'post', 'private', false ), + array( 'post', 'future', false ), + + array( 'page', 'publish', true ), + array( 'page', 'private', false ), + array( 'page', 'future', false ), + + array( 'unregistered_cpt', 'publish', false ), + array( 'unregistered_cpt', 'private', false ), + + array( 'post', 'unregistered_cps', false ), + array( 'page', 'unregistered_cps', false ), + + array( 'attachment', 'inherit', true, 'publish' ), + array( 'attachment', 'inherit', false, 'private' ), + array( 'attachment', 'inherit', false, 'future' ), + array( 'attachment', 'inherit', true, 'trash' ), + array( 'attachment', 'inherit', true, 'delete' ), + + array( 'page', 'publish', true, 'publish' ), + array( 'page', 'publish', true, 'private' ), + array( 'page', 'publish', true, 'future' ), + array( 'page', 'publish', true, 'trash' ), + array( 'page', 'publish', true, 'delete' ), + ); + } +} diff --git a/tests/phpunit/tests/post/isPostStatusViewable.php b/tests/phpunit/tests/post/isPostStatusViewable.php new file mode 100644 index 0000000000..9658ca2066 --- /dev/null +++ b/tests/phpunit/tests/post/isPostStatusViewable.php @@ -0,0 +1,171 @@ +assertSame( $expected, is_post_status_viewable( 'wp_tests_ps' ) ); + // Test status passed as object. + $this->assertSame( $expected, is_post_status_viewable( get_post_status_object( 'wp_tests_ps' ) ) ); + } + + /** + * Data provider for custom post status tests. + * + * @return array[] { + * array CPS registration args. + * bool Expected result. + * } + */ + public function data_custom_post_statuses() { + return array( + // 0. False for non-publically queryable types. + array( + array( + 'publicly_queryable' => false, + '_builtin' => false, + 'public' => true, + ), + false, + ), + // 1. True for publically queryable types. + array( + array( + 'publicly_queryable' => true, + '_builtin' => false, + 'public' => false, + ), + true, + ), + // 2. False for built-in non-public types. + array( + array( + 'publicly_queryable' => false, + '_builtin' => true, + 'public' => false, + ), + false, + ), + // 3. False for non-built-in public types. + array( + array( + 'publicly_queryable' => false, + '_builtin' => false, + 'public' => true, + ), + false, + ), + // 4. True for built-in public types. + array( + array( + 'publicly_queryable' => false, + '_builtin' => true, + 'public' => true, + ), + true, + ), + ); + } + + /** + * Test built-in and unregistered post status. + * + * @dataProvider data_built_unregistered_in_status_types + * @ticket 49380 + * + * @param mixed $status Post status to check. + * @param bool $expected Expected viewable status. + */ + function test_built_unregistered_in_status_types( $status, $expected ) { + // Test status passed as string. + $this->assertSame( $expected, is_post_status_viewable( $status ) ); + // Test status passed as object. + $this->assertSame( $expected, is_post_status_viewable( get_post_status_object( $status ) ) ); + } + + /** + * Data provider for built-in and unregistered post status tests. + * + * @return array[] { + * @type mixed $status Post status to check. + * @type bool $expected Expected viewable status. + * } + */ + public function data_built_unregistered_in_status_types() { + return array( + array( 'publish', true ), + array( 'future', false ), + array( 'draft', false ), + array( 'pending', false ), + array( 'private', false ), + array( 'trash', false ), + array( 'auto-draft', false ), + array( 'inherit', false ), + array( 'request-pending', false ), + array( 'request-confirmed', false ), + array( 'request-failed', false ), + array( 'request-completed', false ), + + // Various unregistered statuses. + array( 'unregistered-status', false ), + array( false, false ), + array( true, false ), + array( 20, false ), + array( null, false ), + array( '', false ), + ); + } + + /** + * Sanitize key should not be run when testing. + * + * @ticket 49380 + */ + public function test_sanitize_key_not_run() { + register_post_status( + 'WP_Tests_ps', + array( + 'publicly_queryable' => true, + '_builtin' => false, + 'public' => true, + ) + ); + + // Sanitized key should return true. + $this->assertTrue( is_post_status_viewable( 'wp_tests_ps' ) ); + $this->assertTrue( is_post_status_viewable( get_post_status_object( 'wp_tests_ps' ) ) ); + + // Unsanitized key should return false. + $this->assertFalse( is_post_status_viewable( 'WP_tests_ps' ) ); + $this->assertFalse( is_post_status_viewable( get_post_status_object( 'WP_tests_ps' ) ) ); + } +}