PluginProbe
ActivityPub / 8.2.0
ActivityPub v8.2.0
9.3.1 9.3.0 9.2.2 9.2.1 9.2.0 9.1.0 9.0.2 9.0.1 9.0.0 8.3.0 8.2.1 8.2.0 8.1.1 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.2.0 1.3.0 2.0.0 2.0.1 2.1.0 2.1.1 All 160 releases
activitypub / includes / collection / class-posts.php

class-posts.php in ActivityPub 8.2.0, at includes/collection/class-posts.php

191 lines 5.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Posts collection file.
4 *
5 * @package Activitypub
6 */
7
8 namespace Activitypub\Collection;
9
10 use Activitypub\Blocks;
11 use Activitypub\Hashtag;
12 use Activitypub\Link;
13
14 use function Activitypub\get_content_visibility;
15
16 /**
17 * Posts collection.
18 *
19 * Provides CRUD methods for local WordPress posts created
20 * via ActivityPub Client-to-Server (C2S) outbox.
21 *
22 * @see Remote_Posts for federated posts received via Server-to-Server (S2S).
23 */
24 class Posts {
25 /**
26 * Create a WordPress post from an ActivityPub activity.
27 *
28 * @since 8.1.0
29 *
30 * @param array $activity The activity data.
31 * @param int $user_id The local user ID.
32 * @param string|null $visibility Content visibility.
33 *
34 * @return \WP_Post|\WP_Error The created post on success, WP_Error on failure.
35 */
36 public static function create( $activity, $user_id, $visibility = null ) {
37 // Verify the user has permission to create posts.
38 if ( $user_id > 0 && ! \user_can( $user_id, 'publish_posts' ) ) {
39 return new \WP_Error(
40 'activitypub_forbidden',
41 \__( 'You do not have permission to create posts.', 'activitypub' ),
42 array( 'status' => 403 )
43 );
44 }
45
46 $object = $activity['object'] ?? array();
47
48 $object_type = $object['type'] ?? '';
49 $content = \wp_kses_post( $object['content'] ?? '' );
50 $name = \sanitize_text_field( $object['name'] ?? '' );
51 $summary = \wp_kses_post( $object['summary'] ?? '' );
52
53 // Process content: autop, autolink, hashtags, and convert to blocks.
54 $content = self::prepare_content( $content );
55
56 // Use name as title for Articles, or generate from content for Notes.
57 $title = $name;
58 if ( empty( $title ) && ! empty( $content ) ) {
59 $title = \wp_trim_words( \wp_strip_all_tags( $content ), 10, '...' );
60 }
61
62 // Determine visibility if not provided.
63 if ( null === $visibility ) {
64 $visibility = get_content_visibility( $activity );
65 }
66
67 $post_data = array(
68 'post_author' => $user_id > 0 ? $user_id : 0,
69 'post_title' => $title,
70 'post_content' => $content,
71 'post_excerpt' => $summary,
72 'post_status' => ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE === $visibility ? 'private' : 'publish',
73 'post_type' => 'post',
74 'meta_input' => array(
75 'activitypub_content_visibility' => $visibility,
76 ),
77 );
78
79 $post_id = \wp_insert_post( $post_data, true );
80
81 if ( \is_wp_error( $post_id ) ) {
82 return $post_id;
83 }
84
85 // Set post format to 'status' for Notes so the transformer maps it back correctly.
86 if ( 'Note' === $object_type ) {
87 \set_post_format( $post_id, 'status' );
88 }
89
90 return \get_post( $post_id );
91 }
92
93 /**
94 * Update a WordPress post from an ActivityPub activity.
95 *
96 * @since 8.1.0
97 *
98 * @param \WP_Post $post The post to update.
99 * @param array $activity The activity data.
100 * @param string|null $visibility Content visibility.
101 *
102 * @return \WP_Post|\WP_Error The updated post on success, WP_Error on failure.
103 */
104 public static function update( $post, $activity, $visibility = null ) {
105 $object = $activity['object'] ?? array();
106
107 $content = \wp_kses_post( $object['content'] ?? '' );
108 $name = \sanitize_text_field( $object['name'] ?? '' );
109 $summary = \wp_kses_post( $object['summary'] ?? '' );
110
111 // Process content: autop, autolink, hashtags, and convert to blocks.
112 $content = self::prepare_content( $content );
113
114 // Use name as title for Articles, or generate from content for Notes.
115 $title = $name;
116 if ( empty( $title ) && ! empty( $content ) ) {
117 $title = \wp_trim_words( \wp_strip_all_tags( $content ), 10, '...' );
118 }
119
120 // Determine visibility if not provided.
121 if ( null === $visibility ) {
122 $visibility = get_content_visibility( $activity );
123 }
124
125 $post_data = array(
126 'ID' => $post->ID,
127 'post_title' => $title,
128 'post_content' => $content,
129 'post_excerpt' => $summary,
130 'meta_input' => array(
131 'activitypub_content_visibility' => $visibility,
132 ),
133 );
134
135 $post_id = \wp_update_post( $post_data, true );
136
137 if ( \is_wp_error( $post_id ) ) {
138 return $post_id;
139 }
140
141 return \get_post( $post_id );
142 }
143
144 /**
145 * Delete (trash) a WordPress post.
146 *
147 * @since 8.1.0
148 *
149 * @param int $post_id The post ID.
150 *
151 * @return \WP_Post|false|null Post data on success, false or null on failure.
152 */
153 public static function delete( $post_id ) {
154 return \wp_trash_post( $post_id );
155 }
156
157 /**
158 * Prepare content for storage as a WordPress post.
159 *
160 * Applies wpautop (for plain text), autolinks bare URLs,
161 * converts hashtags to links, and wraps in block markup.
162 *
163 * @since 8.1.0
164 *
165 * @param string $content The HTML or plain-text content.
166 *
167 * @return string The processed content with block markup.
168 */
169 public static function prepare_content( $content ) {
170 if ( empty( $content ) ) {
171 return '';
172 }
173
174 // Wrap plain text in paragraphs if it has no block-level HTML.
175 if ( ! \preg_match( '/<(p|h[1-6]|ul|ol|blockquote|figure|hr|img|div|pre|table)\b/i', $content ) ) {
176 $content = \wpautop( $content );
177 }
178
179 // Convert bare URLs to links.
180 $content = Link::the_content( $content );
181
182 // Convert #hashtags to links.
183 $content = Hashtag::the_content( $content );
184
185 // Convert HTML to block markup.
186 $content = Blocks::convert_from_html( $content );
187
188 return $content;
189 }
190 }
191