| Filename | /mnt/catalyst/koha/C4/Service.pm |
| Statements | Executed 37 statements in 10.4ms |
| Calls | P | F | Exclusive Time |
Inclusive Time |
Subroutine |
|---|---|---|---|---|---|
| 1 | 1 | 1 | 10.6ms | 256ms | C4::Service::BEGIN@49 |
| 1 | 1 | 1 | 10.2ms | 71.7ms | C4::Service::BEGIN@48 |
| 1 | 1 | 1 | 3.75ms | 10.1ms | C4::Service::BEGIN@50 |
| 1 | 1 | 1 | 2.96ms | 3.72ms | C4::Service::BEGIN@51 |
| 1 | 1 | 1 | 449µs | 472µs | C4::Service::BEGIN@44 |
| 1 | 1 | 1 | 31µs | 3.28ms | C4::Service::return_error |
| 1 | 1 | 1 | 27µs | 24.5ms | C4::Service::new |
| 1 | 1 | 1 | 11µs | 17µs | C4::Service::BEGIN@45 |
| 1 | 1 | 1 | 8µs | 41µs | C4::Service::BEGIN@47 |
| 1 | 1 | 1 | 4µs | 4µs | C4::Service::BEGIN@55 |
| 1 | 1 | 1 | 4µs | 4µs | C4::Service::query |
| 0 | 0 | 0 | 0s | 0s | C4::Service::dispatch |
| 0 | 0 | 0 | 0s | 0s | C4::Service::output_stream |
| 0 | 0 | 0 | 0s | 0s | C4::Service::require_params |
| 0 | 0 | 0 | 0s | 0s | C4::Service::return_multi |
| 0 | 0 | 0 | 0s | 0s | C4::Service::return_success |
| Line | State ments |
Time on line |
Calls | Time in subs |
Code |
|---|---|---|---|---|---|
| 1 | package C4::Service; | ||||
| 2 | # | ||||
| 3 | # Copyright 2008 LibLime | ||||
| 4 | # | ||||
| 5 | # This file is part of Koha. | ||||
| 6 | # | ||||
| 7 | # Koha is free software; you can redistribute it and/or modify it under the | ||||
| 8 | # terms of the GNU General Public License as published by the Free Software | ||||
| 9 | # Foundation; either version 2 of the License, or (at your option) any later | ||||
| 10 | # version. | ||||
| 11 | # | ||||
| 12 | # Koha is distributed in the hope that it will be useful, but WITHOUT ANY | ||||
| 13 | # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR | ||||
| 14 | # A PARTICULAR PURPOSE. See the GNU General Public License for more details. | ||||
| 15 | # | ||||
| 16 | # You should have received a copy of the GNU General Public License along | ||||
| 17 | # with Koha; if not, write to the Free Software Foundation, Inc., | ||||
| 18 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. | ||||
| 19 | |||||
| 20 | =head1 NAME | ||||
| 21 | |||||
| 22 | C4::Service - functions for JSON webservices. | ||||
| 23 | |||||
| 24 | =head1 SYNOPSIS | ||||
| 25 | |||||
| 26 | my $response = C4::Output::XMLStream->new(...); | ||||
| 27 | my $service = C4::Service->new( { needed_flags => { circulate => 1 }, | ||||
| 28 | [ output_stream => $response ], | ||||
| 29 | [ query => CGI->new() ] } ); | ||||
| 30 | my ( $borrowernumber) = $service->require_params( 'borrowernumber' ); | ||||
| 31 | |||||
| 32 | $service->return_error( 'internal', 'Frobnication failed', frobnicator => 'foo' ); | ||||
| 33 | |||||
| 34 | $response->param( frobnicated => 'You' ); | ||||
| 35 | |||||
| 36 | C4::Service->return_success(); | ||||
| 37 | |||||
| 38 | =head1 DESCRIPTION | ||||
| 39 | |||||
| 40 | This module packages several useful functions for webservices. | ||||
| 41 | |||||
| 42 | =cut | ||||
| 43 | |||||
| 44 | 2 | 32µs | 2 | 495µs | # spent 472µs (449+23) within C4::Service::BEGIN@44 which was called:
# once (449µs+23µs) by main::BEGIN@75 at line 44 # spent 472µs making 1 call to C4::Service::BEGIN@44
# spent 23µs making 1 call to strict::import |
| 45 | 2 | 23µs | 2 | 23µs | # spent 17µs (11+6) within C4::Service::BEGIN@45 which was called:
# once (11µs+6µs) by main::BEGIN@75 at line 45 # spent 17µs making 1 call to C4::Service::BEGIN@45
# spent 6µs making 1 call to warnings::import |
| 46 | |||||
| 47 | 2 | 23µs | 2 | 73µs | # spent 41µs (8+32) within C4::Service::BEGIN@47 which was called:
# once (8µs+32µs) by main::BEGIN@75 at line 47 # spent 41µs making 1 call to C4::Service::BEGIN@47
# spent 32µs making 1 call to Exporter::import |
| 48 | 2 | 683µs | 2 | 71.7ms | # spent 71.7ms (10.2+61.5) within C4::Service::BEGIN@48 which was called:
# once (10.2ms+61.5ms) by main::BEGIN@75 at line 48 # spent 71.7ms making 1 call to C4::Service::BEGIN@48
# spent 10µs making 1 call to CGI::import |
| 49 | 2 | 2.78ms | 2 | 257ms | # spent 256ms (10.6+246) within C4::Service::BEGIN@49 which was called:
# once (10.6ms+246ms) by main::BEGIN@75 at line 49 # spent 256ms making 1 call to C4::Service::BEGIN@49
# spent 89µs making 1 call to Exporter::import |
| 50 | 2 | 2.70ms | 2 | 10.3ms | # spent 10.1ms (3.75+6.38) within C4::Service::BEGIN@50 which was called:
# once (3.75ms+6.38ms) by main::BEGIN@75 at line 50 # spent 10.1ms making 1 call to C4::Service::BEGIN@50
# spent 126µs making 1 call to Exporter::import |
| 51 | 2 | 3.28ms | 1 | 3.72ms | # spent 3.72ms (2.96+766µs) within C4::Service::BEGIN@51 which was called:
# once (2.96ms+766µs) by main::BEGIN@75 at line 51 # spent 3.72ms making 1 call to C4::Service::BEGIN@51 |
| 52 | |||||
| 53 | 1 | 200ns | our $debug; | ||
| 54 | |||||
| 55 | # spent 4µs within C4::Service::BEGIN@55 which was called:
# once (4µs+0s) by main::BEGIN@75 at line 57 | ||||
| 56 | 1 | 5µs | $debug = $ENV{DEBUG} || 0; | ||
| 57 | 1 | 708µs | 1 | 4µs | } # spent 4µs making 1 call to C4::Service::BEGIN@55 |
| 58 | |||||
| 59 | =head1 METHODS | ||||
| 60 | |||||
| 61 | =head2 new | ||||
| 62 | |||||
| 63 | my $service = C4::Service->new({needed_flags => { parameters => 1 }, | ||||
| 64 | [ output_stream => C4::Output::XMLStream->new(...) ], | ||||
| 65 | [ query => CGI->new() ]}); | ||||
| 66 | |||||
| 67 | Creates a new instance of C4::Service. It verifies that the provided flags | ||||
| 68 | are met by the current session, and aborts with an exit() call if they're | ||||
| 69 | not. It also accepts an instance of C4::Output::* (or something with the | ||||
| 70 | same interface) to use to generate the output. If none is provided, then | ||||
| 71 | a new instance of L<C4::Output::JSONStream> is created. Similarly, a query | ||||
| 72 | may also be provided. If it's not, a new CGI one will be created. | ||||
| 73 | |||||
| 74 | TODO: exit sucks, make a better way. | ||||
| 75 | |||||
| 76 | =cut | ||||
| 77 | |||||
| 78 | # spent 24.5ms (27µs+24.5) within C4::Service::new which was called:
# once (27µs+24.5ms) by main::process_upsert at line 82 of svc/members/upsert | ||||
| 79 | 1 | 600ns | my $class = shift; | ||
| 80 | |||||
| 81 | 1 | 2µs | my %opts = %{shift()}; | ||
| 82 | |||||
| 83 | 1 | 500ns | my $needed_flags = $opts{needed_flags}; | ||
| 84 | 1 | 300ns | croak "needed_flags is a required option" unless $needed_flags; | ||
| 85 | |||||
| 86 | 1 | 4µs | 1 | 2.81ms | my $query = $opts{query} || CGI->new(); # spent 2.81ms making 1 call to CGI::new |
| 87 | 1 | 21µs | 3 | 24.5ms | my ( $status, $cookie, $sessionID ) = check_api_auth( $query, $needed_flags ); # spent 21.7ms making 1 call to C4::Auth::check_api_auth
# spent 2.75ms making 1 call to CGI::Session::DESTROY
# spent 45µs making 1 call to CGI::Session::Driver::DBI::DESTROY |
| 88 | |||||
| 89 | 1 | 4µs | 1 | 8µs | my $output_stream = $opts{output_stream} || C4::Output::JSONStream->new(); # spent 8µs making 1 call to C4::Output::JSONStream::new |
| 90 | 1 | 2µs | my $self = { | ||
| 91 | needed_flags => $needed_flags, | ||||
| 92 | query => $query, | ||||
| 93 | output_stream => $output_stream, | ||||
| 94 | cookie => $cookie, | ||||
| 95 | }; | ||||
| 96 | 1 | 1µs | bless $self, $class; | ||
| 97 | 1 | 700ns | $self->return_error('auth', $status) if ($status ne 'ok'); | ||
| 98 | |||||
| 99 | 1 | 4µs | return $self; | ||
| 100 | } | ||||
| 101 | |||||
| 102 | =head2 return_error | ||||
| 103 | |||||
| 104 | $service->return_error( $type, $error, %flags ); | ||||
| 105 | |||||
| 106 | Exit the script with HTTP status 400, and return a JSON error object. | ||||
| 107 | |||||
| 108 | C<$type> should be a short, lower case code for the generic type of error (such | ||||
| 109 | as 'auth' or 'input'). | ||||
| 110 | |||||
| 111 | C<$error> should be a more specific code giving information on the error. If | ||||
| 112 | multiple errors of the same type occurred, they should be joined by '|'; i.e., | ||||
| 113 | 'expired|different_ip'. Information in C<$error> does not need to be | ||||
| 114 | human-readable, as its formatting should be handled by the client. | ||||
| 115 | |||||
| 116 | Any additional information to be given in the response should be passed as | ||||
| 117 | param => value pairs. | ||||
| 118 | |||||
| 119 | =cut | ||||
| 120 | |||||
| 121 | # spent 3.28ms (31µs+3.25) within C4::Service::return_error which was called:
# once (31µs+3.25ms) by main::process_upsert at line 152 of svc/members/upsert | ||||
| 122 | 1 | 800ns | my ( $self, $type, $error, %flags ) = @_; | ||
| 123 | |||||
| 124 | 1 | 600ns | my $response = $self->{output_stream}; | ||
| 125 | 1 | 2µs | 1 | 6µs | $response->clear(); # spent 6µs making 1 call to C4::Output::JSONStream::clear |
| 126 | |||||
| 127 | 1 | 4µs | 1 | 10µs | $response->param( message => $error ) if ( $error ); # spent 10µs making 1 call to C4::Output::JSONStream::param |
| 128 | 1 | 2µs | 1 | 16µs | $response->param( type => $type, %flags ); # spent 16µs making 1 call to C4::Output::JSONStream::param |
| 129 | |||||
| 130 | 1 | 7µs | 3 | 3.22ms | output_with_http_headers $self->{query}, $self->{cookie}, $response->output, $response->content_type, '400 Bad Request'; # spent 3.17ms making 1 call to C4::Output::output_with_http_headers
# spent 52µs making 1 call to C4::Output::JSONStream::output
# spent 2µs making 1 call to C4::Output::JSONStream::content_type |
| 131 | 1 | 137µs | 20 | 20.7ms | exit; # spent 19.6ms making 1 call to File::Temp::END
# spent 934µs making 2 calls to Date::Manip::Zones::END, avg 467µs/call
# spent 56µs making 1 call to DBI::END
# spent 28µs making 1 call to XML::LibXML::END
# spent 10µs making 2 calls to Date::Manip::TZ::END, avg 5µs/call
# spent 6µs making 2 calls to Date::Manip::TZ::etgmt00::END, avg 3µs/call
# spent 5µs making 1 call to Date::Manip::Recur::END
# spent 4µs making 1 call to Date::Manip::Date::END
# spent 3µs making 1 call to Date::Manip::Base::END
# spent 3µs making 1 call to Date::Manip::Obj::END
# spent 3µs making 1 call to Date::Manip::Delta::END
# spent 3µs making 1 call to C4::Accounts::END
# spent 2µs making 1 call to C4::Output::END
# spent 2µs making 1 call to C4::Members::END
# spent 2µs making 1 call to Date::Manip::TZ_Base::END
# spent 2µs making 1 call to C4::Auth::END
# spent 2µs making 1 call to C4::Budgets::END |
| 132 | } | ||||
| 133 | |||||
| 134 | =head2 return_multi | ||||
| 135 | |||||
| 136 | $service->return_multi( \@responses, %flags ); | ||||
| 137 | |||||
| 138 | return_multi is similar to return_success or return_error, but allows you to | ||||
| 139 | return different statuses for several requests sent at once (using HTTP status | ||||
| 140 | "207 Multi-Status", much like WebDAV). The toplevel hashref (turned into the | ||||
| 141 | JSON response) looks something like this: | ||||
| 142 | |||||
| 143 | { multi => JSON::true, responses => \@responses, %flags } | ||||
| 144 | |||||
| 145 | Each element of @responses should be either a plain hashref or an arrayref. If | ||||
| 146 | it is a hashref, it is sent to the browser as-is. If it is an arrayref, it is | ||||
| 147 | assumed to be in the same form as the arguments to return_error, and is turned | ||||
| 148 | into an error structure. | ||||
| 149 | |||||
| 150 | All key-value pairs %flags are, as stated above, put into the returned JSON | ||||
| 151 | structure verbatim. | ||||
| 152 | |||||
| 153 | =cut | ||||
| 154 | |||||
| 155 | sub return_multi { | ||||
| 156 | my ( $self, $responses, @flags ) = @_; | ||||
| 157 | |||||
| 158 | my $response = $self->{output_stream}; | ||||
| 159 | $response->clear(); | ||||
| 160 | |||||
| 161 | if ( !@$responses ) { | ||||
| 162 | $self->return_success( $response ); | ||||
| 163 | } else { | ||||
| 164 | my @responses_formatted; | ||||
| 165 | |||||
| 166 | foreach my $response ( @$responses ) { | ||||
| 167 | if ( ref( $response ) eq 'ARRAY' ) { | ||||
| 168 | my ($type, $error, @error_flags) = @$response; | ||||
| 169 | |||||
| 170 | push @responses_formatted, { is_error => $response->true(), type => $type, message => $error, @error_flags }; | ||||
| 171 | } else { | ||||
| 172 | push @responses_formatted, $response; | ||||
| 173 | } | ||||
| 174 | } | ||||
| 175 | |||||
| 176 | $response->param( 'multi' => $response->true(), responses => \@responses_formatted, @flags ); | ||||
| 177 | output_with_http_headers $self->{query}, $self->{cookie}, $response->output, $response->content_type, '207 Multi-Status'; | ||||
| 178 | } | ||||
| 179 | |||||
| 180 | exit; | ||||
| 181 | } | ||||
| 182 | |||||
| 183 | =head2 return_success | ||||
| 184 | |||||
| 185 | $service->return_success(); | ||||
| 186 | |||||
| 187 | Print out the information in the provided C<output_stream>, then | ||||
| 188 | exit with HTTP status 200. To get access to the C<output_stream>, you should | ||||
| 189 | either use the one that you provided, or you should use the C<output_stream()> | ||||
| 190 | accessor. | ||||
| 191 | |||||
| 192 | =cut | ||||
| 193 | |||||
| 194 | sub return_success { | ||||
| 195 | my ( $self ) = @_; | ||||
| 196 | |||||
| 197 | my $response = $self->{output_stream}; | ||||
| 198 | output_with_http_headers $self->{query}, $self->{cookie}, $response->output, $response->content_type; | ||||
| 199 | } | ||||
| 200 | |||||
| 201 | =head2 output_stream | ||||
| 202 | |||||
| 203 | $service->output_stream(); | ||||
| 204 | |||||
| 205 | Provides the output stream object that is in use so that data can be added | ||||
| 206 | to it. | ||||
| 207 | |||||
| 208 | =cut | ||||
| 209 | |||||
| 210 | sub output_stream { | ||||
| 211 | my $self = shift; | ||||
| 212 | |||||
| 213 | return $self->{output_stream}; | ||||
| 214 | } | ||||
| 215 | |||||
| 216 | =head2 query | ||||
| 217 | |||||
| 218 | $service->query(); | ||||
| 219 | |||||
| 220 | Provides the query object that this class is using. | ||||
| 221 | |||||
| 222 | =cut | ||||
| 223 | |||||
| 224 | # spent 4µs within C4::Service::query which was called:
# once (4µs+0s) by main::process_upsert at line 84 of svc/members/upsert | ||||
| 225 | 1 | 400ns | my $self = shift; | ||
| 226 | |||||
| 227 | 1 | 6µs | return $self->{query}; | ||
| 228 | } | ||||
| 229 | |||||
| 230 | =head2 require_params | ||||
| 231 | |||||
| 232 | my @values = $service->require_params( @params ); | ||||
| 233 | |||||
| 234 | Check that each of of the parameters specified in @params was sent in the | ||||
| 235 | request, then return their values in that order. | ||||
| 236 | |||||
| 237 | If a required parameter is not found, send a 'param' error to the browser. | ||||
| 238 | |||||
| 239 | =cut | ||||
| 240 | |||||
| 241 | sub require_params { | ||||
| 242 | my ( $self, @params ) = @_; | ||||
| 243 | |||||
| 244 | my @values; | ||||
| 245 | |||||
| 246 | for my $param ( @params ) { | ||||
| 247 | $self->return_error( 'params', "Missing '$param'" ) if ( !defined( $self->{query}->param( $param ) ) ); | ||||
| 248 | push @values, $self->{query}->param( $param ); | ||||
| 249 | } | ||||
| 250 | |||||
| 251 | return @values; | ||||
| 252 | } | ||||
| 253 | |||||
| 254 | =head2 dispatch | ||||
| 255 | |||||
| 256 | $service->dispatch( | ||||
| 257 | [ $path_regex, \@required_params, \&handler ], | ||||
| 258 | ... | ||||
| 259 | ); | ||||
| 260 | |||||
| 261 | dispatch takes several array-refs, each one describing a 'route', to use the | ||||
| 262 | Rails terminology. | ||||
| 263 | |||||
| 264 | $path_regex should be a string in regex-form, describing which methods and | ||||
| 265 | paths this route handles. Each route is tested in order, from the top down, so | ||||
| 266 | put more specific handlers first. Also, the regex is tested on the request | ||||
| 267 | method, plus the path. For instance, you might use the route [ 'POST /', ... ] | ||||
| 268 | to handle POST requests to your service. | ||||
| 269 | |||||
| 270 | Each named parameter in @required_params is tested for to make sure the route | ||||
| 271 | matches, but does not raise an error if one is missing; it simply tests the next | ||||
| 272 | route. If you would prefer to raise an error, instead use | ||||
| 273 | C<C4::Service->require_params> inside your handler. | ||||
| 274 | |||||
| 275 | \&handler is called with each matched group in $path_regex in its arguments. For | ||||
| 276 | example, if your service is accessed at the path /blah/123, and you call | ||||
| 277 | C<dispatch> with the route [ 'GET /blah/(\\d+)', ... ], your handler will be called | ||||
| 278 | with the argument '123'. | ||||
| 279 | |||||
| 280 | =cut | ||||
| 281 | |||||
| 282 | sub dispatch { | ||||
| 283 | my $self = shift; | ||||
| 284 | |||||
| 285 | my $query = $self->{query}; | ||||
| 286 | my $path_info = $query->path_info || '/'; | ||||
| 287 | |||||
| 288 | ROUTE: foreach my $route ( @_ ) { | ||||
| 289 | my ( $path, $params, $handler ) = @$route; | ||||
| 290 | |||||
| 291 | next unless ( my @match = ( ($query->request_method . ' ' . $path_info) =~ m,^$path$, ) ); | ||||
| 292 | |||||
| 293 | for my $param ( @$params ) { | ||||
| 294 | next ROUTE if ( !defined( $query->param ( $param ) ) ); | ||||
| 295 | } | ||||
| 296 | |||||
| 297 | $debug and warn "Using $path"; | ||||
| 298 | $handler->( @match ); | ||||
| 299 | return; | ||||
| 300 | } | ||||
| 301 | |||||
| 302 | $self->return_error( 'no_handler', '' ); | ||||
| 303 | } | ||||
| 304 | |||||
| 305 | 1 | 4µs | 1; | ||
| 306 | |||||
| 307 | __END__ |