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;
+ }
+}