Filename | /var/www/foswikidev/core/lib/Foswiki/UI/Rest.pm |
Statements | Executed 53 statements in 8.07ms |
Calls | P | F | Exclusive Time |
Inclusive Time |
Subroutine |
---|---|---|---|---|---|
18 | 1 | 1 | 97µs | 97µs | registerRESTHandler | Foswiki::UI::Rest::
1 | 1 | 1 | 13µs | 26µs | BEGIN@13 | Foswiki::UI::Rest::
1 | 1 | 1 | 10µs | 20µs | BEGIN@276 | Foswiki::UI::Rest::
1 | 1 | 1 | 10µs | 23µs | BEGIN@274 | Foswiki::UI::Rest::
1 | 1 | 1 | 9µs | 13µs | BEGIN@14 | Foswiki::UI::Rest::
1 | 1 | 1 | 9µs | 136µs | BEGIN@16 | Foswiki::UI::Rest::
1 | 1 | 1 | 4µs | 4µs | BEGIN@17 | Foswiki::UI::Rest::
1 | 1 | 1 | 4µs | 4µs | BEGIN@19 | Foswiki::UI::Rest::
1 | 1 | 1 | 3µs | 3µs | BEGIN@15 | Foswiki::UI::Rest::
0 | 0 | 0 | 0s | 0s | __ANON__[:277] | Foswiki::UI::Rest::
0 | 0 | 0 | 0s | 0s | __ANON__[:290] | Foswiki::UI::Rest::
0 | 0 | 0 | 0s | 0s | _listHandlers | Foswiki::UI::Rest::
0 | 0 | 0 | 0s | 0s | getRegisteredHandlers | Foswiki::UI::Rest::
0 | 0 | 0 | 0s | 0s | rest | Foswiki::UI::Rest::
Line | State ments |
Time on line |
Calls | Time in subs |
Code |
---|---|---|---|---|---|
1 | # See bottom of file for license and copyright information | ||||
2 | |||||
3 | =begin TML | ||||
4 | |||||
5 | ---+ package Foswiki::UI::Rest | ||||
6 | |||||
7 | UI delegate for REST interface | ||||
8 | |||||
9 | =cut | ||||
10 | |||||
11 | package Foswiki::UI::Rest; | ||||
12 | |||||
13 | 2 | 28µs | 2 | 40µs | # spent 26µs (13+13) within Foswiki::UI::Rest::BEGIN@13 which was called:
# once (13µs+13µs) by Foswiki::Func::registerRESTHandler at line 13 # spent 26µs making 1 call to Foswiki::UI::Rest::BEGIN@13
# spent 13µs making 1 call to strict::import |
14 | 2 | 23µs | 2 | 18µs | # spent 13µs (9+4) within Foswiki::UI::Rest::BEGIN@14 which was called:
# once (9µs+4µs) by Foswiki::Func::registerRESTHandler at line 14 # spent 13µs making 1 call to Foswiki::UI::Rest::BEGIN@14
# spent 4µs making 1 call to warnings::import |
15 | 2 | 23µs | 1 | 3µs | # spent 3µs within Foswiki::UI::Rest::BEGIN@15 which was called:
# once (3µs+0s) by Foswiki::Func::registerRESTHandler at line 15 # spent 3µs making 1 call to Foswiki::UI::Rest::BEGIN@15 |
16 | 2 | 30µs | 2 | 263µs | # spent 136µs (9+127) within Foswiki::UI::Rest::BEGIN@16 which was called:
# once (9µs+127µs) by Foswiki::Func::registerRESTHandler at line 16 # spent 136µs making 1 call to Foswiki::UI::Rest::BEGIN@16
# spent 127µs making 1 call to Error::import |
17 | 2 | 43µs | 1 | 4µs | # spent 4µs within Foswiki::UI::Rest::BEGIN@17 which was called:
# once (4µs+0s) by Foswiki::Func::registerRESTHandler at line 17 # spent 4µs making 1 call to Foswiki::UI::Rest::BEGIN@17 |
18 | |||||
19 | # spent 4µs within Foswiki::UI::Rest::BEGIN@19 which was called:
# once (4µs+0s) by Foswiki::Func::registerRESTHandler at line 24 | ||||
20 | 1 | 5µs | if ( $Foswiki::cfg{UseLocale} ) { | ||
21 | require locale; | ||||
22 | import locale(); | ||||
23 | } | ||||
24 | 1 | 816µs | 1 | 4µs | } # spent 4µs making 1 call to Foswiki::UI::Rest::BEGIN@19 |
25 | |||||
26 | 1 | 300ns | our %restDispatch; | ||
27 | |||||
28 | # Used by Plugin diagnostics to access all the registered handlers | ||||
29 | sub getRegisteredHandlers { | ||||
30 | return \%restDispatch; | ||||
31 | } | ||||
32 | |||||
33 | =begin TML | ||||
34 | |||||
35 | ---++ StaticMethod registerRESTHandler( $subject, $verb, \&fn, %options ) | ||||
36 | |||||
37 | Adds a function to the dispatch table of the REST interface | ||||
38 | for a given subject. See System.CommandAndCGIScripts#rest for more info. | ||||
39 | |||||
40 | * =$subject= - The subject under which the function will be registered. | ||||
41 | * =$verb= - The verb under which the function will be registered. | ||||
42 | * =\&fn= - Reference to the function. | ||||
43 | |||||
44 | The handler function must be of the form: | ||||
45 | <verbatim> | ||||
46 | sub handler(\%session, $subject, $verb, $response) -> $text | ||||
47 | </verbatim> | ||||
48 | where: | ||||
49 | * =\%session= - a reference to the Foswiki session object (may be ignored) | ||||
50 | * =$subject= - The invoked subject (may be ignored) | ||||
51 | * =$verb= - The invoked verb (may be ignored) | ||||
52 | * =$response= reference to the Foswiki::Response object that is used to compose a reply to the request | ||||
53 | |||||
54 | If the =redirectto= parameter is not present on the REST request, then the return | ||||
55 | value from the handler is used to determine the endpoint for the | ||||
56 | request. It can be: | ||||
57 | * =undef= - causes the core to assume the handler handled the complete | ||||
58 | request i.e. the core will not generate any response to the request | ||||
59 | * =text= - any other non-undef value will be written out as the content | ||||
60 | of an HTTP 200 response. Only the standard headers in the response are | ||||
61 | written. | ||||
62 | |||||
63 | Additional options are set in the =%options= hash. These options are important | ||||
64 | to ensuring that requests to your handler can't be used in cross-scripting | ||||
65 | attacks, or used for phishing. | ||||
66 | * =authenticate= - use this boolean option to require authentication for the | ||||
67 | handler. If this is set, then an authenticated session must be in place | ||||
68 | or the REST call will be rejected with a 401 (Unauthorized) status code. | ||||
69 | By default, rest handlers do *not* require authentication. | ||||
70 | * =validate= - use this boolean option to require validation of any requests | ||||
71 | made to this handler. | ||||
72 | By default, requests made to REST handlers are not validated. | ||||
73 | * =http_allow= use this option to specify the HTTP methods that can | ||||
74 | be used to invoke the handler. | ||||
75 | |||||
76 | =cut | ||||
77 | |||||
78 | # spent 97µs within Foswiki::UI::Rest::registerRESTHandler which was called 18 times, avg 5µs/call:
# 18 times (97µs+0s) by Foswiki::Func::registerRESTHandler at line 772 of /var/www/foswikidev/core/lib/Foswiki/Func.pm, avg 5µs/call | ||||
79 | 18 | 25µs | my ( $subject, $verb, $fnref, %options ) = @_; | ||
80 | |||||
81 | 18 | 6.71ms | $restDispatch{$subject}{$verb} = { | ||
82 | function => $fnref, | ||||
83 | %options | ||||
84 | }; | ||||
85 | } | ||||
86 | |||||
87 | sub rest { | ||||
88 | my ( $session, %initialContext ) = @_; | ||||
89 | |||||
90 | my $req = $session->{request}; | ||||
91 | my $res = $session->{response}; | ||||
92 | my $err; | ||||
93 | |||||
94 | # Referer is useful for logging REST request errors | ||||
95 | my $referer = ( defined $ENV{HTTP_REFERER} ) ? $ENV{HTTP_REFERER} : ''; | ||||
96 | |||||
97 | # Must define topic param in the query to avoid plugins being | ||||
98 | # passed the path_info when the are initialised. We can't affect | ||||
99 | # the path_info, but we *can* persuade Foswiki to ignore it. | ||||
100 | my $topic = $req->param('topic'); | ||||
101 | if ($topic) { | ||||
102 | unless ( $topic =~ m/\.|\// ) { | ||||
103 | $res->header( -type => 'text/html', -status => '400' ); | ||||
104 | $err = 'ERROR: (400) Invalid REST invocation' | ||||
105 | . " - Invalid topic parameter $topic\n"; | ||||
106 | $res->print($err); | ||||
107 | $session->logger->log( 'warning', "REST rejected: " . $err, | ||||
108 | " - $referer", ); | ||||
109 | throw Foswiki::EngineException( 400, $err, $res ); | ||||
110 | } | ||||
111 | } | ||||
112 | else { | ||||
113 | |||||
114 | # No topic specified, but we still have to set a topic to stop | ||||
115 | # plugins being passed the subject and verb in place of a topic. | ||||
116 | Foswiki::Func::popTopicContext(); | ||||
117 | Foswiki::Func::pushTopicContext( $Foswiki::cfg{UsersWebName}, | ||||
118 | $Foswiki::cfg{HomeTopicName} ); | ||||
119 | } | ||||
120 | |||||
121 | return | ||||
122 | if $session->satisfiedByCache( 'rest', $session->{webName}, | ||||
123 | $session->{topicName} ); | ||||
124 | |||||
125 | Foswiki::Func::writeDebug( | ||||
126 | "computing REST for $session->{webName}.$session->{topicName}") | ||||
127 | if Foswiki::PageCache::TRACE(); | ||||
128 | |||||
129 | my $pathInfo = Foswiki::urlDecode( $req->path_info() ); | ||||
130 | |||||
131 | # Foswiki rest invocations are defined as having a subject (pluginName) | ||||
132 | # and verb (restHandler in that plugin). Make sure the path_info is | ||||
133 | # well-structured. | ||||
134 | unless ( $pathInfo =~ m#/(.*?)[./]([^/]*)# ) { | ||||
135 | |||||
136 | $res->header( -type => 'text/html', -status => '400' ); | ||||
137 | $err = | ||||
138 | "ERROR: (400) Invalid REST invocation - $pathInfo is malformed\n"; | ||||
139 | $res->print($err); | ||||
140 | $session->logger->log( 'warning', "REST rejected: " . $err, | ||||
141 | " - $referer", ); | ||||
142 | _listHandlers($res) if $session->inContext('command_line'); | ||||
143 | throw Foswiki::EngineException( 400, $err, $res ); | ||||
144 | } | ||||
145 | |||||
146 | # Implicit untaint OK - validated later | ||||
147 | my ( $subject, $verb ) = ( $1, $2 ); | ||||
148 | |||||
149 | my $record = $restDispatch{$subject}{$verb}; | ||||
150 | |||||
151 | # Check we have this handler | ||||
152 | unless ($record) { | ||||
153 | $res->header( -type => 'text/html', -status => '404' ); | ||||
154 | $err = | ||||
155 | 'ERROR: (404) Invalid REST invocation - ' | ||||
156 | . $pathInfo | ||||
157 | . ' does not refer to a known handler'; | ||||
158 | _listHandlers($res) if $session->inContext('command_line'); | ||||
159 | $session->logger->log( 'warning', "REST rejected: " . $err, | ||||
160 | " - $referer", ); | ||||
161 | $res->print($err); | ||||
162 | throw Foswiki::EngineException( 404, $err, $res ); | ||||
163 | } | ||||
164 | |||||
165 | # Log warnings if defaults are needed. | ||||
166 | if ( !defined $record->{http_allow} | ||||
167 | || !defined $record->{authenticate} | ||||
168 | || !defined $record->{validate} ) | ||||
169 | { | ||||
170 | my $msg; | ||||
171 | if ( $Foswiki::cfg{LegacyRESTSecurity} ) { | ||||
172 | $msg = | ||||
173 | 'WARNING: This REST handler does not specify http_allow, validate and/or authenticate. LegacyRESTSecurity is enabled. This handler may be insecure and should be examined:'; | ||||
174 | } | ||||
175 | else { | ||||
176 | $msg = | ||||
177 | 'WARNING: This REST handler does not specify http_allow, validate and/or authenticate. Foswiki has chosen secure defaults:'; | ||||
178 | $record->{http_allow} = 'POST' unless defined $record->{http_allow}; | ||||
179 | $record->{authenticate} = 1 unless defined $record->{authenticate}; | ||||
180 | $record->{validate} = 1 unless defined $record->{validate}; | ||||
181 | } | ||||
182 | $session->logger->log( 'warning', $msg, " $subject/$verb - $referer", ); | ||||
183 | } | ||||
184 | |||||
185 | # Check the method is allowed | ||||
186 | if ( $record->{http_allow} && defined $req->method() ) { | ||||
187 | unless ( $session->inContext('command_line') ) { | ||||
188 | my %allowed = | ||||
189 | map { $_ => 1 } split( /[,\s]+/, $record->{http_allow} ); | ||||
190 | unless ( $allowed{ uc( $req->method() ) } ) { | ||||
191 | $res->header( -type => 'text/html', -status => '405' ); | ||||
192 | $err = | ||||
193 | 'ERROR: (405) Bad Request: ' | ||||
194 | . uc( $req->method() ) | ||||
195 | . ' denied'; | ||||
196 | $session->logger->log( | ||||
197 | 'warning', | ||||
198 | "REST rejected: " . $err, | ||||
199 | " $subject/$verb - $referer", | ||||
200 | ); | ||||
201 | $res->print($err); | ||||
202 | throw Foswiki::EngineException( 405, $err, $res ); | ||||
203 | } | ||||
204 | } | ||||
205 | } | ||||
206 | |||||
207 | # Check someone is logged in | ||||
208 | if ( $record->{authenticate} ) { | ||||
209 | |||||
210 | # no need to exempt cli. LoginManager sets authenticated correctly. | ||||
211 | unless ( $session->inContext('authenticated') | ||||
212 | || $Foswiki::cfg{LoginManager} eq 'none' ) | ||||
213 | { | ||||
214 | $res->header( -type => 'text/html', -status => '401' ); | ||||
215 | $err = "ERROR: (401) $pathInfo requires you to be logged in"; | ||||
216 | $session->logger->log( | ||||
217 | 'warning', | ||||
218 | "REST rejected: " . $err, | ||||
219 | " $subject/$verb - $referer" | ||||
220 | ); | ||||
221 | $res->print($err); | ||||
222 | throw Foswiki::EngineException( 401, $err, $res ); | ||||
223 | } | ||||
224 | } | ||||
225 | |||||
226 | # Validate the request | ||||
227 | # SMELL: We can't use Foswiki::UI::checkValidationKey. | ||||
228 | # The common reoutine expires the key, but if we expired it, | ||||
229 | # then subsequent requests using the same code would have to be | ||||
230 | # interactively confirmed, which isn't really an option with | ||||
231 | # an XHR. Also, the common routine throws a ValidationException | ||||
232 | # and we want a simple engine exception here. | ||||
233 | if ( $record->{validate} | ||||
234 | && $Foswiki::cfg{Validation}{Method} ne 'none' | ||||
235 | && !$session->inContext('command_line') | ||||
236 | && uc( $req->method() eq 'POST' ) ) | ||||
237 | { | ||||
238 | |||||
239 | my $nonce = $req->param('validation_key'); | ||||
240 | if ( | ||||
241 | !defined($nonce) | ||||
242 | || !Foswiki::Validation::isValidNonce( | ||||
243 | $session->getCGISession(), $nonce | ||||
244 | ) | ||||
245 | ) | ||||
246 | { | ||||
247 | $res->header( -type => 'text/html', -status => '403' ); | ||||
248 | $err = "ERROR: (403) Invalid validation code"; | ||||
249 | $session->logger->log( | ||||
250 | 'warning', | ||||
251 | "REST rejected: " . $err, | ||||
252 | " $subject/$verb - $referer" | ||||
253 | ); | ||||
254 | $res->print($err); | ||||
255 | throw Foswiki::EngineException( 403, $err, $res ); | ||||
256 | } | ||||
257 | } | ||||
258 | |||||
259 | my $function = $record->{function}; | ||||
260 | |||||
261 | $session->logger->log( | ||||
262 | { | ||||
263 | level => 'info', | ||||
264 | action => 'rest', | ||||
265 | webTopic => $session->{webName} . '.' . $session->{topicName}, | ||||
266 | extra => "$subject $verb", | ||||
267 | } | ||||
268 | ); | ||||
269 | |||||
270 | my $result; | ||||
271 | my $error = 0; | ||||
272 | |||||
273 | try { | ||||
274 | 2 | 42µs | 2 | 37µs | # spent 23µs (10+14) within Foswiki::UI::Rest::BEGIN@274 which was called:
# once (10µs+14µs) by Foswiki::Func::registerRESTHandler at line 274 # spent 23µs making 1 call to Foswiki::UI::Rest::BEGIN@274
# spent 14µs making 1 call to strict::unimport |
275 | $result = &$function( $session, $subject, $verb, $session->{response} ); | ||||
276 | 2 | 314µs | 2 | 29µs | # spent 20µs (10+9) within Foswiki::UI::Rest::BEGIN@276 which was called:
# once (10µs+9µs) by Foswiki::Func::registerRESTHandler at line 276 # spent 20µs making 1 call to Foswiki::UI::Rest::BEGIN@276
# spent 9µs making 1 call to strict::import |
277 | } | ||||
278 | catch Error::Simple with { | ||||
279 | |||||
280 | # Note: we're *not* catching Error here, just Error::Simple | ||||
281 | # so we catch things like OopsException | ||||
282 | $session->{response}->header( | ||||
283 | -status => 500, | ||||
284 | -type => 'text/plain', | ||||
285 | -charset => 'UTF-8' | ||||
286 | ); | ||||
287 | $session->{response}->print( | ||||
288 | 'ERROR: (500) Internal server error - ' . shift->stringify() ); | ||||
289 | $error = 1; | ||||
290 | }; | ||||
291 | |||||
292 | if ( !$error ) { | ||||
293 | |||||
294 | # endpoint is now deprecated, but may still be | ||||
295 | # used by old rest handlers to redirect to an alternate topic. | ||||
296 | # Note that this might be better validated before dispatching | ||||
297 | # the rest handler however come handlers modify | ||||
298 | # the endPoint and validating it early fails. | ||||
299 | |||||
300 | # endPoint still supported for compatibility | ||||
301 | my $target = $session->redirectto( scalar( $req->param('endPoint') ) ); | ||||
302 | |||||
303 | if ( defined($target) ) { | ||||
304 | $session->redirect($target); | ||||
305 | } | ||||
306 | else { | ||||
307 | if ( defined $req->param('redirectto') | ||||
308 | || defined $req->param('endPoint') ) | ||||
309 | { | ||||
310 | $session->{response}->header( | ||||
311 | -status => 403, | ||||
312 | -type => 'text/plain', | ||||
313 | -charset => 'UTF-8' | ||||
314 | ); | ||||
315 | $session->{response} | ||||
316 | ->print( 'ERROR: (403) Invalid REST invocation - ' | ||||
317 | . ' redirectto does not refer to a valid redirect target' | ||||
318 | ); | ||||
319 | return; | ||||
320 | } | ||||
321 | } | ||||
322 | } | ||||
323 | |||||
324 | if ($result) { | ||||
325 | |||||
326 | # If the handler doesn't want to handle all the details of the | ||||
327 | # response, they can return a page here and get it 200'd | ||||
328 | $session->writeCompletePage($result); | ||||
329 | } | ||||
330 | |||||
331 | # Otherwise it's assumed that the handler dealt with the response. | ||||
332 | } | ||||
333 | |||||
334 | sub _listHandlers { | ||||
335 | $_[0]->print( | ||||
336 | "\nusage: ./rest /PluginName/restHandler param=value\n\n" | ||||
337 | . join( "\n", | ||||
338 | map { $_ . ' : ' . join( ' , ', keys( %{ $restDispatch{$_} } ) ) } | ||||
339 | keys(%restDispatch) ) | ||||
340 | . "\n\n" | ||||
341 | ); | ||||
342 | } | ||||
343 | |||||
344 | 1 | 2µs | 1; | ||
345 | __END__ |