From 7c41d82be34aa9036c6d8681149e800b27ca230a Mon Sep 17 00:00:00 2001
From: Julian Maurice <julian.maurice@biblibre.com>
Date: Wed, 6 Nov 2013 16:04:42 +0100
Subject: [PATCH] Bug 8995: Show OpenURL links in OPAC search results

This patch use GetCOinS sub to provide an OpenURL link in OPAC search
results. It uses 4 new system preferences:
  - OpenURLinOPACResults: enable or disable this feature
  - OpenURLResolverURL: url of the openURL resolver
  - OpenURLText: text of the link
  - OpenURLImageLocation: image of the link

Link is displayed as an image if OpenURLImageLocation is defined, and as
text otherwise.
It works both with and without XSLT enabled.

Changes made to GetCOinSBiblio:

For 'journal':
- Title should be in rft.jtitle instead of rft.title
- rft.date, rft.aulast, rft.aufirst, rft.au, rft.pub and rft.pages have
  no meaning for a subscription, so they are simply removed from URL

This patch refactors GetCOinSBiblio, so the construction of URL is done
only at the end. This way we do not have ugly
  $var .= "&amp;$value"
in the function body.

Also use URI::Escape instead of custom regexps.

This development consider the value of syspref OPACURLOpenInNewWindow
when building the link.

Test plan:
1/ Enable syspref OPACShowOpenURL and put your OpenURL resolver URL in
OpenURLResolverURL syspref (if you don't have one, just fill it with
some fake URL, you'll have to check if OpenURL links are correct)
2/ If you want, set the other sysprefs OpenURLImageLocation and
OpenURLText
3/ Fill syspref OPACOpenURLItemTypes with some (not all) of your
item types.
4/ Empty sysprefs OPACXSLTDetailsDisplay and OPACXSLTResultsDisplay
5/ Go to OPAC and launch a search.
6/ Check you have in the results (near the title) the OpenURL link (only
for itemtypes that are in OPACOpenURLItemTypes).
Toggle OPACURLOpenInNewWindow syspref and check that the behaviour of
the link is correct.
7/ Go to the detail page of one of those and check you have the OpenURL
link too. (Above tags)
Toggle OPACURLOpenInNewWindow syspref and check that the behaviour of
the link is correct.
8/ Set sysprefs OPACXSLTDetailsDisplay and OPACXSLTResultsDisplay to
"default" and repeat steps 5 to 7
---
 C4/Biblio.pm                                       | 129 ++++++++++++---------
 C4/XSLT.pm                                         |  24 +++-
 installer/data/mysql/sysprefs.sql                  |   5 +
 installer/data/mysql/updatedatabase.pl             |  27 +++++
 .../prog/en/modules/admin/preferences/opac.pref    |  27 +++++
 koha-tmpl/opac-tmpl/bootstrap/css/opac-old.css     |   5 +-
 .../opac-tmpl/bootstrap/en/modules/opac-detail.tt  |  25 ++++
 .../opac-tmpl/bootstrap/en/modules/opac-results.tt |  21 ++++
 .../bootstrap/en/xslt/MARC21slim2OPACDetail.xsl    |  64 ++++++++++
 .../bootstrap/en/xslt/MARC21slim2OPACResults.xsl   |  66 ++++++++++-
 .../bootstrap/en/xslt/UNIMARCslim2OPACDetail.xsl   |  65 +++++++++++
 .../bootstrap/en/xslt/UNIMARCslim2OPACResults.xsl  |  64 ++++++++++
 koha-tmpl/opac-tmpl/bootstrap/less/opac.less       |   4 +
 opac/opac-detail.pl                                |  14 +++
 opac/opac-search.pl                                |  18 ++-
 15 files changed, 495 insertions(+), 63 deletions(-)

diff --git a/C4/Biblio.pm b/C4/Biblio.pm
index 8d62a61..edfe839 100644
--- a/C4/Biblio.pm
+++ b/C4/Biblio.pm
@@ -29,6 +29,7 @@ use MARC::File::USMARC;
 use MARC::File::XML;
 use POSIX qw(strftime);
 use Module::Load::Conditional qw(can_load);
+use URI::Escape; # GetCOinSBiblio
 
 use C4::Koha;
 use C4::Dates qw/format_date/;
@@ -1317,45 +1318,44 @@ sub GetCOinSBiblio {
         carp 'GetCOinSBiblio called with undefined record';
         return;
     }
+
     my $pos7 = substr $record->leader(), 7, 1;
     my $pos6 = substr $record->leader(), 6, 1;
     my $mtx;
     my $genre;
     my ( $aulast, $aufirst ) = ( '', '' );
-    my $oauthors  = '';
-    my $title     = '';
-    my $subtitle  = '';
+    my @authors;
+    my $title;
+    my $hosttitle;
     my $pubyear   = '';
     my $isbn      = '';
     my $issn      = '';
     my $publisher = '';
     my $pages     = '';
-    my $titletype = 'b';
+    my $titletype = '';
 
     # For the purposes of generating COinS metadata, LDR/06-07 can be
     # considered the same for UNIMARC and MARC21
-    my $fmts6;
-    my $fmts7;
-    %$fmts6 = (
-                'a' => 'book',
-                'b' => 'manuscript',
-                'c' => 'book',
-                'd' => 'manuscript',
-                'e' => 'map',
-                'f' => 'map',
-                'g' => 'film',
-                'i' => 'audioRecording',
-                'j' => 'audioRecording',
-                'k' => 'artwork',
-                'l' => 'document',
-                'm' => 'computerProgram',
-                'o' => 'document',
-                'r' => 'document',
-            );
-    %$fmts7 = (
-                    'a' => 'journalArticle',
-                    's' => 'journal',
-              );
+    my $fmts6 = {
+        'a' => 'book',
+        'b' => 'manuscript',
+        'c' => 'book',
+        'd' => 'manuscript',
+        'e' => 'map',
+        'f' => 'map',
+        'g' => 'film',
+        'i' => 'audioRecording',
+        'j' => 'audioRecording',
+        'k' => 'artwork',
+        'l' => 'document',
+        'm' => 'computerProgram',
+        'o' => 'document',
+        'r' => 'document',
+    };
+    my $fmts7 = {
+        'a' => 'journalArticle',
+        's' => 'journal',
+    };
 
     $genre = $fmts6->{$pos6} ? $fmts6->{$pos6} : 'book';
 
@@ -1366,6 +1366,7 @@ sub GetCOinSBiblio {
     ##### We must transform mtx to a valable mtx and document type ####
     if ( $genre eq 'book' ) {
             $mtx = 'book';
+            $titletype = 'b';
     } elsif ( $genre eq 'journal' ) {
             $mtx = 'journal';
             $titletype = 'j';
@@ -1377,26 +1378,25 @@ sub GetCOinSBiblio {
             $mtx = 'dc';
     }
 
-    $genre = ( $mtx eq 'dc' ) ? "&amp;rft.type=$genre" : "&amp;rft.genre=$genre";
-
     if ( C4::Context->preference("marcflavour") eq "UNIMARC" ) {
 
         # Setting datas
         $aulast  = $record->subfield( '700', 'a' ) || '';
         $aufirst = $record->subfield( '700', 'b' ) || '';
-        $oauthors = "&amp;rft.au=$aufirst $aulast";
+        push @authors, "$aufirst $aulast" if ($aufirst or $aulast);
 
         # others authors
         if ( $record->field('200') ) {
             for my $au ( $record->field('200')->subfield('g') ) {
-                $oauthors .= "&amp;rft.au=$au";
+                push @authors, $au;
             }
         }
-        $title =
-          ( $mtx eq 'dc' )
-          ? "&amp;rft.title=" . $record->subfield( '200', 'a' )
-          : "&amp;rft.title=" . $record->subfield( '200', 'a' ) . "&amp;rft.btitle=" . $record->subfield( '200', 'a' );
-        $pubyear   = $record->subfield( '210', 'd' ) || '';
+
+        $title     = $record->subfield( '200', 'a' );
+        my $subfield_210d = $record->subfield('210', 'd');
+        if ($subfield_210d and $subfield_210d =~ /(\d{4})/) {
+            $pubyear = $1;
+        }
         $publisher = $record->subfield( '210', 'c' ) || '';
         $isbn      = $record->subfield( '010', 'a' ) || '';
         $issn      = $record->subfield( '011', 'a' ) || '';
@@ -1406,34 +1406,24 @@ sub GetCOinSBiblio {
 
         # Setting datas
         if ( $record->field('100') ) {
-            $oauthors .= "&amp;rft.au=" . $record->subfield( '100', 'a' );
+            push @authors, $record->subfield( '100', 'a' );
         }
 
         # others authors
         if ( $record->field('700') ) {
             for my $au ( $record->field('700')->subfield('a') ) {
-                $oauthors .= "&amp;rft.au=$au";
+                push @authors, $au;
             }
         }
-        $title = "&amp;rft." . $titletype . "title=" . $record->subfield( '245', 'a' );
-        $subtitle = $record->subfield( '245', 'b' ) || '';
-        $title .= $subtitle;
+        $title = $record->subfield( '245', 'a' ) . $record->subfield( '245', 'b' );
         if ($titletype eq 'a') {
             $pubyear   = $record->field('008') || '';
             $pubyear   = substr($pubyear->data(), 7, 4) if $pubyear;
             $isbn      = $record->subfield( '773', 'z' ) || '';
             $issn      = $record->subfield( '773', 'x' ) || '';
-            if ($mtx eq 'journal') {
-                $title    .= "&amp;rft.title=" . (($record->subfield( '773', 't' ) || $record->subfield( '773', 'a')));
-            } else {
-                $title    .= "&amp;rft.btitle=" . (($record->subfield( '773', 't' ) || $record->subfield( '773', 'a')) || '');
-            }
-            foreach my $rel ($record->subfield( '773', 'g' )) {
-                if ($pages) {
-                    $pages .= ', ';
-                }
-                $pages .= $rel;
-            }
+            $hosttitle = $record->subfield( '773', 't' ) || $record->subfield( '773', 'a');
+            my @rels = $record->subfield( '773', 'g' );
+            $pages = join(', ', @rels);
         } else {
             $pubyear   = $record->subfield( '260', 'c' ) || '';
             $publisher = $record->subfield( '260', 'b' ) || '';
@@ -1442,12 +1432,39 @@ sub GetCOinSBiblio {
         }
 
     }
-    my $coins_value =
-"ctx_ver=Z39.88-2004&amp;rft_val_fmt=info%3Aofi%2Ffmt%3Akev%3Amtx%3A$mtx$genre$title&amp;rft.isbn=$isbn&amp;rft.issn=$issn&amp;rft.aulast=$aulast&amp;rft.aufirst=$aufirst$oauthors&amp;rft.pub=$publisher&amp;rft.date=$pubyear&amp;rft.pages=$pages";
-    $coins_value =~ s/(\ |&[^a])/\+/g;
-    $coins_value =~ s/\"/\&quot\;/g;
 
-#<!-- TMPL_VAR NAME="ocoins_format" -->&amp;rft.au=<!-- TMPL_VAR NAME="author" -->&amp;rft.btitle=<!-- TMPL_VAR NAME="title" -->&amp;rft.date=<!-- TMPL_VAR NAME="publicationyear" -->&amp;rft.pages=<!-- TMPL_VAR NAME="pages" -->&amp;rft.isbn=<!-- TMPL_VAR NAME=amazonisbn -->&amp;rft.aucorp=&amp;rft.place=<!-- TMPL_VAR NAME="place" -->&amp;rft.pub=<!-- TMPL_VAR NAME="publishercode" -->&amp;rft.edition=<!-- TMPL_VAR NAME="edition" -->&amp;rft.series=<!-- TMPL_VAR NAME="series" -->&amp;rft.genre="
+    my @params = (
+        [ 'ctx_ver', 'Z39.88-2004' ],
+        [ 'rft_val_fmt', "info:ofi/fmt:kev:mtx:$mtx" ],
+        [ ($mtx eq 'dc' ? 'rft.type' : 'rft.genre'), $genre ],
+        [ "rft.${titletype}title", $title ],
+    );
+
+    # rft.title is authorized only once, so by checking $titletype
+    # we ensure that rft.title is not already in the list.
+    if ($hosttitle and $titletype) {
+        push @params, [ 'rft.title', $hosttitle ];
+    }
+
+    push @params, (
+        [ 'rft.isbn', $isbn ],
+        [ 'rft.issn', $issn ],
+    );
+
+    # If it's a subscription, these informations have no meaning.
+    if ($genre ne 'journal') {
+        push @params, (
+            [ 'rft.aulast', $aulast ],
+            [ 'rft.aufirst', $aufirst ],
+            (map { [ 'rft.au', $_ ] } @authors),
+            [ 'rft.pub', $publisher ],
+            [ 'rft.date', $pubyear ],
+            [ 'rft.pages', $pages ],
+        );
+    }
+
+    my $coins_value = join( '&amp;',
+        map { $$_[1] ? $$_[0] . '=' . uri_escape_utf8( $$_[1] ) : () } @params );
 
     return $coins_value;
 }
diff --git a/C4/XSLT.pm b/C4/XSLT.pm
index a255b33..17c94f8 100644
--- a/C4/XSLT.pm
+++ b/C4/XSLT.pm
@@ -157,7 +157,7 @@ sub _get_best_default_xslt_filename {
 }
 
 sub XSLTParse4Display {
-    my ( $biblionumber, $orig_record, $xslsyspref, $fixamps, $hidden_items ) = @_;
+    my ( $biblionumber, $orig_record, $xslsyspref, $fixamps, $hidden_items, $variables ) = @_;
     my $xslfilename = C4::Context->preference($xslsyspref);
     if ( $xslfilename =~ /^\s*"?default"?\s*$/i ) {
         my $htdocs;
@@ -206,15 +206,31 @@ sub XSLTParse4Display {
                               UseControlNumber IntranetBiblioDefaultView BiblioDefaultView
                               singleBranchMode OPACItemLocation DisplayIconsXSLT
                               AlternateHoldingsField AlternateHoldingsSeparator
-                              TrackClicks opacthemes / )
+                              TrackClicks opacthemes
+                              OPACShowOpenURL OpenURLResolverURL
+                              OpenURLImageLocation OpenURLText / )
     {
         my $sp = C4::Context->preference( $syspref );
         next unless defined($sp);
         $sysxml .= "<syspref name=\"$syspref\">$sp</syspref>\n";
     }
     $sysxml .= "</sysprefs>\n";
-    $xmlrecord =~ s/\<\/record\>/$itemsxml$sysxml\<\/record\>/;
-    if ($fixamps) { # We need to correct the HTML entities that Zebra outputs
+
+    $variables ||= {};
+    if (C4::Context->preference('OPACShowOpenURL')) {
+        my ($biblio) = GetBiblioItemByBiblioNumber($biblionumber);
+        my @itypes = split( /\s/, C4::Context->preference('OPACOpenURLItemTypes') );
+        if (grep /^$biblio->{itemtype}$/, @itypes) {
+            $variables->{COinS} = C4::Biblio::GetCOinSBiblio($orig_record);
+        }
+    }
+    my $varxml = "<variables>\n";
+    while (my ($key, $value) = each %$variables) {
+        $varxml .= "<variable name=\"$key\">$value</variable>\n";
+    }
+    $varxml .= "</variables>\n";
+    $xmlrecord =~ s/\<\/record\>/$itemsxml$sysxml$varxml\<\/record\>/;
+    if ($fixamps) { # We need to correct the ampersand entities that Zebra outputs
         $xmlrecord =~ s/\&amp;amp;/\&amp;/g;
         $xmlrecord =~ s/\&amp\;lt\;/\&lt\;/g;
         $xmlrecord =~ s/\&amp\;gt\;/\&gt\;/g;
diff --git a/installer/data/mysql/sysprefs.sql b/installer/data/mysql/sysprefs.sql
index 89707ef..45d41c5 100644
--- a/installer/data/mysql/sysprefs.sql
+++ b/installer/data/mysql/sysprefs.sql
@@ -473,4 +473,9 @@ INSERT INTO systempreferences ( `variable`, `value`, `options`, `explanation`, `
 ('XSLTResultsDisplay','default','','Enable XSL stylesheet control over results page display on intranet','Free'),
 ('z3950AuthorAuthFields','701,702,700',NULL,'Define the MARC biblio fields for Personal Name Authorities to fill biblio.author','free'),
 ('z3950NormalizeAuthor','0','','If ON, Personal Name Authorities will replace authors in biblio.author','YesNo')
+('OpenURLResolverURL', '', NULL, 'URL of OpenURL Resolver', 'Free')
+('OpenURLText', '', NULL, 'Text of OpenURL links (or image title if OpenURLImageLocation is defined)', 'Free')
+('OpenURLImageLocation', '', NULL, 'Location of image for OpenURL links', 'Free')
+('OPACShowOpenURL', '', NULL, 'Enable display of OpenURL links in OPAC search results and detail page', 'YesNo')
+('OPACOpenURLItemTypes', '', NULL, 'Show the OpenURL link only for these item types', 'Free')
 ;
diff --git a/installer/data/mysql/updatedatabase.pl b/installer/data/mysql/updatedatabase.pl
index 7e00df3..42a14c4 100755
--- a/installer/data/mysql/updatedatabase.pl
+++ b/installer/data/mysql/updatedatabase.pl
@@ -9732,6 +9732,33 @@ if ( CheckVersion($DBversion) ) {
     SetVersion($DBversion);
 }
 
+$DBversion = "3.17.00.XXX";
+if ( C4::Context->preference("Version") < TransformToNum($DBversion) ) {
+    $dbh->do("
+        INSERT INTO systempreferences (variable,value,explanation,options,type)
+        VALUES('OpenURLResolverURL', '', 'URL of OpenURL Resolver', NULL, 'Free')
+    ");
+    $dbh->do("
+        INSERT INTO systempreferences (variable,value,explanation,options,type)
+        VALUES('OpenURLText', '', 'Text of OpenURL links (or image title if OpenURLImageLocation is defined)', NULL, 'Free');
+    ");
+    $dbh->do("
+        INSERT INTO systempreferences (variable,value,explanation,options,type)
+        VALUES('OpenURLImageLocation', '', 'Location of image for OpenURL links', NULL, 'Free');
+    ");
+    $dbh->do("
+        INSERT INTO systempreferences (variable,value,explanation,options,type)
+        VALUES('OPACShowOpenURL', '', 'Enable display of OpenURL links in OPAC search results and detail page', NULL, 'YesNo');
+    ");
+    $dbh->do("
+        INSERT INTO systempreferences (variable,value,explanation,options,type)
+        VALUES('OPACOpenURLItemTypes', '', 'Show the OpenURL link only for these item types', NULL, 'Free');
+    ");
+    print "Upgrade to $DBversion done (Bug 8995 - Add sysprefs for OpenURL)\n";
+    SetVersion($DBversion);
+}
+
+
 =head1 FUNCTIONS
 
 =head2 TableExists($table)
diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/opac.pref b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/opac.pref
index f0c0951..219abf8 100644
--- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/opac.pref
+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/opac.pref
@@ -316,6 +316,33 @@ OPAC:
             - pref: OpacCustomSearch
               type: textarea
               class: code
+        -
+            - 'Complete URL of OpenURL resolver (starting with <tt>http://</tt> or <tt>https://</tt>):'
+            - pref: OpenURLResolverURL
+              class: url
+        -
+            - 'Text of OpenURL links (or image title if OpenURLImageLocation is defined):'
+            - pref: OpenURLText
+        -
+            - 'Location of image for OpenURL links:'
+            - pref: OpenURLImageLocation
+              class: url
+            - '<br />Can be a complete URL starting with <tt>http://</tt> or'
+            - '<tt>https://</tt> or the name of a file in <tt>images</tt> directory'
+            - '<br />Examples:'
+            - '<br />- <tt>http://www.example.com/img/openurl.png</tt>'
+            - '<br />- <tt>OpenURL/OpenURL.png</tt> (file is in <tt>opac-tmpl/&lt;opacthemes&gt;/images/OpenURL/OpenURL.png</tt>)'
+        -
+            - pref: OPACShowOpenURL
+              choices:
+                  yes: Enable
+                  no: Disable
+            - 'display of OpenURL link in OPAC search results and detail page.'
+        -
+            - 'List of item type codes (separated by spaces) for those you want to show the OpenURL link:'
+            - pref: OPACOpenURLItemTypes
+            - '<br />'
+            - 'It uses biblioitems.itemtype field, so if you map a MARC field to it, and link this MARC field to a list of authorised values (for example CCODE), you can use these values for system preference value.'
     Features:
         -
             - pref: opacuserlogin
diff --git a/koha-tmpl/opac-tmpl/bootstrap/css/opac-old.css b/koha-tmpl/opac-tmpl/bootstrap/css/opac-old.css
index 3672ed2..870b13c 100644
--- a/koha-tmpl/opac-tmpl/bootstrap/css/opac-old.css
+++ b/koha-tmpl/opac-tmpl/bootstrap/css/opac-old.css
@@ -1,6 +1,5 @@
 @import url("/opac-tmpl/lib/yui/reset-fonts-grids.css");
 @import url("/opac-tmpl/lib/yui/skin.css");
-
 a {
 	font-weight : bold;
 }
@@ -2860,3 +2859,7 @@ padding: 0.1em 0;
     padding: 3px;
     text-align: center;
 }
+
+a.OpenURL img {
+    vertical-align: middle;
+}
diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-detail.tt b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-detail.tt
index 71b022b..2e548b4 100644
--- a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-detail.tt
+++ b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-detail.tt
@@ -352,6 +352,31 @@
                                 </ul>
                             </div>
                         [% END # / IF MARCURLS %]
+
+                        [% IF (OPACShowOpenURL && OpenURLResolverURL && ocoins) %]
+                            [%# Build OpenURL image location %]
+                            [% IF (OpenURLImageLocation && !OpenURLImageLocation.match('^https?://')) %]
+                                [% openurlimagelocation = "$interface/$theme/images/$OpenURLImageLocation" %]
+                            [% ELSE %]
+                                [% openurlimagelocation = "$OpenURLImageLocation" %]
+                            [% END %]
+
+                            <span class="results_summary">
+                                [% OpenURLTarget = '_self' %]
+                                [% IF ( OPACURLOpenInNewWindow ) %]
+                                    [% OpenURLTarget = '_blank' %]
+                                [% END %]
+                                <a class="OpenURL" href="[% OpenURLResolverURL %]?[% ocoins %]" title="[% OpenURLText %]" target="[% OpenURLTarget %]">
+                                [% IF openurlimagelocation %]
+                                    <img src="[% openurlimagelocation %]" />
+                                [% ELSIF OpenURLText %]
+                                    [% OpenURLText %]
+                                [% ELSE %]
+                                    OpenURL
+                                [% END %]
+                                </a>
+                            </span>
+                        [% END # / IF (OPACShowOpenURL && OpenURLResolverURL && ocoins) %]
                     [% END # / IF OPACXSLTDetailsDisplay %]
 
                     [% IF ( AuthorisedValueImages && authorised_value_images ) %]
diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-results.tt b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-results.tt
index 9a15d84..9cd4543 100644
--- a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-results.tt
+++ b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-results.tt
@@ -167,6 +167,16 @@
 
                                 <!-- TABLE RESULTS START -->
                                 <table class="table table-striped">
+                                    [%# Build OpenURL image location %]
+                                    [% IF (OpenURLImageLocation && !OpenURLImageLocation.match('^https?://')) %]
+                                        [% openurlimagelocation = "$interface/$theme/images/$OpenURLImageLocation" %]
+                                    [% ELSE %]
+                                        [% openurlimagelocation = "$OpenURLImageLocation" %]
+                                    [% END %]
+                                    [% OpenURLTarget = '_self' %]
+                                    [% IF ( OPACURLOpenInNewWindow ) %]
+                                        [% OpenURLTarget = '_blank' %]
+                                    [% END %]
 
                                     <!-- Actual Search Results -->
                                     [% FOREACH SEARCH_RESULT IN SEARCH_RESULTS %]
@@ -318,6 +328,17 @@
                                                     [% ELSE %]
                                                         &nbsp;
                                                     [% END %]
+                                                    [% IF (SEARCH_RESULT.ShowOpenURL && OpenURLResolverURL && SEARCH_RESULT.coins) %]
+                                                        <a class="OpenURL" href="[% OpenURLResolverURL %]?[% SEARCH_RESULT.coins %]" title="[% OpenURLText %]" target="[% OpenURLTarget %]">
+                                                        [% IF openurlimagelocation %]
+                                                            <img src="[% openurlimagelocation %]" />
+                                                        [% ELSIF OpenURLText %]
+                                                            [% OpenURLText %]
+                                                        [% ELSE %]
+                                                            OpenURL
+                                                        [% END %]
+                                                        </a>
+                                                    [% END %]
 
                                                     <span class="results_summary publisher">
                                                         <span class="label">Publication:</span>
diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/xslt/MARC21slim2OPACDetail.xsl b/koha-tmpl/opac-tmpl/bootstrap/en/xslt/MARC21slim2OPACDetail.xsl
index af9ac00..6899b09 100644
--- a/koha-tmpl/opac-tmpl/bootstrap/en/xslt/MARC21slim2OPACDetail.xsl
+++ b/koha-tmpl/opac-tmpl/bootstrap/en/xslt/MARC21slim2OPACDetail.xsl
@@ -1017,6 +1017,70 @@
         </xsl:for-each>
         </xsl:if>
 
+        <!-- OpenURL -->
+        <xsl:variable name="OPACShowOpenURL" select="marc:sysprefs/marc:syspref[@name='OPACShowOpenURL']" />
+        <xsl:variable name="OpenURLResolverURL" select="marc:sysprefs/marc:syspref[@name='OpenURLResolverURL']" />
+        <xsl:variable name="OpenURLImageLocation" select="marc:sysprefs/marc:syspref[@name='OpenURLImageLocation']" />
+        <xsl:variable name="OpenURLText" select="marc:sysprefs/marc:syspref[@name='OpenURLText']" />
+        <xsl:variable name="COinS" select="marc:variables/marc:variable[@name='COinS']" />
+
+        <xsl:if test="$OPACShowOpenURL = 1 and $OpenURLResolverURL != '' and $COinS != ''">
+          <xsl:variable name="openurltext">
+            <xsl:choose>
+              <xsl:when test="$OpenURLText != ''">
+                <xsl:value-of select="$OpenURLText" />
+              </xsl:when>
+              <xsl:otherwise>
+                <xsl:text>OpenURL</xsl:text>
+              </xsl:otherwise>
+            </xsl:choose>
+          </xsl:variable>
+
+          <xsl:variable name="openurlimagelocation">
+            <xsl:choose>
+              <xsl:when test="starts-with($OpenURLImageLocation, 'http://') or starts-with($OpenURLImageLocation, 'https://')">
+                <xsl:value-of select="$OpenURLImageLocation" />
+              </xsl:when>
+              <xsl:when test="$OpenURLImageLocation != ''">
+                <xsl:text>/opac-tmpl/prog/images/</xsl:text>
+                <xsl:value-of select="$OpenURLImageLocation" />
+              </xsl:when>
+            </xsl:choose>
+          </xsl:variable>
+
+          <span class="results_summary"><a>
+            <xsl:attribute name="href">
+              <xsl:value-of select="$OpenURLResolverURL" />
+              <xsl:text>?</xsl:text>
+              <xsl:value-of select="$COinS" />
+            </xsl:attribute>
+            <xsl:attribute name="title">
+              <xsl:value-of select="$openurltext" />
+            </xsl:attribute>
+            <xsl:attribute name="class">
+              <xsl:text>OpenURL</xsl:text>
+            </xsl:attribute>
+            <xsl:if test="$OPACURLOpenInNewWindow='1'">
+              <xsl:attribute name="target">
+                <xsl:text>_blank</xsl:text>
+              </xsl:attribute>
+            </xsl:if>
+            <xsl:choose>
+              <xsl:when test="$openurlimagelocation != ''">
+                <img>
+                  <xsl:attribute name="src">
+                    <xsl:value-of select="$openurlimagelocation" />
+                  </xsl:attribute>
+                </img>
+              </xsl:when>
+              <xsl:otherwise>
+                <xsl:value-of select="$openurltext" />
+              </xsl:otherwise>
+            </xsl:choose>
+          </a></span>
+        </xsl:if>
+        <!-- End of OpenURL -->
+
     </xsl:element>
     </xsl:template>
 
diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/xslt/MARC21slim2OPACResults.xsl b/koha-tmpl/opac-tmpl/bootstrap/en/xslt/MARC21slim2OPACResults.xsl
index 1609ef0..c856e7b 100644
--- a/koha-tmpl/opac-tmpl/bootstrap/en/xslt/MARC21slim2OPACResults.xsl
+++ b/koha-tmpl/opac-tmpl/bootstrap/en/xslt/MARC21slim2OPACResults.xsl
@@ -448,8 +448,72 @@
         </xsl:for-each>
         </xsl:if>
     </a>
-    <p>
 
+    <!-- OpenURL -->
+    <xsl:variable name="OPACShowOpenURL" select="marc:sysprefs/marc:syspref[@name='OPACShowOpenURL']" />
+    <xsl:variable name="OpenURLResolverURL" select="marc:sysprefs/marc:syspref[@name='OpenURLResolverURL']" />
+    <xsl:variable name="OpenURLImageLocation" select="marc:sysprefs/marc:syspref[@name='OpenURLImageLocation']" />
+    <xsl:variable name="OpenURLText" select="marc:sysprefs/marc:syspref[@name='OpenURLText']" />
+    <xsl:variable name="COinS" select="marc:variables/marc:variable[@name='COinS']" />
+
+    <xsl:if test="$OPACShowOpenURL = 1 and $OpenURLResolverURL != '' and $COinS != ''">
+      <xsl:variable name="openurltext">
+        <xsl:choose>
+          <xsl:when test="$OpenURLText != ''">
+            <xsl:value-of select="$OpenURLText" />
+          </xsl:when>
+          <xsl:otherwise>
+            <xsl:text>OpenURL</xsl:text>
+          </xsl:otherwise>
+        </xsl:choose>
+      </xsl:variable>
+
+      <xsl:variable name="openurlimagelocation">
+        <xsl:choose>
+          <xsl:when test="starts-with($OpenURLImageLocation, 'http://') or starts-with($OpenURLImageLocation, 'https://')">
+            <xsl:value-of select="$OpenURLImageLocation" />
+          </xsl:when>
+          <xsl:when test="$OpenURLImageLocation != ''">
+            <xsl:text>/opac-tmpl/prog/images/</xsl:text>
+            <xsl:value-of select="$OpenURLImageLocation" />
+          </xsl:when>
+        </xsl:choose>
+      </xsl:variable>
+
+      <span class="results_summary"><a>
+        <xsl:attribute name="href">
+          <xsl:value-of select="$OpenURLResolverURL" />
+          <xsl:text>?</xsl:text>
+          <xsl:value-of select="$COinS" />
+        </xsl:attribute>
+        <xsl:attribute name="title">
+          <xsl:value-of select="$openurltext" />
+        </xsl:attribute>
+        <xsl:attribute name="class">
+          <xsl:text>OpenURL</xsl:text>
+        </xsl:attribute>
+        <xsl:if test="$OPACURLOpenInNewWindow='1'">
+          <xsl:attribute name="target">
+            <xsl:text>_blank</xsl:text>
+          </xsl:attribute>
+        </xsl:if>
+        <xsl:choose>
+          <xsl:when test="$openurlimagelocation != ''">
+            <img>
+              <xsl:attribute name="src">
+                <xsl:value-of select="$openurlimagelocation" />
+              </xsl:attribute>
+            </img>
+          </xsl:when>
+          <xsl:otherwise>
+            <xsl:value-of select="$openurltext" />
+          </xsl:otherwise>
+        </xsl:choose>
+      </a></span>
+    </xsl:if>
+    <!-- End of OpenURL -->
+
+    <p>
     <!-- Author Statement: Alternate Graphic Representation (MARC 880) -->
     <xsl:if test="$display880">
       <xsl:call-template name="m880Select">
diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/xslt/UNIMARCslim2OPACDetail.xsl b/koha-tmpl/opac-tmpl/bootstrap/en/xslt/UNIMARCslim2OPACDetail.xsl
index 8cb8bf9..b24a459 100644
--- a/koha-tmpl/opac-tmpl/bootstrap/en/xslt/UNIMARCslim2OPACDetail.xsl
+++ b/koha-tmpl/opac-tmpl/bootstrap/en/xslt/UNIMARCslim2OPACDetail.xsl
@@ -442,6 +442,71 @@
       </xsl:for-each>
     </span>
   </xsl:if>
+
+  <!-- OpenURL -->
+  <xsl:variable name="OPACShowOpenURL" select="marc:sysprefs/marc:syspref[@name='OPACShowOpenURL']" />
+  <xsl:variable name="OpenURLResolverURL" select="marc:sysprefs/marc:syspref[@name='OpenURLResolverURL']" />
+  <xsl:variable name="OpenURLImageLocation" select="marc:sysprefs/marc:syspref[@name='OpenURLImageLocation']" />
+  <xsl:variable name="OpenURLText" select="marc:sysprefs/marc:syspref[@name='OpenURLText']" />
+  <xsl:variable name="COinS" select="marc:variables/marc:variable[@name='COinS']" />
+
+  <xsl:if test="$OPACShowOpenURL = 1 and $OpenURLResolverURL != '' and $COinS != ''">
+    <xsl:variable name="openurltext">
+      <xsl:choose>
+        <xsl:when test="$OpenURLText != ''">
+          <xsl:value-of select="$OpenURLText" />
+        </xsl:when>
+        <xsl:otherwise>
+          <xsl:text>OpenURL</xsl:text>
+        </xsl:otherwise>
+      </xsl:choose>
+    </xsl:variable>
+
+    <xsl:variable name="openurlimagelocation">
+      <xsl:choose>
+        <xsl:when test="starts-with($OpenURLImageLocation, 'http://') or starts-with($OpenURLImageLocation, 'https://')">
+          <xsl:value-of select="$OpenURLImageLocation" />
+        </xsl:when>
+        <xsl:when test="$OpenURLImageLocation != ''">
+          <xsl:text>/opac-tmpl/prog/images/</xsl:text>
+          <xsl:value-of select="$OpenURLImageLocation" />
+        </xsl:when>
+      </xsl:choose>
+    </xsl:variable>
+
+    <span class="results_summary"><a>
+      <xsl:attribute name="href">
+        <xsl:value-of select="$OpenURLResolverURL" />
+        <xsl:text>?</xsl:text>
+        <xsl:value-of select="$COinS" />
+      </xsl:attribute>
+      <xsl:attribute name="title">
+        <xsl:value-of select="$openurltext" />
+      </xsl:attribute>
+      <xsl:attribute name="class">
+        <xsl:text>OpenURL</xsl:text>
+      </xsl:attribute>
+      <xsl:if test="$OPACURLOpenInNewWindow='1'">
+        <xsl:attribute name="target">
+          <xsl:text>_blank</xsl:text>
+        </xsl:attribute>
+      </xsl:if>
+      <xsl:choose>
+        <xsl:when test="$openurlimagelocation != ''">
+          <img>
+            <xsl:attribute name="src">
+              <xsl:value-of select="$openurlimagelocation" />
+            </xsl:attribute>
+          </img>
+        </xsl:when>
+        <xsl:otherwise>
+          <xsl:value-of select="$openurltext" />
+        </xsl:otherwise>
+      </xsl:choose>
+    </a></span>
+  </xsl:if>
+  <!-- End of OpenURL -->
+
 </xsl:template>
 
     <xsl:template name="nameABCDQ">
diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/xslt/UNIMARCslim2OPACResults.xsl b/koha-tmpl/opac-tmpl/bootstrap/en/xslt/UNIMARCslim2OPACResults.xsl
index d72ad3f..ff4c2c4 100644
--- a/koha-tmpl/opac-tmpl/bootstrap/en/xslt/UNIMARCslim2OPACResults.xsl
+++ b/koha-tmpl/opac-tmpl/bootstrap/en/xslt/UNIMARCslim2OPACResults.xsl
@@ -82,6 +82,70 @@
     </xsl:for-each>
   </xsl:if>
 
+  <!-- OpenURL -->
+  <xsl:variable name="OPACShowOpenURL" select="marc:sysprefs/marc:syspref[@name='OPACShowOpenURL']" />
+  <xsl:variable name="OpenURLResolverURL" select="marc:sysprefs/marc:syspref[@name='OpenURLResolverURL']" />
+  <xsl:variable name="OpenURLImageLocation" select="marc:sysprefs/marc:syspref[@name='OpenURLImageLocation']" />
+  <xsl:variable name="OpenURLText" select="marc:sysprefs/marc:syspref[@name='OpenURLText']" />
+  <xsl:variable name="COinS" select="marc:variables/marc:variable[@name='COinS']" />
+
+  <xsl:if test="$OPACShowOpenURL = 1 and $OpenURLResolverURL != '' and $COinS != ''">
+    <xsl:variable name="openurltext">
+      <xsl:choose>
+        <xsl:when test="$OpenURLText != ''">
+          <xsl:value-of select="$OpenURLText" />
+        </xsl:when>
+        <xsl:otherwise>
+          <xsl:text>OpenURL</xsl:text>
+        </xsl:otherwise>
+      </xsl:choose>
+    </xsl:variable>
+
+    <xsl:variable name="openurlimagelocation">
+      <xsl:choose>
+        <xsl:when test="starts-with($OpenURLImageLocation, 'http://') or starts-with($OpenURLImageLocation, 'https://')">
+          <xsl:value-of select="$OpenURLImageLocation" />
+        </xsl:when>
+        <xsl:when test="$OpenURLImageLocation != ''">
+          <xsl:text>/opac-tmpl/prog/images/</xsl:text>
+          <xsl:value-of select="$OpenURLImageLocation" />
+        </xsl:when>
+      </xsl:choose>
+    </xsl:variable>
+
+    <span class="results_summary"><a>
+      <xsl:attribute name="href">
+        <xsl:value-of select="$OpenURLResolverURL" />
+        <xsl:text>?</xsl:text>
+        <xsl:value-of select="$COinS" />
+      </xsl:attribute>
+      <xsl:attribute name="title">
+        <xsl:value-of select="$openurltext" />
+      </xsl:attribute>
+      <xsl:attribute name="class">
+        <xsl:text>OpenURL</xsl:text>
+      </xsl:attribute>
+      <xsl:if test="$OPACURLOpenInNewWindow='1'">
+        <xsl:attribute name="target">
+          <xsl:text>_blank</xsl:text>
+        </xsl:attribute>
+      </xsl:if>
+      <xsl:choose>
+        <xsl:when test="$openurlimagelocation != ''">
+          <img>
+            <xsl:attribute name="src">
+              <xsl:value-of select="$openurlimagelocation" />
+            </xsl:attribute>
+          </img>
+        </xsl:when>
+        <xsl:otherwise>
+          <xsl:value-of select="$openurltext" />
+        </xsl:otherwise>
+      </xsl:choose>
+    </a></span>
+  </xsl:if>
+  <!-- End of OpenURL -->
+
   <xsl:call-template name="tag_title">
     <xsl:with-param name="tag">454</xsl:with-param>
     <xsl:with-param name="label">Translation of</xsl:with-param>
diff --git a/koha-tmpl/opac-tmpl/bootstrap/less/opac.less b/koha-tmpl/opac-tmpl/bootstrap/less/opac.less
index 5b69d30..35c6c5f 100644
--- a/koha-tmpl/opac-tmpl/bootstrap/less/opac.less
+++ b/koha-tmpl/opac-tmpl/bootstrap/less/opac.less
@@ -2464,4 +2464,8 @@ a.reviewlink:visited {
     padding-left:20px;
 }
 
+a.OpenURL img {
+    vertical-align: middle;
+}
+
 @import "responsive.less";
diff --git a/opac/opac-detail.pl b/opac/opac-detail.pl
index b8cd194..00a29f8 100755
--- a/opac/opac-detail.pl
+++ b/opac/opac-detail.pl
@@ -1133,4 +1133,18 @@ $template->param(
     'OpacLocationBranchToDisplayShelving' => C4::Context->preference('OpacLocationBranchToDisplayShelving'),
 );
 
+# OpenURL
+if (C4::Context->preference('OPACShowOpenURL')) {
+    my ($biblio) = GetBiblioItemByBiblioNumber($biblionumber);
+    my @itypes = split( /\s/, C4::Context->preference('OPACOpenURLItemTypes') );
+    if (grep /^$biblio->{itemtype}$/, @itypes) {
+        $template->param(
+            OPACShowOpenURL => 1,
+            OpenURLResolverURL => C4::Context->preference('OpenURLResolverURL'),
+            OpenURLText => C4::Context->preference('OpenURLText'),
+            OpenURLImageLocation => C4::Context->preference('OpenURLImageLocation')
+        );
+    }
+}
+
 output_html_with_http_headers $query, $cookie, $template->output;
diff --git a/opac/opac-search.pl b/opac/opac-search.pl
index b0feaeb..33b4439 100755
--- a/opac/opac-search.pl
+++ b/opac/opac-search.pl
@@ -519,6 +519,18 @@ if (C4::Context->preference('OpacSuppression')) {
     }
 }
 
+# OpenURL
+my @OpenURL_itypes;
+if (C4::Context->preference('OPACShowOpenURL')) {
+    @OpenURL_itypes = split( /\s/, C4::Context->preference('OPACOpenURLItemTypes') );
+    $template->param(
+        OPACShowOpenURL => 1,
+        OpenURLResolverURL => C4::Context->preference('OpenURLResolverURL'),
+        OpenURLText => C4::Context->preference('OpenURLText'),
+        OpenURLImageLocation => C4::Context->preference('OpenURLImageLocation')
+    );
+}
+
 $template->param ( LIMIT_INPUTS => \@limit_inputs );
 $template->param ( OPACResultsSidebar => C4::Context->preference('OPACResultsSidebar'));
 
@@ -601,7 +613,11 @@ for (my $i=0;$i<@servers;$i++) {
                 $res->{'incart'} = 1;
             }
 
-            if (C4::Context->preference('COinSinOPACResults')) {
+            if (C4::Context->preference('COinSinOPACResults')
+            or C4::Context->preference('OPACShowOpenURL')) {
+                if (grep /^$res->{itemtype}$/, @OpenURL_itypes) {
+                    $res->{ShowOpenURL} = 1;
+                }
                 my $record = GetMarcBiblio($res->{'biblionumber'});
                 $res->{coins} = GetCOinSBiblio($record);
             }
-- 
1.9.1