16.03.2016Как я встраивал reCAPTCHA v2 в свой антикварный бложек
Предпосылки
Мы живём в интересное время. Когда я увлекался довольно серьёзно руби он рэйлз, лет шесть-семь назад, была версия руби 1.8.7 и версия рельсов 2.3. Все неспешно переходили на руби 1.9.1 и рэйлз 3.0. Сегодня, пять лет спустя, стабильная версия руби — 2.2.3, а про рельсы уже агитируют переходить на 5.0, хоть и бета. Когда же я начал свой первый проект на ноде — три года назад, — версия node.js была что-то типа 0.22. А сегодня уже 5.8!
А недавно имел разговор с читателем на тему того, что стремительно возросшая популярность джаваскрипта создаёт ситуацию, при которой, уходя в отпуск, программисты рискуют вернуться с устаревшими навыками, потому что за две недели появились три новых прекрасный фреймворка, а два не менее прекрасных — умерли.
Антиквариат
До последнего перерыва, рассказ о начинке моего блога мог бы легко заполнить парочку в меру интересных статей, а сейчас это представляет интерес только для археологов. Что касается движков для блога вообще, то про когда-то популярный ЖЖ все уже забыли. Стремительно набирает моду вести канал в Телеграме. А товарищ мой — Илья — в когда-то давно в качестве платформы для блога сделал самый правильный, как мне сегодня кажется, выбор — генератор статического сайта.
Что ещё? Технология оупенайди, на которой у меня были прикручены комментарии, умерла. Рекапча, которую я прикручивал в комментарии Ире, была куплена Гуглом и ещё пока жива, но уже выпустили вторую версию, и я боюсь, как бы они таки не закрыли первую, как Гугл это умеет делать. Но даже тогда джем, который я использовал, чтобы встроить капчу, волшебным образом исчез из библиотек, и мне пришлось таскать его с собой в папочке vendor. Про вёрстку я даже не говорю.
В общем, для того, чтобы взять и переписать проект с нуля, много ума не нужно. Но сегодня мне интересно покопаться в старье и посмотреть, что ещё из него можно выжать. Поэтому, если вы любите свои старые поделки так, как люблю их я, то очень важно овладеть навыком написания обезьяньих заплаток (monkey patch).
reCAPTCHA v2
Когда я решил использовать новую капчу, я даже не стал искать библиотек. С вероятностью 80% они не заработают на моей старинной системе, и с вероятностью 30% перестанут поддерживаться очень скоро. Просто читаем документацию, встраиваем капчу в форму отправки комментария, а на сервере прямо в методе создания комментария пишем, например:
url = URI.parse('https://www.google.com/recaptcha/api/siteverify')
req = Net::HTTP::Post.new(url.path)
req.set_form_data 'secret' => 'SECRET_KEY',
'response' => params['g-recaptcha-response'],
'remoteip' => request.remote_ip
https = Net::HTTP.new(url.host, url.port)
https.use_ssl = true
https.verify_mode = OpenSSL::SSL::VERIFY_NONE
res = JSON.parse(https.start { |p| p.request req }.body)
if res['success'] ...
И далее с ответом можно делать всё, что угодно. В тонкости уже не вдаюсь, потому что, как может увидеть дорогой читатель, даже хэши у меня написаны с ракетами, а не по-современному с двоеточиями.
Да, это противоречит паттерну MVC, да, это противоречит ООП. Но посмотрите на саму капчу: она не учитывает положения формы на странице, когда открывает своё окно. А также не работает задокументированная фича data-tabindex. Полно хороших библиотек и поделок увядают так и не исправив своих ошибок. Такова реальность программиста сегодня. Красивая библиотека для встраивания в проект на рельсах не сделает эту капчу лучше, но исправлять описанные выше ошибки можно тоже обезьяньими заплатками.
В общем, есть, конечно, определённый кайф в том, чтобы сделать всё максимально по науке и близко к идеалу, но не менее приятно пользоваться смекалкой и собрать что-нибудь из грязи и палок. Безусловно, есть ещё и очень приятное чувство освобождения в том, чтобы разрешить себе делать что-то не идеально.
05.11.2010Ротация логов рельсового приложения
Введение
Это уже давно известная тема, и я не претендую на открытие Америки, но для себя зафиксирую это знание.
Даже если вы используете капистрано для выкладывания проекта в сеть, логи приложения хранятся в одном и том же месте (папка shared/log и разрастаются до огромных размеров. Можно, конечно, запускать после каждого обновления файлов проекта комманду:
rake log:clear
Но есть более цивилизованные методы. Тем более, после определённого времени код проекта начинает обновляться всё реже и реже.
С помощью системы
Существует прекрасный системный инструмент, который назвается logrotate. С его помощью архивируются логи апача, баз данных и даже менеджера пакетов.
Чтобы организовать это удовольствие для своего проекта нужно создать файл /etc/logrotate.d/my_project:
/path/to/my_project/shared/log/*.log {
weekly
missingok
rotate 10
nomail
compress
delaycompress
sharedscripts
postrotate
touch /path/to/my_project/current/tmp/restart.txt
endscript
}
Здесь написано:
- weekly — разбивать лог еженедельно;
- missingok — не выходить с ошибкой, если файла нет;
- rotate 10 — хранить 10 предыдущих томов;
- nomail — не высылать удаляемые тома на электронную почту;
- compress — архивировать;
- delaycompress — архивировать не сразу, т.к. после переименования файла и до перезапуска пэссенджера логи пишутся в тот же переименованный файл;
- sharedscripts — запускать скрипт один раз для всех логов по маске;
- postrotate...endscript — скрипт, который нужно запустить после ротации: в данном случае перезапустить пэссенджер.
Файлом должен владеть root:root. Теперь можно проверить и запустить принудительно, убедившись, что наш файл включается в общий список:
sudo logrotate -dv /etc/logrotate.conf
sudo logrotate -fv /etc/logrotate.conf
С помощью руби
В руби есть встроенный метод ротации логов. Достаточно в файе config/environment.rb написать внутри блока Rails::Initializer.run один из вариантов:
config.logger = Logger.new(config.log_path, "weekly")
или
config.logger = Logger.new(config.log_path, 10, 1.megabyte)
Первый вариант осуществляет ротацию раз в неделю, а второй — по достижении файлом размера в 1 мегабайт и хранит 10 старых томов. Только в данном случае архивацию, если она нужна, придётся организовывать отдельно.
Было бы интересно
Для логротейт можно написать такую маску, которая бы включала в себя все логи всех рельсовых проектов. Но мне неизвестен способ потом написать такой скрипт, который бы перезапускал именно те проекты, для которых была сделана ротация. Например, если логротэйт не нашёл нужного файла, то и скрипт не запустит. А если мы указываем путь типа /path/to/*/shared/.log, то и скрипт должен перебирать все эти проекты и создавать или просто менять дату редактирования файлов restart.txt. Или можно просто перезапускать апач.
Материалы для самостоятельного изучения
23.09.2009Получение irb-консоли в среде приложения
Задача
Допустим, есть приложение, написанное на руби. У него есть основной процесс и есть тесты, которые так или иначе загружают объекты приложения.
Хочется получить доступ в среду приложения в виде irb-консоли, чтобы вручную взаимодействовать с объектами и изменять данные. По типу script/console в rails.
Решение
Как большинство подобных решений, необходимость возникает, когда замечаешь себя за повторением одного и того же набора действий множество раз:
irb
require ...
require ...
...
Раз уж мы захотели как в рельсах, то следует предположить, что окружение нашего приложения загружается одним файлом. Например, config/environment.rb. Это будет первым упрощением многократно повторённого процесса.
Теперь сам файл script/console:
#!/usr/bin/env ruby
APP_ROOT = File.expand_path(File.join(File.dirname(__FILE__), ".."))
libs = " -r irb/completion"
libs << " -r pp" # специально для консоли автоподставнока и pretty print
libs << %( -r "#{APP_ROOT}/config/environment.rb")
ENV["APP_ENV"] = "console" # пример того, как сообщить приложению, что оно в консоли
exec "irb #{libs} --simple=prompt"
Здесь стоит сделать два акцента:
- Для сообщения приложению, что оно запущено в консоли, мы использовали волшебный хэш ENV. К нему потом можно обратиться внутри config/environment.rb и сделать что-то по-другому.
- Для запуска консоли мы использовали Kernel.exec, который не просто выполняет системную команду, но и передаёт ей управление, заменяя текущий процесс.
Теперь, если сделать наш файл запускаемым, будет как в сказке:
chmod +x script/console
Я хочу, чтоб эта песня, эта песня не кончааалась
Если есть приятные библиотеки, которые хочется подключать при каждом запуске irb. А так же, если хочется сохранять историю команд консоли при выходе. То следует воспользоваться мощью ~/.irbrc. Создайте этот файл («~» -- это $HOME, на всякий случай) и напишите в него:
require 'pp' # pretty print
require 'irb/completion' # автоподстановка
require 'irb/ext/save-history' # сохранение истории
ARGV.concat [ "--readline", # не пробовал без readline
"--prompt-mode", "simple" ] # --simple-prompt
IRB.conf[:SAVE_HISTORY] = 25 # сколько сохранять
IRB.conf[:HISTORY_FILE] = "#{ENV['HOME']}/.irb-history" # куда сохранять
Материалы для самостоятельного изучения
30.04.2009Тестирование Paperclip с помощью Factory Girl
Введение
Регулярно в своей работе я встречаюсь с новыми инструментами и решаю разные задачи. Иногда новые решения кажутся незначительными и слишком простыми для того, чтобы посвящать им целый пост в блоге. Но в нашем блоггерском деле главное — это регулярность. :)
Задача
Протестировать вновь изученный инструмент Paperclip с помощью вновь изученного инструмента Factory Girl.
Решение
Допустим, прикрепленный файл называется photo. То есть в модели написано следующее:
class Something < ActiveRecord::Base
has_attached_file :photo
end
Кладем файл с фото, например, в папку с fixtures. Тогда файл test/factories.rb (или spec/factories.rb) будет содержать:
Factory.define :something do |smth|
smth.photo File.new("path_to_file_in_fixtures", "rb")
end
И всё! Теперь в тестах, когда нужно создать объект пишем:
Factory.create(:something)
# или
Factory.build(:something)
А в тестах контроллера пишем:
Factory.attributes_for(:something)
Выводы
Если вы хотите написать проект за короткое ограниченное время, то лучше всего использовать инструменты, которые вы уже опробовали и знаете.
Если вы хотите изучить новые инструменты, то лучше всего сделать небольшой проектик с их использованием.
Проекты в первом и втором случае значительно отличаются по объему. :)
Материалы для изучения
20.04.2009Тестирование OpenID с помощью Cucumber
Задача
Какое-то время назад решил приобщиться к bdd (behavior driven development). С помощью такого подхода очень удобно описывать функционал приложения с точки зрения финального пользователя. И, соответственно, писать код так, чтобы выполнялись сценарии.
Для этого подхода существует прекрасный инструмент Cucumber, сценарии которого пишутся непосредственно текстом. Их, соответственно, можно даже обсуждать в качестве технического задания.
В качестве хорошего примера рассмотрим сценарий комментирования в этом блоге. Комментарии здесь можно оставлять только с помощью идентификации по OpenID.
Ресурсы
Нам понадобится, собственно, Cucumber, который для более лёгкого старта лучше использовать вместе с RSpec (фреймворк для удобного написания тестов и test driven development) и WebRat (инструмент для интегральных тестов, который содержит удобные методы работы с клиентским интерфейсом приложения).
Для работы с OpenID со стороны конечного пользователя нам понадобится локальный тестовый сервер OpenID. Есть хороший вариант: ROTS.
Затребуем библиотеки в тестовом окружении. В файле config/environments/test.rb:
config.gem "rspec", :lib => false, :version => ">=1.2.4"
config.gem "rspec-rails", :lib => false, :version => ">=1.2.4"
config.gem "webrat", :lib => false, :version => ">=0.4.4"
config.gem "cucumber", :lib => false, :version => ">=0.3.0"
config.gem "roman-rots", :lib => "rots", :version => ">=0.2.1"
Установим всё это:
sudo apt-get install libxml2-dev libxslt-dev
gem sources -a http://gems.github.com
rake gems:install RAILS_ENV=test
Решение
Предполагается, что как написать аутентификацию через OpenID мы знаем. Наша задача сейчас только протестировать функционал. В любом случае, когда мы приступаем к данному сценарию, у нас уже должны быть описаны и запрограммированы статьи и хотя бы создана модель для комментариев.
Теперь чтобы начать использовать Cucumber в приложении нужно запустить генератор:
script/generate cucumber
Процесс разработки в bdd выглядит так: сначала пишем сценарий, потом пишем всё остальное. Но в данном посте я просто покажу результат.
OpenID сервер
После установки библиотеки ROTS появляется консольная комманда:
rots
При запуске тестов необходимо, чтобы сервер был запущен. Для работы нам хватит параметров по умолчанию. В данном сервере уже есть пользователь, параметры для которого мы и будем использовать в тестах.
Сценарий Сucumber
Сценарии называются features. Создаем файл features/create_comments.feature:
Feature: Create Comments
In order to give feedback
As a reader
I want to create comments
Scenario: Create Comment
Given I visit a page for the published post with 1 approved comment
When I fill in "OpenID" with "http://localhost:1123/john.doe?openid.success=true"
And I fill in "Имя" with "John Doe"
And I fill in "Текст" with "Nice post!"
And I press "Отправить"
Then I should verify my OpenID
And I should see "John Doe"
And I should see "Nice post!"
And the post should have 2 approved comments
Многие из шагов уже определены с помощью WebRat и RSpec. Доопределим то, чего не хватает.
Шаги для статей
Тут есть моё внутреннее соглашение (с самим собой :). Когда я использую определенный артикль the, то имею в виду статью, которую я запоминаю в течение каждого сценария. В файле features/step_definitions/post_steps.rb:
Given /^I visit a page for the (published|draft) post with (\d+) (approved )?comments?$/ do |pub, cmnts_count, appr|
@post = Post.create!(:title => "Cucumber test post",
:body => "This is test post for cucumber comments",
:published => (pub == "published"))
cmnts_count.to_i.times do |cnt|
comment = @post.comments.build(:name => "Test User", :body => "Test body #{cnt}")
comment.openid_url = "http://test.example"
comment.approved = true unless appr.nil?
comment.save!
end
visit post_url(@post)
end
Then /^the post should have (\d+) (approved )?comments?$/ do |count, appr|
if appr.nil?
@post.comments.count.should == count.to_i
else
@post.approved_comments.count.should == count.to_i
end
end
Шаги для комментариев
Этот шаг специально написан для работы с ROTS, который, не требуя от пользователя никакого интерактивного взаимодействия в процессе теста отвечает на запрос редиректом подтверждая или отвергая авторизацию. В файле features/step_definitions/comment_steps.rb:
Then /^I should verify my OpenID$/ do
response = Net::HTTP.get_response(URI.parse(headers['location']))
response.class.should == Net::HTTPSeeOther
visit response['location']
end
Заключение
Вот и всё. Чтобы запустить тест, нужно выполнить:
cucumber features
Далее, конечно же, следует написать сценарии для пустых полей, для отказа в авторизации, для неправильного адреса OpenID и прочее. Но это, как мне кажется, не составляет труда.
Так же известно, что Cucumber поддерживает и русский язык. То есть сценарии можно писать и на русском. Но я пока не пробовал.
Материалы для изучения
RailsCast про авторизацию с помощью OpenID RailsCast про использование Cucumber Документация по Cucumber
28.03.2009Автосохранение с помощью JavaScript
О стандартизации
Ещё недавно я говорил о том, что не очень люблю JavaScript за то, что результат работает неодинаково в разных браузерах разных версий и ратовал за Flash и иже с ним, как за прекрасную альтернативу. Но судьба предоставила мне шанс убедиться, что и Flash с его ActionScript вполне может себя вести непредсказуемо в различный браузерах и разных версиях плеера, если программа достаточно сложная.
Я сразу же вспомнил о своём научном руководителе, который в школе курировал мою работу для поступления в ВУЗ. В своей работе он использовал FORTRAN. И на наши недоумённые вопросы отвечал, что это чуть ли не единственный язык, для которого можно записать текст программы любой сложности на дискетту и перенести на любой компьютер любой платформы. Видимо, так оно до сих пор и есть.
Задача
Чтобы отдать дань не очень справедливо заклейменному JavaScript, я решил написать для своего блога и для блога Иры автосохранение.
Ресурсы
Для работы нам понадобится Prototype, который входит в стандартную поставку Rails и обязательно крутилочка. Без крутилочки не стоит даже начинать. :)
Решение
Решение оказалось не очень сложное, поэтому я просто его опубликую с дополнительными пояснениями по мере надобности.
config/routes.rb
map.resources :posts, :member => {:autosave => :post}
Контроллер
...
def autosave
@post = Post.find(params[:id])
if @post.update_attributes(params[:post])
render :nothing => true
else
render :nothing => true, :status => 400
end
end
...
Шаблон
Привожу только код JavaScript, который понадобится для сохранения. Рекомендую обратить внимание, на решение проблемы authenticity_token
var cachedBody = $("post_body").value;
var cachedTitle = $("post_title").value;
var saveTimeout = 20000;
var modTimeout = 200;
var requestInProgress = false;
function checkSave() {
if ((((cachedBody != $("post_body").value) && ($("post_body").value != "")) ||
((cachedTitle != $("post_title").value) && ($("post_title").value != ""))) &&
!requestInProgress) {
cachedTitle = $("post_title").value;
cachedBody = $("post_body").value;
$("spinner").show();
requestInProgress = true;
new Ajax.Request("<%= autosave_post_url(@post) -%>", {
method: "post",
postBody: Form.serializeElements([$("post_title"), $("post_body")]) +
'&authenticity_token=' + encodeURIComponent('<%= form_authenticity_token -%>'),
onSuccess: function (transport) {
var now = new Date();
$("spinner").hide();
$("autosave_text").innerHTML = "Saved at " +
(now.getHours() < 10 ? "0" : "") + now.getHours() + ":" +
(now.getMinutes() < 10 ? "0" : "") + now.getMinutes() + ":" +
(now.getSeconds() < 10 ? "0" : "") + now.getSeconds();
setTimeout("checkSave()", saveTimeout);
requestInProgress = false;
},
onFailure: function (transport) {
$("spinner").hide();
$("autosave_text").innerHTML = "Failed to autosave";
requestInProgress = false;
}
});
} else {
setTimeout("checkSave()", saveTimeout);
}
}
function checkMod() {
if (((cachedBody != $("post_body").value) && ($("post_body").value != "")) ||
((cachedTitle != $("post_title").value) && ($("post_title").value != ""))) {
if ($("autosave_text").innerHTML.charAt(0) != "*") {
$("autosave_text").innerHTML = "* " + $("autosave_text").innerHTML;
}
}
setTimeout("checkMod()", modTimeout);
}
function checks() {
checkSave();
checkMod();
}
document.observe('dom:loaded', checks);
В данном случае spinner — это id крутилочки. Крутилочка показывается, когда проходит запрос. Так же здесь имеется дополнительный цикл вызовов. Раз уж мы всё равно храним копию того, что сохранено на сервере, то почему бы не показывать, когда текст уже изменен, но не сохранен на сервере. С помощью звездочки.
А как сделать, чтобы звездочка исчезала, если текст в полях снова стал такой, оставлю на самостоятельное изучение.
Материалы для изучения
Интересный пример ненавязчивого JavaScript для создания «бесконечной» страницы
11.03.2009Использование GEdit для разработки на Ruby и Flex
Сегодня я хочу поделиться своими ресурсами для разработки. В частности, хочу рассказать про IDE. Основную часть рабочего времени я провожу под кубунту. После долгих метаний и проб я остановился на GEdit, как на универсальном средстве разработки.
Установка
Для работы нам понадобятся плагины для GEdit. Они содержат основной набор вкусностей: code snippets, file browser pane, terminal pane и тому подобное. Поэтому устанавливаем:
sudo apt-get install gedit gedit-plugins
Дополнительные файлы, которые понадобятся, находятся в теле поста.
Ruby и Ruby on Rails
Подсветка синтаксиса и сниппеты для ruby уже поставляются из коробки. Единственное, если вы откроете файл со спецификацией подсветки синтаксиса для ruby, то обнаружите там пометку FIXME, касающуюся подсветки интерполяции внутри строки. Это связано с особенностями обработки правил подсветки самим редактором. Эти особенности удалось обойти, присвоив этому правилу измененный цвет фона.
Подсветку синтаксиса Ruby положить в /usr/share/gtksourceview-2.0/language-specs/ Для корректной работы подсветки, потребуется определить стиль для ruby:interpolation. Я использую тему darkmate, в которой определил необходимые дополнительные цвета на свой вкус. Положить следует в /usr/share/gtksourceview-2.0/styles/, а затем выбрать эту тему в установках редактора.
Язык для описания подсветки синтаксиса достаточно прост. Кроме всего прочего он позволяет ссылаться из правил одного языка на правила другого. Что, собственно, нам и понадобится для подсветки языка темплейтов erb. Ведь erb — это по сути html со вставками ruby. Теперь, когда у нас есть оба описания, берём описание для html и вставляем в него ссылку на ruby.
Так же следует определить mime-type для erb. Правила подсветки erb предполагают, что в системе определен mime-type text/erb. В четвертом KDE описать свои типы файлов можно в System->System Settings->Advanced->File Associacions
Мой файл использует цвет erb:background, который так же определен в файле с темой.
Подсмотрев code snippets для других языков, а так же в процессе использования, вполне можно самостоятельно написать пару-тройку полезных выражений.
Flash и Flex
Для работы с flex нам понадобится flex sdk, а так же отладочная версия standalone flash player. Я уж не буду вдаваться в подробности, как там написать компилирующий или запускающий скрипт. А выложу сразу подсветку синтаксиса.
Если определить тип (mime-type) text/x-shockwave, то можно использовать правила для подсветки ActionScript3. Которые следует положить по адресу, указаному выше.
Так же, используя описанное выше знание, легко создать описание подсветки синтаксиса mxml, зная, что это xml, со вставками actionscript. Для корректной работы следует определить mime-type text/mxml.
Заключение
Все файлы, выложенные в посте, можно смело изменять под свои нужды. Если вы используете GEdit, чтобы писать под рельсы или флекс. Я не стал выкладывать сниппеты, потому что на мой взгляд, это вопрос личной привычки. А подсветку синтаксиса, конечно, можно улучшать.
24.02.2009Регулярные выражения: радость победы 2 :)
Ещё один бонус, который я ожидал от этого блога, и который уже успел получить — это обратная связь. После разговора с Лёшей Кукушкиным было решено модифицировать задачу из предыдущего поста так, чтобы стало удобнее.
Задача
- Ввести дополнительный тэг для кода
- Печатать код внутри тэга так, как он должен выглядеть (прошлый раз пришлось шаманить)
- Иметь возможность более ли менее безнаказанно употреблять сами тэги в тексте
Решение
Итак, для кода будем использовать тэг [code]...[/code]. Для того, чтобы не провоцировать преобразование кода там, где не нужно, будет использовать знак «/». (То есть в этом абзаце жирным на самом деле написано «/[code]...[/code]»)
Так же используем стандартный метод rails для приведения в порядок того, что находится внутри тэга [code]. Результат выглядит вот так:
def lonelyelk_format(text)
res = "<p>" + text.to_s.dup
codes = []
res.gsub!(//[^\/]\[code\]([\s\S]+?)\[\/code\]/) do |s|
codes.push(s.gsub(/(^[^\/]\[code\]|\[\/code\]$)/, ""))
"#{s[0,1]}[code#{codes.length - 1} /]"
end
res.gsub!(/\r\n?/, "\n")
res.gsub!(/\n*\[h\]\n*/, "</p><h2>")
res.gsub!(/\n*\[\/h\]\n*/, "</h2><p>")
res.gsub!(/\n\n+/, "</p><p>")
res.gsub!(/\n(?=\[code\d+\s\/\])/, "</p><p>")
res.gsub!(/(\[code\d+\s\/\])\n/, '\1</p><p>')
res.gsub!("\n", "<br />")
res += "</p>"
res.gsub!(/<p>\[code\d+\s\/\]<\/p>/) do |s|
"<pre><code>#{h codes[s.gsub(/\D+/, '').to_i]}</code></pre>"
end
res.gsub!("<p></p>", "")
res.gsub!("//[", "[")
res
end
Итоги
По результатам могу сказать, что код ещё можно улучшать и дорабатывать для разных целей. Чем, безусловно, мне ещё предстоит заниматься. Но писать об этой задаче больше не буду. Есть много другого интересного, о чём можно поговорить.