PluginProbe
ActivityPub / 4.0.2
ActivityPub v4.0.2
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 4.0.2, at includes/model/class-follower.php

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