Bug 9032: add ability to invite another to share a private list
authorMarcel de Rooy <m.de.rooy@rijksmuseum.nl>
Mon, 20 May 2013 15:57:39 +0000 (17:57 +0200)
committerGalen Charlton <gmc@esilibrary.com>
Sun, 20 Apr 2014 20:57:41 +0000 (20:57 +0000)
This patch

- Adds a Share button for OPAC private lists.
- Allows you to send an invitation to share a list.
- Checks on validity of email addresses (with Email::Valid).

Test plan:
1) Sharing depends on syspref and login.
Toggle the pref OpacAllowSharingPrivateList.
If enabled, you should see the Share button in OPAC/Private lists.
Click on the Share button. You should get Share a list.
Logout and try to go back to opac/opac-shareshelf.pl
It should now present you the login form.

2) Try to share a public list or a list you do not own.
Find a security hole in the interface. Or hack the shareshelf URL and
replace the shelfnumber with a public list number.

3) Enter no email address or invalid ones (no domain, forbidden chars).
If you enter no address, submit should not work.
If you enter only wrong addresses (separated by: ,:; ), you get a
message.

4) Test if sending the invitation works.
Share one of your private lists. Enter your own email address.
After your proc_message_queue cronjob ran, you should have an email.
Check also if you see a new record in the virtualshelfshares table.
Note that the followup patch handles the second part of accepting this
share.

Signed-off-by: Owen Leonard <oleonard@myacpl.org>
Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl>
Signed-off-by: Dobrica Pavlinusic <dpavlin@rot13.org>
Signed-off-by: Jonathan Druart <jonathan.druart@biblibre.com>
Signed-off-by: Galen Charlton <gmc@esilibrary.com>
C4/VirtualShelves.pm
koha-tmpl/opac-tmpl/prog/en/modules/opac-shareshelf.tt [new file with mode: 0644]
koha-tmpl/opac-tmpl/prog/en/modules/opac-shelves.tt
opac/opac-shareshelf.pl [new file with mode: 0755]

index 7ab875e..8aee037 100644 (file)
@@ -29,6 +29,8 @@ use constant SHELVES_COMBO_MAX => 10; #add to combo in search
 use constant SHELVES_MGRPAGE_MAX => 20; #managing page
 use constant SHELVES_POPUP_MAX => 40; #addbybiblio popup
 
+use constant SHARE_INVITATION_EXPIRY_DAYS => 14; #two weeks to accept
+
 use vars qw($VERSION @ISA @EXPORT @EXPORT_OK);
 
 BEGIN {
@@ -42,7 +44,7 @@ BEGIN {
             &ModShelf
             &ShelfPossibleAction
             &DelFromShelf &DelShelf
-            &GetBibliosShelves
+            &GetBibliosShelves &AddShare
     );
         @EXPORT_OK = qw(
             &GetAllShelves &ShelvesMax
@@ -640,6 +642,31 @@ sub HandleDelBorrower {
     $dbh->do($query,undef,($borrower));
 }
 
+=head2 AddShare
+
+     AddShare($shelfnumber, $key);
+
+Adds a share request to the virtualshelves table.
+Authorization must have been checked, and a key must be supplied. See script
+opac-shareshelf.pl for an example.
+This request is not yet confirmed. So it has no borrowernumber, it does have an
+expiry date.
+
+=cut
+
+sub AddShare {
+    my ($shelfnumber, $key)= @_;
+    return if !$shelfnumber || !$key;
+
+    my $sql;
+    my $dbh = C4::Context->dbh;
+    $sql="DELETE FROM virtualshelfshares WHERE sharedate<NOW() LIMIT 10";
+        #housekeeping: add one, remove max 10 expired ones
+    $dbh->do($sql);
+    $sql="INSERT INTO virtualshelfshares (shelfnumber, invitekey, sharedate) VALUES (?, ?, ADDDATE(NOW(),?))";
+    $dbh->do($sql, undef, ($shelfnumber, $key, SHARE_INVITATION_EXPIRY_DAYS));
+}
+
 # internal subs
 
 sub _shelf_count {
diff --git a/koha-tmpl/opac-tmpl/prog/en/modules/opac-shareshelf.tt b/koha-tmpl/opac-tmpl/prog/en/modules/opac-shareshelf.tt
new file mode 100644 (file)
index 0000000..9da9628
--- /dev/null
@@ -0,0 +1,69 @@
+[% INCLUDE 'doc-head-open.inc' %][% IF ( LibraryNameTitle ) %][% LibraryNameTitle %][% ELSE %]Koha online[% END %] catalog &rsaquo; Share a list
+[% INCLUDE 'doc-head-close.inc' %]
+</head>
+<body id="opac-shareshelf">
+<div id="doc3" class="yui-t1">
+<div id="bd">
+[% INCLUDE 'masthead.inc' %]
+
+<div id="yui-main">
+  <div class="yui-b"><div class="yui-g">
+
+[%# This section contains the essential code for error messages and three operations: invite, confirm_invite and accept. %]
+    <h1>Share a list with another patron</h1>
+    [% IF errcode %]
+        [% IF errcode==1 && op %]<div class="dialog alert">The operation [% op %] is not supported.</div>[% END %]
+        [% IF errcode==1 && !op %]<div class="dialog alert">No operation parameter has been passed.</div>[% END %]
+        [% IF errcode==2 %]<div class="dialog alert">Invalid shelf number.</div>[% END %]
+        [% IF errcode==3 %]<div class="dialog alert">The feature of sharing lists is not in use in this library.</div>[% END %]
+        [% IF errcode==4 %]<div class="dialog alert">You can only share a list if you are the owner.</div>[% END %]
+        [% IF errcode==5 %]<div class="dialog alert">You cannot share a public list.</div>[% END %]
+        [% IF errcode==6 %]<div class="dialog alert">Sorry, but you did not enter any valid email address.</div>[% END %]
+
+    [% ELSIF op=='invite' %]
+        <form method="post" onsubmit="return $('#invite_address').val().trim()!='';">
+            <input type="hidden" name="op" value="conf_invite"/>
+            <input type="hidden" name="shelfnumber" value="[% shelfnumber %]"/>
+            <table>
+            <tr>
+            <td><label for="list_name">List name</label></td>
+            <td>[% shelfname %]</td>
+            </tr>
+            <tr>
+            <td><label for="invite_address">Email address</label></td>
+            <td><input id="invite_address" name="invite_address"/></td>
+            </tr>
+            </table>
+            <input type="submit" value="Send"/>
+        </form>
+
+    [% ELSIF op=='conf_invite' %]
+        <p>We have sent invitation emails to share list [% shelfname %] to the mail queue for [% approvedaddress %].</p>
+        [% IF failaddress %]
+            <p>The following addresses appear to be invalid. Please correct them and try again. These are: [% failaddress %]</p>
+        [% END %]
+        <p>You will receive an email notification if someone accepts your share within two weeks.</p>
+
+    [% ELSIF op=='accept' %]
+        [%# TODO: Replace the following two lines %]
+        <p>Thank you for testing this feature.</p>
+        <p>Your signoff will certainly help in finishing the remaining part!</p>
+
+    [% END %]
+[%# End of essential part %]
+
+</div>
+</div>
+</div>
+
+[% IF ( OpacNav ) %]
+  <div class="yui-b">
+  <div id="opacnav" class="container">
+  [% INCLUDE 'navigation.inc' %]
+  </div>
+  </div>
+[% END %]
+
+</div>
+</div>
+[% INCLUDE 'opac-bottom.inc' %]
index b157994..46ac2ed 100644 (file)
@@ -363,7 +363,15 @@ $(document).ready(function() {
                         <input type="hidden" value="1" name="shelves"/>
                          <input type="hidden" value="1" name="DEL-[% shelfnumber %]"/>
                          [% IF ( showprivateshelves ) %]<input type="hidden" name="display" value="privateshelves"/>[% END %]<input type="submit" class="deleteshelf" value="Delete list" onclick="return confirmDelete(MSG_CONFIRM_DELETE_LIST);"/>
-                      </form> [% END %]
+                      </form>
+                    [% IF showprivateshelves && Koha.Preference('OpacAllowSharingPrivateLists') %]
+                        <form action="opac-shareshelf.pl" method="post">
+                            <input type="hidden" name="shelfnumber" value="[% shelfnumber %]" />
+                            <input type="hidden" name="op" value="invite" />
+                            <input type="submit" class="Share" value="Share" />
+                        </form>
+                    [% END %]
+                [% END %]
 
 
                   </div>
@@ -654,7 +662,14 @@ $(document).ready(function() {
                               [% ELSE %]
                                     <input type="submit" class="deleteshelf" onclick="return confirmDelete(MSG_CONFIRM_DELETE_LIST);" value="Delete" />
                               [% END %]
+                              </form>
+                              [% IF Koha.Preference('OpacAllowSharingPrivateLists') %]
+                                <form action="opac-shareshelf.pl" method="post">
+                                  <input type="hidden" name="shelfnumber" value="[% shelveslooppri.shelf %]" />
+                                  <input type="hidden" name="op" value="invite" />
+                                  <input type="submit" class="Share" value="Share" />
                                 </form>
+                              [% END %]
                             [% END %]&nbsp;
                             </td>
                           </tr>
diff --git a/opac/opac-shareshelf.pl b/opac/opac-shareshelf.pl
new file mode 100755 (executable)
index 0000000..6c95f0d
--- /dev/null
@@ -0,0 +1,226 @@
+#!/usr/bin/perl
+
+# Copyright 2013 Rijksmuseum
+#
+# 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.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+use strict;
+use warnings;
+
+use constant KEYLENGTH => 10;
+
+use CGI;
+use Email::Valid;
+
+use C4::Auth;
+use C4::Context;
+use C4::Letters;
+use C4::Output;
+use C4::VirtualShelves;
+
+#-------------------------------------------------------------------------------
+
+my $query= new CGI;
+my ($shelfname, $owner);
+my ($template, $loggedinuser, $cookie);
+my $errcode=0;
+my (@addr, $fail_addr, @newkey);
+my @base64alphabet= ('A'..'Z', 'a'..'z', 0..9, '+', '/');
+
+my $shelfnumber= $query->param('shelfnumber')||0;
+my $op= $query->param('op')||'';
+my $addrlist= $query->param('invite_address')||'';
+my $key= $query->param('key')||'';
+
+#-------------------------------------------------------------------------------
+
+check_common_errors();
+load_template("opac-shareshelf.tmpl");
+if($errcode) {
+    #nothing to do
+}
+elsif($op eq 'invite') {
+    show_invite();
+}
+elsif($op eq 'conf_invite') {
+    confirm_invite();
+}
+elsif($op eq 'accept') {
+    show_accept();
+}
+load_template_vars();
+output_html_with_http_headers $query, $cookie, $template->output;
+
+#-------------------------------------------------------------------------------
+
+sub check_common_errors {
+    if($op!~/^(invite|conf_invite|accept)$/) {
+        $errcode=1; #no operation specified
+        return;
+    }
+    if($shelfnumber!~/^\d+$/) {
+        $errcode=2; #invalid shelf number
+        return;
+    }
+    if(!C4::Context->preference('OpacAllowSharingPrivateLists')) {
+        $errcode=3; #not or no longer allowed?
+        return;
+    }
+}
+
+sub show_invite {
+    return unless check_owner_category();
+}
+
+sub confirm_invite {
+    return unless check_owner_category();
+    process_addrlist();
+    if(@addr) {
+        send_invitekey();
+    }
+    else {
+        $errcode=6; #not one valid address
+    }
+}
+
+sub show_accept {
+    #TODO Add some code here to accept an invitation (followup report)
+}
+
+sub process_addrlist {
+    my @temp= split /[,:;]/, $addrlist;
+    $fail_addr='';
+    foreach my $a (@temp) {
+        $a=~s/^\s+//;
+        $a=~s/\s+$//;
+        if(IsEmailAddress($a)) {
+            push @addr, $a;
+        }
+        else {
+            $fail_addr.= ($fail_addr? '; ': '').$a;
+        }
+    }
+}
+
+sub send_invitekey {
+    my $fromaddr= C4::Context->preference('KohaAdminEmailAddress');
+    my $url= 'http://'.C4::Context->preference('OPACBaseURL');
+    $url.= "/cgi-bin/koha/opac-shareshelf.pl?shelfnumber=$shelfnumber";
+    $url.= "&op=accept&key=";
+        #FIXME Waiting for the right http or https solution (BZ 8952 a.o.)
+
+    foreach my $a (@addr) {
+        @newkey=randomlist(KEYLENGTH, 64); #generate a new key
+
+        #prepare letter
+        my $letter= C4::Letters::GetPreparedLetter(
+            module => 'members',
+            letter_code => 'SHARE_INVITE',
+            branchcode => C4::Context->userenv->{"branch"},
+            tables => { borrowers => $loggedinuser, },
+            substitute => {
+                listname => $shelfname,
+                shareurl => $url.keytostring(\@newkey,0),
+            },
+        );
+
+        #send letter to queue
+        C4::Letters::EnqueueLetter( {
+            letter                 => $letter,
+            message_transport_type => 'email',
+            from_address           => $fromaddr,
+            to_address             => $a,
+        });
+        #add a preliminary share record
+        AddShare($shelfnumber,keytostring(\@newkey,1));
+    }
+}
+
+sub check_owner_category {
+    #FIXME candidate for a module? what held me back is: getting back the two different error codes and the shelfname
+    (undef,$shelfname,$owner,my $category)= GetShelf($shelfnumber);
+    $errcode=4 if $owner!= $loggedinuser; #should be owner
+    $errcode=5 if !$errcode && $category!=1; #should be private
+    return $errcode==0;
+}
+
+sub load_template {
+    my ($file)= @_;
+    ($template, $loggedinuser, $cookie)= get_template_and_user({
+        template_name   => $file,
+        query           => $query,
+        type            => "opac",
+        authnotrequired => 0, #should be a user
+    });
+}
+
+sub load_template_vars {
+    $template->param(
+        errcode         => $errcode,
+        op              => $op,
+        shelfnumber     => $shelfnumber,
+        shelfname       => $shelfname,
+        approvedaddress => (join '; ', @addr),
+        failaddress     => $fail_addr,
+    );
+}
+
+sub IsEmailAddress {
+    #FIXME candidate for a module?
+    return Email::Valid->address($_[0])? 1: 0;
+}
+
+sub randomlist {
+#uses rand, safe enough for this application but not for more sensitive data
+    my ($length, $base)= @_;
+    return map { int(rand($base)); } 1..$length;
+}
+
+sub keytostring {
+    my ($keyref, $flgBase64)= @_;
+    if($flgBase64) {
+        return join '', map { base64chr($_); } @$keyref;
+    }
+    return join '', map { sprintf("%02d",$_); } @$keyref;
+}
+
+sub stringtokey {
+    my ($str, $flgBase64)= @_;
+    my @temp=split '', $str||'';
+    if($flgBase64) {
+        return map { base64ord($_); } @temp;
+    }
+    return () if $str!~/^\d+$/;
+    my @retval;
+    for(my $i=0; $i<@temp-1; $i+=2) {
+        push @retval, $temp[$i]*10+$temp[$i+1];
+    }
+    return @retval;
+}
+
+sub base64ord { #base64 ordinal
+    my ($char)=@_;
+    return 0 -ord('A')+ord($char) if $char=~/[A-Z]/;
+    return 26-ord('a')+ord($char) if $char=~/[a-z]/;
+    return 52-ord('0')+ord($char) if $char=~/[0-9]/;
+    return 62 if $char eq '+';
+    return 63 if $char eq '/';
+    return;
+}
+
+sub base64chr { #reverse operation for ord
+    return $_[0]=~/^\d+$/ && $_[0]<64? $base64alphabet[$_[0]]: undef;
+}