[Edit][Create New]
[ IndexPage / ネットとプログラミング / CGI / 便利なスクリプト片(未整理) / Check.pm ]

Check.pm

package Check;

use Tool;
use Jcode;
use strict;


sub form_check{
	my($form, $file) = @_;
	my(@error, %error);
	my(%seen, @key);
	my $file2;

	unless ($file){
		$file = $0;
		$file =~ s/.cgi$/.chk/ or Tool::error("$file: 不正なファイル名");
	}

	# chkディレクトリ内のファイル名
	$file2 = $file;
	$file2 =~ s#([^/]+)$#chk/$1#;

	open(FILE, $file) or open(FILE, $file2) or Tool::error("$file or $file2: オープン不可");

	while(<FILE>){
		chomp; tr/\r\n//;

		next if /^\s*#/; # コメント行
		next if /^\s*$/; # 空行

		# ルールの読み込み($keyが$valueでなければ、$msgを出力)
		my($key, $value, $msg) = split;

		# ルール1行分のチェック
		my $error = check_one($form, $key, $value);

		if($error){
			# エラーメッセージ保存
			$msg = jcode($msg)->euc;
			push(@key, $key) unless $seen{$key}++; # 現れた順番を保持
			$error{$key} = $msg;
		}
	}
	
	@error = @error{@key};
	\@error;
}

sub check{
	my $check   = shift;
	my $form    = shift;
	my @return;

	$form || Tool::error("\$formが空");

	foreach my $key (sort keys %$check){
		my $error = check_one($form, $key, $check->{$key});
		push(@return, $error) if $error;	
	}
	@return;
}

sub check_one{
	my($form, $key, $statement) = @_;

	# チェックするキーごとのループ
	my($success, $error);
	my($ors, $ands);

	foreach $ors (split('/', $statement)){
		my @error;
		# OR条件ごとのループ
		foreach $ands (split('\|', $ors)){
			# AND条件ごとのループ
			my($how, $opt) = split('=', $ands); # オプションを切り分け

			my $not    = ($how =~ s/^!//); # 先頭に!があれば条件反転
			my $result = _check($form, $how, $form->{$key}, $opt); # チェック
			   $result = !$result if $not; # 条件反転
			push(@error, $how) unless $result; # エラーがあれば保存
		}
		$success = 1 unless @error; # 一回でも成功すれば成功
		$error = join(":", $key, @error) if @error && !$error;
	}

	$success ? undef : $error;
}

sub _check{
	my($form, $type, $value, $opt) = @_;
	&Jcode::convert(\$value, 'euc');
	my($fail);
	for($type){
		if    (/^BLANK$/){
			# ブランクチェック
			return 0 if $value =~ /\S/;
		}elsif(/^IS$/){
			return 0 if $value ne Jcode::convert($form->{$opt}, 'euc');
		}elsif(/^IS_NUM$/){
			# 0以上の整数チェック
			return 0 if $value =~ /[^0-9]/;
		}elsif(/^IS_ALPHA$/){
			# 英文字チェック(アンダースコア含まず)
			return 0 if $value =~ /[^a-zA-Z]/;
		}elsif(/^IS_ALPHA_$/){
			# 英文字チェック(アンダースコア含む)
			return 0 if $value =~ /[^a-zA-Z_]/;
		}elsif(/^IS_ALPHANUM$/){
			# 英数字チェック(アンダースコア含まず)
			return 0 if $value =~ /[^a-zA-Z0-9]/;
		}elsif(/^IS_ALPHA_NUM$/){
			# 英数字チェック(アンダースコア含む)
			return 0 if $value =~ /[^a-zA-Z_0-9]/;
		}elsif(/^IS_ZENKAKU$/){
			return 0 if $value=~ /[\x00-\x7F]/;	
		}elsif(/^IS_KATAKANA$/){
			my $katakana = '(?:\xA5[\xA1-\xF6]|\xA1[\xA1|\xBC])'; # EUC-JP
			return 0 unless $value =~ /^$katakana*$/o;
		}elsif(/^IS_ASCII$/){
			return 0 if $value=~ /[^\x01-\x7E]/;	
		}elsif(/^IS_HANKAKU$/){
			return 0 if $value=~ /[^\x20-\x7E]/;	
		}elsif(/^TR$/){
			return 0 if !(eval "\$value =~ tr/$opt//"); # tr///に失敗したら返る
		}elsif(/^TRC$/){
			return 0 if !(eval "\$value =~ tr/$opt//c"); # tr///cに失敗したら返る
		}elsif(/^LENGTH$/){
			# 長さチェック(1-100/-100/100-/100)
			$opt ||= '--オプションが空--'; # オプション不正でエラー扱いにする
			my @opt = split(/-/, $opt, -1); 
			if    (@opt == 1){
				return 0 if (length($value) != $opt);
			}elsif(@opt == 2){
				my($from, $to) = @opt;
				return 0 if ($from ne '' && $from > length($value));
				return 0 if ($to   ne '' && $to   < length($value));
			}else{
				&Tool::error("LENGTHのオプション不正 $opt");
			}
		}elsif(/^VALUE$/){
			# 値チェック(記法は長さチェックと同じ)
			return 0 if $value =~ /[^0-9]/;

			$opt ||= '--オプションが空--'; # オプション不正でエラー扱いにする

			my @opt = split(/-/, $opt, -1); 

			if    (@opt == 1){
				return 0 if ($value != $opt);
			}elsif(@opt == 2){
				my($from, $to) = @opt;
				return 0 if ($from ne '' && $from > $value);
				return 0 if ($to   ne '' && $to   < $value);
			}else{
				&Tool::error("VALUEのオプション不正 $opt");
			}
		}elsif(/^IS_VALIDDATE$/){
			#妥当日付チェック
			my( $yy, $mm, $dd ) = $value =~ /^(\d{4,4})(\d{2,2})(\d{2,2})$/;
			return 0 unless( isValidDate( $yy, $mm, $dd ) );
		}else{
			&Tool::error("存在しないチェック型 $_");
		}
	}
	return 1; # チェック終了
	# VALUE
}

# 日付チェック用関数
sub isValidDate{
	my($year, $mon, $day) =@_;
	# $year 4桁の西暦
	# $mon  月を表す1〜12の整数
	# $day  日付

	# 簡単なチェック
	return 0 if $year < 1701 # 英では1700年は閏年。
			 || $year > 2500 # まあここは適当に。
			 || $day  <	1
			 || $day  >   31;
	# $monは間違っていたら以下の$lastdayが0になるのでチェック不要。

	# その月の末日を計算(2行目は閏年の計算)
	my($lastday) = (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)[$mon - 1]
		 + ($mon == 2 && ($year % 4 == 0 && $year % 100 != 0 || $year % 400 == 0
));
	# $day が $lastday 以下ならOK。
	($day <= $lastday) ? 1 : 0;
}


1;
__END__

=head1 NAME

Check.pm - フォーム文字列のチェックを行う

=head1 METHODS

Check::form_check(\%form [, $file]);

Check::check(\%check, \%form);

Check::check(\%check);

=head1 METHODS

一般的には、次のようにして呼びます。

	@errors = Check::check(\%check, \%form);

%checkには、チェックしたいパラメタと規則の対の連想配列 
%formには、分解したフォーム文字列を指定します。

	@errors = Param::form_check(\%form);

と呼ぶこともできます。この場合、.cgiを.chkに変更したファイルを、
.cgiがあるディレクトリか、もしなければその直下のchk/ディレクトリに探しに
行きます。.chkファイルの構文はCheck::checkと同じで、

項目名 条件文字列 エラーメッセージ

の空白区切りで1行1条件で記述します。行頭に#があるとコメントです。

=head2 構文

第一引数にある%checkは、例えば

	$check{USERNAME} = "!BLANK|IS_ALPHA|LENGTH=-20";

のようにして作成します。「IS_ALPHA」の部分を条件文字列と呼びます。

この場合、USERNAMEという名前のパラメタをチェックし、

	「空でない」
	「アルファベットでのみ構成されている」
	「長さが20バイト以下である」

場合に成功を返します。

上記のように、各AND条件間は「|」文字で区切ります。
空白文字は許されません。
文字「|」は通常ORを表すので一見矛盾していますが、
これはCのビット演算子に由来します。

条件文字列の先頭に「!」文字を付加することで、
条件を反転させることができます。

複数のAND条件(および単一条件)を、OR条件で連結することができます。
OR条件の区切りには「/」文字を使います。例えば、

	$check{USERNAME} = "LENGTH=4-8|IS_ALPHA/LENGTH=2-8|IS_NUM/BLANK";

は、4文字〜8文字の英字か、2文字〜8文字の数字の場合、
あるいは空文字列(BLANK)の場合のみ真を返します。
2文字の数字は真ですが、2文字の英字は偽になります。

条件文字列には、以下のものがつかえます

=over

=item BLANK

空文字列です。必須入力にしたい場合は、!BLANKとします。
空白文字のみの場合もBLANKは真になります(2バイト空白を除く)

=item IS=STRING

STRINGというパラメタ名の値と同じであれば真です。
パスワードの入力チェック等に使います。

「is equal (to) STRING」と読めるように命名しています。

=item IS_ALPHA

a〜z、A〜Z「のみ」で構成されている場合、真です。
アンダースコアは含まれません。

!IS_ALPHAとした場合「英字以外の文字を含む文字列」を表すことに注意してください。
「英字を含まない文字列」を指定したい場合は、!ALPHAを使用してください。

=item IS_ALPHA_

IS_ALPHAと同じですが、アンダースコアを含みます。

=item IS_NUM

数字のみで構成されている場合、真です。

=item IS_ALPHANUM

英数字のみで構成されている場合、真です。
アンダースコアは含まれません。

=item IS_ALPHA_NUM

IS_ALPHANUMと同じですが、アンダースコアを含みます。

=item IS_ZENKAKU

すべて全角文字であれば真です。

=item IS_ASCII

ASCII文字であれば真です。0x00(NULL)と0x7F(DEL)を除き、全ての7bit文字をB<含みます>。

=item IS_HANKAKU

すべて半角文字であれば真です。改行コードやタブなどの制御文字をB<含みません>。スペースは含みます。

=item TR=TR_STRING

Perlのtr///構文を実行し、TR_STRINGで指定した文字列が含まれれば真を返します。

また、=や&、|など、パーサが解釈してしまう文字列は今のところ使用できません(BU...仕様です)

=item TRC=TR_STRING

Perlのtr///c構文を実行し、TR_STRINGで指定した文字列B<以外の>文字列がが含まれれば真を返します。

!TRC=a-zA-Z_ は、「a-zA-Z_以外の文字列が含まれれば、偽」となるので、結果的にIS_ALPHA_と同じになります。

TRと同様、=や&、|など、パーサが解釈してしまう文字列は今のところ使用できません(仕様...です)

=item LENGH=n-m

長さがn以上m以下の場合に真です。n,mともに省略可能です。省略された場合、最小値・最大値のチェックをそれぞれ行いません。

=item VALUE=n-m

パラメタを数値と見なし、値がn以上m以下の場合に真です。n,mともに省略可能です。省略された場合、最小値・最大値のチェックをそれぞれ行いません。

=item IS_VALIDDATE

有効な日付かどうか調べます(isValidDateを使用)。YYYYMMDDの形式の日付を対象としています。


=head1 EXSAMPLES

サンプルです。

	# フォーム文字列をパースする
	my %form = get_param($ENV{QUERY_STRING});

	# チェックする項目と内容を決める
	my %check = qw(
		USR_ID                  !BLANK|IS_ALPHANUM|LENGTH=2-9
		USR_PASS                !BLANK|IS_ALPHA_NUM|LENGTH=1-8
		USR_PASS_AGAIN          IS=USR_PASS
		USR_ZOKUSEI             !BLANK|VALUE=1-3
		USR_NAME                !BLANK|IS_ZENKAKU|LENGTH=1-30
	);

	# チェックする
	@errors = &Check::check(\%check, \%form);

	# エラーがあれば表示して終了
	print join("<BR>\n", @errors) and exit if @error;

=head1 BUGS


IS_VALIDDATEを追加しました。外部の関数に依存しています。(y.imamura)

=cut