PluginProbe
ActivityPub / 5.3.1
ActivityPub v5.3.1
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.3.1, at includes/model/class-follower.php

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