Bug 9587 : Mozilla Persona login
authorChris Cormack <chris@bigballofwax.co.nz>
Mon, 11 Feb 2013 09:34:20 +0000 (22:34 +1300)
committerJared Camins-Esakov <jcamins@cpbibliography.com>
Sun, 24 Feb 2013 15:15:35 +0000 (10:15 -0500)
    Working on Mozilla Persona support (browser id)

    This will let a user log into Koha using browser id, if their email
    address used matches the email address inside Koha.

    Once an assertion is received, we simply need to find the user that
    matches that email address, and create a session for them.

    opac/svc/login handles this part.

    The nice thing about it is, the user doesn't have to do anything, like
    linking their account. As long as the email address they are using to
    identify themselves in browserid is the same as the one in Koha it
    will just work.

    This is covered by a systempreference, to allow people to do it, and
    is of course totally opt in, it works alongside normal Koha (or any
    other method) of login. So only those choosing to use it, need use it

Test Plan

1/ Make sure OPACBaseURL is set correctly
2/ Switch on the Persona syspref
3/ Make a borrower (or edit one) to have the email you plan to use as
the primary email
4/ Click sign in with email, make or use a persona account
5/ Logout
6/ Check you can still login and logout the normal way

Signed-off-by: Bernardo Gonzalez Kriegel <bgkriegel@gmail.com>
Comment: Works great.
It's not browser dependent, but tested with chrome, firefox, opera and safari.
Old an new login system works.
Minor errors, addresed in follow-up.

Signed-off-by: Katrin Fischer <Katrin.Fischer.83@web.de>
Signed-off-by: Jared Camins-Esakov <jcamins@cpbibliography.com>
13 files changed:
C4/Auth.pm
installer/data/mysql/sysprefs.sql
installer/data/mysql/updatedatabase.pl
koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/admin.pref
koha-tmpl/opac-tmpl/prog/en/css/persona-buttons.css [new file with mode: 0644]
koha-tmpl/opac-tmpl/prog/en/includes/doc-head-close.inc
koha-tmpl/opac-tmpl/prog/en/includes/masthead.inc
koha-tmpl/opac-tmpl/prog/en/includes/opac-bottom.inc
koha-tmpl/opac-tmpl/prog/en/js/browserid_include.js [new file with mode: 0644]
koha-tmpl/opac-tmpl/prog/en/modules/opac-auth.tt
koha-tmpl/opac-tmpl/prog/en/modules/opac-main.tt
koha-tmpl/opac-tmpl/prog/images/sign_in_green.png [new file with mode: 0644]
opac/svc/login [new file with mode: 0755]

index 0c57a3f..d19a4e2 100644 (file)
@@ -323,6 +323,7 @@ sub get_template_and_user {
             LoginBranchcode              => (C4::Context->userenv?C4::Context->userenv->{"branch"}:"insecure"),
             LoginFirstname               => (C4::Context->userenv?C4::Context->userenv->{"firstname"}:"Bel"),
             LoginSurname                 => C4::Context->userenv?C4::Context->userenv->{"surname"}:"Inconnu",
+            emailaddress                 => C4::Context->userenv?C4::Context->userenv->{"emailaddress"}:undef,
             TagsEnabled                  => C4::Context->preference("TagsEnabled"),
             hide_marc                    => C4::Context->preference("hide_marc"),
             item_level_itypes            => C4::Context->preference('item-level_itypes'),
@@ -333,6 +334,7 @@ sub get_template_and_user {
             using_https                  => $in->{'query'}->https() ? 1 : 0,
             noItemTypeImages             => C4::Context->preference("noItemTypeImages"),
             marcflavour                  => C4::Context->preference("marcflavour"),
+            persona                      => C4::Context->preference("persona"),
     );
 
     if ( $in->{'type'} eq "intranet" ) {
@@ -618,6 +620,7 @@ sub checkauth {
     my $authnotrequired = shift;
     my $flagsrequired   = shift;
     my $type            = shift;
+    my $persona         = shift;
     $type = 'opac' unless $type;
 
     my $dbh     = C4::Context->dbh;
@@ -634,7 +637,7 @@ sub checkauth {
     # when using authentication against multiple CAS servers, as configured in Auth_cas_servers.yaml
     my $casparam = $query->param('cas');
 
-        if ( $userid = $ENV{'REMOTE_USER'} ) {
+    if ( $userid = $ENV{'REMOTE_USER'} ) {
             # Using Basic Authentication, no cookies required
         $cookie = $query->cookie(
             -name     => 'CGISESSID',
@@ -644,6 +647,9 @@ sub checkauth {
         );
         $loggedin = 1;
     }
+    elsif ( $persona ){
+      # we dont want to set a session because we are being called by a persona callback
+    }
     elsif ( $sessionID = $query->cookie("CGISESSID") )
     {    # assignment, not comparison
         my $session = get_session($sessionID);
@@ -728,6 +734,7 @@ sub checkauth {
         }
     }
     unless ($userid || $sessionID) {
+
         #we initiate a session prior to checking for a username to allow for anonymous sessions...
         my $session = get_session("") or die "Auth ERROR: Cannot get_session()";
         my $sessionID = $session->id;
@@ -737,13 +744,14 @@ sub checkauth {
             -value    => $session->id,
             -HttpOnly => 1
         );
-    $userid = $query->param('userid');
+       $userid = $query->param('userid');
         if (   ( $cas && $query->param('ticket') )
             || $userid
             || ( my $pki_field = C4::Context->preference('AllowPKIAuth') ) ne
-            'None' )
+            'None' || $persona )
         {
             my $password = $query->param('password');
+
             my ( $return, $cardnumber );
             if ( $cas && $query->param('ticket') ) {
                 my $retuserid;
@@ -752,7 +760,30 @@ sub checkauth {
                 $userid = $retuserid;
                 $info{'invalidCasLogin'} = 1 unless ($return);
             }
-            elsif (
+
+    elsif ($persona) {
+        my $value = $persona;
+
+        # If we're looking up the email, there's a chance that the person
+        # doesn't have a userid. So if there is none, we pass along the
+        # borrower number, and the bits of code that need to know the user
+        # ID will have to be smart enough to handle that.
+        require C4::Members;
+        my @users_info = C4::Members::GetBorrowersWithEmail($value);
+        if (@users_info) {
+
+            # First the userid, then the borrowernum
+            $value = $users_info[0][1] || $users_info[0][0];
+        }
+        else {
+            undef $value;
+        }
+        $return = $value ? 1 : 0;
+        $userid = $value;
+
+    }
+
+    elsif (
                 ( $pki_field eq 'Common Name' && $ENV{'SSL_CLIENT_S_DN_CN'} )
                 || (   $pki_field eq 'emailAddress'
                     && $ENV{'SSL_CLIENT_S_DN_Email'} )
@@ -780,17 +811,18 @@ sub checkauth {
                     }
                 }
 
-                # 0 for no user, 1 for normal, 2 for demo user.
+
                 $return = $value ? 1 : 0;
                 $userid = $value;
-            }
+
+       }
             else {
                 my $retuserid;
                 ( $return, $cardnumber, $retuserid ) =
                   checkpw( $dbh, $userid, $password, $query );
                 $userid = $retuserid if ( $retuserid ne '' );
-            }
-               if ($return) {
+        }
+           if ($return) {
                #_session_log(sprintf "%20s from %16s logged in  at %30s.\n", $userid,$ENV{'REMOTE_ADDR'},(strftime '%c', localtime));
                if ( $flags = haspermission(  $userid, $flagsrequired ) ) {
                                        $loggedin = 1;
@@ -992,6 +1024,7 @@ sub checkauth {
         wrongip            => $info{'wrongip'},
         PatronSelfRegistration => C4::Context->preference("PatronSelfRegistration"),
         PatronSelfRegistrationDefaultCategory => C4::Context->preference("PatronSelfRegistrationDefaultCategory"),
+        persona            => C4::Context->preference("Persona"),
     );
 
     $template->param( OpacPublic => C4::Context->preference("OpacPublic"));
index 387f977..ca9b6bd 100644 (file)
@@ -415,3 +415,4 @@ INSERT IGNORE INTO systempreferences (variable,value,explanation,options,type) V
 INSERT INTO systempreferences (variable,value,options,explanation,type) VALUES ('OPACNumbersPreferPhrase','0', NULL, 'Control the use of phr operator in callnumber and standard number OPAC searches', 'YesNo');
 INSERT INTO systempreferences (variable,value,options,explanation,type) VALUES ('IntranetNumbersPreferPhrase','0', NULL, 'Control the use of phr operator in callnumber and standard number staff client searches', 'YesNo');
 INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES('UNIMARCField100Language', 'fre','UNIMARC field 100 default language',NULL,'short');
+INSERT IGNORE INTO systempreferences (variable,value,explanation,options,type) VALUES('Persona',0,'Use Mozilla Persona for login','','YesNo');
index 2108365..99cbec3 100755 (executable)
@@ -6428,6 +6428,15 @@ if ( CheckVersion($DBversion) ) {
     SetVersion($DBversion);
 }
 
+$DBversion = "XXX";
+if ( CheckVersion($DBversion) ) {
+    $dbh->do(
+"INSERT IGNORE INTO systempreferences (variable,value,explanation,options,type) VALUES('Persona',0,'Use Mozilla Persona for login','','YesNo')"
+    );
+    print "Upgrade to $DBversion done (Bug 9587 - Allow login via Persona)\n";
+    SetVersion($DBversion);
+}
+
 
 =head1 FUNCTIONS
 
index 4e907e1..afffb3a 100644 (file)
@@ -104,6 +104,14 @@ Administration:
                   Common Name: the Common Name
                   emailAddress: the emailAddress
             - field for SSL client certificate authentication
+    Mozilla Persona:
+        -
+            - pref: Persona
+              default: 0
+              choices:
+                  yes: Allow
+                  no: "Don't Allow"
+            - Mozilla persona for login
     Search Engine:
         -
             - pref: SearchEngine
diff --git a/koha-tmpl/opac-tmpl/prog/en/css/persona-buttons.css b/koha-tmpl/opac-tmpl/prog/en/css/persona-buttons.css
new file mode 100644 (file)
index 0000000..d1acbb0
--- /dev/null
@@ -0,0 +1,228 @@
+/* Link body */
+.persona-button{
+  color: #fff;
+  display: inline-block;
+  font-size: 14px;
+  font-family: Helvetica, Arial, sans-serif;
+  font-weight: bold;
+  line-height: 1.1;
+  overflow: hidden;
+  position: relative;
+  text-decoration: none;
+  text-shadow: 0 1px rgba(0,0,0,0.5), 0 0 2px rgba(0,0,0,0.2);
+
+  background: #297dc3;
+  background: -moz-linear-gradient(top, #43a6e2, #287cc2);
+  background: -ms-linear-gradient(top, #43a6e2, #287cc2);
+  background: -o-linear-gradient(top, #43a6e2, #287cc2);
+  background: -webkit-linear-gradient(top, #43a6e2, #287cc2);
+  background: linear-gradient(top, #43a6e2, #287cc2);
+
+  -moz-border-radius:   3px;
+  -ms-border-radius:     3px;
+  -o-border-radius:     3px;
+  -webkit-border-radius:   3px;
+  border-radius:       3px;
+
+  -moz-box-shadow:   0 1px 0 rgba(0,0,0,0.2);
+  -ms-box-shadow:   0 1px 0 rgba(0,0,0,0.2);
+  -o-box-shadow:     0 1px 0 rgba(0,0,0,0.2);
+  -webkit-box-shadow: 0 1px 0 rgba(0,0,0,0.2);
+  box-shadow:     0 1px 0 rgba(0,0,0,0.2);
+}
+
+.persona-button:hover{
+  background: #21669f;
+  background: -moz-linear-gradient(top, #3788b9, #21669f);
+  background: -ms-linear-gradient(top, #3788b9, #21669f);
+  background: -o-linear-gradient(top, #3788b9, #21669f);
+  background: -webkit-linear-gradient(top, #3788b9, #21669f);
+  background: linear-gradient(top, #3788b9, #21669f);
+}
+
+.persona-button:active, .persona-button:focus{
+  top: 1px;
+  -moz-box-shadow:   none;
+  -ms-box-shadow:   none;
+  -o-box-shadow:     none;
+  -webkit-box-shadow: none;
+  box-shadow:     none;
+}
+
+.persona-button span{
+  display: inline-block;
+  padding: 5px 10px 5px 40px;
+}
+
+/* Icon */
+.persona-button span:after{
+  background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA0AAAAPCAYAAAA/I0V3AAAA4klEQVR42o2RWaqEMBRE3YaCiDjPwQGcd9CrysLv4wTyoLFD90dxqbp1EgdPRB7Kskznea6Zn/aPoKoqUUrJOI5m4l2QBfSyLHKep1zXZSae3An1fS/7vst931bGkzuhaZrsLVbGkzuheZ7lOI6HyJ2QUkqv6yrbtv0LT+6E7G0UrfBfP3lZlpoXH4ZBmHgn5Pv+KwxDfqp0XQdgJp6c/RsUBIGOokiSJDE/s21bACbe5Ozp0TdAHMdSFIXUdS1N01C2wpObPT36HifwCJzI0iX29Oh7XP0E3CB9L01TzM+i/wePv4ZE5RtAngAAAABJRU5ErkJggg==) 10px center no-repeat;
+  content: '';
+  display: block;
+  width: 31px;
+
+  position: absolute;
+  bottom: 0;
+  left: -3px;
+  top: 0;
+  z-index: 10;
+}
+
+/*  Icon background */
+.persona-button span:before{
+  content: '';
+  display: block;
+  height: 100%;
+  width: 20px;
+
+  position: absolute;
+  bottom: 0;
+  left: 0;
+  top: 0;
+  z-index: 1;
+
+  background: #42a9dd;
+  background: -moz-linear-gradient(top, #50b8e8, #3095ce);
+  background: -ms-linear-gradient(top, #50b8e8, #3095ce);
+  background: -o-linear-gradient(top, #50b8e8, #3095ce);
+  background: -webkit-linear-gradient(top, #50b8e8, #3095ce);
+  background: linear-gradient(top, #50b8e8, #3095ce);
+
+  -moz-border-radius:   3px 0 0 3px;
+  -ms-border-radius:     3px 0 0 3px;
+  -o-border-radius:     3px 0 0 3px;
+  -webkit-border-radius:   3px 0 0 3px;
+  border-radius:       3px 0 0 3px;
+}
+
+/* Triangle */
+.persona-button:before{
+  background: #42a9dd;
+  content: '';
+  display: block;
+  height: 26px;
+  width: 26px;
+
+  position: absolute;
+  left: 2px;
+  top: 50%;
+  margin-top: -13px;
+  z-index: 0;
+
+  background: -moz-linear-gradient(-45deg, #50b8e8, #3095ce);
+  background: -ms-linear-gradient(-45deg, #50b8e8, #3095ce);
+  background: -o-linear-gradient(-45deg, #50b8e8, #3095ce);
+  background: -webkit-linear-gradient(-45deg, #50b8e8, #3095ce);
+  background: linear-gradient(-45deg, #3095ce, #50b8e8); /* flipped for updated spec */
+
+  -moz-box-shadow:   1px -1px 1px rgba(0,0,0,0.1);
+  -ms-box-shadow:   1px -1px 1px rgba(0,0,0,0.1);
+  -o-box-shadow:     1px -1px 1px rgba(0,0,0,0.1);
+  -webkit-box-shadow: 1px -1px 1px rgba(0,0,0,0.1);
+  box-shadow:     1px -1px 1px rgba(0,0,0,0.1);
+
+  -moz-transform:   rotate(45deg);
+  -ms-transform:     rotate(45deg);
+  -o-transform:     rotate(45deg);
+  -webkit-transform:   rotate(45deg);
+  transform:       rotate(45deg);
+}
+
+/* Inset shadow (required here because the icon background clips it when on the `a` element) */
+.persona-button:after{
+  content: '';
+  display: block;
+  height: 100%;
+  width: 100%;
+
+  position: absolute;
+  left: 0;
+  top: 0;
+  bottom: 0;
+  right: 0;
+  z-index: 10;
+
+  -moz-border-radius:   3px;
+  -ms-border-radius:     3px;
+  -o-border-radius:     3px;
+  -webkit-border-radius:   3px;
+  border-radius:       3px;
+
+  -moz-box-shadow:   inset 0 -1px 0 rgba(0,0,0,0.3);
+  -ms-box-shadow:   inset 0 -1px 0 rgba(0,0,0,0.3);
+  -o-box-shadow:     inset 0 -1px 0 rgba(0,0,0,0.3);
+  -webkit-box-shadow: inset 0 -1px 0 rgba(0,0,0,0.3);
+  box-shadow:     inset 0 -1px 0 rgba(0,0,0,0.3);
+}
+
+/* ========================================================
+ *    Dark button
+ * ===================================================== */
+.persona-button.dark{
+  background: #3c3c3c;
+  background: -moz-linear-gradient(top, #606060, #3c3c3c);
+  background: -ms-linear-gradient(top, #606060, #3c3c3c);
+  background: -o-linear-gradient(top, #606060, #3c3c3c);
+  background: -webkit-linear-gradient(top, #606060, #3c3c3c);
+  background: linear-gradient(top, #606060, #3c3c3c);
+}
+.persona-button.dark:hover{
+  background: #2d2d2d;
+  background: -moz-linear-gradient(top, #484848, #2d2d2d);
+  background: -ms-linear-gradient(top, #484848, #2d2d2d);
+  background: -o-linear-gradient(top, #484848, #2d2d2d);
+  background: -webkit-linear-gradient(top, #484848, #2d2d2d);
+  background: linear-gradient(top, #484848, #2d2d2d);
+}
+.persona-button.dark span:before{ /* Icon BG */
+  background: #d34f2d;
+  background: -moz-linear-gradient(top, #ebac45, #d34f2d);
+  background: -ms-linear-gradient(top, #ebac45, #d34f2d);
+  background: -o-linear-gradient(top, #ebac45, #d34f2d);
+  background: -webkit-linear-gradient(top, #ebac45, #d34f2d);
+  background: linear-gradient(top, #ebac45, #d34f2d);
+}
+.persona-button.dark:before{ /* Triangle */
+  background: #d34f2d;
+  background: -moz-linear-gradient(-45deg, #ebac45, #d34f2d);
+  background: -ms-linear-gradient(-45deg, #ebac45, #d34f2d);
+  background: -o-linear-gradient(-45deg, #ebac45, #d34f2d);
+  background: -webkit-linear-gradient(-45deg, #ebac45, #d34f2d);
+  background: linear-gradient(-45deg, #d34f2d, #ebac45); /* flipped for updated spec */
+}
+
+/* ========================================================
+ *    Orange button
+ * ===================================================== */
+.persona-button.orange{
+  background: #ee731a;
+  background: -moz-linear-gradient(top, #ee731a, #d03116);
+  background: -ms-linear-gradient(top, #ee731a, #d03116);
+  background: -o-linear-gradient(top, #ee731a, #d03116);
+  background: -webkit-linear-gradient(top, #ee731a, #d03116);
+  background: linear-gradient(top, #ee731a, #d03116);
+}
+.persona-button.orange:hover{
+  background: #cb6216;
+  background: -moz-linear-gradient(top, #cb6216, #b12a13);
+  background: -ms-linear-gradient(top, #cb6216, #b12a13);
+  background: -o-linear-gradient(top, #cb6216, #b12a13);
+  background: -webkit-linear-gradient(top, #cb6216, #b12a13);
+  background: linear-gradient(top, #cb6216, #b12a13);
+}
+.persona-button.orange span:before{ /* Icon BG */
+  background: #e84a21;
+  background: -moz-linear-gradient(top, #f7ad27, #e84a21);
+  background: -ms-linear-gradient(top, #f7ad27, #e84a21);
+  background: -o-linear-gradient(top, #f7ad27, #e84a21);
+  background: -webkit-linear-gradient(top, #f7ad27, #e84a21);
+  background: linear-gradient(top, #f7ad27, #e84a21);
+}
+.persona-button.orange:before{ /* Triangle */
+  background: #e84a21;
+  background: -moz-linear-gradient(-45deg, #f7ad27, #e84a21);
+  background: -ms-linear-gradient(-45deg, #f7ad27, #e84a21);
+  background: -o-linear-gradient(-45deg, #f7ad27, #e84a21);
+  background: -webkit-linear-gradient(-45deg, #f7ad27, #e84a21);
+  background: linear-gradient(-45deg, #e84a21, #f7ad27); /* flipped for updated spec */
+}
index 8c5ff1f..45424e8 100644 (file)
@@ -20,6 +20,9 @@
     <link rel="stylesheet" type="text/css" href="[% themelang %]/css/[% opac_css_override %]" />
 [% END %]
 <link rel="stylesheet" type="text/css" media="print" href="[% themelang %]/css/print.css" />
+[% IF persona %]
+ <link rel="stylesheet" type="text/css" href="[% themelang %]/css/persona-buttons.css" />
+[% END %]
 [% IF ( OPACMobileUserCSS ) %]<style type="text/css" media="screen and (max-width:700px)">[% OPACMobileUserCSS %]</style>[% END %]
 [% IF ( OPACUserCSS ) %]<style type="text/css">[% OPACUserCSS %]</style>[% END %]
 <!-- yui js --> 
index 35db422..31e90bc 100644 (file)
@@ -10,7 +10,7 @@
             [% IF ( ShowOpacRecentSearchLink ) %]
                 <li><a href="/cgi-bin/koha/opac-search-history.pl" title="View your search history">Search history</a> [<a class="logout" href="/cgi-bin/koha/opac-search-history.pl?action=delete" title="Delete your search history" onclick="return confirm(MSG_DELETE_SEARCH_HISTORY);">x</a>]</li>
             [% END %]
-                       [% IF ( loggedinusername ) %]<li><a class="logout" id="logout" href="/cgi-bin/koha/opac-main.pl?logout.x=1">Log Out</a></li>[% END %]
+                       [% IF ( loggedinusername ) %]<li><a class="logout" id="logout" href="/cgi-bin/koha/opac-main.pl?logout.x=1" [% IF persona %] onclick='navigator.id.logout();'[% END %]>Log Out</a></li>[% END %]
        </ul>   
   [% END %]
 </div>
index 3df0221..25d74e2 100644 (file)
 
 [% END %]
 
+[% IF persona %]
+<script src="https://login.persona.org/include.js"></script>
+<script type="text/javascript" language="javascript">
+
+navigator.id.watch({
+    loggedInUser: [% IF emailaddress %]'[% emailaddress %]'[% ELSE %]null[% END %],
+    onlogin: function (assertion) {
+        $.post('/cgi-bin/koha/svc/login',
+            { assertion: assertion },
+            function (data) {
+                window.location = '/cgi-bin/koha/opac-user.pl';
+            }
+        );
+    },
+    onlogout: function () {
+        window.location = '/cgi-bin/koha/opac-main.pl?logout.x=1';
+    }
+});
+
+var signinLink = document.getElementById('browserid');
+
+if (signinLink) {
+    signinLink.onclick = function(evt) {
+        // Requests a signed identity assertion from the user.
+        navigator.id.request({
+            siteName: "[% LibraryName | html %]",
+            returnTo: '/cgi-bin/koha/opac-user.pl',
+            oncancel: function() { alert('user refuses to share identity.'); }
+            });
+        };
+}
+
+</script>
+[% END %]
+
+
 </body>
 </html>
diff --git a/koha-tmpl/opac-tmpl/prog/en/js/browserid_include.js b/koha-tmpl/opac-tmpl/prog/en/js/browserid_include.js
new file mode 100644 (file)
index 0000000..5d65a03
--- /dev/null
@@ -0,0 +1 @@
+(function(){var a=function(){function e(a){return Array.isArray?Array.isArray(a):a.constructor.toString().indexOf("Array")!=-1}function d(a,c,d){var e=b[c][d];for(var f=0;f<e.length;f++)e[f].win===a&&e.splice(f,1);b[c][d].length===0&&delete b[c][d]}function c(a,c,d,e){function f(b){for(var c=0;c<b.length;c++)if(b[c].win===a)return!0;return!1}var g=!1;if(c==="*")for(var h in b){if(!b.hasOwnProperty(h))continue;if(h==="*")continue;if(typeof b[h][d]=="object"){g=f(b[h][d]);if(g)break}}else b["*"]&&b["*"][d]&&(g=f(b["*"][d])),!g&&b[c]&&b[c][d]&&(g=f(b[c][d]));if(g)throw"A channel is already bound to the same window which overlaps with origin '"+c+"' and has scope '"+d+"'";typeof b[c]!="object"&&(b[c]={}),typeof b[c][d]!="object"&&(b[c][d]=[]),b[c][d].push({win:a,handler:e})}"use strict";var a=Math.floor(Math.random()*1000001),b={},f={},g=function(a){try{var c=JSON.parse(a.data);if(typeof c!="object"||c===null)throw"malformed"}catch(a){return}var d=a.source,e=a.origin,g,h,i;if(typeof c.method=="string"){var j=c.method.split("::");j.length==2?(g=j[0],i=j[1]):i=c.method}typeof c.id!="undefined"&&(h=c.id);if(typeof i=="string"){var k=!1;if(b[e]&&b[e][g])for(var h=0;h<b[e][g].length;h++)if(b[e][g][h].win===d){b[e][g][h].handler(e,i,c),k=!0;break}if(!k&&b["*"]&&b["*"][g])for(var h=0;h<b["*"][g].length;h++)if(b["*"][g][h].win===d){b["*"][g][h].handler(e,i,c);break}}else typeof h!="undefined"&&f[h]&&f[h](e,i,c)};window.addEventListener?window.addEventListener("message",g,!1):window.attachEvent&&window.attachEvent("onmessage",g);return{build:function(b){var g=function(a){if(b.debugOutput&&window.console&&window.console.log){try{typeof a!="string"&&(a=JSON.stringify(a))}catch(c){}console.log("["+j+"] "+a)}};if(!window.postMessage)throw"jschannel cannot run this browser, no postMessage";if(!window.JSON||!window.JSON.stringify||!window.JSON.parse)throw"jschannel cannot run this browser, no JSON parsing/serialization";if(typeof b!="object")throw"Channel build invoked without a proper object argument";if(!b.window||!b.window.postMessage)throw"Channel.build() called without a valid window argument";if(window===b.window)throw"target window is same as present window -- not allowed";var h=!1;if(typeof b.origin=="string"){var i;b.origin==="*"?h=!0:null!==(i=b.origin.match(/^https?:\/\/(?:[-a-zA-Z0-9_\.])+(?::\d+)?/))&&(b.origin=i[0].toLowerCase(),h=!0)}if(!h)throw"Channel.build() called with an invalid origin";if(typeof b.scope!="undefined"){if(typeof b.scope!="string")throw"scope, when specified, must be a string";if(b.scope.split("::").length>1)throw"scope may not contain double colons: '::'"}var j=function(){var a="",b="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";for(var c=0;c<5;c++)a+=b.charAt(Math.floor(Math.random()*b.length));return a}(),k={},l={},m={},n=!1,o=[],p=function(a,b,c){var d=!1,e=!1;return{origin:b,invoke:function(b,d){if(!m[a])throw"attempting to invoke a callback of a nonexistent transaction: "+a;var e=!1;for(var f=0;f<c.length;f++)if(b===c[f]){e=!0;break}if(!e)throw"request supports no such callback '"+b+"'";t({id:a,callback:b,params:d})},error:function(b,c){e=!0;if(!m[a])throw"error called for nonexistent message: "+a;delete m[a],t({id:a,error:b,message:c})},complete:function(b){e=!0;if(!m[a])throw"complete called for nonexistent message: "+a;delete m[a],t({id:a,result:b})},delayReturn:function(a){typeof a=="boolean"&&(d=a===!0);return d},completed:function(){return e}}},q=function(a,b,c){return window.setTimeout(function(){if(l[a]){var d="timeout ("+b+"ms) exceeded on method '"+c+"'";(1,l[a].error)("timeout_error",d),delete l[a],delete f[a]}},b)},r=function(a,c,d){if(typeof b.gotMessageObserver=="function")try{b.gotMessageObserver(a,d)}catch(h){g("gotMessageObserver() raised an exception: "+h.toString())}if(d.id&&c){if(k[c]){var i=p(d.id,a,d.callbacks?d.callbacks:[]);m[d.id]={};try{if(d.callbacks&&e(d.callbacks)&&d.callbacks.length>0)for(var j=0;j<d.callbacks.length;j++){var n=d.callbacks[j],o=d.params,q=n.split("/");for(var r=0;r<q.length-1;r++){var s=q[r];typeof o[s]!="object"&&(o[s]={}),o=o[s]}o[q[q.length-1]]=function(){var a=n;return function(b){return i.invoke(a,b)}}()}var t=k[c](i,d.params);!i.delayReturn()&&!i.completed()&&i.complete(t)}catch(h){var u="runtime_error",v=null;typeof h=="string"?v=h:typeof h=="object"&&(h&&e(h)&&h.length==2?(u=h[0],v=h[1]):typeof h.error=="string"&&(u=h.error,h.message?typeof h.message=="string"?v=h.message:h=h.message:v=""));if(v===null)try{v=JSON.stringify(h),typeof v=="undefined"&&(v=h.toString())}catch(w){v=h.toString()}i.error(u,v)}}}else d.id&&d.callback?!l[d.id]||!l[d.id].callbacks||!l[d.id].callbacks[d.callback]?g("ignoring invalid callback, id:"+d.id+" ("+d.callback+")"):l[d.id].callbacks[d.callback](d.params):d.id?l[d.id]?(d.error?(1,l[d.id].error)(d.error,d.message):d.result!==undefined?(1,l[d.id].success)(d.result):(1,l[d.id].success)(),delete l[d.id],delete f[d.id]):g("ignoring invalid response: "+d.id):c&&k[c]&&k[c](null,d.params)};c(b.window,b.origin,typeof b.scope=="string"?b.scope:"",r);var s=function(a){typeof b.scope=="string"&&b.scope.length&&(a=[b.scope,a].join("::"));return a},t=function(a,c){if(!a)throw"postMessage called with null message";var d=n?"post  ":"queue ";g(d+" message: "+JSON.stringify(a));if(!c&&!n)o.push(a);else{if(typeof b.postMessageObserver=="function")try{b.postMessageObserver(b.origin,a)}catch(e){g("postMessageObserver() raised an exception: "+e.toString())}b.window.postMessage(JSON.stringify(a),b.origin)}},u=function(a,c){g("ready msg received");if(n)throw"received ready message while in ready state.  help!";c==="ping"?j+="-R":j+="-L",v.unbind("__ready"),n=!0,g("ready msg accepted."),c==="ping"&&v.notify({method:"__ready",params:"pong"});while(o.length)t(o.pop());typeof b.onReady=="function"&&b.onReady(v)},v={unbind:function(a){if(k[a]){if(delete k[a])return!0;throw"can't delete method: "+a}return!1},bind:function(a,b){if(!a||typeof a!="string")throw"'method' argument to bind must be string";if(!b||typeof b!="function")throw"callback missing from bind params";if(k[a])throw"method '"+a+"' is already bound!";k[a]=b;return this},call:function(b){if(!b)throw"missing arguments to call function";if(!b.method||typeof b.method!="string")throw"'method' argument to call must be string";if(!b.success||typeof b.success!="function")throw"'success' callback missing from call";var c={},d=[],e=function(a,b){if(typeof b=="object")for(var f in b){if(!b.hasOwnProperty(f))continue;var g=a+(a.length?"/":"")+f;typeof b[f]=="function"?(c[g]=b[f],d.push(g),delete b[f]):typeof b[f]=="object"&&e(g,b[f])}};e("",b.params);var g={id:a,method:s(b.method),params:b.params};d.length&&(g.callbacks=d),b.timeout&&q(a,b.timeout,s(b.method)),l[a]={callbacks:c,error:b.error,success:b.success},f[a]=r,a++,t(g)},notify:function(a){if(!a)throw"missing arguments to notify function";if(!a.method||typeof a.method!="string")throw"'method' argument to notify must be string";t({method:s(a.method),params:a.params})},destroy:function(){d(b.window,b.origin,typeof b.scope=="string"?b.scope:""),window.removeEventListener?window.removeEventListener("message",r,!1):window.detachEvent&&window.detachEvent("onmessage",r),n=!1,k={},m={},l={},b.origin=null,o=[],g("channel destroyed"),j=""}};v.bind("__ready",u),setTimeout(function(){},0);return v}}}();WinChan=function(){function j(){var b=window.location,c=window.opener.frames,d=b.protocol+"//"+b.host;for(i=c.length-1;i>=0;i++)try{if(c[i].location.href.indexOf(d)===0&&c[i].name===a)return c[i]}catch(e){}return}function h(a){/^https?:\/\//.test(a)||(a=window.location.href);var b=/^(https?:\/\/[-_a-zA-Z\.0-9:]+)/.exec(a);return b?b[1]:a}function g(){return window.JSON&&window.JSON.stringify&&window.JSON.parse&&window.postMessage}function f(){try{return d.indexOf("Fennec/")!=-1||d.indexOf("Firefox/")!=-1&&d.indexOf("Android")!=-1}catch(a){}return!1}function e(){var a=-1;if(navigator.appName=="Microsoft Internet Explorer"){var b=navigator.userAgent,c=new RegExp("MSIE ([0-9]{1,}[.0-9]{0,})");c.exec(b)!=null&&(a=parseFloat(RegExp.$1))}return a>=8}function c(a,b,c){a.detachEvent?a.detachEvent("on"+b,c):a.removeEventListener&&a.removeEventListener(b,c,!1)}function b(a,b,c){a.attachEvent?a.attachEvent("on"+b,c):a.addEventListener&&a.addEventListener(b,c,!1)}var a="__winchan_relay_frame",k=e();return g()?{open:function(d,e){function p(a){try{var b=JSON.parse(a.data);b.a==="ready"?l.postMessage(n,j):b.a==="error"?e&&(e(b.d),e=null):b.a==="response"&&(c(window,"message",p),c(window,"unload",o),o(),e&&(e(null,b.d),e=null))}catch(a){}}function o(){i&&document.body.removeChild(i),i=undefined,m&&m.close(),m=undefined}if(!e)throw"missing required callback argument";var g;d.url||(g="missing required 'url' parameter"),d.relay_url||(g="missing required 'relay_url' parameter"),g&&setTimeout(function(){e(g)},0);if(!d.window_features||f())d.window_features=undefined;var i,j=h(d.url);if(j!==h(d.relay_url))return setTimeout(function(){e("invalid arguments: origin of url and relay_url must match")},0);var l;k&&(i=document.createElement("iframe"),i.setAttribute("src",d.relay_url),i.style.display="none",i.setAttribute("name",a),document.body.appendChild(i),l=i.contentWindow);var m=window.open(d.url,null,d.window_features);l||(l=m);var n=JSON.stringify({a:"request",d:d.params});b(window,"unload",o),b(window,"message",p);return{close:o,focus:function(){if(m)try{m.focus()}catch(a){}}}}}:{open:function(a,b,c,d){setTimeout(function(){d("unsupported browser")},0)}}}();var b=function(){function l(){return c}function k(){c=g()||h()||i()||j();return!c}function j(){if(!(window.JSON&&window.JSON.stringify&&window.JSON.parse))return"JSON_NOT_SUPPORTED"}function i(){if(!a.postMessage)return"POSTMESSAGE_NOT_SUPPORTED"}function h(){try{var b="localStorage"in a&&a.localStorage!==null;if(b)a.localStorage.setItem("test","true"),a.localStorage.removeItem("test");else return"LOCALSTORAGE_NOT_SUPPORTED"}catch(c){return"LOCALSTORAGE_DISABLED"}}function g(){return f()}function f(){var a=e(),b=a>-1&&a<8;if(b)return"BAD_IE_VERSION"}function e(){var a=-1;if(b.appName=="Microsoft Internet Explorer"){var c=b.userAgent,d=new RegExp("MSIE ([0-9]{1,}[.0-9]{0,})");d.exec(c)!=null&&(a=parseFloat(RegExp.$1))}return a}function d(c,d){b=c,a=d}var a=window,b=navigator,c;return{setTestEnv:d,isSupported:k,getNoSupportReason:l}}();navigator.id||(navigator.id={});if(!navigator.id.request||navigator.id._shimmed){var c="https://browserid.org",d=navigator.userAgent,e=d.indexOf("Fennec/")!=-1||d.indexOf("Firefox/")!=-1&&d.indexOf("Android")!=-1,f=e?undefined:"menubar=0,location=1,resizable=1,scrollbars=1,status=0,dialog=1,width=700,height=375",g,h={login:null,logout:null,ready:null},j=undefined;function k(a){a!==!0;if(j===undefined)j=a;else if(j!=a)throw"you cannot combine the navigator.id.watch() API with navigator.id.getVerifiedEmail() or navigator.id.get()this site should instead use navigator.id.request() and navigator.id.watch()"}var l,m=b.isSupported();function n(){if(!!m)try{if(!l){var b=window.document,d=b.createElement("iframe");d.style.display="none",b.body.appendChild(d),d.src=c+"/communication_iframe",l=a.build({window:d.contentWindow,origin:c,scope:"mozid_ni",onReady:function(){l.call({method:"loaded",success:function(){h.ready&&h.ready()},error:function(){}})}}),l.bind("logout",function(a,b){h.logout&&h.logout()}),l.bind("login",function(a,b){h.login&&h.login(b)})}}catch(e){l=undefined}}function o(a){if(typeof a=="object"){if(a.onlogin&&typeof a.onlogin!="function"||a.onlogout&&typeof a.onlogout!="function"||a.onready&&typeof a.onready!="function")throw"non-function where function expected in parameters to navigator.id.watch()";if(!a.onlogin)throw"'onlogin' is a required argument to navigator.id.watch()";if(!a.onlogout)throw"'onlogout' is a required argument to navigator.id.watch()";h.login=a.onlogin||null,h.logout=a.onlogout||null,h.ready=a.onready||null,n(),typeof a.loggedInEmail!="undefined"&&l&&l.notify({method:"loggedInUser",params:a.loggedInEmail})}}function p(a){if(g)try{g.focus()}catch(d){}else{if(!b.isSupported()){var e=b.getNoSupportReason(),i="unsupported_dialog";e==="LOCALSTORAGE_DISABLED"&&(i="cookies_disabled"),g=window.open(c+"/"+i,null,f);return}l&&l.notify({method:"dialog_running"}),g=WinChan.open({url:c+"/sign_in",relay_url:c+"/relay",window_features:f,params:{method:"get",params:a}},function(b,c){l&&(!b&&c&&c.email&&l.notify({method:"loggedInUser",params:c.email}),l.notify({method:"dialog_complete"})),g=undefined;if(!b&&c&&c.assertion)try{h.login&&h.login(c.assertion)}catch(d){}if(b==="client closed window"||!c)a&&a.oncancel&&a.oncancel(),delete a.oncancel})}}navigator.id={request:function(a){a=a||{},k(!1);return p(a)},watch:function(a){k(!1),o(a)},logout:function(a){n(),l&&l.notify({method:"logout"}),typeof a=="function"&&setTimeout(a,0)},get:function(a,b){b=b||{},k(!0),o({onlogin:function(b){a&&(a(b),a=null)},onlogout:function(){}}),b.oncancel=function(){a&&(a(null),a=null),h.login=h.logout=h.ready=null},b&&b.silent?a&&setTimeout(function(){a(null)},0):p(b)},getVerifiedEmail:function(a){k(!0),navigator.id.get(a)},_shimmed:!0}}})()
\ No newline at end of file
index b5fca46..9d20db3 100644 (file)
@@ -99,7 +99,13 @@ please choose against which one you would like to authenticate: </p>
           <a href="mailto:[% admin %]">email the Koha Administrator</a>.</li>
   <li>Use top menu bar to navigate to another part of Koha.</li>
 </ul>
-[% END %]</div>
+[% END %]
+
+[% IF persona %]
+<a href="#" class="persona-button" id="browserid" ><span>Sign in with your Email</span></a>
+[% END %]
+
+</div>
 </div>
 </div>
 </div>
index 61235e6..045313d 100644 (file)
@@ -63,6 +63,9 @@
        </form>
        </div>
     [% END %]
+    [% IF persona %]
+     <a href="#" class="persona-button" id="browserid" ><span>Sign in with your Email</span></a>
+    [% END %]
     [% END %]
     [% END %]
     [% IF ( OpacNavRight ) %]<div id="opacrightsidebar" class="container">[% OpacNavRight %]</div>[% END %]
diff --git a/koha-tmpl/opac-tmpl/prog/images/sign_in_green.png b/koha-tmpl/opac-tmpl/prog/images/sign_in_green.png
new file mode 100644 (file)
index 0000000..7e84129
Binary files /dev/null and b/koha-tmpl/opac-tmpl/prog/images/sign_in_green.png differ
diff --git a/opac/svc/login b/opac/svc/login
new file mode 100755 (executable)
index 0000000..607b566
--- /dev/null
@@ -0,0 +1,52 @@
+#!/usr/bin/perl
+
+# Copyright chris@bigballofwax.co.nz 2013
+#
+# 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 3 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 CGI;
+use strict;
+use warnings;
+use C4::Auth;
+use C4::Context;
+
+use LWP::UserAgent;
+use HTTP::Request::Common qw{ POST };
+use JSON qw( decode_json );
+
+my $url  = 'https://verifier.login.persona.org/verify';
+
+my $query = CGI->new();
+
+my $host = C4::Context->preference('OPACBaseURL');
+
+my $assertion = $query->param('assertion');
+
+my $ua = LWP::UserAgent->new();
+my $response =
+  $ua->post( $url, [ 'assertion' => $assertion, 'audience' => $host ] );
+
+if ( $response->is_success ) {
+    my $content      = $response->decoded_content();
+    my $decoded_json = decode_json($content);
+    my ( $userid, $cookie, $sessionID ) =
+      checkauth( $query, 1,  { borrow => 1 }, 'opac', $decoded_json->{'email'} );
+    print $query->header( -cookie => $cookie );
+    print $decoded_json;
+}
+else {
+    warn $response->status_line, "\n";
+}