5 use parent qw(Rose::Object);
8 use List::Util qw(first max min sum);
9 use List::MoreUtils qw(all any apply);
10 use Exporter qw(import);
13 use SL::MoreCommon qw(uri_encode uri_decode);
17 our @EXPORT_OK = qw(flatten unflatten read_cgi_input);
19 use Rose::Object::MakeMethods::Generic
21 'scalar --get_set_init' => [ qw(cgi layout presenter is_ajax type) ],
29 return SL::Layout::None->new;
33 return SL::Presenter->new;
37 return ($ENV{HTTP_X_REQUESTED_WITH} || '') eq 'XMLHttpRequest' ? 1 : 0;
45 my ($self, $topic, $default) = @_;
47 $topic = '::' . (caller(0))[0] . "::$topic" unless $topic =~ m{^::};
49 $self->{_cache} //= {};
50 $self->{_cache}->{$topic} //= ($default // {});
52 return $self->{_cache}->{$topic};
56 my ($target, $key, $value) = @_;
57 my @tokens = split /((?:\[\+?\])?(?:\.)|(?:\[\+?\]))/, $key;
61 $curr = \ $target->{ shift @tokens };
65 my $sep = shift @tokens;
66 my $key = shift @tokens;
68 $curr = \ $$curr->[$#$$curr], next if $sep eq '[]' && @tokens;
69 $curr = \ $$curr->[++$#$$curr], next if $sep eq '[]' && !@tokens;
70 $curr = \ $$curr->[++$#$$curr], next if $sep eq '[+]';
71 $curr = \ $$curr->[max 0, $#$$curr] if $sep eq '[].';
72 $curr = \ $$curr->[++$#$$curr] if $sep eq '[+].';
73 $curr = \ $$curr->{$key}
82 $::lxdebug->enter_sub(2);
84 my ($target, $input, $log) = @_;
85 my @pairs = split(/&/, $input);
88 my ($key, $value) = split(/=/, $_, 2);
90 _store_value($target, uri_decode($key), uri_decode($value));
93 $::lxdebug->add_request_params(uri_decode($key) => uri_decode($value)) if $log;
96 $::lxdebug->leave_sub(2);
99 sub _parse_multipart_formdata {
100 my ($target, $temp_target, $input, $log) = @_;
101 my ($name, $filename, $headers_done, $content_type, $boundary_found, $need_cr, $previous, $p_attachment, $encoding, $transfer_encoding);
104 # teach substr and length to use good ol' bytes, not 'em fancy characters
107 # We SHOULD honor encodings and transfer-encodings here, but as hard as I
108 # looked I couldn't find a reasonably recent webbrowser that makes use of
109 # these. Transfer encoding just eats up bandwidth...
111 # so all I'm going to do is add a fail safe that if anyone ever encounters
112 # this, it's going to croak so that debugging is easier
113 $ENV{'CONTENT_TYPE'} =~ /multipart\/form-data\s*;\s*boundary\s*=\s*(.+)$/;
114 my $boundary = '--' . $1;
118 foreach my $line (split m/\n/, $input) {
119 $line_length = length $line;
121 if ($line =~ /^\Q$boundary\E(--)?\r?$/) {
122 my $last_boundary = $1;
123 my $data = substr $input, $data_start, $index - $data_start;
126 if ($previous && !$filename && $transfer_encoding && $transfer_encoding ne 'binary') {
127 ${ $previous } = Encode::decode($encoding, $data);
129 ${ $previous } = $data;
131 $::lxdebug->add_request_params($name, $$previous) if $log;
137 $content_type = "text/plain";
141 $transfer_encoding = undef;
142 last if $last_boundary;
146 next unless $boundary_found;
148 if (!$headers_done) {
149 $line =~ s/[\r\n]*$//;
153 $data_start = $index + $line_length + 1;
157 if ($line =~ m|^content-disposition\s*:.*?form-data\s*;|i) {
158 if ($line =~ m|filename\s*=\s*"(.*?)"|i) {
160 substr $line, $-[0], $+[0] - $-[0], "";
163 if ($line =~ m|name\s*=\s*"(.*?)"|i) {
165 substr $line, $-[0], $+[0] - $-[0], "";
169 # legacy, some old upload routines expect this to be here
170 $temp_target->{FILENAME} = $filename if defined $filename;
172 # Name can potentially be both a normal variable or a file upload.
173 # A file upload can be identified by its "filename" attribute.
174 # The thing is, if a [+] clause vivifies structure in one of the
175 # branches it must be done in both, or subsequent "[]" will fail
176 my $temp_target_slot = _store_value($temp_target, $name);
177 my $target_slot = _store_value($target, $name);
179 # set the reference for appending of multiline data to the correct one
180 $previous = defined $filename ? $target_slot : $temp_target_slot;
182 # for multiple uploads: save the attachments in a SL/Mailer like structure
183 if (defined $filename) {
184 my $target_attachment = _store_value($target, "ATTACHMENTS.$name", {});
185 my $temp_target_attachment = _store_value($temp_target, "ATTACHMENTS.$name", {});
187 $$target_attachment->{data} = $previous;
188 $$temp_target_attachment->{filename} = $filename;
190 $p_attachment = $$temp_target_attachment;
197 if ($line =~ m|^content-type\s*:\s*(.*?)[;\$]|i) {
199 $p_attachment->{content_type} = $1;
201 if ($content_type =~ /^text/ && $line =~ m|;\s*charset\s*:\s*("?)(.*?)\1$|i) {
208 if ($line =~ m|^content-transfer-encoding\s*=\s*(.*?)$|i) {
209 $transfer_encoding = lc($1);
210 if ($transfer_encoding && $transfer_encoding !~ /^[78]bit|binary$/) {
211 die 'Transfer encodings beyond 7bit/8bit and binary are not implemented.';
213 $p_attachment->{transfer_encoding} = $transfer_encoding;
221 next unless $previous;
224 $index += $line_length + 1;
227 $::lxdebug->leave_sub(2);
230 sub _recode_recursively {
231 $::lxdebug->enter_sub;
232 my ($iconv, $from, $to) = @_;
234 if (any { ref $from eq $_ } qw(Form HASH)) {
235 for my $key (keys %{ $from }) {
236 if (!ref $from->{$key}) {
237 # Workaround for a bug: converting $from->{$key} directly
238 # leads to 'undef'. I don't know why. Converting a copy works,
240 $to->{$key} = $iconv->convert("" . $from->{$key}) if defined $from->{$key} && !defined $to->{$key};
242 $to->{$key} ||= {} if 'HASH' eq ref $from->{$key};
243 $to->{$key} ||= [] if 'ARRAY' eq ref $from->{$key};
244 _recode_recursively($iconv, $from->{$key}, $to->{$key});
248 } elsif (ref $from eq 'ARRAY') {
249 foreach my $idx (0 .. scalar(@{ $from }) - 1) {
250 if (!ref $from->[$idx]) {
251 # Workaround for a bug: converting $from->[$idx] directly
252 # leads to 'undef'. I don't know why. Converting a copy works,
254 $to->[$idx] = $iconv->convert("" . $from->[$idx]);
256 $to->[$idx] ||= {} if 'HASH' eq ref $from->[$idx];
257 $to->[$idx] ||= [] if 'ARRAY' eq ref $from->[$idx];
258 _recode_recursively($iconv, $from->[$idx], $to->[$idx]);
262 $main::lxdebug->leave_sub();
266 $::lxdebug->enter_sub;
270 # yes i know, copying all those values around isn't terribly efficient, but
271 # the old version of dumping everything into form and then launching a
272 # tactical recode nuke at the data is still worse.
274 # this way the data can at least be recoded on the fly as soon as we get to
275 # know the source encoding and only in the cases where encoding may be hidden
276 # among the payload we take the hit of copying the request around
277 my $temp_target = { };
279 # since both of these can potentially bring their encoding in INPUT_ENCODING
280 # they get dumped into temp_target
281 _input_to_hash($temp_target, $ENV{QUERY_STRING}, 1) if $ENV{QUERY_STRING};
282 _input_to_hash($temp_target, $ARGV[0], 1) if @ARGV && $ARGV[0];
284 if ($ENV{CONTENT_LENGTH}) {
286 read STDIN, $content, $ENV{CONTENT_LENGTH};
287 if ($ENV{'CONTENT_TYPE'} && $ENV{'CONTENT_TYPE'} =~ /multipart\/form-data/) {
288 # multipart formdata can bring it's own encoding, so give it both
289 # and let it decide on it's own
290 _parse_multipart_formdata($target, $temp_target, $content, 1);
292 # normal encoding must be recoded
293 _input_to_hash($temp_target, $content, 1);
297 my $encoding = delete $temp_target->{INPUT_ENCODING} || 'UTF-8';
299 _recode_recursively(SL::Iconv->new($encoding, 'UTF-8'), $temp_target => $target) if keys %$target;
301 if ($target->{RESTORE_FORM_FROM_SESSION_ID}) {
303 $::auth->restore_form_from_session(delete $target->{RESTORE_FORM_FROM_SESSION_ID}, form => \%temp_form);
304 _store_value($target, $_, $temp_form{$_}) for keys %temp_form;
307 $::lxdebug->leave_sub;
313 my ($source, $target, $prefix, $in_array) = @_;
316 # There are two edge cases that need attention. First: more than one hash
317 # inside an array. Only the first of each nested can have a [+]. Second: if
318 # an array contains mixed values _store_value will rely on autovivification.
319 # So any type change must have a [+]
320 # This closure decides one recursion step AFTER an array has been found if a
321 # [+] needs to be generated
322 my $arr_prefix = sub {
323 return $_[0] ? '[+]' : '[]' if $in_array;
330 for my $key (sort keys %$source) {
331 flatten($source->{$key} => $target, (defined $prefix ? $prefix . $arr_prefix->($first) . '.' : '') . $key);
337 for my $i (0 .. $#$source) {
338 flatten($source->[$i] => $target, $prefix . $arr_prefix->($i == 0), '1');
343 die "can't flatten a pure scalar" unless defined $prefix;
344 push @$target, [ $prefix . $arr_prefix->(0) => $source ];
347 die "unrecognized reference of a data structure $_. cannot serialize refs, globs and code yet. to serialize Form please use the method there";
355 my ($data, $target) = @_;
358 for my $pair (@$data) {
359 _store_value($target, @$pair) if defined $pair->[0];
371 SL::Request.pm - request parsing, data serialization, request information
375 This module handles unpacking of CGI parameters. It also gives
376 information about the request, such as whether or not it was done via AJAX,
377 or the requested content type.
379 use SL::Request qw(read_cgi_input);
381 # read cgi input depending on request type, unflatten and recode
382 read_cgi_input($target_hash_ref);
384 # $hashref and $new_hashref should be identical
385 my $new_arrayref = flatten($hashref);
386 my $new_hashref = unflatten($new_arrayref);
388 # Handle AJAX requests differently than normal requests:
389 if ($::request->is_ajax) {
390 $controller->render('json-mask', { type => 'json' });
392 $controller->render('full-mask');
397 This module provides information about the request made by the
400 It also handles flattening and unflattening of data for request
401 roundtrip purposes. kivitendo uses the format as described below:
407 Hash entries will be connected with a dot (C<.>). A simple hash like this
414 will be serialized to
417 [ order.customer => 5 ],
421 Arrays will be marked by empty brackets (C<[]>). A hash like this
423 selected_id => [ 2, 6, 8, 9 ]
427 [ selected_id[] => 2 ],
428 [ selected_id[] => 6 ],
429 [ selected_id[] => 8 ],
430 [ selected_id[] => 9 ],
432 Since this will produce identical keys, the resulting flattened list can not be
433 used as a hash. It is however very easy to use this in a template to generate
436 [% FOREACH id = selected_ids %]
437 <input type="hidden" name="selected_id[]" value="[% id | html %]">
440 =item Nested structures
442 A special version of this are nested hashes in an array, which is very common.
443 The combined operator (C<[].>) will be used. As a special case, every time a new
444 array slice is started, the special convention (C<[+].>) will be used. Again this
445 is because it's easy to write a template with it.
464 [ order.orderitems[+].id => 1 ],
465 [ order.orderitems[].part => 15 ],
466 [ order.orderitems[+].id => 2 ],
467 [ order.orderitems[].part => 7 ],
471 The format currently does have certain limitations when compared to other
472 serialization formats.
478 The order of serialized values matters to reconstruct arrays properly. This
479 should rarely be a problem if you just flatten and dump into a url or a field
484 The current implementation of flatten does produce correct serialization of
485 empty keys, but unflatten is unable to resolve these. Do no use C<''> or
486 C<undef> as keys. C<0> is fine.
490 You cannot use the tokens C<[]>, C<[+]> and C<.> in keys. No way around it.
494 It is not possible to serialize something like
496 sparse_array => do { my $sa = []; $sa[100] = 1; $sa },
498 This is a feature, as perl doesn't do well with very large arrays.
502 There is currently no support nor prevention for flattening a circular structure.
504 =item Custom Delimiter
506 No support for other delimiters, sorry.
508 =item Other References
510 No support for globs, scalar refs, code refs, filehandles and the like. These will die.
520 =item C<flatten HASHREF [ ARRAYREF ]>
522 This function will flatten the provided hash ref into the provided array ref.
523 The array ref may be non empty, but will be changed in this case.
525 The return value is the flattened array ref.
527 =item C<unflatten ARRAYREF [ HASHREF ]>
529 This function will parse the array ref, and will store the contents into the hash ref. The hash ref may be non empty, in this case any new keys will override the old ones only on leafs with same type. Type changes on a node will die.
533 Returns trueish if the request is an XML HTTP request, also known as
538 Returns the requested content type (either C<html>, C<js> or C<json>).
542 Set and retrieve the layout object for the current request. Must be an instance
543 of L<SL::Layout::Base>. Defaults to an instance of L<SL::Layout::None>.
545 For more information about layouts, see L<SL::Layout::Dispatcher>.
547 =item C<cache $topic[, $default ]>
549 Caches an item for the duration of the request. C<$topic> must be an
550 index name referring to the thing to cache. It is used for retrieving
551 it later on. If C<$topic> doesn't start with C<::> then the caller's
552 package name is prepended to the topic. For example, if the a from
553 package C<SL::StuffedStuff> calls with topic = C<get_stuff> then the
554 actual key will be C<::SL::StuffedStuff::get_stuff>.
556 If no item exists in the cache for C<$topic> then it is created and
557 its initial value is set to C<$default>. If C<$default> is not given
558 (undefined) then a new, empty hash reference is created.
560 Returns the cached item.
564 =head1 SPECIAL FUNCTIONS
566 =head2 C<_store_value()>
568 Parses a complex var name, and stores it in the form.
571 _store_value($target, $key, $value);
573 Keys must start with a string, and can contain various tokens.
574 Supported key structures are:
577 Simple key strings work as expected
582 Separating two keys by a dot (.) will result in a hash lookup for the inner value
583 This is similar to the behaviour of java and templating mechanisms.
585 filter.description => $form->{filter}->{description}
587 3. array+hashref access
589 Adding brackets ([]) before the dot will cause the next hash to be put into an array.
590 Using [+] instead of [] will force a new array index. This is useful for recurring
591 data structures like part lists. Put a [+] into the first varname, and use [] on the
594 Repeating these names in your template:
597 invoice.items[].parts_id
601 $form->{invoice}->{items}->[
615 Using brackets at the end of a name will result in the creation of a pure array.
616 Note that you mustn't use [+], which is reserved for array+hash access and will
617 result in undefined behaviour in array context.
619 filter.status[] => $form->{status}->[ val1, val2, ... ]