From eb6fd7cf3d31158a43f85e87e290f6be03482567 Mon Sep 17 00:00:00 2001
From: Hadi Nategh <hn@stylite.de>
Date: Tue, 17 Feb 2015 10:42:00 +0000
Subject: [PATCH 01/43] * Mobile theme: Login page style improvement

---
 pixelegg/css/mobile.css               | 101 +++++++++++++++++++
 pixelegg/css/mobile.less              | 136 +++++++++++++++++++++++++-
 pixelegg/templates/pixelegg/login.tpl |   6 +-
 3 files changed, 239 insertions(+), 4 deletions(-)

diff --git a/pixelegg/css/mobile.css b/pixelegg/css/mobile.css
index 9c73171882..cde3df2ba2 100644
--- a/pixelegg/css/mobile.css
+++ b/pixelegg/css/mobile.css
@@ -6248,10 +6248,83 @@ a.textSidebox {
 /*@import "../less/layout_nextmatch.less";*/
 /*@import "../less/layout_footer.less";*/
 /*@import "../less/layout_dialog.less";*/
+/********************************/
+/*								*/
+/*		MEDIA DEFINITION		*/
+/*								*/
+/********************************/
+/*Tablets Max-Width*/
+/*Smartphones Max-Width*/
+/*Smartphones Min-Width*/
+/*All devices portrait mode*/
+/*All devices landscape mode*/
+/*Tablets landscape mode*/
+/*Tablets Portrait*/
 @media all {
   body {
     background-color: transparent;
   }
+  body div#loginMainDiv #divAppIconBar #divLogo img[src$="svg"] {
+    width: 40%;
+    margin-top: 5px;
+  }
+  body div#loginMainDiv div#centerBox {
+    position: absolute;
+    margin: 0;
+    width: 100%;
+    background-color: transparent;
+    padding: 0;
+    -webkit-border-top-right-radius: 0;
+    -webkit-border-bottom-right-radius: 0;
+    -webkit-border-bottom-left-radius: 0;
+    -webkit-border-top-left-radius: 0;
+    -moz-border-radius-topright: 0;
+    -moz-border-radius-bottomright: 0;
+    -moz-border-radius-bottomleft: 0;
+    -moz-border-radius-topleft: 0;
+    border-top-right-radius: 0;
+    border-bottom-right-radius: 0;
+    border-bottom-left-radius: 0;
+    border-top-left-radius: 0;
+    background-color: none;
+    background-image: none;
+    background-repeat: none;
+    border: none;
+    border-radius: none;
+  }
+  body div#loginMainDiv div#centerBox form {
+    margin-top: -2em;
+    margin-right: 3em;
+  }
+  body div#loginMainDiv div#centerBox form table.divLoginbox {
+    width: 100%;
+    float: left;
+  }
+  body div#loginMainDiv div#centerBox form table.divLoginbox tr.hiddenCredential {
+    display: none;
+  }
+  body div#loginMainDiv div#centerBox form table.divLoginbox input[type="submit"] {
+    font-size: xx-large;
+  }
+  body div#loginMainDiv div#centerBox form table.divLoginbox input,
+  body div#loginMainDiv div#centerBox form table.divLoginbox select {
+    width: 100%;
+    height: 60px;
+  }
+  body div#loginMainDiv div#centerBox form table.divLoginbox td {
+    font-size: 300%;
+    padding: 0.8%;
+  }
+  body div#loginMainDiv div#centerBox form table.divLoginbox td.registration {
+    font-size: 180%;
+  }
+  body div#loginMainDiv div#centerBox form table.divLoginbox td select {
+    background-size: 48px auto;
+  }
+  body div#loginMainDiv div#centerBox #loginCdMessage {
+    font-size: large;
+    padding: 0;
+  }
   body div.egw_fw_mobile_iOS_popup_appHeader {
     padding-top: 15px;
   }
@@ -6966,3 +7039,31 @@ a.textSidebox {
     background-position: center;
   }
 }
+@media only screen and (max-device-width : 1024px) and (orientation : portrait) {
+  body div#loginMainDiv #divAppIconBar #divLogo img[src$="svg"] {
+    width: 70%;
+  }
+  body div#loginMainDiv div#centerBox form table.divLoginbox {
+    width: 100%;
+    float: left;
+  }
+  body div#loginMainDiv div#centerBox form table.divLoginbox input[type="submit"] {
+    font-size: xx-large;
+  }
+  body div#loginMainDiv div#centerBox form table.divLoginbox input,
+  body div#loginMainDiv div#centerBox form table.divLoginbox select {
+    width: 100%;
+    height: 80px;
+  }
+  body div#loginMainDiv div#centerBox form table.divLoginbox td {
+    font-size: 400%;
+    padding: 0.8%;
+  }
+  body div#loginMainDiv div#centerBox form table.divLoginbox td.registration {
+    font-size: 250%;
+  }
+  body div#loginMainDiv div#centerBox #loginCdMessage {
+    font-size: xx-large;
+    padding: 0;
+  }
+}
diff --git a/pixelegg/css/mobile.less b/pixelegg/css/mobile.less
index 6fa0b774aa..63aac28461 100644
--- a/pixelegg/css/mobile.less
+++ b/pixelegg/css/mobile.less
@@ -12,8 +12,103 @@
 
 @import "pixelegg.less";
 
+/********************************/
+/*								*/
+/*		MEDIA DEFINITION		*/
+/*								*/
+/********************************/
+/*Tablets Max-Width*/
+@tablet-max: 1024px;
+/*Smartphones Max-Width*/
+@smartphone-max: 768px;
+/*Smartphones Min-Width*/
+@smartphone-min: 321px;
+/*All devices portrait mode*/
+@handheld-portrait: ~"only screen and (max-device-width : @{tablet-max}) and (orientation : portrait)";
+/*All devices landscape mode*/
+@handheld-landscape: ~"only screen and (max-device-width : @{tablet-max}) and (orientation : landscape)";
+/*Tablets landscape mode*/
+@tablet-portrait: ~"only screen and (max-device-width : @{tablet-max}) and (min-width: @{smartphone-max}) and (orientation : landscape)";
+/*Tablets Portrait*/
+@tablet-portrait: ~"only screen and (max-device-width : @{tablet-max}) and (min-width: @{smartphone-max}) and (orientation : portrait)";
+
+
 @media all {
 	body{
+
+		div#loginMainDiv{
+			#divAppIconBar {
+				#divLogo img[src$="svg"] {
+					width:40%;
+					margin-top: 5px;
+				}
+			}
+			div#centerBox{
+				position:absolute;
+				margin: 0;
+				width: 100%;
+				background-color: transparent;
+				padding: 0;
+				-webkit-border-top-right-radius: 0;
+				-webkit-border-bottom-right-radius:0;
+				-webkit-border-bottom-left-radius: 0;
+				-webkit-border-top-left-radius: 0;
+				-moz-border-radius-topright: 0;
+				-moz-border-radius-bottomright: 0;
+				-moz-border-radius-bottomleft: 0;
+				-moz-border-radius-topleft: 0;
+				border-top-right-radius: 0;
+				border-bottom-right-radius: 0;
+				border-bottom-left-radius: 0;
+				border-top-left-radius: 0;
+				background-color: none;
+				background-image: none;
+				background-image: none;
+				background-image: none;
+				background-image: none;
+				background-image: none;
+				background-image: none;
+				background-image: none;
+				background-repeat: none;
+				border:none;
+				border-radius: none;
+				form {
+					margin-top: -2em;
+					margin-right: 3em;
+
+					table.divLoginbox {
+						width:100%;
+						float:left;
+						tr.hiddenCredential {
+							display:none;
+						}
+						input[type="submit"] {
+							font-size: xx-large;
+
+						}
+						input, select {
+							width:100%;
+							height:60px;
+						}
+						td {
+							font-size: 300%;
+							padding:0.8%;
+							&.registration{
+								font-size: 180%;
+							}
+							select {
+								background-size: 48px auto;
+							}
+						}
+					}
+				}
+				#loginCdMessage {
+					font-size:large;
+					padding:0;
+				}
+			}
+		}
+
 		background-color: transparent;
 		// iOS appHeader class
 		div.egw_fw_mobile_iOS_popup_appHeader{
@@ -733,4 +828,43 @@
 		background-size: 120px 120px;
 		background-position: center;
 	}
-}
\ No newline at end of file
+}
+@media @handheld-portrait
+{
+	body{
+		div#loginMainDiv{
+			#divAppIconBar {
+				#divLogo img[src$="svg"] {
+					width:70%;
+				}
+			}
+			div#centerBox{
+				form {
+					table.divLoginbox {
+						width:100%;
+						float:left;
+						input[type="submit"] {
+							font-size: xx-large;
+
+						}
+						input, select {
+							width:100%;
+							height:80px;
+						}
+						td {
+							font-size: 400%;
+							padding:0.8%;
+							&.registration {
+								font-size: 250%;
+							}
+						}
+					}
+				}
+				#loginCdMessage {
+					font-size:xx-large;
+					padding:0;
+				}
+			}
+		}
+	}
+}
diff --git a/pixelegg/templates/pixelegg/login.tpl b/pixelegg/templates/pixelegg/login.tpl
index 5d6e81cc07..fafe8b6ac4 100644
--- a/pixelegg/templates/pixelegg/login.tpl
+++ b/pixelegg/templates/pixelegg/login.tpl
@@ -14,8 +14,8 @@
                 <tr class="divLoginboxHeader">
                     <td colspan="3">{website_title}</td>
                 </tr>
-                <tr>
-                    <td colspan="2" height="20">
+                <tr class="hiddenCredential">
+                    <td colspan="2" height="20" >
                         <input type="hidden" name="passwd_type" value="text" />
                         <input type="hidden" name="account_type" value="u" />
                     </td>
@@ -57,7 +57,7 @@
                 </tr>
                 <!-- BEGIN registration -->
                 <tr>
-                    <td colspan="3" height="20" align="center">
+                    <td colspan="3" height="20" align="center" class="registration">
                         {lostpassword_link}
                         {lostid_link}
                         {register_link}

From 2a5971258b1c1af5c61cc223ba6253805d75a88c Mon Sep 17 00:00:00 2001
From: Ralf Becker <ralfbecker@outdoor-training.de>
Date: Tue, 17 Feb 2015 10:52:50 +0000
Subject: [PATCH 02/43] * PostgreSQL: fixed not working new installation due to
 access to egw_mailaccounts table prior to creating it: gets now checked, to
 not abort transaction

---
 phpgwapi/inc/class.accounts_sql.inc.php | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/phpgwapi/inc/class.accounts_sql.inc.php b/phpgwapi/inc/class.accounts_sql.inc.php
index 75e8ab97ca..5e98a802fb 100644
--- a/phpgwapi/inc/class.accounts_sql.inc.php
+++ b/phpgwapi/inc/class.accounts_sql.inc.php
@@ -128,7 +128,9 @@ class accounts_sql
 				$this->contacts_table.'.tel_work AS account_phone,';
 				$join = 'LEFT JOIN '.$this->contacts_table.' ON '.$this->table.'.account_id='.$this->contacts_table.'.account_id';
 		}
-		else
+		// during setup emailadmin might not yet be installed and running below query
+		// will abort transaction in PostgreSQL
+		elseif (!isset($GLOBALS['egw_setup']) || in_array(emailadmin_smtp_sql::TABLE, $this->db->table_names(true)))
 		{
 			$extra_cols = emailadmin_smtp_sql::TABLE.'.mail_value AS account_email,';
 			$join = 'LEFT JOIN '.emailadmin_smtp_sql::TABLE.' ON '.$this->table.'.account_id=-'.emailadmin_smtp_sql::TABLE.'.account_id AND mail_type='.emailadmin_smtp_sql::TYPE_ALIAS;

From 3ead887bf7c4eca31012e45d8c917e1464bc0646 Mon Sep 17 00:00:00 2001
From: Hadi Nategh <hn@stylite.de>
Date: Tue, 17 Feb 2015 12:50:55 +0000
Subject: [PATCH 03/43] Fix splitter widget dock to the fullSize if there is no
 size preference yet

---
 etemplate/js/et2_widget_split.js | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/etemplate/js/et2_widget_split.js b/etemplate/js/et2_widget_split.js
index 3dc542b324..c0fe5075ad 100644
--- a/etemplate/js/et2_widget_split.js
+++ b/etemplate/js/et2_widget_split.js
@@ -196,6 +196,12 @@ var et2_split = et2_DOMWidget.extend([et2_IResizeable,et2_IPrint],
 					this.prefSize = pref[this.orientation == "v" ?'sizeLeft' : 'sizeTop'];
 				}
 			}
+			// If there is no preference yet, set it to half size
+			// Otherwise the right pane gets the fullsize
+			else
+			{
+				this.prefSize = this.orientation == "v" ? options.sizeLeft: options.sizeTop;
+			}
 		}
 
 		// Avoid double init

From b18f0ecc76969bd9022b83beb55a5560b84dd879 Mon Sep 17 00:00:00 2001
From: Hadi Nategh <hn@stylite.de>
Date: Tue, 17 Feb 2015 13:10:21 +0000
Subject: [PATCH 04/43] Fix egw_message does not show newlines

---
 phpgwapi/templates/idots/css/traditional.css | 1 +
 pixelegg/css/mobile.css                      | 1 +
 pixelegg/css/pixelegg.css                    | 1 +
 pixelegg/less/layout_dialog.less             | 1 +
 4 files changed, 4 insertions(+)

diff --git a/phpgwapi/templates/idots/css/traditional.css b/phpgwapi/templates/idots/css/traditional.css
index 2ffbf5fd87..327ee1970b 100755
--- a/phpgwapi/templates/idots/css/traditional.css
+++ b/phpgwapi/templates/idots/css/traditional.css
@@ -974,6 +974,7 @@ body > div#egw_message {
 	border: 2px gray solid;
 	min-width: 100px;
 	z-index: 10;
+	white-space: pre-wrap;
 }
 
 /**
diff --git a/pixelegg/css/mobile.css b/pixelegg/css/mobile.css
index cde3df2ba2..1421dda292 100644
--- a/pixelegg/css/mobile.css
+++ b/pixelegg/css/mobile.css
@@ -3815,6 +3815,7 @@ body > div#egw_message {
   z-index: 100000;
   margin: 0px auto;
   max-width: 90%;
+  white-space: pre-wrap;
 }
 /**
  * Less-file for egroupware
diff --git a/pixelegg/css/pixelegg.css b/pixelegg/css/pixelegg.css
index 8ae0b4635e..edb56fb8e1 100644
--- a/pixelegg/css/pixelegg.css
+++ b/pixelegg/css/pixelegg.css
@@ -3804,6 +3804,7 @@ body > div#egw_message {
   z-index: 100000;
   margin: 0px auto;
   max-width: 90%;
+  white-space: pre-wrap;
 }
 /**
  * Less-file for egroupware
diff --git a/pixelegg/less/layout_dialog.less b/pixelegg/less/layout_dialog.less
index 017dda1a0a..35de1905fa 100755
--- a/pixelegg/less/layout_dialog.less
+++ b/pixelegg/less/layout_dialog.less
@@ -506,4 +506,5 @@ body > div#egw_message {
 	z-index: 100000;
 	margin:  0px auto;
 	max-width: 90%;
+	white-space: pre-wrap;
 }

From 39115a09851d66bb5949735091d76ecde4bc080b Mon Sep 17 00:00:00 2001
From: Ralf Becker <ralfbecker@outdoor-training.de>
Date: Tue, 17 Feb 2015 14:35:40 +0000
Subject: [PATCH 05/43] fixed not shown custom-fields in infolog

---
 etemplate/inc/class.etemplate_widget_customfields.inc.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/etemplate/inc/class.etemplate_widget_customfields.inc.php b/etemplate/inc/class.etemplate_widget_customfields.inc.php
index 5897dd8840..cdc34868f5 100644
--- a/etemplate/inc/class.etemplate_widget_customfields.inc.php
+++ b/etemplate/inc/class.etemplate_widget_customfields.inc.php
@@ -129,7 +129,7 @@ class etemplate_widget_customfields extends etemplate_widget_transformer
 		if ($app && $app != 'stylite' && $app != $GLOBALS['egw_info']['flags']['currentapp'] && (
 			$GLOBALS['egw_info']['flags']['currentapp'] == 'etemplate' || !$this->attrs['customfields'] ||
 			etemplate::$hooked
-		))
+		) || !isset($customfields))
 		{
 			// app changed
 			$customfields =& egw_customfields::get($app);

From 93a514993a3b0eb53488ec225e57eaa77e0e520e Mon Sep 17 00:00:00 2001
From: Ralf Becker <ralfbecker@outdoor-training.de>
Date: Tue, 17 Feb 2015 16:21:50 +0000
Subject: [PATCH 06/43] * InfoLog/Addressbook: refresh CRM view if InfoLog was
 edited without having InfoLog tab open

---
 infolog/js/app.js | 9 +++++++--
 1 file changed, 7 insertions(+), 2 deletions(-)

diff --git a/infolog/js/app.js b/infolog/js/app.js
index f8c1e04f45..0718da9430 100644
--- a/infolog/js/app.js
+++ b/infolog/js/app.js
@@ -100,8 +100,13 @@ app.classes.infolog = AppJS.extend(
 				}
 			}
 		}
-		//Refresh handler for infologs integrated in calendar
-		if (_app == 'infolog' && _id && _type !='delete')
+		// Refresh handler for Addressbook CRM view
+		if (_app == 'infolog' && this.et2._inst.app == 'addressbook' && this.et2._inst.name == 'infolog.index')
+		{
+			this.et2._inst.refresh(_msg, _app, _id, _type);
+		}
+		// Refresh handler for infologs integrated in calendar
+		if (_app == 'infolog' && _id && _type != 'delete')
 		{
 			var info_type = egw.dataGetUIDdata(_app+"::"+_id)?egw.dataGetUIDdata(_app+"::"+_id).data.info_type:false;
 			var cal_show = egw.preference('cal_show','infolog')||false;

From 286cca54e10216d9b98bbb0ce6e66313aefa8f22 Mon Sep 17 00:00:00 2001
From: Nathan Gray <nathangray.bsc@gmail.com>
Date: Tue, 17 Feb 2015 16:27:29 +0000
Subject: [PATCH 07/43] Fix link to no longer pre-selecting last used app

---
 etemplate/js/et2_widget_link.js | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/etemplate/js/et2_widget_link.js b/etemplate/js/et2_widget_link.js
index ffe7faadb8..60e5cab6ac 100644
--- a/etemplate/js/et2_widget_link.js
+++ b/etemplate/js/et2_widget_link.js
@@ -582,7 +582,6 @@ var et2_link_entry = et2_inputWidget.extend(
 
 		// Application selection
 		this.app_select = $j(document.createElement("select")).appendTo(this.div)
-			.val(this.options.value.app||'')
 			.change(function(e) {
 				// Clear cache when app changes
 				self.cache = {};
@@ -607,6 +606,11 @@ var et2_link_entry = et2_inputWidget.extend(
 			this.app_select.hide();
 			this.div.addClass("no_app");
 		}
+		else
+		{
+			// Now that options are in, set to last used app
+			this.app_select.val(this.options.value.app||'');
+		}
 
 		// Search input
 		this.search = $j(document.createElement("input"))

From c988b3390748d5f2b17f9ec142d33ad3b5607162 Mon Sep 17 00:00:00 2001
From: Nathan Gray <nathangray.bsc@gmail.com>
Date: Tue, 17 Feb 2015 16:38:44 +0000
Subject: [PATCH 08/43] Allow notes to scroll as needed.

---
 home/templates/default/note.xet | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/home/templates/default/note.xet b/home/templates/default/note.xet
index 55764d923f..0ccddac8d0 100644
--- a/home/templates/default/note.xet
+++ b/home/templates/default/note.xet
@@ -9,5 +9,10 @@
 			<button statustext="Apply the changes" label="Apply" id="apply" image="apply" background_image="1"/>
 			<button statustext="leave without saveing the entry" label="Cancel" id="cancel" onclick="window.close();" image="cancel" background_image="1"/>
 		</hbox>
+		<styles>
+			.home_note_portlet .et2_container > div {
+				overflow: auto;
+			}
+		</styles>
 	</template>
 </overlay>

From ae0c757ea211743cbc396f8c7f8c86d76db13b44 Mon Sep 17 00:00:00 2001
From: Nathan Gray <nathangray.bsc@gmail.com>
Date: Tue, 17 Feb 2015 18:02:10 +0000
Subject: [PATCH 09/43] Fix some nextmatch custom field bugs: - no custom
 fields in nm rows when there were none explicitly selected - Custom field
 column shown even if none were defined

---
 etemplate/js/et2_extension_nextmatch.js | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/etemplate/js/et2_extension_nextmatch.js b/etemplate/js/et2_extension_nextmatch.js
index bc5c81dbbc..b1df8f0888 100644
--- a/etemplate/js/et2_extension_nextmatch.js
+++ b/etemplate/js/et2_extension_nextmatch.js
@@ -1247,6 +1247,12 @@ var et2_nextmatch = et2_DOMWidget.extend([et2_IResizeable, et2_IInput, et2_IPrin
 			// Custom fields get listed separately
 			if(widget.instanceOf(et2_nextmatch_customfields))
 			{
+				if(jQuery.isEmptyObject(widget.customfields))
+				{
+					// No customfields defined, don't show column
+					delete(columns[col.id]);
+					continue;
+				}
 				for(var field_name in widget.customfields)
 				{
 					columns[widget.prefix+field_name] = " - "+widget.customfields[field_name].label;
@@ -2753,6 +2759,7 @@ var et2_nextmatch_customfields = et2_customfields_list.extend(et2_INextmatchHead
 		}
 		var columnMgr = this.nextmatch.dataview.getColumnMgr();
 		var nm_column = null;
+		var set_fields = {};
 		for(var i = 0; i < this.nextmatch.columns.length; i++)
 		{
 			if(this.nextmatch.columns[i].widget == this)
@@ -2816,7 +2823,13 @@ var et2_nextmatch_customfields = et2_customfields_list.extend(et2_INextmatchHead
 			{
 				cf.hide();
 			}
+			else if (jQuery.isEmptyObject(this.options.fields))
+			{
+				// If we're showing it make sure it's set, but only after
+				set_fields[field_name] = true;
+			}
 		}
+		jQuery.extend(this.options.fields, set_fields);
 	},
 
 	/**

From b2d1fa70d2eb8b1758fadac584589b16ac865426 Mon Sep 17 00:00:00 2001
From: Nathan Gray <nathangray.bsc@gmail.com>
Date: Tue, 17 Feb 2015 18:45:14 +0000
Subject: [PATCH 10/43] Fix bug where old image directory was re-scanned when
 changing it, instead of the new image directory

---
 admin/inc/hook_config_validate.inc.php | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/admin/inc/hook_config_validate.inc.php b/admin/inc/hook_config_validate.inc.php
index 341fb9e387..01c9e60b36 100644
--- a/admin/inc/hook_config_validate.inc.php
+++ b/admin/inc/hook_config_validate.inc.php
@@ -33,5 +33,8 @@ function vfs_image_dir($vfs_image_dir)
 	if ($vfs_image_dir != (string)$GLOBALS['egw_info']['server']['vfs_image_dir'])
 	{
 		common::delete_image_map();
+
+		// Set the global now, or the old value will get re-loaded
+		$GLOBALS['egw_info']['server']['vfs_image_dir'] = $vfs_image_dir;
 	}
 }
\ No newline at end of file

From 5bb66358220c30be83e8b0a33afaf4cf4d1441c8 Mon Sep 17 00:00:00 2001
From: Ralf Becker <ralfbecker@outdoor-training.de>
Date: Tue, 17 Feb 2015 22:25:48 +0000
Subject: [PATCH 11/43] harden ldap auth, by removing \000 bytes, causing
 passwords to be not empty by php, but empty to c libaries

---
 phpgwapi/inc/class.auth_ads.inc.php  | 10 ++++---
 phpgwapi/inc/class.auth_ldap.inc.php | 41 ++++++++++++++++------------
 2 files changed, 29 insertions(+), 22 deletions(-)

diff --git a/phpgwapi/inc/class.auth_ads.inc.php b/phpgwapi/inc/class.auth_ads.inc.php
index 43331d71ec..2e4d265175 100644
--- a/phpgwapi/inc/class.auth_ads.inc.php
+++ b/phpgwapi/inc/class.auth_ads.inc.php
@@ -30,17 +30,19 @@ class auth_ads implements auth_backend
 	 * password authentication
 	 *
 	 * @param string $username username of account to authenticate
-	 * @param string $passwd corresponding password
-	 * @param string $passwd_type='text' 'text' for cleartext passwords (default)
+	 * @param string $_passwd corresponding password
+	 * @param string $passwd_type ='text' 'text' for cleartext passwords (default)
 	 * @return boolean true if successful authenticated, false otherwise
 	 */
-	function authenticate($username, $passwd, $passwd_type='text')
+	function authenticate($username, $_passwd, $passwd_type='text')
 	{
 		unset($passwd_type);	// not used by required in function signature
 		if (preg_match('/[()|&=*,<>!~]/',$username))
 		{
 			return False;
 		}
+		// harden ldap auth, by removing \000 bytes, causing passwords to be not empty by php, but empty to c libaries
+		$passwd = str_replace("\000", '', $_passwd);
 
 		$adldap = accounts_ads::get_adldap();
 		// bind with username@ads_domain, only if a non-empty password given, in case anonymous search is enabled
@@ -145,7 +147,7 @@ class auth_ads implements auth_backend
 	 * @param int $account_id account id of user whose passwd should be changed
 	 * @param string $passwd must be cleartext, usually not used, but may be used to authenticate as user to do the change -> ldap
 	 * @param int $lastpwdchange must be a unixtimestamp or 0 (force user to change pw) or -1 for current time
-	 * @param boolean $return_mod=false true return ldap modification instead of executing it
+	 * @param boolean $return_mod =false true return ldap modification instead of executing it
 	 * @return boolean|array true if account_lastpwd_change successful changed, false otherwise or array if $return_mod
 	 */
 	static function setLastPwdChange($account_id=0, $passwd=NULL, $lastpwdchange=NULL, $return_mod=false)
diff --git a/phpgwapi/inc/class.auth_ldap.inc.php b/phpgwapi/inc/class.auth_ldap.inc.php
index 97c074d068..3a236f07e5 100644
--- a/phpgwapi/inc/class.auth_ldap.inc.php
+++ b/phpgwapi/inc/class.auth_ldap.inc.php
@@ -35,15 +35,18 @@ class auth_ldap implements auth_backend
 	/**
 	 * authentication against LDAP
 	 *
-	 * @param string $username username of account to authenticate
-	 * @param string $passwd corresponding password
+	 * @param string $_username username of account to authenticate
+	 * @param string $_passwd corresponding password
 	 * @return boolean true if successful authenticated, false otherwise
 	 */
-	function authenticate($username, $passwd, $passwd_type='text')
+	function authenticate($_username, $_passwd, $passwd_type='text')
 	{
+		unset($passwd_type);	// not used by required by function signature
+
 		// allow non-ascii in username & password
-		$username = translation::convert($username,translation::charset(),'utf-8');
-		$passwd = translation::convert($passwd,translation::charset(),'utf-8');
+		$username = translation::convert($_username,translation::charset(),'utf-8');
+		// harden ldap auth, by removing \000 bytes, causing passwords to be not empty by php, but empty to c libaries
+		$passwd = str_replace("\000", '', translation::convert($_passwd,translation::charset(),'utf-8'));
 
 		if(!$ldap = common::ldapConnect())
 		{
@@ -59,8 +62,8 @@ class auth_ldap implements auth_backend
 		/* find the dn for this uid, the uid is not always in the dn */
 		$attributes	= array('uid','dn','givenName','sn','mail','uidNumber','shadowExpire','homeDirectory');
 
-		$filter = $GLOBALS['egw_info']['server']['ldap_search_filter'] ? $GLOBALS['egw_info']['server']['ldap_search_filter'] : '(uid=%user)';
-		$filter = str_replace(array('%user','%domain'),array(ldap::quote($username),$GLOBALS['egw_info']['user']['domain']),$filter);
+		$filter = str_replace(array('%user','%domain'),array(ldap::quote($username),$GLOBALS['egw_info']['user']['domain']),
+			$GLOBALS['egw_info']['server']['ldap_search_filter'] ? $GLOBALS['egw_info']['server']['ldap_search_filter'] : '(uid=%user)');
 
 		if ($GLOBALS['egw_info']['server']['account_repository'] == 'ldap')
 		{
@@ -133,6 +136,7 @@ class auth_ldap implements auth_backend
 				elseif ($GLOBALS['egw_info']['server']['pwd_migration_allowed'] &&
 					!empty($GLOBALS['egw_info']['server']['pwd_migration_types']))
 				{
+					$matches = null;
 					// try to query password from ldap server (might fail because of ACL) and check if we need to migrate the hash
 					if (($sri = ldap_search($ldap, $userDN,"(objectclass=*)", array('userPassword'))) &&
 						($values = ldap_get_entries($ldap, $sri)) && isset($values[0]['userpassword'][0]) &&
@@ -147,7 +151,7 @@ class auth_ldap implements auth_backend
 				return $ret;
 			}
 		}
-		if ($this->debug) error_log(__METHOD__."('$username','$password') dn not found or password wrong!");
+		if ($this->debug) error_log(__METHOD__."('$_username', '$_passwd') dn not found or password wrong!");
 		// dn not found or password wrong
 		return False;
 	}
@@ -155,13 +159,13 @@ class auth_ldap implements auth_backend
 	/**
 	 * fetch the last pwd change for the user
 	 *
-	 * @param string $username username of account to authenticate
+	 * @param string $_username username of account to authenticate
 	 * @return mixed false or shadowlastchange*24*3600
 	 */
-	function getLastPwdChange($username)
+	function getLastPwdChange($_username)
 	{
 		// allow non-ascii in username & password
-		$username = translation::convert($username,translation::charset(),'utf-8');
+		$username = translation::convert($_username,translation::charset(),'utf-8');
 
 		if(!$ldap = common::ldapConnect())
 		{
@@ -177,8 +181,8 @@ class auth_ldap implements auth_backend
 		/* find the dn for this uid, the uid is not always in the dn */
 		$attributes	= array('uid','dn','shadowexpire','shadowlastchange');
 
-		$filter = $GLOBALS['egw_info']['server']['ldap_search_filter'] ? $GLOBALS['egw_info']['server']['ldap_search_filter'] : '(uid=%user)';
-		$filter = str_replace(array('%user','%domain'),array(ldap::quote($username),$GLOBALS['egw_info']['user']['domain']),$filter);
+		$filter = str_replace(array('%user','%domain'),array(ldap::quote($username),$GLOBALS['egw_info']['user']['domain']),
+			$GLOBALS['egw_info']['server']['ldap_search_filter'] ? $GLOBALS['egw_info']['server']['ldap_search_filter'] : '(uid=%user)');
 
 		if ($GLOBALS['egw_info']['server']['account_repository'] == 'ldap')
 		{
@@ -237,8 +241,8 @@ class auth_ldap implements auth_backend
 		}
 		//echo "<p>auth_ldap::change_password('$old_passwd','$new_passwd',$account_id) username='$username'</p>\n";
 
-		$filter = $GLOBALS['egw_info']['server']['ldap_search_filter'] ? $GLOBALS['egw_info']['server']['ldap_search_filter'] : '(uid=%user)';
-		$filter = str_replace(array('%user','%domain'),array($username,$GLOBALS['egw_info']['user']['domain']),$filter);
+		$filter = str_replace(array('%user','%domain'),array($username,$GLOBALS['egw_info']['user']['domain']),
+			$GLOBALS['egw_info']['server']['ldap_search_filter'] ? $GLOBALS['egw_info']['server']['ldap_search_filter'] : '(uid=%user)');
 
 		$ds = common::ldapConnect();
 		$sri = ldap_search($ds, $GLOBALS['egw_info']['server']['ldap_context'], $filter);
@@ -270,7 +274,7 @@ class auth_ldap implements auth_backend
 	 * @param string $old_passwd must be cleartext or empty to not to be checked
 	 * @param string $new_passwd must be cleartext
 	 * @param int $account_id account id of user whose passwd should be changed
-	 * @param boolean $update_lastchange=true
+	 * @param boolean $update_lastchange =true
 	 * @return boolean true if password successful changed, false otherwise
 	 */
 	function change_password($old_passwd, $new_passwd, $account_id=0, $update_lastchange=true)
@@ -286,8 +290,8 @@ class auth_ldap implements auth_backend
 		}
 		if ($this->debug) error_log(__METHOD__."('$old_passwd','$new_passwd',$account_id, $update_lastchange) username='$username'");
 
-		$filter = $GLOBALS['egw_info']['server']['ldap_search_filter'] ? $GLOBALS['egw_info']['server']['ldap_search_filter'] : '(uid=%user)';
-		$filter = str_replace(array('%user','%domain'),array($username,$GLOBALS['egw_info']['user']['domain']),$filter);
+		$filter = str_replace(array('%user','%domain'),array($username,$GLOBALS['egw_info']['user']['domain']),
+			$GLOBALS['egw_info']['server']['ldap_search_filter'] ? $GLOBALS['egw_info']['server']['ldap_search_filter'] : '(uid=%user)');
 
 		$ds = $ds_admin = common::ldapConnect();
 		$sri = ldap_search($ds, $GLOBALS['egw_info']['server']['ldap_context'], $filter);
@@ -308,6 +312,7 @@ class auth_ldap implements auth_backend
 				$ds = $user_ds->ldapConnect('',$dn,$old_passwd);
 			}
 			catch (egw_exception_no_permission $e) {
+				unset($e);
 				return false;	// wrong old user password
 			}
 		}

From 3a06bcb28597b97ba2e51f991014dab7ccf249f6 Mon Sep 17 00:00:00 2001
From: Ralf Becker <ralfbecker@outdoor-training.de>
Date: Wed, 18 Feb 2015 08:15:54 +0000
Subject: [PATCH 12/43] disabling dates_range_view in favor of using
 dates-table direct, as it appears 1.5-3 times quicker in two big
 installations I tested with

---
 calendar/inc/class.calendar_so.inc.php | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/calendar/inc/class.calendar_so.inc.php b/calendar/inc/class.calendar_so.inc.php
index 33729f4f91..98645395a6 100644
--- a/calendar/inc/class.calendar_so.inc.php
+++ b/calendar/inc/class.calendar_so.inc.php
@@ -161,6 +161,9 @@ class calendar_so
 	/**
 	 * Return sql to fetch all dates in a given timerange, to be used instead of full dates table in further sql queries
 	 *
+	 * Currently NOT used, as using two views joined together appears slower in my tests (probably because no index) then
+	 * joining cal_range_view with real dates table (with index).
+	 *
 	 * @param int $start
 	 * @param int $end
 	 * @param array $_where =null
@@ -848,7 +851,8 @@ class calendar_so
 		// dates table join only needed to enum recuring events, we use a time-range limited view here too
 		if ($params['enum_recuring'])
 		{
-			$join = "JOIN ".$this->dates_range_view($start, $end, null, $filter == 'everything' ? null : $filter == 'deleted').
+			$join = "JOIN ".$this->dates_table.	// using dates_table direct seems quicker then an other view
+				//$this->dates_range_view($start, $end, null, $filter == 'everything' ? null : $filter == 'deleted').
 				" ON $this->cal_table.cal_id=$this->dates_table.cal_id ".$join;
 		}
 

From 7306abcf5479d605be128d43b3d7893f7855cf22 Mon Sep 17 00:00:00 2001
From: Ralf Becker <ralfbecker@outdoor-training.de>
Date: Wed, 18 Feb 2015 08:46:43 +0000
Subject: [PATCH 13/43] * Calendar: fixed week 13 was skiped (due to daylight
 saving change) when using week navigation, added propper header for multiple
 week view

---
 calendar/inc/class.calendar_uiviews.inc.php | 27 +++++----------------
 1 file changed, 6 insertions(+), 21 deletions(-)

diff --git a/calendar/inc/class.calendar_uiviews.inc.php b/calendar/inc/class.calendar_uiviews.inc.php
index 4d9dcddc2d..51578d3b20 100644
--- a/calendar/inc/class.calendar_uiviews.inc.php
+++ b/calendar/inc/class.calendar_uiviews.inc.php
@@ -655,16 +655,18 @@ class calendar_uiviews extends calendar_ui
 			$this->first = $this->datetime->get_weekday_start($this->year,$this->month,$this->day);
 			$this->last = strtotime("+$weeks weeks",$this->first) - 1;
 			$weekNavH = "$weeks weeks";
+			$navHeader = lang('Week').' '.$this->week_number($this->first).' - '.$this->week_number($this->last).': '.
+				$this->bo->long_date($this->first,$this->last);
 		}
 		else
 		{
 			$this->_week_align_month($this->first,$this->last);
 			$weekNavH = "1 month";
+			$navHeader = lang(adodb_date('F',$this->bo->date2ts($this->date))).' '.$this->year;
 		}
 		if ($this->debug > 0) $this->bo->debug_message('uiviews::month(%1) date=%2: first=%3, last=%4',False,$weeks,$this->date,$this->bo->date2string($this->first),$this->bo->date2string($this->last));
 
-		$GLOBALS['egw_info']['flags']['app_header'] .= ': '.lang(adodb_date('F',$this->bo->date2ts($this->date))).' '.$this->year;
-		$navHeader = lang(adodb_date('F',$this->bo->date2ts($this->date))).' '.$this->year;
+		$GLOBALS['egw_info']['flags']['app_header'] .= ': '.$navHeader;
 
 		$days =& $this->bo->search(array(
 			'start'   => $this->first,
@@ -842,32 +844,15 @@ class calendar_uiviews extends calendar_ui
 			$navHeader = lang('Week').' '.$this->week_number($this->first).': '.$this->bo->long_date($this->first,$this->last);
 		}
 
-        #	temporarly disabled, because it collides with the title for the website
-        #
-		#	// add navigation for previous and next
-		#	// prev. week
-		#	$GLOBALS['egw_info']['flags']['app_header'] = html::a_href(html::image('phpgwapi','first',lang('previous'),$options=' alt="<<"'),array(
-		#		'menuaction' => $this->view_menuaction,
-		#		'date'       => date('Ymd',$this->first-$days*DAY_s),
-		#		)) . ' &nbsp; <b>'.$GLOBALS['egw_info']['flags']['app_header'];
-		#	// next week
-		#	$GLOBALS['egw_info']['flags']['app_header'] .= '</b> &nbsp; '.html::a_href(html::image('phpgwapi','last',lang('next'),$options=' alt=">>"'),array(
-		#		'menuaction' => $this->view_menuaction,
-		#		'date'       => date('Ymd',$this->last+$days*DAY_s),
-		#		));
-		#
-		#		$class = $class == 'row_on' ? 'th' : 'row_on';
-		//echo "<p>weekdaystarts='".$this->cal_prefs['weekdaystarts']."', get_weekday_start($this->year,$this->month,$this->day)=".date('l Y-m-d',$wd_start).", first=".date('l Y-m-d',$this->first)."</p>\n";
-
 		$navHeader = '<div class="calendar_calWeek calendar_calWeekNavHeader">'
 				.html::a_href(html::image('phpgwapi','left',lang('previous'),$options=' alt="<<"'),array(
 				'menuaction' => $this->view_menuaction,
-				'date'       => date('Ymd',$this->first-$days*DAY_s),
+				'date'       => date('Ymd', strtotime("-$days days",$this->first)),
 				)). '<span>'.$navHeader;
 
 		$navHeader = $navHeader.'</span>'.html::a_href(html::image('phpgwapi','right',lang('next'),$options=' alt=">>"'),array(
 				'menuaction' => $this->view_menuaction,
-				'date'       => date('Ymd',$this->last+$days*DAY_s),
+				'date'       => date('Ymd', strtotime("+$days days",$this->last)),
 				)).'</div>';
 
 		$merge = $this->merge();

From 74c771a2c9a5fd4a11ab68320827af52fffc56a1 Mon Sep 17 00:00:00 2001
From: Hadi Nategh <hn@stylite.de>
Date: Wed, 18 Feb 2015 09:30:52 +0000
Subject: [PATCH 14/43] Fix calendar print template

---
 calendar/templates/default/print.xet | 148 +++++++++++++--------------
 1 file changed, 73 insertions(+), 75 deletions(-)

diff --git a/calendar/templates/default/print.xet b/calendar/templates/default/print.xet
index c14fbc1873..190ccac382 100644
--- a/calendar/templates/default/print.xet
+++ b/calendar/templates/default/print.xet
@@ -54,82 +54,80 @@
 		</grid>
 	</template>
 	<template id="calendar.print" template="" lang="" group="0" version="1.6.001">
-		<hbox options="0,0">
-			<grid width="100%" height="200">
-				<columns>
-					<column width="95"/>
-					<column/>
-				</columns>
-				<rows>
-					<row>
-						<hbox>
-							<image src="print" onclick="window.print();" class="calendar_print_button"/>
-							<appicon class="calendar_print_appicon"/>
-						</hbox>
-					</row>
-					<row class="th" height="28">
-						<description value="Title" class="bold" options="bold"/>
-						<textbox id="title" size="80" maxlength="255" readonly="true" span="all" class="bold"/>
-					</row>
-					<row class="row">
-						<description width="95" options=",,,start" value="Start"/>
-						<date-time id="start" readonly="true"/>
-					</row>
-					<row class="row">
-						<description width="0" options=",,,whole_day" value="whole day"/>
-						<checkbox id="whole_day" options=",, ," statustext="Event will occupy the whole day" readonly="true"/>
-					</row>
-					<row class="row">
-						<description width="0" options=",,,duration" value="Duration"/>
-						<hbox options="0,0">
-							<menulist>
-								<menupopup no_lang="1" onchange="set_style_by_class('table','end_hide','visibility',this.value == '' ? 'visible' : 'hidden'); if (this.value == '') document.getElementById(form::name('end[str]')).value = document.getElementById(form::name('start[str]')).value;" id="duration" options="Use end date" statustext="Duration of the meeting" readonly="true"/>
-							</menulist>
-							<date-time id="end" class="end_hide" readonly="true"/>
-						</hbox>
-					</row>
-					<row class="row">
-						<description options=",,,location" value="Location" width="0"/>
-						<textbox maxlength="255" id="location" class="calendar_inputFullWidth" readonly="true"/>
-					</row>
-					<row class="row">
-						<description options=",,,priority" value="Priority" width="0"/>
+		<grid width="100%" height="200">
+			<columns>
+				<column width="95"/>
+				<column/>
+			</columns>
+			<rows>
+				<row>
+					<hbox>
+						<image src="print" onclick="window.print();" class="calendar_print_button"/>
+						<appicon class="calendar_print_appicon"/>
+					</hbox>
+				</row>
+				<row class="th" height="28">
+					<description value="Title" class="bold" options="bold"/>
+					<textbox id="title" size="80" maxlength="255" readonly="true" span="all" class="bold"/>
+				</row>
+				<row class="row">
+					<description width="95" options=",,,start" value="Start"/>
+					<date-time id="start" readonly="true"/>
+				</row>
+				<row class="row">
+					<description width="0" options=",,,whole_day" value="whole day"/>
+					<checkbox id="whole_day" options=",, ," statustext="Event will occupy the whole day" readonly="true"/>
+				</row>
+				<row class="row">
+					<description width="0" options=",,,duration" value="Duration"/>
+					<hbox options="0,0">
 						<menulist>
-							<menupopup type="select-priority" id="priority" readonly="true"/>
+							<menupopup no_lang="1" onchange="set_style_by_class('table','end_hide','visibility',this.value == '' ? 'visible' : 'hidden'); if (this.value == '') document.getElementById(form::name('end[str]')).value = document.getElementById(form::name('start[str]')).value;" id="duration" options="Use end date" statustext="Duration of the meeting" readonly="true"/>
 						</menulist>
-					</row>
-					<row class="row">
-						<description value="Non blocking" width="0"/>
-						<checkbox id="non_blocking" options="1,0, ," statustext="A non blocking event will not conflict with other events" readonly="true"/>
-					</row>
-					<row class="row">
-						<description value="Private"/>
-						<checkbox id="public" options="0,1" readonly="true"/>
-					</row>
-					<row class="row calendar_print_cat">
-						<description value="Categories"/>
-						<menulist>
-							<menupopup type="select-cat" id="category" readonly="true"/>
-						</menulist>
-					</row>
-					<row valign="top">
-						<description value="Description"/>
-						<textbox multiline="true" id="description" readonly="true"/>
-					</row>
-					<row class="th">
-						<description value="custom fields" span="all"/>
-					</row>
-					<row>
-						<customfields span="all" readonly="true"/>
-					</row>
-					<row>
-						<template id="calendar.print.participants" span="all"/>
-					</row>
-					<row>
-						<template span="all" id="calendar.print.links"/>
-					</row>
-				</rows>
-			</grid>
-		</hbox>
+						<date-time id="end" class="end_hide" readonly="true"/>
+					</hbox>
+				</row>
+				<row class="row">
+					<description options=",,,location" value="Location" width="0"/>
+					<textbox maxlength="255" id="location" class="calendar_inputFullWidth" readonly="true"/>
+				</row>
+				<row class="row">
+					<description options=",,,priority" value="Priority" width="0"/>
+					<menulist>
+						<menupopup type="select-priority" id="priority" readonly="true"/>
+					</menulist>
+				</row>
+				<row class="row">
+					<description value="Non blocking" width="0"/>
+					<checkbox id="non_blocking" options="1,0, ," statustext="A non blocking event will not conflict with other events" readonly="true"/>
+				</row>
+				<row class="row">
+					<description value="Private"/>
+					<checkbox id="public" options="0,1" readonly="true"/>
+				</row>
+				<row class="row calendar_print_cat">
+					<description value="Categories"/>
+					<menulist>
+						<menupopup type="select-cat" id="category" readonly="true"/>
+					</menulist>
+				</row>
+				<row valign="top">
+					<description value="Description"/>
+					<textbox multiline="true" id="description" readonly="true"/>
+				</row>
+				<row class="th">
+					<description value="custom fields" span="all"/>
+				</row>
+				<row>
+					<customfields span="all" readonly="true"/>
+				</row>
+				<row>
+					<template id="calendar.print.participants" span="all"/>
+				</row>
+				<row>
+					<template span="all" id="calendar.print.links"/>
+				</row>
+			</rows>
+		</grid>
 	</template>
 </overlay>

From 343bffd9024ac52126ec8f6677bec98472018a64 Mon Sep 17 00:00:00 2001
From: Hadi Nategh <hn@stylite.de>
Date: Wed, 18 Feb 2015 09:44:25 +0000
Subject: [PATCH 15/43] No need to submit after the print is triggerd

---
 calendar/js/app.js | 1 -
 1 file changed, 1 deletion(-)

diff --git a/calendar/js/app.js b/calendar/js/app.js
index 3be3c0bf85..47272a6235 100644
--- a/calendar/js/app.js
+++ b/calendar/js/app.js
@@ -811,7 +811,6 @@ app.classes.calendar = AppJS.extend(
 			{
 				case 'print':
 					this.egw.open_link('calendar.calendar_uiforms.edit&cal_id='+id+'&print=1','_blank','700x700');
-					this.et2._inst.submit();
 					break;
 				case 'mail':
 					this.egw.json('calendar.calendar_uiforms.ajax_custom_mail', [event, !event['id'], false],null,null,null,null).sendRequest();

From 0413898ce403a3ef2e1cac32e67ba274ef36e393 Mon Sep 17 00:00:00 2001
From: Ralf Becker <ralfbecker@outdoor-training.de>
Date: Wed, 18 Feb 2015 10:10:10 +0000
Subject: [PATCH 16/43] * Mail: composed mails saved as draft contains again
 attachments, drafts created by autosaving every 2 minutes do not for
 performance reasons

---
 mail/inc/class.mail_compose.inc.php | 29 +++++++++++++++++------------
 mail/js/app.js                      | 19 ++++++++++---------
 2 files changed, 27 insertions(+), 21 deletions(-)

diff --git a/mail/inc/class.mail_compose.inc.php b/mail/inc/class.mail_compose.inc.php
index 15cb345903..d51bb3f772 100644
--- a/mail/inc/class.mail_compose.inc.php
+++ b/mail/inc/class.mail_compose.inc.php
@@ -2142,10 +2142,11 @@ class mail_compose
 	 * @param egw_mailer $_mailObject
 	 * @param array $_formData
 	 * @param array $_identity
-	 * @param boolean $_send =false true: create to send message: false: create to save as draft
+	 * @param boolean $_autosaving =false true: autosaving, false: save-as-draft or send
 	 */
-	function createMessage(egw_mailer $_mailObject, array $_formData, array $_identity, $_send=false)
+	function createMessage(egw_mailer $_mailObject, array $_formData, array $_identity, $_autosaving=false)
 	{
+		//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);
 
@@ -2233,7 +2234,7 @@ class mail_compose
 			$signature = mail_bo::merge($signature,array($GLOBALS['egw']->accounts->id2name($GLOBALS['egw_info']['user']['account_id'],'person_id')));
 		}
 		*/
-		if ($_formData['attachments'] && $_formData['filemode'] != egw_sharing::ATTACH && $_send)
+		if ($_formData['attachments'] && $_formData['filemode'] != egw_sharing::ATTACH && !$_autosaving)
 		{
 			$attachment_links = $this->getAttachmentLinks($_formData['attachments'], $_formData['filemode'],
 				$_formData['mimeType'] == 'html',
@@ -2267,7 +2268,7 @@ class mail_compose
 				$_mailObject->setBody($this->convertHTMLToText($body, true, true));
 			}
 			// convert URL Images to inline images - if possible
-			if ($_send) mail_bo::processURL2InlineImages($_mailObject, $body);
+			if (!$_autosaving) mail_bo::processURL2InlineImages($_mailObject, $body);
 			if (strpos($body,"<!-- HTMLSIGBEGIN -->")!==false)
 			{
 				$body = str_replace(array('<!-- HTMLSIGBEGIN -->','<!-- HTMLSIGEND -->'),'',$body);
@@ -2343,7 +2344,8 @@ class mail_compose
 								break;
 						}
 					}
-					elseif ($_formData['filemode'] == egw_sharing::ATTACH)
+					// attach files not for autosaving
+					elseif ($_formData['filemode'] == egw_sharing::ATTACH && !$_autosaving)
 					{
 						if (isset($attachment['file']) && parse_url($attachment['file'],PHP_URL_SCHEME) == 'vfs')
 						{
@@ -2431,9 +2433,11 @@ class mail_compose
 	 * Save compose mail as draft
 	 *
 	 * @param array $content content sent from client-side
+	 * @param string $action ='button[saveAsDraft]' 'autosaving', 'button[saveAsDraft]' or 'button[saveAsDraftAndPrint]'
 	 */
-	public function ajax_saveAsDraft ($content)
+	public function ajax_saveAsDraft ($content, $action='button[saveAsDraft]')
 	{
+		//error_log(__METHOD__."(, action=$action)");
 		$response = egw_json_response::get();
 		$success = true;
 
@@ -2455,7 +2459,7 @@ class mail_compose
 			$folder = $this->mail_bo->getDraftFolder();
 			$this->mail_bo->reopen($folder);
 			$status = $this->mail_bo->getFolderStatus($folder);
-			if (($messageUid = $this->saveAsDraft($formData,$folder)))
+			if (($messageUid = $this->saveAsDraft($formData, $folder, $action)))
 			{
 				// saving as draft, does not mean closing the message
 				$messageUid = ($messageUid===true ? $status['uidnext'] : $messageUid);
@@ -2538,13 +2542,14 @@ class mail_compose
 	/**
 	 * Save message as draft to specific folder
 	 *
-	 * @param type $_formData content
-	 * @param type $savingDestination destination folder
+	 * @param array $_formData content
+	 * @param string &$savingDestination ='' destination folder
+	 * @param string $action ='button[saveAsDraft]' 'autosaving', 'button[saveAsDraft]' or 'button[saveAsDraftAndPrint]'
 	 * @return boolean return messageUID| false due to an error
 	 */
-	function saveAsDraft($_formData, &$savingDestination='')
+	function saveAsDraft($_formData, &$savingDestination='', $action='button[saveAsDraft]')
 	{
-		//error_log(__METHOD__.__LINE__);
+		//error_log(__METHOD__."(..., $savingDestination, action=$action)");
 		$mail_bo	= $this->mail_bo;
 		$mail		= new egw_mailer($this->mail_bo->profileID);
 
@@ -2568,7 +2573,7 @@ class mail_compose
 
 		$flags = '\\Seen \\Draft';
 
-		$this->createMessage($mail, $_formData, $identity);
+		$this->createMessage($mail, $_formData, $identity, $action === 'autosaving');
 
 		// folder list as Customheader
 		if (!empty($this->sessionData['folder']))
diff --git a/mail/js/app.js b/mail/js/app.js
index bf590420eb..cf7755be72 100644
--- a/mail/js/app.js
+++ b/mail/js/app.js
@@ -176,7 +176,7 @@ 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('#mail-display_mailDisplayBodySrc').on('load', function(){self.mail_prepare_print();});
 
 				this.mail_isMainWindow = false;
 				this.mail_display();
@@ -199,7 +199,7 @@ app.classes.mail = AppJS.extend(
 
 				// Set autosaving interval to 2 minutes for compose message
 				this.W_INTERVALS.push(window.setInterval(function (){
-					that.saveAsDraft(null,that.et2.getWidgetById('button[saveAsDraft]'),'autosaving');
+					that.saveAsDraft(null, 'autosaving');
 				}, 120000));
 
 				/* Control focus actions on subject to handle expanders properly.*/
@@ -3157,20 +3157,21 @@ app.classes.mail = AppJS.extend(
 	 * Save as Draft (VFS)
 	 * -handel both actions save as draft and save as draft and print
 	 *
-	 * @param {egw object} _egw
-	 * @param {widget object} _widget
-	 * @param {string} _action autosaving trigger action
+	 * @param {egwAction} _egw_action
+	 * @param {array|string} _action string "autosaving", if that triggered the action
 	 */
 	saveAsDraft: function(_egw_action, _action)
 	{
 		//this.et2_obj.submit();
 		var content = this.et2.getArrayMgr('content').data;
-		if (_egw_action )
+		var action = _action;
+		if (_egw_action && _action !== 'autosaving')
 		{
-			var action = _action == 'autosaving'?_action: _egw_action.id;
+			action = _egw_action.id;
 		}
 
-		var widgets = ['from','to','cc','bcc','subject','folder','replyto','mailaccount', 'mail_htmltext', 'mail_plaintext', 'lastDrafted'];
+		var widgets = ['from','to','cc','bcc','subject','folder','replyto','mailaccount',
+			'mail_htmltext', 'mail_plaintext', 'lastDrafted', 'filemode', 'expiration', 'password'];
 		var widget = {};
 		for (var index in widgets)
 		{
@@ -3183,7 +3184,7 @@ app.classes.mail = AppJS.extend(
 		var self = this;
 		if (content)
 		{
-			this.egw.json('mail.mail_compose.ajax_saveAsDraft',[content],function(_data){
+			this.egw.json('mail.mail_compose.ajax_saveAsDraft',[content, action],function(_data){
 				self.savingDraft_response(_data,action);
 			}).sendRequest(true);
 		}

From 4078a48fb8e24040c798e44798a0c4451c490832 Mon Sep 17 00:00:00 2001
From: Ralf Becker <ralfbecker@outdoor-training.de>
Date: Wed, 18 Feb 2015 11:17:28 +0000
Subject: [PATCH 17/43] fixed send mail does not contain attachments

---
 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 d51bb3f772..db4076ead3 100644
--- a/mail/inc/class.mail_compose.inc.php
+++ b/mail/inc/class.mail_compose.inc.php
@@ -2703,7 +2703,7 @@ class mail_compose
 		//error_log($this->sessionData['mailaccount']);
 		//error_log(__METHOD__.__LINE__.':'.array2string($this->sessionData['mailidentity']).'->'.array2string($identity));
 		// create the messages
-		$this->createMessage($mail, $_formData, $identity, true);
+		$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?'>':'');
 		#print "<pre>". $mail->getMessageHeader() ."</pre><hr><br>";

From 426f9e0f8451f6fff5d4cd8d62a2755678a5a0f1 Mon Sep 17 00:00:00 2001
From: Ralf Becker <ralfbecker@outdoor-training.de>
Date: Wed, 18 Feb 2015 11:40:26 +0000
Subject: [PATCH 18/43] * Admin: add a description to stock groups Admins,
 Default and NoGroup, allow to edit that description for LDAP and ADS

---
 admin/inc/class.admin_ui.inc.php         |  1 +
 phpgwapi/inc/class.accounts.inc.php      | 44 ++++++++++++++++++++++++
 phpgwapi/inc/class.accounts_ads.inc.php  |  4 ---
 phpgwapi/inc/class.accounts_ldap.inc.php |  4 ++-
 phpgwapi/lang/egw_de.lang                |  3 ++
 phpgwapi/lang/egw_en.lang                |  3 ++
 6 files changed, 54 insertions(+), 5 deletions(-)

diff --git a/admin/inc/class.admin_ui.inc.php b/admin/inc/class.admin_ui.inc.php
index c7a2cf84b9..8ce6abb20f 100644
--- a/admin/inc/class.admin_ui.inc.php
+++ b/admin/inc/class.admin_ui.inc.php
@@ -426,6 +426,7 @@ class admin_ui
 			{
 				$tree['item'][] = self::fix_userdata(array(
 					'text' => $group['account_lid'],
+					'tooltip' => $group['account_description'],
 					'id' => $root.'/'.$group['account_id'],
 				));
 			}
diff --git a/phpgwapi/inc/class.accounts.inc.php b/phpgwapi/inc/class.accounts.inc.php
index b5c6c8e42b..61839c96a4 100644
--- a/phpgwapi/inc/class.accounts.inc.php
+++ b/phpgwapi/inc/class.accounts.inc.php
@@ -335,6 +335,17 @@ class accounts
 		else
 		{
 			$account_search[$serial]['data'] = $this->backend->search($param);
+			if ($param['type'] !== 'accounts')
+			{
+				foreach($account_search[$serial]['data'] as &$account)
+				{
+					// add default description for Admins and Default group
+					if ($account['account_type'] === 'g' && empty($account['account_description']))
+					{
+						self::add_default_group_description($account);
+					}
+				}
+			}
 			$account_search[$serial]['total'] = $this->total = $this->backend->total;
 		}
 		//echo "<p>accounts::search(".array2string(unserialize($serial)).")= returning ".count($account_search[$serial]['data'])." of $this->total entries<pre>".print_r($account_search[$serial]['data'],True)."</pre>\n";
@@ -429,6 +440,12 @@ class accounts
 
 		$data = self::cache_read($id);
 
+		// add default description for Admins and Default group
+		if ($data['account_type'] === 'g' && empty($data['account_description']))
+		{
+			self::add_default_group_description($data);
+		}
+
 		if ($set_depricated_names && $data)
 		{
 			foreach($this->depricated_names as $name)
@@ -469,6 +486,28 @@ class accounts
 		return json_encode($account);
 	}
 
+	/**
+	 * Add a default description for stock groups: Admins, Default, NoGroup
+	 *
+	 * @param array &$data
+	 */
+	protected static function add_default_group_description(array &$data)
+	{
+		switch($data['account_lid'])
+		{
+			case 'Default':
+				$data['account_description'] = lang('EGroupware all users group, do NOT delete');
+				break;
+			case 'Admins':
+				$data['account_description'] = lang('EGroupware administrators group, do NOT delete');
+				break;
+			case 'NoGroup':
+				$data['account_description'] = lang('EGroupware anonymous users group, do NOT delete');
+				break;
+		}
+		error_log(__METHOD__."(".array2string($data).")");
+	}
+
 	/**
 	 * Saves / adds the data of one account
 	 *
@@ -490,6 +529,11 @@ class accounts
 				}
 			}
 		}
+		// add default description for Admins and Default group
+		if ($data['account_type'] === 'g' && empty($data['account_description']))
+		{
+			self::add_default_group_description($data);
+		}
 		if (($id = $this->backend->save($data)) && $data['account_type'] != 'g')
 		{
 			// if we are not on a pure LDAP system, we have to write the account-date via the contacts class now
diff --git a/phpgwapi/inc/class.accounts_ads.inc.php b/phpgwapi/inc/class.accounts_ads.inc.php
index c454f73e8c..736b43eec7 100644
--- a/phpgwapi/inc/class.accounts_ads.inc.php
+++ b/phpgwapi/inc/class.accounts_ads.inc.php
@@ -578,10 +578,6 @@ class accounts_ads
 	protected function _save_group(array &$data, array $old=null)
 	{
 		//error_log(__METHOD__.'('.array2string($data).', old='.array2string($old).')');
-		if (empty($data['account_description']))
-		{
-			$data['account_description'] = 'EGroupware '.lang('Group').' '.$data['account_lid'];
-		}
 
 		if (!$old)	// new entry
 		{
diff --git a/phpgwapi/inc/class.accounts_ldap.inc.php b/phpgwapi/inc/class.accounts_ldap.inc.php
index 2e90d5b048..ee9d23e105 100644
--- a/phpgwapi/inc/class.accounts_ldap.inc.php
+++ b/phpgwapi/inc/class.accounts_ldap.inc.php
@@ -446,7 +446,7 @@ class accounts_ldap
 			}
 		}
 		$sri = ldap_search($this->ds, $this->group_context,'(&(objectClass=posixGroup)(gidnumber=' . abs($account_id).'))',
-			array('dn', 'gidnumber', 'cn', 'objectclass', static::MAIL_ATTR, 'memberuid'));
+			array('dn', 'gidnumber', 'cn', 'objectclass', static::MAIL_ATTR, 'memberuid', 'description'));
 
 		$ldap_data = ldap_get_entries($this->ds, $sri);
 		if (!$ldap_data['count'])
@@ -467,6 +467,7 @@ class accounts_ldap
 			'objectclass'       => array_map('strtolower', $data['objectclass']),
 			'account_email'     => $data[static::MAIL_ATTR][0],
 			'members'           => array(),
+			'account_description' => $data['description'][0],
 		);
 
 		if (isset($data['memberuid']))
@@ -554,6 +555,7 @@ class accounts_ldap
 	{
 		$to_write['gidnumber'] = abs($data['account_id']);
 		$to_write['cn'] = $data['account_lid'];
+		$to_write['description'] = $data['account_description'];
 
 		return $to_write;
 	}
diff --git a/phpgwapi/lang/egw_de.lang b/phpgwapi/lang/egw_de.lang
index dfb89595d3..31ed10e904 100644
--- a/phpgwapi/lang/egw_de.lang
+++ b/phpgwapi/lang/egw_de.lang
@@ -277,6 +277,9 @@ edit %1 category for	common	de	%1 Kategorie editieren für
 edit categories	common	de	Kategorien editieren
 edit category	common	de	Kategorie editieren
 egroupware	common	de	EGroupware
+egroupware administrators group, do not delete	common	de	EGroupware Gruppe für Administratoren, NICHT löschen
+egroupware all users group, do not delete	common	de	EGroupware Gruppe für alle Benutzer, NICHT löschen
+egroupware anonymous users group, do not delete	common	de	EGroupware Gruppe für anonymen Benutzer, nicht löschen
 egroupware api version	common	de	EGroupware API Version
 egroupware maintenance update %1 available	common	de	EGroupware Fehlerbehebungsupdate %1 ist verfügbar
 egroupware security update %1 needs to be installed!	common	de	EGroupware Sicherheitsupdate %1 muss installiert werden!
diff --git a/phpgwapi/lang/egw_en.lang b/phpgwapi/lang/egw_en.lang
index 0efc51cfac..88d404543b 100644
--- a/phpgwapi/lang/egw_en.lang
+++ b/phpgwapi/lang/egw_en.lang
@@ -277,6 +277,9 @@ edit %1 category for	common	en	Edit %1 category for
 edit categories	common	en	Edit categories
 edit category	common	en	Edit category
 egroupware	common	en	EGroupware
+egroupware administrators group, do not delete	common	en	EGroupware administrators group, do NOT delete
+egroupware all users group, do not delete	common	en	EGroupware all users group, do NOT delete
+egroupware anonymous users group, do not delete	common	en	EGroupware anonymous users group, do NOT delete
 egroupware api version	common	en	EGroupware API version
 egroupware maintenance update %1 available	common	en	EGroupware maintenance update %1 available
 egroupware security update %1 needs to be installed!	common	en	EGroupware security update %1 needs to be installed!

From 55dd493e5d705b3bd31f8c50aec071a308531eb3 Mon Sep 17 00:00:00 2001
From: Hadi Nategh <hn@stylite.de>
Date: Wed, 18 Feb 2015 12:51:23 +0000
Subject: [PATCH 19/43] Make custom theme colors for login page supports
 Cross-Browser styles

---
 pixelegg/inc/class.pixelegg_framework.inc.php | 6 ++++++
 1 file changed, 6 insertions(+)

diff --git a/pixelegg/inc/class.pixelegg_framework.inc.php b/pixelegg/inc/class.pixelegg_framework.inc.php
index 9793555879..b4e550cd06 100755
--- a/pixelegg/inc/class.pixelegg_framework.inc.php
+++ b/pixelegg/inc/class.pixelegg_framework.inc.php
@@ -132,11 +132,17 @@ div#egw_fw_header, div.egw_fw_ui_category:hover,#loginMainDiv,#loginMainDiv #div
 /*Login background*/
 #loginMainDiv #divAppIconBar #divLogo img[src$='svg'] {
 	background-image: -webkit-linear-gradient(top, $color, $color);
+	background-image: -moz-linear-gradient(top, $color, $color);
+	background-image: -o-linear-gradient(top,$color, $color);
+	background-image: linear-gradient(to bottom, $color, $color);
 }
 
 /*Center box in login page*/
 #loginMainDiv div#centerBox {
 	background-image: -webkit-linear-gradient(top,$color_hex_dark,$color_hex_darker);
+	background-image: -moz-linear-gradient(top,$color_hex_dark,$color_hex_darker);
+	background-image: -o-linear-gradient(top,$color_hex_dark,$color_hex_darker);
+	background-image: linear-gradient(to bottom, $color_hex_dark,$color_hex_darker);
 	border-top: solid 1px $color_hex_darker;
 	border-left: solid 1px $color_hex_darker;
 	border-right: solid 1px $color_hex_darker;

From c4da9ba8d9e5036931cfc956d0f80de39435fdbf Mon Sep 17 00:00:00 2001
From: Ralf Becker <ralfbecker@outdoor-training.de>
Date: Wed, 18 Feb 2015 13:45:53 +0000
Subject: [PATCH 20/43] remove permanent error_log

---
 phpgwapi/inc/class.accounts.inc.php | 1 -
 1 file changed, 1 deletion(-)

diff --git a/phpgwapi/inc/class.accounts.inc.php b/phpgwapi/inc/class.accounts.inc.php
index 61839c96a4..b92441c3b3 100644
--- a/phpgwapi/inc/class.accounts.inc.php
+++ b/phpgwapi/inc/class.accounts.inc.php
@@ -505,7 +505,6 @@ class accounts
 				$data['account_description'] = lang('EGroupware anonymous users group, do NOT delete');
 				break;
 		}
-		error_log(__METHOD__."(".array2string($data).")");
 	}
 
 	/**

From 5ea443a889ccf13e0614fde6389c69c0a14381ca Mon Sep 17 00:00:00 2001
From: Ralf Becker <ralfbecker@outdoor-training.de>
Date: Wed, 18 Feb 2015 17:00:24 +0000
Subject: [PATCH 21/43] copy 14.2 changelog to trunk to satisfy update checker

---
 doc/rpm-build/debian.changes | 22 ++++++++++++++++++++++
 1 file changed, 22 insertions(+)

diff --git a/doc/rpm-build/debian.changes b/doc/rpm-build/debian.changes
index 59c1bf26d5..47dab6a087 100644
--- a/doc/rpm-build/debian.changes
+++ b/doc/rpm-build/debian.changes
@@ -1,3 +1,25 @@
+egroupware-epl (14.2.20150218-1) hardy; urgency=low
+
+  * THIS RELEASE CONTAINS IMPORTANT SECURITY FIXES, PLEASE UPDATE ASAP
+  * Critical: Unauthenticated insecure PHP object deseralization allowing arbitrary code execution
+  * High: Cross site scripting by circumventing content security policy
+  * High: Unauthenticated local file access read and write under MS Windows
+  * credits to Andreas Fischer (http://www.andreasfischer.net/) and Lukas Reschke (http://www.statuscode.ch)
+  * Mail: composed mails saved as draft contains again attachments, drafts created by autosaving every 2 minutes do not for performance reasons
+  * Tracker: fixed memberships were not taken into account when opening private tickets or reading restricted comments
+  * InfoLog: new context menu: View parent with children
+  * Univention: mail app was not working for in UCS created users
+  * Admin: add a description to stock groups Admins, Default and NoGroup, allow to edit that description for LDAP and ADS
+  * PostgreSQL: fixed not working new installation
+  * ProjectManager/PostgreSQL: fix SQL error in project-list caused by new resources column
+  * InfoLog/Addressbook: refresh CRM view if InfoLog was edited without having InfoLog tab open
+  * Calendar: fixed week 13 was skiped (due to daylight saving change) when using week navigation, added propper header for multiple week view
+  * SiteMgr: fix not displayed template preferences
+  * SiteMgr: fix accordeon to work in 14.x
+  * Mobile theme: Login page style improvement
+
+ -- Ralf Becker <rb@stylite.de>  Wed, 18 Feb 2015 12:56:48 +0100
+
 egroupware-epl (14.2.20150212-1) hardy; urgency=low
 
   * 14.2 final release

From 529d154514aabcf2707ae145257d46fcf6ef3d87 Mon Sep 17 00:00:00 2001
From: Nathan Gray <nathangray.bsc@gmail.com>
Date: Wed, 18 Feb 2015 17:03:21 +0000
Subject: [PATCH 22/43] * Add ability to select columns to be displayed in
 gantt chart

---
 etemplate/js/et2_widget_gantt.js           | 178 ++++++++++++++++++++-
 etemplate/templates/default/etemplate2.css |  22 +++
 2 files changed, 198 insertions(+), 2 deletions(-)

diff --git a/etemplate/js/et2_widget_gantt.js b/etemplate/js/et2_widget_gantt.js
index 60e86b1ea5..82d9183774 100644
--- a/etemplate/js/et2_widget_gantt.js
+++ b/etemplate/js/et2_widget_gantt.js
@@ -57,6 +57,14 @@ var et2_gantt = et2_inputWidget.extend([et2_IResizeable,et2_IInput],
 			"default": "minute",
 			"description": "The unit for task duration values.  One of minute, hour, week, year."
 		},
+		columns: {
+			name: "Columns",
+			type: "any",
+			default: [
+				{name: "text", label: egw.lang('Title'), tree: true, width: '*'}
+			],
+			description: "Columns for the grid portion of the gantt chart.  An array of objects with keys name, label, etc.  See http://docs.dhtmlx.com/gantt/api__gantt_columns_config.html"
+		},
 		value: {type: 'any'},
 		needed: {ignore: true},
 		onfocus: {ignore: true},
@@ -76,6 +84,7 @@ var et2_gantt = et2_inputWidget.extend([et2_IResizeable,et2_IInput],
 		show_progress: true,
 		order_branch: false,
 		min_column_width: 30,
+		min_grid_column_width: 30,
 		task_height: 25,
 		fit_tasks: true,
 		autosize: '',
@@ -177,6 +186,10 @@ var et2_gantt = et2_inputWidget.extend([et2_IResizeable,et2_IInput],
 		{
 			this.set_value(this.options.value);
 		}
+		if(this.options.columns)
+		{
+			this.set_columns(this.options.columns);
+		}
 		
 		// Update start & end dates with chart values for consistency
 		if(start_date && this.options.value.data && this.options.value.data.length)
@@ -244,6 +257,81 @@ var et2_gantt = et2_inputWidget.extend([et2_IResizeable,et2_IInput],
 		}
 	},
 
+	/**
+	 * Set the columns for the grid (left) portion
+	 *
+	 * @param {Object[]} columns - A list of columns
+	 * @param {string} columns[].name The column's ID
+	 * @param {string} columns[].label The title for the column header
+	 * @param {string} columns[].width Width of the column
+	 *
+	 * @see http://docs.dhtmlx.com/gantt/api__gantt_columns_config.html for full options
+	 */
+	set_columns: function(columns)
+	{
+		this.gantt_config.columns = columns;
+
+		var displayed_columns = [];
+		var gantt_widget = this;
+
+		// Make sure there's enough room for them all
+		var width = 0;
+		for(var col in columns)
+		{
+			// Preserve original width, gantt will resize column to fit
+			if(!columns[col]._width)
+			{
+				columns[col]._width = columns[col].width;
+			}
+			columns[col].width = columns[col]._width;
+			if(!columns[col].template)
+			{
+				// Use an et2 widget to render the column value, if one was provided
+				// otherwise, just display the value
+				columns[col].template = function(task) {
+					var value = typeof task[this.name] == 'undefined'||task[this.name] == null ? '':task[this.name];
+
+					// No value, but there's a project title.  Try reading the project value.
+					if(!value && this.name.indexOf('pe_') == 0 && task.pm_title)
+					{
+						var pm_col = this.name.replace('pe_','pm_');
+						value = typeof task[pm_col] == 'undefined' || task[pm_col] == null ? '':task[pm_col];
+					}
+					if(this.widget && typeof this.widget == 'string')
+					{
+						this.widget = et2_createWidget(this.widget, {readonly:true}, gantt_widget);
+					}
+					if (this.widget)
+					{
+						this.widget.set_value(value);
+						value = $j(this.widget.getDOMNode()).html();
+					}
+					return value;
+				};
+			}
+
+			// Actual hiding is available in the pro version of gantt chart
+			if(!columns[col].hide)
+			{
+				displayed_columns.push(columns[col]);
+				width += parseInt(columns[col]._width) || 0;
+			}
+
+		}
+		// Add in add column
+		displayed_columns.push({name: 'add', width: 26});
+
+		if(width != this.gantt_config.grid_width || typeof this.gantt_config.grid_width == 'undefined')
+		{
+			this.gantt_config.grid_width = Math.min(Math.max(200, width), this.htmlNode.width());
+		}
+
+		if(this.gantt == null) return;
+		this.gantt.config.columns = displayed_columns;
+		this.gantt.config.grid_width = this.gantt_config.grid_width;
+		this.gantt.render();
+	},
+
 	/**
 	 * Sets the data to be displayed in the gantt chart.
 	 *
@@ -700,11 +788,9 @@ var et2_gantt = et2_inputWidget.extend([et2_IResizeable,et2_IInput],
 			// Some crazy stuff make sure timing is OK to scroll after re-render
 			// TODO: Make this more consistently go to where you click
 			var id = gantt_widget.gantt.attachEvent("onGanttRender", function() {
-				console.log('Render');
 				gantt_widget.gantt.detachEvent(id);
 				gantt_widget.gantt.scrollTo(parseInt($j('.gantt_task_scale',gantt_widget.gantt_node).width() *current_position),0);
 				window.setTimeout(function() {
-					console.log("Scroll to");
 					gantt_widget.gantt.scrollTo(parseInt($j('.gantt_task_scale',gantt_widget.gantt_node).width() *current_position),0);
 				},100);
 			});
@@ -729,6 +815,12 @@ var et2_gantt = et2_inputWidget.extend([et2_IResizeable,et2_IInput],
 			*/
 		});
 
+		this.gantt.attachEvent("onGridHeaderClick", function(column_name, e) {
+			if(column_name === "add")
+			{
+				gantt_widget._column_selection(e);
+			}
+		});
 		this.gantt.attachEvent("onContextMenu",function(taskId, linkId, e) {
 			if(gantt_widget.options.readonly) return false;
 			if(taskId)
@@ -901,6 +993,88 @@ var et2_gantt = et2_inputWidget.extend([et2_IResizeable,et2_IInput],
 		}, this, et2_inputWidget);
 	},
 
+	/**
+	 * Start UI for selecting among defined columns
+	 */
+	_column_selection: function(e)
+	{
+		var self = this;
+		var columns = [];
+		var columns_selected = [];
+		for (var i = 0; i < this.gantt_config.columns.length; i++)
+		{
+			var col = this.gantt_config.columns[i];
+			columns.push({
+				value: col.name,
+				label: col.label
+			})
+			if(!col.hide)
+			{
+				columns_selected.push(col.name);
+			}
+		}
+
+		// Build the popup
+		if(!this.selectPopup)
+		{
+			var select = et2_createWidget("select", {
+				multiple: true,
+				rows: 8,
+				empty_label:this.egw().lang("select columns"),
+				selected_first: false
+			}, this);
+			select.set_select_options(columns);
+			select.set_value(columns_selected);
+
+			var okButton = et2_createWidget("buttononly", {}, this);
+			okButton.set_label(this.egw().lang("ok"));
+			okButton.onclick = function() {
+				// Update columns
+				var value = select.getValue();
+				for (var i = 0; i < columns.length; i++)
+				{
+					self.gantt_config.columns[i].hide = value.indexOf(columns[i].value) < 0 ;
+				}
+				self.set_columns(self.gantt_config.columns);
+
+				// Hide popup
+				self.selectPopup.toggle();
+				self.selectPopup.remove();
+				self.selectPopup = null;
+				$j('body').off('click.gantt');
+			};
+
+			var cancelButton = et2_createWidget("buttononly", {}, this);
+			cancelButton.set_label(this.egw().lang("cancel"));
+			cancelButton.onclick = function() {
+				self.selectPopup.toggle();
+				self.selectPopup.remove();
+				self.selectPopup = null;
+				$j('body').off('click.gantt');
+			};
+
+			// Create and add popup
+			this.selectPopup = jQuery(document.createElement("div"))
+				.addClass("colselection ui-dialog ui-widget-content")
+				.append(select.getDOMNode())
+				.append(okButton.getDOMNode())
+				.append(cancelButton.getDOMNode())
+				.appendTo(this.getInstanceManager().DOMContainer);
+			// Bind so if you click elsewhere, it closes
+			window.setTimeout(function() {$j(document).one('mouseup.gantt', function(e){
+				if(!self.selectPopup.is(e.target) && self.selectPopup.has(e.target).length === 0)
+				{
+					cancelButton.onclick();
+				}
+			});},1);
+		}
+		else
+		{
+			this.selectPopup.toggle();
+		}
+		this.selectPopup.position({my:'right top', at:'right bottom', of: e.target});
+	},
+
 	/**
 	 * Link the actions to the DOM nodes / widget bits.
 	 * Overridden to make the gantt chart a container, so it can't be selected.
diff --git a/etemplate/templates/default/etemplate2.css b/etemplate/templates/default/etemplate2.css
index 3374d1291c..b1305da749 100644
--- a/etemplate/templates/default/etemplate2.css
+++ b/etemplate/templates/default/etemplate2.css
@@ -1623,6 +1623,28 @@ div.et2_toolbar_activeList h.ui-accordion-header {
 {
 	overflow: visible;
 }
+/* Style the gantt grid (left side) allowing 2 lines */
+.et2_gantt .gantt_grid_scale :not(.gantt_grid_head_add) {
+	white-space: normal;
+	line-height: 16px;
+	height: auto;
+}
+/* Column selector */
+.et2_gantt .gantt_grid_scale .gantt_grid_head_add {
+	background-image: url(images/selectcols.png);
+	padding: 0px;
+	margin: 0px;
+}
+.et2_gantt .gantt_grid_data .gantt_add {
+	display: none;
+	padding: 0px;
+	margin: 0px;
+}
+/* Display inline, since there's only 1 line*/
+.et2_gantt .gantt_grid_data li {
+	display: inline-block;
+	padding-right: 0.5ex;
+}
 .et2_gantt .gantt_task_progress
 {
 	text-align: left;

From 5318ebdb210766b31cf7db23e5283b98ccd1578b Mon Sep 17 00:00:00 2001
From: Nathan Gray <nathangray.bsc@gmail.com>
Date: Wed, 18 Feb 2015 17:20:50 +0000
Subject: [PATCH 23/43] - Fix JS error when selecting no columns - Fix not
 including selector width caused undesired column resizing

---
 etemplate/js/et2_widget_gantt.js | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/etemplate/js/et2_widget_gantt.js b/etemplate/js/et2_widget_gantt.js
index 82d9183774..cdedef9780 100644
--- a/etemplate/js/et2_widget_gantt.js
+++ b/etemplate/js/et2_widget_gantt.js
@@ -320,6 +320,7 @@ var et2_gantt = et2_inputWidget.extend([et2_IResizeable,et2_IInput],
 		}
 		// Add in add column
 		displayed_columns.push({name: 'add', width: 26});
+		width += 26;
 
 		if(width != this.gantt_config.grid_width || typeof this.gantt_config.grid_width == 'undefined')
 		{
@@ -1030,7 +1031,7 @@ var et2_gantt = et2_inputWidget.extend([et2_IResizeable,et2_IInput],
 			okButton.set_label(this.egw().lang("ok"));
 			okButton.onclick = function() {
 				// Update columns
-				var value = select.getValue();
+				var value = select.getValue() || [];
 				for (var i = 0; i < columns.length; i++)
 				{
 					self.gantt_config.columns[i].hide = value.indexOf(columns[i].value) < 0 ;

From 63cfd63c67fad72534eefc3b99fc38e6af5d3aa0 Mon Sep 17 00:00:00 2001
From: Ralf Becker <ralfbecker@outdoor-training.de>
Date: Wed, 18 Feb 2015 17:37:07 +0000
Subject: [PATCH 24/43] add group description for accounts in sql too

---
 phpgwapi/inc/class.accounts_sql.inc.php |  3 ++-
 phpgwapi/setup/setup.inc.php            |  3 ++-
 phpgwapi/setup/tables_current.inc.php   |  3 ++-
 phpgwapi/setup/tables_update.inc.php    | 13 ++++++++++++-
 4 files changed, 18 insertions(+), 4 deletions(-)

diff --git a/phpgwapi/inc/class.accounts_sql.inc.php b/phpgwapi/inc/class.accounts_sql.inc.php
index 5e98a802fb..ff318e2e89 100644
--- a/phpgwapi/inc/class.accounts_sql.inc.php
+++ b/phpgwapi/inc/class.accounts_sql.inc.php
@@ -508,7 +508,7 @@ class accounts_sql
 		$accounts = array();
 		foreach((array) $GLOBALS['egw']->contacts->search($criteria,
 			"1,n_given,n_family,email,id,created,modified,$this->table.account_id AS account_id",
-			$order,"account_lid,account_type,account_status,account_expires,account_primary_group",
+			$order,"account_lid,account_type,account_status,account_expires,account_primary_group,account_description",
 			$wildcard,false,$query[0] == '!' ? 'AND' : 'OR',
 			$param['offset'] ? array($param['start'], $param['offset']) : (is_null($param['start']) ? false : $param['start']),
 			$filter,$this->contacts_join) as $contact)
@@ -530,6 +530,7 @@ class accounts_sql
 					// addressbook_bo::search() returns everything in user-time, need to convert to server-time
 					'account_created'	=> egw_time::user2server($contact['created']),
 					'account_modified'	=> egw_time::user2server($contact['modified']),
+					'account_description' => $contact['account_description'],
 				);
 			}
 		}
diff --git a/phpgwapi/setup/setup.inc.php b/phpgwapi/setup/setup.inc.php
index 6a07aa9e5c..07188680a0 100755
--- a/phpgwapi/setup/setup.inc.php
+++ b/phpgwapi/setup/setup.inc.php
@@ -12,7 +12,7 @@
 /* Basic information about this app */
 $setup_info['phpgwapi']['name']      = 'phpgwapi';
 $setup_info['phpgwapi']['title']     = 'EGroupware API';
-$setup_info['phpgwapi']['version']   = '14.2';
+$setup_info['phpgwapi']['version']   = '15.0.001';
 $setup_info['phpgwapi']['versions']['current_header'] = '1.29';
 $setup_info['phpgwapi']['enable']    = 3;
 $setup_info['phpgwapi']['app_order'] = 1;
@@ -74,3 +74,4 @@ $setup_info['groupdav']['license'] = 'GPL';
 $setup_info['groupdav']['hooks']['preferences']	= 'groupdav_hooks::menus';
 $setup_info['groupdav']['hooks']['settings']	= 'groupdav_hooks::settings';
 
+
diff --git a/phpgwapi/setup/tables_current.inc.php b/phpgwapi/setup/tables_current.inc.php
index e9fda853d1..efef79ecf7 100644
--- a/phpgwapi/setup/tables_current.inc.php
+++ b/phpgwapi/setup/tables_current.inc.php
@@ -63,7 +63,8 @@ $phpgw_baseline = array(
 			'account_status' => array('type' => 'char','precision' => '1','nullable' => False,'default' => 'A'),
 			'account_expires' => array('type' => 'int','precision' => '4'),
 			'account_type' => array('type' => 'char','precision' => '1'),
-			'account_primary_group' => array('type' => 'int','meta' => 'group','precision' => '4','nullable' => False,'default' => '0')
+			'account_primary_group' => array('type' => 'int','meta' => 'group','precision' => '4','nullable' => False,'default' => '0'),
+			'account_description' => array('type' => 'varchar','precision' => '255','comment' => 'group description')
 		),
 		'pk' => array('account_id'),
 		'fk' => array(),
diff --git a/phpgwapi/setup/tables_update.inc.php b/phpgwapi/setup/tables_update.inc.php
index 94b179344b..5dd9efaa4d 100644
--- a/phpgwapi/setup/tables_update.inc.php
+++ b/phpgwapi/setup/tables_update.inc.php
@@ -59,4 +59,15 @@ function phpgwapi_upgrade14_1_900()
 	$GLOBALS['egw_setup']->add_acl('phpgwapi', 'anonymous', $anonymous);
 
 	return $GLOBALS['setup_info']['phpgwapi']['currentver'] = '14.2';
-}
\ No newline at end of file
+}
+function phpgwapi_upgrade14_2()
+{
+	$GLOBALS['egw_setup']->oProc->AddColumn('egw_accounts','account_description',array(
+		'type' => 'varchar',
+		'precision' => '255',
+		'comment' => 'group description'
+	));
+
+	return $GLOBALS['setup_info']['phpgwapi']['currentver'] = '15.0.001';
+}
+

From 55d5bd98f83d95fc455722c0dca75051a0f12ee1 Mon Sep 17 00:00:00 2001
From: Nathan Gray <nathangray.bsc@gmail.com>
Date: Wed, 18 Feb 2015 18:23:35 +0000
Subject: [PATCH 25/43] Custom fields editing: - Fix length, rows & values
 fields were not properly enabled on first load - If label was not provided,
 use name

---
 admin/inc/class.customfields.inc.php         |  4 ++++
 admin/js/app.js                              | 18 ++++++++++++++++++
 admin/templates/default/customfield_edit.xet |  5 +----
 3 files changed, 23 insertions(+), 4 deletions(-)

diff --git a/admin/inc/class.customfields.inc.php b/admin/inc/class.customfields.inc.php
index 875fe83932..17125a18d8 100644
--- a/admin/inc/class.customfields.inc.php
+++ b/admin/inc/class.customfields.inc.php
@@ -305,6 +305,10 @@ class customfields
 					break;
 				case 'save':
 				case 'apply':
+					if(empty($content['cf_label']))
+					{
+						$content['cf_label'] = $content['cf_name'];
+					}
 					if (!empty($content['cf_values']))
 					{
 						$values = array();
diff --git a/admin/js/app.js b/admin/js/app.js
index d581c68979..3e0c42fb0c 100644
--- a/admin/js/app.js
+++ b/admin/js/app.js
@@ -100,6 +100,11 @@ app.classes.admin = AppJS.extend(
 
 			case 'admin.categories.index':
 				break;
+			case 'admin.customfield_edit':
+				// Load settings appropriate to currently set type
+				var widget = _et2.widgetContainer.getWidgetById('cf_type');
+				this.cf_type_change(null,widget);
+				break;
 		}
 	},
 
@@ -784,5 +789,18 @@ app.classes.admin = AppJS.extend(
 			}
 		};
 		et2_dialog.show_dialog(delDialog_callBack,this.egw.lang("Are you sure you want to delete this category ?"),this.egw.lang("Delete"),{},_buttons,et2_dialog.WARNING_MESSAGE,null,'admin');
+	},
+
+	/**
+	 * Change handler for when you change the type of a custom field.
+	 * It toggles options / attributes as appropriate.
+	 */
+	cf_type_change: function(e,widget) {
+		var root = widget.getRoot();
+		var attributes = widget.getArrayMgr('content').getEntry('attributes['+widget.getValue()+']')||{};
+		root.getWidgetById('cf_values').set_statustext(widget.egw().lang(widget.getArrayMgr('content').getEntry('options['+widget.getValue()+']'))||'');
+		root.getWidgetById('cf_len').set_disabled(!attributes.cf_len);
+		root.getWidgetById('cf_rows').set_disabled(!attributes.cf_rows);
+		root.getWidgetById('cf_values').set_disabled(!attributes.cf_values);
 	}
 });
diff --git a/admin/templates/default/customfield_edit.xet b/admin/templates/default/customfield_edit.xet
index 52422d251d..d1fa8f654a 100644
--- a/admin/templates/default/customfield_edit.xet
+++ b/admin/templates/default/customfield_edit.xet
@@ -29,10 +29,7 @@
 				</row>
 				<row>
 					<description value="Type of field"/>
-					<customfields-types statustext="Type of customfield" id="cf_type" class="et2_fullWidth" span="2" onchange="widget.getRoot().getWidgetById('cf_values').set_statustext(widget.egw().lang(widget.getArrayMgr('content').getEntry('options['+widget.getValue()+']'))||'');
-widget.getRoot().getWidgetById('cf_len').set_disabled(!widget.getArrayMgr('content').getEntry('attributes['+widget.getValue()+'][cf_len]'));
-widget.getRoot().getWidgetById('cf_rows').set_disabled(!widget.getArrayMgr('content').getEntry('attributes['+widget.getValue()+'][cf_rows]'));
-widget.getRoot().getWidgetById('cf_values').set_disabled(!widget.getArrayMgr('content').getEntry('attributes['+widget.getValue()+'][cf_values]'));"/>
+					<customfields-types statustext="Type of customfield" id="cf_type" class="et2_fullWidth" span="2" onchange="app.admin.cf_type_change"/>
 					<hbox span="2">
 						<description value="Required"/>
 						<checkbox id="cf_needed"/>

From 12b8dc1ed4d9b55d42cc812b834402024345910f Mon Sep 17 00:00:00 2001
From: Nathan Gray <nathangray.bsc@gmail.com>
Date: Wed, 18 Feb 2015 23:02:35 +0000
Subject: [PATCH 26/43] Add label attribute

---
 etemplate/js/et2_widget_description.js | 64 ++++++++++++++++++++++++++
 1 file changed, 64 insertions(+)

diff --git a/etemplate/js/et2_widget_description.js b/etemplate/js/et2_widget_description.js
index 3838aaf414..028e65b44a 100644
--- a/etemplate/js/et2_widget_description.js
+++ b/etemplate/js/et2_widget_description.js
@@ -25,6 +25,13 @@
 var et2_description = et2_baseWidget.extend([et2_IDetachedDOM],
 {
 	attributes: {
+		"label": {
+			"name": "Label",
+			"default": "",
+			"type": "string",
+			"description": "The label is displayed by default in front (for radiobuttons behind) each widget (if not empty). If you want to specify a different position, use a '%s' in the label, which gets replaced by the widget itself. Eg. '%s Name' to have the label Name behind a checkbox. The label can contain variables, as descript for name. If the label starts with a '@' it is replaced by the value of the content-array at this index (with the '@'-removed and after expanding the variables).",
+			"translate": true
+		},
 		"value": {
 			"name": "Value",
 			"type": "string",
@@ -119,6 +126,63 @@ var et2_description = et2_baseWidget.extend([et2_IDetachedDOM],
 		}
 	},
 
+	set_label: function(_value) {
+		// Abort if ther was no change in the label
+		if (_value == this.label)
+		{
+			return;
+		}
+
+		if (_value)
+		{
+			// Create the label container if it didn't exist yet
+			if (this._labelContainer == null)
+			{
+				this._labelContainer = $j(document.createElement("label"))
+					.addClass("et2_label");
+				this.getSurroundings().insertDOMNode(this._labelContainer[0]);
+			}
+
+			// Clear the label container.
+			this._labelContainer.empty();
+
+			// Create the placeholder element and set it
+			var ph = document.createElement("span");
+			this.getSurroundings().setWidgetPlaceholder(ph);
+
+			// Split the label at the "%s"
+			var parts = et2_csvSplit(_value, 2, "%s");
+
+			// Update the content of the label container
+			for (var i = 0; i < parts.length; i++)
+			{
+				if (parts[i])
+				{
+					this._labelContainer.append(document.createTextNode(parts[i]));
+				}
+				if (i == 0)
+				{
+					this._labelContainer.append(ph);
+				}
+			}
+		}
+		else
+		{
+			// Delete the labelContainer from the surroundings object
+			if (this._labelContainer)
+			{
+				this.getSurroundings().removeDOMNode(this._labelContainer[0]);
+			}
+			this._labelContainer = null;
+		}
+
+		// Update the surroundings in order to reflect the change in the label
+		this.getSurroundings().update();
+
+		// Copy the given value
+		this.label = _value;
+	},
+
 	set_value: function(_value) {
 		if (!_value) _value = "";
 		if (!this.options.no_lang) _value = this.egw().lang(_value);

From b1b1269e0e3e06999f6e0841b9600846621bc2fc Mon Sep 17 00:00:00 2001
From: Nathan Gray <nathangray.bsc@gmail.com>
Date: Wed, 18 Feb 2015 23:04:59 +0000
Subject: [PATCH 27/43] Entry type / transformer changes - better support for
 entry-types in nextmatch-customfilter - if transformer changes widget type,
 run new widget's beforeSendToClient

---
 .../inc/class.etemplate_widget_nextmatch.inc.php     |  2 +-
 .../inc/class.etemplate_widget_transformer.inc.php   | 12 ++++++++++++
 etemplate/js/et2_widget_entry.js                     |  2 +-
 3 files changed, 14 insertions(+), 2 deletions(-)

diff --git a/etemplate/inc/class.etemplate_widget_nextmatch.inc.php b/etemplate/inc/class.etemplate_widget_nextmatch.inc.php
index 3daa81b5f9..f92102d4a2 100644
--- a/etemplate/inc/class.etemplate_widget_nextmatch.inc.php
+++ b/etemplate/inc/class.etemplate_widget_nextmatch.inc.php
@@ -1207,8 +1207,8 @@ class etemplate_widget_nextmatch_customfilter extends etemplate_widget_transform
 
 		$this->setElementAttribute($form_name, 'options', trim($this->attrs['widget_options']) != '' ? $this->attrs['widget_options'] : '');
 
-		parent::beforeSendToClient($cname, $expand);
 		$this->setElementAttribute($form_name, 'type', $this->attrs['type']);
+		parent::beforeSendToClient($cname, $expand);
 	}
 }
 
diff --git a/etemplate/inc/class.etemplate_widget_transformer.inc.php b/etemplate/inc/class.etemplate_widget_transformer.inc.php
index 34ba40bd85..11306dfecf 100644
--- a/etemplate/inc/class.etemplate_widget_transformer.inc.php
+++ b/etemplate/inc/class.etemplate_widget_transformer.inc.php
@@ -122,6 +122,7 @@ abstract class etemplate_widget_transformer extends etemplate_widget
 
 		//echo $this; _debug_array($unmodified); _debug_array($attrs); _debug_array(array_diff_assoc($attrs, $unmodified));
 		// compute the difference and send it to the client as modifications
+		$type_changed = false;
 		foreach(array_diff_assoc($attrs, $unmodified) as $attr => $val)
 		{
 			switch($attr)
@@ -136,6 +137,7 @@ abstract class etemplate_widget_transformer extends etemplate_widget
 					self::$request->sel_options[$form_name] = $val;
 					break;
 				case 'type':	// not an attribute in etemplate2
+					$type_changed = true;
 					if($val == 'template')
 					{
 						// If the widget has been transformed into a template, we
@@ -146,12 +148,22 @@ abstract class etemplate_widget_transformer extends etemplate_widget
 							$this->expand_widget($transformed_template, $expand);
 							$transformed_template->run('beforeSendToClient',array($cname,$expand));
 						}
+						$type_changed = false;
 					}
 				default:
 					self::setElementAttribute($form_name, $attr, $val);
 					break;
 			}
 		}
+		if($type_changed)
+		{
+			// Run the new widget type's beforeSendToClient
+			$expanded_child = self::factory($attrs['type'], false,$this->id);
+			$expanded_child->id = $this->id;
+			$expanded_child->type = $attrs['type'];
+			$expanded_child->attrs = $attrs;
+			$expanded_child->run('beforeSendToClient',array($cname,$expand));
+		}
 	}
 
 	/**
diff --git a/etemplate/js/et2_widget_entry.js b/etemplate/js/et2_widget_entry.js
index 3b47587612..26cfbdbecf 100644
--- a/etemplate/js/et2_widget_entry.js
+++ b/etemplate/js/et2_widget_entry.js
@@ -150,4 +150,4 @@ var et2_entry = et2_valueWidget.extend(
 	}
 });
 
-et2_register_widget(et2_entry, ["entry", 'contact-value', 'contact-account', 'contact-template', 'infolog-value','tracker-value']);
\ No newline at end of file
+et2_register_widget(et2_entry, ["entry", 'contact-value', 'contact-account', 'contact-template', 'infolog-value','tracker-value','records-value']);
\ No newline at end of file

From 89741b682c1ca468ed8b22c21d2f2796c89b8971 Mon Sep 17 00:00:00 2001
From: Nathan Gray <nathangray.bsc@gmail.com>
Date: Thu, 19 Feb 2015 00:27:21 +0000
Subject: [PATCH 28/43] Force left margin to 0, avoids margin when printing

---
 phpgwapi/js/framework/fw_base.js | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/phpgwapi/js/framework/fw_base.js b/phpgwapi/js/framework/fw_base.js
index faef5dcac6..491b53618f 100644
--- a/phpgwapi/js/framework/fw_base.js
+++ b/phpgwapi/js/framework/fw_base.js
@@ -982,13 +982,15 @@ var fw_base =  Class.extend({
 							},et2_list[i],et2_IPrint);
 						}
 						appWindow.onafterprint = null;
+						// Reset after removing margin
+						$j('#egw_fw_main').css('margin-left', framework.activeApp.sideboxWidth + "px");
 					};
 					if(appWindow.matchMedia) {
 						var mediaQueryList = appWindow.matchMedia('print');
 						var listener = function(mql) {
 							if (!mql.matches) {
-								afterPrint();
 								mediaQueryList.removeListener(listener);
+								afterPrint();
 							}
 						};
 						mediaQueryList.addListener(listener);
@@ -998,7 +1000,9 @@ var fw_base =  Class.extend({
 
 					// Wait for everything to be loaded, then send it off
 					jQuery.when.apply(jQuery, deferred).done(function() {
-						appWindow.print();
+						// Despite being set in the print CSS, this just doesn't work
+						$j('#egw_fw_main').css('margin-left','0px');
+						appWindow.setTimeout(appWindow.print, 0);
 					}).fail(function() {
 						afterPrint();
 					});

From f1e0cc90d71684e62231ce38efcaa2c85033790f Mon Sep 17 00:00:00 2001
From: Nathan Gray <nathangray.bsc@gmail.com>
Date: Thu, 19 Feb 2015 00:54:49 +0000
Subject: [PATCH 29/43] Slightly gentler reset after forcing margin for
 printing

---
 phpgwapi/js/framework/fw_base.js | 23 +++++++++++++++--------
 1 file changed, 15 insertions(+), 8 deletions(-)

diff --git a/phpgwapi/js/framework/fw_base.js b/phpgwapi/js/framework/fw_base.js
index 491b53618f..ad5772dfb4 100644
--- a/phpgwapi/js/framework/fw_base.js
+++ b/phpgwapi/js/framework/fw_base.js
@@ -975,15 +975,22 @@ var fw_base =  Class.extend({
 				{
 					// Try to clean up after - not guaranteed
 					var afterPrint = function() {
-						for(var i = 0; i < et2_list.length; i++)
-						{
-							et2_list[i].widgetContainer.iterateOver(function(_widget) {
-								_widget.afterPrint();
-							},et2_list[i],et2_IPrint);
-						}
-						appWindow.onafterprint = null;
 						// Reset after removing margin
-						$j('#egw_fw_main').css('margin-left', framework.activeApp.sideboxWidth + "px");
+						$j('#egw_fw_main').css('margin-left', (framework.activeApp.sideboxWidth -1)+ "px");
+						var app = framework.activeApp;
+						framework.activeApp = '';
+						framework.setActiveApp(app);
+
+						// Give framework a chance to deal, then reset the etemplates
+						window.setTimeout(function() {
+							for(var i = 0; i < et2_list.length; i++)
+							{
+								et2_list[i].widgetContainer.iterateOver(function(_widget) {
+									_widget.afterPrint();
+								},et2_list[i],et2_IPrint);
+							}
+						},100);
+						appWindow.onafterprint = null;
 					};
 					if(appWindow.matchMedia) {
 						var mediaQueryList = appWindow.matchMedia('print');

From be5dd5435b1036bcb8bf39e5e6383afcd8468032 Mon Sep 17 00:00:00 2001
From: Ralf Becker <ralfbecker@outdoor-training.de>
Date: Thu, 19 Feb 2015 09:43:06 +0000
Subject: [PATCH 30/43] need to use egw_vfs::load_wrapper() to support new
 filesystem layout in trunk, fixes "Unknown scheme" error in mount

---
 filemanager/cli.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/filemanager/cli.php b/filemanager/cli.php
index 3cfbd01d34..43a141a82a 100755
--- a/filemanager/cli.php
+++ b/filemanager/cli.php
@@ -489,7 +489,7 @@ function load_wrapper($url)
 				// get eGW's __autoload() function
 				include_once(EGW_API_INC.'/common_functions.inc.php');
 
-				if (!class_exists(str_replace('.','_',$scheme).'_stream_wrapper'))
+				if (!egw_vfs::load_wrapper($scheme))
 				{
 					die("Unknown scheme '$scheme' in $url !!!\n\n");
 				}

From 52b4856a1846bf8e48833d110c03a0306e3cd78b Mon Sep 17 00:00:00 2001
From: Klaus Leithoff <kl@stylite.de>
Date: Thu, 19 Feb 2015 11:37:42 +0000
Subject: [PATCH 31/43] when dealing with defaults and identities: retrieve the
 default identity associated with the current imap-account, rather than the
 default

---
 mail/inc/class.mail_compose.inc.php | 11 +++++++----
 1 file changed, 7 insertions(+), 4 deletions(-)

diff --git a/mail/inc/class.mail_compose.inc.php b/mail/inc/class.mail_compose.inc.php
index db4076ead3..b7186c33a4 100644
--- a/mail/inc/class.mail_compose.inc.php
+++ b/mail/inc/class.mail_compose.inc.php
@@ -374,6 +374,9 @@ class mail_compose
 			$this->changeProfile($_content['serverID']);
 			$composeProfile = $this->mail_bo->profileID;
 		}
+		// make sure $acc is set/initalized properly with the current composeProfile, as $acc is used down there
+		// at several locations and not neccesaryly initialized before
+		$acc = emailadmin_account::read($composeProfile);
 		$buttonClicked = false;
 		if ($_content['composeToolbar'] === 'send')
 		{
@@ -1022,7 +1025,7 @@ class mail_compose
 
 		// fetch the signature, prepare the select box, ...
 		if (empty($content['mailidentity'])) {
-			$content['mailidentity'] = $acc['ident_id'];
+			$content['mailidentity'] = $acc['ident_id']?$acc['ident_id']:$acc['acc_id'];
 		}
 
 		$disableRuler = false;
@@ -2437,7 +2440,7 @@ class mail_compose
 	 */
 	public function ajax_saveAsDraft ($content, $action='button[saveAsDraft]')
 	{
-		//error_log(__METHOD__."(, action=$action)");
+		//error_log(__METHOD__.__LINE__.array2string($content)."(, action=$action)");
 		$response = egw_json_response::get();
 		$success = true;
 
@@ -3057,8 +3060,8 @@ class mail_compose
 	 */
 	function setDefaults($content=array())
 	{
-		// retrieve the signature accociated with the identity
-		$id = $this->mail_bo->getDefaultIdentity();
+		// retrieve the signature accociated with the identity associated with the current account, rather than the default
+		$id = ($this->mail_bo->icServer->ident_id?$this->mail_bo->icServer->ident_id:$this->mail_bo->getDefaultIdentity());
 		if (isset($GLOBALS['egw_info']['user']['preferences']['mail']['LastSignatureIDUsed']) && !empty($GLOBALS['egw_info']['user']['preferences']['mail']['LastSignatureIDUsed']))
 		{
 			$sigPref = $GLOBALS['egw_info']['user']['preferences']['mail']['LastSignatureIDUsed'];

From 212e98ccdb2f4671da54aab045db93da141ad1e5 Mon Sep 17 00:00:00 2001
From: Hadi Nategh <hn@stylite.de>
Date: Thu, 19 Feb 2015 11:45:05 +0000
Subject: [PATCH 32/43] Fix in mobile theme not able to dismiss the context
 menu: - touch and open entries and swip (left/right) over any rows on the
 next match list will dismiss the context menu

---
 etemplate/js/et2_dataview_view_aoi.js      | 4 +++-
 phpgwapi/js/egw_action/egw_action_popup.js | 3 +++
 2 files changed, 6 insertions(+), 1 deletion(-)

diff --git a/etemplate/js/et2_dataview_view_aoi.js b/etemplate/js/et2_dataview_view_aoi.js
index 8cbefc99cf..b7a6621b9c 100644
--- a/etemplate/js/et2_dataview_view_aoi.js
+++ b/etemplate/js/et2_dataview_view_aoi.js
@@ -77,6 +77,8 @@ function et2_dataview_rowAOI(_node)
 						case "left":
 						case "right":
 							state = 1;
+							// Hide context menu on swip actions
+							if(_egw_active_menu) _egw_active_menu.hide();
 							break;
 					}
 				}
@@ -106,7 +108,7 @@ function et2_dataview_rowAOI(_node)
 				click: function (event)
 				{
 					selectHandler(event);
-				},
+				}
 				
 		});
 	} else {
diff --git a/phpgwapi/js/egw_action/egw_action_popup.js b/phpgwapi/js/egw_action/egw_action_popup.js
index 02cf677e90..f15b5f9f70 100644
--- a/phpgwapi/js/egw_action/egw_action_popup.js
+++ b/phpgwapi/js/egw_action/egw_action_popup.js
@@ -150,6 +150,9 @@ function egwPopupActionImplementation()
 			e.stopPropagation();
 			e.cancelBubble = true;
 
+			// remove context menu if we are in mobile theme
+			// and intended to open the entry
+			if (_egw_active_menu && e.which == 1) _egw_active_menu.hide();
 			return false;
 		};
 

From acb4f11d24eb0de128295083b7e580f6e44727d8 Mon Sep 17 00:00:00 2001
From: Klaus Leithoff <kl@stylite.de>
Date: Thu, 19 Feb 2015 12:01:18 +0000
Subject: [PATCH 33/43] * Mail: feature to allow to void the (configured)
 spam/junk folder on right-click action on foldertree

---
 mail/inc/class.mail_ui.inc.php | 65 +++++++++++++++++++++++++++++++++-
 mail/js/app.js                 | 51 ++++++++++++++++++++++++++
 2 files changed, 115 insertions(+), 1 deletion(-)

diff --git a/mail/inc/class.mail_ui.inc.php b/mail/inc/class.mail_ui.inc.php
index 03ef6d2601..6c58b72b6c 100644
--- a/mail/inc/class.mail_ui.inc.php
+++ b/mail/inc/class.mail_ui.inc.php
@@ -624,6 +624,19 @@ class mail_ui
 						);
 						break;
 				}
+				++$group;	// put empty spam immediately in own group
+				$junkFolder = $this->mail_bo->getJunkFolder();
+				//error_log(__METHOD__.__LINE__.$junkFolder);
+				if ($junkFolder && !empty($junkFolder))
+				{
+					$tree_actions['empty_spam'] = array(
+						'caption' => 'empty spam',
+						'icon' => 'dhtmlxtree/MailFolderJunk',
+						'enabled'	=> 'javaScript:app.mail.spamfolder_enabled',
+						'onExecute' => 'javaScript:app.mail.mail_emptySpam',
+						'group'	=> $group,
+					);
+				}
 
 				// enforce global (group-specific) ACL
 				if (!mail_hooks::access('aclmanagement'))
@@ -817,7 +830,7 @@ class mail_ui
 				'child'=> (int)($acc_id != $_profileID || $folderObjects), // dynamic loading on unfold
 				'parent' => '',
 				// mark on account if Sieve is enabled
-				'data' => array('sieve' => $accountObj->imapServer()->acc_sieve_enabled),
+				'data' => array('sieve' => $accountObj->imapServer()->acc_sieve_enabled,'spamfolder'=>($accountObj->imapServer()->acc_folder_junk?true:false)),
 			);
 			$this->setOutStructure($oA, $out, self::$delimiter);
 
@@ -4213,6 +4226,56 @@ class mail_ui
 		$response->call('app.mail.mail_setQuotaDisplay',array('data'=>$content));
 	}
 
+	/**
+	 * Empty spam/junk folder
+	 *
+	 * @param string $icServerID id of the server to empty its junkFolder
+	 * @param string $selectedFolder seleted(active) folder by nm filter
+	 * @return nothing
+	 */
+	function ajax_emptySpam($icServerID, $selectedFolder)
+	{
+		//error_log(__METHOD__.__LINE__.' '.$icServerID);
+		translation::add_app('mail');
+		$response = egw_json_response::get();
+		$rememberServerID = $this->mail_bo->profileID;
+		if ($icServerID && $icServerID != $this->mail_bo->profileID)
+		{
+			//error_log(__METHOD__.__LINE__.' change Profile to ->'.$icServerID);
+			$this->changeProfile($icServerID);
+		}
+		$junkFolder = $this->mail_bo->getJunkFolder();
+		if(!empty($junkFolder)) {
+			if ($selectedFolder == $icServerID.self::$delimiter.$junkFolder)
+			{
+				// Lock the tree if the active folder is Trash folder
+				$response->call('app.mail.lock_tree');
+			}
+			$this->mail_bo->deleteMessages('all',$junkFolder,'remove_immediately');
+
+			$heirarchyDelimeter = $this->mail_bo->getHierarchyDelimiter(true);
+			$fShortName =  array_pop(explode($heirarchyDelimeter, $junkFolder));
+			$fStatus = array(
+				$icServerID.self::$delimiter.$trashFolder => lang($fShortName)
+			);
+			//Call to reset folder status counter, after junkFolder triggered not from Junk folder
+			//-as we don't have trash folder specific information available on client-side we need to deal with it on server
+			$response->call('app.mail.mail_setFolderStatus',$fStatus);
+		}
+		if ($rememberServerID != $this->mail_bo->profileID)
+		{
+			$oldFolderInfo = $this->mail_bo->getFolderStatus($junkFolder,false,false,false);
+			$response->call('egw.message',lang('empty junk'));
+			$response->call('app.mail.mail_reloadNode',array($icServerID.self::$delimiter.$junkFolder=>$oldFolderInfo['shortDisplayName']));
+			//error_log(__METHOD__.__LINE__.' change Profile to ->'.$rememberServerID);
+			$this->changeProfile($rememberServerID);
+		}
+		else if ($selectedFolder == $icServerID.self::$delimiter.$junkFolder)
+		{
+			$response->call('egw.refresh',lang('empty junk'),'mail');
+		}
+	}
+
 	/**
 	 * Empty trash folder
 	 *
diff --git a/mail/js/app.js b/mail/js/app.js
index cf7755be72..399424375f 100644
--- a/mail/js/app.js
+++ b/mail/js/app.js
@@ -1210,6 +1210,25 @@ app.classes.mail = AppJS.extend(
 		return true;
 	},
 
+	/**
+	 * Check if SpamFolder is enabled on that account
+	 *
+	 * SpamFolder enabled is stored as data { spamfolder: true/false } on account node.
+	 *
+	 * @param {object} _action
+	 * @param {object} _senders the representation of the tree leaf to be manipulated
+	 * @param {object} _currentNode
+	 */
+	spamfolder_enabled: function(_action,_senders,_currentNode)
+	{
+		var ftree = this.et2.getWidgetById(this.nm_index+'[foldertree]');
+		var acc_id = _senders[0].id.split('::')[0];
+		var node = ftree ? ftree.getNode(acc_id) : null;
+
+		return node && node.data && node.data.spamfolder;
+	},
+
+
 	/**
 	 * Check if Sieve is enabled on that account
 	 *
@@ -1574,6 +1593,38 @@ app.classes.mail = AppJS.extend(
 	// setting class of row, the old style
 	},
 
+	/**
+	 * mail_emptySpam
+	 *
+	 * @param {object} action
+	 * @param {object} _senders
+	 */
+	mail_emptySpam: function(action,_senders) {
+		var server = _senders[0].iface.id.split('::');
+		var activeFilters = this.mail_getActiveFilters();
+		var self = this;
+
+		this.egw.message(this.egw.lang('empty spam'));
+		egw.json('mail.mail_ui.ajax_emptySpam',[server[0], activeFilters['selectedFolder']? activeFilters['selectedFolder']:null],function(){self.unlock_tree();})
+			.sendRequest(true);
+
+		// Directly delete any trash cache for selected server
+		if(window.localStorage)
+		{
+			for(var i = 0; i < window.localStorage.length; i++)
+			{
+				var key = window.localStorage.key(i);
+
+				// Find directly by what the key would look like
+				if(key.indexOf('cached_fetch_mail::{"selectedFolder":"'+server[0]+'::') == 0 &&
+					key.toLowerCase().indexOf(egw.lang('junk').toLowerCase()) > 0)
+				{
+					window.localStorage.removeItem(key);
+				}
+			}
+		}
+	},
+
 	/**
 	 * mail_emptyTrash
 	 *

From c67ab8744af398c29b9e45f44aaf3fe104aebdce Mon Sep 17 00:00:00 2001
From: Klaus Leithoff <kl@stylite.de>
Date: Thu, 19 Feb 2015 12:26:44 +0000
Subject: [PATCH 34/43] remove probably wrong assumption on missing ident_id of
 mailaccount object (as it should not be missing at all)

---
 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 b7186c33a4..d232693127 100644
--- a/mail/inc/class.mail_compose.inc.php
+++ b/mail/inc/class.mail_compose.inc.php
@@ -1025,7 +1025,7 @@ class mail_compose
 
 		// fetch the signature, prepare the select box, ...
 		if (empty($content['mailidentity'])) {
-			$content['mailidentity'] = $acc['ident_id']?$acc['ident_id']:$acc['acc_id'];
+			$content['mailidentity'] = $acc['ident_id'];
 		}
 
 		$disableRuler = false;

From 3ef17e277f4da1925f5ff9881bd3cfbd5184eccb Mon Sep 17 00:00:00 2001
From: Klaus Leithoff <kl@stylite.de>
Date: Thu, 19 Feb 2015 13:41:40 +0000
Subject: [PATCH 35/43] add missing translations for emptying spam/junk folder;
 fix setting folder status after voiding spam/junk folder content

---
 mail/inc/class.mail_ui.inc.php | 8 ++++----
 mail/js/app.js                 | 2 +-
 mail/lang/egw_de.lang          | 1 +
 mail/lang/egw_en.lang          | 1 +
 4 files changed, 7 insertions(+), 5 deletions(-)

diff --git a/mail/inc/class.mail_ui.inc.php b/mail/inc/class.mail_ui.inc.php
index 6c58b72b6c..69f13fbea9 100644
--- a/mail/inc/class.mail_ui.inc.php
+++ b/mail/inc/class.mail_ui.inc.php
@@ -630,7 +630,7 @@ class mail_ui
 				if ($junkFolder && !empty($junkFolder))
 				{
 					$tree_actions['empty_spam'] = array(
-						'caption' => 'empty spam',
+						'caption' => 'empty junk',
 						'icon' => 'dhtmlxtree/MailFolderJunk',
 						'enabled'	=> 'javaScript:app.mail.spamfolder_enabled',
 						'onExecute' => 'javaScript:app.mail.mail_emptySpam',
@@ -4248,7 +4248,7 @@ class mail_ui
 		if(!empty($junkFolder)) {
 			if ($selectedFolder == $icServerID.self::$delimiter.$junkFolder)
 			{
-				// Lock the tree if the active folder is Trash folder
+				// Lock the tree if the active folder is junk folder
 				$response->call('app.mail.lock_tree');
 			}
 			$this->mail_bo->deleteMessages('all',$junkFolder,'remove_immediately');
@@ -4256,10 +4256,10 @@ class mail_ui
 			$heirarchyDelimeter = $this->mail_bo->getHierarchyDelimiter(true);
 			$fShortName =  array_pop(explode($heirarchyDelimeter, $junkFolder));
 			$fStatus = array(
-				$icServerID.self::$delimiter.$trashFolder => lang($fShortName)
+				$icServerID.self::$delimiter.$junkFolder => lang($fShortName)
 			);
 			//Call to reset folder status counter, after junkFolder triggered not from Junk folder
-			//-as we don't have trash folder specific information available on client-side we need to deal with it on server
+			//-as we don't have junk folder specific information available on client-side we need to deal with it on server
 			$response->call('app.mail.mail_setFolderStatus',$fStatus);
 		}
 		if ($rememberServerID != $this->mail_bo->profileID)
diff --git a/mail/js/app.js b/mail/js/app.js
index 399424375f..0cb8550669 100644
--- a/mail/js/app.js
+++ b/mail/js/app.js
@@ -1604,7 +1604,7 @@ app.classes.mail = AppJS.extend(
 		var activeFilters = this.mail_getActiveFilters();
 		var self = this;
 
-		this.egw.message(this.egw.lang('empty spam'));
+		this.egw.message(this.egw.lang('empty junk'));
 		egw.json('mail.mail_ui.ajax_emptySpam',[server[0], activeFilters['selectedFolder']? activeFilters['selectedFolder']:null],function(){self.unlock_tree();})
 			.sendRequest(true);
 
diff --git a/mail/lang/egw_de.lang b/mail/lang/egw_de.lang
index 59874e27e5..3750e9f9f5 100644
--- a/mail/lang/egw_de.lang
+++ b/mail/lang/egw_de.lang
@@ -135,6 +135,7 @@ email notification update failed	mail	de	Die Benachrichtigung über den Gelesen-
 email notification update failed! you need to set an email address!	mail	de	Aktualisierung der E-Mail Benachrichtigungen fehlgeschlagen! Es muss eine E-Mail-Adresse ausgewählt sein!
 emailaddress	admin	de	E-Mail
 emailadmin: profilemanagement	mail	de	E-Mail-Admin: Profilmanagement
+empty junk	mail	de	Junk-/Spam- Ordner leeren
 empty trash	mail	de	Papierkorb leeren
 enable	mail	de	Aktivieren
 enabled!	mail	de	aktiviert!
diff --git a/mail/lang/egw_en.lang b/mail/lang/egw_en.lang
index 3a3c1be5bc..af4bdb06cb 100644
--- a/mail/lang/egw_en.lang
+++ b/mail/lang/egw_en.lang
@@ -135,6 +135,7 @@ email notification update failed	mail	en	email notification update failed
 email notification update failed! you need to set an email address!	mail	en	email notification update failed! You need to set an email address!
 emailaddress	admin	en	emailaddress
 emailadmin: profilemanagement	mail	en	eMailAdmin: Profilemanagement
+empty junk	mail	en	empty junk/spam folder
 empty trash	mail	en	empty trash
 enable	mail	en	Enable
 enabled!	mail	en	enabled!

From a6bb56d2e1e2b5bf206fc963d6f645f3a1539101 Mon Sep 17 00:00:00 2001
From: Nathan Gray <nathangray.bsc@gmail.com>
Date: Thu, 19 Feb 2015 17:14:31 +0000
Subject: [PATCH 36/43] Fix bug in rule removal prevented adding more rules

---
 phpgwapi/js/jsapi/egw_css.js | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/phpgwapi/js/jsapi/egw_css.js b/phpgwapi/js/jsapi/egw_css.js
index 590f8eb6c0..00b56969ed 100644
--- a/phpgwapi/js/jsapi/egw_css.js
+++ b/phpgwapi/js/jsapi/egw_css.js
@@ -77,6 +77,10 @@ egw.extend('css', egw.MODULE_WND_LOCAL, function(_app, _wnd) {
 				}
 
 				delete (selectors[_selector]);
+				if(!_rule)
+				{
+					selectorCount--;
+				}
 			}
 			else
 			{

From 904bc8b19fff8a12b7e997fe56589f966bb273e8 Mon Sep 17 00:00:00 2001
From: Nathan Gray <nathangray.bsc@gmail.com>
Date: Thu, 19 Feb 2015 17:16:39 +0000
Subject: [PATCH 37/43] Better row limiting by using CSS instead of grid's
 average height

---
 etemplate/js/et2_extension_nextmatch.js | 61 ++++++++++++++++---------
 1 file changed, 39 insertions(+), 22 deletions(-)

diff --git a/etemplate/js/et2_extension_nextmatch.js b/etemplate/js/et2_extension_nextmatch.js
index b1df8f0888..cd28e13f85 100644
--- a/etemplate/js/et2_extension_nextmatch.js
+++ b/etemplate/js/et2_extension_nextmatch.js
@@ -1875,7 +1875,7 @@ var et2_nextmatch = et2_DOMWidget.extend([et2_IResizeable, et2_IInput, et2_IPrin
 		this.resize();
 		// Reset height to auto (after width resize) so there's no restrictions
 		this.dynheight.innerNode.css('height', 'auto');
-
+		
 		// Check for rows that aren't loaded yet, or lots of rows
 		var range = this.controller._grid.getIndexRange();
 		this.old_height = this.controller._grid._scrollHeight;
@@ -1953,30 +1953,20 @@ var et2_nextmatch = et2_DOMWidget.extend([et2_IResizeable, et2_IInput, et2_IPrin
 										defer.reject();
 										return;
 									}
-									if(value < total)
-									{
-										// Set height to the requested number of rows, using the average height.
-										// We add one, in case there's some larger rows we
-										// try to get most of it but that's pretty hacky
-										nm.controller._grid.setScrollHeight(nm.controller._grid.getAverageHeight() * (value+1));
-									}
+									// Use CSS to hide all but the requested rows
+									// Prevents us from showing more than requested, if actual height was less than average
+									nm.print_row_selector = ".egwGridView_grid > tbody > tr:not(:nth-child(-n+"+value+"))";
+									egw.css(nm.print_row_selector, 'display: none');
+
+									// No scrollbar in print view
+									$j('.egwGridView_scrollarea',this.div).css('overflow-y','hidden');
+									// Show it all
+									$j('.egwGridView_scrollarea',this.div).css('height','auto');
 									
 									// Grid needs to redraw before it can be printed, so wait
 									window.setTimeout(jQuery.proxy(function() {
 										dialog.destroy();
-
-										if(value < total)
-										{
-											// Show requested number, based on average height
-											nm.controller._grid.setScrollHeight(nm.controller._grid.getAverageHeight() * (value));
-											// No scrollbar in print view
-											$j('.egwGridView_scrollarea',this.div).css('overflow-y','hidden');
-										}
-										else
-										{
-											// Show it all
-											$j('.egwGridView_scrollarea',this.div).css('height','auto');
-										}
+										
 										// Should be OK to print now
 										defer.resolve();
 									},nm),ET2_GRID_INVALIDATE_TIMEOUT);
@@ -1991,7 +1981,15 @@ var et2_nextmatch = et2_DOMWidget.extend([et2_IResizeable, et2_IInput, et2_IPrin
 					else
 					{
 						// Don't need more rows, limit to requested and finish
-						this.controller._grid.setScrollHeight(this.controller._grid.getAverageHeight() * (value));
+						
+						// Show it all
+						$j('.egwGridView_scrollarea',this.div).css('height','auto');
+
+						// Use CSS to hide all but the requested rows
+						// Prevents us from showing more than requested, if actual height was less than average
+						this.print_row_selector = ".egwGridView_grid > tbody > tr:not(:nth-child(-n+"+value+"))";
+						egw.css(this.print_row_selector, 'display: none');
+
 						// No scrollbar in print view
 						$j('.egwGridView_scrollarea',this.div).css('overflow-y','hidden');
 						// Give dialog a chance to close, or it will be in the print
@@ -2017,10 +2015,29 @@ var et2_nextmatch = et2_DOMWidget.extend([et2_IResizeable, et2_IInput, et2_IPrin
 		}
 		// Don't return anything, just work normally
 	},
+
+	/**
+	 * Try to clean up the mess we made getting ready for printing
+	 * in beforePrint()
+	 */
 	afterPrint: function() {
+		
 		this.div.removeClass('print');
+
+		// Put scrollbar back
+		$j('.egwGridView_scrollarea',this.div).css('overflow-y','');
+
+		// Correct size of grid, and trigger resize to fix it
 		this.controller._grid.setScrollHeight(this.old_height);
 		delete this.old_height;
+		
+		// Remove CSS rule hiding extra rows
+		if(this.print_row_selector)
+		{
+			egw.css(this.print_row_selector, false);
+			delete this.print_row_selector;
+		}
+
 		this.dynheight.outerNode.css('max-width','inherit');
 		this.resize();
 	}

From f6260c1c8c59c66868f5f7d3dccf9cf14eeafe0d Mon Sep 17 00:00:00 2001
From: Hadi Nategh <hn@stylite.de>
Date: Fri, 20 Feb 2015 12:28:13 +0000
Subject: [PATCH 38/43] Change the login layout if only it is called from
 devices with small screen

---
 pixelegg/css/mobile.css  | 124 ++++++++++++++++----------------
 pixelegg/css/mobile.less | 150 ++++++++++++++++++++-------------------
 2 files changed, 139 insertions(+), 135 deletions(-)

diff --git a/pixelegg/css/mobile.css b/pixelegg/css/mobile.css
index 1421dda292..4344359391 100644
--- a/pixelegg/css/mobile.css
+++ b/pixelegg/css/mobile.css
@@ -6265,67 +6265,6 @@ a.textSidebox {
   body {
     background-color: transparent;
   }
-  body div#loginMainDiv #divAppIconBar #divLogo img[src$="svg"] {
-    width: 40%;
-    margin-top: 5px;
-  }
-  body div#loginMainDiv div#centerBox {
-    position: absolute;
-    margin: 0;
-    width: 100%;
-    background-color: transparent;
-    padding: 0;
-    -webkit-border-top-right-radius: 0;
-    -webkit-border-bottom-right-radius: 0;
-    -webkit-border-bottom-left-radius: 0;
-    -webkit-border-top-left-radius: 0;
-    -moz-border-radius-topright: 0;
-    -moz-border-radius-bottomright: 0;
-    -moz-border-radius-bottomleft: 0;
-    -moz-border-radius-topleft: 0;
-    border-top-right-radius: 0;
-    border-bottom-right-radius: 0;
-    border-bottom-left-radius: 0;
-    border-top-left-radius: 0;
-    background-color: none;
-    background-image: none;
-    background-repeat: none;
-    border: none;
-    border-radius: none;
-  }
-  body div#loginMainDiv div#centerBox form {
-    margin-top: -2em;
-    margin-right: 3em;
-  }
-  body div#loginMainDiv div#centerBox form table.divLoginbox {
-    width: 100%;
-    float: left;
-  }
-  body div#loginMainDiv div#centerBox form table.divLoginbox tr.hiddenCredential {
-    display: none;
-  }
-  body div#loginMainDiv div#centerBox form table.divLoginbox input[type="submit"] {
-    font-size: xx-large;
-  }
-  body div#loginMainDiv div#centerBox form table.divLoginbox input,
-  body div#loginMainDiv div#centerBox form table.divLoginbox select {
-    width: 100%;
-    height: 60px;
-  }
-  body div#loginMainDiv div#centerBox form table.divLoginbox td {
-    font-size: 300%;
-    padding: 0.8%;
-  }
-  body div#loginMainDiv div#centerBox form table.divLoginbox td.registration {
-    font-size: 180%;
-  }
-  body div#loginMainDiv div#centerBox form table.divLoginbox td select {
-    background-size: 48px auto;
-  }
-  body div#loginMainDiv div#centerBox #loginCdMessage {
-    font-size: large;
-    padding: 0;
-  }
   body div.egw_fw_mobile_iOS_popup_appHeader {
     padding-top: 15px;
   }
@@ -7040,6 +6979,69 @@ a.textSidebox {
     background-position: center;
   }
 }
+@media only screen and (max-device-width : 1024px) {
+  div#loginMainDiv #divAppIconBar #divLogo img[src$="svg"] {
+    width: 40%;
+    margin-top: 5px;
+  }
+  div#loginMainDiv div#centerBox {
+    position: absolute;
+    margin: 0;
+    width: 100%;
+    background-color: transparent;
+    padding: 0;
+    -webkit-border-top-right-radius: 0;
+    -webkit-border-bottom-right-radius: 0;
+    -webkit-border-bottom-left-radius: 0;
+    -webkit-border-top-left-radius: 0;
+    -moz-border-radius-topright: 0;
+    -moz-border-radius-bottomright: 0;
+    -moz-border-radius-bottomleft: 0;
+    -moz-border-radius-topleft: 0;
+    border-top-right-radius: 0;
+    border-bottom-right-radius: 0;
+    border-bottom-left-radius: 0;
+    border-top-left-radius: 0;
+    background-color: none;
+    background-image: none;
+    background-repeat: none;
+    border: none;
+    border-radius: none;
+  }
+  div#loginMainDiv div#centerBox form {
+    margin-top: -2em;
+    margin-right: 3em;
+  }
+  div#loginMainDiv div#centerBox form table.divLoginbox {
+    width: 100%;
+    float: left;
+  }
+  div#loginMainDiv div#centerBox form table.divLoginbox tr.hiddenCredential {
+    display: none;
+  }
+  div#loginMainDiv div#centerBox form table.divLoginbox input[type="submit"] {
+    font-size: xx-large;
+  }
+  div#loginMainDiv div#centerBox form table.divLoginbox input,
+  div#loginMainDiv div#centerBox form table.divLoginbox select {
+    width: 100%;
+    height: 60px;
+  }
+  div#loginMainDiv div#centerBox form table.divLoginbox td {
+    font-size: 300%;
+    padding: 0.8%;
+  }
+  div#loginMainDiv div#centerBox form table.divLoginbox td.registration {
+    font-size: 180%;
+  }
+  div#loginMainDiv div#centerBox form table.divLoginbox td select {
+    background-size: 48px auto;
+  }
+  div#loginMainDiv div#centerBox #loginCdMessage {
+    font-size: large;
+    padding: 0;
+  }
+}
 @media only screen and (max-device-width : 1024px) and (orientation : portrait) {
   body div#loginMainDiv #divAppIconBar #divLogo img[src$="svg"] {
     width: 70%;
diff --git a/pixelegg/css/mobile.less b/pixelegg/css/mobile.less
index 63aac28461..161ebe80e0 100644
--- a/pixelegg/css/mobile.less
+++ b/pixelegg/css/mobile.less
@@ -23,6 +23,7 @@
 @smartphone-max: 768px;
 /*Smartphones Min-Width*/
 @smartphone-min: 321px;
+@handheld: ~"only screen and (max-device-width : @{tablet-max})";
 /*All devices portrait mode*/
 @handheld-portrait: ~"only screen and (max-device-width : @{tablet-max}) and (orientation : portrait)";
 /*All devices landscape mode*/
@@ -35,80 +36,6 @@
 
 @media all {
 	body{
-
-		div#loginMainDiv{
-			#divAppIconBar {
-				#divLogo img[src$="svg"] {
-					width:40%;
-					margin-top: 5px;
-				}
-			}
-			div#centerBox{
-				position:absolute;
-				margin: 0;
-				width: 100%;
-				background-color: transparent;
-				padding: 0;
-				-webkit-border-top-right-radius: 0;
-				-webkit-border-bottom-right-radius:0;
-				-webkit-border-bottom-left-radius: 0;
-				-webkit-border-top-left-radius: 0;
-				-moz-border-radius-topright: 0;
-				-moz-border-radius-bottomright: 0;
-				-moz-border-radius-bottomleft: 0;
-				-moz-border-radius-topleft: 0;
-				border-top-right-radius: 0;
-				border-bottom-right-radius: 0;
-				border-bottom-left-radius: 0;
-				border-top-left-radius: 0;
-				background-color: none;
-				background-image: none;
-				background-image: none;
-				background-image: none;
-				background-image: none;
-				background-image: none;
-				background-image: none;
-				background-image: none;
-				background-repeat: none;
-				border:none;
-				border-radius: none;
-				form {
-					margin-top: -2em;
-					margin-right: 3em;
-
-					table.divLoginbox {
-						width:100%;
-						float:left;
-						tr.hiddenCredential {
-							display:none;
-						}
-						input[type="submit"] {
-							font-size: xx-large;
-
-						}
-						input, select {
-							width:100%;
-							height:60px;
-						}
-						td {
-							font-size: 300%;
-							padding:0.8%;
-							&.registration{
-								font-size: 180%;
-							}
-							select {
-								background-size: 48px auto;
-							}
-						}
-					}
-				}
-				#loginCdMessage {
-					font-size:large;
-					padding:0;
-				}
-			}
-		}
-
 		background-color: transparent;
 		// iOS appHeader class
 		div.egw_fw_mobile_iOS_popup_appHeader{
@@ -829,6 +756,81 @@
 		background-position: center;
 	}
 }
+@media @handheld
+{
+	div#loginMainDiv{
+			#divAppIconBar {
+				#divLogo img[src$="svg"] {
+					width:40%;
+					margin-top: 5px;
+				}
+			}
+			div#centerBox{
+				position:absolute;
+				margin: 0;
+				width: 100%;
+				background-color: transparent;
+				padding: 0;
+				-webkit-border-top-right-radius: 0;
+				-webkit-border-bottom-right-radius:0;
+				-webkit-border-bottom-left-radius: 0;
+				-webkit-border-top-left-radius: 0;
+				-moz-border-radius-topright: 0;
+				-moz-border-radius-bottomright: 0;
+				-moz-border-radius-bottomleft: 0;
+				-moz-border-radius-topleft: 0;
+				border-top-right-radius: 0;
+				border-bottom-right-radius: 0;
+				border-bottom-left-radius: 0;
+				border-top-left-radius: 0;
+				background-color: none;
+				background-image: none;
+				background-image: none;
+				background-image: none;
+				background-image: none;
+				background-image: none;
+				background-image: none;
+				background-image: none;
+				background-repeat: none;
+				border:none;
+				border-radius: none;
+				form {
+					margin-top: -2em;
+					margin-right: 3em;
+
+					table.divLoginbox {
+						width:100%;
+						float:left;
+						tr.hiddenCredential {
+							display:none;
+						}
+						input[type="submit"] {
+							font-size: xx-large;
+
+						}
+						input, select {
+							width:100%;
+							height:60px;
+						}
+						td {
+							font-size: 300%;
+							padding:0.8%;
+							&.registration{
+								font-size: 180%;
+							}
+							select {
+								background-size: 48px auto;
+							}
+						}
+					}
+				}
+				#loginCdMessage {
+					font-size:large;
+					padding:0;
+				}
+			}
+		}
+}
 @media @handheld-portrait
 {
 	body{

From 8ab9d48a96ac5407008d03d63a92d9ba7356d74f Mon Sep 17 00:00:00 2001
From: Hadi Nategh <hn@stylite.de>
Date: Fri, 20 Feb 2015 14:05:32 +0000
Subject: [PATCH 39/43] Let the mobile theme uses pixelegg custom color

---
 pixelegg/inc/class.pixelegg_framework.inc.php | 9 ++++-----
 1 file changed, 4 insertions(+), 5 deletions(-)

diff --git a/pixelegg/inc/class.pixelegg_framework.inc.php b/pixelegg/inc/class.pixelegg_framework.inc.php
index b4e550cd06..9ea8a16d6d 100755
--- a/pixelegg/inc/class.pixelegg_framework.inc.php
+++ b/pixelegg/inc/class.pixelegg_framework.inc.php
@@ -98,7 +98,6 @@ class pixelegg_framework extends jdots_framework
 	 */
 	public function _get_css()
 	{
-		if (html::$ua_mobile || $GLOBALS['egw_info']['user']['preferences']['common']['theme'] == 'mobile') return egw_framework::_get_css();
 		$ret = parent::_get_css();
 		// color to use
 		$color = str_replace('custom',$GLOBALS['egw_info']['user']['preferences']['common']['template_custom_color'],
@@ -124,9 +123,9 @@ class pixelegg_framework extends jdots_framework
 -popup toolbar
 */
 div#egw_fw_header, div.egw_fw_ui_category:hover,#loginMainDiv,#loginMainDiv #divAppIconBar #divLogo,
-#egw_fw_sidebar #egw_fw_sidemenu .egw_fw_ui_scrollarea_outerdiv .egw_fw_ui_sidemenu_entry_content .egw_fw_ui_category_active:hover,
+#egw_fw_sidebar #egw_fw_sidemenu .egw_fw_ui_category_active:hover,
 .dialogFooterToolbar, .ui-widget-header{
-	background-color: $color;
+	background-color: $color !important;
 }
 
 /*Login background*/
@@ -150,8 +149,8 @@ div#egw_fw_header, div.egw_fw_ui_category:hover,#loginMainDiv,#loginMainDiv #div
 }
 
 /*Sidebar menu active category*/
-#egw_fw_sidebar #egw_fw_sidemenu .egw_fw_ui_scrollarea_outerdiv .egw_fw_ui_sidemenu_entry_content .egw_fw_ui_category_active{
-	background-color: $color_hex_darker;
+#egw_fw_sidebar #egw_fw_sidemenu .egw_fw_ui_category_active{
+	background-color: $color_hex_darker !important;
 }
 ";
 		}

From d1ce50fe5e8d205cc3c68b12b952f18d22bc8d7d Mon Sep 17 00:00:00 2001
From: Hadi Nategh <hn@stylite.de>
Date: Fri, 20 Feb 2015 16:06:18 +0000
Subject: [PATCH 40/43] Fix toolbar gets background after setting template
 custom color

---
 pixelegg/inc/class.pixelegg_framework.inc.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pixelegg/inc/class.pixelegg_framework.inc.php b/pixelegg/inc/class.pixelegg_framework.inc.php
index 9ea8a16d6d..77a6df04ee 100755
--- a/pixelegg/inc/class.pixelegg_framework.inc.php
+++ b/pixelegg/inc/class.pixelegg_framework.inc.php
@@ -124,7 +124,7 @@ class pixelegg_framework extends jdots_framework
 */
 div#egw_fw_header, div.egw_fw_ui_category:hover,#loginMainDiv,#loginMainDiv #divAppIconBar #divLogo,
 #egw_fw_sidebar #egw_fw_sidemenu .egw_fw_ui_category_active:hover,
-.dialogFooterToolbar, .ui-widget-header{
+.dialogFooterToolbar, .et2_portlet .ui-widget-header{
 	background-color: $color !important;
 }
 

From 229d4063a3b26381c84a1951ba626c1688468a92 Mon Sep 17 00:00:00 2001
From: Hadi Nategh <hn@stylite.de>
Date: Fri, 20 Feb 2015 16:14:34 +0000
Subject: [PATCH 41/43] Add translation of "Link title for events to show" into
 calendar

---
 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 3331f79bdf..a0a60b9688 100644
--- a/calendar/lang/egw_de.lang
+++ b/calendar/lang/egw_de.lang
@@ -307,6 +307,7 @@ last changed	calendar	de	letzte Änderung
 lastname of person to notify	calendar	de	Nachname der zu benachrichtigenden Person
 length of the time interval	calendar	de	Länge des Zeitintervalls
 limit number of description lines in list view (default 5, 0 for no limit)	calendar	de	Anzahl Zeilen der Beschreibung in der Listenansicht (voreingestellt 5, 0 für alle)
+link title for events to show	calendar	de	Erweiterung des Link-Titels für Kalender-Einträge
 link to view the event	calendar	de	Verweis (Weblink) um den Termin anzuzeigen
 links	calendar	de	Verknüpfungen
 links and attached files	calendar	de	Verknüpfungen zu anderen Datensätzen und angefügten Dateien
diff --git a/calendar/lang/egw_en.lang b/calendar/lang/egw_en.lang
index 82070e586e..92b22f74aa 100644
--- a/calendar/lang/egw_en.lang
+++ b/calendar/lang/egw_en.lang
@@ -307,6 +307,7 @@ last changed	calendar	en	Last changed
 lastname of person to notify	calendar	en	Last name of a person to notify
 length of the time interval	calendar	en	Length of the time interval
 limit number of description lines in list view (default 5, 0 for no limit)	calendar	en	Limit number of description lines in list view. Default is 5, 0 for no limit.
+link title for events to show	calendar	en	Link title for events to show
 link to view the event	calendar	en	Link to view the event
 links	calendar	en	Links
 links and attached files	calendar	en	Links and attached files

From 370e503dabbf1c4544437c44f755a4af2d1ca4dc Mon Sep 17 00:00:00 2001
From: Ralf Becker <ralfbecker@outdoor-training.de>
Date: Fri, 20 Feb 2015 19:04:58 +0000
Subject: [PATCH 42/43] * Safari: fix security warning caused by auto-complete
 when submitting from https to about:blank

---
 etemplate/js/etemplate2.js | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

diff --git a/etemplate/js/etemplate2.js b/etemplate/js/etemplate2.js
index 50a0f30fa7..380b822e1f 100644
--- a/etemplate/js/etemplate2.js
+++ b/etemplate/js/etemplate2.js
@@ -597,8 +597,11 @@ etemplate2.prototype.autocomplete_fixer = function ()
 		form.onsubmit = function(){return false;};
 		// Firefox give a security warning when transmitting to "about:blank" from a https site
 		// we work around that by giving existing etemplate/empty.html url
-		if (navigator.userAgent.match(/firefox/i)) jQuery(form).attr({action: egw.webserverUrl+'/etemplate/empty.html',method:'post'});
-
+		// Safari shows same warning, thought Chrome userAgent also includes Safari
+		if (navigator.userAgent.match(/(firefox|safari)/i) && !navigator.userAgent.match(/chrome/i))
+		{
+			jQuery(form).attr({action: egw.webserverUrl+'/etemplate/empty.html',method:'post'});
+		}
 		form.submit();
 	}
 };
@@ -937,7 +940,7 @@ etemplate2.prototype.print = function()
 	},this,et2_IPrint);
 
 	return deferred;
-}
+};
 
 // Some static things to make getting into widget context a little easier //
 

From 9cc1d409eba799feaab7b9659dbcf49943155532 Mon Sep 17 00:00:00 2001
From: Ralf Becker <ralfbecker@outdoor-training.de>
Date: Sat, 21 Feb 2015 13:29:10 +0000
Subject: [PATCH 43/43] harden login page by no longer using www.groupware.org
 to load social media icons

---
 phpgwapi/inc/class.egw_framework.inc.php      | 50 +++++++++++++++----
 phpgwapi/js/login.js                          | 34 +++++++------
 .../default/images/login_contact.svg          | 21 ++++++++
 .../default/images/login_facebook.svg         | 19 +++++++
 .../default/images/login_twitter.svg          | 24 +++++++++
 5 files changed, 123 insertions(+), 25 deletions(-)
 create mode 100644 phpgwapi/templates/default/images/login_contact.svg
 create mode 100644 phpgwapi/templates/default/images/login_facebook.svg
 create mode 100644 phpgwapi/templates/default/images/login_twitter.svg

diff --git a/phpgwapi/inc/class.egw_framework.inc.php b/phpgwapi/inc/class.egw_framework.inc.php
index 65ad82747b..88aa4193d3 100644
--- a/phpgwapi/inc/class.egw_framework.inc.php
+++ b/phpgwapi/inc/class.egw_framework.inc.php
@@ -180,18 +180,50 @@ abstract class egw_framework
 				//error_log(__METHOD__."() setting CSP script-src $attr ".function_backtrace());
 			}
 		}
-		//error_log(__METHOD__."(".array2string($set).") returned ".array2string(implode(' ', self::$csp_script_src_attrs)).' '.function_backtrace());
+		//error_log(__METHOD__."(".array2string($set).") returned ".array2string(implode(' ', self::$csp_connect_src_attrs)).' '.function_backtrace());
 		return implode(' ', self::$csp_connect_src_attrs);
 	}
 
 	/**
-	 * Query additional CSP frame-src from current app
+	 * Additional attributes or urls for CSP frame-src 'self'
 	 *
-	 * @return array
+	 * @var array
 	 */
-	protected function _get_csp_frame_src()
+	private static $csp_frame_src_attrs;
+
+	/**
+	 * Set/get Content-Security-Policy attributes for frame-src:
+	 *
+	 * Calling this method with an empty array sets no frame-src, but "'self'"!
+	 *
+	 * @param string|array $set =array() URL (incl. protocol!)
+	 * @return string with attributes eg. "'unsafe-inline'"
+	 */
+	public static function csp_frame_src_attrs($set=null)
 	{
-		return $GLOBALS['egw']->hooks->single('csp-frame-src', $GLOBALS['egw_info']['flags']['currentapp']);
+		// set frame-src attrs of API and apps via hook
+		if (!isset(self::$csp_frame_src_attrs) && !isset($set))
+		{
+			$frame_src = array('manual.egroupware.org', 'www.egroupware.org');
+			if (($additional = $GLOBALS['egw']->hooks->single('csp-frame-src', $GLOBALS['egw_info']['flags']['currentapp'])))
+			{
+				$frame_src = array_unique(array_merge($frame_src, $additional));
+			}
+			return self::csp_frame_src_attrs($frame_src);
+		}
+
+		if (!isset(self::$csp_frame_src_attrs)) self::$csp_frame_src_attrs = array();
+
+		foreach((array)$set as $attr)
+		{
+			if (!in_array($attr, self::$csp_frame_src_attrs))
+			{
+				self::$csp_frame_src_attrs[] = $attr;
+				//error_log(__METHOD__."() setting CSP script-src $attr ".function_backtrace());
+			}
+		}
+		//error_log(__METHOD__."(".array2string($set).") returned ".array2string(implode(' ', self::$csp_frame_src_attrs)).' '.function_backtrace());
+		return implode(' ', self::$csp_frame_src_attrs);
 	}
 
 	/**
@@ -207,13 +239,10 @@ abstract class egw_framework
 		// - "connect-src 'self'" allows ajax requests only to self
 		// - "style-src 'self' 'unsave-inline'" allows only self and inline style, which we need
 		// - "frame-src 'self' manual.egroupware.org" allows frame and iframe content only for self or manual.egroupware.org
-		$frame_src = array("'self'", 'manual.egroupware.org', 'www.egroupware.org');
-		if (($additional = $this->_get_csp_frame_src())) $frame_src = array_unique(array_merge($frame_src, $additional));
-
 		$csp = "script-src 'self' ".self::csp_script_src_attrs().
 			"; connect-src 'self' ".self::csp_connect_src_attrs().
 			"; style-src 'self' ".self::csp_style_src_attrs().
-			"; frame-src ".implode(' ', $frame_src);
+			"; frame-src 'self' ".self::csp_frame_src_attrs();
 
 		//$csp = "default-src * 'unsafe-eval' 'unsafe-inline'";	// allow everything
 		header("Content-Security-Policy: $csp");
@@ -512,8 +541,7 @@ abstract class egw_framework
 	*/
 	function login_screen($extra_vars)
 	{
-		//allow to include JSONP file with social media urls from egroupware.org
-		self::csp_script_src_attrs('https://www.egroupware.org');
+		self::csp_frame_src_attrs(array());	// array() no external frame-sources
 
 		//error_log(__METHOD__."() server[template_dir]=".array2string($GLOBALS['egw_info']['server']['template_dir']).", this->template=$this->template, this->template_dir=$this->template_dir, get_class(this)=".get_class($this));
 		$tmpl = new Template($GLOBALS['egw_info']['server']['template_dir']);
diff --git a/phpgwapi/js/login.js b/phpgwapi/js/login.js
index 0fff07bc79..26820641fb 100644
--- a/phpgwapi/js/login.js
+++ b/phpgwapi/js/login.js
@@ -1,19 +1,19 @@
-/*
- * To change this license header, choose License Headers in Project Properties.
- * To change this template file, choose Tools | Templates
- * and open the template in the editor.
+/**
+ * EGroupware login page javascript
+ *
+ * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
+ * @package etemplate
+ * @subpackage api
+ * @link http://www.egroupware.org
+ * @version $Id$
  */
 
-
-egw_LAB.wait(function() {
-	$j.ajax('https://www.egroupware.org/social.js', {
-		dataType: "jsonp",
-		jsonp: false,
-		jsonpCallback: "do_social",
-		cache: true
-	}).done(function(_data)
+egw_LAB.wait(function()
+{
+	$j(document).ready(function()
 	{
-		$j(document).ready(function() {
+		function do_social(_data)
+		{
 			var isPixelegg = $j('link[href*="pixelegg.css"]')[0];
 			var social = $j(document.createElement('div'))
 				.attr({
@@ -34,6 +34,12 @@ egw_LAB.wait(function() {
 				.append($j(document.createElement('img'))
 					.attr('src', data.svg));
 			}
-		});
+		}
+
+		do_social([
+			{ "svg": egw_webserverUrl+"/phpgwapi/templates/default/images/login_contact.svg", "url": "https://www.egroupware.org/en/contact.html", "lang": { "de": "https://www.egroupware.org/de/kontakt.html" }},
+			{ "svg": egw_webserverUrl+"/phpgwapi/templates/default/images/login_facebook.svg", "url": "https://www.facebook.com/egroupware" },
+			{ "svg": egw_webserverUrl+"/phpgwapi/templates/default/images/login_twitter.svg", "url": "https://twitter.com/egroupware" }
+		]);
 	});
 });
diff --git a/phpgwapi/templates/default/images/login_contact.svg b/phpgwapi/templates/default/images/login_contact.svg
new file mode 100644
index 0000000000..5ce8142435
--- /dev/null
+++ b/phpgwapi/templates/default/images/login_contact.svg
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<?xml-stylesheet type="text/css" href="../../../../pixelegg/less/svg.css" ?>
+<svg version="1.1" id="mail_navbar" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="28.347px" height="28.347px" viewBox="0 0 28.347 28.347" enable-background="new 0 0 28.347 28.347" xml:space="preserve">
+<g>
+	<path fill-rule="evenodd" clip-rule="evenodd" fill="#ECEDED" d="M20.038,16.039c-3.507,0-5.767,2.659-5.767,5.72
+		c0,2.867,2.195,4.649,4.788,4.649c1.059,0,1.942-0.163,2.812-0.595l-0.253-0.639c-0.647,0.342-1.485,0.521-2.386,0.521
+		c-2.338,0-4.06-1.545-4.06-4.026c0-3.016,2.164-4.947,4.739-4.947c2.448,0,3.823,1.561,3.823,3.729
+		c0,1.708-0.901,2.733-1.707,2.703c-0.521-0.015-0.711-0.534-0.473-1.663l0.537-2.689c-0.412-0.178-1.028-0.312-1.691-0.312
+		c-2.196,0-3.743,1.68-3.743,3.521c0,1.174,0.789,1.871,1.706,1.871c0.947,0,1.674-0.43,2.228-1.307h0.046
+		c-0.03,0.921,0.554,1.307,1.186,1.307c1.469,0,2.797-1.307,2.797-3.535C24.62,17.866,22.787,16.039,20.038,16.039z M20.67,21.061
+		c-0.174,0.92-1.011,2.02-1.927,2.02c-0.695,0-1.043-0.476-1.043-1.129c0-1.44,1.121-2.674,2.512-2.674
+		c0.363,0,0.632,0.06,0.79,0.119L20.67,21.061z"/>
+	<polygon fill-rule="evenodd" clip-rule="evenodd" fill="#ECEDED" points="22.897,4.501 22.467,2.624 2.439,7.218 2.869,9.095 
+		14.319,13.058 	"/>
+	<path fill-rule="evenodd" clip-rule="evenodd" fill="#ECEDED" d="M23.185,6.551l-8.578,8.556l-11.45-3.961l2.44,9.842l7.337-1.684
+		c0.902-2.904,3.611-5.013,6.812-5.013c2.052,0,3.896,0.872,5.197,2.259l0.683-0.157L23.185,6.551z"/>
+</g>
+</svg>
diff --git a/phpgwapi/templates/default/images/login_facebook.svg b/phpgwapi/templates/default/images/login_facebook.svg
new file mode 100644
index 0000000000..11f9e72b08
--- /dev/null
+++ b/phpgwapi/templates/default/images/login_facebook.svg
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="facebook" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="28.35px" height="28.35px" viewBox="0 0 28.35 28.35" enable-background="new 0 0 28.35 28.35" xml:space="preserve">
+<g>
+	<defs>
+		<rect id="SVGID_1_" x="0.003" y="0.003" width="28.344" height="28.344"/>
+	</defs>
+	<clipPath id="SVGID_2_">
+		<use xlink:href="#SVGID_1_"  overflow="visible"/>
+	</clipPath>
+	<path clip-path="url(#SVGID_2_)" fill="#35528F" d="M26.782,28.347c0.863,0,1.564-0.7,1.564-1.564V1.567
+		c0-0.863-0.701-1.563-1.564-1.563H1.567c-0.864,0-1.564,0.7-1.564,1.563v25.216c0,0.864,0.7,1.564,1.564,1.564H26.782z"/>
+	<path clip-path="url(#SVGID_2_)" fill="#FFFFFF" d="M19.56,28.347V17.371h3.684l0.553-4.278H19.56v-2.731
+		c0-1.238,0.344-2.083,2.119-2.083l2.266-0.001V4.452c-0.393-0.053-1.736-0.169-3.301-0.169c-3.266,0-5.502,1.993-5.502,5.654v3.155
+		h-3.693v4.278h3.693v10.976H19.56z"/>
+</g>
+</svg>
diff --git a/phpgwapi/templates/default/images/login_twitter.svg b/phpgwapi/templates/default/images/login_twitter.svg
new file mode 100644
index 0000000000..4fcc581878
--- /dev/null
+++ b/phpgwapi/templates/default/images/login_twitter.svg
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 16.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="twitter" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="28.35px" height="28.35px" viewBox="0 0 28.35 28.35" enable-background="new 0 0 28.35 28.35" xml:space="preserve">
+<g>
+	<defs>
+		<rect id="SVGID_1_" x="0.002" y="0.003" width="28.345" height="28.344"/>
+	</defs>
+	<clipPath id="SVGID_2_">
+		<use xlink:href="#SVGID_1_"  overflow="visible"/>
+	</clipPath>
+	<path clip-path="url(#SVGID_2_)" fill="#6BACD9" d="M26.782,28.347c0.863,0,1.564-0.701,1.564-1.564V1.566
+		c0-0.863-0.701-1.563-1.564-1.563H1.566c-0.864,0-1.564,0.7-1.564,1.563v25.216c0,0.863,0.7,1.564,1.564,1.564H26.782z"/>
+	<path clip-path="url(#SVGID_2_)" fill="#FFFFFF" d="M26.041,7.459c-0.874,0.388-1.813,0.65-2.8,0.768
+		c1.006-0.604,1.779-1.559,2.143-2.697c-0.941,0.559-1.984,0.965-3.096,1.183C21.4,5.764,20.132,5.173,18.73,5.173
+		c-2.693,0-4.875,2.184-4.875,4.875c0,0.383,0.043,0.755,0.125,1.111C9.928,10.956,6.335,9.014,3.93,6.065
+		c-0.42,0.72-0.66,1.558-0.66,2.45c0,1.692,0.861,3.185,2.169,4.06c-0.799-0.026-1.551-0.245-2.208-0.61
+		C3.23,11.985,3.23,12.006,3.23,12.026c0,2.362,1.681,4.333,3.911,4.78c-0.409,0.111-0.84,0.172-1.284,0.172
+		c-0.314,0-0.62-0.031-0.918-0.088c0.621,1.938,2.422,3.348,4.555,3.387c-1.669,1.307-3.771,2.088-6.055,2.088
+		c-0.394,0-0.782-0.023-1.163-0.068c2.157,1.383,4.72,2.189,7.474,2.189c8.968,0,13.872-7.43,13.872-13.872
+		c0-0.212-0.006-0.422-0.014-0.631C24.562,9.296,25.387,8.436,26.041,7.459"/>
+</g>
+</svg>