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 under the
8 # terms of the GNU General Public License as published by the Free Software
9 # Foundation; either version 3 of the License, or (at your option) any later
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.
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.
22 Koha::FrameworkPlugin - Facilitate use of plugins in MARC/items editor
26 use Koha::FrameworkPlugin;
27 my $plugin = Koha::FrameworkPlugin({ name => 'EXAMPLE.pl' });
28 $plugin->build( { id => $id });
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 });
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
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
72 Create object (via Class::Accessor).
76 Build uses the builder subroutine of the plugin to build javascript
81 Run the popup of the plugin, as defined by the launcher subroutine.
87 Filename of the plugin.
91 Optional pathname of the plugin.
92 By default plugins are found in cataloguing/value_builder.
97 If set, the plugin will no longer build or launch.
101 Generated javascript for the caller of the plugin (after building).
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
115 use base qw(Class::Accessor);
118 use C4::Biblio qw/GetMarcFromKohaField/;
120 __PACKAGE__->mk_ro_accessors( qw|
121 name path errstr javascript noclick
126 Returns new object based on Class::Accessor, loads additional params.
127 The params hash currently supports keys: name, path, item_style.
128 Name is mandatory. Path is used in unit testing.
129 Item_style is used to identify old-style item plugins that still use
130 an additional (irrelevant) first parameter in the javascript event
136 my ( $class, $params ) = @_;
137 my $self = $class->SUPER::new();
138 if( ref($params) eq 'HASH' ) {
139 foreach( 'name', 'path', 'item_style' ) {
140 $self->{$_} = $params->{$_};
143 elsif( !ref($params) && $params ) { # use it as plugin name
144 $self->{name} = $params;
145 if( $params =~ /^(.*)\/([^\/]+)$/ ) {
150 $self->_error( 'Plugin needs a name' ) if !$self->{name};
156 Generate html and javascript by calling the builder sub of the plugin.
158 Params is a hashref supporting keys: id (=html id for the input field),
159 record (MARC record or undef), dbh (database handle), tagslib, tabloop.
160 Note that some of these parameters are not used in most (if not all)
161 plugins and may be obsoleted in the future (kept for now to provide
162 backward compatibility).
163 The most important one is id; it is used to construct unique javascript
166 Returns success or failure.
171 my ( $self, $params ) = @_;
172 return if $self->{errstr};
173 return 1 if exists $self->{html}; # no rebuild
175 $self->_load if !$self->{_loaded};
176 return if $self->{errstr}; # load had error
177 return $self->_generate_js( $params );
182 Launches the popup for this plugin by calling its launcher sub
183 Old style plugins still expect to receive a CGI oject, new style
184 plugins expect a params hashref.
185 Returns undef on failure, otherwise launcher return value (if any).
190 my ( $self, $params ) = @_;
191 return if $self->{errstr};
193 $self->_load if !$self->{_loaded};
194 return if $self->{errstr}; # load had error
195 return 1 if !exists $self->{launcher}; #just ignore this request
196 if( defined( &{$self->{launcher}} ) ) {
197 my $arg= $self->{oldschool}? $params->{cgi}: $params;
198 return &{$self->{launcher}}( $arg );
200 return $self->_error( 'No launcher sub defined' );
203 # ************** INTERNAL ROUTINES ********************************************
206 my ( $self, $info ) = @_;
207 $self->{errstr} = 'ERROR: Plugin '. ( $self->{name}//'' ). ': '. $info;
208 return; #always return false
215 return $self->_error( 'Plugin needs a name' ) if !$self->{name}; #2chk
216 $self->{path} //= _valuebuilderpath();
217 $file= $self->{path}. '/'. $self->{name};
218 return $self->_error( 'File not found' ) if !-e $file;
220 # undefine oldschool subroutines before defining them again
221 undef &plugin_parameters;
222 undef &plugin_javascript;
226 return $self->_error( $@ ) if $@;
228 my $type = ref( $rv );
229 if( $type eq 'HASH' ) { # new style
230 $self->{oldschool} = 0;
231 if( exists $rv->{builder} && ref($rv->{builder}) eq 'CODE' ) {
232 $self->{builder} = $rv->{builder};
233 } elsif( exists $rv->{builder} ) {
234 return $self->_error( 'Builder sub is no coderef' );
236 if( exists $rv->{launcher} && ref($rv->{launcher}) eq 'CODE' ) {
237 $self->{launcher} = $rv->{launcher};
238 } elsif( exists $rv->{launcher} ) {
239 return $self->_error( 'Launcher sub is no coderef' );
241 } else { # old school
242 $self->{oldschool} = 1;
243 if( defined(&plugin_javascript) ) {
244 $self->{builder} = \&plugin_javascript;
246 if( defined(&plugin) ) {
247 $self->{launcher} = \&plugin;
250 if( !$self->{builder} && !$self->{launcher} ) {
251 return $self->_error( 'Plugin does not contain builder nor launcher' );
253 $self->{_loaded} = $self->{oldschool}? 0: 1;
254 # old style needs reload due to possible sub redefinition
258 sub _valuebuilderpath {
259 return C4::Context->config('intranetdir') . "/cataloguing/value_builder";
260 #Formerly, intranetdir/cgi-bin was tested first.
261 #But the intranetdir from koha-conf already includes cgi-bin for
262 #package installs, single and standard installs.
266 my ( $self, $params ) = @_;
268 my $sub = $self->{builder};
270 #it is safe to assume here that we do have a launcher
271 #we assume that it is launched in an unorthodox fashion
272 #just useless to build, but no problem
274 if( !defined(&$sub) ) { # 2chk: if there is something, it should be code
275 return $self->_error( 'Builder sub not defined' );
278 my @params = $self->{oldschool}//0 ?
279 ( $params->{dbh}, $params->{record}, $params->{tagslib},
280 $params->{id}, $params->{tabloop} ):
282 my @rv = &$sub( @params );
283 return $self->_error( 'Builder sub failed: ' . $@ ) if $@;
285 my $arg= $self->{oldschool}? pop @rv: shift @rv;
286 #oldschool returns functionname and script; we only use the latter
287 if( $arg && $arg=~/^\s*\<script/ ) {
288 $self->_process_javascript( $params, $arg );
289 return 1; #so far, so good
291 return $self->_error( 'Builder sub returned bad value(s)' );
294 sub _process_javascript {
295 my ( $self, $params, $script ) = @_;
297 #remove the script tags; we add them again later
298 $script =~ s/\<script[^>]*\>\s*(\/\/\<!\[CDATA\[)?\s*//s;
299 $script =~ s/(\/\/\]\]\>\s*)?\<\/script\>//s;
301 my $id = $params->{id}//'';
304 my @events = qw|click focus blur change mouseover mouseout mousedown
305 mouseup mousemove keydown keypress keyup|;
306 foreach my $ev ( @events ) {
307 my $scan = $ev eq 'click' && $self->{oldschool}? 'clic': $ev;
308 if( $script =~ /function\s+($scan\w+)\s*\(([^\)]*)\)/is ) {
309 my ( $bl, $sl ) = $self->_add_binding( $1, $2, $ev, $id );
312 $clickfound = 1 if $ev eq 'click';
315 if( !$clickfound ) { # make buttonDot do nothing
316 my ( $bl ) = $self->_add_binding( 'noclick', '', 'click', $id );
319 $self->{noclick} = !$clickfound;
320 $self->{javascript}= _merge_script( $id, $script, $bind );
324 # adds some jQuery code for event binding:
325 # $bind contains lines for the actual event binding: .click, .focus, etc.
326 # $script contains function definitions (if needed)
327 my ( $self, $fname, $pars, $ev, $id ) = @_;
328 my ( $bind, $script );
329 my $ctl= $ev eq 'click'? 'buttonDot_'.$id: $id;
330 #click event applies to buttonDot
332 if( $pars =~ /^(e|ev|event)$/i ) { # new style event handler assumed
333 $bind= qq| \$("#$ctl").$ev(\{id: '$id'\}, $fname);\n|;
335 } elsif( $fname eq 'noclick' ) { # no click: return false, no scroll
336 $bind= qq| \$("#$ctl").$ev(function () { return false; });\n|;
338 } else { # add real event handler calling the function found
339 $bind=qq| \$("#$ctl").$ev(\{id: '$id'\}, ${fname}_handler);\n|;
340 $script = $self->_add_handler( $ev, $fname );
342 return ( $bind, $script );
346 # adds a handler with event parameter
347 # event.data.id is passed to the plugin function in parameters
348 # for the click event we always return false to prevent scrolling
349 my ( $self, $ev, $fname ) = @_;
350 my $first= $self->_first_item_par( $ev );
351 my $prefix= $ev eq 'click'? '': 'return ';
352 my $suffix= $ev eq 'click'? "\n return false;": '';
354 function ${fname}_handler(event) {
355 $prefix$fname(${first}event.data.id);$suffix
360 sub _first_item_par {
361 my ( $self, $event ) = @_;
362 # needed for backward compatibility
363 # js event functions in old style item plugins have an extra parameter
364 # BUT.. not for all events (exceptions provide employment :)
365 if( $self->{item_style} && $self->{oldschool} &&
366 $event=~/focus|blur|change/ ) {
373 # Combine script and event bindings, enclosed in script tags.
374 # The BindEvents function is added to easily repeat event binding;
375 # this is used in additem.js for dynamically created item blocks.
376 my ( $id, $script, $bind ) = @_;
377 chomp ($script, $bind);
379 <script type="text/javascript">
382 function BindEvents$id() {
385 \$(document).ready(function() {
395 Marcel de Rooy, Rijksmuseum Amsterdam, The Netherlands