New framework for AJAX services
This adds two new C4 modules, C4::Service and ::Output::JSONStream, and makes important modifications to C4::Output. The first two are a basic framework for JSON-based AJAX services and a simple JSON output wrapper, respectively. C4::Output has been slightly refactored, with a new function, output_with_http_headers, that supports different content-types. output_html_with_http_headers still exists, and the three pages affected by this change have been refactored to support it. Signed-off-by: Galen Charlton <galen.charlton@liblime.com>
This commit is contained in:
parent
715aee6b95
commit
b04f432857
95
C4/Output.pm
95
C4/Output.pm
|
|
@ -38,17 +38,17 @@ BEGIN {
|
|||
$VERSION = 3.03;
|
||||
require Exporter;
|
||||
@ISA = qw(Exporter);
|
||||
@EXPORT_OK = qw(&output_ajax_with_http_headers &is_ajax); # More stuff should go here instead
|
||||
@EXPORT_OK = qw(&is_ajax ajax_fail); # More stuff should go here instead
|
||||
%EXPORT_TAGS = ( all =>[qw(&themelanguage &gettemplate setlanguagecookie pagination_bar
|
||||
&output_ajax_with_http_headers &output_html_with_http_headers)],
|
||||
ajax =>[qw(&output_ajax_with_http_headers is_ajax)],
|
||||
html =>[qw(&output_html_with_http_headers)]
|
||||
&output_with_http_headers &output_html_with_http_headers)],
|
||||
ajax =>[qw(&output_with_http_headers is_ajax)],
|
||||
html =>[qw(&output_with_http_headers &output_html_with_http_headers)]
|
||||
);
|
||||
push @EXPORT, qw(
|
||||
&themelanguage &gettemplate setlanguagecookie pagination_bar
|
||||
);
|
||||
push @EXPORT, qw(
|
||||
&output_html_with_http_headers
|
||||
&output_html_with_http_headers &output_with_http_headers
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -93,7 +93,7 @@ sub gettemplate {
|
|||
die_on_bad_params => 1,
|
||||
global_vars => 1,
|
||||
case_sensitive => 1,
|
||||
loop_context_vars => 1, # enable: __first__, __last__, __inner__, __odd__, __counter__
|
||||
loop_context_vars => 1, # enable: __first__, __last__, __inner__, __odd__, __counter__
|
||||
path => ["$htdocs/$theme/$lang/$path"]
|
||||
);
|
||||
my $themelang=( $interface ne 'intranet' ? '/opac-tmpl' : '/intranet-tmpl' )
|
||||
|
|
@ -351,48 +351,69 @@ sub pagination_bar {
|
|||
return $pagination_bar;
|
||||
}
|
||||
|
||||
=item output_html_with_http_headers
|
||||
=item output_with_http_headers
|
||||
|
||||
&output_html_with_http_headers($query, $cookie, $html[, $content_type])
|
||||
&output_with_http_headers($query, $cookie, $data, $content_type[, $status])
|
||||
|
||||
Outputs the HTML page $html with the appropriate HTTP headers,
|
||||
with the authentication cookie $cookie and a Content-Type that
|
||||
corresponds to the HTML page $html.
|
||||
Outputs $data with the appropriate HTTP headers,
|
||||
the authentication cookie $cookie and a Content-Type specified in
|
||||
$content_type.
|
||||
|
||||
If the optional C<$content_type> parameter is called, set the
|
||||
response's Content-Type to that value instead of "text/html".
|
||||
If applicable, $cookie can be undef, and it will not be sent.
|
||||
|
||||
$content_type is one of the following: 'html', 'js', 'json', 'xml', 'rss', or 'atom'.
|
||||
|
||||
$status is an HTTP status message, like '403 Authentication Required'. It defaults to '200 OK'.
|
||||
|
||||
=cut
|
||||
|
||||
sub output_html_with_http_headers ($$$;$) {
|
||||
my $query = shift;
|
||||
my $cookie = shift;
|
||||
my $html = shift;
|
||||
my $content_type = @_ ? shift : "text/html";
|
||||
$content_type = "text/html" unless $content_type =~ m!/!; # very basic sanity check
|
||||
print $query->header(
|
||||
-type => $content_type,
|
||||
-charset => 'UTF-8',
|
||||
-cookie => $cookie,
|
||||
-Pragma => 'no-cache',
|
||||
-'Cache-Control' => 'no-cache',
|
||||
), $html;
|
||||
sub output_with_http_headers($$$$;$) {
|
||||
my ( $query, $cookie, $data, $content_type, $status ) = @_;
|
||||
$status ||= '200 OK';
|
||||
|
||||
my %content_type_map = (
|
||||
'html' => 'text/html',
|
||||
'js' => 'text/javascript',
|
||||
'json' => 'application/json',
|
||||
'xml' => 'text/xml',
|
||||
# NOTE: not using application/atom+xml or application/rss+xml because of
|
||||
# Internet Explorer 6; see bug 2078.
|
||||
'rss' => 'text/xml',
|
||||
'atom' => 'text/xml'
|
||||
);
|
||||
|
||||
die "Unknown content type '$content_type'" if ( !defined( $content_type_map{$content_type} ) );
|
||||
|
||||
if ($cookie) {
|
||||
print $query->header(
|
||||
-type => $content_type_map{$content_type},
|
||||
-status => $status,
|
||||
-charset => 'UTF-8',
|
||||
-cookie => $cookie,
|
||||
-Pragma => 'no-cache',
|
||||
-'Cache-Control' => 'no-cache',
|
||||
);
|
||||
} else {
|
||||
print $query->header(
|
||||
-type => $content_type_map{$content_type},
|
||||
-status => $status,
|
||||
-charset => 'UTF-8',
|
||||
-Pragma => 'no-cache',
|
||||
-'Cache-Control' => 'no-cache',
|
||||
);
|
||||
}
|
||||
|
||||
print $data;
|
||||
}
|
||||
|
||||
sub output_ajax_with_http_headers ($$) {
|
||||
my ($query, $js) = @_;
|
||||
print $query->header(
|
||||
-type => 'text/javascript',
|
||||
-charset => 'UTF-8',
|
||||
-Pragma => 'no-cache',
|
||||
-'Cache-Control' => 'no-cache',
|
||||
-expires =>'-1d',
|
||||
), $js;
|
||||
sub output_html_with_http_headers ($$$) {
|
||||
my ( $query, $cookie, $data ) = @_;
|
||||
output_with_http_headers( $query, $cookie, $data, 'html' );
|
||||
}
|
||||
|
||||
sub is_ajax () {
|
||||
my $x_req = $ENV{HTTP_X_REQUESTED_WITH};
|
||||
return ($x_req and $x_req =~ /XMLHttpRequest/i) ? 1 : 0;
|
||||
my $x_req = $ENV{HTTP_X_REQUESTED_WITH};
|
||||
return ( $x_req and $x_req =~ /XMLHttpRequest/i ) ? 1 : 0;
|
||||
}
|
||||
|
||||
END { } # module clean-up code here (global destructor)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
package C4::Output::JSONStream;
|
||||
#
|
||||
# Copyright 2008 LibLime
|
||||
#
|
||||
# This file is part of Koha.
|
||||
#
|
||||
# Koha is free software; you can redistribute it and/or modify it under the
|
||||
# terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# Koha; if not, write to the Free Software Foundation, Inc., 59 Temple Place,
|
||||
# Suite 330, Boston, MA 02111-1307 USA
|
||||
|
||||
=head1 NAME
|
||||
|
||||
C4::Output::JSONStream - progressively build JSON data
|
||||
|
||||
=head1 SYNOPSIS
|
||||
|
||||
my $json = new C4::Output::JSONStream;
|
||||
|
||||
$json->param( issues => [ 'yes!', 'please', 'no', { emphasis = 'NO' } ] );
|
||||
$json->param( stuff => 'realia' );
|
||||
|
||||
print $json->output;
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
This module allows you to build JSON incrementally.
|
||||
|
||||
=cut
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use JSON;
|
||||
|
||||
sub new {
|
||||
my $class = shift;
|
||||
my $self = {
|
||||
data => {},
|
||||
options => {}
|
||||
};
|
||||
|
||||
bless $self, $class;
|
||||
|
||||
return $self;
|
||||
}
|
||||
|
||||
sub param {
|
||||
my $self = shift;
|
||||
|
||||
if ( @_ % 2 != 0 ) {
|
||||
die 'param() received odd number of arguments (should be called with param => "value" pairs)';
|
||||
}
|
||||
|
||||
for ( my $i = 0; $i < $#_; $i += 2 ) {
|
||||
$self->{data}->{$_[$i]} = $_[$i + 1];
|
||||
}
|
||||
}
|
||||
|
||||
sub output {
|
||||
my $self = shift;
|
||||
|
||||
return to_json( $self->{data} );
|
||||
}
|
||||
|
||||
1;
|
||||
|
|
@ -0,0 +1,291 @@
|
|||
package C4::Service;
|
||||
#
|
||||
# Copyright 2008 LibLime
|
||||
#
|
||||
# This file is part of Koha.
|
||||
#
|
||||
# Koha is free software; you can redistribute it and/or modify it under the
|
||||
# terms of the GNU General Public License as published by the Free Software
|
||||
# Foundation; either version 2 of the License, or (at your option) any later
|
||||
# version.
|
||||
#
|
||||
# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License along with
|
||||
# Koha; if not, write to the Free Software Foundation, Inc., 59 Temple Place,
|
||||
# Suite 330, Boston, MA 02111-1307 USA
|
||||
|
||||
=head1 NAME
|
||||
|
||||
C4::Service - functions for JSON webservices.
|
||||
|
||||
=head1 SYNOPSIS
|
||||
|
||||
my ( $query, $response) = C4::Service->init( { circulate => 1 } );
|
||||
my ( $borrowernumber) = C4::Service->require_params( 'borrowernumber' );
|
||||
|
||||
C4::Service->return_error( 'internal', 'Frobnication failed', frobnicator => 'foo' );
|
||||
|
||||
$response->param( frobnicated => 'You' );
|
||||
|
||||
C4::Service->return_success( $response );
|
||||
|
||||
=head1 DESCRIPTION
|
||||
|
||||
This module packages several useful functions for JSON webservices.
|
||||
|
||||
=cut
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use CGI;
|
||||
use C4::Auth qw( check_api_auth );
|
||||
use C4::Output qw( :ajax );
|
||||
use C4::Output::JSONStream;
|
||||
use JSON;
|
||||
|
||||
our $debug;
|
||||
|
||||
BEGIN {
|
||||
$debug = $ENV{DEBUG} || 0;
|
||||
}
|
||||
|
||||
our ( $query, $cookie );
|
||||
|
||||
=head1 METHODS
|
||||
|
||||
=head2 init
|
||||
|
||||
=over 4
|
||||
|
||||
our ( $query, $response ) = C4::Service->init( %needed_flags );
|
||||
|
||||
=back
|
||||
|
||||
Initialize the service and check for the permissions in C<%needed_flags>.
|
||||
|
||||
Also, check that the user is authorized and has a current session, and return an
|
||||
'auth' error if not.
|
||||
|
||||
init() returns a C<CGI> object and a C<C4::Output::JSONStream>. The latter can
|
||||
be used for both flat scripts and those that use dispatch(), and should be
|
||||
passed to C<return_success()>.
|
||||
|
||||
=cut
|
||||
|
||||
sub init {
|
||||
my ( $class, %needed_flags ) = @_;
|
||||
|
||||
our $query = new CGI;
|
||||
|
||||
my ( $status, $cookie_, $sessionID ) = check_api_auth( $query, \%needed_flags );
|
||||
|
||||
our $cookie = $cookie_; # I have no desire to offend the Perl scoping gods
|
||||
|
||||
$class->return_error( type => 'auth', message => $status ) if ( $status ne 'ok' );
|
||||
|
||||
return ( $query, new C4::Output::JSONStream );
|
||||
}
|
||||
|
||||
=head2 return_error
|
||||
|
||||
=over 4
|
||||
|
||||
C4::Service->return_error( $type, $error, %flags );
|
||||
|
||||
=back
|
||||
|
||||
Exit the script with HTTP status 400, and return a JSON error object.
|
||||
|
||||
C<$type> should be a short, lower case code for the generic type of error (such
|
||||
as 'auth' or 'input').
|
||||
|
||||
C<$error> should be a more specific code giving information on the error. If
|
||||
multiple errors of the same type occurred, they should be joined by '|'; i.e.,
|
||||
'expired|different_ip'. Information in C<$error> does not need to be
|
||||
human-readable, as its formatting should be handled by the client.
|
||||
|
||||
Any additional information to be given in the response should be passed as
|
||||
param => value pairs.
|
||||
|
||||
=cut
|
||||
|
||||
sub return_error {
|
||||
my ( $class, $type, $error, %flags ) = @_;
|
||||
|
||||
my $response = new C4::Output::JSONStream;
|
||||
|
||||
$response->param( message => $error ) if ( $error );
|
||||
$response->param( type => $type, %flags );
|
||||
|
||||
output_with_http_headers $query, $cookie, $response->output, 'json', '400 Bad Request';
|
||||
exit;
|
||||
}
|
||||
|
||||
=head return_multi
|
||||
|
||||
=over 4
|
||||
|
||||
C4::Service->return_multi( \@responses, %flags );
|
||||
|
||||
=back
|
||||
|
||||
return_multi is similar to return_success or return_error, but allows you to
|
||||
return different statuses for several requests sent at once (using HTTP status
|
||||
"207 Multi-Status", much like WebDAV). The toplevel hashref (turned into the
|
||||
JSON response) looks something like this:
|
||||
|
||||
=over 4
|
||||
|
||||
{ multi => JSON::true, responses => \@responses, %flags }
|
||||
|
||||
=back
|
||||
|
||||
Each element of @responses should be either a plain hashref or an arrayref. If
|
||||
it is a hashref, it is sent to the browser as-is. If it is an arrayref, it is
|
||||
assumed to be in the same form as the arguments to return_error, and is turned
|
||||
into an error structure.
|
||||
|
||||
All key-value pairs %flags are, as stated above, put into the returned JSON
|
||||
structure verbatim.
|
||||
|
||||
=cut
|
||||
|
||||
sub return_multi {
|
||||
my ( $class, $responses, @flags ) = @_;
|
||||
|
||||
my $response = new C4::Output::JSONStream;
|
||||
|
||||
if ( !@$responses ) {
|
||||
$class->return_success( $response );
|
||||
} else {
|
||||
my @responses_formatted;
|
||||
|
||||
foreach my $response ( @$responses ) {
|
||||
if ( ref( $response ) eq 'ARRAY' ) {
|
||||
my ($type, $error, @error_flags) = @$response;
|
||||
|
||||
push @responses_formatted, { is_error => JSON::true, type => $type, message => $error, @error_flags };
|
||||
} else {
|
||||
push @responses_formatted, $response;
|
||||
}
|
||||
}
|
||||
|
||||
$response->param( 'multi' => JSON::true, responses => \@responses_formatted, @flags );
|
||||
output_with_http_headers $query, $cookie, $response->output, 'json', '207 Multi-Status';
|
||||
}
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
=head2 return_success
|
||||
|
||||
=over 4
|
||||
|
||||
C4::Service->return_success( $response );
|
||||
|
||||
=back
|
||||
|
||||
Print out the information in the C<C4::Output::JSONStream> C<$response>, then
|
||||
exit with HTTP status 200.
|
||||
|
||||
=cut
|
||||
|
||||
sub return_success {
|
||||
my ( $class, $response ) = @_;
|
||||
|
||||
output_with_http_headers $query, $cookie, $response->output, 'json';
|
||||
}
|
||||
|
||||
=head2 require_params
|
||||
|
||||
=over 4
|
||||
|
||||
my @values = C4::Service->require_params( @params );
|
||||
|
||||
=back
|
||||
|
||||
Check that each of of the parameters specified in @params was sent in the
|
||||
request, then return their values in that order.
|
||||
|
||||
If a required parameter is not found, send a 'param' error to the browser.
|
||||
|
||||
=cut
|
||||
|
||||
sub require_params {
|
||||
my ( $class, @params ) = @_;
|
||||
|
||||
my @values;
|
||||
|
||||
for my $param ( @params ) {
|
||||
$class->return_error( 'params', "Missing '$param'" ) if ( !defined( $query->param( $param ) ) );
|
||||
push @values, $query->param( $param );
|
||||
}
|
||||
|
||||
return @values;
|
||||
}
|
||||
|
||||
=head dispatch
|
||||
|
||||
=over 4
|
||||
|
||||
C4::Service->dispatch(
|
||||
[ $path_regex, \@required_params, \&handler ],
|
||||
...
|
||||
);
|
||||
|
||||
=back
|
||||
|
||||
dispatch takes several array-refs, each one describing a 'route', to use the
|
||||
Rails terminology.
|
||||
|
||||
$path_regex should be a string in regex-form, describing which paths this route
|
||||
handles. Each route is tested in order, from the top down, so put more specific
|
||||
handlers first. Also, the regex is tested on the entire path.
|
||||
|
||||
Each named parameter in @required_params is tested for to make sure the route
|
||||
matches, but does not raise an error if one is missing; it simply tests the next
|
||||
route. If you would prefer to raise an error, instead use
|
||||
C<C4::Service->require_params> inside your handler.
|
||||
|
||||
\&handler is called with each matched group in $path_regex in its arguments. For
|
||||
example, if your service is accessed at the path /blah/123, and you call
|
||||
C<dispatch> with the route [ '/blah/(\\d+)', ... ], your handler will be called
|
||||
with the argument '123'.
|
||||
|
||||
=cut
|
||||
|
||||
sub dispatch {
|
||||
my $class = shift;
|
||||
|
||||
my $path_info = $query->path_info || '/';
|
||||
|
||||
ROUTE: foreach my $route ( @_ ) {
|
||||
my ( $path, $params, $handler ) = @$route;
|
||||
|
||||
next unless ( my @match = ( ($query->request_method . ' ' . $path_info) =~ m,^$path$, ) );
|
||||
|
||||
for my $param ( @$params ) {
|
||||
next ROUTE if ( !defined( $query->param ( $param ) ) );
|
||||
}
|
||||
|
||||
$debug and warn "Using $path";
|
||||
$handler->( @match );
|
||||
return;
|
||||
}
|
||||
|
||||
$class->return_error( 'no_handler', '' );
|
||||
}
|
||||
|
||||
1;
|
||||
|
||||
__END__
|
||||
|
||||
=head1 AUTHORS
|
||||
|
||||
Koha Development Team
|
||||
|
||||
Jesse Weaver <jesse.weaver@liblime.com>
|
||||
|
|
@ -610,15 +610,6 @@ if ( C4::Context->preference("kohaspsuggest") ) {
|
|||
}
|
||||
|
||||
# VI. BUILD THE TEMPLATE
|
||||
# NOTE: not using application/atom+xml or application/rss+xml beccause of Internet Explorer 6;
|
||||
# see bug 2078.
|
||||
my $content_type;
|
||||
if ($cgi->param('format') && $cgi->param('format') =~ /rss|atom/ ){
|
||||
$content_type = "application/xml";
|
||||
}
|
||||
else {
|
||||
$content_type = "text/html";
|
||||
}
|
||||
# Build drop-down list for 'Add To:' menu...
|
||||
my $session = get_session($cgi->cookie("CGISESSID"));
|
||||
my @addpubshelves;
|
||||
|
|
@ -639,4 +630,14 @@ if (defined $barshelves) {
|
|||
$template->param( addbarshelvesloop => $barshelves);
|
||||
}
|
||||
|
||||
output_html_with_http_headers $cgi, $cookie, $template->output, $content_type;
|
||||
my $content_type;
|
||||
|
||||
if ($cgi->param('format') =~ /rss/) {
|
||||
$content_type = 'rss'
|
||||
} elsif ($cgi->param('format') =~ /atom/) {
|
||||
$content_type = 'atom'
|
||||
} else {
|
||||
$content_type = 'html'
|
||||
}
|
||||
|
||||
output_with_http_headers $cgi, $cookie, $template->output, $content_type;
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ my $perBibResults = {};
|
|||
my @globalErrorIndexes = ();
|
||||
|
||||
sub ajax_auth_cgi ($) { # returns CGI object
|
||||
my $needed_flags = shift;
|
||||
my $needed_flags = shift;
|
||||
my %cookies = fetch CGI::Cookie;
|
||||
my $input = CGI->new;
|
||||
my $sessid = $cookies{'CGISESSID'}->value || $input->param('CGISESSID');
|
||||
|
|
@ -58,9 +58,9 @@ sub ajax_auth_cgi ($) { # returns CGI object
|
|||
$debug and
|
||||
print STDERR "($auth_status, $auth_sessid) = check_cookie_auth($sessid," . Dumper($needed_flags) . ")\n";
|
||||
if ($auth_status ne "ok") {
|
||||
output_ajax_with_http_headers $input,
|
||||
output_with_http_headers $input, undef,
|
||||
"window.alert('Your CGI session cookie ($sessid) is not current. " .
|
||||
"Please refresh the page and try again.');\n";
|
||||
"Please refresh the page and try again.');\n", 'js';
|
||||
exit 0;
|
||||
}
|
||||
$debug and print STDERR "AJAX request: " . Dumper($input),
|
||||
|
|
@ -212,7 +212,7 @@ if ($is_ajax) {
|
|||
$js_perbib .= $js_bibres;
|
||||
}
|
||||
|
||||
output_ajax_with_http_headers($query, "$js_reply\n$err_string\n$js_perbib\n};");
|
||||
output_with_http_headers($query, undef, "$js_reply\n$err_string\n$js_perbib\n};", 'js');
|
||||
exit;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -46,9 +46,9 @@ sub ajax_auth_cgi ($) { # returns CGI object
|
|||
$debug and
|
||||
print STDERR "($auth_status, $auth_sessid) = check_cookie_auth($sessid," . Dumper($needed_flags) . ")\n";
|
||||
if ($auth_status ne "ok") {
|
||||
output_ajax_with_http_headers $input,
|
||||
output_with_http_headers $input, undef,
|
||||
"window.alert('Your CGI session cookie ($sessid) is not current. " .
|
||||
"Please refresh the page and try again.');\n";
|
||||
"Please refresh the page and try again.');\n", 'js';
|
||||
exit 0;
|
||||
}
|
||||
$debug and print STDERR "AJAX request: " . Dumper($input),
|
||||
|
|
@ -72,7 +72,7 @@ if (is_ajax()) {
|
|||
if ($tag = $input->param('rej')) {
|
||||
$js_reply = ( blacklist($operator,$tag) ? 'success' : 'failure') . "_reject('$tag');\n";
|
||||
}
|
||||
output_ajax_with_http_headers $input, $js_reply;
|
||||
output_with_http_headers $input, undef, $js_reply, 'js';
|
||||
exit;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue