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
← All changes | includes/class-subscription.php +440 -115 2.8.24.3.2 View file →
@@ -34,11 +34,11 @@
34 34 );
35 35
36 36 public function __construct( \WP_Term $term ) {
37 37 $this->term = $term;
38 - $this->ID = 'friends-virtual-user-' . $term->term_id;
38 + $this->ID = 1e10 + $term->term_id;
39 39
40 - $this->caps = array_fill_keys( get_metadata( 'term', $term->term_id, 'roles' ), true );
40 + $this->caps = array_fill_keys( get_metadata( 'term', $term->term_id, 'roles', false ), true );
41 41 $this->caps['subscription'] = true;
42 42 $this->get_role_caps();
43 43
44 44 $this->roles = array_values( $this->roles );
@@ -45,12 +45,12 @@
45 45
46 46 $this->data = (object) array(
47 47 'ID' => $this->ID,
48 48 'user_login' => $term->name,
49 - 'display_name' => get_metadata( 'term', $term->term_id, 'display_name', true ),
50 - 'user_url' => get_metadata( 'term', $term->term_id, 'user_url', true ),
51 - 'description' => get_metadata( 'term', $term->term_id, 'description', true ),
52 - 'user_registered' => get_metadata( 'term', $term->term_id, 'created', true ),
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 53 );
54 54 }
55 55
56 56 public function get_term_id() {
@@ -73,8 +73,40 @@
73 73 return $this->get_object_id();
74 74 }
75 75
76 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 + /**
77 109 * Registers the taxonomy
78 110 */
79 111 public static function register_taxonomy() {
80 112 $args = array(
@@ -82,9 +114,9 @@
82 114 'name' => _x( 'Virtual User', 'taxonomy general name', 'friends' ),
83 115 'singular_name' => _x( 'Virtual User', 'taxonomy singular name', 'friends' ),
84 116 'menu_name' => __( 'Virtual User', 'friends' ),
85 117 ),
86 - 'hierarchical' => false,
118 + 'hierarchical' => true,
87 119 'show_ui' => true,
88 120 'show_admin_column' => true,
89 121 'query_var' => true,
90 122 'rewrite' => false,
@@ -93,15 +125,8 @@
93 125 register_taxonomy( self::TAXONOMY, 'None', $args );
94 126 }
95 127
96 128 public static function get_by_username( $username ) {
97 - if ( 0 === strpos( $username, 'friends-virtual-user-' ) ) {
98 - $term_id = substr( $username, strlen( 'friends-virtual-user-' ) );
99 - $term = get_term( intval( $term_id ), self::TAXONOMY );
100 - if ( $term ) {
101 - return new self( $term );
102 - }
103 - }
104 129 $term_query = new \WP_Term_Query(
105 130 array(
106 131 'taxonomy' => self::TAXONOMY,
107 132 'name' => $username,
@@ -116,8 +141,11 @@
116 141 return new \WP_Error( 'user-not-found' );
117 142 }
118 143
119 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 + }
120 148 $post_id = wp_insert_post( $postarr, $wp_error, $fire_after_hooks );
121 149 if ( ! is_wp_error( $post_id ) ) {
122 150 wp_set_object_terms( $post_id, $this->get_term_id(), self::TAXONOMY );
123 151 }
@@ -146,9 +174,9 @@
146 174 if ( isset( $args['author'] ) ) {
147 175 unset( $args['author'] );
148 176 }
149 177 if ( ! isset( $args['tax_query'] ) ) {
150 - $args['tax_query'] = array();
178 + $args['tax_query'] = array(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
151 179 } else {
152 180 $args['tax_query']['relation'] = 'AND';
153 181 }
154 182 $args['tax_query'][] =
@@ -172,9 +200,9 @@
172 200 $term_id = $tax_query['terms'];
173 201 if ( is_array( $term_id ) ) {
174 202 $term_id = reset( $term_id );
175 203 }
176 - $authordata = new Subscription( \get_term( $term_id ) );
204 + $authordata = new Subscription( \get_term( $term_id ) ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
177 205 return;
178 206 }
179 207 }
180 208 }
@@ -186,13 +214,19 @@
186 214 *
187 215 * @return object The post stats.
188 216 */
189 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 + }
190 223 global $wpdb;
191 224 $post_types = apply_filters( 'friends_frontend_post_types', array() );
192 - $post_stats = $wpdb->get_row(
225 + $post_stats = $wpdb->get_row( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
193 226 $wpdb->prepare(
194 - 'SELECT SUM(
227 + sprintf(
228 + 'SELECT SUM(
195 229 LENGTH( ID ) +
196 230 LENGTH( post_author ) +
197 231 LENGTH( post_date ) +
198 232 LENGTH( post_date_gmt ) +
@@ -216,9 +250,18 @@
216 250 LENGTH( post_mime_type ) +
217 251 LENGTH( comment_count )
218 252 ) AS total_size,
219 253 COUNT(*) as post_count
220 - FROM ' . $wpdb->posts . ' p, ' . $wpdb->term_taxonomy . ' t, ' . $wpdb->term_relationships . ' r WHERE r.object_id = p.ID AND r.term_taxonomy_id = t.term_taxonomy_id AND t.term_id = %d AND p.post_type IN ( ' . implode( ', ', array_fill( 0, count( $post_types ), '%s' ) ) . ' )',
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 + ),
221 264 array_merge( array( $this->get_term_id() ), $post_types )
222 265 ),
223 266 ARRAY_A
224 267 );
@@ -224,23 +267,68 @@
224 267 );
225 268
226 269 $post_stats['earliest_post_date'] = mysql2date(
227 270 'U',
228 - $wpdb->get_var(
271 + $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
229 272 $wpdb->prepare(
230 - "SELECT MIN(post_date) FROM $wpdb->posts p, $wpdb->term_taxonomy t, $wpdb->term_relationships r WHERE r.object_id = p.ID AND r.term_taxonomy_id = t.term_taxonomy_id AND t.term_id = %d AND p.post_status = 'publish' AND p.post_type IN ( " . implode( ', ', array_fill( 0, count( $post_types ), '%s' ) ) . ' )',
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 + ),
231 286 array_merge( array( $this->get_term_id() ), $post_types )
232 287 )
233 288 )
234 289 );
290 +
291 + wp_cache_set( $cache_key, $post_stats, 'friends', HOUR_IN_SECONDS );
235 292 return $post_stats;
236 293 }
237 294
238 295 public function get_all_post_ids() {
239 296 global $wpdb;
240 - $post_types_to_delete = implode( "', '", apply_filters( 'friends_frontend_post_types', array() ) );
297 + $post_types = apply_filters( 'friends_frontend_post_types', array() );
241 298
242 - $post_ids = $wpdb->get_col( $wpdb->prepare( "SELECT p.ID FROM $wpdb->posts p, $wpdb->term_relationships r WHERE r.object_id = p.ID AND r.term_taxonomy_id = %d AND p.post_type IN ('$post_types_to_delete')", $this->get_term_id() ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
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 +
243 331 return $post_ids;
244 332 }
245 333
246 334 /**
@@ -248,56 +336,61 @@
248 336 *
249 337 * @return array The post counts.
250 338 */
251 339 public function get_post_count_by_post_format() {
252 - $cache_key = 'friends_post_count_by_post_format_author_' . $this->ID;
340 + $cache_key = 'get_post_count_by_post_format_' . $this->ID;
253 341
342 + $counts = wp_cache_get( $cache_key, 'friends' );
343 + if ( false !== $counts ) {
344 + return $counts;
345 + }
254 346 $counts = get_transient( $cache_key );
255 - if ( false === $counts ) {
256 - $counts = array();
257 - $post_types = apply_filters( 'friends_frontend_post_types', array() );
258 - $post_formats_term_ids = array();
259 - foreach ( get_post_format_slugs() as $post_format ) {
260 - $term = get_term_by( 'slug', 'post-format-' . $post_format, 'post_format' );
261 - if ( $term ) {
262 - $post_formats_term_ids[ $term->term_id ] = $post_format;
263 - }
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;
264 357 }
358 + }
265 359
266 - global $wpdb;
360 + global $wpdb;
267 361
268 - $counts = array();
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
269 371
270 - $counts['standard'] = $wpdb->get_var(
271 - $wpdb->prepare(
272 - sprintf(
273 - "SELECT COUNT(DISTINCT posts.ID)
274 - FROM %s AS posts
275 - JOIN %s AS relationships_post_format
276 - JOIN %s AS taxonomy_author
277 - JOIN %s AS relationships_author
278 -
279 - WHERE posts.post_status IN ( 'publish', 'private' )
280 - AND posts.post_type IN ( %s )
281 - AND relationships_post_format.object_id = posts.ID
282 - AND relationships_author.object_id = posts.ID
283 - AND taxonomy_author.term_taxonomy_id = relationships_author.term_taxonomy_id
284 - AND taxonomy_author.term_id = %s",
285 - $wpdb->posts,
286 - $wpdb->term_relationships,
287 - $wpdb->term_taxonomy,
288 - $wpdb->term_relationships,
289 - implode( ',', array_fill( 0, count( $post_types ), '%s' ) ),
290 - '%d'
291 - ),
292 - array_merge(
293 - $post_types,
294 - array( $this->get_term_id() )
295 - )
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() )
296 387 )
297 - );
388 + )
389 + );
298 390
299 - $post_format_counts = $wpdb->get_results(
391 + if ( ! empty( $post_formats_term_ids ) ) {
392 + $post_format_counts = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
300 393 $wpdb->prepare(
301 394 sprintf(
302 395 "SELECT relationships_post_format.term_taxonomy_id AS post_format_id, COUNT(relationships_post_format.term_taxonomy_id) AS count
303 396 FROM %s AS posts
@@ -310,9 +403,9 @@
310 403 AND relationships_post_format.object_id = posts.ID
311 404 AND relationships_post_format.term_taxonomy_id IN ( %s )
312 405 AND relationships_author.object_id = posts.ID
313 406 AND taxonomy_author.term_taxonomy_id = relationships_author.term_taxonomy_id
314 - AND taxonomy_author.term_id = %s
407 + AND taxonomy_author.term_id = %%d
315 408 GROUP BY relationships_post_format.term_taxonomy_id",
316 409 $wpdb->posts,
317 410 $wpdb->term_relationships,
318 411 $wpdb->term_taxonomy,
@@ -317,10 +410,9 @@
317 410 $wpdb->term_relationships,
318 411 $wpdb->term_taxonomy,
319 412 $wpdb->term_relationships,
320 413 implode( ',', array_fill( 0, count( $post_types ), '%s' ) ),
321 - implode( ',', array_fill( 0, count( $post_formats_term_ids ), '%d' ) ),
322 - '%d'
414 + implode( ',', array_fill( 0, count( $post_formats_term_ids ), '%d' ) )
323 415 ),
324 416 array_merge(
325 417 $post_types,
326 418 array_keys( $post_formats_term_ids ),
@@ -332,13 +424,14 @@
332 424 foreach ( $post_format_counts as $row ) {
333 425 $counts[ $post_formats_term_ids[ $row->post_format_id ] ] = $row->count;
334 426 $counts['standard'] -= $row->count;
335 427 }
428 + }
336 429
337 - $counts = array_filter( $counts );
430 + $counts = array_filter( $counts );
338 431
339 - set_transient( $cache_key, $counts, HOUR_IN_SECONDS );
340 - }
432 + set_transient( $cache_key, $counts, HOUR_IN_SECONDS );
433 + wp_cache_set( $cache_key, $counts, 'friends', HOUR_IN_SECONDS );
341 434
342 435 return $counts;
343 436 }
344 437
@@ -350,15 +443,33 @@
350 443 public function get_post_in_trash_count() {
351 444 global $wpdb;
352 445 $post_types = apply_filters( 'friends_frontend_post_types', array() );
353 446
354 - $count = $wpdb->get_var(
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
355 453 $wpdb->prepare(
356 - "SELECT COUNT(*) FROM $wpdb->posts p, $wpdb->term_taxonomy t, $wpdb->term_relationships r WHERE r.object_id = p.ID AND r.term_taxonomy_id = t.term_taxonomy_id AND t.term_id = %d AND post_type IN ( " . implode( ', ', array_fill( 0, count( $post_types ), '%s' ) ) . ' ) AND post_status = "trash"',
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 + ),
357 467 array_merge( array( $this->get_term_id() ), $post_types )
358 468 )
359 469 );
360 470
471 + wp_cache_set( $cache_key, intval( $count ), 'friends', HOUR_IN_SECONDS - 60 );
361 472 return intval( $count );
362 473 }
363 474
364 475 /**
@@ -372,8 +483,97 @@
372 483 public function get_role_name( $group_subscriptions = false, $count = 1 ) {
373 484 return _nx( 'Subscription', 'Subscriptions', $count, 'User role', 'friends' );
374 485 }
375 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( '&', '&', $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 +
376 576 public function get_avatar_url() {
377 577 return get_metadata( 'term', $this->get_term_id(), 'avatar_url', true );
378 578 }
379 579
@@ -402,8 +602,96 @@
402 602 delete_metadata( 'term', $this->get_term_id(), 'starred' );
403 603 return false;
404 604 }
405 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 +
406 694 public function delete() {
407 695 // Allow unsubscribing to all these feeds.
408 696 foreach ( $this->get_active_feeds() as $feed ) {
409 697 do_action( 'friends_user_feed_deactivated', $feed );
@@ -422,75 +710,110 @@
422 710 wp_delete_term( $this->get_term_id(), self::TAXONOMY );
423 711 return true;
424 712 }
425 713
426 -
427 714 public static function convert_from_user( User $user ) {
428 715 if ( $user instanceof Subscription ) {
429 716 return $user;
430 717 }
431 718
432 - $subscription = self::create( $user->user_login, $user->roles[0], $user->user_url, $user->display_name, $user->get_avatar_url(), $user->description, $user->user_registered );
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 );
433 721
434 722 if ( is_wp_error( $subscription ) ) {
435 723 return $subscription;
436 724 }
437 725
438 - $query = new \WP_Query();
439 - $query->set( 'post_type', apply_filters( 'friends_frontend_post_types', array() ) );
440 - $query->set( 'post_status', array( 'publish', 'private', 'draft', 'trash' ) );
441 - $query->set( 'posts_per_page', -1 );
442 - $query = $user->modify_query_by_author( $query );
726 + global $wpdb;
443 727
444 - foreach ( $query->get_posts() as $post ) {
445 - $post->post_author = 0;
446 - wp_update_post( $post );
447 - wp_set_object_terms( $post->ID, $subscription->get_term_id(), self::TAXONOMY );
448 - }
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 + );
449 736
450 - global $wpdb;
451 - // Convert feeds.
452 - $wpdb->query( $wpdb->prepare( "UPDATE $wpdb->term_relationships JOIN $wpdb->term_taxonomy ON $wpdb->term_relationships.term_taxonomy_id = $wpdb->term_taxonomy.term_taxonomy_id SET object_id = %d WHERE object_id = %d AND $wpdb->term_taxonomy.taxonomy = %s", $subscription->get_term_id(), $user->ID, User_Feed::TAXONOMY ) );
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' ) );
453 741
454 - foreach ( self::MIGRATE_USER_OPTIONS as $option_name ) {
455 - $subscription->update_user_option( $option_name, $user->get_user_option( $option_name ) );
456 - }
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 + );
457 749
458 - $user->delete();
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 + );
459 758
460 - return $user;
461 - }
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 + );
462 766
463 - public static function convert_to_user( Subscription $subscription ) {
464 - $user = User::create( $subscription->user_login, $subscription->roles[0], $subscription->user_url, $subscription->display_name, $subscription->get_avatar_url(), $subscription->description, $subscription->user_registered, true );
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 + }
465 771
466 - if ( is_wp_error( $user ) ) {
467 - return $user;
772 + // Update the term count.
773 + wp_update_term_count_now( array( $subscription_tt_id ), self::TAXONOMY );
468 774 }
469 775
470 - $query = new \WP_Query();
471 - $query->set( 'post_type', apply_filters( 'friends_frontend_post_types', array() ) );
472 - $query->set( 'post_status', array( 'publish', 'private', 'draft', 'trashed' ) );
473 - $query->set( 'posts_per_page', -1 );
474 - $query = $subscription->modify_query_by_author( $query );
475 -
476 - foreach ( $query->get_posts() as $post ) {
477 - $post->post_author = $user->ID;
478 - wp_update_post( $post );
479 - wp_remove_object_terms( $post->ID, $subscription->get_term_id(), self::TAXONOMY );
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 );
480 807 }
481 808
482 - global $wpdb;
483 - // Convert feeds.
484 - $wpdb->query( $wpdb->prepare( "UPDATE $wpdb->term_relationships JOIN $wpdb->term_taxonomy ON $wpdb->term_relationships.term_taxonomy_id = $wpdb->term_taxonomy.term_taxonomy_id SET object_id = %d WHERE object_id = %d AND $wpdb->term_taxonomy.taxonomy = %s", $user->ID, $subscription->get_term_id(), User_Feed::TAXONOMY ) );
485 -
486 809 foreach ( self::MIGRATE_USER_OPTIONS as $option_name ) {
487 - $user->update_user_option( $option_name, $subscription->get_user_option( $option_name ) );
810 + $subscription->update_user_option( $option_name, $user->get_user_option( $option_name ) );
488 811 }
489 812
490 - $subscription->delete();
813 + $user->delete();
491 814
492 - return $user;
815 + return $subscription;
493 816 }
494 817
495 818 /**
496 819 * Create a Subscription (virtual user).
@@ -501,13 +824,15 @@
501 824 * @param string $display_name The user's display name.
502 825 * @param string $avatar_url The user_icon_url URL.
503 826 * @param string $description A description for the user.
504 827 * @param string $user_registered When the user was registered.
505 - * @param bool $subscription_override Whether to override the automatic creation of a subscription.
506 828 *
507 829 * @return Subscription|\WP_Error The created subscription or an error.
508 830 */
509 - public static function create( $user_login, $role, $user_url, $display_name = null, $avatar_url = null, $description = null, $user_registered = null, $subscription_override = false ) {
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 +
510 835 $term = term_exists( $user_login, self::TAXONOMY );
511 836
512 837 if ( ! $term || ! isset( $term['term_id'] ) ) {
513 838 $term = wp_insert_term( $user_login, self::TAXONOMY );