From 6768c7e7ef195bef5006a5c4ae85ef7d9e155b0e Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Wed, 22 Jul 2020 08:23:49 +0200 Subject: [PATCH 1/7] webcam support --- lam/lib/html.inc | 63 +++++++++++++++++++++++++++++++ lam/lib/modules/inetOrgPerson.inc | 21 ++++++++++- lam/templates/lib/500_lam.js | 46 ++++++++++++++++++++++ 3 files changed, 128 insertions(+), 2 deletions(-) diff --git a/lam/lib/html.inc b/lam/lib/html.inc index 21fb7b7d..051d88f1 100644 --- a/lam/lib/html.inc +++ b/lam/lib/html.inc @@ -4973,5 +4973,68 @@ class htmlResponsiveTable extends htmlElement { } +/** + * Renders a canvas. + * + * @author Roland Gruber + */ +class htmlCanvas extends htmlElement { + + private $id; + + /** + * Constructor + * + * @param string $id html id + */ + public function __construct($id) { + $this->id = $id; + } + + /** + * @inheritDoc + */ + function generateHTML($module, $input, $values, $restricted, &$tabindex, $scope) { + $classesValue = ''; + if (!empty($this->cssClasses)) { + $classesValue = ' class="' . implode(' ', $this->cssClasses) . '"'; + } + echo ''; + echo ''; + return array(); + } +} + +/** + * Renders a video. + * + * @author Roland Gruber + */ +class htmlVideo extends htmlElement { + + private $id; + + /** + * Constructor + * + * @param string $id html id + */ + public function __construct($id) { + $this->id = $id; + } + + /** + * @inheritDoc + */ + function generateHTML($module, $input, $values, $restricted, &$tabindex, $scope) { + $classesValue = ''; + if (!empty($this->cssClasses)) { + $classesValue = ' class="' . implode(' ', $this->cssClasses) . '"'; + } + echo ''; + return array(); + } +} ?> diff --git a/lam/lib/modules/inetOrgPerson.inc b/lam/lib/modules/inetOrgPerson.inc index 47fbeef2..f6439fab 100644 --- a/lam/lib/modules/inetOrgPerson.inc +++ b/lam/lib/modules/inetOrgPerson.inc @@ -1704,9 +1704,26 @@ class inetOrgPerson extends baseModule implements passwordService { $container->add(new htmlSubTitle(_('Upload image')), 12); $label = _('Photo file'); $container->add(new htmlResponsiveInputFileUpload('photoFile', $label, 'photoUpload'), 12); + $container->addVerticalSpacer('0.5rem'); + $container->addLabel(new htmlOutputText(' ', false)); + $container->addField(new htmlAccountPageButton(get_class($this), 'photo', 'upload', _('Upload'))); $container->addVerticalSpacer('1rem'); - $container->addLabel(new htmlAccountPageButton(get_class($this), 'photo', 'upload', _('Upload'))); - $container->addField(new htmlAccountPageButton(get_class($this), 'attributes', 'back', _('Back'))); + $webcamContent = new htmlResponsiveRow(); + $webcamContent->add(new htmlSubTitle(_('Get from webcam')), 12); + $webcamContent->addLabel(new htmlOutputText(_('Image'))); + $webcamContent->addField(new htmlVideo('lam-webcam-video')); + $webcamContent->addLabel(new htmlOutputText(' ', false)); + $webcamButtonGroup = new htmlGroup(); + $captureButton = new htmlButton('lam-webcam-capture', _('Capture')); + $captureButton->setOnClick('window.lam.tools.startWebcamCapture(event);'); + $webcamButtonGroup->addElement($captureButton); + $webcamButtonGroup->addElement(new htmlAccountPageButton(get_class($this), 'photo', 'upload', _('Upload'))); + $webcamButtonGroup->addElement(new htmlCanvas('lam-webcam-canvas')); + $webcamContent->addField($webcamButtonGroup); + $webcamDiv = new htmlDiv('lam_webcam_div', $webcamContent, array('hidden')); + $container->add($webcamDiv, 12); + $container->addVerticalSpacer('1rem'); + $container->add(new htmlAccountPageButton(get_class($this), 'attributes', 'back', _('Back')), 12); } else { $container->add(new htmlSubTitle(_('Crop image')), 12); diff --git a/lam/templates/lib/500_lam.js b/lam/templates/lib/500_lam.js index d7839e40..4ec1ce52 100644 --- a/lam/templates/lib/500_lam.js +++ b/lam/templates/lib/500_lam.js @@ -934,6 +934,51 @@ window.lam.tools.setInitialFocus = function() { jQuery('.lam-initial-focus').focus(); }; +/** + * Initializes the webcam capture. + */ +window.lam.tools.initWebcamCapture = function() { + var contentDiv = jQuery('#lam_webcam_div'); + if (contentDiv.length === 0) { + return; + } + if (navigator.mediaDevices && navigator.mediaDevices.enumerateDevices) { + navigator.mediaDevices.enumerateDevices() + .then(function(mediaDevices) { + mediaDevices.forEach(mediaDevice => { + if (mediaDevice.kind === 'videoinput') { + contentDiv.show(); + } + }); + }); + } +}; + +/** + * Starts the webcam capture. + */ +window.lam.tools.startWebcamCapture = function(event) { + event.preventDefault(); + var canvas = document.getElementById('lam-webcam-canvas'); + var video = document.getElementById('lam-webcam-videofor'); + navigator.mediaDevices.getUserMedia({ + video: { + facingMode: 'user', + width: { min: 1024, ideal: 1280, max: 1920 }, + height: { min: 576, ideal: 720, max: 1080 } + }, + audio: false + }) + .then(function(stream) { + video.srcObject = stream; + video.play(); + }) + .catch(function(err) { + console.log("An error occurred: " + err); + }); + return false; +} + window.lam.tools.schema = window.lam.tools.schema || {}; /** @@ -1788,6 +1833,7 @@ jQuery(document).ready(function() { window.lam.tools.addSavedSelectListener(); window.lam.tools.activateTab(); window.lam.tools.setInitialFocus(); + window.lam.tools.initWebcamCapture(); window.lam.tools.schema.select(); window.lam.html.activateLightboxes(); window.lam.html.preventEnter(); From 3ad5dcf65aad8c7a521d5de25a4f0bccc2d3b44f Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Wed, 22 Jul 2020 13:28:17 +0200 Subject: [PATCH 2/7] webcam support --- lam/lib/html.inc | 6 +++ lam/lib/modules/inetOrgPerson.inc | 79 ++++++++++++++++++------------- lam/style/500_layout.css | 5 ++ lam/templates/lib/500_lam.js | 39 +++++++++++++-- 4 files changed, 93 insertions(+), 36 deletions(-) diff --git a/lam/lib/html.inc b/lam/lib/html.inc index 051d88f1..e41ed223 100644 --- a/lam/lib/html.inc +++ b/lam/lib/html.inc @@ -3078,7 +3078,13 @@ class htmlStatusMessage extends htmlElement { * @return array List of input field names and their type (name => type) */ public function generateHTML($module, $input, $values, $restricted, &$tabindex, $scope) { + if (!empty($this->cssClasses)) { + echo '
'; + } StatusMessage($this->type, $this->title, $this->text, $this->params); + if (!empty($this->cssClasses)) { + echo '
'; + } return array(); } diff --git a/lam/lib/modules/inetOrgPerson.inc b/lam/lib/modules/inetOrgPerson.inc index f6439fab..d9852cb7 100644 --- a/lam/lib/modules/inetOrgPerson.inc +++ b/lam/lib/modules/inetOrgPerson.inc @@ -1629,7 +1629,7 @@ class inetOrgPerson extends baseModule implements passwordService { if ($this->isAdminReadOnly('jpegPhoto')) { return array(); } - if (isset($_POST['form_subpage_' . get_class($this) . '_photo_upload'])) { + if (isset($_POST['form_subpage_' . get_class($this) . '_photo_upload']) || isset($_POST['webcamData'])) { return $this->uploadPhoto(); } if (isset($_POST['form_subpage_' . get_class($this) . '_attributes_crop'])) { @@ -1656,7 +1656,11 @@ class inetOrgPerson extends baseModule implements passwordService { */ private function uploadPhoto() { $messages = array(); - if ($_FILES['photoFile'] && ($_FILES['photoFile']['size'] > 0)) { + if ((empty($_FILES['photoFile']) || ($_FILES['photoFile']['size'] <= 0)) && empty($_POST['webcamData'])) { + $messages[] = $this->messages['file'][0]; + return $messages; + } + if (!empty($_FILES['photoFile']['tmp_name'])) { $handle = fopen($_FILES['photoFile']['tmp_name'], "r"); $data = fread($handle, 100000000); if (!empty($this->moduleSettings['inetOrgPerson_jpegPhoto_maxSize'][0]) && (strlen($data) > (1024 * $this->moduleSettings['inetOrgPerson_jpegPhoto_maxSize'][0]))) { @@ -1666,30 +1670,32 @@ class inetOrgPerson extends baseModule implements passwordService { return array($errMsg); } fclose($handle); - // convert to JPG - try { - include_once dirname(__FILE__) . '/../imageutils.inc'; - $imageManipulator = ImageManipulationFactory::getImageManipulator($data); - // resize if maximum values specified - if (!empty($this->moduleSettings['inetOrgPerson_jpegPhoto_maxWidth'][0]) || !empty($this->moduleSettings['inetOrgPerson_jpegPhoto_maxHeight'][0])) { - $maxWidth = empty($this->moduleSettings['inetOrgPerson_jpegPhoto_maxWidth'][0]) ? $imageManipulator->getWidth() : $this->moduleSettings['inetOrgPerson_jpegPhoto_maxWidth'][0]; - $maxHeight = empty($this->moduleSettings['inetOrgPerson_jpegPhoto_maxHeight'][0]) ? $imageManipulator->getHeight() : $this->moduleSettings['inetOrgPerson_jpegPhoto_maxHeight'][0]; - $imageManipulator->thumbnail($maxWidth, $maxHeight); - } - $imageManipulator->convertToJpeg(); - $data = $imageManipulator->getImageData(); - } - catch (Exception $e) { - $msg = $this->messages['file'][2]; - $msg[] = htmlspecialchars($e->getMessage()); - $messages[] = $msg; - return $messages; - } - $this->attributes['jpegPhoto'][0] = $data; } - else { - $messages[] = $this->messages['file'][0]; + elseif (isset($_POST['webcamData'])) { + $data = $_POST['webcamData']; + $data = str_replace('data:image/png;base64,', '', $data); + $data = base64_decode($data); } + // convert to JPG + try { + include_once dirname(__FILE__) . '/../imageutils.inc'; + $imageManipulator = ImageManipulationFactory::getImageManipulator($data); + // resize if maximum values specified + if (!empty($this->moduleSettings['inetOrgPerson_jpegPhoto_maxWidth'][0]) || !empty($this->moduleSettings['inetOrgPerson_jpegPhoto_maxHeight'][0])) { + $maxWidth = empty($this->moduleSettings['inetOrgPerson_jpegPhoto_maxWidth'][0]) ? $imageManipulator->getWidth() : $this->moduleSettings['inetOrgPerson_jpegPhoto_maxWidth'][0]; + $maxHeight = empty($this->moduleSettings['inetOrgPerson_jpegPhoto_maxHeight'][0]) ? $imageManipulator->getHeight() : $this->moduleSettings['inetOrgPerson_jpegPhoto_maxHeight'][0]; + $imageManipulator->thumbnail($maxWidth, $maxHeight); + } + $imageManipulator->convertToJpeg(); + $data = $imageManipulator->getImageData(); + } + catch (Exception $e) { + $msg = $this->messages['file'][2]; + $msg[] = htmlspecialchars($e->getMessage()); + $messages[] = $msg; + return $messages; + } + $this->attributes['jpegPhoto'][0] = $data; return $messages; } @@ -1710,16 +1716,23 @@ class inetOrgPerson extends baseModule implements passwordService { $container->addVerticalSpacer('1rem'); $webcamContent = new htmlResponsiveRow(); $webcamContent->add(new htmlSubTitle(_('Get from webcam')), 12); - $webcamContent->addLabel(new htmlOutputText(_('Image'))); - $webcamContent->addField(new htmlVideo('lam-webcam-video')); - $webcamContent->addLabel(new htmlOutputText(' ', false)); - $webcamButtonGroup = new htmlGroup(); - $captureButton = new htmlButton('lam-webcam-capture', _('Capture')); + $errorMessage = new htmlStatusMessage('ERROR', ''); + $errorMessage->setCSSClasses(array('hidden', 'lam-webcam-message')); + $webcamContent->add($errorMessage, 12); + $captureButton = new htmlButton('lam-webcam-capture', _('Start capture')); $captureButton->setOnClick('window.lam.tools.startWebcamCapture(event);'); - $webcamButtonGroup->addElement($captureButton); - $webcamButtonGroup->addElement(new htmlAccountPageButton(get_class($this), 'photo', 'upload', _('Upload'))); - $webcamButtonGroup->addElement(new htmlCanvas('lam-webcam-canvas')); - $webcamContent->addField($webcamButtonGroup); + $webcamContent->add($captureButton, 12, 12, 12, 'text-center'); + $video = new htmlVideo('lam-webcam-video'); + $video->setCSSClasses(array('hidden')); + $webcamContent->add($video, 12, 12, 12, 'text-center'); + $webcamContent->addVerticalSpacer('0.5rem'); + $webcamUploadButton = new htmlButton('uploadWebcam', _('Upload')); + $webcamUploadButton->setCSSClasses(array('btn-lam-webcam-upload', 'hidden')); + $webcamUploadButton->setOnClick('window.lam.tools.startWebcamUpload();'); + $webcamContent->add($webcamUploadButton, 12, 12, 12, 'text-center'); + $canvas = new htmlCanvas('lam-webcam-canvas'); + $canvas->setCSSClasses(array('hidden')); + $webcamContent->add($canvas, 12); $webcamDiv = new htmlDiv('lam_webcam_div', $webcamContent, array('hidden')); $container->add($webcamDiv, 12); $container->addVerticalSpacer('1rem'); diff --git a/lam/style/500_layout.css b/lam/style/500_layout.css index 3312b922..157d6f23 100644 --- a/lam/style/500_layout.css +++ b/lam/style/500_layout.css @@ -314,6 +314,11 @@ table.collapse { font-weight: bold; } +#lam-webcam-video { + max-width: 200px; + max-height: 200px; +} + /** buttons */ .saveButton { background-image: url(../graphics/save.png) !important; diff --git a/lam/templates/lib/500_lam.js b/lam/templates/lib/500_lam.js index 4ec1ce52..edb3d32e 100644 --- a/lam/templates/lib/500_lam.js +++ b/lam/templates/lib/500_lam.js @@ -959,8 +959,9 @@ window.lam.tools.initWebcamCapture = function() { */ window.lam.tools.startWebcamCapture = function(event) { event.preventDefault(); - var canvas = document.getElementById('lam-webcam-canvas'); - var video = document.getElementById('lam-webcam-videofor'); + var video = document.getElementById('lam-webcam-video'); + var msg = jQuery('.lam-webcam-message'); + msg.hide(); navigator.mediaDevices.getUserMedia({ video: { facingMode: 'user', @@ -972,13 +973,45 @@ window.lam.tools.startWebcamCapture = function(event) { .then(function(stream) { video.srcObject = stream; video.play(); + window.lam.tools.webcamStream = stream; + jQuery('#btn_lam-webcam-capture').hide(); + jQuery('.btn-lam-webcam-upload').show(); + jQuery('#lam-webcam-video').show(); }) .catch(function(err) { - console.log("An error occurred: " + err); + msg.find('.statusTitle').text(err); + msg.show(); }); return false; } +/** + * Starts the webcam upload. + */ +window.lam.tools.startWebcamUpload = function() { + var canvas = document.getElementById('lam-webcam-canvas'); + var video = document.getElementById('lam-webcam-video'); + var form = jQuery('#lam-webcam-canvas').closest('form'); + canvas.setAttribute('width', video.videoWidth); + canvas.setAttribute('height', video.videoHeight); + var context = canvas.getContext('2d'); + context.drawImage(video, 0, 0, video.videoWidth, video.videoHeight); + var canvasData = canvas.toDataURL("image/png"); + var canvasDataInput = jQuery(""); + canvasDataInput.attr('name', 'webcamData'); + canvasDataInput.attr('type', 'hidden'); + canvasDataInput.attr('value', canvasData); + video.pause(); + window.lam.tools.webcamStream.getTracks().forEach(function(track) { + track.stop(); + }); + form.append(canvasDataInput); + jQuery(canvas).remove(); + jQuery(video).remove(); + form.submit(); + return true; +} + window.lam.tools.schema = window.lam.tools.schema || {}; /** From 75120fc25d835d45d1dd01eb74bfe42d1682d09f Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Fri, 24 Jul 2020 10:06:22 +0200 Subject: [PATCH 3/7] webcam support --- lam/HISTORY | 1 + lam/graphics/webcam.png | Bin 0 -> 3457 bytes lam/lib/html.inc | 15 ++++- lam/lib/modules/inetOrgPerson.inc | 50 ++++++++++++++--- lam/style/500_layout.css | 5 -- lam/style/responsive/120_lam.css | 15 +++++ lam/templates/lib/500_lam.js | 88 +++++++++++++++++++++++++----- 7 files changed, 146 insertions(+), 28 deletions(-) create mode 100644 lam/graphics/webcam.png diff --git a/lam/HISTORY b/lam/HISTORY index 8e72b205..bcae7f22 100644 --- a/lam/HISTORY +++ b/lam/HISTORY @@ -3,6 +3,7 @@ September 2020 - Configuration export and import - Show password prompt when a user with expired password logs into LAM admin interface (requires PHP 7.2) - Better error messages on login when account is expired/deactivated/... + - Personal: photo can be uploaded via webcam - Windows users: group display format can be configured (cn/dn) - LAM Pro: -> Windows: new cron job to send users a summary of their managed groups diff --git a/lam/graphics/webcam.png b/lam/graphics/webcam.png new file mode 100644 index 0000000000000000000000000000000000000000..f55a6f3dbd5edba48583c1b39473661ae2de38e5 GIT binary patch literal 3457 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 zm))OraOuMgonClick . '"'; } $idAttr = ''; - if ($this->showAsButton) { - $id = 'a_' . preg_replace('/[^a-zA-Z0-9_]+/', '_', $this->target); + if ($this->showAsButton || !empty($this->id)) { + $id = !empty($this->id) ? $this->id : 'a_' . preg_replace('/[^a-zA-Z0-9_]+/', '_', $this->target); $idAttr = ' id="' . $id . '"'; } $classAttr = ''; @@ -3463,6 +3465,15 @@ class htmlLink extends htmlElement { $this->onClick = htmlspecialchars($event); } + /** + * Sets the element id. + * + * @param string $id unique id + */ + public function setId($id) { + $this->id = $id; + } + } /** diff --git a/lam/lib/modules/inetOrgPerson.inc b/lam/lib/modules/inetOrgPerson.inc index d9852cb7..f7b74bd7 100644 --- a/lam/lib/modules/inetOrgPerson.inc +++ b/lam/lib/modules/inetOrgPerson.inc @@ -1715,12 +1715,12 @@ class inetOrgPerson extends baseModule implements passwordService { $container->addField(new htmlAccountPageButton(get_class($this), 'photo', 'upload', _('Upload'))); $container->addVerticalSpacer('1rem'); $webcamContent = new htmlResponsiveRow(); - $webcamContent->add(new htmlSubTitle(_('Get from webcam')), 12); + $webcamContent->add(new htmlSubTitle(_('Use webcam')), 12); $errorMessage = new htmlStatusMessage('ERROR', ''); $errorMessage->setCSSClasses(array('hidden', 'lam-webcam-message')); $webcamContent->add($errorMessage, 12); $captureButton = new htmlButton('lam-webcam-capture', _('Start capture')); - $captureButton->setOnClick('window.lam.tools.startWebcamCapture(event);'); + $captureButton->setOnClick('window.lam.tools.webcam.capture(event);'); $webcamContent->add($captureButton, 12, 12, 12, 'text-center'); $video = new htmlVideo('lam-webcam-video'); $video->setCSSClasses(array('hidden')); @@ -1728,7 +1728,7 @@ class inetOrgPerson extends baseModule implements passwordService { $webcamContent->addVerticalSpacer('0.5rem'); $webcamUploadButton = new htmlButton('uploadWebcam', _('Upload')); $webcamUploadButton->setCSSClasses(array('btn-lam-webcam-upload', 'hidden')); - $webcamUploadButton->setOnClick('window.lam.tools.startWebcamUpload();'); + $webcamUploadButton->setOnClick('window.lam.tools.webcam.upload();'); $webcamContent->add($webcamUploadButton, 12, 12, 12, 'text-center'); $canvas = new htmlCanvas('lam-webcam-canvas'); $canvas->setCSSClasses(array('hidden')); @@ -3092,6 +3092,33 @@ class inetOrgPerson extends baseModule implements passwordService { $uploadStatus = new htmlDiv('inetOrgPersonPhotoUploadStatus', new htmlOutputText('')); $uploadStatus->setCSSClasses(array('qq-upload-list')); $row->add($uploadStatus, 12); + // webcam button + $webcamContent = new htmlResponsiveRow(); + $webcamContent->addVerticalSpacer('0.5rem'); + $errorMessage = new htmlStatusMessage('ERROR', ''); + $errorMessage->setCSSClasses(array('hidden', 'lam-webcam-message')); + $webcamContent->add($errorMessage, 12); + $webcamContent->addVerticalSpacer('0.5rem'); + $captureButton = new htmlLink(_('Use webcam'), '#', '../../graphics/webcam.png', true); + $captureButton->setId('btn_lam-webcam-capture'); + $captureButton->setOnClick('window.lam.tools.webcam.capture(event);'); + $webcamContent->add($captureButton, 12, 12, 12); + $video = new htmlVideo('lam-webcam-video'); + $video->setCSSClasses(array('hidden')); + $webcamContent->add($video, 12, 12, 12, 'text-center'); + $webcamContent->addVerticalSpacer('1rem'); + $webcamUploadButton = new htmlLink(_('Upload'), '#', '../../graphics/up.gif', true); + $webcamUploadButton->setId('btn-lam-webcam-upload'); + $webcamUploadButton->setCSSClasses(array('btn-lam-webcam-upload', 'hidden')); + $webcamUploadButton->setOnClick('window.lam.tools.webcam.uploadSelfService(event, "' . getSecurityTokenName() + . '", "' . getSecurityTokenValue() . '", "inetOrgPerson", "user", "' . _('File upload failed!') . '", "inetOrgPersonPhotoUploadContent");'); + $webcamContent->add($webcamUploadButton, 12, 12, 12); + $canvas = new htmlCanvas('lam-webcam-canvas'); + $canvas->setCSSClasses(array('hidden')); + $webcamContent->add($canvas, 12); + $webcamDiv = new htmlDiv('lam_webcam_div', $webcamContent, array('hidden')); + $webcamContent->addVerticalSpacer('1rem'); + $row->add($webcamDiv, 12); return $row; } @@ -3125,6 +3152,7 @@ class inetOrgPerson extends baseModule implements passwordService { if (data.success) { if (data.html) { jQuery(\'#inetOrgPersonPhotoUploadContent\').html(data.html); + window.lam.tools.webcam.init(); } } else { @@ -3149,6 +3177,7 @@ class inetOrgPerson extends baseModule implements passwordService { function inetOrgPersonDeletePhotoHandleReply(data) { if (data.errorsOccured == "false") { jQuery(\'#inetOrgPersonPhotoUploadContent\').html(data.html); + window.lam.tools.webcam.init(); } else { alert(data.errormessage); @@ -3820,13 +3849,20 @@ class inetOrgPerson extends baseModule implements passwordService { */ private function ajaxUploadPhoto() { $result = array('success' => true); - if (!isset($_FILES['qqfile']) || ($_FILES['qqfile']['size'] < 100)) { + if ((!isset($_FILES['qqfile']) || ($_FILES['qqfile']['size'] < 100)) && empty($_POST['webcamData'])) { $result = array('error' => _('No file received.')); } else { - $handle = fopen($_FILES['qqfile']['tmp_name'], "r"); - $data = fread($handle, 100000000); - fclose($handle); + if (empty($_POST['webcamData'])) { + $handle = fopen($_FILES['qqfile']['tmp_name'], "r"); + $data = fread($handle, 100000000); + fclose($handle); + } + else { + $data = $_POST['webcamData']; + $data = str_replace('data:image/png;base64,', '', $data); + $data = base64_decode($data); + } try { include_once dirname(__FILE__) . '/../imageutils.inc'; $imageManipulator = ImageManipulationFactory::getImageManipulator($data); diff --git a/lam/style/500_layout.css b/lam/style/500_layout.css index 157d6f23..3312b922 100644 --- a/lam/style/500_layout.css +++ b/lam/style/500_layout.css @@ -314,11 +314,6 @@ table.collapse { font-weight: bold; } -#lam-webcam-video { - max-width: 200px; - max-height: 200px; -} - /** buttons */ .saveButton { background-image: url(../graphics/save.png) !important; diff --git a/lam/style/responsive/120_lam.css b/lam/style/responsive/120_lam.css index 190b9c42..174983a5 100644 --- a/lam/style/responsive/120_lam.css +++ b/lam/style/responsive/120_lam.css @@ -193,6 +193,11 @@ table.responsive-table td { padding: 5px 5px 5px 5px; } + #lam-webcam-video { + max-width: 200px; + max-height: 200px; + } + } /* tablet */ @@ -222,6 +227,11 @@ table.responsive-table td { padding: 5px 20px 5px 20px; } + #lam-webcam-video { + max-width: 300px; + max-height: 300px; + } + } /* desktop */ @@ -255,4 +265,9 @@ table.responsive-table td { padding: 5px 20px 5px 20px; } + #lam-webcam-video { + max-width: 400px; + max-height: 400px; + } + } diff --git a/lam/templates/lib/500_lam.js b/lam/templates/lib/500_lam.js index edb3d32e..5a451667 100644 --- a/lam/templates/lib/500_lam.js +++ b/lam/templates/lib/500_lam.js @@ -934,10 +934,12 @@ window.lam.tools.setInitialFocus = function() { jQuery('.lam-initial-focus').focus(); }; +window.lam.tools.webcam = window.lam.tools.webcam || {}; + /** * Initializes the webcam capture. */ -window.lam.tools.initWebcamCapture = function() { +window.lam.tools.webcam.init = function() { var contentDiv = jQuery('#lam_webcam_div'); if (contentDiv.length === 0) { return; @@ -957,7 +959,7 @@ window.lam.tools.initWebcamCapture = function() { /** * Starts the webcam capture. */ -window.lam.tools.startWebcamCapture = function(event) { +window.lam.tools.webcam.capture = function(event) { event.preventDefault(); var video = document.getElementById('lam-webcam-video'); var msg = jQuery('.lam-webcam-message'); @@ -988,28 +990,86 @@ window.lam.tools.startWebcamCapture = function(event) { /** * Starts the webcam upload. */ -window.lam.tools.startWebcamUpload = function() { +window.lam.tools.webcam.upload = function() { + var form = jQuery('#lam-webcam-canvas').closest('form'); + canvasData = window.lam.tools.webcam.prepareData(); + var canvasDataInput = jQuery(""); + canvasDataInput.attr('name', 'webcamData'); + canvasDataInput.attr('id', 'webcamData'); + canvasDataInput.attr('type', 'hidden'); + canvasDataInput.attr('value', canvasData); + form.append(canvasDataInput); + form.submit(); + return true; +} + +/** + * Starts the webcam upload. + * + * @param event click event + * @param tokenName security token name + * @param tokenValue security token value + * @param moduleName module name + * @param scope account type + * @param uploadErrorMessage error message if upload fails + * @param contentId id of content to replace + */ +window.lam.tools.webcam.uploadSelfService = function(event, tokenName, tokenValue, moduleName, scope, uploadErrorMessage, contentId) { + event.preventDefault(); + var msg = jQuery('.lam-webcam-message'); + canvasData = window.lam.tools.webcam.prepareData(); + var data = { + webcamData: canvasData + }; + data[tokenName] = tokenValue; + jQuery.ajax({ + url: '../misc/ajax.php?selfservice=1&action=ajaxPhotoUpload' + + '&module=' + moduleName + '&scope=' + scope, + method: 'POST', + data: data + }) + .done(function(jsonData) { + if (jsonData.success) { + if (jsonData.html) { + jQuery('#' + contentId).html(jsonData.html); + window.lam.tools.webcam.init(); + } + return false; + } + else if (jsonData.error) { + msg.find('.statusTitle').text(jsonData.error); + msg.show(); + } + }) + .fail(function() { + msg.find('.statusTitle').text(errorMessage); + msg.show(); + }); + jQuery('#btn_lam-webcam-capture').show(); + jQuery('.btn-lam-webcam-upload').hide(); + return false; +} + +/** + * Starts the webcam upload. + * + * @return webcam data as string + */ +window.lam.tools.webcam.prepareData = function() { var canvas = document.getElementById('lam-webcam-canvas'); var video = document.getElementById('lam-webcam-video'); - var form = jQuery('#lam-webcam-canvas').closest('form'); canvas.setAttribute('width', video.videoWidth); canvas.setAttribute('height', video.videoHeight); var context = canvas.getContext('2d'); context.drawImage(video, 0, 0, video.videoWidth, video.videoHeight); var canvasData = canvas.toDataURL("image/png"); - var canvasDataInput = jQuery(""); - canvasDataInput.attr('name', 'webcamData'); - canvasDataInput.attr('type', 'hidden'); - canvasDataInput.attr('value', canvasData); video.pause(); window.lam.tools.webcamStream.getTracks().forEach(function(track) { track.stop(); }); - form.append(canvasDataInput); - jQuery(canvas).remove(); - jQuery(video).remove(); - form.submit(); - return true; + jQuery(canvas).hide(); + jQuery(video).hide(); + return canvasData; } window.lam.tools.schema = window.lam.tools.schema || {}; @@ -1866,7 +1926,7 @@ jQuery(document).ready(function() { window.lam.tools.addSavedSelectListener(); window.lam.tools.activateTab(); window.lam.tools.setInitialFocus(); - window.lam.tools.initWebcamCapture(); + window.lam.tools.webcam.init(); window.lam.tools.schema.select(); window.lam.html.activateLightboxes(); window.lam.html.preventEnter(); From 018513ac952174586e7d7e857a76ad0e54bd9c41 Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Fri, 24 Jul 2020 20:49:04 +0200 Subject: [PATCH 4/7] webcam support --- lam/lib/modules/inetOrgPerson.inc | 2 +- lam/lib/modules/windowsUser.inc | 86 +++++++++++++++++++++---------- 2 files changed, 59 insertions(+), 29 deletions(-) diff --git a/lam/lib/modules/inetOrgPerson.inc b/lam/lib/modules/inetOrgPerson.inc index f7b74bd7..b1960f7b 100644 --- a/lam/lib/modules/inetOrgPerson.inc +++ b/lam/lib/modules/inetOrgPerson.inc @@ -1663,13 +1663,13 @@ class inetOrgPerson extends baseModule implements passwordService { if (!empty($_FILES['photoFile']['tmp_name'])) { $handle = fopen($_FILES['photoFile']['tmp_name'], "r"); $data = fread($handle, 100000000); + fclose($handle); if (!empty($this->moduleSettings['inetOrgPerson_jpegPhoto_maxSize'][0]) && (strlen($data) > (1024 * $this->moduleSettings['inetOrgPerson_jpegPhoto_maxSize'][0]))) { $errMsg = $this->messages['file'][3]; $errMsg[] = null; $errMsg[] = array($this->moduleSettings['inetOrgPerson_jpegPhoto_maxSize'][0]); return array($errMsg); } - fclose($handle); } elseif (isset($_POST['webcamData'])) { $data = $_POST['webcamData']; diff --git a/lam/lib/modules/windowsUser.inc b/lam/lib/modules/windowsUser.inc index ade505f0..e789b716 100644 --- a/lam/lib/modules/windowsUser.inc +++ b/lam/lib/modules/windowsUser.inc @@ -2087,9 +2087,33 @@ class windowsUser extends baseModule implements passwordService { $container->add(new htmlSubTitle(_('Upload image')), 12); $label = _('Photo file'); $container->add(new htmlResponsiveInputFileUpload('photoFile', $label, 'photoUpload'), 12); - $container->addVerticalSpacer('2rem'); - $container->addLabel(new htmlAccountPageButton(get_class($this), 'photo', 'upload', _('Upload'))); - $container->addField(new htmlAccountPageButton(get_class($this), 'attributes', 'back', _('Back'))); + $container->addVerticalSpacer('0.5rem'); + $container->addLabel(new htmlOutputText(' ', false)); + $container->addField(new htmlAccountPageButton(get_class($this), 'photo', 'upload', _('Upload'))); + $container->addVerticalSpacer('1rem'); + $webcamContent = new htmlResponsiveRow(); + $webcamContent->add(new htmlSubTitle(_('Use webcam')), 12); + $errorMessage = new htmlStatusMessage('ERROR', ''); + $errorMessage->setCSSClasses(array('hidden', 'lam-webcam-message')); + $webcamContent->add($errorMessage, 12); + $captureButton = new htmlButton('lam-webcam-capture', _('Start capture')); + $captureButton->setOnClick('window.lam.tools.webcam.capture(event);'); + $webcamContent->add($captureButton, 12, 12, 12, 'text-center'); + $video = new htmlVideo('lam-webcam-video'); + $video->setCSSClasses(array('hidden')); + $webcamContent->add($video, 12, 12, 12, 'text-center'); + $webcamContent->addVerticalSpacer('0.5rem'); + $webcamUploadButton = new htmlButton('uploadWebcam', _('Upload')); + $webcamUploadButton->setCSSClasses(array('btn-lam-webcam-upload', 'hidden')); + $webcamUploadButton->setOnClick('window.lam.tools.webcam.upload();'); + $webcamContent->add($webcamUploadButton, 12, 12, 12, 'text-center'); + $canvas = new htmlCanvas('lam-webcam-canvas'); + $canvas->setCSSClasses(array('hidden')); + $webcamContent->add($canvas, 12); + $webcamDiv = new htmlDiv('lam_webcam_div', $webcamContent, array('hidden')); + $container->add($webcamDiv, 12); + $container->addVerticalSpacer('1rem'); + $container->add(new htmlAccountPageButton(get_class($this), 'attributes', 'back', _('Back')), 12); } else { $container->add(new htmlSubTitle(_('Crop image')), 12); @@ -2115,7 +2139,7 @@ class windowsUser extends baseModule implements passwordService { if (isset($_POST['form_subpage_' . get_class($this) . '_attributes_back'])) { return array(); } - if (isset($_POST['form_subpage_' . get_class($this) . '_photo_upload'])) { + if (isset($_POST['form_subpage_' . get_class($this) . '_photo_upload']) || isset($_POST['webcamData'])) { return $this->uploadPhoto(); } if (isset($_POST['form_subpage_' . get_class($this) . '_attributes_crop'])) { @@ -2142,40 +2166,46 @@ class windowsUser extends baseModule implements passwordService { */ private function uploadPhoto() { $messages = array(); - if ($_FILES['photoFile'] && ($_FILES['photoFile']['size'] > 0)) { + if ((empty($_FILES['photoFile']) || ($_FILES['photoFile']['size'] <= 0)) && empty($_POST['webcamData'])) { + $messages[] = $this->messages['file'][0]; + return $messages; + } + if (!empty($_FILES['photoFile']['tmp_name'])) { $handle = fopen($_FILES['photoFile']['tmp_name'], "r"); $data = fread($handle, 10000000); + fclose($handle); if (!empty($this->moduleSettings['windowsUser_jpegPhoto_maxSize'][0]) && (strlen($data) > (1024 * $this->moduleSettings['windowsUser_jpegPhoto_maxSize'][0]))) { $errMsg = $this->messages['file'][3]; $errMsg[] = null; $errMsg[] = array($this->moduleSettings['windowsUser_jpegPhoto_maxSize'][0]); return array($errMsg); } - fclose($handle); - // convert to JPG - try { - include_once dirname(__FILE__) . '/../imageutils.inc'; - $imageManipulator = ImageManipulationFactory::getImageManipulator($data); - // resize if maximum values specified - if (!empty($this->moduleSettings['windowsUser_jpegPhoto_maxWidth'][0]) || !empty($this->moduleSettings['windowsUser_jpegPhoto_maxHeight'][0])) { - $maxWidth = empty($this->moduleSettings['windowsUser_jpegPhoto_maxWidth'][0]) ? $imageManipulator->getWidth() : $this->moduleSettings['windowsUser_jpegPhoto_maxWidth'][0]; - $maxHeight = empty($this->moduleSettings['windowsUser_jpegPhoto_maxHeight'][0]) ? $imageManipulator->getHeight() : $this->moduleSettings['windowsUser_jpegPhoto_maxHeight'][0]; - $imageManipulator->thumbnail($maxWidth, $maxHeight); - } - $imageManipulator->convertToJpeg(); - $data = $imageManipulator->getImageData(); - } - catch (Exception $e) { - $msg = $this->messages['file'][2]; - $msg[] = htmlspecialchars($e->getMessage()); - $messages[] = $msg; - return $messages; - } - $this->attributes['jpegPhoto'][0] = $data; } - else { - $messages[] = $this->messages['file'][0]; + elseif (isset($_POST['webcamData'])) { + $data = $_POST['webcamData']; + $data = str_replace('data:image/png;base64,', '', $data); + $data = base64_decode($data); } + // convert to JPG + try { + include_once dirname(__FILE__) . '/../imageutils.inc'; + $imageManipulator = ImageManipulationFactory::getImageManipulator($data); + // resize if maximum values specified + if (!empty($this->moduleSettings['windowsUser_jpegPhoto_maxWidth'][0]) || !empty($this->moduleSettings['windowsUser_jpegPhoto_maxHeight'][0])) { + $maxWidth = empty($this->moduleSettings['windowsUser_jpegPhoto_maxWidth'][0]) ? $imageManipulator->getWidth() : $this->moduleSettings['windowsUser_jpegPhoto_maxWidth'][0]; + $maxHeight = empty($this->moduleSettings['windowsUser_jpegPhoto_maxHeight'][0]) ? $imageManipulator->getHeight() : $this->moduleSettings['windowsUser_jpegPhoto_maxHeight'][0]; + $imageManipulator->thumbnail($maxWidth, $maxHeight); + } + $imageManipulator->convertToJpeg(); + $data = $imageManipulator->getImageData(); + } + catch (Exception $e) { + $msg = $this->messages['file'][2]; + $msg[] = htmlspecialchars($e->getMessage()); + $messages[] = $msg; + return $messages; + } + $this->attributes['jpegPhoto'][0] = $data; return $messages; } From b00fd8d83e54a25bdceeebf62ab440bde57f32cd Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Fri, 24 Jul 2020 20:54:19 +0200 Subject: [PATCH 5/7] typo --- lam/lib/modules/windowsUser.inc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lam/lib/modules/windowsUser.inc b/lam/lib/modules/windowsUser.inc index e789b716..20cd6fb2 100644 --- a/lam/lib/modules/windowsUser.inc +++ b/lam/lib/modules/windowsUser.inc @@ -4379,7 +4379,7 @@ if (interface_exists('\LAM\JOB\Job', false)) { /** * Returns the month interval. * - * @param arry $options config options + * @param array $options config options * @param $jobId job id * @return int interval */ From e5344b0568a1e047742aaecb1182ada2c3e587a9 Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Fri, 24 Jul 2020 20:56:20 +0200 Subject: [PATCH 6/7] fixed test --- lam/tests/lib/persistenceTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lam/tests/lib/persistenceTest.php b/lam/tests/lib/persistenceTest.php index dbad924f..6a107dee 100644 --- a/lam/tests/lib/persistenceTest.php +++ b/lam/tests/lib/persistenceTest.php @@ -86,7 +86,7 @@ class ConfigDataExporterTest extends TestCase { $exporter = $this->getMockBuilder('\LAM\PERSISTENCE\ConfigDataExporter') ->setMethods(array('_getMainConfigData', '_getCertificates', '_getServerProfiles', '_getAccountProfiles', '_getAccountProfileTemplates', '_getPdfProfiles', - '_getPdfProfileTemplates', '_getSelfServiceProfiles')) + '_getPdfProfileTemplates', '_getSelfServiceProfiles', '_getWebauthn')) ->getMock(); $exporter->method('_getMainConfigData')->willReturn($mainData); $exporter->method('_getCertificates')->willReturn('certs'); From b3905d73ca081aa525b42cec593d6328e1dec960 Mon Sep 17 00:00:00 2001 From: Roland Gruber Date: Fri, 24 Jul 2020 21:00:36 +0200 Subject: [PATCH 7/7] fixed test --- lam/tests/lib/modules/windowsUserTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/lam/tests/lib/modules/windowsUserTest.php b/lam/tests/lib/modules/windowsUserTest.php index 4d109ae3..1e137bd2 100644 --- a/lam/tests/lib/modules/windowsUserTest.php +++ b/lam/tests/lib/modules/windowsUserTest.php @@ -80,6 +80,7 @@ use PHPUnit\Framework\TestCase; public function testWindowsManagedGroupsNotifyJob_getLastEffectiveExecutionDate() { if (!interface_exists('\LAM\JOB\Job', false)) { + $this->markTestSkipped(); return; } $resultLog = new \LAM\JOB\JobResultLog();