0 ? $user_id : \get_current_user_id(); /* * Authorize the request: * - Per-user path: require `publish_posts` on the URL-specified user. * - Blog actor path (post_author falls back to current user): require * the act-as-blog grant. `publish_posts` is implicit because the * helper defaults to `manage_options` (administrators). * - Cron/CLI path keeps `post_author = 0` and bypasses both checks. */ if ( $post_author > 0 ) { $authorized = $post_author === (int) $user_id ? \user_can( $user_id, 'publish_posts' ) : user_can_act_as_blog(); if ( ! $authorized ) { return new \WP_Error( 'activitypub_forbidden', \__( 'You do not have permission to create posts.', 'activitypub' ), array( 'status' => 403 ) ); } } $object = $activity['object'] ?? array(); $object_type = $object['type'] ?? ''; $content = \wp_kses_post( $object['content'] ?? '' ); $name = \sanitize_text_field( $object['name'] ?? '' ); $summary = \wp_kses_post( $object['summary'] ?? '' ); $plain_summary = \sanitize_text_field( $summary ); // A summary marked sensitive is a content warning (plain text); otherwise it's a regular excerpt. // Route on the sanitized summary so whitespace-only values don't pollute either field. $content_warning = ! empty( $object['sensitive'] ) && '' !== $plain_summary ? $plain_summary : ''; $post_excerpt = '' === $content_warning && '' !== $plain_summary ? $summary : ''; // Process content: autop, autolink, hashtags, and convert to blocks. $content = self::prepare_content( $content ); // Use name as title for Articles, or generate from content for Notes. $title = $name; if ( empty( $title ) && ! empty( $content ) ) { $title = \wp_trim_words( \wp_strip_all_tags( $content ), 10, '...' ); } // Determine visibility if not provided. if ( null === $visibility ) { $visibility = get_content_visibility( $activity ); } $post_data = array( 'post_author' => $post_author, 'post_title' => $title, 'post_content' => $content, 'post_excerpt' => $post_excerpt, 'post_status' => ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE === $visibility ? 'private' : 'publish', 'post_type' => 'post', 'meta_input' => array( 'activitypub_content_visibility' => $visibility, 'activitypub_content_warning' => $content_warning, ), ); $post_id = \wp_insert_post( $post_data, true ); if ( \is_wp_error( $post_id ) ) { return $post_id; } // Set post format to 'status' for Notes so the transformer maps it back correctly. if ( 'Note' === $object_type ) { \set_post_format( $post_id, 'status' ); } return \get_post( $post_id ); } /** * Update a WordPress post from an ActivityPub activity. * * @since 8.1.0 * * @param \WP_Post $post The post to update. * @param array $activity The activity data. * @param string|null $visibility Content visibility. * * @return \WP_Post|\WP_Error The updated post on success, WP_Error on failure. */ public static function update( $post, $activity, $visibility = null ) { $object = $activity['object'] ?? array(); $content = \wp_kses_post( $object['content'] ?? '' ); $name = \sanitize_text_field( $object['name'] ?? '' ); $summary = \wp_kses_post( $object['summary'] ?? '' ); $plain_summary = \sanitize_text_field( $summary ); // A summary marked sensitive is a content warning (plain text); otherwise it's a regular excerpt. // Route on the sanitized summary so whitespace-only values don't pollute either field. $content_warning = ! empty( $object['sensitive'] ) && '' !== $plain_summary ? $plain_summary : ''; $post_excerpt = '' === $content_warning && '' !== $plain_summary ? $summary : ''; // Process content: autop, autolink, hashtags, and convert to blocks. $content = self::prepare_content( $content ); // Use name as title for Articles, or generate from content for Notes. $title = $name; if ( empty( $title ) && ! empty( $content ) ) { $title = \wp_trim_words( \wp_strip_all_tags( $content ), 10, '...' ); } // Determine visibility if not provided. if ( null === $visibility ) { $visibility = get_content_visibility( $activity ); } $post_data = array( 'ID' => $post->ID, 'post_title' => $title, 'post_content' => $content, 'post_excerpt' => $post_excerpt, 'meta_input' => array( 'activitypub_content_visibility' => $visibility, 'activitypub_content_warning' => $content_warning, ), ); $post_id = \wp_update_post( $post_data, true ); if ( \is_wp_error( $post_id ) ) { return $post_id; } return \get_post( $post_id ); } /** * Delete (trash) a WordPress post. * * @since 8.1.0 * * @param int $post_id The post ID. * * @return \WP_Post|false|null Post data on success, false or null on failure. */ public static function delete( $post_id ) { return \wp_trash_post( $post_id ); } /** * Prepare content for storage as a WordPress post. * * Applies wpautop (for plain text), autolinks bare URLs, * converts hashtags to links, and wraps in block markup. * * @since 8.1.0 * * @param string $content The HTML or plain-text content. * * @return string The processed content with block markup. */ public static function prepare_content( $content ) { if ( empty( $content ) ) { return ''; } // Wrap plain text in paragraphs if it has no block-level HTML. if ( ! \preg_match( '/<(p|h[1-6]|ul|ol|blockquote|figure|hr|img|div|pre|table)\b/i', $content ) ) { $content = \wpautop( $content ); } // Convert bare URLs to links. $content = Link::the_content( $content ); // Convert #hashtags to links. $content = Hashtag::the_content( $content ); // Convert HTML to block markup. $content = Blocks::convert_from_html( $content ); return $content; } }