From 237d1d809eb636ef6205c18cad1330eb462b5612 Mon Sep 17 00:00:00 2001 From: Nathan Gray Date: Mon, 11 May 2015 17:29:31 +0000 Subject: [PATCH 01/31] * If column information is stored in a favorite, restore it along with the filters To get column information in the favorite, change the visible columns before you create the favorite. If the favorite has no column information, the visible columns will not be changed. --- etemplate/js/et2_extension_nextmatch.js | 62 +++++++++++++++++++++++++ infolog/inc/class.infolog_ui.inc.php | 3 +- infolog/js/app.js | 9 +++- phpgwapi/js/jsapi/app_base.js | 5 ++ 4 files changed, 76 insertions(+), 3 deletions(-) diff --git a/etemplate/js/et2_extension_nextmatch.js b/etemplate/js/et2_extension_nextmatch.js index a43793d7db..570cdeaf7b 100644 --- a/etemplate/js/et2_extension_nextmatch.js +++ b/etemplate/js/et2_extension_nextmatch.js @@ -1436,6 +1436,68 @@ var et2_nextmatch = et2_DOMWidget.extend([et2_IResizeable, et2_IInput, et2_IPrin .css("left", s_position.left + this.div.width() - this.selectPopup.width()); }, + /** + * Set the currently displayed columns, without updating user's preference + * + * @param {string[]} column_list List of column names + * @param {boolean} trigger_update=false - explicitly trigger an update + */ + set_columns: function(column_list, trigger_update) + { + var columnMgr = this.dataview.getColumnMgr(); + var visibility = {}; + + // Initialize to false + for (var i = 0; i < columnMgr.columns.length; i++) + { + var col = columnMgr.columns[i]; + if(col.caption && col.visibility != ET2_COL_VISIBILITY_ALWAYS_NOSELECT ) + { + visibility[col.id] = {visible: false}; + } + } + for(var i = 0; i < this.columns.length; i++) + { + + var widget = this.columns[i].widget; + var colName = this._getColumnName(widget); + if(column_list.indexOf(colName) !== -1) + { + visibility[columnMgr.columns[i].id].visible = true; + } + // Custom fields are listed seperately in column list, but are only 1 column + if(widget && widget.instanceOf(et2_nextmatch_customfields)) { + + // Just the ID for server side, not the whole nm name - some apps use it to skip custom fields + colName = widget.id; + if(column_list.indexOf(colName) !== -1) + { + visibility[columnMgr.columns[i].id].visible = true; + } + + var cf = this.columns[i].widget.options.customfields; + var visible = this.columns[i].widget.options.fields; + + // Turn off all custom fields + for(var field_name in cf) + { + visible[field_name] = false; + } + // Turn on selected custom fields - start from 0 in case they're not in order + for(var j = 0; j < column_list.length; j++) + { + if(column_list[j].indexOf(et2_customfields_list.prototype.prefix) != 0) continue; + visible[column_list[j].substring(1)] = true; + } + widget.set_visible(visible); + } + } + columnMgr.setColumnVisibilitySet(visibility); + + // We don't want to update user's preference, so directly update + this.dataview._updateColumns(); + }, + /** * Set the letter search preference, and update the UI * diff --git a/infolog/inc/class.infolog_ui.inc.php b/infolog/inc/class.infolog_ui.inc.php index b84f5bf1b4..30e179a44b 100644 --- a/infolog/inc/class.infolog_ui.inc.php +++ b/infolog/inc/class.infolog_ui.inc.php @@ -422,8 +422,7 @@ class infolog_ui $columselection = $this->prefs[$columnselection_pref]; - //_debug_array($columselection); - if ($columselection) + if (!$query['selectcols'] && $columselection) { $columselection = is_array($columselection) ? $columselection : explode(',',$columselection); } diff --git a/infolog/js/app.js b/infolog/js/app.js index 8cf7a2e217..7717e52358 100644 --- a/infolog/js/app.js +++ b/infolog/js/app.js @@ -213,7 +213,12 @@ app.classes.infolog = AppJS.extend( { // Show / hide descriptions this.show_details(filter2.value == 'all', nm.getDOMNode(nm)); + } + // Only change columns for a real user event, to avoid interfering with + // favorites + if (nm && filter2 && !nm.update_in_progress) + { // Store selection as implicit preference egw.set_preference('infolog', nm.options.settings.columnselection_pref.replace('-details','')+'-details-pref', filter2.value); @@ -223,6 +228,8 @@ app.classes.infolog = AppJS.extend( // Load new preferences var colData = nm.columns.slice(); for(var i = 0; i < nm.columns.length; i++) colData[i].disabled=false; + + nm.set_columns(egw.preference(nm.options.settings.columnselection_pref,'infolog').split(',')); nm._applyUserPreferences(nm.columns, colData); // Now apply them to columns @@ -231,7 +238,7 @@ app.classes.infolog = AppJS.extend( nm.dataview.getColumnMgr().columns[i].set_width(colData[i].width); nm.dataview.getColumnMgr().columns[i].set_visibility(!colData[i].disabled); } - nm.dataview.getColumnMgr().updated = true; + nm.dataview.getColumnMgr().updated = true; // Update page nm.dataview.updateColumns(); } diff --git a/phpgwapi/js/jsapi/app_base.js b/phpgwapi/js/jsapi/app_base.js index dc96bb9586..8214fd4f73 100644 --- a/phpgwapi/js/jsapi/app_base.js +++ b/phpgwapi/js/jsapi/app_base.js @@ -291,6 +291,11 @@ var AppJS = Class.extend( { _widget.sortBy(state.state.sort.id, state.state.sort.asc,false); } + if(state.state && state.state.selectcols) + { + // Make sure it's a real array, not an object, then set cols + _widget.set_columns(jQuery.extend([],state.state.selectcols)); + } _widget.applyFilters(state.state || state.filter || {}); nextmatched = true; }, this, et2_nextmatch); From 652cec5463146f6b665993ea60f8bcfc0c39768d Mon Sep 17 00:00:00 2001 From: Nathan Gray Date: Mon, 11 May 2015 19:33:57 +0000 Subject: [PATCH 02/31] Fix drag and drop multiple files into a subdirectory didn't get all files to the right path --- etemplate/js/et2_widget_file.js | 4 ++-- filemanager/js/app.js | 9 +++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/etemplate/js/et2_widget_file.js b/etemplate/js/et2_widget_file.js index beeeaffd29..3cf1e6349e 100644 --- a/etemplate/js/et2_widget_file.js +++ b/etemplate/js/et2_widget_file.js @@ -452,9 +452,9 @@ var et2_file = et2_inputWidget.extend( var file_count = this.resumable.files.length; // Remove files from list - for(var i = 0; i < this.resumable.files.length; i++) + while(this.resumable.files.length > 0) { - this.resumable.removeFile(this.resumable.files[i]); + this.resumable.removeFile(this.resumable.files[this.resumable.files.length -1]); } var event = jQuery.Event('upload'); diff --git a/filemanager/js/app.js b/filemanager/js/app.js index 9fceb793ee..befe3bd23c 100644 --- a/filemanager/js/app.js +++ b/filemanager/js/app.js @@ -830,12 +830,17 @@ app.classes.filemanager = AppJS.extend( var widget = this.et2.getWidgetById('upload'); // Override finish to specify a potentially different path - var old_onfinish = widget.options.onFinishOne; + var old_onfinishone = widget.options.onFinishOne; + var old_onfinish = widget.options.onFinish; widget.options.onFinishOne = function(_event, _file_count) { - widget.options.onFinishOne = old_onfinish; self.upload(_event, _file_count, path); }; + + widget.options.onFinish = function() { + widget.options.onFinish = old_onfinish; + widget.options.onFinishOne = old_onfinishone; + } // This triggers the upload widget.set_value(files); From d599cadf8aaf6defb1db7495198dfabc19270cda Mon Sep 17 00:00:00 2001 From: Nathan Gray Date: Mon, 11 May 2015 21:03:29 +0000 Subject: [PATCH 03/31] Pre-set contact in new infologs opened from context menu. Contact taken from link filter, or current contact when in CRM view. --- infolog/inc/class.infolog_ui.inc.php | 3 +-- infolog/js/app.js | 32 ++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/infolog/inc/class.infolog_ui.inc.php b/infolog/inc/class.infolog_ui.inc.php index 30e179a44b..f9cebf3539 100644 --- a/infolog/inc/class.infolog_ui.inc.php +++ b/infolog/inc/class.infolog_ui.inc.php @@ -1044,8 +1044,7 @@ class infolog_ui 'icon' => $type, ); $types_add[$type] = $data + array( - 'url' => 'menuaction=infolog.infolog_ui.edit&type='.$type, - 'popup' => egw_link::get_registry('infolog', 'add_popup'), + 'onExecute' => "javaScript:app.infolog.add_action_handler" ); } diff --git a/infolog/js/app.js b/infolog/js/app.js index 7717e52358..6596c01d35 100644 --- a/infolog/js/app.js +++ b/infolog/js/app.js @@ -488,6 +488,27 @@ app.classes.infolog = AppJS.extend( egw.open('','infolog','add'); }, + /** + * Wrapper so add -> New actions in the context menu can pass current + * filter values into new edit dialog + * + * @see add_with_extras + * + * @param {egwAction} action + * @param {egwActionObject[]} selected + */ + add_action_handler: function(action, selected) + { + var nm = action.getManager().data.nextmatch || false; + if(nm) + { + this.add_with_extras(nm,action.id, + nm.getArrayMgr('content').getEntry('action'), + nm.getArrayMgr('content').getEntry('action_id') + ); + } + }, + /** * Opens a new edit dialog with some extra url parameters pulled from * standard locations. Done with a function instead of hardcoding so @@ -514,6 +535,17 @@ app.classes.infolog = AppJS.extend( // Need a real array here action_id = jQuery.map(action_id,function(val) {return val;}); } + + // No action? Try the linked filter, in case it's set + if(!_action && !_action_id) + { + if(nm_value.col_filter && nm_value.col_filter.linked) + { + var split = nm_value.col_filter.linked.split(':') || ''; + _action = split[0] || ''; + action_id = split[1] || ''; + } + } var extras = { type: _type || nm_value.filter || "", cat_id: nm_value.cat_id || "", From b6235ba024a4eeea8a7cb67a5b20b295bac80343 Mon Sep 17 00:00:00 2001 From: Klaus Leithoff Date: Tue, 12 May 2015 09:06:08 +0000 Subject: [PATCH 04/31] reenabling the observance of the preference setting for forward as attachments; add notice to body that forwarded messagebody is found in attachments; add folder info in getAttachment calls when available --- mail/inc/class.mail_activesync.inc.php | 15 ++++++++------- mail/lang/egw_de.lang | 1 + mail/lang/egw_en.lang | 1 + 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/mail/inc/class.mail_activesync.inc.php b/mail/inc/class.mail_activesync.inc.php index 6689608478..0498c6411b 100644 --- a/mail/inc/class.mail_activesync.inc.php +++ b/mail/inc/class.mail_activesync.inc.php @@ -676,8 +676,6 @@ class mail_activesync implements activesync_plugin_write, activesync_plugin_send // receive entire mail (header + body) // get message headers for specified message $headers = $this->mail->getMessageEnvelope($uid, $_partID, true, $folder); - // do forwarding as inline, until we understand why asmail fails - $preferencesArray['message_forwarding']='inline'; // build a new mime message, forward entire old mail as file if ($preferencesArray['message_forwarding'] == 'asmail') { @@ -685,6 +683,9 @@ class mail_activesync implements activesync_plugin_write, activesync_plugin_send $rawHeader = $this->mail->getMessageRawHeader($smartdata['itemid'], $_partID,$folder); $rawBody = $this->mail->getMessageRawBody($smartdata['itemid'], $_partID,$folder); $mailObject->AddStringAttachment($rawHeader.$rawBody, $headers['SUBJECT'].'.eml', 'message/rfc822'); + $AltBody = $AltBody."
".lang("See Attachments for Content of the Orignial Mail").$sigTextHtml; + $Body = $Body."\r\n".lang("See Attachments for Content of the Orignial Mail").$sigTextPlain; + $isforward = true; } else { @@ -1092,7 +1093,7 @@ class mail_activesync implements activesync_plugin_write, activesync_plugin_send break; default: $attachmentData = ''; - $attachmentData = $this->mail->getAttachment($id, $attachment['partID'],0,false); + $attachmentData = $this->mail->getAttachment($id, $attachment['partID'],0,false,false,$_folderName); $mailObject->AddStringAttachment($attachmentData['attachment'], $mailObject->EncodeHeader($attachment['name']), 'base64', $attachment['mimeType']); break; } @@ -1215,7 +1216,7 @@ class mail_activesync implements activesync_plugin_write, activesync_plugin_send // pass meeting requests to calendar plugin if (strtolower($attach['mimeType']) == 'text/calendar' && strtolower($attach['method']) == 'request' && isset($GLOBALS['egw_info']['user']['apps']['calendar']) && - ($attachment = $this->mail->getAttachment($id, $attach['partID'],0,false)) && + ($attachment = $this->mail->getAttachment($id, $attach['partID'],0,false,false,$_folderName)) && ($output->meetingrequest = calendar_activesync::meetingRequest($attachment['attachment']))) { $output->messageclass = "IPM.Schedule.Meeting.Request"; @@ -1316,7 +1317,7 @@ class mail_activesync implements activesync_plugin_write, activesync_plugin_send */ function GetAttachmentData($fid,$attname) { debugLog("getAttachmentData: $fid (attname: '$attname')"); - //error_log(__METHOD__.__LINE__." Fid: $fid (attname: '$attname')"); + error_log(__METHOD__.__LINE__." Fid: $fid (attname: '$attname')"); list($folderid, $id, $part) = explode(":", $attname); $this->splitID($folderid, $account, $folder); @@ -1324,7 +1325,7 @@ class mail_activesync implements activesync_plugin_write, activesync_plugin_send if (!isset($this->mail)) $this->mail = mail_bo::getInstance(false,self::$profileID,true,false,true); $this->mail->reopen($folder); - $attachment = $this->mail->getAttachment($id,$part,0,false); + $attachment = $this->mail->getAttachment($id,$part,0,false,false,$folder); print $attachment['attachment']; unset($attachment); return true; @@ -1350,7 +1351,7 @@ class mail_activesync implements activesync_plugin_write, activesync_plugin_send if (!isset($this->mail)) $this->mail = mail_bo::getInstance(false, self::$profileID,true,false,true); $this->mail->reopen($folder); - $att = $this->mail->getAttachment($id,$part,0,false); + $att = $this->mail->getAttachment($id,$part,0,false,false,$folder); $attachment = new SyncAirSyncBaseFileAttachment(); /* debugLog(__METHOD__.__LINE__.array2string($att)); diff --git a/mail/lang/egw_de.lang b/mail/lang/egw_de.lang index 63ac6820fc..8c7dcb6925 100644 --- a/mail/lang/egw_de.lang +++ b/mail/lang/egw_de.lang @@ -373,6 +373,7 @@ save: mail de Speichern saving of message %1 failed. destination folder %2 does not exist. mail de Speichern der Nachricht %1 ist fehlgeschlagen. Ziel Ordner %2 existiert nicht. saving of message %1 succeeded. check folder %2. mail de Speichern der Nachricht %1 war erfolgreich. Prüfen Sie den Ziel Ordner %2 saving the rule failed: mail de Speichern der Regel ist fehlgeschlagen: +see attachments for content of the orignial mail mail de Die Original Nachricht wurde als Anhang an diese e-Mail angehängt select all mail de Alle Auswählen select an existing entry in order to append mail content to it mail de Bestehendes Ticket auswählen, zu dem die Mail als Kommentar hinzugefügt werden soll. select file to attach to message mail de Wählen Sie die Dateien aus, die Sie an diese Nachricht anhängen möchten. diff --git a/mail/lang/egw_en.lang b/mail/lang/egw_en.lang index 45d9e4f76e..47f643d2c1 100644 --- a/mail/lang/egw_en.lang +++ b/mail/lang/egw_en.lang @@ -373,6 +373,7 @@ save: mail en Save: saving of message %1 failed. destination folder %2 does not exist. mail en Saving of message %1 failed. Destination Folder %2 does not exist. saving of message %1 succeeded. check folder %2. mail en Saving of message %1 succeeded. Check Folder %2. saving the rule failed: mail en Saving the rule failed: +see attachments for content of the orignial mail mail en See Attachments for Content of the Orignial Mail select all mail en Select all select an existing entry in order to append mail content to it mail en Select an existing entry in order to append mail content to it select file to attach to message mail en Select file to attach to message From ebd36ab82efe1fb007cf0f9501ca05a64961e607 Mon Sep 17 00:00:00 2001 From: Klaus Leithoff Date: Tue, 12 May 2015 12:23:36 +0000 Subject: [PATCH 05/31] simplyfy attachment loops, as we do not have to distinguish between attachments and attached message/rfc anymore --- mail/inc/class.mail_activesync.inc.php | 39 +++++--------------------- 1 file changed, 7 insertions(+), 32 deletions(-) diff --git a/mail/inc/class.mail_activesync.inc.php b/mail/inc/class.mail_activesync.inc.php index 0498c6411b..1d974d058a 100644 --- a/mail/inc/class.mail_activesync.inc.php +++ b/mail/inc/class.mail_activesync.inc.php @@ -732,21 +732,10 @@ class mail_activesync implements activesync_plugin_write, activesync_plugin_send { if ($this->debugLevel>0) debugLog(__METHOD__.__LINE__.' Key:'.$key.'->'.array2string($attachment)); $attachmentNames .= $attachment['name']."\n"; - switch(strtoupper($attachment['mimeType'])) - { - case 'MESSAGE/RFC822': - $rawHeader = $rawBody = ''; - $rawHeader = $this->mail->getMessageRawHeader($uid, $attachment['partID'],$folder); - $rawBody = $this->mail->getMessageRawBody($uid, $attachment['partID'],$folder); - $mailObject->AddStringAttachment($rawHeader.$rawBody, $mailObject->EncodeHeader($attachment['name']), 'message/rfc822'); - break; - default: - $attachmentData = ''; - $attachmentData = $this->mail->getAttachment($uid, $attachment['partID'],0,false,false,$folder); - /*$x =*/ $mailObject->AddStringAttachment($attachmentData['attachment'], $mailObject->EncodeHeader($attachment['name']), $attachment['mimeType']); - //debugLog(__METHOD__.__LINE__.' added part with number:'.$x); - break; - } + $attachmentData = ''; + $attachmentData = $this->mail->getAttachment($uid, $attachment['partID'],0,false,false,$folder); + /*$x =*/ $mailObject->AddStringAttachment($attachmentData['attachment'], $mailObject->EncodeHeader($attachment['name']), $attachment['mimeType']); + //debugLog(__METHOD__.__LINE__.' added part with number:'.$x); } } } @@ -1080,23 +1069,9 @@ class mail_activesync implements activesync_plugin_write, activesync_plugin_send foreach((array)$attachments as $key => $attachment) { if ($this->debugLevel>0) debugLog(__METHOD__.__LINE__.' Key:'.$key.'->'.array2string($attachment)); - switch($attachment['type']) - { - case 'MESSAGE/RFC822': - $rawHeader = $rawBody = ''; - if (isset($attachment['partID'])) - { - $rawHeader = $this->mail->getMessageRawHeader($id, $attachment['partID'],$_folderName); - } - $rawBody = $this->mail->getMessageRawBody($id, $attachment['partID'],$_folderName); - $mailObject->AddStringAttachment($rawHeader.$rawBody, $mailObject->EncodeHeader($attachment['name']), '7bit', 'message/rfc822'); - break; - default: - $attachmentData = ''; - $attachmentData = $this->mail->getAttachment($id, $attachment['partID'],0,false,false,$_folderName); - $mailObject->AddStringAttachment($attachmentData['attachment'], $mailObject->EncodeHeader($attachment['name']), 'base64', $attachment['mimeType']); - break; - } + $attachmentData = ''; + $attachmentData = $this->mail->getAttachment($id, $attachment['partID'],0,false,false,$_folderName); + $mailObject->AddStringAttachment($attachmentData['attachment'], $mailObject->EncodeHeader($attachment['name']), $attachment['mimeType']); } } From 8f20e57599966fb32e2ecfcdccba16352dfa9470 Mon Sep 17 00:00:00 2001 From: Hadi Nategh Date: Tue, 12 May 2015 13:21:08 +0000 Subject: [PATCH 06/31] Make sure the popup has value and not false, the mail integration hooks may not be registered yet --- mail/js/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mail/js/app.js b/mail/js/app.js index bc10e7843f..23c20b0f34 100644 --- a/mail/js/app.js +++ b/mail/js/app.js @@ -2630,7 +2630,7 @@ app.classes.mail = AppJS.extend( if (typeof _action.data != 'undefined' ) { - if (typeof _action.data.popup != 'undefined') w_h = _action.data.popup.split('x'); + if (typeof _action.data.popup != 'undefined' && _action.data.popup) w_h = _action.data.popup.split('x'); if (typeof _action.data.mail_import != 'undefined') var mail_import_hook = _action.data.mail_import; } From e3bfbeeee96b9ab6b3399ed5bf501177108be8b5 Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Tue, 12 May 2015 14:57:29 +0000 Subject: [PATCH 07/31] using exception / exit code 92 for "Domain XXX does NOT exist !!!" --- setup/inc/class.setup_cmd.inc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup/inc/class.setup_cmd.inc.php b/setup/inc/class.setup_cmd.inc.php index 05b3e88718..2dc7cfe09a 100644 --- a/setup/inc/class.setup_cmd.inc.php +++ b/setup/inc/class.setup_cmd.inc.php @@ -273,7 +273,7 @@ abstract class setup_cmd extends admin_cmd $domains = $GLOBALS['egw_domain']; if ($domain) // domain to check given { - if (!isset($GLOBALS['egw_domain'][$domain])) throw new egw_exception_wrong_userinput(lang("Domain '%1' does NOT exist !!!",$domain)); + if (!isset($GLOBALS['egw_domain'][$domain])) throw new egw_exception_wrong_userinput(lang("Domain '%1' does NOT exist !!!",$domain), 92); $domains = array($domain => $GLOBALS['egw_domain'][$domain]); } From d571dffd2027e37b50e47351d6a0e0c266a1fbf4 Mon Sep 17 00:00:00 2001 From: Hadi Nategh Date: Wed, 13 May 2015 15:01:30 +0000 Subject: [PATCH 08/31] New approach to history widget resize, considering if the history tab is not active and window is resized --- etemplate/js/et2_widget_historylog.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/etemplate/js/et2_widget_historylog.js b/etemplate/js/et2_widget_historylog.js index bdaee1ebe1..8c126d98ae 100644 --- a/etemplate/js/et2_widget_historylog.js +++ b/etemplate/js/et2_widget_historylog.js @@ -96,6 +96,15 @@ var et2_historylog = et2_valueWidget.extend([et2_IDataProvider,et2_IResizeable], // Bind the action to when the tab is selected var handler = function(e) { e.data.div.unbind("click.history"); + // Bind on click tap, because we need to update history size + // after a rezise happend and history log was not the active tab + e.data.div.bind("click.history",{"history": e.data.history, div: tabs.tabData[i].flagDiv}, function(e){ + e.data.history.dynheight.update(function(_w, _h) + { + e.data.history.dataview.resize(_w, _h); + }); + }); + e.data.history.finishInit(); e.data.history.dynheight.update(function(_w, _h) { e.data.history.dataview.resize(_w, _h); @@ -556,7 +565,15 @@ var et2_historylog = et2_valueWidget.extend([et2_IDataProvider,et2_IResizeable], _height = (this.options.resize_ratio != '')? _height * this.options.resize_ratio: _height; if (_height != 0) { + // 250px is the default value for history widget + // if it's not loaded yet and window is resized + // then add the default height with excess_height + if (this.div.height() == 0) _height += 250; this.div.height(this.div.height() + _height); + + // trigger the history registered resize + // in order to update the height with new value + this.div.trigger('resize.' +this.options.value.app + this.options.value.id); } } } From 8f92df1a869ea7d33de209fd8a3ddb3570ba717f Mon Sep 17 00:00:00 2001 From: Hadi Nategh Date: Wed, 13 May 2015 16:21:50 +0000 Subject: [PATCH 09/31] Escape from infinitive loadingDeferred if the diferred did not get resolved or rejected, and give user a chance to try other tabs --- phpgwapi/js/framework/fw_browser.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/phpgwapi/js/framework/fw_browser.js b/phpgwapi/js/framework/fw_browser.js index 5324e0590a..258f2573fa 100644 --- a/phpgwapi/js/framework/fw_browser.js +++ b/phpgwapi/js/framework/fw_browser.js @@ -155,14 +155,25 @@ var fw_browser = Class.extend({ var self = this; this.ajaxLoaderDiv = jQuery('
'+egw.lang('please wait...')+'
').insertBefore(this.baseDiv); this.loadingDeferred = new jQuery.Deferred(); + + // Try to escape from infinitive not resolved loadingDeferred + // At least user can close the broken tab and work with the others. + // Define a escape timeout for 5 sec + this.ajaxLoaderDivTimeout = setTimeout(function(){ + self.ajaxLoaderDiv.hide().remove(); + self.ajaxLoaderDiv = null; + },5000); + this.loadingDeferred.always(function() { if(self.ajaxLoaderDiv) { self.ajaxLoaderDiv.hide().remove(); self.ajaxLoaderDiv = null; + // Remove escape timeout + clearTimeout(self.ajaxLoaderDivTimeout); } }); - + // Check whether the given url is a pseudo url which should be executed // by calling the ajax_exec function // we now send whole url back to server, so apps can use $_GET['ajax']==='true' From d35a0947d3db22476db310eb0722243b512ed316 Mon Sep 17 00:00:00 2001 From: Hadi Nategh Date: Fri, 15 May 2015 08:45:48 +0000 Subject: [PATCH 10/31] Do not show the dropdown menu if there is no actions on toolbar "more..." menu --- etemplate/js/et2_widget_toolbar.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/etemplate/js/et2_widget_toolbar.js b/etemplate/js/et2_widget_toolbar.js index 944993fe4e..93daa16d52 100644 --- a/etemplate/js/et2_widget_toolbar.js +++ b/etemplate/js/et2_widget_toolbar.js @@ -357,6 +357,11 @@ var et2_toolbar = et2_DOMWidget.extend([et2_IInput], }, create: function (event, ui) { $j('html').unbind('click.outsideOfMenu'); + }, + beforeActivate: function () + { + // Nothing to show in menulist + if (menulist.children().length == 0) return false; } }); }, From a4da02db6940effa5e3a0ae2bdde2c427f15a961 Mon Sep 17 00:00:00 2001 From: Klaus Leithoff Date: Fri, 15 May 2015 13:20:30 +0000 Subject: [PATCH 11/31] add missing config lang strings --- calendar/lang/egw_de.lang | 1 + calendar/lang/egw_en.lang | 1 + 2 files changed, 2 insertions(+) diff --git a/calendar/lang/egw_de.lang b/calendar/lang/egw_de.lang index a0a60b9688..5ce441aab8 100644 --- a/calendar/lang/egw_de.lang +++ b/calendar/lang/egw_de.lang @@ -560,6 +560,7 @@ unable to save calendar de Speichern nicht möglich update timezones common de Zeitzonen aktualisieren updated calendar de Aktualisiert use end date calendar de Enddatum benutzen +use range-views to optimise calendar queries? calendar de Sollen spezielle Bereichs-Ansichten verwendet werden, um Kalenderabfragen zu optimieren? use the selected time and close the popup calendar de benutzt die ausgewählte Zeit und schließt das Pop-up use this tag for addresslabels. put the content, you want to repeat, between two tags. calendar de Verwenden Sie diesen Platzhalter für Aufkleber. Fügen die den Inhalt, der wiederholt werden soll, zwischen zwei dieser Platzhalter ein. use this timezone to export calendar data. calendar de Diese Zeitzone zum exportieren von Kalenderdaten verwenden diff --git a/calendar/lang/egw_en.lang b/calendar/lang/egw_en.lang index 92b22f74aa..c31bc266f3 100644 --- a/calendar/lang/egw_en.lang +++ b/calendar/lang/egw_en.lang @@ -560,6 +560,7 @@ unable to save calendar en Unable to save update timezones common en Update time zones updated calendar en Updated use end date calendar en Use end date +use range-views to optimise calendar queries? calendar en Use range-views to optimise calendar queries? use the selected time and close the popup calendar en Use the selected time and close the popup use this tag for addresslabels. put the content, you want to repeat, between two tags. calendar en Use this tag for address labels. Place the content you want to repeat between two tags. use this timezone to export calendar data. calendar en Use this time zone to export calendar data. From 73499db1c70f7cb956b6bc098de33d356a759d25 Mon Sep 17 00:00:00 2001 From: Hadi Nategh Date: Fri, 15 May 2015 14:00:37 +0000 Subject: [PATCH 12/31] WIP mail inline image - Define static methods for resolving inline images from CID, based on types - Fix not showing inline images on reply --- mail/inc/class.mail_compose.inc.php | 3 +- mail/inc/class.mail_ui.inc.php | 259 +++++++++++++--------------- 2 files changed, 123 insertions(+), 139 deletions(-) diff --git a/mail/inc/class.mail_compose.inc.php b/mail/inc/class.mail_compose.inc.php index 512707ca28..90e7ee555a 100644 --- a/mail/inc/class.mail_compose.inc.php +++ b/mail/inc/class.mail_compose.inc.php @@ -2081,6 +2081,7 @@ class mail_compose } $this->sessionData['body'] .= '
'; + $this->sessionData['body'] = mail_ui::resolve_inline_images($this->sessionData['body'], $_folder, $_uid, $_partID, 'html'); } else { //$this->sessionData['body'] = @htmlspecialchars(lang("on")." ".$headers['DATE']." ".$mail_bo->decode_header($fromAddress), ENT_QUOTES) . " ".lang("wrote").":\r\n"; // take care the way the ReplyHeader is created here, is used later on in uicompose::compose, in case you force replys to be HTML (prefs) @@ -2100,7 +2101,7 @@ class mail_compose if ($bodyParts[$i]['charSet']===false) $bodyParts[$i]['charSet'] = mail_bo::detect_encoding($bodyParts[$i]['body']); $newBody = translation::convert($bodyParts[$i]['body'], $bodyParts[$i]['charSet']); #error_log( "GetReplyData (Plain) CharSet:".mb_detect_encoding($bodyParts[$i]['body'] . 'a' , strtoupper($bodyParts[$i]['charSet']).','.strtoupper($this->displayCharset).',UTF-8, ISO-8859-1')); - + $newBody = mail_ui::resolve_inline_images($newBody, $_folder, $_uid, $_partID, 'plain'); $this->sessionData['body'] .= "\r\n"; // create body new, with good line breaks and indention foreach(explode("\n",$newBody) as $value) { diff --git a/mail/inc/class.mail_ui.inc.php b/mail/inc/class.mail_ui.inc.php index 60927f5319..f4182bf3e3 100644 --- a/mail/inc/class.mail_ui.inc.php +++ b/mail/inc/class.mail_ui.inc.php @@ -3013,7 +3013,7 @@ class mail_ui // create links for inline images if ($modifyURI) { - $newBody = preg_replace_callback("/\[cid:(.*)\]/iU",array($this,'image_callback_plain'),$newBody); + $newBody = self::resolve_inline_images($newBody, $this->mailbox, $this->uid, $this->partID, 'plain'); } //TODO:$newBody = $this->highlightQuotes($newBody); @@ -3064,9 +3064,7 @@ class mail_ui // create links for inline images if ($modifyURI) { - $newBody = preg_replace_callback("/src=(\"|\')cid:(.*)(\"|\')/iU",array($this,'image_callback'),$newBody); - $newBody = preg_replace_callback("/url\(cid:(.*)\);/iU",array($this,'image_callback_url'),$newBody); - $newBody = preg_replace_callback("/background=(\"|\')cid:(.*)(\"|\')/iU",array($this,'image_callback_background'),$newBody); + $newBody = self::resolve_inline_images ($newBody, $this->mailbox, $this->uid, $this->partID); } // email addresses / mailto links get now activated on client-side } @@ -3082,161 +3080,146 @@ class mail_ui return $body; } - + /** - * preg_replace callback to replace image cid url's + * Resolve inline images from CID to proper url * - * @param array $matches matches from preg_replace("/src=(\"|\')cid:(.*)(\"|\')/iU",...) - * @return string src attribute to replace + * @param string $_body message content + * @param string $_mailbox mail folder + * @param string $_uid uid + * @param string $_partID part id + * @param string $_messageType = 'html', message type is either html or plain + * @return string message body including all CID images replaced */ - function image_callback($matches) + public static function resolve_inline_images ($_body,$_mailbox, $_uid, $_partID, $_messageType = 'html') { - static $cache = array(); // some caching, if mails containing the same image multiple times - - $linkData = array ( - 'menuaction' => 'mail.mail_ui.displayImage', - 'uid' => $this->uid, - 'mailbox' => base64_encode($this->mailbox), - 'cid' => base64_encode($matches[2]), - 'partID' => $this->partID, - ); - $imageURL = egw::link('/index.php', $linkData); - - // to test without data uris, comment the if close incl. it's body - if (html::$user_agent != 'msie' || html::$ua_version >= 8) + if ($_messageType === 'plain') { - if (!isset($cache[$imageURL])) - { - $attachment = $this->mail_bo->getAttachmentByCID($this->uid, $matches[2], $this->partID); - - // only use data uri for "smaller" images, as otherwise the first display of the mail takes to long - if (($attachment instanceof Horde_Mime_Part) && $attachment->getBytes() < 8192) // msie=8 allows max 32k data uris - { - $this->mail_bo->fetchPartContents($this->uid, $attachment); - $cache[$imageURL] = 'data:'.$attachment->getType().';base64,'.base64_encode($attachment->getContents()); - } - else - { - $cache[$imageURL] = $imageURL; - } - } - $imageURL = $cache[$imageURL]; + return self::resolve_inline_image_byType($_body, $_mailbox, $_uid, $_partID, 'plain'); + } + else + { + foreach(array('src','url','background') as $type) + { + $_body = self::resolve_inline_image_byType($_body, $_mailbox, $_uid, $_partID, $type); + } + return $_body; } - return 'src="'.$imageURL.'"'; } - + /** - * preg_replace callback to replace image cid url's + * Replace CID with proper type of content understandable by browser * - * @param array $matches matches from preg_replace("/src=(\"|\')cid:(.*)(\"|\')/iU",...) - * @return string src attribute to replace + * @param type $_body content of message + * @param type $_mailbox mail box + * @param type $_uid uid + * @param type $_partID part id + * @param type $_type = 'src' type of inline image that needs to be resolved and replaced + * - types: {plain|src|url|background} + * @return string returns body content including all CID replacements */ - function image_callback_plain($matches) + public static function resolve_inline_image_byType ($_body,$_mailbox, $_uid, $_partID, $_type ='src') { - static $cache = array(); // some caching, if mails containing the same image multiple times - //error_log(__METHOD__.__LINE__.array2string($matches)); - $linkData = array ( - 'menuaction' => 'mail.mail_ui.displayImage', - 'uid' => $this->uid, - 'mailbox' => base64_encode($this->mailbox), - 'cid' => base64_encode($matches[1]), - 'partID' => $this->partID, - ); - $imageURL = egw::link('/index.php', $linkData); - - // to test without data uris, comment the if close incl. it's body - if (html::$user_agent != 'msie' || html::$ua_version >= 8) + /** + * Callback for preg_replace_callback function + * returns matched CID replacement string based on given type + * @param array $matches + * @param string $_mailbox + * @param string $_uid + * @param string $_partID + * @param string $_type + * @return string|boolean returns the replace + */ + $replace_callback = function ($matches) use ($_mailbox,$_uid, $_partID, $_type) { - if (!isset($cache[$imageURL])) + if (!$_type) return false; + $CID = ''; + // Build up matches according to selected type + switch ($_type) { - $attachment = $this->mail_bo->getAttachmentByCID($this->uid, $matches[1], $this->partID); + case "plain": + $CID = $matches[1]; + break; + case "src": + $CID = $matches[2]; + break; + case "url": + $CID = $matches[1]; + break; + case "background": + $CID = $matches[2]; + break; + } - // only use data uri for "smaller" images, as otherwise the first display of the mail takes to long - if (($attachment instanceof Horde_Mime_Part) && bytes($attachment->getBytes()) < 8192) // msie=8 allows max 32k data uris + static $cache = array(); // some caching, if mails containing the same image multiple times + + if (is_array($matches) && $CID) + { + $linkData = array ( + 'menuaction' => 'mail.mail_ui.displayImage', + 'uid' => $_uid, + 'mailbox' => base64_encode($_mailbox), + 'cid' => base64_encode($CID), + 'partID' => $_partID, + ); + $imageURL = egw::link('/index.php', $linkData); + // to test without data uris, comment the if close incl. it's body + if (html::$user_agent != 'msie' || html::$ua_version >= 8) { - $this->mail_bo->fetchPartContents($this->uid, $attachment); - $cache[$imageURL] = 'data:'.$attachment->getType().';base64,'.base64_encode($attachment->getContents()); + if (!isset($cache[$imageURL])) + { + if ($_type !="background") + { + $bo = emailadmin_imapbase::getInstance(false, self::$icServerID); + $attachment = $bo->getAttachmentByCID($_uid, $CID, $_partID); + + // only use data uri for "smaller" images, as otherwise the first display of the mail takes to long + if (($attachment instanceof Horde_Mime_Part) && $attachment->getBytes() < 8192) // msie=8 allows max 32k data uris + { + $bo->fetchPartContents($_uid, $attachment); + $cache[$imageURL] = 'data:'.$attachment->getType().';base64,'.base64_encode($attachment->getContents()); + } + else + { + $cache[$imageURL] = $imageURL; + } + } + else + { + $cache[$imageURL] = $imageURL; + } + } + $imageURL = $cache[$imageURL]; } - else + + // Decides the final result of replacement according to the type + switch ($_type) { - $cache[$imageURL] = $imageURL; + case "plain": + return ''; + case "src": + return 'src="'.$imageURL.'"'; + case "url": + return 'url('.$imageURL.');'; + case "background": + return 'background="'.$imageURL.'"'; } } - $imageURL = $cache[$imageURL]; - } - return ''; - } - - /** - * preg_replace callback to replace image cid url's - * - * @param array $matches matches from preg_replace("/src=(\"|\')cid:(.*)(\"|\')/iU",...) - * @return string src attribute to replace - */ - function image_callback_url($matches) - { - static $cache = array(); // some caching, if mails containing the same image multiple times - //error_log(__METHOD__.__LINE__.array2string($matches)); - $linkData = array ( - 'menuaction' => 'mail.mail_ui.displayImage', - 'uid' => $this->uid, - 'mailbox' => base64_encode($this->mailbox), - 'cid' => base64_encode($matches[1]), - 'partID' => $this->partID, - ); - $imageURL = egw::link('/index.php', $linkData); - - // to test without data uris, comment the if close incl. it's body - if (html::$user_agent != 'msie' || html::$ua_version >= 8) + return false; + }; + + // return new body content base on chosen type + switch($_type) { - if (!isset($cache[$imageURL])) - { - $attachment = $this->mail_bo->getAttachmentByCID($this->uid, $matches[1], $this->partID); - - // only use data uri for "smaller" images, as otherwise the first display of the mail takes to long - if (($attachment instanceof Horde_Mime_Part) && $attachment->getBytes() < 8192) // msie=8 allows max 32k data uris - { - $this->mail_bo->fetchPartContents($this->uid, $attachment); - $cache[$imageURL] = 'data:'.$attachment->getType().';base64,'.base64_encode($attachment->getContents()); - } - else - { - $cache[$imageURL] = $imageURL; - } - } - $imageURL = $cache[$imageURL]; + case"plain": + return preg_replace_callback("/\[cid:(.*)\]/iU",$replace_callback,$_body); + case "src": + return preg_replace_callback("/src=(\"|\')cid:(.*)(\"|\')/iU",$replace_callback,$_body); + case "url": + return preg_replace_callback("/url\(cid:(.*)\);/iU",$replace_callback,$_body); + case "background": + return preg_replace_callback("/background=(\"|\')cid:(.*)(\"|\')/iU",$replace_callback,$_body); } - return 'url('.$imageURL.');'; - } - - /** - * preg_replace callback to replace image cid url's - * - * @param array $matches matches from preg_replace("/src=(\"|\')cid:(.*)(\"|\')/iU",...) - * @return string src attribute to replace - */ - function image_callback_background($matches) - { - static $cache = array(); // some caching, if mails containing the same image multiple times - $linkData = array ( - 'menuaction' => 'mail.mail_ui.displayImage', - 'uid' => $this->uid, - 'mailbox' => base64_encode($this->mailbox), - 'cid' => base64_encode($matches[2]), - 'partID' => $this->partID, - ); - $imageURL = egw::link('/index.php', $linkData); - - // to test without data uris, comment the if close incl. it's body - if (html::$user_agent != 'msie' || html::$ua_version >= 8) - { - if (!isset($cache[$imageURL])) - { - $cache[$imageURL] = $imageURL; - } - $imageURL = $cache[$imageURL]; - } - return 'background="'.$imageURL.'"'; } /** From 38bf42b5dbe6a07bc8d42231bd23cbaa737e3593 Mon Sep 17 00:00:00 2001 From: Klaus Leithoff Date: Fri, 15 May 2015 14:07:36 +0000 Subject: [PATCH 13/31] use static function emailadmin_imapbase::merge instead of ->mail->merge --- mail/inc/class.mail_activesync.inc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mail/inc/class.mail_activesync.inc.php b/mail/inc/class.mail_activesync.inc.php index 1d974d058a..efe4fa188c 100644 --- a/mail/inc/class.mail_activesync.inc.php +++ b/mail/inc/class.mail_activesync.inc.php @@ -606,7 +606,7 @@ class mail_activesync implements activesync_plugin_write, activesync_plugin_send $beforePlain = $beforeHtml = ""; $beforeHtml = ($disableRuler ?' 
':' 

'); $beforePlain = ($disableRuler ?"\r\n\r\n":"\r\n\r\n-- \r\n"); - $sigText = $this->mail->merge($signature,array($GLOBALS['egw']->accounts->id2name($GLOBALS['egw_info']['user']['account_id'],'person_id'))); + $sigText = emailadmin_imapbase::merge($signature,array($GLOBALS['egw']->accounts->id2name($GLOBALS['egw_info']['user']['account_id'],'person_id'))); if ($this->debugLevel>0) debugLog(__METHOD__.__LINE__.' Signature to use:'.$sigText); $sigTextHtml = $beforeHtml.$sigText; $sigTextPlain = $beforePlain.translation::convertHTMLToText($sigText); From 58aaff6b9b3758559eeaf77a6bc14606a61839b9 Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Sun, 17 May 2015 19:03:45 +0000 Subject: [PATCH 14/31] WIP mailvelope API integration: - compose of plaintext mails works now - display of encrypted mails in preview and display popup ToDo: html compose, switching html on/off and resize of mailvelope iframe in compose --- mail/js/app.js | 152 ++++++++++++++++++++++++++--- mail/templates/default/compose.xet | 6 +- 2 files changed, 142 insertions(+), 16 deletions(-) diff --git a/mail/js/app.js b/mail/js/app.js index 23c20b0f34..c356fa276c 100644 --- a/mail/js/app.js +++ b/mail/js/app.js @@ -153,6 +153,20 @@ app.classes.mail = AppJS.extend( break; case 'mail.mobile_index': case 'mail.index': + var self = this; + jQuery('iframe#mail-index_messageIFRAME').on('load', function() + { + // decrypt preview body if mailvelope is available + if (typeof mailvelope !== 'undefined') { + self.mailvelopeDisplay.call(self); + } else { + jQuery(window).on('mailvelope', function() + { + self.mailvelopeDisplay.call(self); + }); + } + self.mail_prepare_print(); + }); var nm = this.et2.getWidgetById(this.nm_index); this.mail_isMainWindow = true; this.mail_disablePreviewArea(true); @@ -177,7 +191,19 @@ app.classes.mail = AppJS.extend( // Prepare display dialog for printing // copies iframe content to a DIV, as iframe causes // trouble for multipage printing - jQuery('#mail-display_mailDisplayBodySrc').on('load', function(){self.mail_prepare_print();}); + jQuery('iframe#mail-display_mailDisplayBodySrc').on('load', function() + { + // encrypt body if mailvelope is available + if (typeof mailvelope !== 'undefined') { + self.mailvelopeDisplay.call(self); + } else { + jQuery(window).on('mailvelope', function() + { + self.mailvelopeDisplay.call(self); + }); + } + self.mail_prepare_print(); + }); this.mail_isMainWindow = false; this.mail_display(); @@ -189,7 +215,12 @@ app.classes.mail = AppJS.extend( ); break; case 'mail.compose': - // use a wrapper on a different url to be able to use a different fpm pool + if (typeof mailvelope !== 'undefined') { + this.mailvelopeCompose(); + } else { + jQuery(window).on('mailvelope', jQuery.proxy(this.mailvelopeCompose, this)); + } + // use a wrapper on a different url to be able to use a different fpm pool et2.menuaction = 'mail_compose::ajax_send'; var that = this; this.mail_isMainWindow = false; @@ -862,6 +893,10 @@ app.classes.mail = AppJS.extend( var IframeHandle = this.et2.getWidgetById('messageIFRAME'); IframeHandle.set_src('about:blank'); + // show iframe, in case we hide it from mailvelopes one and remove that + jQuery(IframeHandle.getDOMNode()).show() + .next('iframe[src^=chrome-extension]').remove(); + // Set up additional content that can be expanded. // We add a new URL widget for each address, so they get all the UI // TO addresses have the first one split out, not all together @@ -2625,9 +2660,9 @@ app.classes.mail = AppJS.extend( { var app = _action.id; var w_h = ['750','580']; // define a default wxh if there's no popup size registered - + var add_as_new = true; - + if (typeof _action.data != 'undefined' ) { if (typeof _action.data.popup != 'undefined' && _action.data.popup) w_h = _action.data.popup.split('x'); @@ -2650,16 +2685,16 @@ app.classes.mail = AppJS.extend( } } } - + var url = window.egw_webserverUrl+ '/index.php?menuaction=mail.mail_integration.integrate&rowid=' + _elems[0].id + '&app='+app; - + /** * Checks the application entry existance and offers user * to select desire app id to append mail content into it, * or add the mail content as a new app entry - * + * * @param {string} _title select app entry title - * @param {string} _appName app to be integrated + * @param {string} _appName app to be integrated * @param {string} _appCheckCallback registered mail_import hook method * for check app entry existance */ @@ -2668,7 +2703,7 @@ app.classes.mail = AppJS.extend( var data = egw.dataGetUIDdata(_elems[0].id); var subject = (data && typeof data.data != 'undefined')? data.data.subject : ''; egw.json(_appCheckCallback, subject,function(_entryId){ - + // if there's no entry saved already // open dialog in order to select one if (!_entryId) @@ -2703,8 +2738,8 @@ app.classes.mail = AppJS.extend( egw_openWindowCentered(url,'import_mail_'+_elems[0].id,w_h[0],w_h[1]); } },this,true,this).sendRequest(); - } - + }; + if (mail_import_hook && typeof mail_import_hook.app_entry_method != 'undefined') { check_app_entry('Select '+ app + ' entry', app, mail_import_hook.app_entry_method); @@ -2713,7 +2748,7 @@ app.classes.mail = AppJS.extend( { egw_openWindowCentered(url,'import_mail_'+_elems[0].id,w_h[0],w_h[1]); } - + }, /** @@ -3583,7 +3618,7 @@ app.classes.mail = AppJS.extend( if (egwIsMobile()) { var nm = this.et2.getWidgetById(this.nm_index); - nm.set_disabled(!!_url) + nm.set_disabled(!!_url); iframe.set_disabled(!_url); } // Set extra_iframe a class with height and width @@ -4309,12 +4344,103 @@ app.classes.mail = AppJS.extend( } }, + /** + * Called on load of preview or display iframe, if mailvelope is available + * + * @ToDo signatures + */ + mailvelopeDisplay: function() + { + var self = this; + var mailvelope = window.mailvelope; + var iframe = jQuery('iframe#mail-display_mailDisplayBodySrc,iframe#mail-index_messageIFRAME'); + var armored = iframe.contents().find('td.td_display > pre').text().trim(); + + if (armored == "" || armored.indexOf('-----BEGIN PGP MESSAGE-----') === -1) return; + + mailvelope.getKeyring('mailvelope').then(function(_keyring) + { + var container = iframe.parent()[0]; + var container_selector = container.id ? '#'+container.id : 'div.mailDisplayContainer'; + mailvelope.createDisplayContainer(container_selector, armored, _keyring).then(function() + { + // hide our iframe to give space for mailvelope iframe with encrypted content + iframe.hide(); + }, + function(_err) + { + self.egw.message(_err.message, 'error'); + }); + }, + function(_err) + { + self.egw.message(_err.message, 'error'); + }); + }, + + /** + * Editor object of active compose + * + * @var {Editor} + */ + mailvelope_editor: undefined, + + /** + * Called on compose, if mailvelope is available + */ + mailvelopeCompose: function() + { + var self = this; + var mailvelope = window.mailvelope; + + delete this.mailvelope_editor; + mailvelope.getKeyring('mailvelope').then(function(_keyring) + { + var is_html = self.et2.getWidgetById('mimeType').get_value(); + var container = is_html ? '.mailComposeHtmlContainer' : '.mailComposeTextContainer'; + var editor = self.et2.getWidgetById(is_html ? 'mail_htmltext' : 'mail_plaintext'); + mailvelope.createEditorContainer(container, _keyring, { + predefinedText: editor.get_value() + }).then(function(_editor) + { + self.mailvelope_editor = _editor; + editor.set_disabled(true); + }, + function(_err) + { + self.egw.message(_err.message, 'error'); + }); + }, + function(_err) + { + self.egw.message(keyringId+': '+_err.message, 'error'); + }); + }, + /** * Set the relevant widget to toolbar actions and submit * @param {type} _action toolbar action */ compose_submitAction: function (_action) { + if (this.mailvelope_editor) + { + var self = this; + var recipients = this.et2.getWidgetById('to').get_value(); + recipients.concat(this.et2.getWidgetById('cc').get_value()); + // todo: bcc, do we disclosure them by adding them here? + this.mailvelope_editor.encrypt(recipients).then(function(_armored) + { + self.et2.getWidgetById('mimeType').set_value(false); + self.et2.getWidgetById('mail_plaintext').set_disabled(false); + self.et2.getWidgetById('mail_plaintext').set_value(_armored); + self.et2._inst.submit(null,null,true); + }, function(_err) + { + self.egw.message(_err.message, 'error'); + }); + return false; + } this.et2._inst.submit(null,null,true); }, diff --git a/mail/templates/default/compose.xet b/mail/templates/default/compose.xet index 2be62e97f8..ffed21997f 100644 --- a/mail/templates/default/compose.xet +++ b/mail/templates/default/compose.xet @@ -16,7 +16,7 @@ - + @@ -85,10 +85,10 @@ - + - + From 09fdc8d0fe2e31f6f0882b6698bad7079986c182 Mon Sep 17 00:00:00 2001 From: Hadi Nategh Date: Mon, 18 May 2015 11:36:11 +0000 Subject: [PATCH 15/31] W.I.P. mail inline images: Include inline images as inline attachments before send --- mail/inc/class.mail_compose.inc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mail/inc/class.mail_compose.inc.php b/mail/inc/class.mail_compose.inc.php index 90e7ee555a..75b12ed1c1 100644 --- a/mail/inc/class.mail_compose.inc.php +++ b/mail/inc/class.mail_compose.inc.php @@ -2285,7 +2285,7 @@ class mail_compose $_mailObject->setBody($this->convertHTMLToText($body, true, true)); } // convert URL Images to inline images - if possible - if (!$_autosaving) mail_bo::processURL2InlineImages($_mailObject, $body); + if (!$_autosaving) mail_bo::processURL2InlineImages($_mailObject, $body, $mail_bo); if (strpos($body,"")!==false) { $body = str_replace(array('',''),'',$body); From 38b3122bb8b89a0451e897f91e22c6381e3953b2 Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Mon, 18 May 2015 19:23:05 +0000 Subject: [PATCH 16/31] send OpenPGP/Mime message according to rfc3156, section 4 --- mail/inc/class.mail_compose.inc.php | 8 ++++++++ phpgwapi/inc/class.egw_mailer.inc.php | 29 ++++++++++++++++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/mail/inc/class.mail_compose.inc.php b/mail/inc/class.mail_compose.inc.php index 75b12ed1c1..64b6994e0d 100644 --- a/mail/inc/class.mail_compose.inc.php +++ b/mail/inc/class.mail_compose.inc.php @@ -2159,6 +2159,10 @@ class mail_compose */ function createMessage(egw_mailer $_mailObject, array $_formData, array $_identity, $_autosaving=false) { + if (substr($_formData['body'], 0, 27) == '-----BEGIN PGP MESSAGE-----') + { + $_formData['mimeType'] = 'openpgp'; + } //error_log(__METHOD__."(, formDate[filemode]=$_formData[filemode], _autosaving=".array2string($_autosaving).') '.function_backtrace()); $mail_bo = $this->mail_bo; $activeMailProfile = emailadmin_account::read($this->mail_bo->profileID); @@ -2292,6 +2296,10 @@ class mail_compose } $_mailObject->setHtmlBody($body, null, false); // false = no automatic alternative, we called setBody() } + elseif ($_formData['mimeType'] == 'openpgp') + { + $_mailObject->setOpenPgpBody($_formData['body']); + } else { $body = $this->convertHTMLToText($_formData['body'],false); diff --git a/phpgwapi/inc/class.egw_mailer.inc.php b/phpgwapi/inc/class.egw_mailer.inc.php index 184e784ab6..b432760586 100644 --- a/phpgwapi/inc/class.egw_mailer.inc.php +++ b/phpgwapi/inc/class.egw_mailer.inc.php @@ -453,7 +453,8 @@ class egw_mailer extends Horde_Mime_Mail ), array(), true); // true = call all apps try { - parent::send($this->account->smtpTransport(), true); // true: keep Message-ID + parent::send($this->account->smtpTransport(), true, // true: keep Message-ID + $this->_body->getType() != 'multipart/encrypted'); // no flowed for encrypted messages } catch (Exception $e) { // in case of errors/exceptions call hook again with previous returned mail_id and error-message to log @@ -632,6 +633,32 @@ class egw_mailer extends Horde_Mime_Mail return parent::addMimePart($part); } + /** + * Sets OpenPGP encrypted body according to rfc3156, section 4 + * + * @param string $body The message content. + * @link https://tools.ietf.org/html/rfc3156#section-4 + */ + public function setOpenPgpBody($body) + { + $this->_body = new Horde_Mime_Part(); + $this->_body->setType('multipart/encrypted'); + $this->_body->setContentTypeParameter('protocol', 'application/pgp-encrypted'); + $this->_body->setContents(''); + + $part1 = new Horde_Mime_Part(); + $part1->setType('application/pgp-encrypted'); + $part1->setContents("Version: 1\r\n", array('encoding' => '7bit')); + $this->_body->addPart($part1); + + $part2 = new Horde_Mime_Part(); + $part2->setType('application/octet-stream'); + $part2->setContents($body, array('encoding' => '7bit')); + $this->_body->addPart($part2); + + $this->_base = null; + } + /** * Clear all non-standard headers * From 50aaafe293c661f8a288c24e4652774a2c51652a Mon Sep 17 00:00:00 2001 From: Klaus Leithoff Date: Tue, 19 May 2015 09:26:08 +0000 Subject: [PATCH 17/31] src:cid url its likely to be urlencoded. so decode, before using it --- mail/inc/class.mail_ui.inc.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/mail/inc/class.mail_ui.inc.php b/mail/inc/class.mail_ui.inc.php index f4182bf3e3..a8e481ce2f 100644 --- a/mail/inc/class.mail_ui.inc.php +++ b/mail/inc/class.mail_ui.inc.php @@ -2053,6 +2053,8 @@ class mail_ui //error_log(__METHOD__.__LINE__.array2string($envelope)); $this->mail_bo->getMessageRawHeader($uid, $partID,$mailbox); $fetchEmbeddedImages = false; + // if we are in HTML so its likely that we should show the embedded images; as a result + // we do NOT want to see those, that are embedded in the list of attachments if ($htmlOptions !='always_display') $fetchEmbeddedImages = true; $attachments = $this->mail_bo->getMessageAttachments($uid, $partID, null, $fetchEmbeddedImages,true,true,$mailbox); //error_log(__METHOD__.__LINE__.array2string($attachments)); @@ -2841,8 +2843,9 @@ class mail_ui $bodyParts = $this->mail_bo->getMessageBody($uid, ($htmlOptions?$htmlOptions:''), $partID, $structure, false, $mailbox); //error_log(__METHOD__.__LINE__.array2string($bodyParts)); + // attachments here are only fetched to determine if there is a meeting request + // and if. use the appropriate action. so we do not need embedded images $fetchEmbeddedImages = false; - if ($htmlOptions !='always_display') $fetchEmbeddedImages = true; $attachments = (array)$this->mail_bo->getMessageAttachments($uid, $partID, $structure, $fetchEmbeddedImages, true,true,$mailbox); //error_log(__METHOD__.__LINE__.array2string($attachments)); foreach ($attachments as &$attach) @@ -3141,7 +3144,8 @@ class mail_ui $CID = $matches[1]; break; case "src": - $CID = $matches[2]; + // as src:cid contains some kind of url, it is likely to be urlencoded + $CID = urldecode($matches[2]); break; case "url": $CID = $matches[1]; From bab4de6d16bb3013ed43b1b8381d3ee0bfbc9c2b Mon Sep 17 00:00:00 2001 From: Hadi Nategh Date: Tue, 19 May 2015 13:30:48 +0000 Subject: [PATCH 18/31] Apply resized height value to parent container of textarea in compose --- mail/js/app.js | 1 + 1 file changed, 1 insertion(+) diff --git a/mail/js/app.js b/mail/js/app.js index c356fa276c..035b088339 100644 --- a/mail/js/app.js +++ b/mail/js/app.js @@ -3834,6 +3834,7 @@ app.classes.mail = AppJS.extend( if (textArea.id != "mail_htmltext") { + textArea.getParent().set_height(bodySize); textArea.set_height(bodySize); } else if (typeof textArea != 'undefined' && textArea.id == 'mail_htmltext') From 01fdd4d33a6c323bd615eaf5db1ca287f8b75fe5 Mon Sep 17 00:00:00 2001 From: Klaus Leithoff Date: Tue, 19 May 2015 14:27:06 +0000 Subject: [PATCH 19/31] fix/avoid warning on NULL sel_options subarray in fix_sel_options --- etemplate/inc/class.etemplate_new.inc.php | 1 + 1 file changed, 1 insertion(+) diff --git a/etemplate/inc/class.etemplate_new.inc.php b/etemplate/inc/class.etemplate_new.inc.php index 297c72c90d..f344a13cef 100644 --- a/etemplate/inc/class.etemplate_new.inc.php +++ b/etemplate/inc/class.etemplate_new.inc.php @@ -258,6 +258,7 @@ class etemplate_new extends etemplate_widget_template { foreach($sel_options as &$options) { + if (!is_array($options)||empty($options)) continue; foreach($options as $key => $value) { if (is_numeric($key) && (!is_array($value) || !isset($value['value']))) From f2793cad97a3781f05d52d2b441b9cdf3aae6a68 Mon Sep 17 00:00:00 2001 From: Hadi Nategh Date: Tue, 19 May 2015 14:34:35 +0000 Subject: [PATCH 20/31] Always display html for openned drafted message --- mail/inc/class.mail_compose.inc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mail/inc/class.mail_compose.inc.php b/mail/inc/class.mail_compose.inc.php index 64b6994e0d..75c64e2060 100644 --- a/mail/inc/class.mail_compose.inc.php +++ b/mail/inc/class.mail_compose.inc.php @@ -1606,7 +1606,7 @@ class mail_compose // remove a printview tag if composing $searchfor = '/^\['.lang('printview').':\]/'; $this->sessionData['subject'] = preg_replace($searchfor,'',$this->sessionData['subject']); - $bodyParts = $mail_bo->getMessageBody($_uid, $this->mailPreferences['always_display'], $_partID); + $bodyParts = $mail_bo->getMessageBody($_uid,'always_display', $_partID); //_debug_array($bodyParts); #$fromAddress = ($headers['FROM'][0]['PERSONAL_NAME'] != 'NIL') ? $headers['FROM'][0]['RFC822_EMAIL'] : $headers['FROM'][0]['EMAIL']; if($bodyParts['0']['mimeType'] == 'text/html') { From 77c2b3d9afd7726772f1e208c4cc352a8aa55b3c Mon Sep 17 00:00:00 2001 From: Klaus Leithoff Date: Tue, 19 May 2015 14:44:30 +0000 Subject: [PATCH 21/31] fix for missing mail_bo::replaceEmailAdresses call. Fix for not supported FromName Attribute in compose/egw_mailer --- mail/inc/class.mail_compose.inc.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mail/inc/class.mail_compose.inc.php b/mail/inc/class.mail_compose.inc.php index 75c64e2060..e6e0efcc62 100644 --- a/mail/inc/class.mail_compose.inc.php +++ b/mail/inc/class.mail_compose.inc.php @@ -1441,7 +1441,7 @@ class mail_compose static function replaceEmailAdresses(&$text) { // replace emailaddresses eclosed in <> (eg.: ) with the emailaddress only (e.g: me@you.de) - mail_bo::replaceEmailAdresses($text); + translation::replaceEmailAdresses($text); return 1; } @@ -2748,7 +2748,7 @@ class mail_compose // create the messages $this->createMessage($mail, $_formData, $identity); // remember the identity - if ($_formData['to_infolog'] == 'on' || $_formData['to_tracker'] == 'on') $fromAddress = $mail->FromName.($mail->FromName?' <':'').$mail->From.($mail->FromName?'>':''); + if ($_formData['to_infolog'] == 'on' || $_formData['to_tracker'] == 'on') $fromAddress = $mail->From;//$mail->FromName.($mail->FromName?' <':'').$mail->From.($mail->FromName?'>':''); #print "
". $mail->getMessageHeader() ."


"; #print "
". $mail->getMessageBody() ."


"; #exit; From a82f7baf2caca477ace435e24f11378b28e2f358 Mon Sep 17 00:00:00 2001 From: Nathan Gray Date: Tue, 19 May 2015 14:57:35 +0000 Subject: [PATCH 22/31] Avoid deprecated message caused by calling non-static method statically. Fixed by creating an instance to use. --- infolog/inc/class.infolog_so.inc.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/infolog/inc/class.infolog_so.inc.php b/infolog/inc/class.infolog_so.inc.php index 538d436e8f..0b014ce1d4 100644 --- a/infolog/inc/class.infolog_so.inc.php +++ b/infolog/inc/class.infolog_so.inc.php @@ -828,7 +828,8 @@ class infolog_so if ($this->db->capabilities['like_on_text']) $columns[] = 'info_des'; $wildcard = $op = null; - $search = so_sql::search2criteria($query['search'], $wildcard, $op, null, $columns); + $so_sql = new so_sql('infolog', $this->info_table, $this->db); + $search = $so_sql->search2criteria($query['search'], $wildcard, $op, null, $columns); $sql_query = 'AND ('.(is_numeric($query['search']) ? 'main.info_id='.(int)$query['search'].' OR ' : ''). implode($op, $search) .')'; From 9dc4cd76b35b4a53346442785a84ef1cedd4b43c Mon Sep 17 00:00:00 2001 From: Hadi Nategh Date: Tue, 19 May 2015 16:12:28 +0000 Subject: [PATCH 23/31] Delay the drag action for d-n-d emails in compose --- mail/js/app.js | 1 + 1 file changed, 1 insertion(+) diff --git a/mail/js/app.js b/mail/js/app.js index 035b088339..5c194e4682 100644 --- a/mail/js/app.js +++ b/mail/js/app.js @@ -4138,6 +4138,7 @@ app.classes.mail = AppJS.extend( cursorAt:{left:2}, //cancel dragging on close button to avoid conflict with close action cancel:'.ms-close-btn', + delay: '300', /** * function to act on draggable item on revert's event * @returns {Boolean} return true From a480dfc022532562f782ac6053367e72e5b00b04 Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Tue, 19 May 2015 18:56:12 +0000 Subject: [PATCH 24/31] Encrypt toggle-button in compose to switch PGP encrypted mail on and off --- mail/inc/class.mail_compose.inc.php | 9 +++ mail/js/app.js | 116 +++++++++++++++------------- phpgwapi/js/jsapi/app_base.js | 41 ++++++++++ phpgwapi/js/jsapi/egw_message.js | 3 +- 4 files changed, 115 insertions(+), 54 deletions(-) diff --git a/mail/inc/class.mail_compose.inc.php b/mail/inc/class.mail_compose.inc.php index e6e0efcc62..291b3c02c8 100644 --- a/mail/inc/class.mail_compose.inc.php +++ b/mail/inc/class.mail_compose.inc.php @@ -115,6 +115,15 @@ class mail_compose 'hint' => 'Send', 'toolbarDefault' => true ), + 'pgp' => array( + 'caption' => 'Encrypt', + 'icon' => 'lock', + 'group' => ++$group, + 'onExecute' => 'javaScript:app.mail.togglePgpEncrypt', + 'hint' => 'Send message PGP encrypted: requires keys from all recipients!', + 'checkbox' => true, + 'toolbarDefault' => true + ), 'button[saveAsDraft]' => array( 'caption' => 'Save', 'icon' => 'save', diff --git a/mail/js/app.js b/mail/js/app.js index 5c194e4682..a7dfab2ee0 100644 --- a/mail/js/app.js +++ b/mail/js/app.js @@ -157,14 +157,7 @@ app.classes.mail = AppJS.extend( jQuery('iframe#mail-index_messageIFRAME').on('load', function() { // decrypt preview body if mailvelope is available - if (typeof mailvelope !== 'undefined') { - self.mailvelopeDisplay.call(self); - } else { - jQuery(window).on('mailvelope', function() - { - self.mailvelopeDisplay.call(self); - }); - } + self.mailvelopeAvailable(self.mailvelopeDisplay); self.mail_prepare_print(); }); var nm = this.et2.getWidgetById(this.nm_index); @@ -194,14 +187,7 @@ app.classes.mail = AppJS.extend( jQuery('iframe#mail-display_mailDisplayBodySrc').on('load', function() { // encrypt body if mailvelope is available - if (typeof mailvelope !== 'undefined') { - self.mailvelopeDisplay.call(self); - } else { - jQuery(window).on('mailvelope', function() - { - self.mailvelopeDisplay.call(self); - }); - } + self.mailvelopeAvailable(self.mailvelopeDisplay); self.mail_prepare_print(); }); @@ -215,10 +201,9 @@ app.classes.mail = AppJS.extend( ); break; case 'mail.compose': - if (typeof mailvelope !== 'undefined') { - this.mailvelopeCompose(); - } else { - jQuery(window).on('mailvelope', jQuery.proxy(this.mailvelopeCompose, this)); + if (this.et2.getWidgetById('composeToolbar')._actionManager.getActionById('pgp').checked) + { + this.mailvelopeAvailable(this.mailvelopeCompose); } // use a wrapper on a different url to be able to use a different fpm pool et2.menuaction = 'mail_compose::ajax_send'; @@ -4349,9 +4334,10 @@ app.classes.mail = AppJS.extend( /** * Called on load of preview or display iframe, if mailvelope is available * + * @param {Keyring} _keyring Mailvelope keyring to use * @ToDo signatures */ - mailvelopeDisplay: function() + mailvelopeDisplay: function(_keyring) { var self = this; var mailvelope = window.mailvelope; @@ -4360,19 +4346,12 @@ app.classes.mail = AppJS.extend( if (armored == "" || armored.indexOf('-----BEGIN PGP MESSAGE-----') === -1) return; - mailvelope.getKeyring('mailvelope').then(function(_keyring) + var container = iframe.parent()[0]; + var container_selector = container.id ? '#'+container.id : 'div.mailDisplayContainer'; + mailvelope.createDisplayContainer(container_selector, armored, _keyring).then(function() { - var container = iframe.parent()[0]; - var container_selector = container.id ? '#'+container.id : 'div.mailDisplayContainer'; - mailvelope.createDisplayContainer(container_selector, armored, _keyring).then(function() - { - // hide our iframe to give space for mailvelope iframe with encrypted content - iframe.hide(); - }, - function(_err) - { - self.egw.message(_err.message, 'error'); - }); + // hide our iframe to give space for mailvelope iframe with encrypted content + iframe.hide(); }, function(_err) { @@ -4389,36 +4368,67 @@ app.classes.mail = AppJS.extend( /** * Called on compose, if mailvelope is available + * + * @param {Keyring} _keyring Mailvelope keyring to use */ - mailvelopeCompose: function() + mailvelopeCompose: function(_keyring) { - var self = this; - var mailvelope = window.mailvelope; - delete this.mailvelope_editor; - mailvelope.getKeyring('mailvelope').then(function(_keyring) + + // currently Mailvelope only supports plain-text, to this is unnecessary + var mimeType = this.et2.getWidgetById('mimeType'); + var is_html = mimeType.get_value(); + var container = is_html ? '.mailComposeHtmlContainer' : '.mailComposeTextContainer'; + var editor = this.et2.getWidgetById(is_html ? 'mail_htmltext' : 'mail_plaintext'); + + var self = this; + mailvelope.createEditorContainer(container, _keyring, { + predefinedText: editor.get_value() + }).then(function(_editor) { - var is_html = self.et2.getWidgetById('mimeType').get_value(); - var container = is_html ? '.mailComposeHtmlContainer' : '.mailComposeTextContainer'; - var editor = self.et2.getWidgetById(is_html ? 'mail_htmltext' : 'mail_plaintext'); - mailvelope.createEditorContainer(container, _keyring, { - predefinedText: editor.get_value() - }).then(function(_editor) - { - self.mailvelope_editor = _editor; - editor.set_disabled(true); - }, - function(_err) - { - self.egw.message(_err.message, 'error'); - }); + self.mailvelope_editor = _editor; + editor.set_disabled(true); + mimeType.set_readonly(true); }, function(_err) { - self.egw.message(keyringId+': '+_err.message, 'error'); + self.egw.message(_err.message, 'error'); }); }, + /** + * Switch sending PGP encrypted mail on and off + * + * @param {object} _action toolbar action + */ + togglePgpEncrypt: function (_action) + { + if (_action.checked) + { + if (typeof mailvelope == 'undefined') + { + this.egw.message(this.egw.lang('You need to install Mailvelope plugin available for Chrome and Firefox and enable it for your domain.')+ + "\n"+this.egw.lang('Download from %1','mailvelope.com'), 'info'); + return; + } + var mimeType = this.et2.getWidgetById('mimeType'); + // currently Mailvelope only supports plain-text, switch to it if necessary + if (mimeType.get_value()) + { + mimeType.set_value(false); + this.et2._inst.submit(); + return; // ToDo: do that without reload + } + this.mailvelopeAvailable(this.mailvelopeCompose); + // ToDo: check recipients + } + else + { + // switch Mailvelop off again + this.et2._inst.submit(); // ToDo: do that without reload + } + }, + /** * Set the relevant widget to toolbar actions and submit * @param {type} _action toolbar action diff --git a/phpgwapi/js/jsapi/app_base.js b/phpgwapi/js/jsapi/app_base.js index 8214fd4f73..9f2d662f80 100644 --- a/phpgwapi/js/jsapi/app_base.js +++ b/phpgwapi/js/jsapi/app_base.js @@ -825,5 +825,46 @@ var AppJS = Class.extend( egw.refresh(data.msg||'',ids[0],ids[1],'update'); }).sendRequest(true); } + }, + + /** + * Check if Mailvelope is available, open (or create) "egroupware" keyring and call callback with it + * + * @param {function} _callback called if and only if mailvelope is available (context is this!) + */ + mailvelopeAvailable: function(_callback) + { + var self = this; + if (typeof mailvelope !== 'undefined') + { + self._mailvelopeOpenKeyring.call(self, _callback); + } + else + { + jQuery(window).on('mailvelope', function() + { + self._mailvelopeOpenKeyring.call(self, _callback); + }); + } + }, + + /** + * Open (or create) "egroupware" keyring and call callback with it + * + * @param {function} _callback called if and only if mailvelope is available (context is this!) + */ + _mailvelopeOpenKeyring: function(_callback) + { + var callback = _callback; + var self = this; + + mailvelope.getKeyring('mailvelope').then(function(_keyring) + { + callback.call(self, _keyring); + }, + function(_err) + { + self.egw.message(_err.message, 'error'); + }); } }); diff --git a/phpgwapi/js/jsapi/egw_message.js b/phpgwapi/js/jsapi/egw_message.js index bb6a3dd576..87fef38f07 100644 --- a/phpgwapi/js/jsapi/egw_message.js +++ b/phpgwapi/js/jsapi/egw_message.js @@ -109,9 +109,10 @@ egw.extend('message', egw.MODULE_WND_LOCAL, function(_app, _wnd) if (matches) { var parts = _msg.split(matches[0]); + var href = html_entity_decode(matches[1]); msg_div.text(parts[0]); msg_div.append(jQuery(_wnd.document.createElement('a')) - .attr('href', html_entity_decode(matches[1])) + .attr({href: href, target: href.indexOf(egw.webserverUrl) != 0 ? '_blank' : '_self'}) .text(matches[2])); msg_div.append(jQuery(_wnd.document.createElement('span')).text(parts[1])); } From 69f1fc46963601947e30fc10df73d13142d8bb86 Mon Sep 17 00:00:00 2001 From: Nathan Gray Date: Tue, 19 May 2015 19:24:02 +0000 Subject: [PATCH 25/31] Fix some problems with merging into email files: - Use correct merge sub-class when merging multiple entries - Accept merge placeholders in to/cc/bcc fields (displayed as invalid, but still accepted) - Use merge placeholders to pull addresses from associated entry --- etemplate/inc/class.bo_merge.inc.php | 2 +- etemplate/inc/class.etemplate_widget_taglist.inc.php | 5 ++++- mail/inc/class.mail_compose.inc.php | 12 ++++++++---- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/etemplate/inc/class.bo_merge.inc.php b/etemplate/inc/class.bo_merge.inc.php index 340966d3f9..9c29deb131 100644 --- a/etemplate/inc/class.bo_merge.inc.php +++ b/etemplate/inc/class.bo_merge.inc.php @@ -1883,7 +1883,7 @@ abstract class bo_merge $action['nm_action'] = 'long_task'; $action['popup'] = egw_link::get_registry('mail', 'edit_popup'); $action['message'] = lang('insert in %1',egw_vfs::decodePath($file['name'])); - $action['menuaction'] = 'mail.mail_compose.ajax_merge&document='.$file['path']; + $action['menuaction'] = 'mail.mail_compose.ajax_merge&document='.$file['path'].'&merge='. get_called_class(); } /** diff --git a/etemplate/inc/class.etemplate_widget_taglist.inc.php b/etemplate/inc/class.etemplate_widget_taglist.inc.php index b55ef2a6a6..535f374b1b 100644 --- a/etemplate/inc/class.etemplate_widget_taglist.inc.php +++ b/etemplate/inc/class.etemplate_widget_taglist.inc.php @@ -120,7 +120,10 @@ class etemplate_widget_taglist extends etemplate_widget self::set_validation_error($form_name,lang("'%1' is NOT allowed ('%2')!",$val,implode("','",array_keys($lists))),''); } } - else if($this->type == 'taglist-email' && !preg_match(etemplate_widget_url::EMAIL_PREG, $val)) + else if($this->type == 'taglist-email' && !preg_match(etemplate_widget_url::EMAIL_PREG, $val) && + // Allow merge placeholders. Might be a better way to do this though. + !preg_match('/{{.+}}|\$\$.+\$\$/',$val) + ) { self::set_validation_error($form_name,lang("'%1' has an invalid format",$val),''); } diff --git a/mail/inc/class.mail_compose.inc.php b/mail/inc/class.mail_compose.inc.php index 291b3c02c8..8c7e8d3cc7 100644 --- a/mail/inc/class.mail_compose.inc.php +++ b/mail/inc/class.mail_compose.inc.php @@ -3382,7 +3382,14 @@ class mail_compose public function ajax_merge($contact_id) { $response = egw_json_response::get(); - $document_merge = new addressbook_merge(); + if(class_exists($_REQUEST['merge']) && is_subclass_of($_REQUEST['merge'],'bo_merge')) + { + $document_merge = new $_REQUEST['merge'](); + } + else + { + $document_merge = new addressbook_merge(); + } $this->mail_bo->openConnection(); if(($error = $document_merge->check_document($_REQUEST['document'],''))) @@ -3391,9 +3398,6 @@ class mail_compose return; } - // Merge does not work correctly (missing to) if current app is not addressbook - //$GLOBALS['egw_info']['flags']['currentapp'] = 'addressbook'; - // Actually do the merge $folder = $merged_mail_id = null; $results = $this->mail_bo->importMessageToMergeAndSend( From 1beba97dcfbac3ec4dd930cccc5017a936f2264f Mon Sep 17 00:00:00 2001 From: Nathan Gray Date: Tue, 19 May 2015 19:58:43 +0000 Subject: [PATCH 26/31] Fixed custom field select options removed empty label if there was white space after options --- admin/inc/class.customfields.inc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/admin/inc/class.customfields.inc.php b/admin/inc/class.customfields.inc.php index c36211e36e..ebddb2a213 100644 --- a/admin/inc/class.customfields.inc.php +++ b/admin/inc/class.customfields.inc.php @@ -324,7 +324,7 @@ class customfields } else { - foreach(explode("\n",$content['cf_values']) as $line) + foreach(explode("\n",trim($content['cf_values'])) as $line) { list($var,$value) = explode('=',trim($line),2); $var = trim($var); From 8e8a3b7b61e6a748bb03fd9f6ed4d89d5e7bdc92 Mon Sep 17 00:00:00 2001 From: Nathan Gray Date: Tue, 19 May 2015 20:13:38 +0000 Subject: [PATCH 27/31] Fix not found class, name has changed. --- infolog/inc/class.infolog_import_infologs_csv.inc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infolog/inc/class.infolog_import_infologs_csv.inc.php b/infolog/inc/class.infolog_import_infologs_csv.inc.php index 38d21bc87b..8b01555ac5 100644 --- a/infolog/inc/class.infolog_import_infologs_csv.inc.php +++ b/infolog/inc/class.infolog_import_infologs_csv.inc.php @@ -509,7 +509,7 @@ class infolog_import_infologs_csv implements importexport_iface_import_plugin { if (!is_object($boprojects)) { - $boprojects =& CreateObject('projectmanager.boprojectmanager'); + $boprojects = new projectmanager_bo(); } if (($projects = $boprojects->search(array('pm_number' => $num_or_title))) || ($projects = $boprojects->search(array('pm_title' => $num_or_title)))) From 517286fdab0313dfc4f7d0876bb16480b80036aa Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Tue, 19 May 2015 20:23:38 +0000 Subject: [PATCH 28/31] using now a domain-specific "egroupware" keyring, instead of default "mailvelope", which only works on localhost, plus improved instructions --- mail/js/app.js | 5 +++-- phpgwapi/js/jsapi/app_base.js | 28 ++++++++++++++++++++++++++-- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/mail/js/app.js b/mail/js/app.js index a7dfab2ee0..37ba69cb22 100644 --- a/mail/js/app.js +++ b/mail/js/app.js @@ -4407,8 +4407,9 @@ app.classes.mail = AppJS.extend( { if (typeof mailvelope == 'undefined') { - this.egw.message(this.egw.lang('You need to install Mailvelope plugin available for Chrome and Firefox and enable it for your domain.')+ - "\n"+this.egw.lang('Download from %1','mailvelope.com'), 'info'); + this.egw.message(this.egw.lang('You need to install Mailvelope plugin available for Chrome and Firefox from %1.','mailvelope.com')+"\n"+ + this.egw.lang('Add your domain as "%1" in options to list of email providers and enable API.', + '*.'+this._mailvelopeDomain()), 'info'); return; } var mimeType = this.et2.getWidgetById('mimeType'); diff --git a/phpgwapi/js/jsapi/app_base.js b/phpgwapi/js/jsapi/app_base.js index 9f2d662f80..dd5c814011 100644 --- a/phpgwapi/js/jsapi/app_base.js +++ b/phpgwapi/js/jsapi/app_base.js @@ -858,13 +858,37 @@ var AppJS = Class.extend( var callback = _callback; var self = this; - mailvelope.getKeyring('mailvelope').then(function(_keyring) + mailvelope.getKeyring('egroupware').then(function(_keyring) { callback.call(self, _keyring); }, function(_err) { - self.egw.message(_err.message, 'error'); + mailvelope.createKeyring('egroupware').then(function(_keyring) + { + self.egw.message(self.egw.lang('Keyring "%1" created.', self._mailvelopeDomain()+' (egroupware)')+"\n\n"+ + self.egw.lang('Please click on lock icon in lower right corner to create or import a key:')+"\n"+ + self.egw.lang("Go to Key Management and create a new key-pair or import your existing one.")+"\n\n"+ + self.egw.lang("You will NOT be able to send or receive encrypted mails before completing that step!"), 'info'); + + callback.call(self, _keyring); + }, + function(_err) + { + self.egw.message(_err.message, 'error'); + }); }); + }, + + /** + * Mailvelope uses Domain without first part: eg. "stylite.de" for "egw.stylite.de" + * + * @returns {string} + */ + _mailvelopeDomain: function() + { + var parts = document.location.hostname.split('.'); + if (parts.length > 1) parts.shift(); + return parts.join('.'); } }); From 8ade7b39262e52fe54fc8c41ff60b31c552ed297 Mon Sep 17 00:00:00 2001 From: Nathan Gray Date: Tue, 19 May 2015 20:58:30 +0000 Subject: [PATCH 29/31] Fix 'No project' column filter --- timesheet/inc/class.timesheet_bo.inc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/timesheet/inc/class.timesheet_bo.inc.php b/timesheet/inc/class.timesheet_bo.inc.php index f70f3400a7..32ef2ff8d3 100644 --- a/timesheet/inc/class.timesheet_bo.inc.php +++ b/timesheet/inc/class.timesheet_bo.inc.php @@ -1042,7 +1042,7 @@ class timesheet_bo extends so_sql_cf $data =& $this->data; } // allways store ts_project to be able to search for it, even if no custom project is set - if (empty($data['ts_project'])) + if (empty($data['ts_project']) && !is_null($data['ts_project'])) { $data['ts_project'] = $data['pm_id'] ? egw_link::title('projectmanager', $data['pm_id']) : ''; } From c7765473f353a8e1f6268316773e5440606ae206 Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Wed, 20 May 2015 07:26:15 +0000 Subject: [PATCH 30/31] - fix autosave and save as draft to store encrypted content - fix inline reply to encrypted message to clientside decrypt message and add signature --- mail/js/app.js | 86 ++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 79 insertions(+), 7 deletions(-) diff --git a/mail/js/app.js b/mail/js/app.js index 37ba69cb22..4a205b6f3e 100644 --- a/mail/js/app.js +++ b/mail/js/app.js @@ -201,7 +201,9 @@ app.classes.mail = AppJS.extend( ); break; case 'mail.compose': - if (this.et2.getWidgetById('composeToolbar')._actionManager.getActionById('pgp').checked) + if (this.et2.getWidgetById('composeToolbar')._actionManager.getActionById('pgp').checked || + this.et2.getArrayMgr('content').data.mail_plaintext && + this.et2.getArrayMgr('content').data.mail_plaintext.indexOf(this.begin_pgp_message) != -1) { this.mailvelopeAvailable(this.mailvelopeCompose); } @@ -3268,6 +3270,22 @@ app.classes.mail = AppJS.extend( var self = this; if (content) { + // if we compose an encrypted message, we have to get the encrypted content + if (this.mailvelope_editor) + { + this.mailvelope_editor.encrypt([]).then(function(_armored) + { + content['mail_plaintext'] = _armored; + self.egw.json('mail.mail_compose.ajax_saveAsDraft',[content, action],function(_data){ + self.savingDraft_response(_data,action); + }).sendRequest(true); + }, function(_err) + { + self.egw.message(_err.message, 'error'); + }); + return false; + } + this.egw.json('mail.mail_compose.ajax_saveAsDraft',[content, action],function(_data){ self.savingDraft_response(_data,action); }).sendRequest(true); @@ -4331,6 +4349,21 @@ app.classes.mail = AppJS.extend( } }, + /** + * Mailvelope (clientside PGP) integration: + * - detect Mailvelope plugin and open "egroupware" keyring (app_base.mailvelopeAvailable and _mailvelopeOpenKeyring) + * - display and preview of encrypted messages (mailvelopeDisplay) + * - button to toggle between regular and encrypted mail (togglePgpEncrypt) + * - compose encrypted messages (mailvelopeCompose, compose_submitAction) + * - fix autosave and save as draft to store encrypted content (saveAsDraft) + * - fix inline reply to encrypted message to clientside decrypt message and add signature (mailvelopeCompose) + * @todo check recipients for key available and warn user if not + * @todo lookup missing keys in addressbook, DANE DNS recored, maybe keyserver + * @todo offer user to store his public key in accounts addressbook (ask admin to make it user-editable) and DANE + */ + begin_pgp_message: '-----BEGIN PGP MESSAGE-----', + end_pgp_message: '-----END PGP MESSAGE-----', + /** * Called on load of preview or display iframe, if mailvelope is available * @@ -4344,7 +4377,7 @@ app.classes.mail = AppJS.extend( var iframe = jQuery('iframe#mail-display_mailDisplayBodySrc,iframe#mail-index_messageIFRAME'); var armored = iframe.contents().find('td.td_display > pre').text().trim(); - if (armored == "" || armored.indexOf('-----BEGIN PGP MESSAGE-----') === -1) return; + if (armored == "" || armored.indexOf(this.begin_pgp_message) === -1) return; var container = iframe.parent()[0]; var container_selector = container.id ? '#'+container.id : 'div.mailDisplayContainer'; @@ -4380,11 +4413,34 @@ app.classes.mail = AppJS.extend( var is_html = mimeType.get_value(); var container = is_html ? '.mailComposeHtmlContainer' : '.mailComposeTextContainer'; var editor = this.et2.getWidgetById(is_html ? 'mail_htmltext' : 'mail_plaintext'); + var options = { predefinedText: editor.get_value() }; + + // check if we have some sort of reply to an encrypted message + // --> parse header, encrypted mail to quote and signature so Mailvelope understands it + var start_pgp = options.predefinedText.indexOf(this.begin_pgp_message); + if (start_pgp != -1) + { + var end_pgp = options.predefinedText.indexOf(this.end_pgp_message); + if (end_pgp != -1) + { + options = { + quotedMailHeader: options.predefinedText.slice(0, start_pgp).replace(/> /mg, '').trim()+"\n", + quotedMail: options.predefinedText.slice(start_pgp, end_pgp+this.end_pgp_message.length+1).replace(/> /mg, ''), + quotedMailIndent: start_pgp != 0, + predefinedText: options.predefinedText.slice(end_pgp+this.end_pgp_message.length+1).replace(/^> \s*/m,'') + }; + // set encrypted checkbox, if not already set + var pgp_action = this.et2.getWidgetById('composeToolbar')._actionManager.getActionById('pgp'); + if (pgp_action && !pgp_action.checked) + { + pgp_action.set_checked(true); + jQuery('button#composeToolbar-pgp').toggleClass('toolbar_toggled'); + } + } + } var self = this; - mailvelope.createEditorContainer(container, _keyring, { - predefinedText: editor.get_value() - }).then(function(_editor) + mailvelope.createEditorContainer(container, _keyring, options).then(function(_editor) { self.mailvelope_editor = _editor; editor.set_disabled(true); @@ -4425,13 +4481,29 @@ app.classes.mail = AppJS.extend( } else { - // switch Mailvelop off again - this.et2._inst.submit(); // ToDo: do that without reload + // switch Mailvelop off again, but warn user he will loose his content + var self = this; + et2_dialog.show_dialog(function (_button_id) + { + if (_button_id == et2_dialog.YES_BUTTON ) + { + self.et2._inst.submit(); + } + else + { + self.et2.getWidgetById('composeToolbar')._actionManager.getActionById('pgp').set_checked(true); + jQuery('button#composeToolbar-pgp').toggleClass('toolbar_toggled'); + } + }, + this.egw.lang('You will loose current message body, unless you save it to your clipboard!'), + this.egw.lang('Switch off encryption?'), + {}, et2_dialog.BUTTON_YES_NO, et2_dialog.WARNING_MESSAGE, undefined, this.egw); } }, /** * Set the relevant widget to toolbar actions and submit + * * @param {type} _action toolbar action */ compose_submitAction: function (_action) From 84b9f579c7a190960228c583166666e705d61bb7 Mon Sep 17 00:00:00 2001 From: Hadi Nategh Date: Wed, 20 May 2015 10:26:08 +0000 Subject: [PATCH 31/31] Add missing action icons in compose toolbar, and fix dragging icon has no height --- etemplate/templates/default/etemplate2.css | 1 + .../templates/default/images/filemanager.png | Bin 0 -> 2879 bytes phpgwapi/templates/default/images/high.png | Bin 0 -> 1368 bytes .../templates/default/images/to_calendar.png | Bin 0 -> 2255 bytes 4 files changed, 1 insertion(+) create mode 100644 phpgwapi/templates/default/images/filemanager.png create mode 100644 phpgwapi/templates/default/images/high.png create mode 100644 phpgwapi/templates/default/images/to_calendar.png diff --git a/etemplate/templates/default/etemplate2.css b/etemplate/templates/default/etemplate2.css index f15b020f3c..15aca7b097 100644 --- a/etemplate/templates/default/etemplate2.css +++ b/etemplate/templates/default/etemplate2.css @@ -236,6 +236,7 @@ button.et2_button_with_image { background-repeat: no-repeat; background-position: 4px center; background-size: 16px; + height: 24px; } /* et2_box_widget ###*/ button[id="cancel"], diff --git a/phpgwapi/templates/default/images/filemanager.png b/phpgwapi/templates/default/images/filemanager.png new file mode 100644 index 0000000000000000000000000000000000000000..0644f13170bab112a3f4a4ec75e19f4c54e08d27 GIT binary patch literal 2879 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 Q*#&%l^xOYvAN}(G0XoHNM*si- literal 0 HcmV?d00001 diff --git a/phpgwapi/templates/default/images/high.png b/phpgwapi/templates/default/images/high.png new file mode 100644 index 0000000000000000000000000000000000000000..5ceeece043c6d11e317209732722633880068031 GIT binary patch literal 1368 zcmV-e1*iInP)AAo6~})w_s+fdecbnKzZZW!M{x{MLK8xSOF#~URD?jHSR%A4EV?O5DN?Hl36)J3 zNTo!g0>p|PD{uH{k&r-wN@m&;NXlaL)14R>76T0ak-Ekf$&*UaACv^v<1ZFL>&TE|-bC)}<$*Q1U2MJ&Zy0 zjr2bj*DAj;`L+En6(710UP+P;IBUofC`RJv_dI>@S#*>CPQTe~E$S(G9WYllk)9x~ zg;WPaOoq4TSGP?GcN&p>%N>(rFW`u;hrv|=(iKN*F6G-VQp$Dz$kxYqN4$3eKKhzX z4{ofl8;O38`#%9ZPf#5xQ5%cN4dI?&-}9_&|C^Wvl^)d!t;L)=_roOos zms8wxc>iPe)3>;|kh1vxr(Agd4En-6Vx`HgdmE2B$Chg|aIPD+11w}6!~ExMn$ru^ zZ;Jo6`QgoS_V`Jx2d?;>RkTb(g|X><#JJ5R)5* z4jh@>mw)gE7Vx78FMxs%qFg6m2EIPc#AM^4JY~DPO2MVIdhO%{Nh@RiY@3aDHjX#; zh4TEzvxEW2H2cBKwA*L}e6hsvEsLVDDSA0eSMz=ufU!8+9X4YedG0V;j^F?2uFdxC zzv9I;$Sl^Zg{Y+^nS=B*4gWmP!9$htEaOh2uaF{}W!+LMflvaSA-DXj@%O%o4qwg~ zLSYCL6vDM}w=>_tS3m_g52YQ;V$1gE%`8Lyn1wVt2MacLnA9Nzhwgv;_P+G>cj>K^ z34DVpf%4WEgwi02AQVslKIky{i?}b<9Mb7(rdBCPt2Lf436#WW)b{&sm>8-xo*rJ~CQx$4^rp z?M0E}Pd5pc!1psas&Gd`&i9QoaNtP>cRqkR{6}a3N)SfSy9ojXf0l66gH*%1Z6Fmy z5d=Qi)wVfh+4X?-iXl^lo(S>E-D=xUw}|N*YMJj8h!Cg(MFHgsl&jEFgL(ss1;{-x zIn1Ahb_?2zu-t*g4y-t^z%b05sJM6DOSby)y7E}xPlo*FC;y>T7lcwk8sY^d&@tq7 zq+16U*Mk}W2O@3Za*0jOVV5K}k+=-(#T2x`O*5tjsn^ziW}F83Kp%J%YGMSnnn5AEL7HM#>w;C=8Ae#1-&7B;DO5 aq~w1$)+bQ7CY!Cbt{4CS2wX`- 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 d0bu{H<=;jhCx(!@qyhi{002ovPDHLkV1jDzO