Хитрый глюк WordPress: Теряем хвосты постов до «%»!

Проекту исполнилось 15 лет! Поддержать проект материально, проспонсировать проекты Автора или сделать ему подарок можно на этой странице: "Донаты и Спонсорство, Список Желаний".

Число просмотров: 6 271 

У меня тут новые приключения, блин! Были перед самым отдыхом! Дописывал я пост про ГидроЛоки и надо было мне поставить туда ссылку на пост про Рогалики, в котором вроде как упоминалась защита от протечек «Нептун», которую мы там в щит ставили. И вот ищу я, значится, этот пост и листаю его в поисках фотки… и вдруг нахожу такую же неожиданность, как и в посте про Генераторы и АВР, который вдруг был-был, а потом неожиданно закончился.

Вот какую:

Пост WordPress, обрезанный в конце из-за неверного SQL-запроса

Пост WordPress, обрезанный в конце из-за неверного SQL-запроса

Пост вдруг заканчивается на символе «%» и обрезается нахрен. Конечно же его остаток безвозвратно теряется, если у вас нет бэкапа. Вот у меня не было! С постом про генераторы пришлось просить наших камрадов найти текст, если у кого в RSS остался, а с постом про Рогалики — рыть публичный Web Archive, где я его таки нашёл. Короче, как обычно: сисадмины делятся на тех, кто ещё не начал делать бэкапы и на тех, кто УЖЕ начал ;) Только вот все мои бэкапы были уже с повреждёнными постами, мать его!

Заинтересовала меня эта тема, и я решил разобраться в двух вещах: построить догадки о том, почему посты так обрезаются (хорошо бы ещё найти, после каких действий) и искать такие посты и вовремя их восстанавливать из бэкапов.

Сначала выдумываем диагностику. Как я заметил — если пост срезается, то у него ещё и комментарии закрываются. Раз в конце поста всегда есть «%» и комменты закрыты — то мы можем составить парочку запросов к базе данных с такими вот условиями (тут «XXX» — это название таблицы posts в вашей базе; обычно все таблицы WordPress называют с каким-нить префиксом, чтобы враги не догадались и не атаковали сайты):

SELECT * FROM `XXX` WHERE `post_type`='post' AND RIGHT(`post_content`, 1)='%';

Чего мы тут делаем? Отбираем только посты (потому что в этой же таблице валяются и ревизии, и вложения) и смотрим, равен ли последний символ поста процентику. Если равен — то это наш клиент, вот:

Запрос для поиска обрезанных в конце постов WordPress

Запрос для поиска обрезанных в конце постов WordPress

Как я уже говорил, у меня если пост обрезается — то у него ещё и отключаются комментарии. Отключенные комментарии у поста можно проверить таким вот запросом:

SELECT * FROM `XXX` WHERE `post_type`='post' AND `comment_status`='closed';

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

Если косяков с постами не будет — то запрос будет пустой:

Запрос для поиска обрезанных в конце постов WordPress (посты не найдены)

Запрос для поиска обрезанных в конце постов WordPress (посты не найдены)

Как бы теперь разобраться с тем, какого чёрта такая обрезка происходит?

Давайте посмотрим на текст поста, который идёт дальше, после обрезанного места:

Обрезанный пост восстановлен из архива и продолжается дальше

Обрезанный пост восстановлен из архива и продолжается дальше

А там, оказывается, была кавычка и запятая! Хе хе! WordPress отвечает за автоматическое форматирование текста, поэтому конструкции вида «пробел — дефис — пробел» он превращает в длинное тире, а обычные кавычки сам меняет на открывающие и закрывающие. Но на самом деле эти красивые кавычки — это простой символ кавычек:

"

И поэтому моя конструкция текста в посте превратится вот в какую:

",

А этот кусочек ОЧЕНЬ похож на закрывание какого-нибудь SQL-запроса, например запроса на добавление чего-то в таблицу:

INSERT INTO XXX VALUES (231, "Заголовок поста", "Текст поста", "open", "post");

Чуете грёбаный подвох? Если в каком-то текстовом поле будет содержаться наша хитрая конструкция, то запрос MySQL поймёт её так, что у нас кончилось одно поле и остальные данные надо занести в другие, следующие поля. То есть, огрызок текста после кавычек может попадать на поле «comment_status», а WordPress, если не видит в нём значения «open» — считает комменты к посту закрытыми. Во как!

Стоп-стоп! А как же тогда я написал этот пост? Как же он вообще добавился в базу? А очень просто! Для таких вот конструкций применяется экранирование символов — стандартный приём для таких случаев на уровне всех движков баз данных и языков программирования. Наш запрос можно написать вот так:

INSERT INTO XXX VALUES (231, "Заголовок поста", "Текст поста \"в кавычках\", текст дальше", "open", "post");

В этом случае движок поймёт, что кавычки тут надо читать не как конец значения, а как… да как просто кавычки =) Значит, первое, к чему такое может приводить — это чей-то хреновый код. Сам движок WordPress, конечно же, экранирует все символы нормально.

Но у нас есть ещё и знак процента! А в MySQL часто процентами обозначают всякие условия. Скажем, если нам надо найти слово в поле, то его пишут как «LIKE ‘%слово%'». И если кто-то напутает с экранированием символов — то может быть такой вот косяк.

Конечно же виновника мне вычислить не удалось, ибо в обоих случаях посты повреждались как-то сами по себе и я не знал про это. Теперь я буду после каждых обновлений движка и серьёзных перемен прогонять через базу тот поисковый запрос и, если соображу, кто мне так гадит — допишу этот пост! В общем, будьте внимательны!

Проекту исполнилось 15 лет! Поддержать проект материально, проспонсировать проекты Автора или сделать ему подарок можно на этой странице: "Донаты и Спонсорство, Список Желаний".

9 Отзывов на “Хитрый глюк WordPress: Теряем хвосты постов до «%»!”


  • 1 El Sueno  [Украина, Черкассы]

    Вернулся! Писал же что до 25-го ничо делать не будешь. А сам накатал два поста — про Нептуна и WordPress.

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

    Не по теме: напишешь почему «Хочу назад, там жить и работать световиком!» ? =)
    После поста о Кипре стало интересно, где же в этот раз был и что ТАКОЕ там нашел)

  • 2 CS  [Москва]

    А я обязан прям исполнять всё, что писал? Вон я ща кааак на ЮТуб вывалю видосов про Medieval Fest — и опять все гавном поливаться будут — те, кто думал что им про щитки будут втирать.
    А вот как раз ТАКОЕ и было. Напишу потом пост.

  • 3 andy.pmb  [Харьков]

    WordPress уже давно имеет устойчивую репутацию самой дырявой CMS-ки в мире. И в то же время каким-то чудом остаётся самой популярной CMS-кой. Парадокс.

    А вообще экранирование- это костыль. Правильный путь- это parameter binding (ХЗ как это по-русски) в SQL запросах, и тот же MySQL его умеет с незапамятных времен. Забыть что-то заэкранировать проще простого. Куда надёжнее не использовать динамическое формирование запросов как таковое. Но вот пых-пыхеры, по какой-то причине, настойчиво обходят «правильные подходы» стороной.

  • 4 CS  [Москва]

    Так вроде как сама CMS не дырявая. Больше всего дыр в плагинах было всегда. В самом WP основная уязвимость пёрла через XML-RPC, который тупо лочишь через .htaccess и всё (у меня так и сделано).
    А вот кто как плагин напишет — там поди угадай. Поэтому я стараюсь дофига плагинов и фишек не использовать.

    Parameter Binding. Я не знаю, как это на MySQL и пока вообще первый раз слышу. А как им собрать какую-нить строку SQL-запроса типа Select/Update? Расскажи общий смысл, а?

  • 5 Caesarion  [Новосибирск]

    CS, это как аргументы функций в программировании. Можно так:
    f(123);
    а можно так:
    int a = 123;
    f(a);
    А вот мануал на русском про PHP и базы данных: http://php.net/manual/ru/pdo.prepared-statements.php

  • 6 andy.pmb  [Харьков]

    Ну вот постит, скажем, кто-то новый комент, и нужно добавить его в таблицу с комментариями. Есть у нас три переменных- topic_id, comment_date, comment_text, и нам нужно значения из этих переменных вставить в строку в таблице. В случае с динамическим формированием запросов будет что-то вроде (псевдокод):

    sql_stmt = "insert into topic_comments (topic_id, comment_date, comment_text) values(" + topic_id + ", " + comment_date + ", " + escape_sql(comment_text) + ")"; sql_exec(sql_stmt);

    В примере выше escape_sql() экранирует спец-символы, и если про него забыть, будут проблемы, как описаны в статье. Альтернативный подход выглядит примерно так (тоже псевдокод):

    sql_stmt = "insert into topic_comments(topic_id, comment_date, comment_text) values(?, ?, ?)"; prep_stmt = sql_prepare(sql_stmt) sql_stmt_bind(1, topic_id); sql_stmt_bind(2, comment_date); sql_stmt_bind(3, comment_text); sql_stmt_exec(prep_stmt); sql_stmt_close(prep_stmt);

    У такого подхода есть несколько преимущества. Во-первых ничего экранировать не нужно. Текст запроса и значения параметров в базу передаются раздельно, и значения параметров никаким специальным образом не обрабатываются, даже если выглядят как валидные SQL-запросы. Во-вторых код выглядит чище, особенно когда запрос сложный, и параметров много. Ну и в третьих, если нужно выполнить один и тот же запрос но с разными значениями параметров много раз, то синтаксический разбор запроса повторно выполнять не нужно, и код будет работать немного быстрее (опять псевдокод):

    sql_stmt = "insert into topic_comments(topic_id, comment_date, comment_text) values(?, ?, ?)"; prep_stmt = sql_prepare(sql_stmt) sql_stmt_bind(1, topic_id); sql_stmt_bind(2, comment_date); sql_stmt_bind(3, comment_text); sql_stmt_exec(prep_stmt); topic_id = another_topic_id; comment_date = another_comment_date; comment_text = another_comment_text; sql_stmt_exec(prep_stmt); topic_id = yet_another_topic_id; comment_date = yet_another_comment_date; comment_text = yet_another_comment_text; sql_stmt_exec(prep_stmt); sql_stmt_close(prep_stmt);

  • 7 CS  [Москва]

    Млять!!! ПОЧЕМУ Я ЭТОГО НЕ ЗНАЛ?!!! Я когда в 2005-7 году поднимал один сайт — я там тупо составлял запросы склеиванием строк и затрахался это делать, потому что пропустишь чего-то в склейке — и пиздец!!
    Это ж как удобно-то! Я примеры посмотрел и взял на заметку!

  • 8 b-s-a

    Я использую в PHP класс PDO:
    $db = new PDO("my_server_url"); if ($db === NULL)     ... $db->exec("SET SESSION timezone TO 'UTC'"); $st = $db->prepare("SELECT something FROM table WHERE p1 = :v1 AND p2 = :v2"); if ($st === FALSE)     ... if ($st->execute(['v1' => $v1, 'v2' => $v2]) === FALSE)     ... $res = $st->fetch(PDO::FETCH_ASSOC); ...

    @CS, не могли бы вы сделать статью описывающую, как правильно подключать к щитам вводные кабели, речь про алюминиевые и бронированные (типа АВБШв), а то не очень понятно, можно ли как-то сделать, чтобы постоянно не подтягивать винты (закрепил на винт в гребенке ноль (4 мм^2), так через два года там фейрверк начался).

  • 9 CS  [Москва]

    b-s-a Ну, классы… ООП местами слишком грузит всех и вся. Мы уже и так докатились, что у нас щас стало нормой, что какое-нить приложение, которое показывает три окошка, весит под 150-200 мегабайт. Давайте и PHP нагружать. А чё? Нагрузим PHP, он будет тормозить, а мы возьмём сервера позлее, и тады тормозящий PHP на них будет работать так же быстро, как и до того, когда мы его не нагружали =)
    Но идея понятна.

    Пост сделать? Я уже в одном посте тебя ругал за то, что ты не читаешь, а пишешь, так тут продолжу. А наконечники ТМЛ/ТА — не? Не годятся?
    Поста не будет. Особенно по таким запросам вида «я ничё не нашёл, сделай пост». И про что будет пост? Про «как подключать жирные кабели к щитам для дибилов?». Но я для дибилов не пишу =)

Оставить отзыв

Вы должны войти на блог, чтобы оставить комментарий.