Making WordPress.org


Ignore:
Timestamp:
09/26/2025 06:51:30 PM (11 months ago)
Author:
coffee2code
Message:

Photo Directory, Tags: Fix and improve tag merging.

  • Split merging and rename handling
  • Add helper function for explicit rename/merge
  • Add support for rename/merge via Quick Edit
  • Improve admin notice handling
  • Hide new tag form since tags shouldn't be directly created
  • Prevent direct new tag creation from tags listing
File:
1 edited

Legend:

Unmodified
Added
Removed
  • sites/trunk/wordpress.org/public_html/wp-content/plugins/photo-directory/inc/tags.php

    r14540 r14541  
    1616        private static $processing_merge = false;
    1717
    18         /** @var array The old terms. */
    19         protected static $old_terms = [];
    20 
    2118        /** @var string[] Memoized array of taxonomies that have merge functionality enabled, as determined by `get_mergeable_taxonomies()`. */
    2219        protected static $mergeable_taxonomies = [];
     
    3835                add_filter( 'pre_insert_term', [ __CLASS__, 'prevent_remapped_term_creation' ], 10, 3 );
    3936
     37                // Handle quick edit merge.
     38                add_action( 'wp_ajax_inline-save-tax',      [ __CLASS__, 'maybe_handle_quick_edit_merge' ], 1 );
     39
    4040                if ( is_admin() ) {
    4141                        self::init_admin();
     
    4949                foreach ( self::get_mergeable_taxonomies() as $taxonomy ) {
    5050                        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 );
    5352                }
    5453                add_action( 'admin_notices',              [ __CLASS__, 'admin_notices' ] );
     54                add_action( 'load-edit-tags.php',         [ __CLASS__, 'maybe_handle_term_merge_submit' ] );
    5555                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 );
    5660        }
    5761
     
    109113
    110114        /**
    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.
    112159         *
    113160         * @param int $term_id The ID of the term just edited.
    114161         * @param int $tt_id   The term taxonomy ID.
    115162         */
    116         public static function capture_old_slug( $term_id, $tt_id ) {
     163        public static function process_rename( $term_id, $tt_id ) {
    117164                // Get taxonomy from the term.
    118165                $term = get_term( $term_id );
     
    122169                $taxonomy = $term->taxonomy;
    123170
    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 
    153171                // Bail if not a mergeable taxonomy.
    154172                if ( ! self::is_mergeable_taxonomy( $taxonomy ) ) {
     
    156174                }
    157175
     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
    158188                // 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.
    168241                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.
    178257                $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.
    179274                $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.
    181286                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
    197367                        );
    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 );
    219387                        }
    220388                }
    221389
    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}";
    228417        }
    229418
     
    248437
    249438                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 );
    252441                }
    253442
     
    257446
    258447        /**
     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        /**
    259457         * Outputs admin notice if one is queued.
    260458         */
    261459        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                );
    269474        }
    270475
     
    582787        }
    583788
     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
    584856}
    585857
Note: See TracChangeset for help on using the changeset viewer.