jQuery: решение проблемы с куками на WordPress при использовании MaxCache

7 декабря 2009 г.

Этот пост адресован пользователям WordPress, у которых стоит кэширование с помощь MaxCache.

WordPress имеет в своем функционале две очень хорошие особенности, которые повышают юзабилити сайта для посетителей, оставляющих комментарии:

  1. Однажды оставив одобренный администратором сайта комментарий, посетитель избавляется от необходимости при каждом следующем комментарии вновь вводить свои данные, такие, как имя, e-mail, и адрес сайта.
  2. Если добавляемый комментарий уходит на модерацию, то комментирующий все равно видит его на сайте, обычно с пометкой “коммент ожидает модерации”. Это дает посетителю понять, что комментарий успешно добавлен и не попал в спам-фильтр.

Проблема

Не так давно известный программист Максим создал очень полезный и нужный скрипт - кэш для WordPress (MaxCache), который существенно снижает нагрузку на сервер хостинга. Для моего тормознутого хостинга, на котором в данный момент мой блог dimox.name вынужден, так сказать, волочить свое существование, этот скрипт оказался спасением.

Но речь сейчас не о хостинге и не о скрипте MaxCache. Суть поста заключается в нижеследующем.

При использовании кэша MaxCache WordPress-сайт теряет те самые 2 замечательные функции, о которых я написал выше. Это связано с особенностью работы MaxCache, и, к сожалению, на уровне скрипта ничего сделать нельзя.

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

Я всегда стараюсь идти на встречу посетителям своих сайтов, поэтому я нашел выход из данной ситуации - написал на jQuery скрипт, который выполняет 2 утерянные после установки кэша функции.

Далее я подробненько опишу код скрипта. Если у вас нет желания все это читать, то можете сразу “перескочить” на конечный код скрипта.

Функция №1 - “вспоминаем” комментирующего

Чтобы избежать возможных конфликтов jQuery с другими фреймворками, создадим вот такую переменную, которую в дальнейшем и будем использовать:

1
var $j = jQuery.noConflict();

Ну а все содержимое скрипта будем размещать вот в такую функцию, которая выполнит находящийся в ней код после загрузки веб-страницы:

1
$j(function() { ... })

Для того, чтобы “вспомнить” нашего посетителя, который уже оставлял комментарий, мы создадим куки, в которых запишем его имя, e-mail и адрес сайта. Работать мы будешь, соответственно, с формой комментирования WordPress. Ее HTML-код выглядит примерно следующим образом (показываю только теги, необходимы нам):

1
2
3
4
5
6
<form action="/wp-comments-post.php" method="post" id="commentform">
  <input type="text" name="author" />
  <input type="text" name="email" />
  <input type="text" name="url" />
  ...
</form>

Чтобы скрипт, который мы получим в результате, у вас работал, убедитесь, что в вашем шаблоне WordPress у тега form используется точно такой же идентификатор (id=”commentform”).

У каждого input-тега в этой форме есть неизменный параметр name=”", за которые мы и будем цепляться при написании скрипта.

Куки мы будет создавать в тот момент, когда комментирующий нажимает кнопку “Добавить комментарий”, для этого воспользуемся вот такой функцией:

1
$j('#commentform').submit(function() { ... });

В ней мы считаем значения полей input[name=”author”], input[name=”email”], input[name=”url”] и с помощью функции createCookie() (ее код я приведу в конце, дабы не раздувать размер статьи) поместим их в куки браузера:

1
2
3
4
5
6
7
$j('#commentform').submit(function() {

  createCookie('wp_commenter_author', $j('input[name="author"]').val(), 365);
  createCookie('wp_commenter_email', $j('input[name="email"]').val(), 365);
  createCookie('wp_commenter_url', $j('input[name="url"]').val(), 365);

});

В результате в браузере пользователя появляются куки со следующими названиями: wp_commenter_author, wp_commenter_email и wp_commenter_url. Цифра 365 указывает на количество дней хранения этих куков.

Можно было так и оставить вышеуказанный код создания куков, но в ходе тестирования обнаружился следующий момент. Как вы знаете, если вы залогинены на своем блоге, то при добавлении комментария поля имя/е-mail/сайт за ненадобностью не отображаются. Поэтому, поскольку этих полей нет, то в 3-х создаваемых куках записывается значение “undifined” (т.е. значение не определено). А, поскольку, далее мы будем эти куки считывать, то нам таких неопределенностей необходимо избежать. Поэтому мы делаем это следующим образом:

1
2
3
4
5
6
7
$j('#commentform').submit(function() {

  if ($j('input[name="author"]').length) createCookie('wp_commenter_author', $j('input[name="author"]').val(), 365);
  if ($j('input[name="email"]').length) createCookie('wp_commenter_email', $j('input[name="email"]').val(), 365);
  if ($j('input[name="url"]').length) createCookie('wp_commenter_url', $j('input[name="url"]').val(), 365);

});

Т.е. условием вида if ($j(’input[name=”author”]’).length) мы проверяем существования 3-х полей, и, если они есть на странице (т.е. это не залогиненный пользователь), то создаем куки со значениями данных полей.

Хорошо, куки создали. Теперь нужно их считать для последующий подстановки в эти поля. Создадим переменные, в которые сразу же заносим значения наших кук (код функции readCookie() будет приведен также в конце):

1
2
3
var author = readCookie('wp_commenter_author');
var email = readCookie('wp_commenter_email');
var url = readCookie('wp_commenter_url');

Куки прочитали, теперь нужно заполнить ими 3 наших поля (имя/е-mail/сайт):

1
2
3
if (author) $j('input[name="author"]').val(author);
if (email) $j('input[name="email"]').val(email);
if (url) $j('input[name="url"]').val(url);

Условие вида if (author) говорит нам, что, если кука существует в браузере пользователя, то мы вставляем ее значение в соответствующее поле. Эта проверка необходима, чтобы избежать ошибки, если кук нет.

В результате мы получаем вот такой конечный скрипт, который реализует 1-ю функцию - запоминает данные, введенные комментирующим, и в последствии автоматически подставляет их в поля.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var $j = jQuery.noConflict();

$j(function() {

  var author = readCookie('wp_commenter_author');
  var email = readCookie('wp_commenter_email');
  var url = readCookie('wp_commenter_url');

  if (author) $j('input[name="author"]').val(author);
  if (email) $j('input[name="email"]').val(email);
  if (url) $j('input[name="url"]').val(url);

  $j('#commentform').submit(function() {
    if ($j('input[name="author"]').length) createCookie('wp_commenter_author', $j('input[name="author"]').val(), 365);
    if ($j('input[name="email"]').length) createCookie('wp_commenter_email', $j('input[name="email"]').val(), 365);
    if ($j('input[name="url"]').length) createCookie('wp_commenter_url', $j('input[name="url"]').val(), 365);
  });

})

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

Чтобы подобного избежать, мы доработаем наш скрипт.

Функция №2 - даем знать, что комментарий добавлен

Сразу отмечу, что, для того, чтобы работала нижеследующая часть кода, необходимо, чтобы в шаблоне комментариев стояла якорь-ссылка для каждого комментария, т.е. в PHP-коде это выглядит примерно следующим образом:

1
<a href="<?php echo htmlspecialchars( get_comment_link( $comment->comment_ID ) ) ?>"><?php printf(__('%1$s at %2$s'), get_comment_date(),  get_comment_time()) ?></a>

А если смотреть исходный HTML-код страницы WordPress-сайта, то должен быть примерно такой код:

1
<a href="адрес_страницы/#comment-123" title="02 февраля, 2008 в 10:06 пп">текст ссылки</a>

Сначала мы добавим новую куку, которая будет создаваться также при нажатии на кнопку “Добавить комментарий” и создадим переменную, в которую ее будем считывать:

1
2
3
4
5
6
var submitCheck = readCookie('wp_submit_check');

$j('#commentform').submit(function() {
  ...
  createCookie('wp_submit_check', '1', 1);
});

Ее значение ставим равным “1″ и срок ее хранения - 1 день.

Все, что мы будем делать дальше, будет работать только при условии, что значение куки “wp_submit_check” равно “1″.

1
if (submitCheck == '1') { ... }

А кука эта когда у нас появляется? Правильно - при отправке формы. Т.е. все манипуляции с кукой wp_submit_check сделаны для того, чтобы выполнять нижеследующие действия только тогда, когда отправлен комментарий, и посетителю нужно об этом сообщить.

Если кратко на словах, то мы будем делать следующее - после добавления комментария посетитель перенаправляется на страницу с адресом вида http://site.ru/postname/#comment-123, где цифра - это идентификатор добавленного комментария. Далее проверяем, есть ли уже на странице ссылка с точно таким же якорем (#comment-123), если нет, то показываем посетителю сообщение о том, что его комментарий добавлен.

Дальнейшие комментарии напишу в самом скрипте:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
if (submitCheck == '1') {

  // считываем URL из адресной строки браузера и заносим в переменную
  var href = window.location.href;

  // проверяем, находимся ли мы на странице вида http://site.ru/postname/#comment-123
  // (страница с таким адресом открывается всегда при добавлении комментария в WordPress)
  if (/.+#comment-(\d+)/ig.test(href)) {

    // получаем цифру из URL в адресной строке,
    // которая стоит после "#comment-",
    // и помещаем ее в переменную
    var cid = href.replace(/.+#comment-(\d+)/ig, '$1');

    // создаем регулярное выражение, которое будем использовать дальше,
    // и заносим его в переменную reg
    var reg = new RegExp('#comment-' + cid,'ig');

    // переменная, значение которой будем менять,
    // если ссылка с якорем найдется
    var commentIdFind = 0;

    // пробегаемся по каждой ссылке в комментариях
    $j('#commentlist a').each(function() {

      // тестируем URL ссылки (атрибут href) с помощью
      // регулярного выражения reg (см. выше)
      if (reg.test($j(this).attr('href'))) {

        // если нашли такую ссылку с якорем из адресной строки
        // (а это значит, что коммент выведен на страницу и ничего
        // дополнительно показывать посетителю не нужно),
        // то переменной commentIdFind ставим значение = 1
        // и удаляем куку wp_submit_check
        commentIdFind = 1;
        eraseCookie('wp_submit_check');
      }

    })

    // если commentIdFind = 0, получается, что ссылка не была найдена,
    // значит комментарий не выведен на страницу, и нужно показать сообщение
    if (commentIdFind == 0) {

      // выводим сообщение путем добавления тега <li></li> в конец списка с комментариями
      // и назначаем ему идентификатор вида comment-123,
      // где вместо цифры подставляется ID добавленного комментария,
      // для того, чтобы проскроллить страницу к этому сообщению
      // HTML-код сообщения измените под себя
      $j('#commentlist').append('<li id="comment-' + cid + '"><em><strong>' + author + '</strong>, спасибо за Ваш комментарий! Он будет опубликован, как только его проверит администратор сайта.</em></li>');

      // считываем юзер-агента для определения браузера Opera,
      // чтобы применить к нему особые стили
      var userAgent = navigator.userAgent.toLowerCase();
      if (/opera/.test(userAgent)) { var top = 'html'; } else { var top = 'html, body'; }

      // определяем расстояние от верхнего края браузера
      // до элемента с нашим сообщением, заносим в переменную
      var offset = $j('#comment-' + cid).offset().top;

      // плавно прокручиваем окно браузера до сообщения,
      // которое показываем посетителю
      $j(top).animate({scrollTop: offset}, 500);
    }

  // если мы ушли со странице вида http://site.ru/postname/#comment-123,
  // то удаляем куку wp_submit_check
  } else {
    eraseCookie('wp_submit_check');
  }

}

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

%username%, спасибо за Ваш комментарий! Он будет опубликован, как только его проверит администратор сайта.

Конечный код jQuery-скрипта

В итоге у нас получился вот такой скрипт (для тех, кто не читал мои подробности по коду - обратите внимание на условия работы скрипта: условие раз, условие два):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
function createCookie(name,value,days) {
  if (days) {
    var date = new Date();
    date.setTime(date.getTime()+(days*24*60*60*1000));
    var expires = "; expires="+date.toGMTString();
  }
  else var expires = "";
  document.cookie = name+"="+value+expires+"; path=/";
}
function readCookie(name) {
  var nameEQ = name + "=";
  var ca = document.cookie.split(';');
  for(var i=0;i < ca.length;i++) {
    var c = ca[i];
    while (c.charAt(0)==' ') c = c.substring(1,c.length);
    if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length);
  }
  return null;
}
function eraseCookie(name) {
  createCookie(name,"",-1);
}

var $j = jQuery.noConflict();

$j(function() {

  var author = readCookie('wp_commenter_author');
  var email = readCookie('wp_commenter_email');
  var url = readCookie('wp_commenter_url');
  var submitCheck = readCookie('wp_submit_check');

  if (author) $j('input[name="author"]').val(author);
  if (email) $j('input[name="email"]').val(email);
  if (url) $j('input[name="url"]').val(url);

  $j('#commentform').submit(function() {
    if ($j('input[name="author"]').length) createCookie('wp_commenter_author', $j('input[name="author"]').val(), 365);
    if ($j('input[name="email"]').length) createCookie('wp_commenter_email', $j('input[name="email"]').val(), 365);
    if ($j('input[name="url"]').length) createCookie('wp_commenter_url', $j('input[name="url"]').val(), 365);
    createCookie('wp_submit_check', '1', 1);
  });

  if (submitCheck == '1') {

    var href = window.location.href;

    if (/.+#comment-(\d+)/ig.test(href)) {

      var cid = href.replace(/.+#comment-(\d+)/ig, '$1');
      var reg = new RegExp('#comment-' + cid,'ig');
      var commentIdFind = 0;

      $j('#commentlist a').each(function() {
        if (reg.test($j(this).attr('href'))) {
          commentIdFind = 1;
          eraseCookie('wp_submit_check');
        }
      })
      if (commentIdFind == 0) {
        $j('#commentlist').append('<li id="comment-' + cid + '"><em><strong>' + author + '</strong>, спасибо за Ваш комментарий! Он будет опубликован, как только его проверит администратор сайта.</em></li>');
        var userAgent = navigator.userAgent.toLowerCase();
        if (/opera/.test(userAgent)) { var top = 'html'; } else { var top = 'html, body'; }
        var offset = $j('#comment-' + cid).offset().top;
        $j(top).animate({scrollTop: offset}, 500);
      }
    } else {
      eraseCookie('wp_submit_check');
    }

  }

})

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

Теги: , , , автор: Dimox | рубрика jQuery

Комментарии (30): »

  1. Огромное спасибо, Димокс! В закладки и блог-шоу однозначно!

  2. Пожалуйста, Миша ;)

  3. Большое спасибо!

    И проблема решена важная и статья познавательная.

  4. Дима, я так подумал, наверное всё-таки правильней в форме прицепиться не к id, а к name. В WordPress’е эти name стандартные, а id, как ты и написал, могут меняться. Так что переход к name гарантирует работу (заполнение полей) с любым шаблоном.

    @
  5. Точно! Как же я об этом не подумал? Привык всегда цепляться за id =) Спасибо, Макс, за подсказку! Внесу поправки.

  6. Ура, данные сохраняются :)

  7. Хм, только что-то после добавки предыдущего коммента моё имя само изменилось сами видите на что.

  8. Странненько… я сколько тестировал, все нормально было.

  9. Я вот глянул, а оно будет куки в днях хранить? Может было бы лучше в Unix?

  10. Что значит “в Unix”?

  11. Чтобы писать $, а не $j
    Можно весь свой скрипт обернуть в такую конструкцию, это самый правильный вариан:
    (function($){
    здесь ваш скрипт…
    })(jQuery);

    @
  12. Это гарантирует отстутствие конфликтов с другими фреймворками?

  13. Спасибо Dimox огромное, давно искал +один подписчик

  14. Тоже кириллические имена сохраняются как смайлики.

  15. Да, есть такая проблема. Пока не знаю, как решить.

  16. Я бы назвал все вышеизложенное “ПерАнусДерАстро” :-D
    Все можно сделать гораздо проще.
    1. Удалить из кеша страницу с комментарием, и для этого пользователя (однократно) вывести лайф версию.
    2. Не писать новые свои куки, а взять родные которые создал Вердпресс, и джаваскриптом распихать в инпуты.

    Таким образом не придется править код при обновлении движка.

    Пример скрипта который надо впихнуть в свой шаблон комментариев:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    function searchCookie(name)
    {
      var cookie = " " + document.cookie;
      var search = " " + name;
      var setStr = null;
      var offset = 0;
      var end = 0;
      if (cookie.length > 0) {
        offset = cookie.indexOf(search);
        if (offset != -1) {
          offset += search.length;
          end = cookie.indexOf(";", offset)
          if (end == -1) {
            end = cookie.length;
          }
          setStr = cookie.substring(offset, end);
          begin=setStr.indexOf('=')+1;
          setStr = unescape(setStr.substring(begin,setStr.length));
        }
      }
      return(setStr);
    }
    function getToInput()
    {
      var arg = getToInput.arguments;
      if (!arg[0]) arg=new Array('author','email','url');

      if (document.cookie)
      {
        var commData=new Array(searchCookie('comment_author'), searchCookie('comment_author_email'), searchCookie('comment_author_url'));
        if (!commData.join('')) return false;
        for (i=0;i<3;i++)
        {
          obj=document.getElementById(arg[i]);
          if ((obj) && (obj.value=='')) obj.value=commData[i];
        }
      }
    }
    // инициализация обязательно вставить после вывода инпутов.
    getToInput('author','email','url');
    @
  17. 1. Удалить из кеша страницу с комментарием, и для этого пользователя (однократно) вывести лайф версию.

    Как удалить из кеша страницу с комментарием? И что такое “лайф версия”?

  18. Как удалить из кеша страницу с комментарием?

    Для этого в файле wp-comments-post.php (в корне сайта) добавить
    перед wp_redirect($location); ?> (строка находится в самом конце файла) setcookie(’comment’,'clearCache’);

    а в начале функции max_cashe() после

    1
    2
    $dir = realpath(dirname(__FILE__)) . '/cache/';
      $filepath = $dir . md5($url);

    вставить:

    1
    2
    3
    4
    5
    6
    7
    if ((isset($_COOKIE) && strstr(implode($_COOKIE), 'clearCache')))
      {
        if (is_file($filepath)) unlink($filepath);
        $my_cache_no = true;
        setcookie('comment','');
        return false;
      }

    И что такое “лайф версия”?

    Версия не из кеша.

    @
  19. Ой там не $my_cache_no = true;, а наверное $max_cache_no = true;
    Не знаю точно, пока оригинального кода Max’a не видел. Но идея я думаю понятна?

    @
  20. Тут вот в чем дело. В платной версии это уже все работает автоматом. Поэтому ничего менять и доделывать не нужно. :)

    А вот что касается использования родных куков вместо своих - нужно думать. То что не нужно менять шаблон заманчиво, но отлавливать и устанавливать куку нужно до отработки WordPress и тут может быть сложность: в WordPress редиректы при комментировании. Кроме этого кука сама по себе отлавливается WordPress’ом в своих недрах и явно подставляется в инпуты в value. То есть на уровне html-кода. Так что вариант Димы более надежен.

    @
  21. Мы сейчас говорим, не о подстановке куков в вордпресовскую версию а о подстановке куков в кешированную версию, где поля (по идее) всегда должны быть чистыми.

    Кроме того, джаваскриптовые функции можно внедрять в момент кеширования только в кешированные версии. В таком случае даже рекомендаций по изменению шаблона не нужно будет делать.

    @
  22. Кешированная страница отдается как есть голой статикой. В ней если и использовать куки, то только на уровне js. Но внедрять js в кэшированную версию не есть хорошо. Код должен оставаться чистым, так как его отдает WordPress.

    @
  23. У меня выскочила аналогичная проблема, Джаваскрипт вычитывает из куков не совсем так как хотелось бы. К примеру:
    русс = %D1%80%D1%83%D1%81%D1%81;
    unescape(’%D1%80%D1%83%D1%81%D1%81′) = русс;

    Решается проблема следующим методом:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    var cookie = {
      // public method for Cookie decoding
      decode : function (string) {
        return this._utf8_decode(unescape(string));
      },
      // private method for UTF-8 decoding
      _utf8_decode : function (utftext) {
        var string = "";
        var i = 0;
        var c = c1 = c2 = 0;
        while ( i < utftext.length ) {
          c = utftext.charCodeAt(i);
          if (c < 128) { string += String.fromCharCode(c); i++; }
          else if((c > 191) && (c < 224)) {
            c2 = utftext.charCodeAt(i+1);
            string += String.fromCharCode(((c & 31) << 6) | (c2 & 63));
            i += 2;
          }
          else {
            c2 = utftext.charCodeAt(i+1);
            c3 = utftext.charCodeAt(i+2);
            string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
            i += 3;
          }
        }
        return string;
      }
    }

    В результате:
    cookie.decode(’%D1%80%D1%83%D1%81%D1%81′) = русс

    @
  24. Спасибо! В свободное время проверю и дополню пост.

  25. Мм, мучился мучился. Вроде все условия выполнены, но во-первых, так и не заработало, а во-вторых, “редактор” вместо простановки тегов лепит мне .
    Скрин вот

  26. Мм, мучился мучился. Вроде все условия выполнены, но во-первых, так и не заработало

    Не знаю, чем тебе помочь.

    а во-вторых, “редактор” вместо простановки тегов лепит мне .

    Прочитай про обновление плагина.

  27. Прочитай про обновление плагина.

    Вот, редактор ожил, спасибо.

    Эх, че ж делать то с куками..

  28. Наконец-то я добрался. Попробовал прицепить ваш код к своему скрипту - ничего не изменилось. Непонятно, что я неправильно делаю…

    cookie.decode() нужно применять при записи в куки или при чтении из кук?

  29. При чтении из кук.

    @

Присоединяйтесь к обсуждению!

Отправляя кoммeнтapий, Вы автоматически принимаете правила кoммeнтиpoвaния на этом блоге.

Правила кoммeнтиpoвaния на блоге dimox.name:

  1. Первый кoммeнтapий всегда проходит премодерацию.
  2. В поле "URL блога" можно указывать только ссылку на главную страницу вашего блога. Ссылки на прочие веб-ресурсы (в том числе блоги/сплоги, созданные не для людей) будут удалены.
  3. Запрещается использовать в качестве имени комментатора слоганы/названия сайтов, рекламные фразы, ключевые и т.п. слова. В случае несоблюдения этого условия имя изменяется по усмотрению владельца блога. Просьба указывать нормальное имя или ник.
  4. Весьма вероятно, что короткий и неинформативный кoммeнтapий вида "Спасибо!", "Интересная статья", будет удален. Исключение составляют знакомые автору блога комментаторы.

Подписаться, не комментируя

Предыдущие из рубрики