PluginProbe
ActivityPub / 3.2.5
ActivityPub v3.2.5
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 / model / class-follower.php

class-follower.php in ActivityPub 3.2.5, at includes/model/class-follower.php

396 lines 8.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace Activitypub\Model;
3
4 use WP_Error;
5 use WP_Query;
6 use Activitypub\Activity\Actor;
7 use Activitypub\Collection\Followers;
8
9 /**
10 * ActivityPub Follower Class
11 *
12 * This Object represents a single Follower.
13 * There is no direct reference to a WordPress User here.
14 *
15 * @author Matt Wiebe
16 * @author Matthias Pfefferle
17 *
18 * @see https://www.w3.org/TR/activitypub/#follow-activity-inbox
19 */
20 class Follower extends Actor {
21 /**
22 * The complete Remote-Profile of the Follower
23 *
24 * @var array
25 */
26 protected $_id; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore
27
28 /**
29 * Get the errors.
30 *
31 * @return mixed
32 */
33 public function get_errors() {
34 return get_post_meta( $this->_id, 'activitypub_errors' );
35 }
36
37 /**
38 * Get the Summary.
39 *
40 * @return int The Summary.
41 */
42 public function get_summary() {
43 if ( isset( $this->summary ) ) {
44 return $this->summary;
45 }
46
47 return '';
48 }
49
50 /**
51 * Getter for URL attribute.
52 *
53 * Falls back to ID, if no URL is set. This is relevant for
54 * Plattforms like Lemmy, where the ID is the URL.
55 *
56 * @return string The URL.
57 */
58 public function get_url() {
59 if ( $this->url ) {
60 return $this->url;
61 }
62
63 return $this->id;
64 }
65
66 /**
67 * Reset (delete) all errors.
68 *
69 * @return void
70 */
71 public function reset_errors() {
72 delete_post_meta( $this->_id, 'activitypub_errors' );
73 }
74
75 /**
76 * Count the errors.
77 *
78 * @return int The number of errors.
79 */
80 public function count_errors() {
81 $errors = $this->get_errors();
82
83 if ( is_array( $errors ) && ! empty( $errors ) ) {
84 return count( $errors );
85 }
86
87 return 0;
88 }
89
90 /**
91 * Return the latest error message.
92 *
93 * @return string The error message.
94 */
95 public function get_latest_error_message() {
96 $errors = $this->get_errors();
97
98 if ( is_array( $errors ) && ! empty( $errors ) ) {
99 return reset( $errors );
100 }
101
102 return '';
103 }
104
105 /**
106 * Update the current Follower-Object.
107 *
108 * @return void
109 */
110 public function update() {
111 $this->save();
112 }
113
114 /**
115 * Validate the current Follower-Object.
116 *
117 * @return boolean True if the verification was successful.
118 */
119 public function is_valid() {
120 // the minimum required attributes
121 $required_attributes = array(
122 'id',
123 'preferredUsername',
124 'inbox',
125 'publicKey',
126 'publicKeyPem',
127 );
128
129 foreach ( $required_attributes as $attribute ) {
130 if ( ! $this->get( $attribute ) ) {
131 return false;
132 }
133 }
134
135 return true;
136 }
137
138 /**
139 * Save the current Follower-Object.
140 *
141 * @return int|WP_Error The Post-ID or an WP_Error.
142 */
143 public function save() {
144 if ( ! $this->is_valid() ) {
145 return new WP_Error( 'activitypub_invalid_follower', __( 'Invalid Follower', 'activitypub' ), array( 'status' => 400 ) );
146 }
147
148 if ( ! $this->get__id() ) {
149 global $wpdb;
150
151 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
152 $post_id = $wpdb->get_var(
153 $wpdb->prepare(
154 "SELECT ID FROM $wpdb->posts WHERE guid=%s",
155 esc_sql( $this->get_id() )
156 )
157 );
158
159 if ( $post_id ) {
160 $post = get_post( $post_id );
161 $this->set__id( $post->ID );
162 }
163 }
164
165 $post_id = $this->get__id();
166
167 $args = array(
168 'ID' => $post_id,
169 'guid' => esc_url_raw( $this->get_id() ),
170 'post_title' => wp_strip_all_tags( sanitize_text_field( $this->get_name() ) ),
171 'post_author' => 0,
172 'post_type' => Followers::POST_TYPE,
173 'post_name' => esc_url_raw( $this->get_id() ),
174 'post_excerpt' => sanitize_text_field( wp_kses( $this->get_summary(), 'user_description' ) ),
175 'post_status' => 'publish',
176 'meta_input' => $this->get_post_meta_input(),
177 );
178
179 if ( ! empty( $post_id ) ) {
180 // If this is an update, prevent the "followed" date from being
181 // overwritten by the current date.
182 $post = get_post( $post_id );
183 $args['post_date'] = $post->post_date;
184 $args['post_date_gmt'] = $post->post_date_gmt;
185 }
186
187 $post_id = wp_insert_post( $args );
188 $this->_id = $post_id;
189
190 return $post_id;
191 }
192
193 /**
194 * Upsert the current Follower-Object.
195 *
196 * @return int|WP_Error The Post-ID or an WP_Error.
197 */
198 public function upsert() {
199 return $this->save();
200 }
201
202 /**
203 * Delete the current Follower-Object.
204 *
205 * Beware that this os deleting a Follower for ALL users!!!
206 *
207 * To delete only the User connection (unfollow)
208 * @see \Activitypub\Rest\Followers::remove_follower()
209 *
210 * @return void
211 */
212 public function delete() {
213 wp_delete_post( $this->_id );
214 }
215
216 /**
217 * Update the post meta.
218 *
219 * @return void
220 */
221 protected function get_post_meta_input() {
222 $meta_input = array();
223 $meta_input['activitypub_inbox'] = $this->get_shared_inbox();
224 $meta_input['activitypub_actor_json'] = $this->to_json();
225
226 return $meta_input;
227 }
228
229 /**
230 * Get the icon.
231 *
232 * Sets a fallback to better handle API and HTML outputs.
233 *
234 * @return array The icon.
235 */
236 public function get_icon() {
237 if ( isset( $this->icon['url'] ) ) {
238 return $this->icon;
239 }
240
241 return array(
242 'type' => 'Image',
243 'mediaType' => 'image/jpeg',
244 'url' => ACTIVITYPUB_PLUGIN_URL . 'assets/img/mp.jpg',
245 );
246 }
247
248 /**
249 * Get Name.
250 *
251 * Tries to extract a name from the URL or ID if not set.
252 *
253 * @return string The name.
254 */
255 public function get_name() {
256 if ( $this->name ) {
257 return $this->name;
258 } elseif ( $this->preferred_username ) {
259 return $this->preferred_username;
260 }
261
262 return $this->extract_name_from_uri();
263 }
264
265 /**
266 * The preferred Username.
267 *
268 * Tries to extract a name from the URL or ID if not set.
269 *
270 * @return string The preferred Username.
271 */
272 public function get_preferred_username() {
273 if ( $this->preferred_username ) {
274 return $this->preferred_username;
275 }
276
277 return $this->extract_name_from_uri();
278 }
279
280 /**
281 * Get the Icon URL (Avatar)
282 *
283 * @return string The URL to the Avatar.
284 */
285 public function get_icon_url() {
286 $icon = $this->get_icon();
287
288 if ( ! $icon ) {
289 return '';
290 }
291
292 if ( is_array( $icon ) ) {
293 return $icon['url'];
294 }
295
296 return $icon;
297 }
298
299 /**
300 * Get the Icon URL (Avatar)
301 *
302 * @return string The URL to the Avatar.
303 */
304 public function get_image_url() {
305 $image = $this->get_image();
306
307 if ( ! $image ) {
308 return '';
309 }
310
311 if ( is_array( $image ) ) {
312 return $image['url'];
313 }
314
315 return $image;
316 }
317
318 /**
319 * Get the shared inbox, with a fallback to the inbox.
320 *
321 * @return string|null The URL to the shared inbox, the inbox or null.
322 */
323 public function get_shared_inbox() {
324 if ( ! empty( $this->get_endpoints()['sharedInbox'] ) ) {
325 return $this->get_endpoints()['sharedInbox'];
326 } elseif ( ! empty( $this->get_inbox() ) ) {
327 return $this->get_inbox();
328 }
329
330 return null;
331 }
332
333 /**
334 * Convert a Custom-Post-Type input to an Activitypub\Model\Follower.
335 *
336 * @return string The JSON string.
337 *
338 * @return array Activitypub\Model\Follower
339 */
340 public static function init_from_cpt( $post ) {
341 $actor_json = get_post_meta( $post->ID, 'activitypub_actor_json', true );
342 $object = self::init_from_json( $actor_json );
343 $object->set__id( $post->ID );
344 $object->set_id( $post->guid );
345 $object->set_name( $post->post_title );
346 $object->set_summary( $post->post_excerpt );
347 $object->set_published( gmdate( 'Y-m-d H:i:s', strtotime( $post->post_date ) ) );
348 $object->set_updated( gmdate( 'Y-m-d H:i:s', strtotime( $post->post_modified ) ) );
349
350 return $object;
351 }
352
353 /**
354 * Infer a shortname from the Actor ID or URL. Used only for fallbacks,
355 * we will try to use what's supplied.
356 *
357 * @return string Hopefully the name of the Follower.
358 */
359 protected function extract_name_from_uri() {
360 // prefer the URL, but fall back to the ID.
361 if ( $this->url ) {
362 $name = $this->url;
363 } else {
364 $name = $this->id;
365 }
366
367 if ( \filter_var( $name, FILTER_VALIDATE_URL ) ) {
368 $name = \rtrim( $name, '/' );
369 $path = \wp_parse_url( $name, PHP_URL_PATH );
370
371 if ( $path ) {
372 if ( \strpos( $name, '@' ) !== false ) {
373 // expected: https://example.com/@user (default URL pattern)
374 $name = \preg_replace( '|^/@?|', '', $path );
375 } else {
376 // expected: https://example.com/users/user (default ID pattern)
377 $parts = \explode( '/', $path );
378 $name = \array_pop( $parts );
379 }
380 }
381 } elseif (
382 \is_email( $name ) ||
383 \strpos( $name, 'acct' ) === 0 ||
384 \strpos( $name, '@' ) === 0
385 ) {
386 // expected: user@example.com or acct:user@example (WebFinger)
387 $name = \ltrim( $name, '@' );
388 $name = \ltrim( $name, 'acct:' );
389 $parts = \explode( '@', $name );
390 $name = $parts[0];
391 }
392
393 return $name;
394 }
395 }
396