Добро пожаловать в Пение с Синатрой ! В этой третьей и последней части мы будем расширять приложение « Recall », которое мы создали на предыдущем уроке. Мы собираемся добавить канал RSS в приложение с невероятно полезным гемом Builder , который делает создание XML-файлов в Ruby несложным делом. Мы узнаем, насколько просто Sinatra делает возможным экранирование HTML от пользовательского ввода для предотвращения XSS-атак, и мы улучшим некоторые из кодов обработки ошибок.
Пользователи плохие, m’kay
Общее правило при создании веб-приложений — быть параноиком. Параноидально, что каждый из ваших пользователей хочет получить вас, уничтожив ваш сайт или нападая на него других пользователей. В своем приложении попробуйте добавить новую заметку со следующим содержанием:
1
|
</article>woops <script>alert(«zomg haxz»);</script>
|
В настоящее время наши пользователи могут свободно вводить любой HTML-код, который им нравится. Это оставляет приложение открытым для атак XSS, где пользователь может вводить вредоносный JavaScript-код для атаки или перенаправления других пользователей сайта. Поэтому первое, что нам нужно сделать, — это скопировать весь пользовательский контент, чтобы приведенный выше код был преобразован в сущности HTML, например, так:
1
|
</article>woops <script>alert("zomg haxz");</script>
|
Для этого добавьте следующий блок кода в ваш файл 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 на других страницах
Нажмите на ссылку «изменить» для заметки с кодом 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 Feed Массы
Важной частью любого динамического веб-сайта является некая форма 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 вместе с готовым приложением .