PluginProbe
Friends / 4.3.2
Friends v4.3.2
4.3.2 4.3.1 4.3.0 4.2.2 4.2.1 4.2.0 4.1.0 2.7.4 2.7.5 2.7.6 2.7.7 2.7.8 2.7.9 2.8.0 2.8.1 2.8.2 2.8.3 2.8.4 2.8.5 2.8.6 2.8.7 2.8.8 2.8.9 2.9.0 2.9.1 All 88 releases
friends / includes / class-subscription.php

class-subscription.php in Friends 4.3.2, at includes/class-subscription.php

928 lines 27.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Friend Subscription
4 *
5 * This is a virtual user to allow subscriptions without a WordPress user
6 *
7 * @package Friends
8 */
9
10 namespace Friends;
11
12 /**
13 * This is the class for the User part of the Friends Plugin.
14 *
15 * @since 3.0
16 *
17 * @package Friends
18 * @author Alex Kirk
19 */
20 class Subscription extends User {
21 const TAXONOMY = 'friends-virtual-user';
22 private $term;
23 const MIGRATE_USER_OPTIONS = array(
24 'friends_retention_number',
25 'friends_enable_retention_days',
26 'friends_retention_days',
27 'friends_feed_rules',
28 'friends_feed_catch_all',
29 'friends_in_token',
30 'friends_out_token',
31 'friends_starred',
32 'friends_rest_url',
33 'friends_user_icon_url',
34 );
35
36 public function __construct( \WP_Term $term ) {
37 $this->term = $term;
38 $this->ID = 1e10 + $term->term_id;
39
40 $this->caps = array_fill_keys( get_metadata( 'term', $term->term_id, 'roles', false ), true );
41 $this->caps['subscription'] = true;
42 $this->get_role_caps();
43
44 $this->roles = array_values( $this->roles );
45
46 $this->data = (object) array(
47 'ID' => $this->ID,
48 'user_login' => $term->name,
49 'display_name' => $this->get_user_option( 'display_name' ),
50 'user_url' => $this->get_user_option( 'user_url' ),
51 'description' => $this->get_user_option( 'description' ),
52 'user_registered' => $this->get_user_option( 'created' ),
53 );
54 }
55
56 public function get_term_id() {
57 return $this->term->term_id;
58 }
59
60 public function get_object_id() {
61 return $this->get_term_id();
62 }
63
64 public function save() {
65 foreach ( array(
66 'first_name',
67 'display_name',
68 'description',
69 'user_url',
70 ) as $key ) {
71 $this->update_user_option( $key, $this->$key );
72 }
73 return $this->get_object_id();
74 }
75
76 /**
77 * Update the user_login (term name) for this subscription.
78 *
79 * @param string $new_user_login The new user login.
80 * @return bool True on success.
81 */
82 public function update_user_login( $new_user_login ) {
83 $existing = term_exists( $new_user_login, self::TAXONOMY );
84 if ( $existing && (int) $existing['term_id'] !== $this->get_term_id() ) {
85 return false;
86 }
87
88 $result = wp_update_term(
89 $this->get_term_id(),
90 self::TAXONOMY,
91 array(
92 'name' => $new_user_login,
93 'slug' => $new_user_login,
94 )
95 );
96
97 if ( ! is_wp_error( $result ) ) {
98 $this->term->name = $new_user_login;
99 $this->term->slug = $new_user_login;
100 $this->data->user_login = $new_user_login;
101 $this->user_login = $new_user_login;
102 return true;
103 }
104
105 return false;
106 }
107
108 /**
109 * Registers the taxonomy
110 */
111 public static function register_taxonomy() {
112 $args = array(
113 'labels' => array(
114 'name' => _x( 'Virtual User', 'taxonomy general name', 'friends' ),
115 'singular_name' => _x( 'Virtual User', 'taxonomy singular name', 'friends' ),
116 'menu_name' => __( 'Virtual User', 'friends' ),
117 ),
118 'hierarchical' => true,
119 'show_ui' => true,
120 'show_admin_column' => true,
121 'query_var' => true,
122 'rewrite' => false,
123 'public' => false,
124 );
125 register_taxonomy( self::TAXONOMY, 'None', $args );
126 }
127
128 public static function get_by_username( $username ) {
129 $term_query = new \WP_Term_Query(
130 array(
131 'taxonomy' => self::TAXONOMY,
132 'name' => $username,
133 'hide_empty' => false,
134 )
135 );
136
137 foreach ( $term_query->get_terms() as $term ) {
138 return new self( $term );
139 }
140
141 return new \WP_Error( 'user-not-found' );
142 }
143
144 public function insert_post( array $postarr, $wp_error = false, $fire_after_hooks = true ) {
145 if ( ! isset( $postarr['post_author'] ) ) {
146 $postarr['post_author'] = 0;
147 }
148 $post_id = wp_insert_post( $postarr, $wp_error, $fire_after_hooks );
149 if ( ! is_wp_error( $post_id ) ) {
150 wp_set_object_terms( $post_id, $this->get_term_id(), self::TAXONOMY );
151 }
152
153 return $post_id;
154 }
155
156 public function modify_query_by_author( \WP_Query $query ) {
157 $tax_query = $query->get( 'tax_query' );
158 if ( ! $tax_query ) {
159 $tax_query = array();
160 } else {
161 $tax_query['relation'] = 'AND';
162 }
163 $tax_query[] =
164 array(
165 'taxonomy' => self::TAXONOMY,
166 'field' => 'term_id',
167 'terms' => $this->get_term_id(),
168 );
169 $query->set( 'tax_query', $tax_query );
170 return $query;
171 }
172
173 public function modify_get_posts_args_by_author( $args ) {
174 if ( isset( $args['author'] ) ) {
175 unset( $args['author'] );
176 }
177 if ( ! isset( $args['tax_query'] ) ) {
178 $args['tax_query'] = array(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
179 } else {
180 $args['tax_query']['relation'] = 'AND';
181 }
182 $args['tax_query'][] =
183 array(
184 'taxonomy' => self::TAXONOMY,
185 'field' => 'term_id',
186 'terms' => $this->get_term_id(),
187 );
188 return $args;
189 }
190
191
192 public static function set_authordata_by_query( $query ) {
193 global $authordata;
194 if ( $query->get( 'tax_query' ) ) {
195 foreach ( $query->get( 'tax_query' ) as $tax_query ) {
196 if ( ! is_array( $tax_query ) || ! isset( $tax_query['taxonomy'] ) || self::TAXONOMY !== $tax_query['taxonomy'] ) {
197 continue;
198 }
199 if ( 'term_id' === $tax_query['field'] ) {
200 $term_id = $tax_query['terms'];
201 if ( is_array( $term_id ) ) {
202 $term_id = reset( $term_id );
203 }
204 $authordata = new Subscription( \get_term( $term_id ) ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
205 return;
206 }
207 }
208 }
209 }
210
211
212 /**
213 * Gets the post stats.
214 *
215 * @return object The post stats.
216 */
217 public function get_post_stats() {
218 $cache_key = 'post_stats_author_' . $this->ID;
219 $post_stats = wp_cache_get( $cache_key, 'friends' );
220 if ( false !== $post_stats ) {
221 return $post_stats;
222 }
223 global $wpdb;
224 $post_types = apply_filters( 'friends_frontend_post_types', array() );
225 $post_stats = $wpdb->get_row( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
226 $wpdb->prepare(
227 sprintf(
228 'SELECT SUM(
229 LENGTH( ID ) +
230 LENGTH( post_author ) +
231 LENGTH( post_date ) +
232 LENGTH( post_date_gmt ) +
233 LENGTH( post_content ) +
234 LENGTH( post_title ) +
235 LENGTH( post_excerpt ) +
236 LENGTH( post_status ) +
237 LENGTH( comment_status ) +
238 LENGTH( ping_status ) +
239 LENGTH( post_password ) +
240 LENGTH( post_name ) +
241 LENGTH( to_ping ) +
242 LENGTH( pinged ) +
243 LENGTH( post_modified ) +
244 LENGTH( post_modified_gmt ) +
245 LENGTH( post_content_filtered ) +
246 LENGTH( post_parent ) +
247 LENGTH( guid ) +
248 LENGTH( menu_order ) +
249 LENGTH( post_type ) +
250 LENGTH( post_mime_type ) +
251 LENGTH( comment_count )
252 ) AS total_size,
253 COUNT(*) as post_count
254 FROM %s p, %s t, %s r
255 WHERE r.object_id = p.ID
256 AND r.term_taxonomy_id = t.term_taxonomy_id
257 AND t.term_id = %%d
258 AND p.post_type IN ( %s )',
259 $wpdb->posts,
260 $wpdb->term_taxonomy,
261 $wpdb->term_relationships,
262 implode( ', ', array_fill( 0, count( $post_types ), '%s' ) )
263 ),
264 array_merge( array( $this->get_term_id() ), $post_types )
265 ),
266 ARRAY_A
267 );
268
269 $post_stats['earliest_post_date'] = mysql2date(
270 'U',
271 $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
272 $wpdb->prepare(
273 sprintf(
274 'SELECT MIN(post_date)
275 FROM %s p, %s t, %s r
276 WHERE r.object_id = p.ID
277 AND r.term_taxonomy_id = t.term_taxonomy_id
278 AND t.term_id = %%d
279 AND p.post_status = "publish"
280 AND p.post_type IN ( %s )',
281 $wpdb->posts,
282 $wpdb->term_taxonomy,
283 $wpdb->term_relationships,
284 implode( ', ', array_fill( 0, count( $post_types ), '%s' ) )
285 ),
286 array_merge( array( $this->get_term_id() ), $post_types )
287 )
288 )
289 );
290
291 wp_cache_set( $cache_key, $post_stats, 'friends', HOUR_IN_SECONDS );
292 return $post_stats;
293 }
294
295 public function get_all_post_ids() {
296 global $wpdb;
297 $post_types = apply_filters( 'friends_frontend_post_types', array() );
298
299 $cache_key = 'get_all_post_ids_' . $this->ID . '_' . implode( '_', $post_types );
300 $post_ids = wp_cache_get( $cache_key, 'friends' );
301 if ( false !== $post_ids ) {
302 return $post_ids;
303 }
304
305 $post_ids = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
306 $wpdb->prepare(
307 sprintf(
308 'SELECT posts.ID
309 FROM %s AS posts
310 JOIN %s AS taxonomy_author
311 JOIN %s AS relationships_author
312 WHERE posts.post_type IN ( %s )
313 AND relationships_author.object_id = posts.ID
314 AND taxonomy_author.term_taxonomy_id = relationships_author.term_taxonomy_id
315 AND taxonomy_author.term_id = %%d
316 ',
317 $wpdb->posts,
318 $wpdb->term_taxonomy,
319 $wpdb->term_relationships,
320 implode( ', ', array_fill( 0, count( $post_types ), '%s' ) )
321 ),
322 array_merge(
323 $post_types,
324 array( $this->get_term_id() )
325 )
326 )
327 );
328
329 wp_cache_set( $cache_key, $post_ids, 'friends', HOUR_IN_SECONDS - 60 );
330
331 return $post_ids;
332 }
333
334 /**
335 * Gets the post counts by post format.
336 *
337 * @return array The post counts.
338 */
339 public function get_post_count_by_post_format() {
340 $cache_key = 'get_post_count_by_post_format_' . $this->ID;
341
342 $counts = wp_cache_get( $cache_key, 'friends' );
343 if ( false !== $counts ) {
344 return $counts;
345 }
346 $counts = get_transient( $cache_key );
347 if ( false !== $counts ) {
348 return $counts;
349 }
350 $counts = array();
351 $post_types = apply_filters( 'friends_frontend_post_types', array() );
352 $post_formats_term_ids = array();
353 foreach ( get_post_format_slugs() as $post_format ) {
354 $term = get_term_by( 'slug', 'post-format-' . $post_format, 'post_format' );
355 if ( $term ) {
356 $post_formats_term_ids[ $term->term_taxonomy_id ] = $post_format;
357 }
358 }
359
360 global $wpdb;
361
362 $counts = array();
363 $counts['standard'] = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
364 $wpdb->prepare(
365 sprintf(
366 "SELECT COUNT(DISTINCT posts.ID)
367 FROM %s AS posts
368 JOIN %s AS relationships_post_format
369 JOIN %s AS taxonomy_author
370 JOIN %s AS relationships_author
371
372 WHERE posts.post_status IN ( 'publish', 'private' )
373 AND posts.post_type IN ( %s )
374 AND relationships_post_format.object_id = posts.ID
375 AND relationships_author.object_id = posts.ID
376 AND taxonomy_author.term_taxonomy_id = relationships_author.term_taxonomy_id
377 AND taxonomy_author.term_id = %%d",
378 $wpdb->posts,
379 $wpdb->term_relationships,
380 $wpdb->term_taxonomy,
381 $wpdb->term_relationships,
382 implode( ',', array_fill( 0, count( $post_types ), '%s' ) )
383 ),
384 array_merge(
385 $post_types,
386 array( $this->get_term_id() )
387 )
388 )
389 );
390
391 if ( ! empty( $post_formats_term_ids ) ) {
392 $post_format_counts = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
393 $wpdb->prepare(
394 sprintf(
395 "SELECT relationships_post_format.term_taxonomy_id AS post_format_id, COUNT(relationships_post_format.term_taxonomy_id) AS count
396 FROM %s AS posts
397 JOIN %s AS relationships_post_format
398 JOIN %s AS taxonomy_author
399 JOIN %s AS relationships_author
400
401 WHERE posts.post_status IN ( 'publish', 'private' )
402 AND posts.post_type IN ( %s )
403 AND relationships_post_format.object_id = posts.ID
404 AND relationships_post_format.term_taxonomy_id IN ( %s )
405 AND relationships_author.object_id = posts.ID
406 AND taxonomy_author.term_taxonomy_id = relationships_author.term_taxonomy_id
407 AND taxonomy_author.term_id = %%d
408 GROUP BY relationships_post_format.term_taxonomy_id",
409 $wpdb->posts,
410 $wpdb->term_relationships,
411 $wpdb->term_taxonomy,
412 $wpdb->term_relationships,
413 implode( ',', array_fill( 0, count( $post_types ), '%s' ) ),
414 implode( ',', array_fill( 0, count( $post_formats_term_ids ), '%d' ) )
415 ),
416 array_merge(
417 $post_types,
418 array_keys( $post_formats_term_ids ),
419 array( $this->get_term_id() )
420 )
421 )
422 );
423
424 foreach ( $post_format_counts as $row ) {
425 $counts[ $post_formats_term_ids[ $row->post_format_id ] ] = $row->count;
426 $counts['standard'] -= $row->count;
427 }
428 }
429
430 $counts = array_filter( $counts );
431
432 set_transient( $cache_key, $counts, HOUR_IN_SECONDS );
433 wp_cache_set( $cache_key, $counts, 'friends', HOUR_IN_SECONDS );
434
435 return $counts;
436 }
437
438 /**
439 * Gets the post counts by post format.
440 *
441 * @return int The post count.
442 */
443 public function get_post_in_trash_count() {
444 global $wpdb;
445 $post_types = apply_filters( 'friends_frontend_post_types', array() );
446
447 $cache_key = 'get_post_in_trash_count_' . $this->get_term_id() . '_' . implode( '_', $post_types );
448 if ( false !== wp_cache_get( $cache_key, 'friends' ) ) {
449 return wp_cache_get( $cache_key, 'friends' );
450 }
451
452 $count = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
453 $wpdb->prepare(
454 sprintf(
455 'SELECT COUNT(*)
456 FROM %s p, %s t, %s r
457 WHERE r.object_id = p.ID
458 AND r.term_taxonomy_id = t.term_taxonomy_id
459 AND t.term_id = %%d
460 AND post_type IN ( %s )
461 AND post_status = "trash"',
462 $wpdb->posts,
463 $wpdb->term_taxonomy,
464 $wpdb->term_relationships,
465 implode( ', ', array_fill( 0, count( $post_types ), '%s' ) )
466 ),
467 array_merge( array( $this->get_term_id() ), $post_types )
468 )
469 );
470
471 wp_cache_set( $cache_key, intval( $count ), 'friends', HOUR_IN_SECONDS - 60 );
472 return intval( $count );
473 }
474
475 /**
476 * Gets the role name (for a specific count).
477 *
478 * @param bool $group_subscriptions Whether to group all types of subscriptions into the name "Subscriptions".
479 * @param int $count The count if more than one.
480 *
481 * @return string The role name.
482 */
483 public function get_role_name( $group_subscriptions = false, $count = 1 ) {
484 return _nx( 'Subscription', 'Subscriptions', $count, 'User role', 'friends' );
485 }
486
487 public function get_feeds() {
488 $term_query = new \WP_Term_Query(
489 array(
490 'taxonomy' => User_Feed::TAXONOMY,
491 'parent' => $this->get_term_id(),
492 'hide_empty' => false,
493 )
494 );
495
496 $feeds = array();
497 foreach ( $term_query->get_terms() as $term ) {
498 $feeds[ $term->term_id ] = new User_Feed( $term, $this );
499 }
500
501 return $feeds;
502 }
503
504 public function save_feeds( $feeds = array() ) {
505 $errors = new \WP_Error();
506 foreach ( $feeds as $feed_url => $options ) {
507 if ( ! is_string( $feed_url ) || ! Friends::check_url( $feed_url ) ) {
508 $errors->add( 'invalid-url', 'An invalid URL was provided', $feed_url );
509 unset( $feeds[ $feed_url ] );
510 continue;
511 }
512
513 $default_options = array(
514 'active' => false,
515 'parser' => 'simplepie',
516 'post-format' => 'standard',
517 'mime-type' => 'application/rss+xml',
518 'title' => $this->display_name . ' RSS Feed',
519 );
520
521 $feeds[ $feed_url ] = array_merge( $default_options, $options );
522 }
523
524 $subscription_term_id = $this->get_term_id();
525 $all_urls = array();
526 foreach ( get_terms(
527 array(
528 'taxonomy' => User_Feed::TAXONOMY,
529 'parent' => $subscription_term_id,
530 'hide_empty' => false,
531 )
532 ) as $term ) {
533 $url = str_replace( '&amp;', '&', $term->name );
534 $all_urls[ $url ] = $term->term_id;
535 }
536
537 foreach ( $feeds as $url => $options ) {
538 if ( isset( $all_urls[ $url ] ) ) {
539 continue;
540 }
541 $result = wp_insert_term( $url, User_Feed::TAXONOMY, array( 'parent' => $subscription_term_id ) );
542 if ( is_wp_error( $result ) ) {
543 if ( 'term_exists' === $result->get_error_code() ) {
544 $existing_term_id = (int) $result->get_error_data();
545 wp_update_term( $existing_term_id, User_Feed::TAXONOMY, array( 'parent' => $subscription_term_id ) );
546 $all_urls[ $url ] = $existing_term_id;
547 }
548 } else {
549 $all_urls[ $url ] = $result['term_id'];
550 }
551 }
552
553 foreach ( $feeds as $url => $feed_options ) {
554 if ( ! isset( $all_urls[ $url ] ) ) {
555 continue;
556 }
557 $term_id = $all_urls[ $url ];
558 foreach ( $feed_options as $key => $value ) {
559 if ( in_array( $key, array( 'active', 'parser', 'post-format', 'mime-type', 'title', 'interval', 'modifier' ) ) ) {
560 if ( metadata_exists( 'term', $term_id, $key ) ) {
561 update_metadata( 'term', $term_id, $key, $value );
562 } else {
563 add_metadata( 'term', $term_id, $key, $value, true );
564 }
565 }
566 }
567 }
568
569 if ( $errors->has_errors() ) {
570 return $errors;
571 }
572
573 return $all_urls;
574 }
575
576 public function get_avatar_url() {
577 return get_metadata( 'term', $this->get_term_id(), 'avatar_url', true );
578 }
579
580 /**
581 * Determines if starred.
582 *
583 * @return bool True if starred, False otherwise.
584 */
585 public function is_starred() {
586 return get_metadata( 'term', $this->get_term_id(), 'starred', true );
587 }
588
589 /**
590 * Marks a friend as starred or unstarred.
591 *
592 * @param bool $starred Whether to star the friend.
593 *
594 * @return bool The new star status.
595 */
596 public function set_starred( $starred ) {
597 if ( $starred ) {
598 update_metadata( 'term', $this->get_term_id(), 'starred', true );
599 return true;
600 }
601
602 delete_metadata( 'term', $this->get_term_id(), 'starred' );
603 return false;
604 }
605
606 /**
607 * Get the folder (parent term) this subscription belongs to.
608 *
609 * @return \WP_Term|null The folder term, or null if at root.
610 */
611 public function get_folder() {
612 if ( $this->term->parent ) {
613 $parent = get_term( $this->term->parent, self::TAXONOMY );
614 if ( $parent && ! is_wp_error( $parent ) ) {
615 return $parent;
616 }
617 }
618 return null;
619 }
620
621 /**
622 * Move this subscription to a folder.
623 *
624 * @param int $folder_term_id The folder term ID, or 0 for root.
625 * @return bool True on success.
626 */
627 public function move_to_folder( $folder_term_id ) {
628 $result = wp_update_term(
629 $this->get_term_id(),
630 self::TAXONOMY,
631 array( 'parent' => $folder_term_id )
632 );
633 if ( ! is_wp_error( $result ) ) {
634 $this->term->parent = $folder_term_id;
635 return true;
636 }
637 return false;
638 }
639
640 /**
641 * Create a folder in the subscription taxonomy.
642 *
643 * @param string $name The folder name.
644 * @param int $parent_id Optional parent folder ID.
645 * @return \WP_Term|\WP_Error The created folder term or error.
646 */
647 public static function create_folder( $name, $parent_id = 0 ) {
648 $result = wp_insert_term(
649 $name,
650 self::TAXONOMY,
651 array(
652 'parent' => $parent_id,
653 'slug' => sanitize_title( 'folder-' . $name ),
654 )
655 );
656 if ( is_wp_error( $result ) ) {
657 return $result;
658 }
659 $term = get_term( $result['term_id'], self::TAXONOMY );
660 update_term_meta( $term->term_id, 'is_folder', true );
661 return $term;
662 }
663
664 /**
665 * Get all folders.
666 *
667 * @param int $parent_id Optional parent folder ID.
668 * @return \WP_Term[] Array of folder terms.
669 */
670 public static function get_folders( $parent_id = null ) {
671 $args = array(
672 'taxonomy' => self::TAXONOMY,
673 'hide_empty' => false,
674 'meta_key' => 'is_folder', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
675 'meta_value' => '1', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
676 );
677 if ( null !== $parent_id ) {
678 $args['parent'] = $parent_id;
679 }
680 $query = new \WP_Term_Query( $args );
681 return $query->get_terms();
682 }
683
684 /**
685 * Check if a term is a folder.
686 *
687 * @param int $term_id The term ID.
688 * @return bool
689 */
690 public static function is_folder( $term_id ) {
691 return (bool) get_term_meta( $term_id, 'is_folder', true );
692 }
693
694 public function delete() {
695 // Allow unsubscribing to all these feeds.
696 foreach ( $this->get_active_feeds() as $feed ) {
697 do_action( 'friends_user_feed_deactivated', $feed );
698 $feed->delete();
699 }
700
701 // Delete the rest.
702 foreach ( $this->get_feeds() as $feed ) {
703 $feed->delete();
704 }
705
706 foreach ( $this->get_all_post_ids() as $post_id ) {
707 wp_delete_post( $post_id );
708 }
709
710 wp_delete_term( $this->get_term_id(), self::TAXONOMY );
711 return true;
712 }
713
714 public static function convert_from_user( User $user ) {
715 if ( $user instanceof Subscription ) {
716 return $user;
717 }
718
719 $role = isset( $user->roles[0] ) ? $user->roles[0] : 'subscription';
720 $subscription = self::create( $user->user_login, $role, $user->user_url, $user->display_name, $user->get_avatar_url(), $user->description, $user->user_registered );
721
722 if ( is_wp_error( $subscription ) ) {
723 return $subscription;
724 }
725
726 global $wpdb;
727
728 // Get the term_taxonomy_id for the subscription term.
729 $subscription_tt_id = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
730 $wpdb->prepare(
731 "SELECT term_taxonomy_id FROM {$wpdb->term_taxonomy} WHERE term_id = %d AND taxonomy = %s",
732 $subscription->get_term_id(),
733 self::TAXONOMY
734 )
735 );
736
737 // Migrate posts: add subscription term and set post_author to 0, all in bulk.
738 $post_types = apply_filters( 'friends_frontend_post_types', array() );
739 if ( ! empty( $post_types ) ) {
740 $type_in = implode( ',', array_fill( 0, count( $post_types ), '%s' ) );
741
742 // Get affected post IDs for cache invalidation.
743 $affected_post_ids = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
744 $wpdb->prepare(
745 "SELECT ID FROM {$wpdb->posts} WHERE post_author = %d AND post_type IN (" . $type_in . ')', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
746 array_merge( array( $user->ID ), $post_types )
747 )
748 );
749
750 // Add subscription term relationship for all posts by this user.
751 $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
752 $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
753 "INSERT IGNORE INTO {$wpdb->term_relationships} (object_id, term_taxonomy_id)
754 SELECT ID, %d FROM {$wpdb->posts} WHERE post_author = %d AND post_type IN (" . $type_in . ')', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
755 array_merge( array( $subscription_tt_id, $user->ID ), $post_types )
756 )
757 );
758
759 // Update post_author to 0 for all posts by this user.
760 $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
761 $wpdb->prepare(
762 "UPDATE {$wpdb->posts} SET post_author = 0 WHERE post_author = %d AND post_type IN (" . $type_in . ')', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
763 array_merge( array( $user->ID ), $post_types )
764 )
765 );
766
767 // Clean post caches after direct DB update.
768 foreach ( $affected_post_ids as $affected_post_id ) {
769 clean_post_cache( $affected_post_id );
770 }
771
772 // Update the term count.
773 wp_update_term_count_now( array( $subscription_tt_id ), self::TAXONOMY );
774 }
775
776 // Convert feeds: set parent to the subscription term_id and remove the object relationship.
777 $feed_term_ids = wp_get_object_terms( $user->ID, User_Feed::TAXONOMY, array( 'fields' => 'ids' ) );
778 $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
779 $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
780 sprintf(
781 'UPDATE %s tt
782 JOIN %s tr ON tt.term_taxonomy_id = tr.term_taxonomy_id
783 SET tt.parent = %%d
784 WHERE tr.object_id = %%d
785 AND tt.taxonomy = %%s',
786 $wpdb->term_taxonomy,
787 $wpdb->term_relationships
788 ),
789 $subscription->get_term_id(),
790 $user->ID,
791 User_Feed::TAXONOMY
792 )
793 );
794 // Remove old object_id-based relationships so the delete_user hook won't find and delete these feeds.
795 // Use direct DB delete to avoid wp_remove_object_terms side effects (hook cascades, term count resets).
796 $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
797 $wpdb->prepare(
798 "DELETE tr FROM {$wpdb->term_relationships} tr
799 JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
800 WHERE tr.object_id = %d AND tt.taxonomy = %s",
801 $user->ID,
802 User_Feed::TAXONOMY
803 )
804 );
805 if ( ! empty( $feed_term_ids ) && ! is_wp_error( $feed_term_ids ) ) {
806 clean_term_cache( $feed_term_ids, User_Feed::TAXONOMY );
807 }
808
809 foreach ( self::MIGRATE_USER_OPTIONS as $option_name ) {
810 $subscription->update_user_option( $option_name, $user->get_user_option( $option_name ) );
811 }
812
813 $user->delete();
814
815 return $subscription;
816 }
817
818 /**
819 * Create a Subscription (virtual user).
820 *
821 * @param string $user_login The user login.
822 * @param string $role The role: subscription.
823 * @param string $user_url The site URL.
824 * @param string $display_name The user's display name.
825 * @param string $avatar_url The user_icon_url URL.
826 * @param string $description A description for the user.
827 * @param string $user_registered When the user was registered.
828 *
829 * @return Subscription|\WP_Error The created subscription or an error.
830 */
831 public static function create( $user_login, $role, $user_url, $display_name = null, $avatar_url = null, $description = null, $user_registered = null ) {
832 // Sanitize the username to prevent special characters like apostrophes.
833 $user_login = User::sanitize_username( $user_login );
834
835 $term = term_exists( $user_login, self::TAXONOMY );
836
837 if ( ! $term || ! isset( $term['term_id'] ) ) {
838 $term = wp_insert_term( $user_login, self::TAXONOMY );
839 if ( is_wp_error( $term ) ) {
840 return $term;
841 }
842 }
843
844 $term_id = $term['term_id'];
845
846 delete_metadata( 'term', $term_id, 'roles' );
847 add_metadata( 'term', $term_id, 'roles', $role, true );
848 delete_metadata( 'term', $term_id, 'user_url' );
849 add_metadata( 'term', $term_id, 'user_url', $user_url, true );
850
851 delete_metadata( 'term', $term_id, 'display_name' );
852 if ( $display_name ) {
853 add_metadata( 'term', $term_id, 'display_name', $display_name, true );
854 }
855
856 delete_metadata( 'term', $term_id, 'avatar_url' );
857 if ( $avatar_url ) {
858 add_metadata( 'term', $term_id, 'avatar_url', $avatar_url, true );
859 }
860 delete_metadata( 'term', $term_id, 'description' );
861 if ( $description ) {
862 add_metadata( 'term', $term_id, 'description', $description, true );
863 }
864 delete_metadata( 'term', $term_id, 'created' );
865 add_metadata( 'term', $term_id, 'created', $user_registered ? $user_registered : gmdate( 'Y-m-d H:i:s' ), true );
866
867 $term = get_term( $term['term_id'] );
868 if ( is_wp_error( $term ) ) {
869 return $term;
870 }
871
872 return new self( $term );
873 }
874
875 private function map_option( $option_name ) {
876 $option_map = array(
877 'friends_user_icon_url' => 'avatar_url',
878 );
879 if ( isset( $option_map[ $option_name ] ) ) {
880 $option_name = $option_map[ $option_name ];
881 }
882
883 return $option_name;
884 }
885
886 /**
887 * Wrap get_user_option
888 *
889 * @param string $option_name User option name.
890 * @return int|bool User meta ID if the option didn't exist, true on successful update,
891 * false on failure.
892 */
893 public function get_user_option( $option_name ) {
894 $value = get_metadata( 'term', $this->get_term_id(), $option_name, true );
895 if ( false === $value ) {
896 $value = get_metadata( 'term', $this->get_term_id(), $this->map_option( $option_name ), true );
897 }
898
899 return $value;
900 }
901
902 /**
903 * Wrap update_user_option
904 *
905 * @param string $option_name User option name.
906 * @param mixed $new_value User option value.
907 * @param bool $is_global Optional. Whether option name is global or blog specific.
908 * Default false (blog specific).
909 * @return int|bool User meta ID if the option didn't exist, true on successful update,
910 * false on failure.
911 */
912 public function update_user_option( $option_name, $new_value, $is_global = false ) {
913 return update_metadata( 'term', $this->get_term_id(), $this->map_option( $option_name ), $new_value );
914 }
915
916 /**
917 * Wrap delete_user_option
918 *
919 * @param string $option_name User option name.
920 * @param bool $is_global Optional. Whether option name is global or blog specific.
921 * Default false (blog specific).
922 * @return bool True on success, false on failure.
923 */
924 public function delete_user_option( $option_name, $is_global = false ) {
925 return delete_metadata( 'term', $this->get_term_id(), $option_name );
926 }
927 }
928