Bug 6815: Capture member photo via webcam
authorMark Tompsett <mtompset@hotmail.com>
Wed, 15 Jul 2020 04:55:06 +0000 (04:55 +0000)
committerFridolin Somers <fridolin.somers@biblibre.com>
Fri, 25 Mar 2022 00:22:10 +0000 (14:22 -1000)
This patch builds on a patch by Mark Tompsett, adding the option to take
a patron's picture using the computer's webcam. The photo can then be
saved to the patron's account.

To test, apply the patch and rebuild the staff interface CSS
(https://wiki.koha-community.org/wiki/Working_with_SCSS_in_the_OPAC_and_staff_client).

- Go to Administration -> System preferences and enable the
  'patronimages' preference.
- View a patron record. In the sidebar, hover your mouse over the blank
  patron image. Click the "Edit" button which appears.
- A modal window should appear with two sections, "Upload patron photo"
  and "Take patron photo."
  - If your computer has a webcam, your browser should ask permission to
    access it. Grant access.
  - You should see the view of your webcam shown under the "Take photo"
    button.
  - Click the "Take photo" button. The captured photo should be shown in
    place of the live video from the webcam.
  - You should now see three buttons: "Retake photo," "Download photo,"
    and "Upload photo."
    - Clicking "Retake photo" should hide those buttons and return you
      to a live video view.
    - Clicking "Download" should make your browser download the image.
    - Clicking "Upload" should cause the page to redirect back to the
      patron detail page where you should see the new patron image
      displayed in the sidebar.
    - Trigger the modal again and click the "cancel" button. The
      modal should disappear and camera access should stop.
- If your computer has no webcam the modal should appear correctly but
  there should be a banner at the bottom indicating that a camera is not
  available.
- Try the test again but this time deny your browser access to the
  webcam. You may need to reset the camera permissions in your browser's
  settings. When the modal appears you should see a message saying
  access to the camera is denied.
- The patron image edit modal should be available on all pages which
  show the patron image in the sidebar: Check out, Batch check out,
  Details, Accounting, Routing lists, Circulation history, Holds
  history, Modification log, Notices, Statistics, Files, Purchase
  suggestions, Discharges, Housebound, and ILL requests history.
- Test adding an image to a patron record using the "Upload photo"
  option. It should still work correctly.
- If the patron has an image attached, the "Upload photo" section should
  have a "Delete" button. Test that it works correctly.

Signed-off-by: Nicolas Legrand <nicolas.legrand@bulac.fr>
Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
Signed-off-by: Fridolin Somers <fridolin.somers@biblibre.com>
koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss
koha-tmpl/intranet-tmpl/prog/en/includes/circ-menu.inc
koha-tmpl/intranet-tmpl/prog/en/includes/str/members-menu.inc
koha-tmpl/intranet-tmpl/prog/en/modules/members/housebound.tt
koha-tmpl/intranet-tmpl/prog/en/modules/members/moremember-patronimage.tt [new file with mode: 0644]
koha-tmpl/intranet-tmpl/prog/en/modules/members/moremember.tt
koha-tmpl/intranet-tmpl/prog/js/members-menu.js
koha-tmpl/intranet-tmpl/prog/js/patron-webcam.js [new file with mode: 0644]
members/moremember-patronimage.pl [new file with mode: 0755]
tools/picture-upload.pl

index fbf5200..cd1c0ba 100644 (file)
@@ -4618,6 +4618,26 @@ div .suggestion_note {
     }
 }
 
+#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) {
 
 }
index 9b4940b..5bc5fb1 100644 (file)
             [% 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">&times;</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="" />
index 777f814..530e9bf 100644 (file)
@@ -1,7 +1,11 @@
+[% 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 %]";
@@ -26,7 +30,7 @@
         $("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;
index 330a55c..ae21d73 100644 (file)
 
 [% MACRO jsinclude BLOCK %]
     [% INCLUDE 'calendar.inc' %]
-    [% Asset.js("js/members-menu.js") | $raw %]
     <script>
         $(document).ready(function() {
             $("a.delete").click(function(){
diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/members/moremember-patronimage.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/members/moremember-patronimage.tt
new file mode 100644 (file)
index 0000000..bbacd02
--- /dev/null
@@ -0,0 +1,83 @@
+[% 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&amp;borrowernumber=[% patron.borrowernumber | html %]&amp;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 %]
index 335bbc2..eb68600 100644 (file)
                                 </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&amp;borrowernumber=[% patron.borrowernumber | html %]&amp;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");
index def803d..fb198bd 100644 (file)
@@ -90,23 +90,42 @@ $(document).ready(function(){
         $("#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;
diff --git a/koha-tmpl/intranet-tmpl/prog/js/patron-webcam.js b/koha-tmpl/intranet-tmpl/prog/js/patron-webcam.js
new file mode 100644 (file)
index 0000000..0736bcb
--- /dev/null
@@ -0,0 +1,184 @@
+/* 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();
+    }
+}
diff --git a/members/moremember-patronimage.pl b/members/moremember-patronimage.pl
new file mode 100755 (executable)
index 0000000..9370898
--- /dev/null
@@ -0,0 +1,59 @@
+#!/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
index 57c55b9..8399768 100755 (executable)
@@ -24,6 +24,7 @@ use Modern::Perl;
 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 );
@@ -51,7 +52,8 @@ my ($template, $loggedinuser, $cookie)
 
 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') || '';
@@ -83,7 +85,7 @@ our @counts = ();
 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({
@@ -100,18 +102,35 @@ if ( ( $op eq 'Upload' ) && $uploadfile ) {
       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' ) {
@@ -221,7 +240,9 @@ sub handle_dir {
             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");
@@ -241,7 +262,6 @@ sub handle_dir {
             $source = "$dir/$filename";
             %counts = handle_file( $cardnumber, $source, $template, %counts );
         }
-        close $fh;
         closedir DIR;
     }
     else {