Pinto公開に向けて #5 ― Erubisでビュー周りを実装した
あらすじ
いじり始めたばかりのRubyで、Pintoというソーシャルブックマークサービスを作ろうとしています。前回はコントローラ周りを実装しました。今回はErubisを使ってビュー周りを実装したのでその報告です(誰に)。
Webアプリのセキュリティについても少し触れますよ。
表示テスト用のテンプレート
まず、eRubyベースのテンプレートを view/script/top.erb に作成しました。あ、「まず」とかいってますが、テンプレート作成に至るまでには、かなり試行錯誤したんですよ。そのまま書くと文章が納豆スパゲティ状態になるので、分かりやすさ優先のつもりで書きますね。
さて、ファイルの中身はこうです。
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja"> <head> <title>Pinto</title> </head> <body> <p style="color:<%== in_quote @color %>"><%= @greeting %></p> <%== has_tag @address %> </body> </html>
in_quote と has_tag は私の独自拡張です。「<%= @greeting %>」では「&<>」をエスケープ、「<%== in_quote @color %>」ではおまけに「"」もエスケープ、「<%== has_tag @address %>」はエスケープなし、という感じです。
@greeting でも「"」をエスケープして良いのですが、そこをしないのがプログラマとしての矜持というか、単なるへそ曲がりというか。
コントローラからの呼び出し
続いて、コントローラからビューを扱う部分を実装しました(ということにさせて)。
# lib/pinto/controller/top.rb require 'pinto/view' module Pinto module Controller class Top def self.run(request) param = { :greeting => '"<こんにちはRuby>"', :color => 'red', :address => '<address>id:IwamotoTakashi</address>' } response_body = Pinto::View.render('top', param) return [ 200, {'Content-Type' => 'text/html; charset=UTF-8'}, [response_body] ] end end end end
見ての通り、ビューオブジェクトにビュー名とパラメータを渡して render しているだけですね。
ビューオブジェクト
ビューオブジェクトはこんな感じです。
# lib/pinto/view.rb require 'pinto/view/xhtml' module Pinto class View def self.render(view, param = {}) template = File.read("../lib/pinto/view/script/#{view}.erb") return Pinto::View::Engine::XHTML.new(template).evaluate(param) end end end
XHTML用テンプレートエンジン
XHTML用テンプレートエンジンは Erubis::EscapedEruby を継承させました。
# lib/pinto/view/engine/xhtml.rb require 'erubis' require 'pinto/view/engine/xhtml/context' module Pinto class View class Engine class XHTML < Erubis::EscapedEruby def escaped_expr(code) return "h(#{code})" end def evaluate(param) super Pinto::View::Engine::XHTML::Context.new(param) end end end end end
escaped_expr はエスケープ用のメソッドで、Erubis::EscapedEruby#escaped_expr をオーバーライドしています。こいつのおかげで「<%= @greeting %>」が「h( @greeting )」に置換されます。この h は、Pinto::View::Engine::XHTML::Context#h を指します。
evaluate もオーバーライドです。独自のコンテキストオブジェクトを渡すためですが、それはテンプレートで in_quote と has_tag を使いたいからです。
コンテキストオブジェクト
独自のコンテキストオブジェクトはこんな感じです。
# lib/pinto/view/engine/xhtml/context.rb require 'erubis/context' require 'pinto/encoding/utf8' module Pinto class View class Engine class XHTML < Erubis::EscapedEruby class Context < Erubis::Context ESCAPE_TABLE = { '&' => '&', '<' => '<', '>' => '>', '"' => '"' } def escape_for_value(value) escape_chars = ['&', '<', '>'] return eliminate(value, escape_chars) end def escape_for_quote(value) escape_chars = ['&', '<', '>', '"'] return eliminate(value, escape_chars) end def escape_for_tag(value) return eliminate(value) end alias h escape_for_value alias in_quote escape_for_quote alias has_tag escape_for_tag def eliminate(value, escape_chars = nil) str = value.to_s raise ArgumentError unless Pinto::Encoding::UTF8.valid? str control_codes = / [\x00-\x08]|\x0B|\x0C|[\x0E-\x1F] # C0 control codes |\x7F # DEL |\xC2[\x80-\x9F] # C1 control codes /ux str.gsub!(control_codes, '') return str unless escape_chars.is_a? Array return str.gsub(/[#{escape_chars.join}]/) {|s| ESCAPE_TABLE[s]} end end end end end end
各種エスケープメソッドですね。土台となる eliminate メソッドでは下記の処理をおこなっています。
1はセキュリティを考慮しています。(X)HTML内に不正シーケンスが含まれると脆弱性の原因となりえるので。
2はXMLの仕様を考慮しています。XML 1.0では、C0制御文字集合(#x00〜#x1F)のうち、#x09(HT、いわゆるタブ文字)、#x0A(LF)、#x0D(CR)以外はすべて使用不可と決められているので、それを削除しているわけです。\x7F(DEL)および C1制御文字集合(\xC2\x80〜\xC2\x9F)は使用可能なのですが、XHTMLに残しておく意味はないので、合わせて削除しています。
UTF-8の不正シーケンスチェック
UTF-8の不正シーケンスチェックは下記のようにおこなっています。
# lib/pinto/encoding/utf8.rb module Pinto module Encoding class UTF8 def self.valid?(value) valid_utf8 = /^(?: [\x00-\x7F] # U+0000 - U+007F |[\xC2-\xDF][\x80-\xBF] # U+0080 - U+07FF |\xE0[\xA0-\xBF][\x80-\xBF] # U+0800 - U+0FFF |[\xE1-\xEC][\x80-\xBF]{2} # U+1000 - U+CFFF |\xED[\x80-\x9F][\x80-\xBF] # U+D000 - U+D7FF |\xEF[\x80-\xBF][\x80-\xBD] # U+E000 - U+FFFD |\xF0[\x90-\xBF][\x80-\xBF]{2} # U+10000 - U+3FFFF |[\xF1-\xF3][\x80-\xBF]{3} # U+40000 - U+FFFFF |\xF4[\x80-\x8F][\x80-\xBF]{2} # U+100000 - U+10FFFF )*$/x return !value.to_s.match(valid_utf8).nil? end end end end
弾さんのコードそのままですね。
次のアクション
- トップページをちゃんと書く(多言語を考慮、やっぱり gettext かな)
最後までお読みいただきありがとうございました。