Статьи

Пение с Синатрой — бис

Добро пожаловать в Пение с Синатрой ! В этой третьей и последней части мы будем расширять приложение « Recall », которое мы создали на предыдущем уроке. Мы собираемся добавить канал RSS в приложение с невероятно полезным гемом Builder , который делает создание XML-файлов в Ruby несложным делом. Мы узнаем, насколько просто Sinatra делает возможным экранирование HTML от пользовательского ввода для предотвращения XSS-атак, и мы улучшим некоторые из кодов обработки ошибок.


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

1
</article>woops <script>alert(«zomg haxz»);</script>

В настоящее время наши пользователи могут свободно вводить любой HTML-код, который им нравится. Это оставляет приложение открытым для атак XSS, где пользователь может вводить вредоносный JavaScript-код для атаки или перенаправления других пользователей сайта. Поэтому первое, что нам нужно сделать, — это скопировать весь пользовательский контент, чтобы приведенный выше код был преобразован в сущности HTML, например, так:

1
&lt;/article&gt;woops &lt;script&gt;alert(&quot;zomg haxz&quot;);&lt;/script&gt;

Для этого добавьте следующий блок кода в ваш файл recall.rb , например, в DataMapper.auto_upgrade! линия:

1
2
3
4
helpers do
    include Rack::Utils
    alias_method :h, :escape_html
end

Это включает в себя набор методов, предоставляемых Rack. Теперь у нас есть доступ к методу h() для экранирования HTML.

Чтобы выйти из HTML на домашней странице, откройте файл views/home.erb и измените <%= note.content %> (вокруг строки 11) на:

1
<%=h note.content %>

В качестве альтернативы мы могли бы записать это как <%= h(note.content) %> , но вышеприведенный стиль гораздо более распространен в сообществе Ruby. Обновите страницу, и отправленный HTML-код теперь должен быть экранирован и не выполнен браузером:

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

1
</textarea> <script>alert(«haha»)</script>

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

Внутри файла views/edit.erb экранируйте содержимое внутри textarea , запустив его через метод h (строка 4):

1
<textarea name=»content»><%=h @note.content %></textarea>

И сделайте то же самое в файле views/delete.erb в строке 2:

1
<p>Are you sure you want to delete the following note: <em>»<%=h @note.content %>»</em>?</p>

Вот и все — теперь мы в безопасности от XSS. Просто не забывайте избегать всех пользовательских данных при создании других веб-приложений в будущем!

Вам может быть интересно «а как же SQL-инъекции?» Что ж, DataMapper обрабатывает это для нас так же, как мы используем методы DataMapper для получения данных из базы данных (т. Е. Не выполняем необработанный SQL).


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

1
gem install builder

В зависимости от того, как вы настроили RubyGems в вашей системе, вам может потребоваться префикс gem install с помощью sudo .

Теперь добавьте новый маршрут к вашему recall.rb приложения recall.rb для запроса GET в /rss.xml :

1
2
3
4
get ‘/rss.xml’ do
    @notes = Note.all :order => :id.desc
    builder :rss
end

Убедитесь, что вы добавили этот маршрут где-то выше маршрута get '/:id' , иначе запрос на rss.xml будет ошибочно принят за идентификатор сообщения!

В маршруте мы просто запрашиваем все заметки из базы данных и загружаем rss.builder представления rss.builder . Обратите внимание, как раньше мы использовали механизм ERB для отображения файла .erb , теперь мы используем Builder для обработки файла. Файл Builder — это в основном обычный Ruby-файл со специальным xml объектом для создания XML-тегов.

Запустите ваш views/rss.builder view views/rss.builder со следующим:

1
2
3
4
5
6
xml.instruct!
xml.rss :version => «2.0» do
    xml.channel do
         
    end
end

Очень важное примечание: в первую секунду кода, приведенного выше, удалите точку ( . ) В тексте :.xml . WordPress мешает фрагментам кода.

Строитель разберет это так:

1
2
3
4
5
6
<?xml version=»1.0″ encoding=»UTF-8″?>
<rss version=»2.0″>
    <channel>
         
    </channel>
</rss>

Итак, мы начали с создания структуры для допустимого файла XML. Теперь давайте добавим теги для заголовка ленты, описания и ссылки на основной сайт. Добавьте в блок xml.channel do :

1
2
3
xml.title «Recall»
xml.description «’cause you’re too busy to remember»
xml.link request.url

Обратите внимание, как мы получаем текущий URL из объекта request . Мы могли бы написать это вручную, но идея в том, что вы можете загрузить приложение куда угодно, не меняя непонятные фрагменты кода.

Однако существует одна проблема: теперь для ссылки установлено (например) http://localhost:9393/rss.xml . В идеале мы бы хотели, чтобы ссылка была на домашнюю страницу, а не на фид. Объект request также имеет метод path_info который устанавливается в текущей строке маршрута; так что в нашем случае /rss.xml .

Зная это, мы можем теперь использовать метод chomp Руби, чтобы удалить путь из конца URL. Измените xml.link request.url на:

1
xml.link request.url.chomp request.path_info

Ссылка в нашем XML-файле теперь установлена ​​на http://localhost:9393 . Теперь мы можем просмотреть каждую заметку и создать для нее новый элемент XML:

1
2
3
4
5
6
7
8
9
@notes.each do |note|
    xml.item do
        xml.title h note.content
        xml.link «#{request.url.chomp request.path_info}/#{note.id}»
        xml.guid «#{request.url.chomp request.path_info}/#{note.id}»
        xml.pubDate Time.parse(note.created_at.to_s).rfc822
        xml.description h note.content
    end
end

Обратите внимание, что в строках 3 и 7 мы избегаем содержимого заметки, используя h , как мы это делали в основных видах. Немного странно отображать одинаковое содержимое для тегов title и description , но мы следуем примеру Twitter, и других данных мы не можем там разместить.

В строке 6 мы конвертируем время created_at примечания в RFC822 , необходимый формат для времен в RSS-каналах.

Теперь попробуйте это в браузере! Перейдите на /rss.xml и ваши заметки должны отображаться правильно.


Есть одна небольшая проблема с нашей реализацией. В нашем представлении RSS у нас есть заголовок и описание сайта. Мы также получили их в файле views/layout.erb для основной части сайта. Но теперь, если мы хотим изменить название или описание сайта, нам нужно обновить два разных места. Лучшим решением было бы установить заголовок и описание в одном месте, а затем ссылаться на них оттуда.

Внутри recall.rb приложения recall.rb добавьте следующие две строки в начало файла, сразу после операторов require , чтобы определить две константы:

1
2
SITE_TITLE = «Recall»
SITE_DESCRIPTION = «’cause you’re too busy to remember»

Теперь вернемся в views/rss.builder изменим строки 4 и 5 на:

1
2
xml.title SITE_TITLE
xml.description SITE_DESCRIPTION

А внутри views/layout.erb измените <title> в строке 5 на:

1
<title><%= «#{@title} | #{SITE_TITLE}» %></title>

И измените теги заголовков h1 и h2 в строках 12 и 13 на:

1
2
<h1><a href=»/»><%= SITE_TITLE %></a></h1>
<h2><%= SITE_DESCRIPTION %></h2>

Мы также должны включить ссылку на канал RSS в head страницы, чтобы браузеры могли отображать кнопку RSS в адресной строке. Добавьте следующее непосредственно перед </head> :

1
<link href=»/rss.xml» rel=»alternate» type=»application/rss+xml»>

Нам нужен какой-то способ информировать пользователя, когда что-то пошло не так — или правильно, например, подтверждающее сообщение, когда добавляется новая заметка, удаляется заметка и т. Д.

Наиболее распространенный и логичный способ достижения этого — «флэш-сообщения» — короткие сообщения, добавляемые в сеанс браузера пользователя, которые отображаются и очищаются на следующей странице, которую они просматривают. И вот так случилось, что есть пара RubyGems, чтобы помочь достичь этого! Введите в Терминал следующее, чтобы установить Rack Flash и Sinatra Redirect с гемами Flash :

1
gem install rack-flash sinatra-redirect-with-flash

В зависимости от того, как вы настроили RubyGems в вашей системе, вам может потребоваться префикс gem install с помощью sudo .

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

1
2
3
4
5
require ‘rack-flash’
require ‘sinatra/redirect_with_flash’
 
enable :sessions
use Rack::Flash, :sweep => true

Добавить новое флэш-сообщение так же просто, как flash[:error] = "Something went wrong!" , Давайте отобразим ошибку на домашней странице, когда в базе данных нет заметок.

Измените ваш маршрут get '/' на:

1
2
3
4
5
6
7
8
get ‘/’ do
    @notes = Note.all :order => :id.desc
    @title = ‘All Notes’
    if @notes.empty?
        flash[:error] = ‘No notes found.
    end
    erb :home
end

Очень просто. Если @notes экземпляра @notes пуста, создайте новую ошибку флэш-памяти. Чтобы отобразить эти флэш-сообщения на странице, добавьте следующее в файл views/layout.erb перед <%= yield %> :

1
2
3
4
5
6
7
<% if flash[:notice] %>
    <p class=»notice»><%= flash[:notice] %>
<% end %>
 
<% if flash[:error] %>
    <p class=»error»><%= flash[:error] %>
<% end %>

И добавьте следующие стили в ваш файл public/style.css чтобы отображать уведомления зеленым цветом и ошибки красным:

1
2
.notice { color: green;
.error { color: red;

Теперь ваша домашняя страница должна отображать сообщение «заметки не найдены», когда база данных пуста:

Теперь давайте отобразим сообщение об ошибке или об успехе в зависимости от того, можно ли добавить новую запись в базу данных. Измените ваш post '/' маршрут на:

01
02
03
04
05
06
07
08
09
10
11
post ‘/’ do
    n = Note.new
    n.content = params[:content]
    n.created_at = Time.now
    n.updated_at = Time.now
    if n.save
        redirect ‘/’, :notice => ‘Note created successfully.’
    else
        redirect ‘/’, :error => ‘Failed to save note.’
    end
end

Код довольно логичен. Если заметка может быть сохранена, перенаправьте ее на домашнюю страницу с помощью флеш-сообщения «Уведомление», в противном случае перенаправьте на страницу с флеш-сообщением об ошибке. Здесь вы можете увидеть альтернативный синтаксис для установки флеш-сообщения и перенаправления страницы, предлагаемой гемом Sinatra-Redirect-With-Flash.

Также было бы идеально отображать ошибку на странице «Редактировать заметку», если запрошенная заметка не существует. Измените маршрут get '/:id' на:

1
2
3
4
5
6
7
8
9
get ‘/:id’ do
    @note = Note.get params[:id]
    @title = «Edit note ##{params[:id]}»
    if @note
        erb :edit
    else
        redirect ‘/’, :error => «Can’t find that note.»
    end
end

А также на странице запроса PUT для обновления заметки. Измените put '/:id' на:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
put ‘/:id’ do
    n = Note.get params[:id]
    unless n
        redirect ‘/’, :error => «Can’t find that note.»
    end
    n.content = params[:content]
    n.complete = params[:complete] ?
    n.updated_at = Time.now
    if n.save
        redirect ‘/’, :notice => ‘Note updated successfully.’
    else
        redirect ‘/’, :error => ‘Error updating note.’
    end
end

Измените маршрут get '/:id/delete' на:

1
2
3
4
5
6
7
8
9
get ‘/:id/delete’ do
    @note = Note.get params[:id]
    @title = «Confirm deletion of note ##{params[:id]}»
    if @note
        erb :edit
    else
        redirect ‘/’, :error => «Can’t find that note.»
    end
end

И соответствующий ему запрос DELETE, delete '/:id' для:

1
2
3
4
5
6
7
8
delete ‘/:id’ do
    n = Note.get params[:id]
    if n.destroy
        redirect ‘/’, :notice => ‘Note deleted successfully.’
    else
        redirect ‘/’, :error => ‘Error deleting note.’
    end
end

Наконец, измените маршрут get '/:id/complete' на следующий:

01
02
03
04
05
06
07
08
09
10
11
12
13
get ‘/:id/complete’ do
    n = Note.get params[:id]
    unless n
        redirect ‘/’, :error => «Can’t find that note.»
    end
    n.complete = n.complete ?
    n.updated_at = Time.now
    if n.save
        redirect ‘/’, :notice => ‘Note marked as complete.’
    else
        redirect ‘/’, :error => ‘Error marking note as complete.’
    end
end

Работающее, безопасное и чувствительное к ошибкам веб-приложение, написанное на удивление небольшим количеством кода! За этот короткий мини-сериал мы узнали, как обрабатывать различные HTTP-запросы с помощью интерфейса RESTful, обрабатывать отправку форм, избегать потенциально опасного содержимого, подключаться к базе данных, работать с пользовательскими сессиями для отображения флэш-сообщений, генерировать динамический канал RSS и как изящно обрабатывать ошибки приложения.

Если вы хотите продвинуть приложение дальше, вы можете заняться аутентификацией пользователя, такой как гем аутентификации Sinatra .

Если вы хотите развернуть приложение на веб-сервере, поскольку Sinatra построен с использованием Rake, вы можете очень легко разместить свои приложения Sinatra на серверах Apache и Nginx, установив Passenger .

Кроме того, вы можете воспользоваться Heroku , хостинг-платформой на основе Git, которая делает развертывание ваших веб-приложений на Ruby таким же простым, как git push heroku (бесплатные аккаунты доступны!)

Если вы хотите узнать больше о Sinatra, ознакомьтесь с очень подробным файлом Readme , страницами документации и бесплатной Sinatra Book .

Примечание: исходные файлы для каждой части этой мини-серии доступны на GitHub вместе с готовым приложением .