WordPress и длинные “ярлыки”

При эксплуатации WordPress на одном из сайтов был обнаружен неприятный дефект – некоторые автоматически сгенерированные ярлыки (“slug”) для категорий имели покоцанные последние символы, вследствие чего перейти по ссылкам на такие категории не представлялось возможным. Однако при ручном их указании все сохранялось корректно.

Данная проблема явно не относится к тем, с которыми можно часто столкнуться: она возникает при наличии, к примеру, многоуровневой иерархии категорий с повторяющимися названиями узлов. Предположим следующую гипотетическую структуру категорий:

  • Энергетические решения для дома
    • Дизельные установки
    • Ветряные генераторы
    • Микроядерные установки
  • Энергетические решения для офиса
    • Дизельные установки
    • Микроядерные установки
    • Термоядерные блоки

Причина проблемы состоит в том, что WordPress не допускает существование одинаковых ярлыков, даже если они находятся в разных ветвях иерархии. Поэтому, если при создании данной структуры не заполнять вручную ярлыки (“slugs”), автоматически получим такую структуру ярлыков:

  • /категории/энергетические-решения-для-дома
    • /категории/энергетические-решения-для-дома/дизельные-установки
    • /категории/энергетические-решения-для-дома/ветряные-генераторы
    • /категории/энергетические-решения-для-дома/микроядерные-установки
  • /категории/энергетические-решения-для-офиса
    • /категории/энергетические-решения-для-офиса/дизельные-установки-энергетические-решения-для-оф%0с
    • /категории/энергетические-решения-для-офиса/микроядерные-установки-энергетические-решения-дл%3bf~
    • /категории/энергетические-решения-для-офиса/термоядерные-блоки

Стандартный алгоритм пытается автозаполнить ярлыки категорий, исходя из их названий. Когда названия повторяются – WordPress пытается приписать к ярлыку название его родительской категории, создав тем самым более уникальное имя.

Почему сгенерированные названия получаются нерабочими? – Дело в том, что в базе данных на длину поля ярлыка выделено 200 символов. В принципе, это довольно большое число, которого должно было хватить с запасом в том числе и на имя родительской категории. Однако, как выяснилось, это верно лишь при использовании латинских символов. В базе данных ярлык хранится уже в URL-encoded форме, что при учете использования UTF-8 приводит к тому, что одна русская буква заменяется на последовательность из 6 символов. В результате, фактически в этом поле умещается название, в котором не более 33 русских букв. И, как выяснилось, никакой проверки этого факта в WordPress не предусмотрено: при записи слишком длинного ярлыка в базу просто пишется та его часть, которая уложилась в 200 символов. А при учете, что на одну русскую букву в кодированной форме приходится 6 символов, получаем, что одна из букв оказывается разорвана пополам. Как итог – битая нерабочая ссылка.

Для исправления этой ситуации можно пропатчить файл wp-includes/taxonomy.php:

--- a/taxonomy.php	2014-02-24 13:14:30.649795666 +0400
+++ b/taxonomy.php	2014-02-25 11:23:51.443231774 +0400
@@ -2436,6 +2436,15 @@
  * @param object $term The term object that the $slug will belong too
  * @return string Will return a true unique slug.
  */
+function truncate_slug_to_200_chars($slug, $limit = 200) {
+	while (strlen($slug) >= $limit)
+	{
+		$slug = rawurldecode($slug);
+		$slug = mb_substr($slug,0,-1);
+		$slug = rawurlencode($slug);
+	}
+	return $slug;
+}
 function wp_unique_term_slug($slug, $term) {
 	global $wpdb;
 
@@ -2451,6 +2460,10 @@
 			if ( is_wp_error($parent_term) || empty($parent_term) )
 				break;
 			$slug .= '-' . $parent_term->slug;
+			// [kreon] slugs are url-encoded and limited to 200 chars
+			// if this limit is exceeded then non-ascii urlencoded data will probably break
+			if ( strlen( $slug ) >= 200 )
+				$slug = truncate_slug_to_200_chars( $slug );
 			if ( ! term_exists( $slug ) )
 				return $slug;
 
@@ -2468,6 +2481,10 @@
 
 	if ( $wpdb->get_var( $query ) ) {
 		$num = 2;
+		// [kreon] same thing here. if result exceeds 200 chars, then things may break
+		// make sure we have free space for 3 additional chars to hold '-num' parameter
+		if ( strlen( $slug ) >= 197 )
+			$slug = truncate_slug_to_200_chars ( $slug, 197 );
 		do {
 			$alt_slug = $slug . "-$num";
 			$num++;

Этот патч не позволяет автозаполнению сгенерировать слишком длинный ярлык, сокращая побуквенно длину декодированного ярлыка до тех пор, пока его кодированное представление не станет менее 200 символов.

Патч не исправит уже имеющиеся поломанные ярлыки – их необходимо пересохранить повторно вручную.

This entry was posted in Сеть и интернет and tagged , . Bookmark the permalink.

3 Responses to WordPress и длинные “ярлыки”

  1. root says:

    Начиная с WordPress 4.0 это исправление более неактуально.

    • ALEXEY MYSHKIN says:

      А вот проблема так и живёт..WP 5.0.3
      При создании атрибута через API получаю ошибку:
      “Слаг “доп-характеристики” слишком длинный (больше 28 символов). Укоротите, пожалуйста.”
      И вот что с этим делать? прикрутить автогенерацию слагов, делая их бессмысленными? Транслитерировать слаги? (вероятно наилучший вариант)

      • Silwer says:

        Проблема актуальна так же при импорте штатными средствами WooCommerce csv файла, если поле характеристик кириллицей больше пары символов (порядка 8 помоему). Решение пока не нашел, только создать атрибут, попустим “Внешний диаметр” со слагом “diameter” и тогла товарі импортируются без проблем. НО такую процедуру надо провести со всеми “длинными” атрибутами, а если их сотни – то это не весело, пока не нашел способа импортировать таблицу атрибутов с короткими слагами, так как слаги создаются вручную на базе имени атрибута. Или таксономию ковырять, или в БД импортировать, но относительноп ростого решения с кабины (админки) не нашел пока.

Leave a Reply to ALEXEY MYSHKIN Cancel reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>