Login
  Days of Liris

2007/02/07

captcha widgetの作り方(適当編)

Tag: turbogears

10分で作るCaptchaの作り方です。Captchaが何なのか、と言うのは、すでに知っていると言う前提で。

作り方としては、CaptchaWidgetクラスを定義して、テンプレートを書くプロセスと、バリデーションクラスを作るプロセスの二つです。多分、このサイトで使っているものと、ほぼ同じ物になるはず。

Captchaは、 http://captcha.net/ のものを使います。これには、画像の物とオーディオ形式の2種類がありますが、日本人にはオーディオ形式はちょっとね。さて、どちらもユーザ登録して、ユーザIDとパスワードをもらいます。Widget内で直接ユーザIDとパスワードを管理してもいいのですが、何となくいやん。というか、バリデータと共通で使うところがあるので、分けています。なので、ロジックが入りそうなところはCaptchaConfigというクラスを作って、その人にやってもらうことにします。そのクラスがこんな感じ

alphabet = "abcdefghijklmnopqrstuvwxyz"
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本体です。

class CaptchaWidget(CompoundFormField):
    
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なんですが、めんどくさいので、上のコードはプログラマの責任で、と言うスタンス。

次にテンプレート。

<div xmlns:py="http://purl.org/kid/ns#"
  
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を拡張して、複数のフィールドにまたがってバリデートするようなやつを使います。

class CaptchaValidator(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の作り方(適当編)

letters = string.ascii_letters + string.digits
本筋じゃないところですみません‥‥

Widgetの作り方、とても参考になりました。

Re: captcha widgetの作り方(適当編)

あら、stringモジュールを直接importすることがほとんどないのでstring.ascii_lettersがあるとは知りませんでした。

PloneCaptchaとほぼ同じコードなので、PloneCaptchaの人にも教えてあげてください。
Captcha Image