=head1 NAME Message::Field::Params --- Perl module for Internet message field body consist of parameters, such as C field =cut package Message::Field::Params; use strict; require 5.6.0; use re 'eval'; use vars qw(%DEFAULT @ISA %REG $VERSION); $VERSION=do{my @r=(q$Revision: 1.20 $=~/\d+/g);sprintf "%d."."%02d" x $#r,@r}; require Message::Util; require Message::MIME::Charset; require Message::Field::Structured; push @ISA, qw(Message::Field::Structured); use overload '""' => sub { $_[0]->stringify }, '0+' => sub { $_[0]->count }, '.=' => sub { $_[0]->add ($_[1], ['', value => 1]); $_[0] }, fallback => 1; %REG = %Message::Util::REG; $REG{S_parameter} = qr/(?:[^\x22\x28\x3B\x3C]|$REG{comment}|$REG{quoted_string}|$REG{angle_quoted})+/; $REG{S_parameter_separator} = qr/;/; $REG{S_comma_parameter} = qr/(?:[^\x22\x28\x2C\x3C]|$REG{comment}|$REG{quoted_string}|$REG{angle_quoted})+/; $REG{S_comma_parameter_separator} = qr/,/; $REG{MS_parameter_avpair} = qr/([^\x22\x3C\x3D]+)=([\x00-\xFF]*)/; $REG{M_parameter_avpair} = qr/([^\x22\x3C\x3D]+)=([^\x3D]*)/; %DEFAULT = ( -_HASH_NAME => 'params', -_MEMBERS => [qw/params/], -_METHODS => [qw/add replace delete item parameter scan/], ## count item_exist <- not implemented yet -accept_coderange => '7bit', -by => 'attribute', #encoding_after_encode #encoding_before_decode #field_param_name #field_name #field_ns #format #header_default_charset #header_default_charset_input #hook_encode_string #hook_decode_string -output_comment => 1, -output_parameter_extension => 0, -parameter_rule => 'S_parameter', ## regex name of parameter -parameter_attribute_case_sensible => 0, -parameter_attribute_unsafe_rule => 'NON_http_attribute_char', -parameter_av_Mrule => 'MS_parameter_avpair', -parameter_no_value_attribute_unsafe_rule => 'NON_http_attribute_char', -parameter_value_max_length => 60, -parameter_value_split_length => 35, -parameter_value_unsafe_rule => 'NON_http_attribute_char', #parse_all -separator => '; ', -separator_rule => 'parameter_separator', -use_comment => 1, -use_parameter_extension => 1, #value_type ); =head1 CONSTRUCTORS The following methods construct new objects: =over 4 =cut ## Initialize of this class -- called by constructors sub _init ($;%) { my $self = shift; my %options = @_; $self->SUPER::_init (%DEFAULT, %options); $self->{param} = []; my $field = $self->{option}->{field_name}; if ($field eq 'p3p') { $self->{option}->{parameter_rule} = 'S_comma_parameter'; $self->{option}->{separator_rule} = 'S_comma_parameter_separator'; $self->{option}->{separator} = ', '; } if ($self->{option}->{format} =~ /news-usefor/) { $self->{option}->{accept_coderange} = '8bit'; } elsif ($self->{option}->{format} =~ /http/) { $self->{option}->{accept_coderange} = 'binary'; } } =item $p = Message::Field::Params->new ([%options]) Constructs a new object. You might pass some options as parameters to the constructor. =cut ## Inherited =item $p = Message::Field::Params->parse ($field-body, [%options]) Constructs a new object with given field body. You might pass some options as parameters to the constructor. =cut sub parse ($$;%) { my $class = shift; my $self = bless {}, $class; my $body = shift; $self->_init (@_); my @param; $body =~ s{ ($REG{ $self->{option}->{parameter_rule} }) (?: $REG{ $self->{option}->{separator_rule} } | $ ) }{ push @param, $self->_parse_parameter_item ($1, $self->{option}); ''; }gesx; $self->_decode_parameters (\@param, $self->{option}); $self->_save_parameters (\@param, $self->{option}); $self; } ## $self->_parse_parameter_item ($item, \%option) ## -- parses a parameter item (into attribute/value pair or no-value-attribute) sub _parse_parameter_item ($$\%) { my $self = shift; my ($item, $option) = @_; my @comment; ($item, @comment) = $self->Message::Util::delete_comment_to_array ($item, -use_angle_quoted); $item =~ s/^$REG{WSP}+//g; $item =~ s/$REG{WSP}+$//g; my %item; if ($item =~ /^$REG{ $option->{parameter_av_Mrule} }$/) { my $encoded = 0; ($item{attribute}, $item{value}) = ($1, $2); $item{attribute} =~ tr/\x09\x0A\x0D\x20//d; $item{value} =~ s/^$REG{WSP}+//g; if ($option->{use_parameter_extension} && $item{attribute} =~ /^([^*]+)(?:\*([0-9]+)(\*)?|(\*))\z/) { $item{attribute} = $1; $item{section_no} = $2; $encoded = $3 || $4; $item{section_no} = -1 if $4; if ($item{section_no} <= 0 && $encoded && $item{value} =~ /^([^']*)'([^']*)'([\x00-\xFF]*)$/) { $item{charset} = $1; $item{charset} =~ tr/\x09\x0A\x0D\x20//d; $item{language} = $2; $item{language} =~ tr/\x09\x0A\x0D\x20//d; $item{value} = $3; $item{value} =~ s/^$REG{WSP}+//g; } if ($encoded) { $item{value} =~ s/%([0-9A-Fa-f][0-9A-Fa-f])/chr(hex($1))/ge; } } else { $item{section_no} = -1; } ($item{value}, $encoded) = Message::Util::unquote_if_quoted_string ($item{value}) unless $encoded; ($item{value}, $encoded) = Message::Util::unquote_if_angle_quoted ($item{value}) unless $encoded; $item{charset} = '*bare' if !$encoded && !$item{charset}; $item{attribute} = lc $item{attribute} unless $option->{parameter_attribute_case_sensible}; } else { my $encoded = 0; ($item, $encoded) = Message::Util::unquote_if_quoted_string ($item) unless $encoded; ($item, $encoded) = Message::Util::unquote_if_angle_quoted ($item) unless $encoded; $item{attribute} = $item; $item{charset} = '*bare' if !$encoded; $item{no_value} = 1; } $item{comment} = \@comment; \%item; } ## $self->_decode_parameters (\@parameter, \%option) ## -- join RFC 2231 splited fragments and decode each parameter sub _decode_parameters ($\@\%) { my $self = shift; my ($param, $option) = @_; my %fragment; my @fparameter; for my $parameter (@$param) { if ($parameter->{no_value}) { my %item; $item{no_value} = 1; $item{comment} = $parameter->{comment}; if ($parameter->{charset} ne '*bare') { ## non quoted-string my %s = &{$self->{option}->{hook_decode_string}} ($self, $parameter->{attribute}, charset => $option->{encoding_before_decode}, type => 'parameter/no-value-attribute'); if ($s{charset}) { ## Convertion failed $item{charset} = $s{charset}; } elsif (!$s{success}) { $item{charset} = $option->{header_default_charset_input}; } $item{attribute} = $s{value}; } else { $item{attribute} = $parameter->{attribute}; } $parameter = \%item; } elsif ($parameter->{section_no} < 0) { my %item; $item{attribute} = $parameter->{attribute}; $item{language} = $parameter->{language} if $parameter->{language}; $item{comment} = $parameter->{comment}; if ($parameter->{charset} ne '*bare') { ## non 2231 encoded my %s = &{$self->{option}->{hook_decode_string}} ($self, $parameter->{value}, charset => $parameter->{charset} || $option->{encoding_before_decode}, type => 'parameter/value/quoted-string'); if ($s{charset}) { ## Convertion failed $item{charset} = $s{charset}; } elsif (!$s{success}) { $item{charset} = $option->{header_default_charset_input}; } elsif ($parameter->{charset}) { $item{output_charset} = $parameter->{charset}; } $item{value} = $s{value}; } else { $item{value} = $parameter->{value}; } $parameter = \%item; } else { ## fragment $fragment{ $parameter->{attribute} }->[ $parameter->{section_no} ] = $parameter->{value}; if ($parameter->{section_no} == 0) { $fragment{'*property'}->{ $parameter->{attribute} } ->{language} = $parameter->{language}; $fragment{'*property'}->{ $parameter->{attribute} } ->{charset} = $parameter->{charset}; } if (ref $parameter->{comment} && @{$parameter->{comment}} > 0) { push @{ $fragment{'*property'}->{ $parameter->{attribute} } ->{comment} }, @{$parameter->{comment}}; } $parameter = undef; } } for (keys %fragment) { next if $_ eq '*property'; my %item; $item{attribute} = $_; $item{comment} = $fragment{'*property'}->{ $item{attribute} }->{comment}; $item{language} = $fragment{'*property'}->{ $item{attribute} }->{language}; delete $item{language} unless $item{language}; my $charset = $fragment{'*property'}->{ $item{attribute} }->{charset}; my %s = &{$self->{option}->{hook_decode_string}} ($self, join ('', @{ $fragment{ $item{attribute} } }), charset => $charset || $option->{encoding_before_decode}, type => 'parameter/extended-value/encoded'); if ($s{charset}) { ## Convertion failed $item{charset} = $s{charset}; } elsif ($charset) { $item{output_charset} = $charset; } $item{value} = $s{value}; push @fparameter, \%item; } @$param = (grep { ref $_ eq 'HASH' } @$param, @fparameter); } ## $self->_parse_values_of_paramters (\@parameter, \%option) ## --- Parse each values of parameters sub _parse_values_of_parameters ($\@\%) { my $self = shift; my ($param, $option) = @_; @$param = map { if (!$_->{no_value}) { $_->{value} = $self->_parse_value ($_->{attribute} => $_->{value}); } else { $_->{value} = $self->_parse_value ('*no_value_attribute' => $_->{value}); } $_; } @$param; } ## $self->_save_parameters (\@parameter, \%option) ## -- Save parameters in $self sub _save_parameters ($\@\%) { my $self = shift; my ($param, $option) = @_; $self->_parse_values_of_parameters ($param, $option) if $option->{parse_all}; $self->{ $option->{_HASH_NAME} } = $param; } *__save_parameters = \&_save_parameters; =back =head1 METHODS =over 4 =item $p->add ($name => [$value], [$name => $value,...]) Adds parameter name=value pair. Example: $p->add (title => 'foo of bar'); ## title="foo of bar" $p->add (subject => 'hogehoge, foo'); ## subject*=''hogehoge%2C%20foo $p->add (foo => ['bar', language => 'en']) ## foo*='en'bar $p->add ('text/plain', ['', value => 1]) ## text/plain This method returns array reference of (name, {value => value, attribute...}). Available options: charset (charset name), language (language tag), value (1/0, see example above). =cut sub _add_hash_check ($$$\%) { my $self = shift; my ($name, $value, $option) = @_; my $value_option = {}; if (ref $value eq 'ARRAY') { ($value, %$value_option) = @$value; } ## -- attribute only (no value) parameter if ($value_option->{no_value}) { $name = $self->_parse_value ('*no_value_attribute' => $name) if $$option{parse}; return (1, $name => { attribute => $name, no_value => 1, language => $value_option->{language}, comment => $value_option->{comment}, }); } ## -- attribute=value pair if ($$option{validate} && $name =~ /^$REG{NON_http_attribute_char}$/) { if ($$option{dont_croak}) { return (0); } else { Carp::croak qq{add: $name: Invalid parameter name}; } } $value = $self->_parse_value ($name => $value) if $$option{parse}; (1, $name => { attribute => $name, value => $value, output_charset => $value_option->{charset}, charset => $value_option->{current_charset}, language => $value_option->{language}, comment => $value_option->{comment}, }); } *_add_return_value = \&_replace_return_value; ## (1/0, $name => $value) = $self->_replace_hash_check ($name => $value, \%option) ## -- Checks given value and prepares saving value (hash version) *_replace_hash_check = \&_add_hash_check; ## $value = $self->_replace_hash_shift (\%values, $name, $option) ## -- Returns a value (from %values) and deletes it from %values ## (like CORE::shift for array). sub _replace_hash_shift ($\%$\%) { shift; my $r = shift; my $n = $_[0]->{attribute}; if ($$r{$n}) { my $d = $$r{$n}; $$r{$n} = undef; return $d; } undef; } ## $value = $self->_replace_return_value (\$item, \%option) ## -- Returns returning value of replace method sub _replace_return_value ($\$\%) { my $self = shift; my ($item, $value) = @_; if ($$item->{no_value}) { $$item->{attribute}; } else { $$item->{value}; } } ## 1/0 = $self->_delete_match ($by, \$item, \%delete_list, \%option) ## -- Checks and returns whether given item is matched with ## deleting item list sub _delete_match ($$\$\%\%) { my $self = shift; my ($by, $item, $list, $option) = @_; return 0 unless ref $$item; ## Already removed if ($by eq 'attribute' || $by eq 'name') { return 1 if $$list{ $$item->{attribute} }; } elsif ($by eq 'value') { return 1 if $$list{ $$item->{value} }; } elsif ($by eq 'charset') { return 1 if $$list{ $$item->{output_charset} } || $$list{ $$item->{charset} }; } elsif ($by eq 'language') { return 1 if $$list{ $$item->{language} }; } elsif ($by eq 'type') { if ($$item->{no_value}) { return 1 if $$list{no_value_attribute}; } else { return 1 if $$list{attribute_value_pair}; } } 0; } ## Delete empty items sub _delete_empty ($) { my $self = shift; my $array = $self->{option}->{_HASH_NAME}; $self->{ $array } = [grep { ref $_ } @{$self->{ $array }}] if $array; } =item @param = $p->parameter ($name => ($new_value), (%option)) =cut sub parameter ($;@) { my $self = shift; if (@_ == 2) { ## $p->parameter (hoge => 'foo') $self->replace (@_); } else { ## $p->parameter ('foo') $self->item (@_); } } *_item_match = \&_delete_match; *_item_return_value = \&_replace_return_value; ## $item = $self->_item_new_value ($name, \%option) ## -- Returns new item with key of $name (called when ## no returned value is found and -new_value_unless_exist ## option is true) sub _item_new_value ($$\%) { my $self = shift; my ($key, $option) = @_; if ($option->{by} eq 'attribute' || $option->{by} eq 'name') { return {attribute => $key}; } undef; } ## TODO: Implement count,item_exist method =item $field-body = $p->stringify () Returns C as a string. =cut sub stringify ($;%) { my $self = shift; my %o = @_; my %option = %{$self->{option}}; for (grep {/^-/} keys %o) {$option{substr ($_, 1)} = $o{$_}} $option{output_parameter_extension} = 0 unless $option{use_parameter_extension}; $option{output_comment} = 0 unless $option{use_comment}; $self->_delete_empty; my @param; $self->scan( sub {shift; my ($item, $option) = @_; my $r = 1; ($r, $item) = $self->_stringify_param_check ($item, $option); return unless $r; my $comment = ''; if ($option->{output_comment} && ref $item->{comment} && @{$item->{comment}} > 0) { my @c; for (@{$item->{comment}}) { push @c, '('. $self->Message::Util::encode_ccontent ($_) .')'; } $comment = ' '. join ' ', @c; } if ($item->{no_value}) { push @param, Message::Util::quote_unsafe_string ($item->{attribute}, unsafe => $option->{parameter_no_value_attribute_unsafe_rule}).$comment; } else { my $xparam = 0; my $attribute = $item->{attribute}; return unless length $attribute; my $value = ''.$item->{value}; if ($attribute =~ /$REG{ $option->{parameter_attribute_unsafe_rule} }/) { #return 0; $attribute =~ s/($REG{NON_http_attribute_char})/sprintf('%%%02X', ord $1)/ge; } my %e; if ($option->{output_parameter_extension}) { if ($item->{charset}) { %e = %$item; } else { %e = &{$self->{option}->{hook_encode_string}} ($self, $value, charset => $item->{output_charset} || $option->{encoding_after_encode}, current_charset => $option->{internal_charset}, language => $item->{language}, type => 'parameter/value'); } $xparam = 1 if (length $e{value} > $option->{parameter_value_max_length}) || $e{charset} || $e{language} || $e{value} =~ /\x0D|\x0A/s || $e{value} =~ /$REG{WSP}$REG{WSP}+/s || ($option->{accept_coderange} eq '7bit' && $e{value} =~ /[\x80-\xFF]/) || ($option->{accept_coderange} ne 'binary' && $e{value} =~ /\x00/) ; } else { ## Don't use paramext if ($item->{charset}) { ## But parameter value is undecodable charset value %e = %$item; $xparam = 1; } else { %e = &{$self->{option}->{hook_encode_string}} ($self, $value, charset => $option->{encoding_after_encode}, current_charset => $option->{header_default_charset}, language => $item->{language}, type => 'parameter/value'); } } if ($xparam) { if (length $e{value} > $option->{parameter_value_max_length}) { for my $i (0..(length ($e{value}) /$option->{parameter_value_split_length})) { my $v = substr ($e{value}, $i * $option->{parameter_value_split_length}, $option->{parameter_value_split_length}); if ($i == 0) { $v =~ s/($REG{NON_http_attribute_char})/sprintf('%%%02X', ord $1)/ge; my $charset = Message::MIME::Charset::name_minimumize ($e{charset} || $option->{header_default_charset}, $value); push @param, sprintf q{%s*0*=%s'%s'%s%s}, $attribute, $charset, $e{language}, $v, $comment; } else { # $i > 0 if ($e{charset} || $v =~ /\x0A|\x0D/s || ($option->{accept_coderange} ne 'binary' && $v =~ /\x00/) || ($option->{accept_coderange} eq '7bit' && $v =~ /[\x80-\xFF]/)) { $v =~ s/($REG{NON_http_attribute_char})/sprintf('%%%02X', ord $1)/ge; push @param, sprintf q{%s*%d*=%s}, $attribute, $i, $v; } else { $v = Message::Util::quote_unsafe_string ($v, unsafe => $option->{parameter_value_unsafe_rule}); $v = q{""} if length $v == 0; push @param, sprintf q{%s*%d=%s}, $attribute, $i, $v; } } } } else { unless ($e{charset}) { $e{charset} = Message::MIME::Charset::name_minimumize ($option->{header_default_charset}, $e{value}); } $e{value} =~ s/($REG{NON_http_attribute_char})/sprintf('%%%02X', ord $1)/ge; push @param, sprintf q{%s*=%s'%s'%s%s}, $attribute, $e{charset}, $e{language}, $e{value}, $comment; } } else { $e{value} = Message::Util::quote_unsafe_string ($e{value}, unsafe => $option->{parameter_value_unsafe_rule}); $e{value} = q{""} if length $e{value} == 0; push @param, sprintf '%s=%s%s', $attribute, $e{value}, $comment; } } }, options => \%option ); join $option{separator}, @param; } *as_string = \&stringify; ## $self->_stringify_param_check (\%item, \%option) ## -- Checks parameter (and modify if necessary). ## Returns either 1 (ok) or 0 (don't output) sub _stringify_param_check ($\%\%) { my $self = shift; my ($item, $option) = @_; (1, $item); } ## scan: Inherited ## TODO: ... sub _scan_sort ($\@) { #my $self = shift; @{$_[1]}; } =item $option-value = $p->option ($option-name) Gets option value. =item $p->option ($option-name, $option-value, ...) Set option value(s). You can pass multiple option name-value pair as parameter when setting. =cut ## $self->_option_recursive (\%argv) sub _option_recursive ($\%) { my $self = shift; my $o = shift; for (@{$self->{ $self->{option}->{_HASH_NAME} }}) { $_->{value}->option (%$o) if ref $_ && ref $_->{value}; } } ## value_type: Inherited =item $clone = $p->clone () Returns a copy of the object. =cut ## Inherited =head1 LICENSE Copyright 2002 wakaba Ew@suika.fam.cxE. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; see the file COPYING. If not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. =head1 CHANGE See F. $Date: 2002/08/01 06:42:38 $ =cut 1;