Bug 23019: (follow-up) Fix test permissions and API definition bugs
[koha.git] / Koha / FrameworkPlugin.pm
bloba50328a1f65454a0ea1ef10008b24a76f9be3c86
1 package Koha::FrameworkPlugin;
3 # Copyright 2014 Rijksmuseum
5 # This file is part of Koha.
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
20 =head1 NAME
22 Koha::FrameworkPlugin - Facilitate use of plugins in MARC/items editor
24 =head1 SYNOPSIS
26 use Koha::FrameworkPlugin;
27 my $plugin = Koha::FrameworkPlugin({ name => 'EXAMPLE.pl' });
28 $plugin->build( { id => $id });
29 $template->param(
30 javascript => $plugin->javascript,
31 noclick => $plugin->noclick,
34 use Koha::FrameworkPlugin;
35 my $plugin = Koha::FrameworkPlugin({ name => 'EXAMPLE.pl' });
36 $plugin->launch( { cgi => $query });
38 =head1 DESCRIPTION
40 A framework plugin provides additional functionality to a MARC or item
41 field. It can be attached to a field in the framework structure.
42 The functionality is twofold:
43 - Additional actions on the field via javascript in the editor itself
44 via events as onfocus, onblur, etc.
45 Focus may e.g. fill an empty field, Blur or Change may validate.
46 - Provide an additional form to edit the field value, possibly a
47 combination of various subvalues. Look at e.g. MARC leader.
48 The additional form is a popup on top of the MARC/items editor.
50 The plugin code is a perl script (with template for the popup),
51 essentially doing two things:
52 1) Build: The plugin returns javascript to the caller (addbiblio.pl a.o.)
53 2) Launch: The plugin launches the additional form (popup). Launching is
54 centralized via the plugin_launcher.pl script.
56 This object support two code styles:
57 - In the new style, the plugin returns a hashref with a builder and a
58 launcher key pointing to two anynomous subroutines.
59 - In the old style, the builder is subroutine plugin_javascript and the
60 launcher is subroutine plugin. For each plugin the routines are
61 redefined.
63 In cataloguing/value_builder/EXAMPLE.pl, you can find a detailed example
64 of a new style plugin. As long as we support the old style plugins, the
65 unit test t/db_dependent/FrameworkPlugin.t still contains an example
66 of the old style too.
68 =head1 METHODS
70 =head2 new
72 Create object (via Class::Accessor).
74 =head2 build
76 Build uses the builder subroutine of the plugin to build javascript
77 for the plugin.
79 =head2 launch
81 Run the popup of the plugin, as defined by the launcher subroutine.
83 =head1 PROPERTIES
85 =head2 name
87 Filename of the plugin.
89 =head2 path
91 Optional pathname of the plugin.
92 By default plugins are found in cataloguing/value_builder.
94 =head2 errstr
96 Error message.
97 If set, the plugin will no longer build or launch.
99 =head2 javascript
101 Generated javascript for the caller of the plugin (after building).
103 =head2 noclick
105 Tells you (after building) that this plugin has no action connected to
106 to clicking on the buttonDot anchor. (Note that some item plugins
107 redirect click to focus instead of launching a popup.)
109 =head1 ADDITIONAL COMMENTS
111 =cut
113 use Modern::Perl;
115 use base qw(Class::Accessor);
117 use C4::Context;
119 __PACKAGE__->mk_ro_accessors( qw|
120 name path errstr javascript noclick
123 =head2 new
125 Returns new object based on Class::Accessor, loads additional params.
126 The params hash currently supports keys: name, path, item_style.
127 Name is mandatory. Path is used in unit testing.
128 Item_style is used to identify old-style item plugins that still use
129 an additional (irrelevant) first parameter in the javascript event
130 functions.
132 =cut
134 sub new {
135 my ( $class, $params ) = @_;
136 my $self = $class->SUPER::new();
137 if( ref($params) eq 'HASH' ) {
138 foreach( 'name', 'path', 'item_style' ) {
139 $self->{$_} = $params->{$_};
142 elsif( !ref($params) && $params ) { # use it as plugin name
143 $self->{name} = $params;
144 if( $params =~ /^(.*)\/([^\/]+)$/ ) {
145 $self->{name} = $2;
146 $self->{path} = $1;
149 $self->_error( 'Plugin needs a name' ) if !$self->{name};
150 return $self;
153 =head2 build
155 Generate html and javascript by calling the builder sub of the plugin.
157 Params is a hashref supporting keys: id (=html id for the input field),
158 record (MARC record or undef), dbh (database handle), tagslib, tabloop.
159 Note that some of these parameters are not used in most (if not all)
160 plugins and may be obsoleted in the future (kept for now to provide
161 backward compatibility).
162 The most important one is id; it is used to construct unique javascript
163 function names.
165 Returns success or failure.
167 =cut
169 sub build {
170 my ( $self, $params ) = @_;
171 return if $self->{errstr};
172 return 1 if exists $self->{html}; # no rebuild
174 $self->_load if !$self->{_loaded};
175 return if $self->{errstr}; # load had error
176 return $self->_generate_js( $params );
179 =head2 launch
181 Launches the popup for this plugin by calling its launcher sub
182 Old style plugins still expect to receive a CGI oject, new style
183 plugins expect a params hashref.
184 Returns undef on failure, otherwise launcher return value (if any).
186 =cut
188 sub launch {
189 my ( $self, $params ) = @_;
190 return if $self->{errstr};
192 $self->_load if !$self->{_loaded};
193 return if $self->{errstr}; # load had error
194 return 1 if !exists $self->{launcher}; #just ignore this request
195 if( defined( &{$self->{launcher}} ) ) {
196 my $arg= $self->{oldschool}? $params->{cgi}: $params;
197 return &{$self->{launcher}}( $arg );
199 return $self->_error( 'No launcher sub defined' );
202 # ************** INTERNAL ROUTINES ********************************************
204 sub _error {
205 my ( $self, $info ) = @_;
206 $self->{errstr} = 'ERROR: Plugin '. ( $self->{name}//'' ). ': '. $info;
207 return; #always return false
210 sub _load {
211 my ( $self ) = @_;
213 my ( $rv, $file );
214 return $self->_error( 'Plugin needs a name' ) if !$self->{name}; #2chk
215 $self->{path} //= _valuebuilderpath();
216 $file= $self->{path}. '/'. $self->{name};
217 return $self->_error( 'File not found' ) if !-e $file;
219 # undefine oldschool subroutines before defining them again
220 undef &plugin_parameters;
221 undef &plugin_javascript;
222 undef &plugin;
224 $rv = do( $file );
225 return $self->_error( $@ ) if $@;
227 my $type = ref( $rv );
228 if( $type eq 'HASH' ) { # new style
229 $self->{oldschool} = 0;
230 if( exists $rv->{builder} && ref($rv->{builder}) eq 'CODE' ) {
231 $self->{builder} = $rv->{builder};
232 } elsif( exists $rv->{builder} ) {
233 return $self->_error( 'Builder sub is no coderef' );
235 if( exists $rv->{launcher} && ref($rv->{launcher}) eq 'CODE' ) {
236 $self->{launcher} = $rv->{launcher};
237 } elsif( exists $rv->{launcher} ) {
238 return $self->_error( 'Launcher sub is no coderef' );
240 } else { # old school
241 $self->{oldschool} = 1;
242 if( defined(&plugin_javascript) ) {
243 $self->{builder} = \&plugin_javascript;
245 if( defined(&plugin) ) {
246 $self->{launcher} = \&plugin;
249 if( !$self->{builder} && !$self->{launcher} ) {
250 return $self->_error( 'Plugin does not contain builder nor launcher' );
252 $self->{_loaded} = $self->{oldschool}? 0: 1;
253 # old style needs reload due to possible sub redefinition
254 return 1;
257 sub _valuebuilderpath {
258 return C4::Context->config('intranetdir') . "/cataloguing/value_builder";
259 #Formerly, intranetdir/cgi-bin was tested first.
260 #But the intranetdir from koha-conf already includes cgi-bin for
261 #package installs, single and standard installs.
264 sub _generate_js {
265 my ( $self, $params ) = @_;
267 my $sub = $self->{builder};
268 return 1 if !$sub;
269 #it is safe to assume here that we do have a launcher
270 #we assume that it is launched in an unorthodox fashion
271 #just useless to build, but no problem
273 if( !defined(&$sub) ) { # 2chk: if there is something, it should be code
274 return $self->_error( 'Builder sub not defined' );
277 my @params = $self->{oldschool}//0 ?
278 ( $params->{dbh}, $params->{record}, $params->{tagslib},
279 $params->{id}, $params->{tabloop} ):
280 ( $params );
281 my @rv = &$sub( @params );
282 return $self->_error( 'Builder sub failed: ' . $@ ) if $@;
284 my $arg= $self->{oldschool}? pop @rv: shift @rv;
285 #oldschool returns functionname and script; we only use the latter
286 if( $arg && $arg=~/^\s*\<script/ ) {
287 $self->_process_javascript( $params, $arg );
288 return 1; #so far, so good
290 return $self->_error( 'Builder sub returned bad value(s)' );
293 sub _process_javascript {
294 my ( $self, $params, $script ) = @_;
296 #remove the script tags; we add them again later
297 $script =~ s/\<script[^>]*\>\s*(\/\/\<!\[CDATA\[)?\s*//s;
298 $script =~ s/(\/\/\]\]\>\s*)?\<\/script\>//s;
300 my $id = $params->{id}//'';
301 my $bind = '';
302 my $clickfound = 0;
303 my @events = qw|click focus blur change mouseover mouseout mousedown
304 mouseup mousemove keydown keypress keyup|;
305 foreach my $ev ( @events ) {
306 my $scan = $ev eq 'click' && $self->{oldschool}? 'clic': $ev;
307 if( $script =~ /function\s+($scan\w+)\s*\(([^\)]*)\)/is ) {
308 my ( $bl, $sl ) = $self->_add_binding( $1, $2, $ev, $id );
309 $script .= $sl;
310 $bind .= $bl;
311 $clickfound = 1 if $ev eq 'click';
314 if( !$clickfound ) { # make buttonDot do nothing
315 my ( $bl ) = $self->_add_binding( 'noclick', '', 'click', $id );
316 $bind .= $bl;
318 $self->{noclick} = !$clickfound;
319 $self->{javascript}= _merge_script( $id, $script, $bind );
322 sub _add_binding {
323 # adds some jQuery code for event binding:
324 # $bind contains lines for the actual event binding: .click, .focus, etc.
325 # $script contains function definitions (if needed)
326 my ( $self, $fname, $pars, $ev, $id ) = @_;
327 my ( $bind, $script );
328 my $ctl= $ev eq 'click'? 'buttonDot_'.$id: $id;
329 #click event applies to buttonDot
331 if( $pars =~ /^(e|ev|event)$/i ) { # new style event handler assumed
332 $bind= qq| \$("#$ctl").$ev(\{id: '$id'\}, $fname);\n|;
333 $script='';
334 } elsif( $fname eq 'noclick' ) { # no click: return false, no scroll
335 $bind= qq| \$("#$ctl").$ev(function () { return false; });\n|;
336 $script='';
337 } else { # add real event handler calling the function found
338 $bind=qq| \$("#$ctl").$ev(\{id: '$id'\}, ${fname}_handler);\n|;
339 $script = $self->_add_handler( $ev, $fname );
341 return ( $bind, $script );
344 sub _add_handler {
345 # adds a handler with event parameter
346 # event.data.id is passed to the plugin function in parameters
347 # for the click event we always return false to prevent scrolling
348 my ( $self, $ev, $fname ) = @_;
349 my $first= $self->_first_item_par( $ev );
350 my $prefix= $ev eq 'click'? '': 'return ';
351 my $suffix= $ev eq 'click'? "\n return false;": '';
352 return <<HERE;
353 function ${fname}_handler(event) {
354 $prefix$fname(${first}event.data.id);$suffix
356 HERE
359 sub _first_item_par {
360 my ( $self, $event ) = @_;
361 # needed for backward compatibility
362 # js event functions in old style item plugins have an extra parameter
363 # BUT.. not for all events (exceptions provide employment :)
364 if( $self->{item_style} && $self->{oldschool} &&
365 $event=~/focus|blur|change/ ) {
366 return qq/'0',/;
368 return '';
371 sub _merge_script {
372 # Combine script and event bindings, enclosed in script tags.
373 # The BindEvents function is added to easily repeat event binding;
374 # this is used in additem.js for dynamically created item blocks.
375 my ( $id, $script, $bind ) = @_;
376 chomp ($script, $bind);
377 return <<HERE;
378 <script>
379 $script
380 function BindEvents$id() {
381 $bind
383 \$(document).ready(function() {
384 BindEvents$id();
386 </script>
387 HERE
390 =head1 AUTHOR
392 Marcel de Rooy, Rijksmuseum Amsterdam, The Netherlands
394 =cut