diff --git a/lam/HISTORY b/lam/HISTORY
index ccbf908e..45e6d39c 100644
--- a/lam/HISTORY
+++ b/lam/HISTORY
@@ -1,6 +1,7 @@
June 2018 6.4
- Imagick PHP extension required
- Passwords can be checked against external service (e.g. https://api.pwnedpasswords.com/range)
+ - Personal/Windows: image cropping support
- IMAP: create mailbox via file upload
- PHP 7.2 support
- LAM Pro:
diff --git a/lam/docs/manual-sources/chapter-modules.xml b/lam/docs/manual-sources/chapter-modules.xml
index 2f676ab8..eee95be9 100644
--- a/lam/docs/manual-sources/chapter-modules.xml
+++ b/lam/docs/manual-sources/chapter-modules.xml
@@ -354,11 +354,8 @@
The Personal module provides support for managing various personal
data of your users including mail addresses and telephone numbers. You
- can also add photos of your users (please install PHP
- Imagick/ImageMagick for full file format support). If you do not
- need to manage all attributes then you can deactivate them in your
- server profile.
+ can also add photos of your users. If you do not need to manage all
+ attributes then you can deactivate them in your server profile.
Configuration
diff --git a/lam/lib/modules/inetOrgPerson.inc b/lam/lib/modules/inetOrgPerson.inc
index fb48ea71..9ba8ef8f 100644
--- a/lam/lib/modules/inetOrgPerson.inc
+++ b/lam/lib/modules/inetOrgPerson.inc
@@ -56,6 +56,8 @@ class inetOrgPerson extends baseModule implements passwordService {
/** session variable for existing user certificates in self service */
const SESS_CERTIFICATES_LIST = 'inetOrgPerson_certificatesList';
+ /** session variable for existing user certificates in self service */
+ const SESS_PHOTO = 'inetOrgPerson_jpegPhoto';
/**
* This function fills the message array.
@@ -1648,7 +1650,7 @@ class inetOrgPerson extends baseModule implements passwordService {
$name = $_FILES['photoFile']['name'];
$extension = strtolower(substr($name, strpos($name, '.') + 1));
$handle = fopen($_FILES['photoFile']['tmp_name'], "r");
- $data = fread($handle, 10000000);
+ $data = fread($handle, 100000000);
if (!empty($this->moduleSettings['inetOrgPerson_jpegPhoto_maxSize'][0]) && (strlen($data) > (1024 * $this->moduleSettings['inetOrgPerson_jpegPhoto_maxSize'][0]))) {
$errMsg = $this->messages['file'][3];
$errMsg[] = null;
@@ -2740,31 +2742,18 @@ class inetOrgPerson extends baseModule implements passwordService {
);
}
if (in_array('jpegPhoto', $fields)) {
+ $_SESSION[self::SESS_PHOTO] = null;
if (isset($attributes['jpegPhoto'][0])) {
- $jpeg_filename = 'jpegPhoto' . session_id() . '.jpg';
- $outjpeg = fopen(realpath('../../') . '/tmp/' . $jpeg_filename, "wb");
- fwrite($outjpeg, $attributes['jpegPhoto'][0]);
- fclose ($outjpeg);
- $photoFile = '../../tmp/' . $jpeg_filename;
- $photoSub = new htmlTable();
- $img = new htmlImage($photoFile);
- $img->setCSSClasses(array('photo'));
- $photoSub->addElement($img, true);
- if (!in_array('jpegPhoto', $readOnlyFields)) {
- $photoSubSub = new htmlTable();
- $upload = new htmlInputFileUpload('photoFile');
- $upload->colspan = 2;
- $photoSubSub->addElement($upload, true);
- $photoSubSub->addElement(new htmlTableExtendedInputCheckbox('removeReplacePhoto', false, _('Remove/replace photo'), null, false));
- $photoSub->addElement($photoSubSub);
- }
- $return['jpegPhoto'] = new htmlResponsiveRow(new htmlOutputText($this->getSelfServiceLabel('jpegPhoto', _('Photo'))), $photoSub);
+ $_SESSION[self::SESS_PHOTO] = $attributes['jpegPhoto'][0];
}
- elseif (!in_array('jpegPhoto', $readOnlyFields)) {
- $photoSub = new htmlTable();
- $photoSub->addElement(new htmlTableExtendedInputFileUpload('photoFile', _('Add photo')));
- $photoSub->addElement(new htmlHiddenInput('addPhoto', 'true'));
- $return['jpegPhoto'] = new htmlResponsiveRow(new htmlOutputText($this->getSelfServiceLabel('jpegPhoto', _('Photo'))), $photoSub);
+ $readOnlyPhoto = in_array('jpegPhoto', $readOnlyFields);
+ if (!empty($attributes['jpegPhoto'][0]) || !$readOnlyPhoto) {
+ $photoSub = new htmlDiv('inetOrgPersonPhotoUploadContent', $this->getSelfServicePhoto($readOnlyPhoto, false));
+ $photoRow = new htmlResponsiveRow();
+ $photoRow->add($this->getSelfServicePhotoJS($readOnlyPhoto), 0);
+ $photoRow->addLabel(new htmlOutputText($this->getSelfServiceLabel('jpegPhoto', _('Photo'))));
+ $photoRow->addField(new htmlDiv('jpegPhotoDiv', $photoSub));
+ $return['jpegPhoto'] = $photoRow;
}
}
if (in_array('departmentNumber', $fields)) {
@@ -2822,7 +2811,6 @@ class inetOrgPerson extends baseModule implements passwordService {
// upload status
$uploadStatus = new htmlDiv('inetOrgPerson_upload_status_cert', new htmlOutputText(''));
$uploadStatus->setCSSClasses(array('qq-upload-list'));
- $uploadStatus->colspan = 7;
$certTable->add($uploadStatus, 12);
$certLabel = new htmlOutputText($this->getSelfServiceLabel('userCertificate', _('User certificates')));
$return['userCertificate'] = new htmlResponsiveRow($certLabel, $certTable);
@@ -2926,6 +2914,110 @@ class inetOrgPerson extends baseModule implements passwordService {
return $return;
}
+ /**
+ * Renders the photo area for self service.
+ *
+ * @param boolean $readOnly content is read-only
+ * @param boolean $crop enable cropping
+ * @return htmlResponsiveRow content
+ */
+ private function getSelfServicePhoto($readOnly, $crop) {
+ $photo = $_SESSION[self::SESS_PHOTO];
+ $row = new htmlResponsiveRow();
+ if (!empty($photo)) {
+ $jpeg_filename = 'jpegPhoto' . getRandomNumber() . '.jpg';
+ $outjpeg = fopen(realpath('../../') . '/tmp/' . $jpeg_filename, "wb");
+ fwrite($outjpeg, $photo);
+ fclose ($outjpeg);
+ $photoFile = '../../tmp/' . $jpeg_filename;
+ $img = new htmlImage($photoFile);
+ $img->setCSSClasses(array('photo'));
+ if ($crop) {
+ $img->enableCropping();
+ }
+ $row->add($img, 12);
+ if (!$readOnly) {
+ $row->addVerticalSpacer('0.5rem');
+ $deleteButton = new htmlLink(_('Delete'), '#', '../../graphics/delete.png', true);
+ $deleteButton->setOnClick('inetOrgPersonDeletePhoto(); return false;');
+ $row->add($deleteButton, 12);
+ }
+ $row->addVerticalSpacer('0.5rem');
+ }
+ // upload button
+ $row->add(new htmlDiv('inetOrgPersonPhotoUploadId', new htmlOutputText('')), 12);
+ $row->add(new htmlJavaScript('inetOrgPersonUploadPhoto(\'inetOrgPersonPhotoUploadId\');'), 0);
+ $uploadStatus = new htmlDiv('inetOrgPersonPhotoUploadStatus', new htmlOutputText(''));
+ $uploadStatus->setCSSClasses(array('qq-upload-list'));
+ $row->add($uploadStatus, 12);
+ return $row;
+ }
+
+ /**
+ * Returns the Java Script functions to manage the photo.
+ *
+ * @param boolean $readOnly content is read-only
+ * @return htmlJavaScript JS block
+ */
+ private static function getSelfServicePhotoJS($readOnly) {
+ if ($readOnly) {
+ return new htmlGroup();
+ }
+ $content = '
+ function inetOrgPersonUploadPhoto(elementID) {
+ var uploadStatus = document.getElementById(\'inetOrgPersonPhotoUploadStatus\');
+ var params = { action: \'ajaxPhotoUpload\' };
+ params["' . getSecurityTokenName() . '"] = "' . getSecurityTokenValue() . '";
+ var uploader = new qq.FineUploader({
+ element: document.getElementById(elementID),
+ listElement: uploadStatus,
+ request: {
+ endpoint: \'../misc/ajax.php?selfservice=1&module=inetOrgPerson&scope=user'
+ . '&' . getSecurityTokenName() . '=' . getSecurityTokenValue() . '\',
+ forceMultipart: true,
+ params: params
+ },
+ multiple: false,
+ callbacks: {
+ onComplete: function(id, fileName, data) {
+ if (data.success) {
+ if (data.html) {
+ jQuery(\'#inetOrgPersonPhotoUploadContent\').html(data.html);
+ }
+ }
+ else {
+ alert(data.error);
+ }
+ }
+ }
+ });
+ }
+
+ function inetOrgPersonDeletePhoto(id) {
+ var actionJSON = {
+ "action": "deletePhoto",
+ "id": id
+ };
+ var data = {jsonInput: actionJSON};
+ data["' . getSecurityTokenName() . '"] = "' . getSecurityTokenValue() . '";
+ jQuery.post(\'../misc/ajax.php?selfservice=1&module=inetOrgPerson&scope=user\',
+ data, function(data) {inetOrgPersonDeletePhotoHandleReply(data);}, \'json\');
+ }
+
+ function inetOrgPersonDeletePhotoHandleReply(data) {
+ if (data.errorsOccured == "false") {
+ jQuery(\'#inetOrgPersonPhotoUploadContent\').html(data.html);
+ }
+ else {
+ alert(data.errormessage);
+ }
+ }
+
+ ';
+ return new htmlJavaScript($content);
+ }
+
+
/**
* Returns the meta HTML code to display the certificate area.
* This also includes the file upload.
@@ -2986,7 +3078,7 @@ class inetOrgPerson extends baseModule implements passwordService {
$content = '
function inetOrgPersonDeleteCertificate(id) {
var actionJSON = {
- "action": "delete",
+ "action": "deleteCert",
"id": id
};
var data = {jsonInput: actionJSON};
@@ -3243,39 +3335,40 @@ class inetOrgPerson extends baseModule implements passwordService {
}
// photo
if (in_array('jpegPhoto', $fields) && !in_array('jpegPhoto', $readOnlyFields)) {
+ $data = $_SESSION[self::SESS_PHOTO];
// remove photo
- if (isset($_POST['removeReplacePhoto']) && ($_POST['removeReplacePhoto'] == 'on')
- && (empty($_FILES['replacePhotoFile']) || ($_FILES['replacePhotoFile']['size'] == 0))) {
+ if (!empty($attributes['jpegPhoto'][0]) && empty($data)) {
$return['mod']['jpegPhoto'] = array();
}
// set/replace photo
- if (isset($_FILES['photoFile']) && ($_FILES['photoFile']['size'] > 0)) {
+ elseif (!empty($data) && (empty($attributes['jpegPhoto'][0]) || ($data != $attributes['jpegPhoto'][0]))) {
$moduleSettings = $this->selfServiceSettings->moduleSettings;
- if (!empty($moduleSettings['inetOrgPerson_jpegPhoto_maxSize'][0]) && ($moduleSettings['inetOrgPerson_jpegPhoto_maxSize'][0] < ($_FILES['photoFile']['size'] / 1024))) {
- $msg = $this->messages['file'][3];
- $msg[] = null;
- $msg[] = htmlspecialchars($moduleSettings['inetOrgPerson_jpegPhoto_maxSize'][0]);
- $return['messages'][] = $msg;
- }
- else {
- $handle = fopen($_FILES['photoFile']['tmp_name'], "r");
- $data = fread($handle, 100000000);
- fclose($handle);
- try {
- $data = inetOrgPerson::resizeAndConvertImage($data, $moduleSettings);
- }
- catch (Exception $e) {
- $msg = $this->messages['file'][2];
- $msg[] = htmlspecialchars($e->getMessage());
+ try {
+ $image = new Imagick();
+ $image->readimageblob($data);
+ $image->cropimage($_POST['croppingDataWidth'], $_POST['croppingDataHeight'], $_POST['croppingDataX'], $_POST['croppingDataY']);
+ $data = $image->getimageblob();
+ $data = inetOrgPerson::resizeAndConvertImage($data, $moduleSettings);
+ if (!empty($moduleSettings['inetOrgPerson_jpegPhoto_maxSize'][0]) && ($moduleSettings['inetOrgPerson_jpegPhoto_maxSize'][0] < (strlen($data) / 1024))) {
+ $msg = $this->messages['file'][3];
+ $msg[] = null;
+ $msg[] = htmlspecialchars($moduleSettings['inetOrgPerson_jpegPhoto_maxSize'][0]);
$return['messages'][] = $msg;
}
- if (isset($_POST['removeReplacePhoto']) && ($_POST['removeReplacePhoto'] == 'on')) {
- $return['mod']['jpegPhoto'][0] = $data;
- }
- elseif (isset($_POST['addPhoto'])) {
- $return['add']['jpegPhoto'][0] = $data;
+ else {
+ if (!empty($attributes['jpegPhoto'][0])) {
+ $return['mod']['jpegPhoto'][0] = $data;
+ }
+ else {
+ $return['add']['jpegPhoto'][0] = $data;
+ }
}
}
+ catch (Exception $e) {
+ $msg = $this->messages['file'][2];
+ $msg[] = htmlspecialchars($e->getMessage());
+ $return['messages'][] = $msg;
+ }
}
}
// departments
@@ -3408,23 +3501,30 @@ class inetOrgPerson extends baseModule implements passwordService {
public function handleAjaxRequest() {
// AJAX uploads are non-JSON
if (isset($_GET['action']) && ($_GET['action'] == 'ajaxCertUpload')) {
- $this->ajaxUpload();
+ $this->ajaxUploadCert();
+ return;
+ }
+ if (isset($_GET['action']) && ($_GET['action'] == 'ajaxPhotoUpload')) {
+ $this->ajaxUploadPhoto();
return;
}
$jsonInput = $_POST['jsonInput'];
$jsonReturn = self::invalidAjaxRequest();
if (isset($jsonInput['action'])) {
- if ($jsonInput['action'] == 'delete') {
+ if ($jsonInput['action'] == 'deleteCert') {
$jsonReturn = $this->ajaxDeleteSelfServiceUserCertificate($jsonInput);
}
+ elseif ($jsonInput['action'] == 'deletePhoto') {
+ $jsonReturn = $this->ajaxDeleteSelfServicePhoto($jsonInput);
+ }
}
echo json_encode($jsonReturn);
}
/**
- * Handles an AJAX file upload and prints the JSON result.
+ * Handles an AJAX certificate file upload and prints the JSON result.
*/
- private function ajaxUpload() {
+ private function ajaxUploadCert() {
$result = array('success' => true);
if (!isset($_FILES['qqfile']) || ($_FILES['qqfile']['size'] < 100)) {
$result = array('error' => _('No file received.'));
@@ -3458,6 +3558,65 @@ class inetOrgPerson extends baseModule implements passwordService {
echo json_encode($result);
}
+ /**
+ * Handles an AJAX photo file upload and prints the JSON result.
+ */
+ private function ajaxUploadPhoto() {
+ $result = array('success' => true);
+ if (!isset($_FILES['qqfile']) || ($_FILES['qqfile']['size'] < 100)) {
+ $result = array('error' => _('No file received.'));
+ }
+ else {
+ $handle = fopen($_FILES['qqfile']['tmp_name'], "r");
+ $data = fread($handle, 100000000);
+ fclose($handle);
+ $image = new Imagick();
+ try {
+ $image->readImageBlob($data);
+ $image->setImageCompression(Imagick::COMPRESSION_JPEG);
+ $image->setImageFormat('jpeg');
+ $data = $image->getimageblob();
+ }
+ catch (Exception $e) {
+ $result = array('success' => false, 'error' => htmlspecialchars($e->getMessage()));
+ echo json_encode($result);
+ return;
+ }
+ $_SESSION[inetOrgPerson::SESS_PHOTO] = $data;
+ ob_start();
+ $contentElement = $this->getSelfServicePhoto(false, true);
+ ob_end_clean();
+ ob_start();
+ $tabindex = 999;
+ parseHtml(null, $contentElement, array(), true, $tabindex, $this->get_scope());
+ $content = ob_get_contents();
+ ob_end_clean();
+ $result['html'] = $content;
+ }
+ echo json_encode($result);
+ }
+
+ /**
+ * Manages the deletion of a photo.
+ *
+ * @param array $data JSON data
+ */
+ private function ajaxDeleteSelfServicePhoto($data) {
+ $_SESSION[self::SESS_PHOTO] = null;
+ ob_start();
+ $contentElement = $this->getSelfServicePhoto(false, false);
+ ob_end_clean();
+ ob_start();
+ $tabindex = 999;
+ parseHtml(null, $contentElement, array(), true, $tabindex, $this->get_scope());
+ $content = ob_get_contents();
+ ob_end_clean();
+ return array(
+ 'errorsOccured' => 'false',
+ 'html' => $content,
+ );
+ }
+
/**
* Manages the deletion of a certificate.
*