PluginProbe
ActivityPub / 5.9.0
ActivityPub v5.9.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 / model / class-follower.php

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

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