}
}
+#camera, #output {
+ border: 8px solid #EDF4F6;
+ padding: 1em;
+}
+
+#photo {
+ display: block;
+ margin: auto;
+}
+
+#camera-error {
+ display: none;
+ flex-direction: row;
+ flex-wrap: nowrap;
+
+ div {
+ padding: 0 .5em;
+ }
+}
+
@media (min-width: 200px) {
}
[% IF ( patron.image ) %]
<img src="/cgi-bin/koha/members/patronimage.pl?borrowernumber=[% patron.borrowernumber | uri %]" class="patronimage" alt="[% patron.firstname | html %] [% patron.surname | html %] ([% patron.cardnumber | html %])" />
<div class="patronimage-controls">
- <div class="patronimage-control"><a data-borrowernumber="[% patron.borrowernumber | uri %]" class="btn btn-default edit-patronimage" title="Edit patron image" href="#"><i class="fa fa-pencil"></i> Edit</a></div>
+ <div class="patronimage-control"><a data-borrowernumber="[% patron.borrowernumber | uri %]" data-cardnumber="[% patron.cardnumber | html %]" class="btn btn-default edit-patronimage" title="Patron photo" href="#"><i class="fa fa-pencil"></i> Edit</a></div>
</div>
[% ELSE %]
<div class="patronimage empty"></div>
<div class="patronimage-controls">
- <div class="patronimage-control"><a data-borrowernumber="[% patron.borrowernumber | uri %]" class="btn btn-default edit-patronimage" title="Add patron image" href="#"><i class="fa fa-plus"></i> Add</a></div>
+ <div class="patronimage-control"><a data-borrowernumber="[% patron.borrowernumber | uri %]" data-cardnumber="[% patron.cardnumber | html %]" class="btn btn-default edit-patronimage" title="Patron photo" href="#"><i class="fa fa-plus"></i> Add</a></div>
</div>
[% END %]
</div>
<div class="modal-content">
<div class="modal-header">
<button type="button" class="closebtn" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
- <h4 class="modal-title" id="patronImageEditLabel">Modal title</h4>
+ <h4 class="modal-title" id="patronImageEditLabel">Patron photo</h4>
</div>
<div class="modal-body">
<img src="[% interface | html %]/[% theme | html %]/img/spinner-small.gif" alt="" />
+[% USE raw %]
[% USE scalar %]
[% USE Koha %]
[% USE Categories %]
<!-- str/members-menu.inc -->
+[% IF ( Koha.Preference('patronimages') ) %]
+ [% Asset.js("js/patron-webcam.js") | $raw %]
+[% END %]
<script>
var advsearch = "[% advsearch | html %]";
var destination = "[% destination | html %]";
$("body").on("click", "#delpicture", function(){
return confirm(_("Are you sure you want to delete this patron image? This cannot be undone."));
});
- $('#manage-patron-image').find("input[value*=Upload]").click(function(){
+ $('#upload-patron-image').find("input[value*=Upload]").click(function(){
if($("#uploadfile").val() == ""){
alert(_("Please choose a file to upload"));
return false;
[% MACRO jsinclude BLOCK %]
[% INCLUDE 'calendar.inc' %]
- [% Asset.js("js/members-menu.js") | $raw %]
<script>
$(document).ready(function() {
$("a.delete").click(function(){
--- /dev/null
+[% USE Koha %]
+
+[% IF ( Koha.Preference('patronimages') ) %]
+ [% IF ( CAN_user_tools_batch_upload_patron_images ) %]
+ <div id="upload-patron-image" class="patroninfo-section">
+ <h5>Upload patron photo</h5>
+ <form method="post" id="picture-upload" action="/cgi-bin/koha/tools/picture-upload.pl"
+ enctype="multipart/form-data">
+ [% IF ( patron.image ) %]
+ <div class="hint">
+ To update the image for [% patron.title | html %] [% patron.firstname | html %] [% patron.surname | html %], select a new image file and click 'Upload.' <br />Click the 'Delete' button to
+ remove the current image.
+ </div>
+ [% ELSE %]
+ <div class="hint">
+ [% patron.title | html %] [% patron.firstname | html %] [% patron.surname | html %] does not currently have an image available. To import an image for [% patron.title | html %] [% patron.firstname | html %] [% patron.surname | html %], enter the name of an image file to upload.
+ </div>
+ [% END %]
+ <p>Only PNG, GIF, JPEG, XPM formats are supported.</p>
+ <label for="uploadfile">Select the file to upload: </label>
+ <input type="file" id="uploadfile" name="uploadfile" required="required" />
+ <div class="action">
+ <input type="hidden" id="csrf_token" name="csrf_token" value="[% csrf_token | html %]" />
+ <input type="hidden" id="image" name="filetype" value="image" />
+ <input type="hidden" id="cardnumber" name="cardnumber" value="[% patron.cardnumber | html %]" />
+ <input type="hidden" id="borrowernumber" name="borrowernumber" value="[% patron.borrowernumber | html %]" />
+ <button type="submit" class="btn btn-default btn-xs"><i class="fa fa-upload"></i> Upload photo</button>
+ <input name="op" type="hidden" value="Upload" />
+ [% IF ( patron.image ) %]
+ <a id="delpicture"
+ href="/cgi-bin/koha/tools/picture-upload.pl?op=Delete&borrowernumber=[% patron.borrowernumber | html %]&csrf_token=[% csrf_token | html %]"
+ class="btn btn-default btn-xs delete"><i class="fa fa-trash"></i> Delete</a>
+ [% END %]
+ </div>
+ </form>
+ </div>
+ <div id="capture-patron-image" class="patroninfo-section">
+ <h5>Take patron photo</h5>
+ <form method="post" id="camera-upload" action="/cgi-bin/koha/tools/picture-upload.pl">
+ <div class="btn-toolbar">
+ <div class="btn-group">
+ <button class="btn btn-default" id="takebutton"><i class="fa fa-camera"></i> Take photo</button>
+ </div>
+ <div class="btn-group">
+ <button class="btn btn-default" id="retakebutton" style="display:none;"><i class="fa fa-refresh"></i> Retake photo</button>
+ </div>
+ <div class="btn-group">
+ <a id="downloadbutton" href="#" class="btn btn-default" style="display:none;"><i class="fa fa-download"></i> Download photo</a>
+ </div>
+ <div class="btn-group">
+ <button id="savebutton" type="submit" class="btn btn-default" style="display:none;"><i class="fa fa-hdd-o"></i> Upload</button>
+ </div>
+ </div>
+ <div id="camera">
+ <video id="viewfinder">Video stream not available.</video>
+ </div>
+ <canvas id="canvas" style="display:none"></canvas>
+ <div id="output" style="display:none">
+ <img style="width:100%;height:auto;" id="photo" alt="The screen capture will appear in this box.">
+ </div>
+ <input type="hidden" name="uploadfilename" value="patron-photo.jpg" />
+ <textarea id="uploadfiletext" name="uploadfiletext" style="display:none;"></textarea>
+ <input type="hidden" name="csrf_token" value="[% csrf_token | html %]" />
+ <input type="hidden" name="filetype" value="image" />
+ <input type="hidden" name="cardnumber" value="[% patron.cardnumber | html %]" />
+ <input type="hidden" name="borrowernumber" value="[% patron.borrowernumber | html %]" />
+ <input name="op" type="hidden" value="Upload" />
+ </form>
+ </div>
+ <div class="dialog message" style="display:none" id="camera-error">
+ <div>
+ <span class="fa-stack fa-lg">
+ <i class="fa fa-camera fa-stack-1x"></i>
+ <i class="fa fa-ban fa-stack-2x text-danger"></i>
+ </span>
+ </div>
+ <div>
+ <strong>Cannot take patron photo.</strong>
+ <span id="camera-error-message"></span>
+ </div>
+ </div>
+ [% END %]
+[% END %]
</div> [% # /div.rows %]
</div> [% # /div#patron-information %]
- [% IF ( patronimages ) %]
- [% IF ( CAN_user_tools_batch_upload_patron_images ) %]
- <div id="manage-patron-image" class="patroninfo-section">
- [% IF ( patron.image ) %]
- <div class="patroninfo-heading">
- <h3>Manage patron image</h3>
- <a class="btn btn-default btn-xs" id="show-picture-upload" href="#"><i class="fa fa-pencil"></i> Edit</a>
- </div>
- [% ELSE %]
- <div class="patroninfo-heading">
- <h3>Upload patron image</h3>
- <a class="btn btn-default btn-xs" id="show-picture-upload" href="#"><i class="fa fa-plus"></i> Add</a>
- </div>
- [% END %]
- <form method="post" id="picture-upload" style="display:none;" action="/cgi-bin/koha/tools/picture-upload.pl" enctype="multipart/form-data">
- [% IF ( patron.image ) %]
- <div class="hint">To update the image for [% patron.title | html %] [% patron.firstname | html %] [% patron.surname | html %], select a new image file and click 'Upload.' <br />Click the 'Delete' button to remove the current image.</div>
- [% ELSE %]
- <div class="hint">[% patron.title | html %] [% patron.firstname | html %] [% patron.surname | html %] does not currently have an image available. To import an image for [% patron.title | html %] [% patron.firstname | html %] [% patron.surname | html %], enter the name of an image file to upload.</div>
- [% END %]
- <p>Only PNG, GIF, JPEG, XPM formats are supported. Maximum image size is 2MB.</p>
- <label for="uploadfile">Select the file to upload: </label>
- <input type="file" id="uploadfile" name="uploadfile" required="required" />
- <div class="action">
- <input type="hidden" name="csrf_token" value="[% csrf_token | html %]" />
- <input type="hidden" id="image" name="filetype" value="image" />
- <input type="hidden" id="cardnumber" name="cardnumber" value="[% patron.cardnumber | html %]" />
- <input type="hidden" name="borrowernumber" value="[% patron.borrowernumber | html %]" />
- <button type="submit" class="btn btn-default btn-xs"><i class="fa fa-upload"></i> Upload</button>
- <input name="op" type="hidden" value="Upload" />
- [% IF ( patron.image ) %]
- <a id="delpicture" href="/cgi-bin/koha/tools/picture-upload.pl?op=Delete&borrowernumber=[% patron.borrowernumber | html %]&csrf_token=[% csrf_token | html %]" class="btn btn-default btn-xs delete"><i class="fa fa-trash"></i> Delete</a>
- [% END %]
- <a href="#" id="cancel-picture-upload" class="cancel">Cancel</a>
- </div>
- </form>
- </div> [% # /div#manage-patron-image %]
- [% END %]
- [% END %]
-
[% IF Koha.Preference('HouseboundModule') %]
<div id="houseboundroles" class="patroninfo-section">
[% IF ( housebound_role.housebound_chooser == 1 OR housebound_role.housebound_deliverer == 1 ) %]
$("#view_restrictions").on("click",function(){
$('#debarments-tab-link').click();
});
-
- $("#show-picture-upload").on("click", function(e){
- e.preventDefault();
- $(this).toggle();
- $("#picture-upload").toggle();
- });
-
- $("#cancel-picture-upload").on("click", function(e){
- e.preventDefault();
- $("#picture-upload, #show-picture-upload").toggle();
- });
-
});
function uncheck_sibling(me){
nodename=me.getAttribute("name");
$("#borrower_message").val( $(this).val() );
});
+ $("#patronImageEdit").on("shown.bs.modal", function(){
+ startup();
+ });
+
$(".edit-patronimage").on("click", function(e){
e.preventDefault();
var borrowernumber = $(this).data("borrowernumber");
- $.get("/cgi-bin/koha/members/moremember.pl", { borrowernumber : borrowernumber }, function( data ){
- var image_form = $(data).find("#picture-upload");
- image_form.show().find(".cancel").remove();
- $("#patronImageEdit .modal-body").html( image_form );
- });
+ var cardnumber = $(this).data("cardnumber");
var modalTitle = $(this).attr("title");
- $("#patronImageEdit .modal-title").text(modalTitle);
- $("#patronImageEdit").modal("show");
+ $.ajax({
+ url: "/cgi-bin/koha/members/moremember-patronimage.pl",
+ type: "GET",
+ data: { borrowernumber: borrowernumber, cardnumber: cardnumber },
+ success: function ( data ) {
+ $("#patronImageEdit .modal-body").html( data );
+ $("#patronImageEdit .modal-title").text(modalTitle);
+ $("#patronImageEdit").modal("show");
+ },
+ error: function () {
+ location.href="/cgi-bin/koha/members/moremember-patronimage.pl?borrowernumber=" + borrowernumber;
+ }
+ });
+ $("#patronImageEdit").on("hidden.bs.modal", function(){
+ /* Stop using the user's camera when modal is closed */
+ let viewfinder = document.getElementById("viewfinder");
+ if( viewfinder.srcObject ){
+ viewfinder.srcObject.getTracks().forEach( track => {
+ if( track.readyState == 'live' && track.kind === 'video'){
+ track.stop();
+ }
+ });
+ }
+ });
});
-
});
-
-
function searchfield_date_tooltip(filter) {
var field = "#searchmember" + filter;
var type = "#searchfieldstype" + filter;
--- /dev/null
+/* global __ */
+/* exported startup */
+
+/* Adapted from Mozilla's article "Taking still photos with WebRTC"
+* https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Taking_still_photos
+*/
+
+var width = 480; // We will scale the photo width to this
+var height = 0; // This will be computed based on the input stream
+
+// |streaming| indicates whether or not we're currently streaming
+// video from the camera. Obviously, we start at false.
+
+var streaming = false;
+
+// The various HTML elements we need to configure or control. These
+// will be set by the startup() function.
+
+var video = null;
+var canvas = null;
+var photo = null;
+var takebutton = null;
+var retakebutton = null;
+var downloadbutton = null;
+var savebutton = null;
+var output = null;
+var camera = null;
+var uploadfiletext = null;
+
+/**
+ * Initiate the camera and add some click handlers
+ */
+
+function startup() {
+ video = document.getElementById('viewfinder');
+ canvas = document.getElementById('canvas');
+ photo = document.getElementById('photo');
+ takebutton = document.getElementById('takebutton');
+ retakebutton = document.getElementById('retakebutton');
+ downloadbutton = document.getElementById('downloadbutton');
+ savebutton = document.getElementById('savebutton');
+ output = document.getElementById("output");
+ camera = document.getElementById("camera");
+ uploadfiletext = document.getElementById("uploadfiletext");
+
+ navigator.mediaDevices.getUserMedia({
+ video: true,
+ audio: false
+ })
+ .then(function (stream) {
+ video.srcObject = stream;
+ video.play();
+ })
+ .catch(function (err) {
+ $("#capture-patron-image").hide();
+ $("#camera-error").css("display", "flex");
+ $("#camera-error-message").text( showMediaErrors( err ) );
+ });
+
+ video.addEventListener('canplay', function () {
+ if (!streaming) {
+ height = video.videoHeight / (video.videoWidth / width);
+
+ // Firefox currently has a bug where the height can't be read from
+ // the video, so we will make assumptions if this happens.
+
+ if (isNaN(height)) {
+ height = width / (4 / 3);
+ }
+
+ video.setAttribute('width', width);
+ video.setAttribute('height', height);
+ canvas.setAttribute('width', width);
+ canvas.setAttribute('height', height);
+ photo.setAttribute('width', width);
+ photo.setAttribute('height', height);
+ streaming = true;
+ }
+ }, false);
+
+ takebutton.addEventListener('click', function (ev) {
+ takepicture();
+ ev.preventDefault();
+ }, false);
+
+ retakebutton.addEventListener('click', function (ev) {
+ ev.preventDefault();
+ retakephoto();
+ }, false);
+
+ clearphoto();
+}
+
+function showMediaErrors( err ){
+ // Example error: "NotAllowedError: Permission denied"
+ var errorcode = err.toString().split(":");
+ var output;
+ switch ( errorcode[0] ) {
+ case "NotFoundError":
+ case "DevicesNotFoundError":
+ output = __("No camera detected.");
+ break;
+ case "NotReadableError":
+ case "TrackStartError":
+ output = __("Could not access camera.");
+ break;
+ case "NotAllowedError":
+ case "PermissionDeniedError":
+ output = __("Access to camera denied.");
+ break;
+ default:
+ output = __("An unknown error occurred: ") + err;
+ break;
+ }
+ return output;
+}
+
+/**
+ * Clear anything passed to the canvas element and the corresponding image.
+ */
+
+function clearphoto() {
+ var context = canvas.getContext('2d');
+ context.fillStyle = "#AAA";
+ context.fillRect(0, 0, canvas.width, canvas.height);
+
+ var data = canvas.toDataURL('image/jpeg', 1.0);
+ photo.setAttribute('src', data);
+}
+
+/**
+ * Reset the interface to hide download and save buttons.
+ * Redisplay camera "shutter" button.
+ */
+
+function retakephoto(){
+ downloadbutton.href= "";
+ downloadbutton.style.display = "none";
+ takebutton.style.display = "inline-block";
+ retakebutton.style.display = "none";
+ savebutton.style.display = "none";
+ output.style.display = "none";
+ photo.src = "";
+ camera.style.display = "block";
+ uploadfiletext.value = "";
+}
+
+/**
+ * Capture the data from the user's camera and write it to the canvas element.
+ * The canvas data is converted to a data-url, and that URL set as the src
+ * attribute of an image.
+ * Display two controls for the captured photo: Download (to save to the
+ * user's computer) and Upload (save to the patron's record in Koha).
+ */
+
+function takepicture() {
+ var context = canvas.getContext('2d');
+ var cardnumber = document.getElementById("cardnumber").value;
+ camera.style.display = "none";
+ downloadbutton.style.display = '';
+ output.style.display = "block";
+ takebutton.style.display = "none";
+ retakebutton.style.display = "inline-block";
+ savebutton.style.display = "inline-block";
+ if (width && height) {
+ canvas.width = width;
+ canvas.height = height;
+ context.drawImage(video, 0, 0, width, height);
+
+ var data = canvas.toDataURL('image/jpeg', 1.0);
+ photo.setAttribute('src', data);
+ if( cardnumber !== '' ){
+ // Download a file which the patrons card number as its name
+ downloadbutton.download = cardnumber + ".jpg";
+ } else {
+ downloadbutton.download = "patron-photo.jpg";
+ }
+ downloadbutton.href = data;
+ uploadfiletext.value = data;
+
+ } else {
+ clearphoto();
+ }
+}
--- /dev/null
+#!/usr/bin/perl
+
+# Copyright 2020 Mark Tompsett
+#
+# 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, see <http://www.gnu.org/licenses>.
+
+use Modern::Perl;
+use CGI qw ( -utf8 );
+use C4::Auth qw( get_template_and_user );
+use C4::Output qw( output_and_exit_if_error output_and_exit output_html_with_http_headers );
+use Koha::Patrons;
+use Koha::Token;
+
+my $input = CGI->new;
+
+my ( $template, $loggedinuser, $cookie ) = get_template_and_user(
+ {
+ template_name => 'members/moremember-patronimage.tt',
+ query => $input,
+ type => 'intranet',
+ flagsrequired => { borrowers => 'edit_borrowers' },
+ }
+);
+
+my $borrowernumber = $input->param('borrowernumber');
+my $cardnumber = $input->param('cardnumber');
+my $patron = Koha::Patrons->find($borrowernumber);
+my $logged_in_user = Koha::Patrons->find($loggedinuser);
+
+$template->param(
+ csrf_token => Koha::Token->new->generate_csrf(
+ { session_id => $input->cookie('CGISESSID'), }
+ ),
+ patron => $patron,
+ logged_in_user => $logged_in_user,
+);
+
+output_html_with_http_headers $input, $cookie, $template->output;
+
+__END__
+
+=head1 moremember-patronimage.pl
+
+ script to provide modal window for patron images
+
+=cut
use File::Temp;
use CGI qw ( -utf8 );
use GD;
+use MIME::Base64;
use C4::Context;
use C4::Auth qw( get_template_and_user );
use C4::Output qw( output_and_exit output_html_with_http_headers );
our $filetype = $input->param('filetype') || '';
my $cardnumber = $input->param('cardnumber');
-our $uploadfilename = $input->param('uploadfile') || '';
+our $uploadfilename = $input->param('uploadfile') || $input->param('uploadfilename') || '';
+my $uploadfiletext = $input->param('uploadfiletext') || '';
my $uploadfile = $input->upload('uploadfile');
my $borrowernumber = $input->param('borrowernumber');
my $op = $input->param('op') || '';
our %errors = ();
# Case is important in these operational values as the template must use case to be visually pleasing!
-if ( ( $op eq 'Upload' ) && $uploadfile ) {
+if ( ( $op eq 'Upload' ) && ($uploadfile || $uploadfiletext) ) {
output_and_exit( $input, $cookie, $template, 'wrong_csrf_token' )
unless Koha::Token->new->check_csrf({
File::Temp::tempfile( SUFFIX => $filesuffix, UNLINK => 1 );
my ( @directories, $results );
- $errors{'NOTZIP'} = 1
- if ( $uploadfilename !~ /\.zip$/i && $filetype =~ m/zip/i );
$errors{'NOWRITETEMP'} = 1 unless ( -w $dirname );
- $errors{'EMPTYUPLOAD'} = 1 unless ( length($uploadfile) > 0 );
+ if ( length($uploadfiletext) == 0 ) {
+ $errors{'NOTZIP'} = 1
+ if ( $uploadfilename !~ /\.zip$/i && $filetype =~ m/zip/i );
+ $errors{'EMPTYUPLOAD'} = 1 unless ( length($uploadfile) > 0 );
+ }
if (%errors) {
$template->param( ERRORS => [ \%errors ] );
output_html_with_http_headers $input, $cookie, $template->output;
exit;
}
- while (<$uploadfile>) {
- print $tfh $_;
+
+ if ( length($uploadfiletext) == 0 ) {
+ while (<$uploadfile>) {
+ print $tfh $_;
+ }
+ } else {
+ # data type controlled in toDataURL() in template
+ if ( $uploadfiletext =~ /data:image\/jpeg;base64,(.*)/ ) {
+ my $encoded_picture = $1;
+ my $decoded_picture = decode_base64($encoded_picture);
+ print $tfh $decoded_picture;
+ } else {
+ $errors{'BADPICTUREDATA'} = 1;
+ $template->param( ERRORS => [ \%errors ] );
+ output_html_with_http_headers $input, $cookie, $template->output;
+ exit;
+ }
}
close $tfh;
if ( $filetype eq 'zip' ) {
return \%direrrors;
}
- while ( my $line = <$fh> ) {
+ my @lines = <$fh>;
+ close $fh;
+ foreach my $line (@lines) {
$logger->debug("Reading contents of $file");
chomp $line;
$logger->debug("Examining line: $line");
$source = "$dir/$filename";
%counts = handle_file( $cardnumber, $source, $template, %counts );
}
- close $fh;
closedir DIR;
}
else {