Bug 20687: Check all share keys for a given list
[koha.git] / opac / opac-shareshelf.pl
1 #!/usr/bin/perl
2
3 # Copyright 2013 Rijksmuseum
4 #
5 # This file is part of Koha.
6 #
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.
11 #
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.
16 #
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>.
19
20 use Modern::Perl;
21
22 use constant KEYLENGTH     => 10;
23 use constant TEMPLATE_NAME => 'opac-shareshelf.tt';
24 use constant SHELVES_URL =>
25   '/cgi-bin/koha/opac-shelves.pl?display=privateshelves&viewshelf=';
26
27 use CGI qw ( -utf8 );
28 use Email::Valid;
29
30 use C4::Auth;
31 use C4::Context;
32 use C4::Letters;
33 use C4::Members ();
34 use C4::Output;
35
36 use Koha::Patrons;
37 use Koha::Virtualshelves;
38 use Koha::Virtualshelfshares;
39
40
41 # if virtualshelves is disabled, leave immediately
42 if ( ! C4::Context->preference('virtualshelves') ) {
43     my $query = new CGI;
44     print $query->redirect("/cgi-bin/koha/errors/404.pl");
45     exit;
46 }
47
48 #-------------------------------------------------------------------------------
49
50 my $pvar = _init( {} );
51 if ( !$pvar->{errcode} ) {
52     show_invite($pvar)    if $pvar->{op} eq 'invite';
53     confirm_invite($pvar) if $pvar->{op} eq 'conf_invite';
54     show_accept($pvar)    if $pvar->{op} eq 'accept';
55 }
56 load_template_vars($pvar);
57 output_html_with_http_headers $pvar->{query}, $pvar->{cookie},
58   $pvar->{template}->output;
59
60 #-------------------------------------------------------------------------------
61
62 sub _init {
63     my ($param) = @_;
64     my $query = new CGI;
65     $param->{query}       = $query;
66     $param->{shelfnumber} = $query->param('shelfnumber') || 0;
67     $param->{op}          = $query->param('op') || '';
68     $param->{addrlist}    = $query->param('invite_address') || '';
69     $param->{key}         = $query->param('key') || '';
70     $param->{appr_addr}   = [];
71     $param->{fail_addr}   = [];
72     $param->{errcode}     = check_common_errors($param);
73
74     # trim email address
75     if ( $param->{addrlist} ) {
76         $param->{addrlist} =~ s|^\s+||;
77         $param->{addrlist} =~ s|\s+$||;
78     }
79
80     #get some list details
81     my $shelf;
82     my $shelfnumber = $param->{shelfnumber};
83     $shelf = Koha::Virtualshelves->find( $shelfnumber ) unless $param->{errcode};
84     $param->{shelfname} = $shelf ? $shelf->shelfname : q||;
85     $param->{owner}     = $shelf ? $shelf->owner : -1;
86     $param->{category}  = $shelf ? $shelf->category : -1;
87
88     load_template($param);
89     return $param;
90 }
91
92 sub check_common_errors {
93     my ($param) = @_;
94     if ( $param->{op} !~ /^(invite|conf_invite|accept)$/ ) {
95         return 1;    #no operation specified
96     }
97     if ( $param->{shelfnumber} !~ /^\d+$/ ) {
98         return 2;    #invalid shelf number
99     }
100     if ( !C4::Context->preference('OpacAllowSharingPrivateLists') ) {
101         return 3;    #not or no longer allowed?
102     }
103     return;
104 }
105
106 sub show_invite {
107     my ($param) = @_;
108     return unless check_owner_category($param);
109 }
110
111 sub confirm_invite {
112     my ($param) = @_;
113     return unless check_owner_category($param);
114     process_addrlist($param);
115     if ( @{ $param->{appr_addr} } ) {
116         send_invitekey($param);
117     }
118     else {
119         $param->{errcode} = 6;    #not one valid address
120     }
121 }
122
123 sub show_accept {
124     my ($param) = @_;
125
126     my $shelfnumber = $param->{shelfnumber};
127     my $shelf = Koha::Virtualshelves->find( $shelfnumber );
128
129     # The key for accepting is checked later in Koha::Virtualshelf->share
130     # You must not be the owner and the list must be private
131     if ( $shelf->category == 2 or $shelf->owner == $param->{loggedinuser} ) {
132         return;
133     }
134
135     # We could have used ->find with the share id, but we don't want to change
136     # the url sent to the patron
137     my $shared_shelves = Koha::Virtualshelfshares->search(
138         {
139             shelfnumber => $param->{shelfnumber},
140         },
141         {
142             order_by => { -desc => 'sharedate' },
143         }
144     );
145
146     if ( $shared_shelves ) {
147         my $key = keytostring( stringtokey( $param->{key}, 0 ), 1 );
148         while ( my $shared_shelf = $shared_shelves->next ) {
149             my $is_accepted = eval { $shared_shelf->accept( $key, $param->{loggedinuser} ) };
150             if ( $is_accepted ) {
151                 notify_owner($param);
152
153                 #redirect to view of this shared list
154                 print $param->{query}->redirect(
155                     -uri    => SHELVES_URL . $param->{shelfnumber},
156                     -cookie => $param->{cookie}
157                 );
158                 exit;
159             }
160         }
161         $param->{errcode} = 7;    #not accepted (key not found or expired)
162     } else {
163         # This shelf is not shared
164     }
165 }
166
167 sub notify_owner {
168     my ($param) = @_;
169
170     my $patron = Koha::Patrons->find( $param->{owner} );
171     return unless $patron;
172
173     my $toaddr = $patron->notice_email_address or return;
174
175     #prepare letter
176     my $letter = C4::Letters::GetPreparedLetter(
177         module      => 'members',
178         letter_code => 'SHARE_ACCEPT',
179         branchcode  => C4::Context->userenv->{"branch"},
180         lang        => $patron->lang,
181         tables      => { borrowers => $param->{loggedinuser}, },
182         substitute  => { listname => $param->{shelfname}, },
183     );
184
185     #send letter to queue
186     C4::Letters::EnqueueLetter(
187         {
188             letter                 => $letter,
189             message_transport_type => 'email',
190             from_address => C4::Context->preference('KohaAdminEmailAddress'),
191             to_address   => $toaddr,
192         }
193     );
194 }
195
196 sub process_addrlist {
197     my ($param) = @_;
198     my @temp = split /[,:;]/, $param->{addrlist};
199     my @appr_addr;
200     my @fail_addr;
201     foreach my $a (@temp) {
202         $a =~ s/^\s+//;
203         $a =~ s/\s+$//;
204         if ( IsEmailAddress($a) ) {
205             push @appr_addr, $a;
206         }
207         else {
208             push @fail_addr, $a;
209         }
210     }
211     $param->{appr_addr} = \@appr_addr;
212     $param->{fail_addr} = \@fail_addr;
213 }
214
215 sub send_invitekey {
216     my ($param) = @_;
217     my $fromaddr = C4::Context->preference('KohaAdminEmailAddress');
218     my $url =
219         C4::Context->preference('OPACBaseURL')
220       . "/cgi-bin/koha/opac-shareshelf.pl?shelfnumber="
221       . $param->{shelfnumber}
222       . "&op=accept&key=";
223
224     #TODO Waiting for the right http or https solution (BZ 8952 a.o.)
225
226     my @ok;    #the addresses that were processed well
227     foreach my $a ( @{ $param->{appr_addr} } ) {
228         my @newkey = randomlist( KEYLENGTH, 64 );    #generate a new key
229
230         #add a preliminary share record
231         my $shelf = Koha::Virtualshelves->find( $param->{shelfnumber} );
232         my $key = keytostring( \@newkey, 1 );
233         my $is_shared = eval { $shelf->share( $key ); };
234         # TODO Better error handling, catch the exceptions
235         if ( $@ or not $is_shared ) {
236             push @{ $param->{fail_addr} }, $a;
237             next;
238         }
239         push @ok, $a;
240
241         #prepare letter
242         my $letter = C4::Letters::GetPreparedLetter(
243             module      => 'members',
244             letter_code => 'SHARE_INVITE',
245             branchcode  => C4::Context->userenv->{"branch"},
246             lang        => 'default', # Not sure how we could use something more useful else here
247             tables      => { borrowers => $param->{loggedinuser}, },
248             substitute  => {
249                 listname => $param->{shelfname},
250                 shareurl => $url . keytostring( \@newkey, 0 ),
251             },
252         );
253
254         #send letter to queue
255         C4::Letters::EnqueueLetter(
256             {
257                 letter                 => $letter,
258                 message_transport_type => 'email',
259                 from_address           => $fromaddr,
260                 to_address             => $a,
261             }
262         );
263     }
264     $param->{appr_addr} = \@ok;
265 }
266
267 sub check_owner_category {
268     my ($param) = @_;
269
270     #sharing user should be the owner
271     #list should be private
272     $param->{errcode} = 4 if $param->{owner} != $param->{loggedinuser};
273     $param->{errcode} = 5 if !$param->{errcode} && $param->{category} != 1;
274     return !defined $param->{errcode};
275 }
276
277 sub load_template {
278     my ($param) = @_;
279     ( $param->{template}, $param->{loggedinuser}, $param->{cookie} ) =
280       get_template_and_user(
281         {
282             template_name   => TEMPLATE_NAME,
283             query           => $param->{query},
284             type            => "opac",
285             authnotrequired => 0,                 #should be a user
286         }
287       );
288 }
289
290 sub load_template_vars {
291     my ($param) = @_;
292     my $template = $param->{template};
293     my $appr = join '; ', @{ $param->{appr_addr} };
294     my $fail = join '; ', @{ $param->{fail_addr} };
295     $template->param(
296         errcode         => $param->{errcode},
297         op              => $param->{op},
298         shelfnumber     => $param->{shelfnumber},
299         shelfname       => $param->{shelfname},
300         approvedaddress => $appr,
301         failaddress     => $fail,
302     );
303 }
304
305 sub IsEmailAddress {
306
307     #TODO candidate for a module?
308     return Email::Valid->address( $_[0] ) ? 1 : 0;
309 }
310
311 sub randomlist {
312
313     #uses rand, safe enough for this application but not for more sensitive data
314     my ( $length, $base ) = @_;
315     return map { int( rand($base) ); } 1 .. $length;
316 }
317
318 sub keytostring {
319     my ( $keyref, $flgBase64 ) = @_;
320     if ($flgBase64) {
321         my $alphabet = [ 'A' .. 'Z', 'a' .. 'z', 0 .. 9, '+', '/' ];
322         return join '', map { alphabet_char( $_, $alphabet ); } @$keyref;
323     }
324     return join '', map { sprintf( "%02d", $_ ); } @$keyref;
325 }
326
327 sub stringtokey {
328     my ( $str, $flgBase64 ) = @_;
329     my @temp = split '', $str || '';
330     if ($flgBase64) {
331         my $alphabet = [ 'A' .. 'Z', 'a' .. 'z', 0 .. 9, '+', '/' ];
332         return [ map { alphabet_ordinal( $_, $alphabet ); } @temp ];
333     }
334     return [] if $str !~ /^\d+$/;
335     my @retval;
336     for ( my $i = 0 ; $i < @temp - 1 ; $i += 2 ) {
337         push @retval, $temp[$i] * 10 + $temp[ $i + 1 ];
338     }
339     return \@retval;
340 }
341
342 sub alphabet_ordinal {
343     my ( $char, $alphabet ) = @_;
344     for my $ord ( 0 .. $#$alphabet ) {
345         return $ord if $char eq $alphabet->[$ord];
346     }
347     return '';    #ignore missing chars
348 }
349
350 sub alphabet_char {
351
352     #reverse operation for ordinal; ignore invalid numbers
353     my ( $num, $alphabet ) = @_;
354     return $num =~ /^\d+$/ && $num <= $#$alphabet ? $alphabet->[$num] : '';
355 }