diff --git a/projects/packages/sync/changelog/add-sync-custom-post-types-for-activity-log b/projects/packages/sync/changelog/add-sync-custom-post-types-for-activity-log new file mode 100644 index 000000000000..7fc0d55248da --- /dev/null +++ b/projects/packages/sync/changelog/add-sync-custom-post-types-for-activity-log @@ -0,0 +1,4 @@ +Significance: minor +Type: added + +Add Activity Log custom event support with a private custom post type and class API for creating entries. diff --git a/projects/packages/sync/src/class-actions.php b/projects/packages/sync/src/class-actions.php index 808f8b005941..296d11136e6c 100644 --- a/projects/packages/sync/src/class-actions.php +++ b/projects/packages/sync/src/class-actions.php @@ -126,6 +126,9 @@ public static function init() { // Sync connected user role changes to WordPress.com. Users::init(); + // Activity Log custom events. + Activity_Log_Event::init(); + // Publicize filter to prevent publicizing blacklisted post types. add_filter( 'publicize_should_publicize_published_post', array( __CLASS__, 'prevent_publicize_blacklisted_posts' ), 10, 2 ); diff --git a/projects/packages/sync/src/class-activity-log-event.php b/projects/packages/sync/src/class-activity-log-event.php new file mode 100644 index 000000000000..b1595732aa19 --- /dev/null +++ b/projects/packages/sync/src/class-activity-log-event.php @@ -0,0 +1,344 @@ + true, + 'success' => true, + 'warning' => true, + 'error' => true, + ); + + /** + * Maximum title length. + */ + const MAX_TITLE_LENGTH = 200; + + /** + * Maximum content length. + */ + const MAX_CONTENT_LENGTH = 5000; + + /** + * Maximum source length. + */ + const MAX_SOURCE_LENGTH = 100; + + /** + * Initialize Activity Log custom event hooks. + */ + public static function init() { + add_action( 'init', array( __CLASS__, 'register_post_type' ) ); + add_filter( 'wp_insert_post_empty_content', array( __CLASS__, 'prevent_invalid_post_insert' ), 10, 2 ); + add_filter( 'wp_insert_post_data', array( __CLASS__, 'normalize_post_data' ), 10, 2 ); + add_filter( 'publicize_should_publicize_published_post', array( __CLASS__, 'prevent_publicize' ), 10, 2 ); + add_filter( 'jetpack_sitemap_post_types', array( __CLASS__, 'filter_sitemap_post_types' ) ); + } + + /** + * Registers the Activity Log CPT with hardened defaults that prevent leakage + * to front-end queries, RSS, REST, search, sitemaps, and exports. + */ + public static function register_post_type() { + register_post_type( + self::POST_TYPE, + array( + 'labels' => array( + 'name' => __( 'Activity Log Events', 'jetpack-sync' ), + 'singular_name' => __( 'Activity Log Event', 'jetpack-sync' ), + ), + 'public' => false, + 'publicly_queryable' => false, + 'show_ui' => false, + 'show_in_menu' => false, + 'show_in_nav_menus' => false, + 'show_in_rest' => false, + 'show_in_admin_bar' => false, + 'exclude_from_search' => true, + 'has_archive' => false, + 'rewrite' => false, + 'query_var' => false, + 'can_export' => false, + 'supports' => array( 'title', 'editor' ), + ) + ); + } + + /** + * Logs a custom event to the Jetpack Activity Log. + * + * Prefer calling this on or after the `init` action so Sync listeners are registered. + * The Activity Log post type is registered defensively if needed before insert. + * + * @param array $args { + * Activity log event arguments. + * + * @type string $title Required. Plain-text title, truncated to 200 chars. + * @type string $content Required. Plain-text body, truncated to 5000 chars. + * @type string $source Optional. Identifier for the source of the event, e.g. 'mc'. + * @type string $severity Optional. 'info', 'success', 'warning', or 'error'. Defaults to 'info'. + * } + * @return int|false Post ID on success, false if validation fails. + */ + public static function create( array $args ) { + $payload = self::build_payload( $args ); + if ( false === $payload ) { + return false; + } + + if ( ! post_type_exists( self::POST_TYPE ) ) { + self::register_post_type(); + } + + $post_content = wp_json_encode( $payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ); + if ( false === $post_content ) { + return false; + } + + $post_id = wp_insert_post( + wp_slash( + array( + 'post_type' => self::POST_TYPE, + 'post_title' => $payload['title'], + 'post_content' => $post_content, + 'post_status' => 'publish', + ) + ), + true + ); + + return is_wp_error( $post_id ) ? false : $post_id; + } + + /** + * Checks that an Activity Log custom event has a valid payload before enqueueing it for sync, + * in case data bypasses the Activity_Log_Event::create() helper. + * + * @param \WP_Post $post Activity Log post. + * @return bool + */ + public static function is_valid_post( $post ) { + if ( ! $post instanceof \WP_Post || self::POST_TYPE !== $post->post_type ) { + return false; + } + + // Build a sanitized candidate to validate the payload contract without mutating the stored post. + return false !== self::build_payload_from_post_content( $post->post_content ); + } + + /** + * Prevents invalid Activity Log event posts from being inserted or updated via wp_insert_post(). + * + * @param bool $maybe_empty Whether the post should be considered empty. + * @param array $postarr Post data passed to wp_insert_post(). + * @return bool + */ + public static function prevent_invalid_post_insert( $maybe_empty, $postarr ) { + if ( ! is_array( $postarr ) || self::POST_TYPE !== self::get_postarr_post_type( $postarr ) ) { + return $maybe_empty; + } + + return false === self::build_payload_from_post_content( $postarr['post_content'] ?? '' ); + } + + /** + * Normalizes Activity Log event posts before they are inserted or updated via wp_insert_post(). + * + * @param array $data Slashed, sanitized post data. + * @param array $postarr Post data passed to wp_insert_post(). + * @return array + */ + public static function normalize_post_data( $data, $postarr ) { + if ( ! is_array( $data ) || ! is_array( $postarr ) || self::POST_TYPE !== self::get_postarr_post_type( $postarr ) ) { + return $data; + } + + $payload = self::build_payload_from_post_content( $data['post_content'] ?? '' ); + if ( false === $payload ) { + return $data; + } + + $post_content = wp_json_encode( $payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ); + if ( false === $post_content ) { + return $data; + } + + $data['post_title'] = wp_slash( $payload['title'] ); + $data['post_content'] = wp_slash( $post_content ); + + return $data; + } + + /** + * Never auto-share Activity Log entries via Jetpack Social, + * even if a third party adds 'publicize' post-type support to this CPT. + * + * @param bool $should_publicize Publicize status prior to this filter running. + * @param \WP_Post $post The post to test for Publicizability. + * @return bool + */ + public static function prevent_publicize( $should_publicize, $post ) { + return ( $post && self::POST_TYPE === $post->post_type ) ? false : $should_publicize; + } + + /** + * Never include Activity Log entries in Jetpack sitemaps, + * even if a third party adds this CPT to the sitemap post-type list. + * + * @param string[] $types Sitemap post types. + * @return string[] + */ + public static function filter_sitemap_post_types( $types ) { + return array_values( array_diff( (array) $types, array( self::POST_TYPE ) ) ); + } + + /** + * Builds an Activity Log event payload from raw input. + * + * @param array $args Raw event arguments. + * @return array|false Sanitized payload, or false if validation fails. + */ + private static function build_payload( array $args ) { + $title = self::sanitize_string( $args['title'] ?? '', self::MAX_TITLE_LENGTH ); + $content = self::sanitize_string( $args['content'] ?? '', self::MAX_CONTENT_LENGTH ); + + if ( '' === $title || '' === $content ) { + return false; + } + + $severity = self::sanitize_severity( $args['severity'] ?? self::DEFAULT_SEVERITY ); + if ( false === $severity ) { + return false; + } + + $payload = array( + 'title' => $title, + 'content' => $content, + 'severity' => $severity, + ); + + $source = self::sanitize_string( $args['source'] ?? '', self::MAX_SOURCE_LENGTH ); + if ( '' !== $source ) { + $payload['source'] = $source; + } + + return $payload; + } + + /** + * Builds an Activity Log event payload from post content. + * + * @param mixed $post_content Raw post content. + * @return array|false Sanitized payload, or false if validation fails. + */ + private static function build_payload_from_post_content( $post_content ) { + $data = self::decode_payload( $post_content ); + if ( ! is_array( $data ) ) { + return false; + } + + return self::build_payload( $data ); + } + + /** + * Decodes an Activity Log event payload from post content. + * + * @param mixed $post_content Raw post content. + * @return array|false + */ + private static function decode_payload( $post_content ) { + $data = json_decode( (string) $post_content, true ); + if ( ! is_array( $data ) ) { + $data = json_decode( wp_unslash( (string) $post_content ), true ); + } + + return is_array( $data ) ? $data : false; + } + + /** + * Gets the post type from wp_insert_post() input. + * + * @param array $postarr Post data passed to wp_insert_post(). + * @return string + */ + private static function get_postarr_post_type( array $postarr ) { + if ( isset( $postarr['post_type'] ) ) { + return (string) $postarr['post_type']; + } + + if ( ! empty( $postarr['ID'] ) ) { + return (string) get_post_type( (int) $postarr['ID'] ); + } + + return ''; + } + + /** + * Strips HTML/PHP from a value and truncates it to a maximum character length, multibyte-safe. + * + * @param mixed $value Raw value. + * @param int $max Maximum length in characters. + * @return string + */ + private static function sanitize_string( $value, $max ) { + if ( is_array( $value ) || is_object( $value ) ) { + return ''; + } + + $value = wp_strip_all_tags( (string) $value, true ); + $value = preg_replace( '/\s+/', ' ', $value ); + if ( null === $value ) { + return ''; + } + + $value = trim( $value ); + + if ( function_exists( 'mb_substr' ) ) { + return mb_substr( $value, 0, $max ); + } + + return substr( $value, 0, $max ); + } + + /** + * Sanitizes an Activity Log severity value. + * + * @param mixed $severity Raw severity. + * @return string|false Sanitized severity, or false if invalid. + */ + private static function sanitize_severity( $severity ) { + if ( is_array( $severity ) || is_object( $severity ) ) { + return false; + } + + $severity = strtolower( trim( (string) $severity ) ); + if ( '' === $severity ) { + return self::DEFAULT_SEVERITY; + } + + return isset( self::ALLOWED_SEVERITIES[ $severity ] ) ? $severity : false; + } +} diff --git a/projects/packages/sync/src/modules/class-posts.php b/projects/packages/sync/src/modules/class-posts.php index 6e1ea7b544b6..06065b55eb27 100644 --- a/projects/packages/sync/src/modules/class-posts.php +++ b/projects/packages/sync/src/modules/class-posts.php @@ -9,6 +9,7 @@ use Automattic\Jetpack\Constants as Jetpack_Constants; use Automattic\Jetpack\Roles; +use Automattic\Jetpack\Sync\Activity_Log_Event; use Automattic\Jetpack\Sync\Modules; use Automattic\Jetpack\Sync\Settings; @@ -480,6 +481,10 @@ public function filter_jetpack_sync_before_enqueue_jetpack_sync_save_post( $args return false; } + if ( Activity_Log_Event::POST_TYPE === $post->post_type && ! Activity_Log_Event::is_valid_post( $post ) ) { + return false; + } + return array( (int) $post_id, $this->filter_post_content_and_add_links( $post ), $update, $previous_state ); } @@ -500,6 +505,11 @@ public function filter_jetpack_sync_before_enqueue_jetpack_published_post( $args } list( $post_id, $flags, $post ) = $args; + + if ( Activity_Log_Event::POST_TYPE === $post->post_type && ! Activity_Log_Event::is_valid_post( $post ) ) { + return false; + } + return array( (int) $post_id, $flags, $this->filter_post_content_and_add_links( $post ) ); } diff --git a/projects/packages/sync/tests/php/Activity_Log_Event_Test.php b/projects/packages/sync/tests/php/Activity_Log_Event_Test.php new file mode 100644 index 000000000000..734ac5e80c46 --- /dev/null +++ b/projects/packages/sync/tests/php/Activity_Log_Event_Test.php @@ -0,0 +1,483 @@ + ' Cache flushed ', + 'content' => "First line\nSecond line", + 'source' => ' mc ', + 'severity' => ' SUCCESS ', + 'external_id' => " sync-run-123\x00 ", + 'link' => 'https://example.com/logs/123', + ) + ); + + $this->assertIsInt( $post_id ); + + $post = get_post( $post_id ); + + $this->assertInstanceOf( \WP_Post::class, $post ); + $this->assertSame( Activity_Log_Event::POST_TYPE, $post->post_type ); + + $payload = $this->get_activity_log_payload( $post_id ); + + $this->assertSame( 'Cache flushed', $payload['title'] ); + $this->assertSame( 'First line Second line', $payload['content'] ); + $this->assertSame( 'mc', $payload['source'] ); + $this->assertSame( 'success', $payload['severity'] ); + $this->assertArrayNotHasKey( 'external_id', $payload ); + $this->assertArrayNotHasKey( 'link', $payload ); + } + + /** + * Tests that create registers the Activity Log post type if it is missing. + */ + public function test_activity_log_event_registers_post_type_before_insert() { + unregister_post_type( Activity_Log_Event::POST_TYPE ); + + $this->assertFalse( post_type_exists( Activity_Log_Event::POST_TYPE ) ); + + $post_id = Activity_Log_Event::create( + array( + 'title' => 'Cache flushed', + 'content' => 'Plain text note.', + ) + ); + + $this->assertIsInt( $post_id ); + $this->assertTrue( post_type_exists( Activity_Log_Event::POST_TYPE ) ); + } + + /** + * Tests that source is optional. + */ + public function test_activity_log_event_allows_missing_source() { + $post_id = Activity_Log_Event::create( + array( + 'title' => 'Cache flushed', + 'content' => "Plain text\nnote.", + ) + ); + + $this->assertIsInt( $post_id ); + + $payload = $this->get_activity_log_payload( $post_id ); + + $this->assertSame( 'Cache flushed', $payload['title'] ); + $this->assertSame( 'Plain text note.', $payload['content'] ); + $this->assertArrayNotHasKey( 'source', $payload ); + } + + /** + * Tests that empty severity defaults to info. + */ + public function test_activity_log_event_defaults_empty_severity_to_info() { + $post_id = Activity_Log_Event::create( + array( + 'title' => 'Cache flushed', + 'content' => 'Plain text note.', + 'source' => 'mc', + 'severity' => '', + ) + ); + + $this->assertIsInt( $post_id ); + + $payload = $this->get_activity_log_payload( $post_id ); + + $this->assertSame( 'info', $payload['severity'] ); + } + + /** + * Tests that invalid severity values fail validation. + */ + public function test_activity_log_event_rejects_invalid_severity() { + $post_id = Activity_Log_Event::create( + array( + 'title' => 'Cache flushed', + 'content' => 'Plain text note.', + 'source' => 'mc', + 'severity' => 'critical', + ) + ); + + $this->assertFalse( $post_id ); + } + + /** + * Tests that required fields must be scalar values. + */ + public function test_activity_log_event_rejects_non_scalar_required_values() { + $post_id = Activity_Log_Event::create( + array( + 'title' => array( 'Cache flushed' ), + 'content' => 'Plain text note.', + 'source' => 'mc', + ) + ); + + $this->assertFalse( $post_id ); + } + + /** + * Tests that Activity Log event post data is normalized before insertion. + */ + public function test_activity_log_event_normalizes_post_data() { + $data = wp_slash( + array( + 'post_type' => Activity_Log_Event::POST_TYPE, + 'post_title' => 'Direct insert title', + 'post_content' => wp_json_encode( + array( + 'title' => ' Cache flushed ', + 'content' => "Plain text\nnote.", + 'source' => ' mc ', + 'severity' => ' WARNING ', + 'link' => 'https://example.com/logs/123', + ), + JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE + ), + 'post_status' => 'publish', + ) + ); + + $normalized = Activity_Log_Event::normalize_post_data( $data, $data ); + + $this->assertSame( 'Cache flushed', wp_unslash( $normalized['post_title'] ) ); + + $payload = json_decode( wp_unslash( $normalized['post_content'] ), true ); + + $this->assertIsArray( $payload ); + + $this->assertSame( 'Cache flushed', $payload['title'] ); + $this->assertSame( 'Plain text note.', $payload['content'] ); + $this->assertSame( 'mc', $payload['source'] ); + $this->assertSame( 'warning', $payload['severity'] ); + $this->assertArrayNotHasKey( 'link', $payload ); + } + + /** + * Tests that the core insert hook rejects invalid Activity Log event payloads. + */ + public function test_activity_log_event_insert_validation_rejects_invalid_payload() { + $this->add_activity_log_post_insert_filters(); + + try { + $post_id = wp_insert_post( + wp_slash( + array( + 'post_type' => Activity_Log_Event::POST_TYPE, + 'post_title' => 'Direct insert', + 'post_content' => wp_json_encode( + array( + 'title' => 'Cache flushed', + 'content' => 'Plain text note.', + 'severity' => 'critical', + ), + JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE + ), + 'post_status' => 'publish', + ) + ), + true + ); + } finally { + $this->remove_activity_log_post_insert_filters(); + } + + $this->assertInstanceOf( \WP_Error::class, $post_id ); + } + + /** + * Tests that helper-created events pass Sync published-post enqueue validation. + */ + public function test_activity_log_event_passes_sync_published_post_enqueue_validation() { + $post_id = Activity_Log_Event::create( + array( + 'title' => 'Cache flushed', + 'content' => 'Plain text note.', + 'source' => 'mc', + ) + ); + + $this->assertIsInt( $post_id ); + + $post = get_post( $post_id ); + + $this->assertInstanceOf( \WP_Post::class, $post ); + $this->assertIsArray( $this->filter_activity_log_sync_published_post( $post_id, $post ) ); + } + + /** + * Tests that helper-created events enqueue through Sync save-post. + */ + public function test_activity_log_event_passes_sync_save_post_enqueue() { + $post_id = Activity_Log_Event::create( + array( + 'title' => 'Cache flushed', + 'content' => 'Plain text note.', + 'source' => 'mc', + ) + ); + + $this->assertIsInt( $post_id ); + + $post = get_post( $post_id ); + + $this->assertInstanceOf( \WP_Post::class, $post ); + $this->assertIsArray( $this->filter_activity_log_sync_save_post( $post_id, $post ) ); + } + + /** + * Tests that direct CPT inserts with invalid payloads fail Sync save-post enqueue validation. + */ + public function test_activity_log_sync_save_post_validation_rejects_invalid_payload() { + $post_id = $this->insert_activity_log_post( + array( + 'title' => 'Cache flushed', + 'content' => 'Plain text note.', + 'severity' => 'critical', + ) + ); + + $post = get_post( $post_id ); + + $this->assertInstanceOf( \WP_Post::class, $post ); + $this->assertFalse( $this->filter_activity_log_sync_save_post( $post_id, $post ) ); + } + + /** + * Tests that Activity Log events cannot be publicized. + */ + public function test_activity_log_event_prevents_publicize() { + $post_id = Activity_Log_Event::create( + array( + 'title' => 'Cache flushed', + 'content' => 'Plain text note.', + ) + ); + + $this->assertIsInt( $post_id ); + + $post = get_post( $post_id ); + + $this->assertInstanceOf( \WP_Post::class, $post ); + $this->assertFalse( Activity_Log_Event::prevent_publicize( true, $post ) ); + } + + /** + * Tests that Activity Log events are removed from Jetpack sitemap post types. + */ + public function test_activity_log_event_filters_sitemap_post_types() { + $post_types = Activity_Log_Event::filter_sitemap_post_types( + array( + 'post', + Activity_Log_Event::POST_TYPE, + 'page', + ) + ); + + $this->assertSame( array( 'post', 'page' ), $post_types ); + } + + /** + * Tests that direct CPT inserts without a source pass Sync published-post enqueue validation. + */ + public function test_activity_log_sync_published_post_validation_allows_missing_source() { + $post_id = $this->insert_activity_log_post( + array( + 'title' => 'Cache flushed', + 'content' => 'Plain text note.', + ) + ); + + $post = get_post( $post_id ); + + $this->assertInstanceOf( \WP_Post::class, $post ); + $this->assertIsArray( $this->filter_activity_log_sync_published_post( $post_id, $post ) ); + } + + /** + * Tests that direct CPT inserts with non-scalar required fields fail Sync published-post enqueue validation. + */ + public function test_activity_log_sync_published_post_validation_rejects_non_scalar_required_values() { + $post_id = $this->insert_activity_log_post( + array( + 'title' => array( 'Cache flushed' ), + 'content' => 'Plain text note.', + 'source' => 'mc', + ) + ); + + $post = get_post( $post_id ); + + $this->assertInstanceOf( \WP_Post::class, $post ); + $this->assertFalse( $this->filter_activity_log_sync_published_post( $post_id, $post ) ); + } + + /** + * Tests that direct CPT inserts with invalid severity fail Sync published-post enqueue validation. + */ + public function test_activity_log_sync_published_post_validation_rejects_invalid_severity() { + $post_id = $this->insert_activity_log_post( + array( + 'title' => 'Cache flushed', + 'content' => 'Plain text note.', + 'severity' => 'critical', + ) + ); + + $post = get_post( $post_id ); + + $this->assertInstanceOf( \WP_Post::class, $post ); + $this->assertFalse( $this->filter_activity_log_sync_published_post( $post_id, $post ) ); + } + + /** + * Tests that direct CPT inserts with content that sanitizes to empty fail Sync published-post enqueue validation. + */ + public function test_activity_log_sync_published_post_validation_rejects_content_that_sanitizes_to_empty() { + $post_id = $this->insert_activity_log_post( + array( + 'title' => 'Cache flushed', + 'content' => " \n\t", + ) + ); + + $post = get_post( $post_id ); + + $this->assertInstanceOf( \WP_Post::class, $post ); + $this->assertFalse( $this->filter_activity_log_sync_published_post( $post_id, $post ) ); + } + + /** + * Gets the stored activity log payload for a post. + * + * @param int $post_id Post ID. + * @return array + */ + private function get_activity_log_payload( $post_id ) { + $post = get_post( $post_id ); + + $this->assertInstanceOf( \WP_Post::class, $post ); + + $payload = json_decode( $post->post_content, true ); + if ( ! is_array( $payload ) ) { + $payload = json_decode( wp_unslash( $post->post_content ), true ); + } + + $this->assertIsArray( $payload ); + + return $payload; + } + + /** + * Filters an Activity Log post through Sync save-post enqueue validation. + * + * @param int $post_id Post ID. + * @param \WP_Post $post Post object. + * @return array|false + */ + private function filter_activity_log_sync_save_post( $post_id, \WP_Post $post ) { + $module = new Posts(); + + return $module->filter_jetpack_sync_before_enqueue_jetpack_sync_save_post( + array( + $post_id, + $post, + false, + array( + 'previous_status' => 'new', + ), + ) + ); + } + + /** + * Filters an Activity Log post through Sync published-post enqueue validation. + * + * @param int $post_id Post ID. + * @param \WP_Post $post Post object. + * @return array|false + */ + private function filter_activity_log_sync_published_post( $post_id, \WP_Post $post ) { + $module = new Posts(); + + return $module->filter_jetpack_sync_before_enqueue_jetpack_published_post( + array( + $post_id, + array( + 'post_type' => Activity_Log_Event::POST_TYPE, + ), + $post, + ) + ); + } + + /** + * Adds Activity Log event insert filters for tests. + */ + private function add_activity_log_post_insert_filters() { + add_filter( 'wp_insert_post_empty_content', array( Activity_Log_Event::class, 'prevent_invalid_post_insert' ), 10, 2 ); + add_filter( 'wp_insert_post_data', array( Activity_Log_Event::class, 'normalize_post_data' ), 10, 2 ); + } + + /** + * Removes Activity Log event insert filters for tests. + */ + private function remove_activity_log_post_insert_filters() { + remove_filter( 'wp_insert_post_empty_content', array( Activity_Log_Event::class, 'prevent_invalid_post_insert' ), 10 ); + remove_filter( 'wp_insert_post_data', array( Activity_Log_Event::class, 'normalize_post_data' ), 10 ); + } + + /** + * Inserts an Activity Log CPT post directly. + * + * @param array $payload Activity Log payload. + * @return int + */ + private function insert_activity_log_post( array $payload ) { + $post_id = wp_insert_post( + wp_slash( + array( + 'post_type' => Activity_Log_Event::POST_TYPE, + 'post_title' => 'Direct insert', + 'post_content' => wp_json_encode( $payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ), + 'post_status' => 'publish', + ) + ), + true + ); + + $this->assertIsInt( $post_id ); + + return $post_id; + } +}