File Coverage

blib/lib/PAGI/Middleware/MethodOverride.pm
Criterion Covered Total %
statement 57 62 91.9
branch 10 16 62.5
condition 11 20 55.0
subroutine 8 8 100.0
pod 1 1 100.0
total 87 107 81.3


line stmt bran cond sub pod time code
1             package PAGI::Middleware::MethodOverride;
2             $PAGI::Middleware::MethodOverride::VERSION = '0.002000';
3 1     1   537 use strict;
  1         2  
  1         32  
4 1     1   5 use warnings;
  1         1  
  1         67  
5 1     1   4 use parent 'PAGI::Middleware';
  1         1  
  1         5  
6 1     1   50 use Future::AsyncAwait;
  1         2  
  1         5  
7              
8             =head1 NAME
9              
10             PAGI::Middleware::MethodOverride - Override HTTP method from request data
11              
12             =head1 SYNOPSIS
13              
14             use PAGI::Middleware::Builder;
15              
16             my $app = builder {
17             enable 'MethodOverride',
18             param => '_method',
19             allowed_methods => [qw(PUT PATCH DELETE)];
20             $my_app;
21             };
22              
23             =head1 DESCRIPTION
24              
25             PAGI::Middleware::MethodOverride allows overriding the HTTP method
26             using a form field, query parameter, or header. This enables HTML
27             forms (which only support GET and POST) to submit PUT, PATCH, and
28             DELETE requests.
29              
30             =head1 CONFIGURATION
31              
32             =over 4
33              
34             =item * param (default: '_method')
35              
36             Form field or query parameter name for method override.
37              
38             =item * header (default: 'X-HTTP-Method-Override')
39              
40             HTTP header name for method override.
41              
42             =item * allowed_methods (default: [PUT, PATCH, DELETE])
43              
44             Methods that can be overridden to. GET and POST are not allowed
45             for security reasons.
46              
47             =item * check_header (default: 1)
48              
49             Check the X-HTTP-Method-Override header.
50              
51             =item * check_param (default: 1)
52              
53             Check the _method query/form parameter.
54              
55             =back
56              
57             =cut
58              
59             sub _init {
60 4     4   13 my ($self, $config) = @_;
61              
62 4   50     34 $self->{param} = $config->{param} // '_method';
63 4   50     61 $self->{header} = $config->{header} // 'x-http-method-override';
64 4   100     23 $self->{allowed_methods} = $config->{allowed_methods} // [qw(PUT PATCH DELETE)];
65 4   50     19 $self->{check_header} = $config->{check_header} // 1;
66 4   50     27 $self->{check_param} = $config->{check_param} // 1;
67              
68             # Build allowed method lookup
69 4         9 $self->{allowed_lookup} = { map { uc($_) => 1 } @{$self->{allowed_methods}} };
  10         46  
  4         12  
70             }
71              
72             sub wrap {
73 4     4 1 52 my ($self, $app) = @_;
74              
75 4     4   155 return async sub {
76 4         12 my ($scope, $receive, $send) = @_;
77 4 50       16 if ($scope->{type} ne 'http') {
78 0         0 await $app->($scope, $receive, $send);
79 0         0 return;
80             }
81              
82             # Only apply to POST requests
83 4 100 50     20 if (uc($scope->{method} // '') ne 'POST') {
84 1         5 await $app->($scope, $receive, $send);
85 1         151 return;
86             }
87              
88 3         11 my $override_method = $self->_get_override_method($scope);
89              
90 3 50       10 if ($override_method) {
91             # Validate method is allowed
92 3         9 my $upper_method = uc($override_method);
93 3 100       13 if ($self->{allowed_lookup}{$upper_method}) {
94             # Create new scope with overridden method
95             my $new_scope = $self->modify_scope($scope, {
96             method => $upper_method,
97             original_method => $scope->{method},
98 2         23 });
99 2         11 await $app->($new_scope, $receive, $send);
100 2         273 return;
101             }
102             }
103              
104 1         4 await $app->($scope, $receive, $send);
105 4         24 };
106             }
107              
108             sub _get_override_method {
109 3     3   8 my ($self, $scope) = @_;
110              
111             # Check header first (most secure)
112 3 50       10 if ($self->{check_header}) {
113 3         12 my $header_name = lc($self->{header});
114 3   50     6 for my $h (@{$scope->{headers} // []}) {
  3         15  
115 2 50       10 if (lc($h->[0]) eq $header_name) {
116 2         8 return $h->[1];
117             }
118             }
119             }
120              
121             # Check query parameter
122 1 50       5 if ($self->{check_param}) {
123 1   50     6 my $query = $scope->{query_string} // '';
124 1         3 my $param_name = $self->{param};
125              
126             # Simple query string parsing
127 1         5 for my $pair (split /&/, $query) {
128 1         72 my ($key, $value) = split /=/, $pair, 2;
129 1   50     6 $key //= '';
130 1   50     4 $value //= '';
131              
132             # URL decode
133 1         4 $key =~ s/\+/ /g;
134 1         3 $key =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/ge;
  0         0  
135 1         4 $value =~ s/\+/ /g;
136 1         4 $value =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/ge;
  0         0  
137              
138 1 50       4 if ($key eq $param_name) {
139 1         5 return $value;
140             }
141             }
142             }
143              
144 0           return;
145             }
146              
147             1;
148              
149             __END__