Commit 579044140f for wordpress.org

commit 579044140f37ef3d219fb89235270bdd6c4f3426
Author: peterwilsoncc <peterwilsoncc@git.wordpress.org>
Date:   Wed Sep 23 23:56:46 2026 +0000

    Taxonomy: Limit term slugs to 200 characters in `wp_unique_term_slug()`.

    Prevents the auto-generation of term slugs that exceed database schema's maximum of 200 characters. This prevents database errors inserting the term.

    This is particularly useful for slugs generated for non-latin alphabets as the slug is stored in it's URL encoded form in the database, for example `Категория на продукта` generates a 116 character slug.

    * Introduces `wp_truncate_slug()` as a generically named function for truncating the slug of any object
    * Deprecates `_truncate_post_slug()` in favour of above

    The test suite for `_truncate_post_slug()` is retained (with the addition of expecting a deprecation notice) in order to ensure that future updates do not result in errors.

    Props khokansardar, arkaprabhachowdhury, callumbw95, mikejolley, mindctrl, sajib1223, subrataemfluence, swissspidy.
    Fixes #46010.




    Built from https://develop.svn.wordpress.org/trunk@63909


    git-svn-id: http://core.svn.wordpress.org/trunk@63078 1a063a9b-81f0-0310-95a4-ce76da25c4cd

diff --git a/wp-includes/deprecated.php b/wp-includes/deprecated.php
index 3b78d1610f..d2b987ec18 100644
--- a/wp-includes/deprecated.php
+++ b/wp-includes/deprecated.php
@@ -6532,3 +6532,20 @@ function wp_sanitize_script_attributes( $attributes ) {
 	}
 	return $attributes_string;
 }
+
+/**
+ * Truncates a post slug.
+ *
+ * @since 3.6.0
+ * @deprecated 7.2.0 Use wp_truncate_slug() instead.
+ * @see wp_truncate_slug()
+ *
+ * @param string $slug   The slug to truncate.
+ * @param int    $length Optional. Max length of the slug. Default 200 (characters).
+ * @return string The truncated slug.
+ */
+function _truncate_post_slug( $slug, $length = 200 ) {
+	_deprecated_function( __FUNCTION__, '7.2.0', 'wp_truncate_slug()' );
+
+	return wp_truncate_slug( $slug, $length );
+}
diff --git a/wp-includes/formatting.php b/wp-includes/formatting.php
index 5bf001c430..faa94c6f6b 100644
--- a/wp-includes/formatting.php
+++ b/wp-includes/formatting.php
@@ -2400,6 +2400,34 @@ function sanitize_title_with_dashes( $title, $raw_title = '', $context = 'displa
 	return $title;
 }

+/**
+ * Truncates a slug to a given length.
+ *
+ * Non-ASCII slugs are stored percent-encoded, so the slug is truncated on a
+ * character boundary to avoid cutting a percent-encoded sequence in half.
+ *
+ * @since 7.2.0
+ * @access private
+ *
+ * @see utf8_uri_encode()
+ *
+ * @param string $slug   The slug to truncate.
+ * @param int    $length Optional. Max length of the slug. Default 200 (characters).
+ * @return string The truncated slug.
+ */
+function wp_truncate_slug( $slug, $length = 200 ) {
+	if ( strlen( $slug ) > $length ) {
+		$decoded_slug = urldecode( $slug );
+		if ( $decoded_slug === $slug ) {
+			$slug = substr( $slug, 0, $length );
+		} else {
+			$slug = utf8_uri_encode( $decoded_slug, $length, true );
+		}
+	}
+
+	return rtrim( $slug, '-' );
+}
+
 /**
  * Ensures a string is a valid SQL 'order by' clause.
  *
diff --git a/wp-includes/post.php b/wp-includes/post.php
index d36a44bde2..c268544418 100644
--- a/wp-includes/post.php
+++ b/wp-includes/post.php
@@ -5721,7 +5721,7 @@ function wp_unique_post_slug( $slug, $post_id, $post_status, $post_type, $post_p
 		) {
 			$suffix = 2;
 			do {
-				$alt_post_name   = _truncate_post_slug( $slug, 200 - ( strlen( $suffix ) + 1 ) ) . "-$suffix";
+				$alt_post_name   = wp_truncate_slug( $slug, 200 - ( strlen( $suffix ) + 1 ) ) . "-$suffix";
 				$post_name_check = $wpdb->get_var( $wpdb->prepare( $check_sql, $alt_post_name, $post_id ) );
 				++$suffix;
 			} while ( $post_name_check );
@@ -5758,7 +5758,7 @@ function wp_unique_post_slug( $slug, $post_id, $post_status, $post_type, $post_p
 		) {
 			$suffix = 2;
 			do {
-				$alt_post_name   = _truncate_post_slug( $slug, 200 - ( strlen( $suffix ) + 1 ) ) . "-$suffix";
+				$alt_post_name   = wp_truncate_slug( $slug, 200 - ( strlen( $suffix ) + 1 ) ) . "-$suffix";
 				$post_name_check = $wpdb->get_var( $wpdb->prepare( $check_sql, $alt_post_name, $post_type, $post_id, $post_parent ) );
 				++$suffix;
 			} while ( $post_name_check );
@@ -5814,7 +5814,7 @@ function wp_unique_post_slug( $slug, $post_id, $post_status, $post_type, $post_p
 		) {
 			$suffix = 2;
 			do {
-				$alt_post_name   = _truncate_post_slug( $slug, 200 - ( strlen( $suffix ) + 1 ) ) . "-$suffix";
+				$alt_post_name   = wp_truncate_slug( $slug, 200 - ( strlen( $suffix ) + 1 ) ) . "-$suffix";
 				$post_name_check = $wpdb->get_var( $wpdb->prepare( $check_sql, $alt_post_name, $post_type, $post_id ) );
 				++$suffix;
 			} while ( $post_name_check );
@@ -5837,31 +5837,6 @@ function wp_unique_post_slug( $slug, $post_id, $post_status, $post_type, $post_p
 	return apply_filters( 'wp_unique_post_slug', $slug, $post_id, $post_status, $post_type, $post_parent, $original_slug );
 }

-/**
- * Truncates a post slug.
- *
- * @since 3.6.0
- * @access private
- *
- * @see utf8_uri_encode()
- *
- * @param string $slug   The slug to truncate.
- * @param int    $length Optional. Max length of the slug. Default 200 (characters).
- * @return string The truncated slug.
- */
-function _truncate_post_slug( $slug, $length = 200 ) {
-	if ( strlen( $slug ) > $length ) {
-		$decoded_slug = urldecode( $slug );
-		if ( $decoded_slug === $slug ) {
-			$slug = substr( $slug, 0, $length );
-		} else {
-			$slug = utf8_uri_encode( $decoded_slug, $length, true );
-		}
-	}
-
-	return rtrim( $slug, '-' );
-}
-
 /**
  * Adds tags to a post.
  *
@@ -8726,7 +8701,7 @@ function wp_add_trashed_suffix_to_post_name_for_post( $post ) {
 		return $post->post_name;
 	}
 	add_post_meta( $post->ID, '_wp_desired_post_slug', $post->post_name );
-	$post_name = _truncate_post_slug( $post->post_name, 191 ) . '__trashed';
+	$post_name = wp_truncate_slug( $post->post_name, 191 ) . '__trashed';
 	$wpdb->update( $wpdb->posts, array( 'post_name' => $post_name ), array( 'ID' => $post->ID ) );
 	clean_post_cache( $post->ID );
 	return $post_name;
diff --git a/wp-includes/taxonomy.php b/wp-includes/taxonomy.php
index d13a11e655..ff90651e6b 100644
--- a/wp-includes/taxonomy.php
+++ b/wp-includes/taxonomy.php
@@ -3213,9 +3213,15 @@ function wp_remove_object_terms( $object_id, $terms, $taxonomy ) {
  * If that still doesn't return a unique slug, then it tries to append a number
  * until it finds a number that is truly unique.
  *
+ * Appending a parent slug or a number can push the result past the 200 character
+ * limit of the `slug` column in the terms table, so the slug is truncated to make
+ * room for whatever is appended to it.
+ *
  * The only purpose for `$term` is for appending a parent, if one exists.
  *
  * @since 2.3.0
+ * @since 7.2.0 The returned slug is truncated to 200 characters when a parent slug
+ *              or a numeric suffix is appended to it.
  *
  * @global wpdb $wpdb WordPress database abstraction object.
  *
@@ -3271,7 +3277,7 @@ function wp_unique_term_slug( $slug, $term ) {
 	 */
 	if ( apply_filters( 'wp_unique_term_slug_is_bad_slug', $needs_suffix, $slug, $term ) ) {
 		if ( $parent_suffix ) {
-			$slug .= $parent_suffix;
+			$slug = wp_truncate_slug( $slug . $parent_suffix, 200 );
 		}

 		if ( ! empty( $term->term_id ) ) {
@@ -3283,7 +3289,9 @@ function wp_unique_term_slug( $slug, $term ) {
 		if ( $wpdb->get_var( $query ) ) { // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
 			$num = 2;
 			do {
-				$alt_slug = $slug . "-$num";
+				// Reserve room for the suffix so the result still fits the 200 character column.
+				$numeric_suffix = "-$num";
+				$alt_slug       = wp_truncate_slug( $slug, 200 - strlen( $numeric_suffix ) ) . $numeric_suffix;
 				++$num;
 				$slug_check = $wpdb->get_var( $wpdb->prepare( "SELECT slug FROM $wpdb->terms WHERE slug = %s", $alt_slug ) );
 			} while ( $slug_check );
diff --git a/wp-includes/theme-templates.php b/wp-includes/theme-templates.php
index 301820f78f..f82d970c1c 100644
--- a/wp-includes/theme-templates.php
+++ b/wp-includes/theme-templates.php
@@ -87,7 +87,7 @@ function wp_filter_wp_template_unique_post_slug( $override_slug, $slug, $post_id
 		$suffix = 2;
 		do {
 			$query_args                  = $check_query_args;
-			$alt_post_name               = _truncate_post_slug( $override_slug, 200 - ( strlen( $suffix ) + 1 ) ) . "-$suffix";
+			$alt_post_name               = wp_truncate_slug( $override_slug, 200 - ( strlen( $suffix ) + 1 ) ) . "-$suffix";
 			$query_args['post_name__in'] = array( $alt_post_name );
 			$query                       = new WP_Query( $query_args );
 			++$suffix;
diff --git a/wp-includes/version.php b/wp-includes/version.php
index 993502b95f..bdb8517726 100644
--- a/wp-includes/version.php
+++ b/wp-includes/version.php
@@ -16,7 +16,7 @@
  *
  * @global string $wp_version
  */
-$wp_version = '7.2-alpha-63907';
+$wp_version = '7.2-alpha-63909';

 /**
  * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.