2012-10-26
■[Ruby] sinatra-formkeeper
最近sinatraを触り始めた。
rubyではmodelでvalidationをする文化があるが、いくつか理由があって、自分はcontrollerの入り口でvalidationする方が好きだ。
ということで、sinatra-formkeeperというライブラリをざっくり作ってリリースしてみた。
https://github.com/lyokato/sinatra-formkeeper
sinatra用のプラグインだが、コアの機能はformkeeperという別のgemにしてある。
また、formkeeper-japaneseという日本語用のプラグインも別に用意してある。
これについては後で説明する。
それぞれrubygemsにリリース済なのでgemコマンドでインストールできる
gem install sinatra-formkeeper gem install formkeeper-japanese
主要な機能は次の三つ
- filtering
- validation
- fill in form
使い方
簡単な例を次にのせる。見れば何となく雰囲気は分かると思う。
require 'sinatra/formkeeper' post '/sign_up' do form do filters :strip, :my_filter field :username, :present => true, :length => 4..8 field :mail_address, :present => true, :email => true, :length => 0..255 field :age, :present => true, :int => { :gte => 18 } field :password01, :present => true, :length => 4..8 field :password02, :present => true, :length => 4..8 same :same_password, [:password01, :password02] end if form.failed? "signup failed" else "singup success " + form[:username] end end
1. まず最初にformブロックを用意し、そこにDSLでルールを定義していく。ルールの定義方法の詳細は後で説明する。
2. 次に form.failed? で、ユーザーの入力値がルールを満たしたかどうかを確認する
formの検証に問題がなかった場合
if form.failed? # ... else username = form[:username] psasword = form[:password] email = form[:mail_address] end
当然、入力された値を利用して何らかの処理をしていくわけだが、そのときに、上記のように form[:field_name] という形で値が取れる。paramsで取るのと何が違うのかというと
- 検証済のフィールドの値しか入っていないので、うっかり未検証の値を使うことはない
- ルールブロックで設定したフィルタが実行済である
であるという二つの点がある。
formの検証に問題があった場合
入力値に問題があった場合は、もう一度入力フォームの画面を表示させて、入力値の何がいけなかったか、というエラーメッセージを添えるのが一般的だろう。
次の例のように、form.failed_on? を利用できる
post '/sign_up' do form do #... end if form.failed? erb :signup else # ... end end __END__ @@signup <html> <body> <% if form.failed? %> <p>入力値を修正してください。</p> <ul> <% if form.faled_on?(:username) %> <li>名前の入力に問題がありました</li> <% end %> <% if form.failed_on?(:password) %> <li>パスワードの入力に問題がありました</li> <% end %> </ul> <% end %> <form> <!-- ... --> </form> </body> </html>
failed_on? にフィールド名を渡すと、そのフィールドが検証に失敗したかどうか判定できる
<% if form.faled_on?(:username) %> <li>名前の入力に問題がありました</li> <% end %>
もう少し細かいメッセージを出すために、次のように、
フィールド名に続き制約の種別も指定できる。
<% if form.faled_on?(:username, :present) %> <li>名前の入力がありません</li> <% end %> <% if form.faled_on?(:username, :length) %> <li>名前は、4文字以上、8文字以下で入力して下さい。</li> <% end %>
このようにHTMLテンプレートの中に、すべての項目に対するエラーメッセージを書き込んでいくのは非常に手間になる。
メッセージハンドリング
メッセージはYAMLなどのファイルにまとめることが出来る。
--- messages.yaml signup: username: present: input name! length: intput name (length should be between 0 and 10) email: DEFAULT: input correct email address post_entry: title: present: Title not found DEFAULT: username: present: username not found -- ...
一番上の階層は「アクション名」であり、それっぽい名前を好きに付ける。二階層目がフィールド名で、三階層目が制約の名前になる。このように、それぞれの制約に対応するメッセージをリストアップしておく
まずは、このファイルをsinatraアプリケーションから読み込んでおく必要がある。form_messagesメソッドに、ファイルパスを指定する。
form_messages File.expand_path(File.join(File.dirname(__FILE__), 'config', 'form_messages.yaml')) post '/entry' do #... end
HTMLテンプレートから次のように、まとめて呼ぶことが可能になる。form.messagesは、全ての失敗したフィールドと制約に紐づくエラーメッセージを、YAMLから検索してリストを返す。その際に、YAMLで指定したsignup, post_entryなどのアクション名を指定する。エラーメッセージは、該当するアクションのブロックから検索される。該当するアクションや該当する制約が存在しなかった場合、DEFAULTが利用される。制約ごとに別々のメッセージを用意する必要ない場合は、DEFAULTを利用するとよいだろう。
<html> <head><title>Entry</title></head> <body> <% if form.failed? %> <ul> <% form.messages(:post_entry).each do |message| %> <li><%= message %></li> <% end %> </ul> <% end %> </body> </html>
HTMLの上部にまとめてメッセージを羅列するよりも、一つ一つの入力フォームの側にそのフォームの問題を表示してあげるほうが優しいかもしれない。
次のように、form.messagesには、フィールド名を限定して、メッセージのリストを取得する機能もある。
<html> <head><title>Entry</title></head> <body> <form> <% if form.failed_on?(:username) %> <ul> <% form.messages(:login, :username).each do |message| %> <li><%= message %></li> <% end %> </ul> <% end %> <label>username</label><input type="text" name="username"> <% if form.failed_on?(:password) %> <ul> <% form.messages(:login, :password).each do |message| %> <li><%= message %></li> <% end %> </ul> <% end %> <label>password</label><input type="text" name="password"> </body> </html>
入力値の自動埋め込み
入力フォームを表示するとき、ユーザーが前回入力した値で、フォームを埋めておいたほうがよいだろう。
問題なかった値は再度入力する手間がはぶけるし、問題のあったフィールドにおいては、自分が何を入力したために問題になったのか、エラーメッセージとともに、自分の前回の入力値を比較して、問題を認識しやすくなるだろう。
次のようにfill_in_formメソッドに出力値を渡すと、paramsの中の値を使ってフォームの値を埋める。
post '/entry' do form do #... end if form.failed? output = erb :entry fill_in_form(output) else #... end end
CSRF対策のトークンを発行しなおしたりするなど、paramsの値をそのまま使いたくない場合は、上書きするパラメータをハッシュとして渡す。
output = erb :entry
fill_in_form(output, { "token" => new_token } )
おおまかな処理の流れは以上のようになる。
ルールの作成
フィルタについて
form do filters :strip, :downcase field :username, :present => true end
まずは、フィルタについて説明する。
上記のように、formルールのブロックないでfiltersメソッドを読んで、フィルタを指定できる。:stripを指定すると、入力値の前後の空白を取り除いてから検証を行うし、:downcaseは入力値を全部小文字にしてから検証を行う
form[:username]
検証成功後、このようにformを通して値を取ると、strip & downcase済の値が取れる。
上記のようにすると、全ての入力値にstrip & downcaseのフィルタがかかる。
特定のフィールドのみにfilterを指定したい場合は次のように出来る。
form do field :username, :present => true field :password, :present => true, :filters => [:strip, :downcase] end
一個だけなら配列じゃなくてよい。
form do field :username, :present => true field :password, :present => true, :filters => :strip end
「空白を入力しないでください」とか「全て小文字で入力してください」とかバカバカしいエラーメッセージを表示して、再度入力という煩わしい思いをユーザーにさせずに済む。「全角数字で入力してください」などのエラーは今でもよく見かけて非常に苛立つ。
formkeeper-japaneseでは、このためのフィルタが用意されている。
require 'sinatra/formkeeper' require 'formkeeper/japanese' form do filters :zenkaku_to_hankaku field :field01, :present => true, :int => true end
japaneseプラグインには、ほかにもhankaku_to_zenkaku, katakana_to_hiragana, hiragata_to_katakanaなどのフィルタがある。
サーバー側の実装ではあるものの、これもある種のUIエンジニアリングであり、プログラムでよしなにサポートできることはしてしまい、エンドユーザーの労力を最小化させるのがよいと思う。
カスタムフィルタ
次のようにお手軽にフィルタを書いて使うことも出来る。
form_filter :my_capitalize_filter do |value| value.capitalize end post '/entry' do form do filters :my_capitalize_filter end end
文字コードについて
2012年にこんなものが必要かどうかも分からないし、Rack Middlewareでやるべきかもしれないが、一応文字コードのサポートも用意しておいた。
次のように、encodingメソッドで文字コードを指定しておくと、入力値を、指定された文字コードからUTF-8に変換する。
form do encoding 'EUC-JP' field :field01, :present => true, :hiragana => true end
制約について
入力フィールドごとにルールを指定していく。
fieldメソッドに、最初の引数としてフィールド名をわたして、そのあとにハッシュ形式で、制約を並べていく。
上述のように、filtersの設定をここに混ぜることが出来る。
form do field :field01, :present => true, :length => 0..10 end
見ればなんとなく雰囲気は分かると思うが、
特殊な制約があるので、それについて説明する
present
入力値が存在するかどうかを判定する。
他の制約に先立って、一番はじめに検証が行われる。
この制約が設定されていて入力値が存在しなかった場合は、この制約の検証に失敗した旨が記録され、以降の制約については検証しない。
この制約が設定されておらず、入力値も存在しなかった場合、以降の制約について検証しないが、検証は成功と記録される。
default
presentの制約が設定されていない場合のみ指定できる。
入力値が存在しなかった場合、このdefaultで指定された値が使われる。以降の制約についての検証は行わず、検証は成功と記録される。
form do field :field01, :default => 'hogehoge', :length => 0..10 end
その他の制約
プリセットとしていくつか用意されている。
詳しくは元のドキュメントを。
https://github.com/lyokato/sinatra-formkeeper
- length
- bytesize
- regexp
- int
- uint
- alpha
- alpha_space
- alnum
- anum_space
- uri
japaneseプラグインには
- kana
- katakana
- hiragana
などの制約が用意されている。
selection
フィールドが複数値を持つ場合
たとえば
<input type="checkbox" name="field_name[]" value="1" checked> <label>check1</label> <input type="checkbox" name="field_name[]" value="2" checked> <label>check2</label> <input type="checkbox" name="field_name[]" value="3" checked> <label>check3</label>
こういうのとか
<select name="favorite[]" multiple> <option value="1" selected="selected">white</option> <option value="2">black</option> <option value="3">blue</option> </select>
こういうのとか。
フィールド名を 'foo[]' のように配列にしておくとparam['foo']で配列で取れる。
そういうタイプの検証は次のようにする。
form do selection :favorite, :count => 0..2 end
fieldの代わりにselectionで指定する。presentの代わりにcount制約を使う。
count制約には、数値やRangeでの指定が出来る。
コンビネーション
複数フィールドを組み合わせて一つの検証をするパターン。
フォーム入力によくあるパターンとして、パスワードやe-mailアドレスを二回入力させ、同じ値が入力されたかを確認する、というものがある。
formkeeperではコンビネーションという機能がある。
combination(ルール名、:fields => [対象となる複数のフィールド], 制約名 => true)
メールアドレスの例を実装すると次のようになる。
form do combination :same_address, :fields => ["email01", "email02"], :same => true end if form.failed_on?(:same_address) "アドレスが同じじゃないよ" end
ほかにも「どれか一個でも入力されてたらオッケー」な any とか
form do combination :favorite_color, :fields => ["white", "black", "blue"], :any => true end
あとdateとかtimeとかdatetimeもある。
コンビネーションにはシンタックスシュガーがあり次のように簡単に書くことが可能
form do same :same_address, ["email01", "email02"] any :favorite_color, ["white", "black", "blue"] end
制約名をメソッド名として呼び、ルール名、ターゲットとなるフィールド名リスト、と続く。
まとめ
とりあえずこんな感じ。
まだ作ったばっかなので、robustでないし、ちょくちょく機能足したり、細かいところを変更したりはするかも。
2005年にPerlでFormValidator::Simple, FormValidator::Simple::Plugin::Japanese, Catalyst::Plugin::FormValidator::Simple
を書いたのを思い出しデジャヴ感。
2012年に同じようなことをしていて進歩してない感じが悲しい。
でもFVSには無かったfilter機能はたぶん便利。
2011-12-29
■[Objective-C][C] C/Objective-C GeoHash Library
GeoHashの勉強がてら、Cでライブラリを書いて、
さらにそのObjective-C wrapperも用意してみた。
C版
https://github.com/lyokato/libgeohash
Objective-C Wrapper
https://github.com/lyokato/objc-geohash
GeoHashについてはこのあたりを参照のこと。
http://blog.masuidrive.jp/index.php/2010/01/13/geohash/
使い方
緯度と軽度と求めたいハッシュの長さを指定すると
ハッシュ値を取得できる。
C版
#include <geohash.h> char* hash = GEOHASH_encode(35.6894875, 139.6917064, 13); ... free(hash);
Objective-C版
#import "GeoHash.h" NSString *hash = [GeoHash hashForLatitude:35.6894875 longitude:139.6917064 length:13]; /* hash equals to @"xn774c06kdtve" */
GEOHASHの値から、それが表す緯度経度の範囲を取得する
C版
GEOHASH *area; area = GEOHASH_decode("c216ne"); /* 次のように、指定されたハッシュが表す範囲の、緯度経度の最大値、最小値をdoubleで取得できる area->latitude.max; area->latitude.min; area->longitude.max; area->longitude.min; */ GEOHASH_free_area(area);
Objective-C版
GHArea *area = [GeoHash areaForHash:@"c216ne"]; /* 次のように、指定されたハッシュが表す範囲の、緯度経度の最大値、最小値をNSNumberのオブジェクトとして取得できる area.latitude.max area.latitude.min area.longitude.max area.longitude.min */
あるハッシュがあらわすブロックに隣接するブロックのハッシュ値を取得する。
隣接する方角を指定する。
C版
char *adjacent; /* 次の四つから、隣接する方角を指定する - GEOHASH_NORTH - GEOHASH_WEST - GEOHASH_EAST - GEOHASH_SOUTH */ adjacent = GEOHASH_get_adjacent("dqcjq", GEOHASH_NORTH); free(adjacent);
Objective-C版
/* 次の四つから、隣接する方角を指定する - GHDirectionNorth - GHDirectionWest - GHDirectionEast - GHDirectionSouth */ NSString *adjacentHash = [GeoHash adjacentForHash:@"dqcjq" direction:GHDirectionNorth];
周囲8ブロックのハッシュ値をまとめて取得
C版
GEOHASH *neighbors; neighbors = GEOHASH_get_neighbors("dqcw5"); /* 次のようにそれぞれの方角のブロックのハッシュ値が取れる。 neighbors->north; neighbors->south; neighbors->east; neighbors->west; neighbors->north_east; neighbors->north_west; neighbors->south_east; neighbors->south_west; */ GEOHASH_free_neighbors(neighbors);
Objective-C版
NSNeighbors *neighbors = [GeoHash neighborsForHash:@"dqcw5"]; /* 次のようにそれぞれの方角のブロックのハッシュ値が取れる。 neighbors.north neighbors.south neighbors.east neighbors.west neighbors.northEast neighbors.northWest neighbors.southEast neighbors.southWest */
上記の各関数は、不正なハッシュ値をうけると、CならNULL, Objective-Cならnilを返すようになってる。
ハッシュ値の検証は次のように出来る
C版
#include <stdbool.h> bool result = GEOHASH_verify_hash("abcde");
Objective-C版
BOOL result = [GeoHash verifyHash:@"abcde"];
とりあえずこんな感じ
2010-10-17
■[Perl] YAPC::Asia2010
YAPC::Asia2010に参加し、 「DataPortability ans SocialWeb Protocols」という
タイトルで発表させて頂きました。
発表資料
Protocol紹介
まず始めに、ざっくりプロトコル群を紹介していきましたが、
ここに名前を上げた以外にもどんどん新しいプロトコルが出てきてますね。
Perlのカンファレンスなので触れませんでしたが、このブログに書いてきたとおり、
他の言語でAtompubのライブラリなんかも書いていたりします。
ruby製のatomutilなんかは比較的よく使って頂いているようです。
正規表現しくってて1.9で動かないとか、余計なwarningが出るとか、
libxml_rubyじゃなくてREXML使ってるので、実務で使えるスピードじゃないとかいう
意見があがってるのは承知しているのですが、rubyに触る機会も減り、時間とモチベーションも確保できないので、
こちらも我こそはという方がいればメンテナを委譲したいところであります。
XMPPもActionScriptでas3xmppclientというのを作ってgithubに置いてあります。
他にもAS製XMPPクライアントライブラリはあるのですが, GPLだったので個人的に使いにくかったり、
自分が使いたい拡張があったりして自作しました。
その中で使ってるas3saxparserやas3saslclientも分離してgithubに置いてあります。
XMPPやリアルタイム系は色々と面白いので、別の機会にまた何かしらアウトプットしたいと思っています。
認証認可について
OpenID, OAuth, OpenSocialに絞ってプロトコルのフローのざっくりとした紹介を行い、
それらのライブラリについても紹介を行いました。
いくつかの紹介記事で「OpenIDのライブラリがなかったのでOpenID::Liteを作った」というように書かれてたのを見ましたが、そこは誤解ですね。
正確には、「JanRainに相当する、OpenID2.0のフルスペックサポートのライブラリがなかったから」です。
とは言っても実際はXRIなどの機能はなかなか使われてなかったりします。
OAuth2.0
OAuth2.0のおおざっぱな紹介をしつつ、
OAuth::Lite2というOAuth2.0対応のライブラリの説明をしました。
OAuth2.0は未だdraftでありながら、既にfacebookやmixiで使われ始めています。
OAuth on Native Apps
カスタムURIを利用したリダイレクトのパターンや
トークンの保存方法を各プラットフォーム毎に軽く紹介。
こちらも機会があればもっと細かいコードを出せればいいですね。
あとネイティブアプリでのOAuth利用時に問題とされている点や
Twitterでの事例などを紹介しておきました。
まとめ
毎年のことながら、他社の方とのコミュニケーションが楽しかったですね。
特に今年は転職などで環境を大きく変えた方がたくさんいて刺激になりました。
運営をはじめとする関係者の皆様、おつかれさまでした!
2010-02-19
■[OAuth][Perl] OAuth::LiteでxAuth
OAuth::Lite 1.25変更点
今までのget_access_token
my $access_token = $consumer->get_access_token( url => $access_token_url, token => $request_token, verifier => $verification_code, );
tokenリクエストトークンを渡さなかったり
verifierを渡さなかったりしたら、エラーとして扱っていました。
これはOAuth 1.0 Rev a で必ず必要なパラメータだったためです。
またメソッドの返り値として得られるのはOAuth::Lite::Tokenオブジェクトでした。
しかしSession Extensionや、先日のxAuthなどでは、token以外の情報も
レスポンスに含まれるようになってきました。
また、xAuthでは、tokenやverifierなしでaccess_tokenを要求します。
そんなわけで扱いづらくなってきたので, APIを変更したかったのですが、
後方互換の問題があるので、メソッド名を変えて別に用意しました。
get_access_tokenは今まで通り利用できます。
my $res = $consumer->obtain_access_token( url => $access_token_url, params => { x_auth_username => $username, x_auth_password => $password, x_auth_mode => "client_auth", } ); my $access_token = $res->token; say $access_token->token; say $access_token->secret; say $res->param('other_param');
戻り値に、ダイレクトにtokenのオブジェクトを返すのではなくて
OAuth::Lite::Responseを挟むようにしました。
$res->tokenで、OAuth::Lite::Tokenのオブジェクトが取れます。
あわせて、get_request_tokenに対してobtain_request_tokenも用意しました。
こちらも返り値がOAuth::Lite::Tokenでなく、OAuth::Lite::Responseのオブジェクトになりますが
それ以外は同じ動作をします。
twitterで実験
packageにexamples/twitter_xauth.plを足しておきました。
twitterでのxAuthを利用してみると、
access_tokenのエンドポイントは必ずhttpsのほうにアクセスすることになっています。
レスポンスを見るとxAuthで規定されているx_auth_expires以外にも
user_id, screen_nameが取れるようですね。
my $consumer = OAuth::Lite::Consumer->new( consumer_key => $consumer_key, consumer_secret => $consumer_secret, ); my $res = $consumer->obtain_access_token( url => q{https://twitter.com/oauth/access_token}, params => { x_auth_username => $username, x_auth_password => $password, x_auth_mode => "client_auth", }, ); unless($res) { say "Failed to get access token"; die dump($consumer->oauth_response); } say "[GOT ACCESS TOKEN]"; say "TOKEN:".$res->token->token; say "TOKEN-SECRET:".$res->token->secret; say "EXPIRES:".$res->param('x_auth_expires'); say $res->param('screen_name'); say $res->param('user_id');
特にコンシューマに対して制限なく、xAuthを利用できちゃうというところで、
ポリシーに関してはいろいろと思うところもありますが。
2010-02-12
■[OAuth] OAuthでデスクトップアプリがブラウザを経由させたくないときのxAuth
最近twitter APIまわりで話題に出てきているようなので。
ちゃんと追いかけきれてないけど、恐らくこれのことですね。
http://tools.ietf.org/html/draft-dehora-farrell-oauth-accesstoken-creds-00
OAuth WRAPではUsername and Password Profileとして組み込まれてます。
http://d.hatena.ne.jp/lyokato/20091118/1258524429
WRAP/2.0が来るまでにOAuth1.0aで使いたい時に。
利用状況
ブラウザがない、あるいはブラウザを使うのが適切ではない状況での
いわゆるデスクトップアプリのためのもの。
組み込みだったり、搭載されているブラウザが貧弱だったりとか。
ブラウザと連携させたくない、あるいは出来ないとき。
仕様
まず始めにクライアントは、エンドユーザーにユーザー名とパスワードを入力させます。
クライアントは、いきなりアクセストークンを取得しにいく。
リクエストトークンを取得したり、ユーザーをプロバイダのページに飛ばして認可させる、という処理は省きます。
ユーザー名とパスワードを預かってる時点で、「ユーザーがクライアントを信頼してる」ということになるので。
アクセストークンURLにリクエストするときのパラメータは次のようになります。
- x_auth_username
- x_auth_password
- x_auth_mode
- oauth_consumer_key
- oauth_signature_method
- oauth_signature
- oauth_timestamp
- oauth_nonce
- oauth_version
普通のOAuthのリクエストで使われるものと何が違うのか
まずoauth_tokenパラメータを省きます。 このやりとりでは、リクエストトークンを使わないので必要ありません。
x_authで始まる3つのパラメータが追加されています。(なのでxAuthと呼ばれてるみたい)
エンドユーザーから預かったユーザー名とパスワードを、
x_auth_username, x_auth_passwordの各パラメータの値として使うということですね。
x_auth_modeは必ず"client_auth"にしておきます。
このリクエストに対するレスポンスには次のパラメータが含まれます。
- oauth_token
- oauth_token_secret
- x_auth_expires
tokenとtoken_secretはいつものですね。
x_auth_expiresが加わっています。unix timeの秒数が入ります。
その時間になったら、アクセストークンの有効期限が切れます。
ここが0になっていたときは無期限です。
所感
クライアントは、「ユーザー名とパスワード」というアカウント情報を、アクセストークン取得時にのみ使い、
その後は、アクセストークンのみ保存し、アカウント情報は記憶しないというのが行儀のよい使い方、ということになると思います。
とは言ってもエンドユーザーから見て、クライアントがそうしてくれる保証はないわけですし、フィッシングの危険性は伴います。
提供しているデータの性質を考えるとか、特定の信頼できるコンシューマのみに利用させるとかで、プロバイダ側はバランスを考える必要があるのではないでしょうか。
あと先日のGumblrのデスクトップ上に保存されたパスワード盗難の件もあることですし、トークンの保存時は気をつけましょう。
