T

SPIKEのWebhookをRailsで受け取る

久しぶりにブログでも書こう。

手数料無料の驚きのカード決済サービスSPIKE、使わない手はないということで試してみたのだけれど、RailsでWebhookを受け取るとエラーになってしまう。

Error occurred while parsing request parameters.
Contents:

なんかURLエンコードされた文字列

JSONをPOSTされた際にうまくパースできないようなのだが、コントローラで受けたときにはもうエラーになっているのでアプリケーションのレベルでは対処しようがない。結局、こちらを参考にモンキーパッチで動かしてみたのだけれど、もっと楽なやり方があれば教えてもらいたい。

まずはinitializerを用意して、特定のページへのアクセスでデフォルトのパーサを迂回させる。

# config/initialize/webhook.rb
# https://github.com/intridea/grape/issues/340
require File.dirname(__FILE__) + '/../../lib/webhook_parser.rb'

MyApp::Application.config.middleware.swap ActionDispatch::ParamsParser, MyApp::ParamsParser, :ignore_prefix => '/path/to/webhook'

これでwebhookへのアクセスのときはlib/webhook_parser.rbが呼び出されるようになった。

#lib/webhook_parser.rb
# https://github.com/intridea/grape/issues/340
module MyApp
  class ParamsParser < ActionDispatch::ParamsParser
    def initialize(app, opts = {})
      @app = app
      @opts = opts
      super
    end

    def call(env)
      if @opts[:ignore_prefix].nil? or !env['PATH_INFO'].start_with?(@opts[:ignore_prefix])
        super(env)
      else
        @app.call(env)
      end
    end
  end
end

これでSPIKEからのWebhookをエラー無しにコントローラで処理できるようにはなった。

  #webhookを受け取るコントローラ
  def webhook
    # SPIKEから送信されるrequest bodyから余計なバックスラッシュと先頭と最後のダブルクォートを消す
    parsed_request_body = JSON.parse(URI.decode(request.body.read).match(/\A"(.+)"\Z/)[1].gsub(/\\/, ''))
    # あとはお好きなように
  end

rbenvでcron

しょっちゅう忘れるのでメモ。rbenvの環境でRubyをcronから実行すると、細かい実行環境の違いで失敗する。なので、こんな工夫が必要になる。

42 4 * * * /bin/bash -c 'export LANG=ja_JP.UTF-8;export PATH="$HOME/.rbenv/bin:$PATH";\
 eval "$(rbenv init -)";ここから実際にやりたいことを書く'

* 実際には一行で。

まず-cでbashを続く文字列から読み込んで実行するモードにする。LANGの設定をここかcrontabの先頭で指定しておかないとRubyのスクリプトの先頭に「# -*- coding: UTF-8 -*-」と書いていても非ASCII文字がどうたらというエラーになる。PATHも同じで、インラインで書くなら上みたいな感じにする。あとはおまじないだと思いましょう。

register_globals?そんなのデフォルトでオフですよ

Ruby on Railsのmass assignment絡みの脆弱性について、すごくよくまとまった記事。Railsで開発している人は必見ですね。

かつてPHPにはregister_globalsというこれと似た機能があり、現在はデフォルトでオフかつ5.4以降は廃止されたのですが、それまでは散々恐ろしい問題を引き起こしていました。PHPのウェブアプリケーションにセキュリティ問題が多いという評判が定着してしまったのは、これが主な原因といっても過言ではありません。PHPは何年もかけてようやく負の遺産を返済することができたのですが、Railsにはこのregister_globals相当の機能があり、今回見つかった脆弱性はかつてPHPが経験したことをそのままなぞっているようで興味深いところです。

ちなみに、Railsの本と名乗っていながら冒頭からRubyのオブジェクトの仕組みからメタプログラミングについて詳細に解説しまくる名著『実践Rails』では(初版なら)142ページ目にこの問題についての解説があります(Safari Onlineはこちら)。さすがです。

というわけで、何年も前に指摘されていた脆弱性に今頃慌てふためいている愚かでコンピュータサイエンスを学ぶには無能すぎてRailsくらいしか使えないくせにPHPerだなんだと他人の尻馬に乗って調子こいてたボケナス共は、土下座してmodelの修正に取りかかるがよい。わっはっは。

Lavishでカラースキーム

画像のURLを指定すると、画像に使用されている主な色をprizmで解析、そこからカラースキームを作ってウェブページのテンプレートを吐き出す「Lavish」というRailsアプリケーションをTwitter経由で知ったので試してみたが、素晴らしかった。使い方は画像のURLをフォームに入力して「Go Lavish」ボタンをクリックするだけ。

モノクロ画像だとレイアウトがおかしくなったり、まだまだ熟成していないプロジェクトではあるが、開発者がちょっとしたデザイン仕事をするのには強力な手助けになってくれる。

ちなみにprizmもLavishも同じQuan Nguyenさんが作っている。素晴らしい。

Rails3 + Sorceryで複数のモデル(管理者と一般ユーザとか)を扱う

Deviseが魔術すぎるので一部のユーザには人気のあるシンプル指向のユーザ認証用プラグインSorceryですが、例えば「管理者」と「一般ユーザ」みたいに複数の種類があるアカウントをそれぞれ別のモデルとして扱うのはあまり得意ではありません。どのウェブアプリケーションでも普通はこれくらいのアカウント種別はあるでしょうし、もし一般ユーザに管理側の機能やアカウントが使えるようになってしまっては困るので、うっかりミスでとんでもないことにならないよう、それぞれ別のモデルで扱いたくなるのは当然です。これが出来ないのであれば、折角シンプルさが気に入ってSorceryを使い始めても、すぐに挫折してしまいます。大変残念です。

でも、まあ確かに得意じゃないとはいっても、別に出来ないというわけではありません。Sorceryで複数のモデルで複数のアカウント種別を扱う最も一般的なやり方は、Single Table Inheritance(単一テーブル継承)を利用するものです。備忘録として使い方をまとめておきます。

単一テーブル継承といっても、別にPostgreSQLにあるようなテーブルの継承を利用するような高度なものではありません。単純に、同じテーブルにデータを格納しつつ、typeカラムに入った文字列でアカウントの種別を判定する、というだけのことです。データとしてはそんなしょぼい構成になりますが、アクセスするモデルを別にすることができるのが利点です。

ややこしいのでモデルの例で説明すると、

$ bundle exec rails g sorcery:install

これでusersテーブルを扱うUserモデルが作成されます。これにtypeカラムを追加します。

$ bundle exec rails g migration addTypeToUsers type:string
$ bundle exec rake db:migrate

さて、これで親となるモデル「User」が完成しました。Sorceryのマニュアルに従い、app/models/user.rbを編集してSorceryで使えるようにしましょう。initializerの設定も忘れずに。config.user_classはUserで構いません。ひとつだけ、config/initializers/sorcery.rbで継承の利用をtrueにしておくことをお忘れなく。

user.subclasses_inherit_config = true

次に、これを「継承」した子モデルを作成します。そうですね、memberとadminという名前にしましょうか。

railsのジェネレータで作成したモデルはActiveRecord::Baseを継承しています。でもSingle Table Inheritanceのモデルは親テーブルを扱うクラスを継承します。これにより親テーブルのvalidationなどの情報を引き継ぐことができます。またSorcery固有のauthenticates_with_sorcery!も子テーブルでは呼ぶ必要はありません。

class Admin < User
end

class Member < User
end

これで何が出来るようになるかというと、Sorceryの各種メソッドが使えるようになったのはもちろん、例えば

@member = Member.new(params[:member])
@member.save

これでtypeカラムに「Member」という文字列が入った状態でデータが保存されます。保存されるテーブルは「users」です。同じくAdminモデルから作成したオブジェクトではtypeカラムに「Admin」という文字列が入ります。

またそれぞれのモデルからデータを引っ張り出すときには、勝手にtypeカラムの内容を検索条件に追加してくれます。

User.find(params[:id])
 #=> SELECT `users`.* FROM `users` WHERE `users`.`id` = 2 LIMIT 1
Member.find(params[:id])
 #=> SELECT `users`.* FROM `users` WHERE (`users`.`id` = 2 AND `users`.`type` IN ('Member')) LIMIT 1
Admin.find(params[:id])
 #=> SELECT `users`.* FROM `users` WHERE (`users`.`id` = 2 AND `users`.`type` IN ('Admin')) LIMIT 1

あとは、各コントローラでAdminかどうか認証する場合は、application_controller.rbに

  protected
  def is_admin?
    if current_user && current_user.type == 'Admin'
      true
    else
      false
    end
  end

  def admin_required
    unless is_admin?
      redirect_to root_path, :alert => "You are so cool..."
    end
  end

こんな感じのフィルタ用メソッドを用意しておけば、管理者のみアクセスさせたいコントローラで

class TopSecretsController < ApplicationController
  before_filter :admin_required

アクセス制限をかけることができるようになります。「Admin」という文字がハードコーディングされているのがキモいので定数にしておくといいでしょう。

RubyKaigi ticket available (3 days)

=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=
UPDATE: 閉め切りました。こちらの方が一番早かったので決まりました。PHPのテストについてのエントリとか、いろいろ参考になります。当日は楽しんできてください。
=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=

RubyKaigiのチケット余ってます。3日間の通しで参加できます。

時間もないし、是非誰かに参加してもらってRubyの世界に触れてほしいので、以下の条件で譲ります。

(1)学生、生徒は無料
(2)仕事してる人は払える分だけ
(3)下記の特殊な条件を満たす方は無料

余っているチケットはこちらです。ただし譲渡の方法がわからないので、わかり次第お知らせします。先着1名様のみ。

特殊な条件は以下の通りです:

  • ヨルダンの王妃であらせられる
  • ジェダイマスターである/だった
  • 綾瀬はるかその人

応募方法は以下の通りです:
(1)lokka.orgで20行以上コミットして「is rubykaigi ticket still available?」とコメントする
(2)yagitoshiroアットマークgmail.comにメールする
(3)星に願う

いずれの方法でもぼくに最初に連絡がきた方を優先します。ただし下記の条件に当てはまる場合はお断りさせて頂くことがあります。

  • すでに死んでいる方
  • ダフ屋行為をして目立とうと目論む方
  • Rush Limbaugh

lokkaでFerret

みんな大好きlokkaでFerretによる全文検索プラグインを作ってみました。

https://github.com/yagitoshiro/lokka-ferret

クラウド環境での動作を前提にしている、というかみんなが愛してやまないHerokuでの動作を前提としているlokkaなので、MeCabで分かち書きというわけにもいかないですから、Yahoo! Japanの形態素解析サービスを利用できるオプションもつけてみました。アプリケーションIDを登録すると使えるようになります。

もちろん、Herokuでない環境でMeCabが使える人は、Gemfileの一行目をコメントアウトしてくれたらMeCabのRuby用バインディングをインストールするようになっているので、そちらを使ってください。

ferret-lokka.png

Ferretのインデックスは、/tmp/ferretとか適当な場所を指定すればいいと思います。上の例ではlokkaのトップディレクトリにしれっと置いてあります。

typusええのう

Ruby on Railsである程度アプリケーションが出来てきた(モデルの作成が終わっている)なら、面倒な管理画面はtypusで作ってしまうと楽だった。Gemfileに「gem ‘typus’」を追加してbundle installしたら

$ rails g typus

これで管理画面のセットを作成する。

$ rails g typus:migration
$ rake db:migrate

これでパスワード認証の仕組みを作成する。

$ rails g typus:views

これでview関連のファイルをapp/views以下に設置していじりたくなったらいじる。

generatorの詳細は:

$ rails generate typus:migration -h

rakeタスクの詳細は:

$ rake -T typus

Lokkaでスタティックな変数みたいなものを

Lokkaのプラグインを作成していて、スタティックな変数が欲しいなあと思った。

例えば、メニューのリストがあって、複数のプラグインからリストに値を追加したい場合。というとややこしいので、

class Menu
  @menu_list = []
  def self.menu_list=(name)
    @menu_list << name
  end

 def self.menu_list
    @menu_list
  end
end

Menu.menu_list = 'hoge'
p Menu.menu_list #=> ["hoge"]
Menu.menu_list = 'fuga'
p Menu.menu_list #=> ["hoge", "fuga"]

普通のRubyのプログラムであればこんな感じになるケース。

SinatraをベースにしたLokkaの場合、プラグインとgetなどのアクセスできるスコープが異なる箇所にインスタンス変数やクラス変数を使うとしたら、configのsetを利用するのが手っ取り早い。

configure do
  set :foo => :bar
end

get '/hoge' do
  "#{settings.foo}" #=> "bar"
end

単純な値であればこれで事足りるわけだが、例えば複数のスコープになってしまう箇所でこの変数(例えば配列)に値を追加していきたい、なんて場合はconfigureのブロック内で

configure do
  My_Struct = OpenStruct.new(:list => [])
  set :menu_list => Proc.new{|menu| My_Struct.list << menu unless My_Struct.list.include? menu}
end

としておいて、

before do
  settings.menu_list 'もんじゃ焼き'
end

get '/hoge' do
  settings.menu_list 'たこ焼き'
  My_Struct.list.each do |menu|
    "#{menu}" # => 'もんじゃ焼き'と'たこ焼き'
  end
end

とやれば、スタティック変数みたいに扱うことができる。以上。

rails 3のroutes.rbからいちいちモデルを呼ぶ

rails 3のroutes.rbで、パフォーマンスという面ではいかがなものかと思うけど、面白いことができた。

例えば、Categoryクラスがあって、それはモデルで、データとして「野球」「サッカー」「盥回し」みたいなカテゴリが登録されているとする。カテゴリ名はslugという別名をもっている。野球だったらbaseball、サッカーだったらfootball、盥回しだったらspin_basinとか。

rails generate model category name:string slug:string

で、突然SEOコンサルなる人が現れて、ほぼ構築が完了したこのサイトには/baseballとか、/footballとかの素敵なURLが必要、といわれてしまう。当然、/categories/1なんてのは却下だ。

やけっぱちになったエンジニアは、どうせこんな間抜けなコンサルを雇ってるバカなサービスは人気も出ないまますぐ潰れるだろう、とサイトのパフォーマンス低下は無視することにして、config/routes.rbに下のようなコードを挿入した。

Category.all.each do |cat|
  match "#{cat.slug}" => 'categories#view', :id => cat.id
end

こんなのでも動くんだね。