Обновление сайта до Ruby on Rails 3 - отчет
За последний месяц я обновил два сайта на Ruby on Rails 3. Первый — RentFeed — обновить было несложно, потому что функционала там немного. На обновление CarGid ушло около 40 человеко-часов; теперь и он работает на Ruby on Rails 3.
До обновления существующего сайта советую ознакомиться с Rails 3 на новом проекте. Нет, не так — считаю, что нельзя обновлять проект, не работав никогда с Rails 3.
Эта статья — не руководство по обновлению, а мои заметки о нем. Полноты раскрытия темы не обещаю.
Мотивация
Зачем тратить 40 часов на то, что не принесет ни новой функциональности, ни новых пользователей, ни существенного прироста производительности? Причины такие:
- Возможность использовать свежие библиотеки. Все больше библиотек перестают поддерживать Rails 2.3, и скоро тебе придется обновляться.
- Исправленные баги и недочеты в безопасности.
- В некоторых местах — намного более красивый и наглядный код. Красивые запросы на Arel в моделях. Лаконичные роуты. Понятные мейлеры.
Мое такое мнение, что обновляться уже пора. После знакомства с третьими рельсами недостатки вторых начинают бросаться в глаза.
Подготовка
Создаем ветку rails3 в репозитории.
Устанавливаем плагин rails_upgrade. Следуя инструкциям плагина, создаем резервные копии ценных файлов, а также создаем новый routes.rb и application.rb, и, если bundler еще не задействован, новый Gemfile. Помимо это, делаем:
rake rails:upgrade:check > upgrade_log.txt
Пригодится.
Коммитим изменения, дабы ничего не потерять.
Подбор гемов
Удаляем плагин rails_upgrade, ибо он больше не пригодится.
В Gemfile нужно принудительно указать gem 'rails', '~> 3.0.0', а кроме этого, если используется MySQL — то и gem 'mysql2'. Остальные гемы оставляем без изменений.
Возносим молитвы.
Производим bundle update. Эта команда попробует подобрать такие версии гемов, которые заработают с Rails 3.
Скорее всего, у нее это не получится. Проблемы с совместимостью решаются гуглением — большая часть гемов уже работает с Rails 3. Заодно пройдись по плагинам — возможно, некоторые из них также уже доступны в виде гемов.
Результатом этого этапа будет успешно собранный Gemfile.lock; коммитимся.
Установка Rails 3
Выполняем в каталоге проекта
rails .
Да, просто так. Эта команда предложит затереть ряд файлов — разрешаем, ведь все под контролем (версий, в смысле).
Коммитимся. Теоретически проект уже работоспособен (но вряд ли).
Зачистка
Убираем все файлы в script, кроме script/rails – они больше не используются.
Меняем mysql на mysql2 в database.yml, если используется MySQL.
Заменяем базовый config/application.rb на тот, который сгенерировали.
Подчищаем роуты
Подставляем тот routes.rb, который сгенерировали из старых роутов. Увы, скорее всего он сломанный и содержит синтаксические ошибки. Устраняем их.
Вот теперь проект точно должен запуститься.
Модели
В моделях надо будет поменять named_scope на scope и переформулировать запросы в Arel. Это пока необязательно.
Контроллеры
В контроллерах ничего особенного не поменялось
Виды
Большая часть времени уйдет именно на обновление видов.
Изменения в Javascript
Разнообразные рельсовые обработчики событий теперь вынесены в отдельный Javascript-файл, что прекрасно. Его нужно подключить. Для jQuery этот файл берется из репозитория rails/jquery-ujs.
Этому файлу нужен authenticity_token, поэтому в заголовок дописываем
<head>
...
<%=csrf_meta_tag %>
</head>
- style block helpers are deprecated. Please use =
Это такая шутка разработчиков, по-видимому. Везде, где используются блочные хелперы, придется дефис поменять на знак равенства:
<!-- было -->
<%-form_for @foo do %>
<!-- надо -->
<%=form_for @foo do %>
Слава богу, делать это не обязательно. Но ошибки в лог будут сыпаться.
Введение обязательного html_safe
В Rails 3 все строки, выводимые в шаблон, неизбежно проходят через эксейпинг. Это прекрасно, поскольку в 98% случаев ты выводишь текст, требующий оного.
<%=@blog_post.title_needs_to_be_escaped%>
<script>alert('U NO ESCAPE JAVASCRIPT');</script>
Хелперы h можно не убирать, двойного эскейпинга не произойдет:
<%=h @blog_post.title_needs_to_be_escaped%>
<script>alert('U NO ESCAPE JAVASCRIPT');</script>
Как же выводить текст с тегами? Применять к нему метод String#html_safe:
<%=@blog_post.nice_text_with_markup%>
<FONT COLOR="#FF00FF">ЛОВИ СИМПАФКУ!</FONT&;gt
<%=@blog_post.nice_text_with_markup.html_safe%>
<FONT COLOR="#FF00FF">ЛОВИ СИМПАФКУ!</FONT>
Получается, во местах, где ты выводишь строку с тегами, придется дописывать html_safe. Это сложный и кропотливый кусок работы, который нужно сделать в обмен на чистый и безопасный код.
ActionMailer
Синтаксис ActionMailer поменялся. Мейлеры придется переписать, а скорее, переформатировать. Это довольно прозрачный процесс.
Тесты
Разумеется, процесс обновления будет проще, если проект покрыт тестами. Если не покрыт, то, вполне возможно, ты (как и я) решишь заняться тестами перед обновлением. Однако советую начинать сразу после обновления: Rspec 2 работает только с Rails 3.
Обновляем
Скорее всего, ничего кроме исходников обновлять на сервере не придется, поэтому обновление ничем не будет отличаться от обычного деплоя. Возможно, придется сначала обновить на сервере Rubygems и Bundler.

Очень советую перед переводом на третьи рельсы поставить себе rails_xss плагин, тем самым избавившись от необходимости постфактум чинить вьюхи.
По-моему это тот случай, когда от перестановки слагаемых сумма не меняется. Объем работы тот же самый.
Другое дело что тем, кто уже пользуется rails_xss'ом, крупно повезло с апгрейдом. :)
Мы переехали на rails_xss. Скоро будем переезжать на третьи рельсы. Обьем работы тот же, но показать готовый результат ты сможешь быстрее. Дробя задачу на подзадачи ты сможешь сделать больше мелких работающих релизов. Принципы agile ;)
Вместо
<%=@blog_post.nice_text_with_markup.html_safe%>
можно
<%=raw @blog_post.nice_text_with_markup %>
или
<%== @blog_post.nice_text_with_markup %>
О, спасибо. По-моему так нагляднее. Хотя
rawидентичен.html_safe.Не идентичен. html_safe на nil выдаст ошибку.
Вопрос… А как позволить проходить тегам script, заключенным в pre, нетронутыми? Никак не могу допедрить…
То есть я хочу с помощью sanitize чистить текст от «плохих» тегов, но не чистить их в pre, чтобы можно было выкладывать примеры скриптов.
Так скрипты внутри <pre> не менее опасны. Я бы сначала парсил документ нокогирей, эспейпил содержимое тегов <pre>, сохранял результат, а потом уже делал sanitize.
А чем они опасны, если они будут выводиться заэскейпенными? По-моему, это то же самое, что не применять метод html_safe, а в этом случае скрипты не опасны. Я не прав?
UPD: а, я, видимо, в первый раз выразился не совсем верно… Я как раз и имел в виду, что sanitize не должен чистить скрипты, находящиеся в pre, но не подумал, что для этого их нужно просто заэскейпить.
С nokogiri не работал, но попробую разобраться, как заэскейпить содержимое pre, спасибо за совет.
Весь stackoverflow облазил, так и не смог разобраться с nokogiri. Можете привести пример, как пропарсить @post.content (и @post.title, наверное) на наличие pre и заэскейпить содержимое? Только, наверное, нужно какое-то исключение, чтобы не эскейпить тег code внутри pre.
Может проще через регулярку пропустить?
И правильно ли я понимаю, что пропускать через nokogiri придется все поля, содержащие текст (title, content и т.д.)?
Не надо HTML через регулярку, он этого не любит :)
https://gist.github.com/1394229 – где-то так.
Если я правильно понял задачу, то нужно сначала сделать это, а потом прогнать по результату sanitize, который поубирает лишние теги.
Спасибо, Леонид! Попробую так.
Так я правильно понял, что метод escape_pres с последующим sanitize применять перед сохранением поста ко всему, что имеет текст? Или sanitize (насколько я понял по тому, что именно так и реализовал) только к самому @post? То есть так:
escape_pres(@post.title)
escape_pres(@post.content)
@post = Sanitize.clean(@post, …)
Только к тем полям, где по дизайну может быть <pre>. По-моему к названию можно и не применять.
Да, как-то не подумал… К title только sanitize нужно будет.
Игрался с этим скриптом, пытаясь определить очередность выполнения среди преобразования текста в html через redcloth, очистки с помощью sanitize и эскейпингом pre. В итоге понял, что я изначально делал не ту очередность, и теперь работает все и без скрипта, вот таким вот макаром: