Adding another developer to the history
[wip/koha-chris_n.git] / opac / opac-tags.pl
1 #!/usr/bin/perl
2
3 # Copyright 2000-2002 Katipo Communications
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
21 =head1 NAME
22
23 opac-tags.pl
24
25 =head1 DESCRIPTION
26
27 TODO :: Description here
28
29 C4::Scrubber is used to remove all markup content from the sumitted text.
30
31 =cut
32
33 use strict;
34 use warnings;
35 use CGI;
36 use CGI::Cookie; # need to check cookies before having CGI parse the POST request
37
38 use C4::Auth qw(:DEFAULT check_cookie_auth);
39 use C4::Context;
40 use C4::Debug;
41 use C4::Output 3.02 qw(:html :ajax pagination_bar);
42 use C4::Dates qw(format_date);
43 use C4::Scrubber;
44 use C4::Biblio;
45 use C4::Tags qw(add_tag get_approval_rows get_tag_rows remove_tag);
46
47 my %newtags = ();
48 my @deltags = ();
49 my %counts  = ();
50 my @errors  = ();
51 my $perBibResults = {};
52
53 # Indexes of @errors that do not apply to a particular biblionumber.
54 my @globalErrorIndexes = ();
55
56 sub ajax_auth_cgi ($) {     # returns CGI object
57         my $needed_flags = shift;
58         my %cookies = fetch CGI::Cookie;
59         my $input = CGI->new;
60         my $sessid = $cookies{'CGISESSID'}->value || $input->param('CGISESSID');
61         my ($auth_status, $auth_sessid) = check_cookie_auth($sessid, $needed_flags);
62         $debug and
63         print STDERR "($auth_status, $auth_sessid) = check_cookie_auth($sessid," . Dumper($needed_flags) . ")\n";
64         if ($auth_status ne "ok") {
65                 output_with_http_headers $input, undef,
66                 "window.alert('Your CGI session cookie ($sessid) is not current.  " .
67                 "Please refresh the page and try again.');\n", 'js';
68                 exit 0;
69         }
70         $debug and print STDERR "AJAX request: " . Dumper($input),
71                 "\n(\$auth_status,\$auth_sessid) = ($auth_status,$auth_sessid)\n";
72         return $input;
73 }
74
75 # The trick here is to support multiple tags added to multiple bilbios in one POST.
76 # The HTML might not use this, but it makes it more web-servicey from the start.
77 # So the name of param has to have biblionumber built in.
78 # For lack of anything more compelling, we just use "newtag[biblionumber]"
79 # We split the value into tags at comma and semicolon
80
81 my $is_ajax = is_ajax();
82 my $openadds = C4::Context->preference('TagsModeration') ? 0 : 1;
83 my $query = ($is_ajax) ? &ajax_auth_cgi({}) : CGI->new();
84 unless (C4::Context->preference('TagsEnabled')) {
85         push @errors, {+ tagsdisabled=>1 };
86     push @globalErrorIndexes, $#errors;
87 } else {
88         foreach ($query->param) {
89                 if (/^newtag(.*)/) {
90                         my $biblionumber = $1;
91                         unless ($biblionumber =~ /^\d+$/) {
92                                 $debug and warn "$_ references non numerical biblionumber '$biblionumber'";
93                                 push @errors, {+'badparam' => $_ };
94                 push @globalErrorIndexes, $#errors;
95                                 next;
96                         }
97                         $newtags{$biblionumber} = $query->param($_);
98                 } elsif (/^del(\d+)$/) {
99                         push @deltags, $1;
100                 }
101         }
102 }
103
104 my $add_op = (scalar(keys %newtags) + scalar(@deltags)) ? 1 : 0;
105 my ($template, $loggedinuser, $cookie);
106 if ($is_ajax) {
107         $loggedinuser = C4::Context->userenv->{'number'};  # must occur AFTER auth
108         $debug and print STDERR "op: $loggedinuser\n";
109 } else {
110         ($template, $loggedinuser, $cookie) = get_template_and_user({
111                 template_name   => "opac-tags.tmpl",
112                 query           => $query,
113                 type            => "opac",
114                 authnotrequired => ($add_op ? 0 : 1),   # auth required to add tags
115                 debug           => 1,
116         });
117 }
118
119 if ($add_op) {
120         unless ($loggedinuser) {
121                 push @errors, {+'login' => 1 };
122         push @globalErrorIndexes, $#errors;
123                 %newtags=();    # zero out any attempted additions
124                 @deltags=();    # zero out any attempted deletions
125         }
126 }
127
128 my $scrubber;
129 my @newtags_keys = (keys %newtags);
130 if (scalar @newtags_keys) {
131         $scrubber = C4::Scrubber->new();
132         foreach my $biblionumber (@newtags_keys) {
133         my $bibResults = {adds=>0, errors=>[]};
134                 my @values = split /[;,]/, $newtags{$biblionumber};
135                 foreach (@values) {
136                         s/^\s*(.+)\s*$/$1/;
137                         my $clean_tag = $scrubber->scrub($_);
138                         unless ($clean_tag eq $_) {
139                                 if ($clean_tag =~ /\S/) {
140                                         push @errors, {scrubbed=>$clean_tag};
141                                         push @{$bibResults->{errors}}, {scrubbed=>$clean_tag};
142                                 } else {
143                                         push @errors, {scrubbed_all_bad=>1};
144                                         push @{$bibResults->{errors}}, {scrubbed_all_bad=>1};
145                                         next;   # we don't add it if there's nothing left!
146                                 }
147                         }
148                         my $result = ($openadds) ?
149                                 add_tag($biblionumber,$clean_tag,$loggedinuser,$loggedinuser) : # pre-approved
150                                 add_tag($biblionumber,$clean_tag,$loggedinuser)   ;
151                         if ($result) {
152                                 $counts{$biblionumber}++;
153                 $bibResults->{adds}++;
154                         } else {
155                                 push @errors, {failed_add_tag=>$clean_tag};
156                                 push @{$bibResults->{errors}}, {failed_add_tag=>$clean_tag};
157                                 $debug and warn "add_tag($biblionumber,$clean_tag,$loggedinuser...) returned bad result (" . (defined $result ? $result : 'UNDEF') .")";
158                         }
159                 }
160         $perBibResults->{$biblionumber} = $bibResults;
161         }
162 }
163 my $dels = 0;
164 foreach (@deltags) {
165         if (remove_tag($_,$loggedinuser)) {
166                 $dels++;
167         } else {
168                 push @errors, {failed_delete=>$_};
169         }
170 }
171
172 if ($is_ajax) {
173         my $sum = 0;
174         foreach (values %counts) {$sum += $_;}
175         my $js_reply = sprintf("response = {\n\tadded: %d,\n\tdeleted: %d,\n\terrors: %d",$sum,$dels,scalar @errors);
176
177     # If no add attempts were made, flag global errors.
178     if (@globalErrorIndexes) {
179         $js_reply .= ",\n\tglobal_errors: [";
180         my $first = 1;
181         foreach (@globalErrorIndexes) {
182             $js_reply .= "," unless $first;
183             $first = 0;
184             $js_reply .= "\n\t\t$_";
185         }
186         $js_reply .= "\n\t]";
187     }
188     
189         my $err_string = '';
190         if (scalar @errors) {
191                 $err_string = ",\n\talerts: ["; # open response_function
192                 my $i = 1;
193                 foreach (@errors) {
194                         my $key = (keys %$_)[0];
195                         $err_string .= "\n\t\t KOHA.Tags.tag_message.$key(\"" . $_->{$key} . '")';
196                         if($i < scalar @errors){ $err_string .= ","; }
197                         $i++;
198                 }
199                 $err_string .= "\n\t]\n";       # close response_function
200         }
201
202     # Add per-biblionumber results for use on results page
203     my $js_perbib = "";
204     for my $bib (keys %$perBibResults) {
205         my $bibResult = $perBibResults->{$bib};
206         my $js_bibres = ",\n\t$bib: {\n\t\tadded: $bibResult->{adds}";
207         $js_bibres .= ",\n\t\terrors: [";
208         my $i = 0;
209         foreach (@{$bibResult->{errors}}) {
210             $js_bibres .= "," if ($i);
211                         my $key = (keys %$_)[0];
212                         $js_bibres .= "\n\t\t\t KOHA.Tags.tag_message.$key(\"" . $_->{$key} . '")';
213             $i++;
214         }
215         $js_bibres .= "\n\t\t]\n\t}";
216         $js_perbib .= $js_bibres;
217     }
218
219         output_with_http_headers($query, undef, "$js_reply\n$err_string\n$js_perbib\n};", 'js');
220         exit;
221 }
222
223 my $results = [];
224 my $my_tags = [];
225
226 if ($loggedinuser) {
227         $my_tags = get_tag_rows({borrowernumber=>$loggedinuser});
228         foreach (@$my_tags) {
229                 my $biblio = GetBiblioData($_->{biblionumber});
230                 $_->{bib_summary} = $biblio->{title}; 
231                 ($biblio->{author}) and $_->{bib_summary} .= " by " . $biblio->{author};
232                 my $date = $_->{date_created} || '';
233                 $date =~ /\s+(\d{2}\:\d{2}\:\d{2})/;
234                 $_->{time_created_display} = $1;
235                 $_->{date_created_display} = format_date($_->{date_created});
236         }
237 }
238
239 $template->param(tagsview => 1,
240 dateformat => C4::Context->preference("dateformat"));
241
242 if ($add_op) {
243         my $adds = 0;
244         for (values %counts) {$adds += $_;}
245         $template->param(
246                 add_op => 1,
247                 added_count => $adds,
248                 deleted_count => $dels,
249         );
250 } else {
251         my ($arg,$limit,$mine);
252         my $hardmax = 100;      # you might disagree what this value should be, but there definitely should be a max
253         $limit = $query->param('limit') || $hardmax;
254     $mine =  $query->param('mine') || 0; # set if the patron want to see only his own tags.
255         ($limit =~ /^\d+$/ and $limit <= $hardmax) or $limit = $hardmax;
256         $template->param(limit => $limit);
257         my $arghash = {approved=>1, limit=>$limit, 'sort'=>'-weight_total'};
258     $arghash->{'borrowernumber'} = $loggedinuser if $mine;
259         # ($openadds) or $arghash->{approved} = 1;
260         if ($arg = $query->param('tag')) {
261                 $arghash->{term} = $arg;
262         } elsif ($arg = $query->param('biblionumber')) {
263                 $arghash->{biblionumber} = $arg;
264         }
265         $results = get_approval_rows($arghash);
266
267         my $count = scalar @$results;
268         $template->param(TAGLOOP_COUNT => $count, mine => $mine);
269         # Here we make a halfhearted attempt to separate the tags into "strata" based on weight_total
270         # FIXME: code4lib probably has a better algorithm, iirc
271         # FIXME: when we get a better algorithm, move to C4
272         my $maxstrata = 5;
273         my $strata = 1;
274         my $previous = 0;
275         my $chunk = ($count/$maxstrata)/2;
276         my $total = 0;
277         my %cloud;
278         foreach (reverse @$results) {
279                 my $current = $_->{weight_total};
280                 $total++;
281                 $cloud{$strata}++;
282                 if ($current == $previous) {
283                         $_->{cloudweight} = $strata;
284                         next;
285                 } 
286                 if ($strata < $maxstrata and 
287                         ($cloud{$strata} > $chunk or 
288                         $count-$total <= $maxstrata-$strata)) {
289                         $strata++;
290                 }
291                 $_->{cloudweight} = $strata;
292                 $previous = $current;
293         }
294 }
295 (scalar @errors  ) and $template->param(ERRORS  => \@errors);
296 my @orderedresult = sort { uc($a->{'term'}) cmp uc($b->{'term'}) } @$results;
297 (scalar @$results) and $template->param(TAGLOOP => \@orderedresult );
298 (scalar @$my_tags) and $template->param(MY_TAGS => $my_tags);
299
300 output_html_with_http_headers $query, $cookie, $template->output;
301 __END__
302
303 =head1 EXAMPLE AJAX POST PARAMETERS
304
305 CGISESSID       7c6288263107beb320f70f78fd767f56
306 newtag396       fire,+<a+href="foobar.html">foobar</a>,+<img+src="foo.jpg"+/>
307
308 So this request is trying to add 3 tags to biblio #396.  The CGISESSID is the same as that the browser would
309 typically communicate using cookies.  If it is valid, the server will split the value of "newtag396" and 
310 process the components for addition.  In this case the intended tags are:
311         fire
312         <a+href="foobar.html">foobar</a>
313         <img src="foo.jpg" />
314
315 The first tag is acceptable.  The second will be scrubbed of markup, resulting in the tag "foobar".  
316 The third tag is all markup, and will be rejected.  
317
318 =head1 EXAMPLE AJAX JSON response
319
320 response = {
321         added: 2,
322         deleted: 0,
323         errors: 2,
324         alerts: [
325                  KOHA.Tags.tag_message.scrubbed("foobar"),
326                  KOHA.Tags.tag_message.scrubbed_all_bad("1"),
327         ],
328 };
329
330 =cut
331