Changeset 14541
- Timestamp:
- 09/26/2025 06:51:30 PM (10 hours ago)
- File:
-
- 1 edited
Legend:
- Unmodified
- Added
- Removed
-
sites/trunk/wordpress.org/public_html/wp-content/plugins/photo-directory/inc/tags.php
r14540 r14541 16 16 private static $processing_merge = false; 17 17 18 /** @var array The old terms. */19 protected static $old_terms = [];20 21 18 /** @var string[] Memoized array of taxonomies that have merge functionality enabled, as determined by `get_mergeable_taxonomies()`. */ 22 19 protected static $mergeable_taxonomies = []; … … 38 35 add_filter( 'pre_insert_term', [ __CLASS__, 'prevent_remapped_term_creation' ], 10, 3 ); 39 36 37 // Handle quick edit merge. 38 add_action( 'wp_ajax_inline-save-tax', [ __CLASS__, 'maybe_handle_quick_edit_merge' ], 1 ); 39 40 40 if ( is_admin() ) { 41 41 self::init_admin(); … … 49 49 foreach ( self::get_mergeable_taxonomies() as $taxonomy ) { 50 50 add_action( "{$taxonomy}_edit_form_fields", [ __CLASS__, 'inject_custom_slug_description' ], 20, 2 ); 51 add_action( "edit_{$taxonomy}", [ __CLASS__, 'capture_old_slug' ], 1, 2 ); 52 add_action( "edited_{$taxonomy}", [ __CLASS__, 'process_merge_rename' ], 10, 2 ); 51 add_action( "edited_{$taxonomy}", [ __CLASS__, 'process_rename' ], 10, 2 ); 53 52 } 54 53 add_action( 'admin_notices', [ __CLASS__, 'admin_notices' ] ); 54 add_action( 'load-edit-tags.php', [ __CLASS__, 'maybe_handle_term_merge_submit' ] ); 55 55 add_filter( 'redirect_term_location', [ __CLASS__, 'maybe_redirect_merged_term' ], 10, 2 ); 56 57 // Hide the new tag form and prevent direct tag creation. 58 add_action( "{$taxonomy}_add_form", [ __CLASS__, 'hide_add_tag_form' ], 1 ); 59 add_filter( 'pre_insert_term', [ __CLASS__, 'prevent_direct_tag_creation' ], 10, 3 ); 56 60 } 57 61 … … 109 113 110 114 /** 111 * Stores the name and slug of term being edited before changes are saved. 115 * Hides the new tag form for mergeable taxonomies. 116 * 117 * @param string $taxonomy The taxonomy slug. 118 */ 119 public static function hide_add_tag_form( $taxonomy ) { 120 if ( 121 self::is_mergeable_taxonomy( $taxonomy ) 122 && isset( $_GET['post_type'] ) 123 && Registrations::get_post_type() === $_GET['post_type'] 124 ) { 125 echo '<style>#col-left { display: none !important; } #col-right { float: none !important; width: 100% !important; }</style>'; 126 } 127 } 128 129 /** 130 * Prevents direct tag creation for mergeable taxonomies. 131 * 132 * @param mixed $term The term data. 133 * @param string $taxonomy The taxonomy. 134 * @param array $args Additional arguments. 135 * @return mixed|WP_Error The term data or WP_Error to prevent creation. 136 */ 137 public static function prevent_direct_tag_creation( $term, $taxonomy, $args ) { 138 if ( self::is_mergeable_taxonomy( $taxonomy ) ) { 139 $post_type = $_POST['post_type'] ?? ''; 140 $photo_post_type = Registrations::get_post_type(); 141 // Check if this is coming from the add tag form (and not programmatic). 142 if ( 143 wp_doing_ajax() 144 && isset( $_POST['action'] ) 145 && 'add-tag' === $_POST['action'] 146 && $photo_post_type === $post_type 147 ) { 148 return new \WP_Error( 'tag_creation_disabled', 149 __( 'Direct tag creation is disabled. Please use the merge functionality instead.', 'wporg-photos' ) 150 ); 151 } 152 } 153 154 return $term; 155 } 156 157 /** 158 * Handles tag rename. 112 159 * 113 160 * @param int $term_id The ID of the term just edited. 114 161 * @param int $tt_id The term taxonomy ID. 115 162 */ 116 public static function capture_old_slug( $term_id, $tt_id ) {163 public static function process_rename( $term_id, $tt_id ) { 117 164 // Get taxonomy from the term. 118 165 $term = get_term( $term_id ); … … 122 169 $taxonomy = $term->taxonomy; 123 170 124 if ( ! self::is_mergeable_taxonomy( $taxonomy ) ) {125 return;126 }127 128 self::$old_terms[ $taxonomy ][ $term_id ] = [129 'name' => $term->name,130 'slug' => $term->slug,131 ];132 }133 134 /**135 * Handles tag merge/rename.136 *137 * @param int $term_id The ID of the term just edited.138 * @param int $tt_id The term taxonomy ID.139 */140 public static function process_merge_rename( $term_id, $tt_id ) {141 // Bail if function has already been invoked (prevents recursion).142 if ( self::$processing_merge ) {143 return;144 }145 146 // Get taxonomy from the term.147 $term = get_term( $term_id );148 if ( ! $term instanceof \WP_Term ) {149 return;150 }151 $taxonomy = $term->taxonomy;152 153 171 // Bail if not a mergeable taxonomy. 154 172 if ( ! self::is_mergeable_taxonomy( $taxonomy ) ) { … … 156 174 } 157 175 176 // Get the old slug and count from the transient. 177 $key = self::pre_rename_key( $taxonomy, $term_id ); 178 $data = get_transient( $key ); 179 180 // Bail if there was no pre-rename note. 181 if ( false === $data || ! is_array( $data ) ) { 182 return; 183 } 184 185 // Remove the transient. 186 delete_transient( $key ); 187 158 188 // Old term data was captured earlier. 159 $old_data = self::$old_terms[ $taxonomy ][ $term_id ] ?? [ 'name' => '', 'slug' => '' ]; 160 $old_name = $old_data['name']; 161 $old_slug = $old_data['slug']; 162 163 // Get new term data. 164 $new_slug = ( $term instanceof \WP_Term ) ? $term->slug : ''; 165 $new_name = ( $term instanceof \WP_Term ) ? $term->name : ''; 166 167 // Mimic core and auto-generate slug from name if no slug was provided. 189 $old_slug = isset( $data['old_slug'] ) ? sanitize_title( $data['old_slug'] ) : ''; 190 $old_count = isset( $data['count'] ) ? (int) $data['count'] : 0; 191 $new_slug = $term->slug; 192 193 // Only act when slug actually changed and the old term had posts. 194 if ( $old_slug && $new_slug && $old_slug !== $new_slug && $old_count > 0 ) { 195 self::add_slug_redirect( $old_slug, $new_slug, $taxonomy ); 196 } 197 } 198 199 /** 200 * Handles merging tags and setting a redirect when the user attempts to rename 201 * a term to a slug that already exists. 202 */ 203 public static function maybe_handle_term_merge_submit() { 204 // Bail if merge is already in progress. 205 if ( self::$processing_merge ) { 206 return; 207 } 208 209 // Bail if not a POST request. 210 if ( 'POST' !== $_SERVER['REQUEST_METHOD'] ) { 211 return; 212 } 213 214 // Bail if not an edited tag request. 215 if ( empty( $_POST['action'] ) || 'editedtag' !== $_POST['action'] ) { 216 return; 217 } 218 219 // Bail if not a mergeable taxonomy. 220 $taxonomy = isset( $_POST['taxonomy'] ) ? sanitize_key( $_POST['taxonomy'] ) : ''; 221 if ( ! self::is_mergeable_taxonomy( $taxonomy ) ) { 222 return; 223 } 224 225 // Bail if no from term ID. 226 $from_term_id = isset( $_POST['tag_ID'] ) ? (int) $_POST['tag_ID'] : 0; 227 if ( $from_term_id <= 0 ) { 228 return; 229 } 230 231 // Bail if user doesn't have the capability. 232 if ( ! current_user_can( 'edit_term', $from_term_id ) ) { 233 return; 234 } 235 check_admin_referer( 'update-tag_' . $from_term_id ); 236 237 $new_slug_raw = isset( $_POST['slug'] ) ? wp_unslash( $_POST['slug'] ) : ''; 238 $new_slug = sanitize_title( $new_slug_raw ); 239 240 // Bail if the slug field was not changed or is empty. 168 241 if ( '' === $new_slug ) { 169 $new_slug = sanitize_title( $new_name ); 170 } 171 172 // Bail if no change in slug. 173 if ( ! $new_slug || $old_slug === $new_slug ) { 174 return; 175 } 176 177 // Check if target tag exists. 242 return; 243 } 244 245 // Bail if from term doesn't exist. 246 $from = get_term( $from_term_id, $taxonomy ); 247 if ( ! $from || is_wp_error( $from ) ) { 248 return; 249 } 250 251 // Bail if user didn't actually change the slug. 252 if ( $new_slug === $from->slug ) { 253 return; 254 } 255 256 // Bail if the term doesn't exist. 178 257 $exists = term_exists( $new_slug, $taxonomy ); 258 if ( ! $exists || is_wp_error( $exists ) ) { 259 // Queue up a transient to store the old slug and count for later use by `process_rename()`. 260 set_transient( 261 self::pre_rename_key( $taxonomy, $from_term_id ), 262 [ 263 'old_slug' => $from->slug, 264 'count' => (int) $from->count, 265 ], 266 5 * MINUTE_IN_SECONDS 267 ); 268 269 // Let core handle the new, unique slug. 270 return; 271 } 272 273 // Bail if the term ID is invalid. 179 274 $to_term_id = is_array( $exists ) ? (int) $exists['term_id'] : (int) $exists; 180 275 if ( $to_term_id <= 0 || $to_term_id === $from_term_id ) { 276 return; 277 } 278 279 // Keep the old before we mutate/delete it. 280 $old_slug = $from->slug; 281 $old_count = (int) $from->count; 282 $to = get_term( $to_term_id, $taxonomy ); 283 $to_name = $to && ! is_wp_error( $to ) ? $to->name : ''; 284 285 // Merge terms. 181 286 self::$processing_merge = true; 182 183 // Merge terms if target term exists. 184 if ( $to_term_id && $to_term_id !== $term_id ) { 185 self::merge_terms( $term_id, $to_term_id, $taxonomy ); 186 $new_term = get_term( $to_term_id, $taxonomy ); 187 188 self::add_slug_redirect( $old_slug, $new_term->slug, $taxonomy ); 189 190 set_transient( "photo_directory_tag_merge_redirect_{$term_id}", $to_term_id, 60 ); 191 192 $message = sprintf( 193 /* translators: 1: Old term, 2: Existing term */ 194 __( 'Merged term <strong>%1$s</strong> into <strong>%2$s</strong>.', 'wporg-photos' ), 195 esc_html( $old_name ), 196 esc_html( $new_term->name ) 287 self::merge_terms( $from_term_id, $to_term_id, $taxonomy ); 288 289 // Add redirect only if the old term actually had posts assigned. 290 if ( $old_count > 0 ) { 291 self::add_slug_redirect( $old_slug, $to->slug, $taxonomy ); 292 } 293 294 self::queue_admin_notice( [ 295 'type' => 'success', 296 'message' => sprintf( 297 /* translators: 1: Old term name, 2: New/existing term name */ 298 __( 'Merged term "%1$s" into "%2$s".', 'wporg-photos' ), 299 $from->name, 300 $to_name 301 ) 302 ] ); 303 304 // Redirect to surviving term’s edit screen, skipping core's updater. 305 $location = add_query_arg( 306 [ 307 'action' => 'edit', 308 'post_type' => Registrations::get_post_type(), 309 'taxonomy' => $taxonomy, 310 'tag_ID' => $to_term_id, 311 ], 312 admin_url( 'term.php' ) 313 ); 314 wp_safe_redirect( $location ); 315 exit; 316 } 317 318 /** 319 * Handles quick edit merge. 320 */ 321 public static function maybe_handle_quick_edit_merge() { 322 // Security and context checks mirror core. 323 check_ajax_referer( 'taxinlineeditnonce', '_inline_edit' ); 324 325 // Bail if merge is already in progress. 326 if ( self::$processing_merge ) { 327 return; 328 } 329 330 // Bail if not a mergeable taxonomy. 331 $taxonomy = isset( $_POST['taxonomy'] ) ? sanitize_key( $_POST['taxonomy'] ) : ''; 332 if ( ! self::is_mergeable_taxonomy( $taxonomy ) ) { 333 return; // Let core proceed. 334 } 335 336 // Bail if no from term ID. 337 $from_id = isset( $_POST['tax_ID'] ) ? (int) $_POST['tax_ID'] : 0; 338 if ( $from_id <= 0 || ! current_user_can( 'edit_term', $from_id ) ) { 339 return; 340 } 341 342 // Bail if from term doesn't exist. 343 $from = get_term( $from_id, $taxonomy ); 344 if ( ! $from || is_wp_error( $from ) ) { 345 return; 346 } 347 348 $new_slug_raw = isset( $_POST['slug'] ) ? wp_unslash( $_POST['slug'] ) : ''; 349 $new_slug = sanitize_title( $new_slug_raw ); 350 351 // Bail if the slug field was not changed or is empty. 352 if ( '' === $new_slug || $new_slug === $from->slug ) { 353 return; 354 } 355 356 $exists = term_exists( $new_slug, $taxonomy ); 357 358 // Bail if no existing term, but stash old slug/count so process_rename() can add a redirect. 359 if ( ! $exists || is_wp_error( $exists ) ) { 360 set_transient( 361 self::pre_rename_key( $taxonomy, $from_id ), 362 [ 363 'old_slug' => $from->slug, 364 'count' => (int) $from->count, 365 ], 366 5 * MINUTE_IN_SECONDS 197 367 ); 198 199 // Else just rename existing term. 200 } else { 201 $updated = wp_update_term( $term_id, $taxonomy, [ 202 'name' => $new_name, 203 'slug' => $new_slug, 204 ] ); 205 206 if ( is_wp_error( $updated ) ) { 207 $message = $updated->get_error_message(); 208 } else { 209 // Only create redirect if the tag has posts. 210 if ( $term->count > 0 ) { 211 self::add_slug_redirect( $old_slug, $new_slug, $taxonomy ); 212 } 213 214 $message = sprintf( 215 /* translators: %s: New term name */ 216 __( 'Changed term slug to <strong>%s</strong>.', 'wporg-photos' ), 217 esc_html( $new_name ) 218 ); 368 // Core's wp_ajax_inline_save_tax() will perform wp_update_term(). 369 return; 370 } 371 372 // Bail if the to term ID is invalid or the same as the from term ID. 373 $to_id = is_array( $exists ) ? (int) $exists['term_id'] : (int) $exists; 374 if ( $to_id <= 0 || $to_id === $from_id ) { 375 return; 376 } 377 378 // Perform merge and produce the same kind of response core would. 379 $old_count = (int) $from->count; 380 self::$processing_merge = true; 381 self::merge_terms( $from_id, $to_id, $taxonomy ); 382 383 if ( $old_count > 0 ) { 384 $to_slug = get_term_field( 'slug', $to_id, $taxonomy ); 385 if ( ! is_wp_error( $to_slug ) && $to_slug ) { 386 self::add_slug_redirect( $from->slug, $to_slug, $taxonomy ); 219 387 } 220 388 } 221 389 222 // Queue up an admin notice. 223 set_transient( 'photo_directory_tag_merge_admin_notice', $message, 30 ); 224 225 self::$processing_merge = false; 226 227 return; 390 // Render the updated row HTML for the surviving term, just like core does. 391 $tag = get_term( $to_id, $taxonomy ); 392 $wp_list_table = _get_list_table( 'WP_Terms_List_Table', [ 'screen' => 'edit-' . $taxonomy ] ); 393 394 // Compute level for hierarchical taxonomies (tags will just be level 0). 395 $level = 0; 396 $parent = $tag && ! is_wp_error( $tag ) ? (int) $tag->parent : 0; 397 while ( $parent > 0 ) { 398 $parent_tag = get_term( $parent, $taxonomy ); 399 $parent = $parent_tag ? (int) $parent_tag->parent : 0; 400 ++$level; 401 } 402 403 $wp_list_table->single_row( $tag, $level ); 404 // Stop core's default AJAX handler from running. 405 wp_die(); 406 } 407 408 /** 409 * Generates a transient key for pre-rename data. 410 * 411 * @param string $taxonomy The taxonomy slug. 412 * @param int $term_id The term ID. 413 * @return string The transient key. 414 */ 415 protected static function pre_rename_key( $taxonomy, $term_id ) { 416 return "photos_tags_pre_rename_{$taxonomy}_{$term_id}"; 228 417 } 229 418 … … 248 437 249 438 foreach ( $object_ids as $object_id ) { 250 wp_ remove_object_terms( $object_id, $from_term_id, $taxonomy );251 wp_ add_object_terms( $object_id, $to_term_id, $taxonomy );439 wp_add_object_terms( $object_id, [ $to_term_id ], $taxonomy ); 440 wp_remove_object_terms( $object_id, [ $from_term_id ], $taxonomy ); 252 441 } 253 442 … … 257 446 258 447 /** 448 * Queues an admin notice. 449 * 450 * @param string $message The message to queue. 451 */ 452 public static function queue_admin_notice( $message ) { 453 set_transient( 'photo_directory_tag_merge_admin_notice', $message, 30 ); 454 } 455 456 /** 259 457 * Outputs admin notice if one is queued. 260 458 */ 261 459 public static function admin_notices() { 262 if ( $msg = get_transient( 'photo_directory_tag_merge_admin_notice' ) ) { 263 printf( 264 '<div class="notice notice-success is-dismissible"><p>%s</p></div>', 265 wp_kses_post( $msg ) 266 ); 267 delete_transient( 'photo_directory_tag_merge_admin_notice' ); 268 } 460 $data = get_transient( 'photo_directory_tag_merge_admin_notice' ); 461 if ( ! $data ) { 462 return; 463 } 464 delete_transient( 'photo_directory_tag_merge_admin_notice' ); 465 466 $type = in_array( $data['type'], [ 'success', 'warning', 'error', 'info' ], true ) ? $data['type'] : 'success'; 467 $message = $data['message']; 468 469 printf( 470 '<div class="notice notice-%1$s is-dismissible"><p>%2$s</p></div>', 471 esc_attr( $type ), 472 wp_kses_post( $message ) 473 ); 269 474 } 270 475 … … 582 787 } 583 788 789 /** 790 * API function to rename or merge tags. 791 * 792 * Use this instead of `wp_update_term()` to "rename to existing slug = merge". 793 * 794 * @return array|WP_Error Result of `wp_update_term()` on simple rename, or ['merged' => true, 'to_term_id' => X] on merge. 795 */ 796 public static function rename_or_merge( int $term_id, string $taxonomy, string $new_slug ) { 797 // Bail if not a mergeable taxonomy. 798 if ( ! self::is_mergeable_taxonomy( $taxonomy ) ) { 799 return new WP_Error( 'invalid_taxonomy', __( 'Taxonomy is not merge-enabled.', 'your-textdomain' ) ); 800 } 801 802 $new_slug = sanitize_title( $new_slug ); 803 $from = get_term( $term_id, $taxonomy ); 804 805 // Bail if the term doesn't exist. 806 if ( ! $from || is_wp_error( $from ) ) { 807 return new WP_Error( 'invalid_term', __( 'Term not found.', 'wporg-photos' ) ); 808 } 809 810 // Bail if the new slug is the same as the old slug. 811 if ( '' === $new_slug || $new_slug === $from->slug ) { 812 // Nothing to do. 813 return [ 'term_id' => $term_id ]; 814 } 815 816 // Handle a merge. 817 $exists = term_exists( $new_slug, $taxonomy ); 818 if ( $exists && ! is_wp_error( $exists ) ) { 819 $to_term_id = is_array( $exists ) ? (int) $exists['term_id'] : (int) $exists; 820 if ( $to_term_id && $to_term_id !== $term_id ) { 821 if ( self::$processing_merge ) { 822 return new WP_Error( 'reentrancy', __( 'Merge already in progress.', 'wporg-photos' ) ); 823 } 824 self::$processing_merge = true; 825 826 $old_count = (int) $from->count; 827 $old_slug = $from->slug; 828 self::merge_terms( $term_id, $to_term_id, $taxonomy ); 829 830 if ( $old_count > 0 ) { 831 $to = get_term( $to_term_id, $taxonomy ); 832 if ( $to && ! is_wp_error( $to ) ) { 833 self::add_slug_redirect( $old_slug, $to->slug, $taxonomy ); 834 } 835 } 836 837 self::$processing_merge = false; 838 return [ 'merged' => true, 'to_term_id' => $to_term_id ]; 839 } 840 } 841 842 // Handle a simple rename. 843 $result = wp_update_term( $term_id, $taxonomy, [ 'slug' => $new_slug ] ); 844 if ( is_wp_error( $result ) ) { 845 return $result; 846 } 847 848 // Optional: add redirect if old term had posts. 849 if ( (int) $from->count > 0 ) { 850 self::add_slug_redirect( $from->slug, $new_slug, $taxonomy ); 851 } 852 853 return $result; 854 } 855 584 856 } 585 857
Note: See TracChangeset
for help on using the changeset viewer.