Еще со времен запуска первой версии Записок я ношу в себе идею разработки собственного блогодвижка. Задача, казалось бы, не ахти какая сложная и очень многие ее успешно решают, движков понаписано великое множество на всех языках. Собственно, поэтому идея и пролежала в моей голове без дела почти 4 года. И вот буквально после новогодних праздников я наконец нашел достойный повод взяться за нее – блоги практически не представлены на Facebook.
Существует всего несколько приложений под Facebook, относящихся в той или иной степени к блоггингу. Стандартные записки (Notes) убивают всякий коммерческий интерес к разработке альтернатив. И тем не менее, я взялся такую альтернативу создать, в первую очередь ради эксперимента. Получилась вот такая штука: Blog Box (демонстрационный блог)
По ходу работы над приложением выявилось множество нюансов, которые встречаются от проекта к проекту. Одним из первых вопросов, с которыми я столкнулся – как работать с пользовательским контентом.
Разметка
Практически каждый человек хочет как-то разнообразить свои посты, добавить в них ссылки, выделить текст жирным или курсивом, вставить список или табличку. Надо понимать, что большинство пользователей не знают и знать не хотят что такое HTML. В блогоплатформах для пользователей обычно ставится WYSIWYG-редактор типа TinyMCE, FCKEditor, Xinha или любой другой. Однако в Facebook такой вариант не пройдет – платформа очень жестко контролирует JavaScript, используемый приложением, и практически сводит на нет возможность использования готовых библиотек.
Для ручной разметки текста и избавления от необходимости в визуальном редакторе можно использовать упрощенные системы разметки, которые часто применяются в wiki-системах. Для Ruby есть два реализованных формата разметки – Textile (с помощью RedCloth) и Markdown (с помощью BlueCloth). Textile более гибок в плане стилевой разметки, Markdown проще для освоения. Я решил остановиться на Textile.
Обработку текста я вынес в отдельный хелпер, единый как для постов, так и для комментариев:
def format_content(content)
textilize(content.to_s)
end
Метод textilize – стандартный рельсовый, становится доступен после установки RedCloth. Для обработки постов и комментариев я создал отдельные хелперы, передающие соответствующий контент для форматирования:
def post_content(post)
format_content(post.text)
end
def post_excerpt(post)
post.excerpt.blank? ? post_content(post) : format_content(post.excerpt)
end
def comment_content(comment)
format_content(comment.text)
end
Далее вызываем нужные нам хелперы хоть на странице просмотра поста, хоть при выводе списка постов.
Очистка HTML
Если честно, я ожидал, что мое приложение сразу кто-нибудь попробует использовать в гнусных целях, похакать, засунуть XSS или какой-нибудь мерзкий JavaScript. Однако я не ожидал, что это случится в первый же день жизни приложения. Хорошо, что я озаботился где возможно прикрыть вывод пользовательских данных с помощью хелпера h():
<%=h @post.title %>
Однако текст поста, который форматируется с помощью Textile, так не обезопасишь. Поэтому следующее что пришлось делать – это очистку текста постов от потенциально опасных тэгов и атрибутов. Для решения этой задачи существует несколько различных вариантов. В рельсах есть встроенный хелпер для очистки от ненужных тэгов – sanitize. Есть сторонние решения, такие как xss_terminate, Dryopteris или sanitize, использующие возможности более быстрых библиотек для работы с HTML – Hpricot и Nokigiri.
Я решил использовать в своем проекте плагин Sanitize – он для меня оказался более удобен в конфигурировании. Для интеграции с моделями я написал небольшой модуль, в котором немного поменял стандартные схемы очистки тэгов и атрибутов, а также добавил метод sanitize_attributes для быстрого привязывания очистки атрибутов. Весь входящий HTML разбирается средствами Hpricot, вычищаются ненужные тэги и атрибуты, добавляются нужные атрибуты (например, rel=nofollow для ссылок из комментариев) и обработанный код сохраняется в БД.
Специальные тэги
Мое знакомство с блоггингом, как и у многих, началось с LiveJournal. Система во многом неудобная, но в ней есть одна изюминка, которая мне очень нравилась – специальные тэги. Например, если я вставлю в текст поста тэг <lj user="my_friend">, то в тексте поста будет показана красиво оформленная ссылочка на журнал моего друга со всплывающей по наведению мышки информацией. Фишка очень интересная и я обязательно хотел использовать ее в своем блогодвижке.
Что самое смешное, я был даже вынужден ее использовать, когда дело дошло до вставки видео. В Facebook есть суровое ограничение на то, какие тэги можно использовать на страницах, а какие – нет. Это ограничение касается и тэга <object>, который по факту запрещен. Вместо него для вставки Flash используется тэг <fb:swf>, однако он сильно ограничен в плане конфигурирования. Видео-хостинги же для вставки видео дают только код на основе тэга <object>, без вариантов. Я задумался.
Решение нашлось довольно быстро – выводить обычный код вставки видео внутри тэга <iframe>, на который ограничение не накладывается. Однако опять же, код для iframe нужно каким-то образом сформировать, да и сам iframe нужно как-то вставить. Для формирования кода я решил использовать плагин acts_as_unvlogable, позволяющий из URL страницы с видео сформировать код. Вопрос со вставкой iframe встал на первый план.
На помощь пришла библиотека Radius, ключевая фишка Radiant CMS. С ее помощью можно создавать свои тэги, которые затем парсятся и обрабатываются ruby-кодом. Для размещения видео пользователю необходимо сформировать и вставить в текст поста вот такой код:
<bb:video url="http://www.youtube.com/watch?v=v9Lj0lF9Lao"></bb:video>
Обработка radius-кода вынесена в отдельный модуль, упрощающий добавление новых тэгов и их использование. Метод класса define_tag объявляет новый тэг, создает из блока метод экземпляра, который вызывается при обработке тэга, а так же добавляет тэг в общий список спец-тэгов, который можно затем использовать для расширения схемы очистки HTML, используемой sanitizer’ом:
Blogbox::RadiusContext.tags_with_namespace– все спецтэгиBlogbox::RadiusContext.tag_attributes– все разрешенные атрибуты
Для того, чтобы подключить спец-тэги стразу везде, меняем хелпер форматирования контента и хелперы вывода постов и комментариев:
def format_content(content, assigns = {}, &block)
textilized_content = textilize(content.to_s)
@@radius_context ||= Blogbox::RadiusContext.new(&block)
@@radius_context.set_current_context(self,
assigns.merge(
:view => self,
:controller => controller
)
)
Radius::Parser.new(@@radius_context,
:tag_prefix => Blogbox::RadiusContext::NAMESPACE
).parse(textilized_content)
end
def post_content(post)
format_content(post.text, :post => post)
end
def post_excerpt(post)
return post_content(post) if post.excerpt.blank?
format_content(post.excerpt, :post => post)
end
def comment_content(comment)
format_content(comment.text, :post => post, :comment => comment)
end
Что особенно хорошо в спец-тэгах, так это возможность использования вашего приложения (или сторонних приложений) для создания действительно богатого пользовательского контента. Например, постов со вставленными в них фотогалереями, списками пользователей, голосованиями и т.д. От вас требуется только немного фантазии для создания набора спец-тэгов.
Резюме
Пользовательский контент – далеко не банальная вещь. Здесь есть и подводные камни, и простор для творчества, и необходимость в разумном обстоятельном подходе. Если вы работаете с пользовательским контентом – заранее обдумайте, какие возможности вы хотите дать пользователю и какие точно не хотите давать. Исходя из специфики задачи и среды можно использовать те инструменты, которые уже разработаны ruby-сообществом, и сделать свой проект по истине уникальным.
