2007/02/07
captcha widgetの作り方(適当編)
10分で作るCaptchaの作り方です。Captchaが何なのか、と言うのは、すでに知っていると言う前提で。
作り方としては、CaptchaWidgetクラスを定義して、テンプレートを書くプロセスと、バリデーションクラスを作るプロセスの二つです。多分、このサイトで使っているものと、ほぼ同じ物になるはず。
Captchaは、 http://captcha.net/ のものを使います。これには、画像の物とオーディオ形式の2種類がありますが、日本人にはオーディオ形式はちょっとね。さて、どちらもユーザ登録して、ユーザIDとパスワードをもらいます。Widget内で直接ユーザIDとパスワードを管理してもいいのですが、何となくいやん。というか、バリデータと共通で使うところがあるので、分けています。なので、ロジックが入りそうなところはCaptchaConfigというクラスを作って、その人にやってもらうことにします。そのクラスがこんな感じ
letters = alphabet + alphabet.upper () + "0123456789"
class CaptchaConfig:
def __init__(self, captcha_username, captcha_password,
captcha_width = 240,
captcha_height = 80,
base_url = 'http://image.captchas.net',
captcha_letters = 6):
self.base_url = base_url
self.captcha_alphabet = alphabet
self.captcha_letters = captcha_letters
self.captcha_username = captcha_username
self.captcha_password = captcha_password
self.captcha_width = captcha_width
self.captcha_height = captcha_height
def getCaptchaCode(self):
random_string = ''
for i in range(50):
random_string += random.choice(letters)
return random_string
def getGeneratedCaptchaCode(self, random_string):
code = self.captcha_password + random_string
if self.captcha_alphabet != alphabet or self.captcha_letters != 6:
code += ':' + self.captcha_alphabet + ':' + str(self.captcha_letters)
code_md5 = md5.new(code).digest()
generated_code = ''
for p in range(self.captcha_letters):
n = ord(code_md5[p]) % len(self.captcha_alphabet)
generated_code += self.captcha_alphabet[n]
return generated_code
def getImageLink(self, random_string):
return "%s?client=%s&random=%s&alphabet=%s&letters=%s&width=%s&height=%s" % (
self.base_url, self.captcha_username,
random_string, self.captcha_alphabet,
self.captcha_letters, self.captcha_width,
self.captcha_height)
もともとが PloneCaptchaベースです。次にWidget本体です。
validator = None
template = "myproj.templates.captcha"
params = ["attrs", "captchaConfig", "captcjaCode"]
member_widgets = ["text_field"]
attrs = {}
width = "240px"
height = "80px"
captcha_code = ""
text_field = TextField(name="text")
def display(self, value, **params):
params["captchaCode"] = self.captchaConfig.getCaptchaCode()
return super(CaptchaWidget, self).display(value, **params)
とこれだけです。Widgetの継承元は、CompoundFormFieldで、一つのWidgetの中に複数のフィールドを含めたい場合などはこれを使うらしい。ここでは、text_fieldというテキストの入力フィールドがあります。テンプレートの中に隠しフィールドが直接あって、これをウィジェットでやると嬉しくないことがあるので、そうなっています。
displayの振る舞いを変えちゃうのはどうなのかな?と言う気もしますが、ここでは、そうなっています。インスタンス化するときにCaptchaConfigのオブジェクトが必要です。mustなんですが、めんどくさいので、上のコードはプログラマの責任で、と言うスタンス。
次にテンプレート。
class="${field_class}"
id="${field_id}"
py:attrs="attrs">
<img width="${captchaConfig.captcha_width}"
height="${captchaConfig.captcha_height}"
src="${captchaConfig.getImageLink(captchaCode)}"
alt="Captcha Image" />
<input type="hidden" name="${name}.hidden" value="${captchaCode}"/>
${text_field.display("", **params_for(text_field))}
</div>
と言う感じ。最初のネームスペースの宣言とか、class,id、属性の指定はそういう物だと思ってください。ウィジェットの初期化のときにしていしたcaptchaConfigとか、displayで指定したcaptchaCodeとかは魔法により使えます。displayを変更せずにtemplateの中でcaptchaCodeを作ると言うのもあり。
これでウィジェットは完成。次は、バリデータ。CompoundFormFieldは普通のバリデータが使えない(使えるんだったら教えてください)ので、とりあえずは、FormValidatorを拡張して、複数のフィールドにまたがってバリデートするようなやつを使います。
messages = {"notMatch": "not match"}
validate_partial_form = True
def __init__(self, captchaConfig, fieldId):
self.captchaConfig = captchaConfig
self.fieldId = fieldId
super(CaptchaValidator, self).__init__()
def validate_partial(self, field_dict, state):
field_names = (self.fieldId + ".hidden", self.fieldId + ".text")
for name in field_names:
if not field_dict.has_key(name):
return
self.validate_python(field_dict, state)
def validate_python(self, field_dict, state):
code_generated = self.captchaConfig.getGeneratedCaptchaCode(field_dict[self.fieldId]["hidden"])
if field_dict[self.fieldId]["text"] != code_generated:
raise Invalid("notMatch", field_dict, state,
error_dict={self.fieldId: "No Match"})
validate_pythonだけで動くはずなですが、パスワードをバリデートするやつがvalidate_partialも定義していたのでそれに習っています。CompoundFormFieldだと、各フィールドはfieldid.textとか、fieldid.hiddenとか言う感じでポストされて、小人さんがそれらを辞書にしてくれます。バリデータなのにWidgetの知識が必要なのは気に入らないと言う人は、適当に直してみてね。
で、あとは、使うだけ。CompoundFormFieldなので、スキーマにcaptcha = CaptchaValidator(capcha_config, "captcha")とはかけなくって、chained_validators = [CaptchaValidator(captcha_config, "captcha")]のようににしてください。
Re: captcha widgetの作り方(適当編)
本筋じゃないところですみません‥‥
Widgetの作り方、とても参考になりました。
Re: captcha widgetの作り方(適当編)
PloneCaptchaとほぼ同じコードなので、PloneCaptchaの人にも教えてあげてください。