PluginProbe
Friends / trunk
Friends vtrunk
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 2.9.2 All 87 releases
friends / includes / class-subscription.php

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

927 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 $subscription = self::create( $user->user_login, $user->roles[0], $user->user_url, $user->display_name, $user->get_avatar_url(), $user->description, $user->user_registered );
720
721 if ( is_wp_error( $subscription ) ) {
722 return $subscription;
723 }
724
725 global $wpdb;
726
727 // Get the term_taxonomy_id for the subscription term.
728 $subscription_tt_id = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
729 $wpdb->prepare(
730 "SELECT term_taxonomy_id FROM {$wpdb->term_taxonomy} WHERE term_id = %d AND taxonomy = %s",
731 $subscription->get_term_id(),
732 self::TAXONOMY
733 )
734 );
735
736 // Migrate posts: add subscription term and set post_author to 0, all in bulk.
737 $post_types = apply_filters( 'friends_frontend_post_types', array() );
738 if ( ! empty( $post_types ) ) {
739 $type_in = implode( ',', array_fill( 0, count( $post_types ), '%s' ) );
740
741 // Get affected post IDs for cache invalidation.
742 $affected_post_ids = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
743 $wpdb->prepare(
744 "SELECT ID FROM {$wpdb->posts} WHERE post_author = %d AND post_type IN (" . $type_in . ')', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
745 array_merge( array( $user->ID ), $post_types )
746 )
747 );
748
749 // Add subscription term relationship for all posts by this user.
750 $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
751 $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
752 "INSERT IGNORE INTO {$wpdb->term_relationships} (object_id, term_taxonomy_id)
753 SELECT ID, %d FROM {$wpdb->posts} WHERE post_author = %d AND post_type IN (" . $type_in . ')', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
754 array_merge( array( $subscription_tt_id, $user->ID ), $post_types )
755 )
756 );
757
758 // Update post_author to 0 for all posts by this user.
759 $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
760 $wpdb->prepare(
761 "UPDATE {$wpdb->posts} SET post_author = 0 WHERE post_author = %d AND post_type IN (" . $type_in . ')', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
762 array_merge( array( $user->ID ), $post_types )
763 )
764 );
765
766 // Clean post caches after direct DB update.
767 foreach ( $affected_post_ids as $affected_post_id ) {
768 clean_post_cache( $affected_post_id );
769 }
770
771 // Update the term count.
772 wp_update_term_count_now( array( $subscription_tt_id ), self::TAXONOMY );
773 }
774
775 // Convert feeds: set parent to the subscription term_id and remove the object relationship.
776 $feed_term_ids = wp_get_object_terms( $user->ID, User_Feed::TAXONOMY, array( 'fields' => 'ids' ) );
777 $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
778 $wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
779 sprintf(
780 'UPDATE %s tt
781 JOIN %s tr ON tt.term_taxonomy_id = tr.term_taxonomy_id
782 SET tt.parent = %%d
783 WHERE tr.object_id = %%d
784 AND tt.taxonomy = %%s',
785 $wpdb->term_taxonomy,
786 $wpdb->term_relationships
787 ),
788 $subscription->get_term_id(),
789 $user->ID,
790 User_Feed::TAXONOMY
791 )
792 );
793 // Remove old object_id-based relationships so the delete_user hook won't find and delete these feeds.
794 // Use direct DB delete to avoid wp_remove_object_terms side effects (hook cascades, term count resets).
795 $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
796 $wpdb->prepare(
797 "DELETE tr FROM {$wpdb->term_relationships} tr
798 JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
799 WHERE tr.object_id = %d AND tt.taxonomy = %s",
800 $user->ID,
801 User_Feed::TAXONOMY
802 )
803 );
804 if ( ! empty( $feed_term_ids ) && ! is_wp_error( $feed_term_ids ) ) {
805 clean_term_cache( $feed_term_ids, User_Feed::TAXONOMY );
806 }
807
808 foreach ( self::MIGRATE_USER_OPTIONS as $option_name ) {
809 $subscription->update_user_option( $option_name, $user->get_user_option( $option_name ) );
810 }
811
812 $user->delete();
813
814 return $subscription;
815 }
816
817 /**
818 * Create a Subscription (virtual user).
819 *
820 * @param string $user_login The user login.
821 * @param string $role The role: subscription.
822 * @param string $user_url The site URL.
823 * @param string $display_name The user's display name.
824 * @param string $avatar_url The user_icon_url URL.
825 * @param string $description A description for the user.
826 * @param string $user_registered When the user was registered.
827 *
828 * @return Subscription|\WP_Error The created subscription or an error.
829 */
830 public static function create( $user_login, $role, $user_url, $display_name = null, $avatar_url = null, $description = null, $user_registered = null ) {
831 // Sanitize the username to prevent special characters like apostrophes.
832 $user_login = User::sanitize_username( $user_login );
833
834 $term = term_exists( $user_login, self::TAXONOMY );
835
836 if ( ! $term || ! isset( $term['term_id'] ) ) {
837 $term = wp_insert_term( $user_login, self::TAXONOMY );
838 if ( is_wp_error( $term ) ) {
839 return $term;
840 }
841 }
842
843 $term_id = $term['term_id'];
844
845 delete_metadata( 'term', $term_id, 'roles' );
846 add_metadata( 'term', $term_id, 'roles', $role, true );
847 delete_metadata( 'term', $term_id, 'user_url' );
848 add_metadata( 'term', $term_id, 'user_url', $user_url, true );
849
850 delete_metadata( 'term', $term_id, 'display_name' );
851 if ( $display_name ) {
852 add_metadata( 'term', $term_id, 'display_name', $display_name, true );
853 }
854
855 delete_metadata( 'term', $term_id, 'avatar_url' );
856 if ( $avatar_url ) {
857 add_metadata( 'term', $term_id, 'avatar_url', $avatar_url, true );
858 }
859 delete_metadata( 'term', $term_id, 'description' );
860 if ( $description ) {
861 add_metadata( 'term', $term_id, 'description', $description, true );
862 }
863 delete_metadata( 'term', $term_id, 'created' );
864 add_metadata( 'term', $term_id, 'created', $user_registered ? $user_registered : gmdate( 'Y-m-d H:i:s' ), true );
865
866 $term = get_term( $term['term_id'] );
867 if ( is_wp_error( $term ) ) {
868 return $term;
869 }
870
871 return new self( $term );
872 }
873
874 private function map_option( $option_name ) {
875 $option_map = array(
876 'friends_user_icon_url' => 'avatar_url',
877 );
878 if ( isset( $option_map[ $option_name ] ) ) {
879 $option_name = $option_map[ $option_name ];
880 }
881
882 return $option_name;
883 }
884
885 /**
886 * Wrap get_user_option
887 *
888 * @param string $option_name User option name.
889 * @return int|bool User meta ID if the option didn't exist, true on successful update,
890 * false on failure.
891 */
892 public function get_user_option( $option_name ) {
893 $value = get_metadata( 'term', $this->get_term_id(), $option_name, true );
894 if ( false === $value ) {
895 $value = get_metadata( 'term', $this->get_term_id(), $this->map_option( $option_name ), true );
896 }
897
898 return $value;
899 }
900
901 /**
902 * Wrap update_user_option
903 *
904 * @param string $option_name User option name.
905 * @param mixed $new_value User option value.
906 * @param bool $is_global Optional. Whether option name is global or blog specific.
907 * Default false (blog specific).
908 * @return int|bool User meta ID if the option didn't exist, true on successful update,
909 * false on failure.
910 */
911 public function update_user_option( $option_name, $new_value, $is_global = false ) {
912 return update_metadata( 'term', $this->get_term_id(), $this->map_option( $option_name ), $new_value );
913 }
914
915 /**
916 * Wrap delete_user_option
917 *
918 * @param string $option_name User option name.
919 * @param bool $is_global Optional. Whether option name is global or blog specific.
920 * Default false (blog specific).
921 * @return bool True on success, false on failure.
922 */
923 public function delete_user_option( $option_name, $is_global = false ) {
924 return delete_metadata( 'term', $this->get_term_id(), $option_name );
925 }
926 }
927