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              
3 1     1   735 use strict;
  1         3  
  1         49  
4 1     1   5 use warnings;
  1         3  
  1         59  
5 1     1   6 use parent 'PAGI::Middleware';
  1         1  
  1         8  
6 1     1   78 use Future::AsyncAwait;
  1         2  
  1         9  
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   8 my ($self, $config) = @_;
61              
62 4   50     19 $self->{param} = $config->{param} // '_method';
63 4   50     13 $self->{header} = $config->{header} // 'x-http-method-override';
64 4   100     14 $self->{allowed_methods} = $config->{allowed_methods} // [qw(PUT PATCH DELETE)];
65 4   50     10 $self->{check_header} = $config->{check_header} // 1;
66 4   50     11 $self->{check_param} = $config->{check_param} // 1;
67              
68             # Build allowed method lookup
69 4         5 $self->{allowed_lookup} = { map { uc($_) => 1 } @{$self->{allowed_methods}} };
  10         30  
  4         8  
70             }
71              
72             sub wrap {
73 4     4 1 30 my ($self, $app) = @_;
74              
75 4     4   84 return async sub {
76 4         6 my ($scope, $receive, $send) = @_;
77 4 50       12 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     11 if (uc($scope->{method} // '') ne 'POST') {
84 1         45 await $app->($scope, $receive, $send);
85 1         105 return;
86             }
87              
88 3         7 my $override_method = $self->_get_override_method($scope);
89              
90 3 50       7 if ($override_method) {
91             # Validate method is allowed
92 3         4 my $upper_method = uc($override_method);
93 3 100       7 if ($self->{allowed_lookup}{$upper_method}) {
94             # Create new scope with overridden method
95             my $new_scope = {
96             %$scope,
97             method => $upper_method,
98             original_method => $scope->{method},
99 2         11 };
100 2         6 await $app->($new_scope, $receive, $send);
101 2         199 return;
102             }
103             }
104              
105 1         3 await $app->($scope, $receive, $send);
106 4         14 };
107             }
108              
109             sub _get_override_method {
110 3     3   4 my ($self, $scope) = @_;
111              
112             # Check header first (most secure)
113 3 50       7 if ($self->{check_header}) {
114 3         7 my $header_name = lc($self->{header});
115 3   50     3 for my $h (@{$scope->{headers} // []}) {
  3         10  
116 2 50       5 if (lc($h->[0]) eq $header_name) {
117 2         5 return $h->[1];
118             }
119             }
120             }
121              
122             # Check query parameter
123 1 50       4 if ($self->{check_param}) {
124 1   50     3 my $query = $scope->{query_string} // '';
125 1         2 my $param_name = $self->{param};
126              
127             # Simple query string parsing
128 1         4 for my $pair (split /&/, $query) {
129 1         3 my ($key, $value) = split /=/, $pair, 2;
130 1   50     2 $key //= '';
131 1   50     2 $value //= '';
132              
133             # URL decode
134 1         3 $key =~ s/\+/ /g;
135 1         2 $key =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/ge;
  0         0  
136 1         2 $value =~ s/\+/ /g;
137 1         2 $value =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/ge;
  0         0  
138              
139 1 50       2 if ($key eq $param_name) {
140 1         3 return $value;
141             }
142             }
143             }
144              
145 0           return;
146             }
147              
148             1;
149              
150             __END__