From 85695f0d41e1dd4d2626306facd529a5b8a134a7 Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Sun, 13 Mar 2016 11:22:44 +0000 Subject: [PATCH] split html class to Api\Html, Api\Header\Content, Api\Header\UserAgent and translation --- api/src/Db/Backup.php | 4 +- api/src/Header/Content.php | 167 ++ api/src/Header/UserAgent.php | 119 ++ api/src/Html.php | 1310 +++++++++++++++ api/src/Image.php | 9 +- api/src/Link.php | 4 +- api/src/Session.php | 3 +- api/src/Storage/Tracking.php | 11 +- mail/inc/class.mail_compose.inc.php | 13 +- phpgwapi/inc/class.egw_htmLawed.inc.php | 105 +- phpgwapi/inc/class.html.inc.php | 1750 ++------------------- phpgwapi/inc/class.translation.inc.php | 43 +- phpgwapi/js/colorpicker/ed_color_bg.gif | Bin 181 -> 0 bytes phpgwapi/js/colorpicker/license.txt | 30 - phpgwapi/js/colorpicker/select_color.html | 370 ----- 15 files changed, 1866 insertions(+), 2072 deletions(-) create mode 100644 api/src/Header/Content.php create mode 100644 api/src/Header/UserAgent.php create mode 100644 api/src/Html.php delete mode 100644 phpgwapi/js/colorpicker/ed_color_bg.gif delete mode 100644 phpgwapi/js/colorpicker/license.txt delete mode 100644 phpgwapi/js/colorpicker/select_color.html diff --git a/api/src/Db/Backup.php b/api/src/Db/Backup.php index f595f17dc7..e8a1e07216 100644 --- a/api/src/Db/Backup.php +++ b/api/src/Db/Backup.php @@ -16,8 +16,6 @@ namespace EGroupware\Api\Db; use EGroupware\Api; use ZipArchive; -use html; - /** * DB independent backup and restore of EGroupware database * @@ -1100,7 +1098,7 @@ class Backup else { $def = " complete rewrite in 6/2006 and earlier modifications + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @author RalfBecker-AT-outdoor-training.de + * @copyright 2001-2016 by RalfBecker@outdoor-training.de + * @package api + * @version $Id$ + */ + +namespace EGroupware\Api\Header; + +use EGroupware\Api; + +/** + * Safe content type and disposition headers + */ +class Content +{ + /** + * Output safe content headers for user-content, mitigating risk of javascript or html + * + * Mitigate risk of serving javascript or css from our domain, + * which will get around same origin policy and CSP! + * + * Mitigate risk of html downloads by using CSP or force download for IE + * + * @param resource|string &$content content might be changed by this call + * @param string $path filename or path for content-disposition header + * @param string &$mime ='' mimetype or '' (default) to detect it from filename, using mime_magic::filename2mime() + * on return used, maybe changed, mime-type + * @param int $length =0 content length, default 0 = skip that header + * on return changed size + * @param boolean $nocache =true send headers to disallow browser/proxies to cache the download + * @param boolean $force_download =true send content-disposition attachment header + * @param boolean $no_content_type =false do not send actual content-type and content-length header, just content-disposition + */ + public static function safe(&$content, $path, &$mime='', &$length=0, $nocache=true, $force_download=true, $no_content_type=false) + { + // change old/aliased mime-types to new one, eg. image/pdf to application/pdf + $mime = Api\Api\MimeMagic::fix_mime_type($mime); + + // mitigate risk of serving javascript or css via webdav from our domain, + // which will get around same origin policy and CSP + list($type, $subtype) = explode('/', strtolower($mime)); + if (!$force_download && in_array($type, array('application', 'text')) && + in_array($subtype, array('javascript', 'x-javascript', 'ecmascript', 'jscript', 'vbscript', 'css'))) + { + // unfortunatly only Chrome and IE >= 8 allow to switch content-sniffing off with X-Content-Type-Options: nosniff + if (UserAgent::type() == 'chrome' || UserAgent::type() == 'msie' && UserAgent::version() >= 8) + { + $mime = 'text/plain'; + header('X-Content-Type-Options: nosniff'); // stop IE & Chrome from content-type sniffing + } + // for the rest we change mime-type to text/html and let code below handle it safely + // this stops Safari and Firefox from using it as src attribute in a script tag + // but only for "real" browsers, we dont want to modify data for our WebDAV clients + elseif (isset($_SERVER['HTTP_REFERER'])) + { + $mime = 'text/html'; + if (is_resource($content)) + { + $data = fread($content, $length); + fclose($content); + $content =& $data; + unset($data); + } + $content = '
'.$content;
+				$length += 5;
+			}
+		}
+		// mitigate risk of html downloads by using CSP or force download for IE
+		if (!$force_download && in_array($mime, array('text/html', 'application/xhtml+xml')))
+		{
+			// use CSP only for current user-agents/versions I was able to positivly test
+			if (UserAgent::type() == 'chrome' && UserAgent::version() >= 24 ||
+				// mobile FF 24 on Android does NOT honor CSP!
+				UserAgent::type() == 'firefox' && !UserAgent::mobile() && UserAgent::version() >= 24 ||
+				UserAgent::type() == 'safari' && !UserAgent::mobile() && UserAgent::version() >= 536 ||	// OS X
+				UserAgent::type() == 'safari' && UserAgent::mobile() && UserAgent::version() >= 9537)	// iOS 7
+			{
+				$csp = "script-src 'none'";	// forbid to execute any javascript
+				header("Content-Security-Policy: $csp");
+				header("X-Webkit-CSP: $csp");	// Chrome: <= 24, Safari incl. iOS
+				//header("X-Content-Security-Policy: $csp");	// FF <= 22
+				//error_log(__METHOD__."('$options[path]') ".UserAgent::type().'/'.UserAgent::version().(UserAgent::mobile()?'/mobile':'').": using Content-Security-Policy: $csp");
+			}
+			else	// everything else get's a Content-dispostion: attachment, to be on save side
+			{
+				//error_log(__METHOD__."('$options[path]') ".UserAgent::type().'/'.UserAgent::version().(UserAgent::mobile()?'/mobile':'').": using Content-disposition: attachment");
+				$force_download = true;
+			}
+		}
+		if ($no_content_type)
+		{
+			if ($force_download) self::disposition(Api\Vfs::basename($path), $force_download);
+		}
+		else
+		{
+			self::type(Api\Vfs::basename($path), $mime, $length, $nocache, $force_download);
+		}
+	}
+
+	/**
+	 * Output content-type headers for file downloads
+	 *
+	 * This function should only be used for non-user supplied content!
+	 * For uploaded files, mail attachmentes, etc, you have to use safe_content_header!
+	 *
+	 * @author Miles Lott originally in browser class
+	 * @param string $fn filename
+	 * @param string $mime ='' mimetype or '' (default) to detect it from filename, using mime_magic::filename2mime()
+	 * @param int $length =0 content length, default 0 = skip that header
+	 * @param boolean $nocache =true send headers to disallow browser/proxies to cache the download
+	 * @param boolean $forceDownload =true send headers to handle as attachment/download
+	 */
+	public static function type($fn,$mime='',$length=0,$nocache=True,$forceDownload=true)
+	{
+		// if no mime-type is given or it's the default binary-type, guess it from the extension
+		if(empty($mime) || $mime == 'application/octet-stream')
+		{
+			$mime = Api\MimeMagic::filename2mime($fn);
+		}
+		if($fn)
+		{
+			// Show this for all
+			self::disposition($fn,$forceDownload);
+			header('Content-type: '.$mime);
+
+			if($length)
+			{
+				header('Content-length: '.$length);
+			}
+
+			if($nocache)
+			{
+				header('Pragma: no-cache');
+				header('Pragma: public');
+				header('Expires: 0');
+				header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
+			}
+		}
+	}
+
+	/**
+	 * Output content-disposition header for file downloads
+	 *
+	 * @param string $fn filename
+	 * @param boolean $forceDownload =true send headers to handle as attachment/download
+	 */
+	public static function disposition($fn, $forceDownload=true)
+	{
+		if ($forceDownload)
+		{
+			$attachment = ' attachment;';
+		}
+		else
+		{
+			$attachment = ' inline;';
+		}
+
+		header('Content-disposition:'.$attachment.' filename="'.Api\Translation::to_ascii($fn).'"; filename*=utf-8\'\''.rawurlencode($fn));
+	}
+}
diff --git a/api/src/Header/UserAgent.php b/api/src/Header/UserAgent.php
new file mode 100644
index 0000000000..7cdf273b4d
--- /dev/null
+++ b/api/src/Header/UserAgent.php
@@ -0,0 +1,119 @@
+ complete rewrite in 6/2006 and earlier modifications
+ * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
+ * @author RalfBecker-AT-outdoor-training.de
+ * @copyright 2001-2016 by RalfBecker@outdoor-training.de
+ * @package api
+ * @subpackage header
+ * @version $Id$
+ */
+
+namespace EGroupware\Api\Header;
+
+/**
+ * Server-side browser and mobile device detection based on User-Agent header
+ */
+class UserAgent
+{
+	/**
+	 * Normalized type of user-agent
+	 *
+	 * @return string 'firefox', 'msie', 'safari' (incl. iPhone), 'chrome', 'opera', 'konqueror', 'mozilla'
+	 */
+	public function type()
+	{
+		return self::$user_agent;
+	}
+
+	/**
+	 * Version of user-agent as specified by browser
+	 *
+	 * @return string
+	 */
+	public function version()
+	{
+		return self::$ua_version;
+	}
+
+	/**
+	 * Mobile device type
+	 *
+	 * @return string "iphone", "ipod", "ipad", "android", "symbianos", "blackberry", "kindle", "opera mobi", "windows phone"
+	 */
+	public function mobile()
+	{
+		return self::$ua_mobile;
+	}
+
+	/**
+	 * user-agent: 'firefox', 'msie', 'safari' (incl. iPhone), 'chrome', 'opera', 'konqueror', 'mozilla'
+	 *
+	 * @var string
+	 */
+	protected static $user_agent;
+	/**
+	 * User agent is mobile browser: "iphone", "ipod", "ipad", "android", "symbianos", "blackberry", "kindle", "opera mobi", "windows phone"
+	 *
+	 * @var string with name of mobile browser or null, if not mobile browser
+	 */
+	protected static $ua_mobile;
+
+	/**
+	 * version of user-agent as specified by browser
+	 *
+	 * @var string
+	 */
+	protected static $ua_version;
+
+	/**
+	 * initialise our static vars
+	 */
+	static function _init_static()
+	{
+		// should be Ok for all HTML 4 compatible browsers
+		$parts = $all_parts = null;
+		if(!preg_match('/compatible; ([a-z]+)[\/ ]+([0-9.]+)/i',$_SERVER['HTTP_USER_AGENT'],$parts))
+		{
+			preg_match_all('/([a-z]+)\/([0-9.]+)/i',$_SERVER['HTTP_USER_AGENT'],$all_parts,PREG_SET_ORDER);
+			$parts = array_pop($all_parts);
+			foreach($all_parts as $p)
+			{
+				if ($p[1] == 'Chrome' && $parts[1] != 'Edge')
+				{
+					$parts = $p;
+					break;
+				}
+			}
+		}
+		list(,self::$user_agent,self::$ua_version) = $parts;
+		if ((self::$user_agent = strtolower(self::$user_agent)) == 'version') self::$user_agent = 'opera';
+		// IE no longer reports MSIE, but "Trident/7.0; rv:11.0"
+		if (self::$user_agent=='trident')
+		{
+			self::$user_agent='msie';
+			$matches = null;
+			self::$ua_version = preg_match('|Trident/[0-9.]+; rv:([0-9.]+)|i', $_SERVER['HTTP_USER_AGENT'], $matches) ?
+				$matches[1] : 11.0;
+		}
+		// iceweasel is based on mozilla and we treat it like as firefox
+		if (self::$user_agent == 'iceweasel')
+		{
+			self::$user_agent = 'firefox';
+		}
+		// MS Edge sometimes reports just "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
+		if (self::$user_agent == 'mozilla' && self::$ua_version == '5.0')
+		{
+			self::$user_agent = 'edge';
+			self::$ua_version = '12.0';
+		}
+		self::$ua_mobile = preg_match('/(iPhone|iPod|iPad|Android|SymbianOS|Blackberry|Kindle|Opera Mobi|Windows Phone)/i',
+			$_SERVER['HTTP_USER_AGENT'], $matches) ? strtolower($matches[1]) : null;
+
+		//error_log("HTTP_USER_AGENT='$_SERVER[HTTP_USER_AGENT]', UserAgent: '".self::$user_agent."', Version: '".self::$ua_version."', isMobile=".array2string(self::$ua_mobile));
+	}
+}
+UserAgent::_init_static();
diff --git a/api/src/Html.php b/api/src/Html.php
new file mode 100644
index 0000000000..1e3db255c8
--- /dev/null
+++ b/api/src/Html.php
@@ -0,0 +1,1310 @@
+ complete rewrite in 6/2006 and earlier modifications
+ * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
+ * @author RalfBecker-AT-outdoor-training.de
+ * @copyright 2001-2016 by RalfBecker@outdoor-training.de
+ * @package api
+ * @subpackage html
+ * @version $Id$
+ */
+
+namespace EGroupware\Api;
+
+// explicitly import classes not yet imported into Api
+use egw_framework;	// validate_file, includeCSS
+use egw_ckeditor_config;
+use egw;	// link
+use egw_htmLawed;
+
+/**
+ * Generates html with methods representing html-tags or higher widgets
+ *
+ * The class has only static methods now, so there's no need to instanciate as object anymore!
+ */
+class Html
+{
+	/**
+	 * Automatically turn on enhanced selectboxes if there's more than this many options
+	 */
+	const SELECT_ENHANCED_ROW_COUNT = 12;
+
+	/**
+	 * activates URLs in a text, URLs get replaced by html-links
+	 *
+	 * @param string $content text containing URLs
+	 * @return string html with activated links
+	 */
+	static function activate_links($content)
+	{
+		if (!$content || strlen($content) < 20) return $content;	// performance
+
+		// Exclude everything which is already a link
+		$NotAnchor = '(?$1 AT $2 DOT $3",
+			$content);
+
+		//  First match things beginning with http:// (or other protocols)
+		$optBracket0 = '(<|<)';
+		$Protocol = '(http:\/\/|(ftp:\/\/|https:\/\/))';	// only http:// gets removed, other protocolls are shown
+		$Domain = '([\w-]+\.[\w-.]+)';
+		$Subdir = '([\w\-\.,@?^=%&;:\/~\+#]*[\w\-\@?^=%&\/~\+#])?';
+		$optBracket = '(>|>)';
+		$Expr = '/' .$optBracket0. $NotAnchor . $Protocol . $Domain . $Subdir . $optBracket . '/i';
+		// use preg_replace_callback as we experienced problems with https links
+		$result2 = preg_replace_callback($Expr, function ($match)
+		{
+			return $match[1]."".$match[4].$match[5]."".$match[6];
+		}, $result);
+
+		if (true)	// hack to keep IDE from complaing about double assignments
+		{
+			//  First match things beginning with http:// (or other protocols)
+			$Protocol = '(http:\/\/|(ftp:\/\/|https:\/\/))';	// only http:// gets removed, other protocolls are shown
+			$Domain = '([\w-]+\.[\w-.]+)';
+			$Subdir = '([\w\-\.,@?^=%&;:\/~\+#]*[\w\-\@?^=%&\/~\+#])?';
+			$Expr = '/' . $NotAnchor . $Protocol . $Domain . $Subdir . '/i';
+			// use preg_replace_callback as we experienced problems with https links
+			$result3 = preg_replace_callback($Expr, function ($match)
+			{
+				return "".$match[3].$match[4]."";
+			}, $result2);
+
+			//  Now match things beginning with www.
+			$optBracket0 = '(<|<)?';
+			$NotHTTP = '(?)';	//	avoid running again on http://www links already handled above
+			$Domain2 = 'www(\.[\w-.]+)';
+			$Subdir2 = '([\w\-\.,@?^=%&:\/~\+#]*[\w\-\@?^=%&\/~\+#])?';
+			$optBracket = '(>|>)?';
+			$Expr = '/' .$optBracket0. $NotAnchor . $NotHTTP . $Domain2 . $Subdir2 .$optBracket. '/i';
+			//$Expr = '/' . $NotAnchor . $NotHTTP . $Domain . $Subdir . $optBracket . '/i';
+		}
+		$result4 = preg_replace( $Expr, "$1www$2$3$4$5$6", $result3 );
+
+		//return preg_replace( $Expr, "www$1$2$3 ", $result );
+		return $result4;
+	}
+
+	/**
+	 * escapes chars with special meaning in html as entities
+	 *
+	 * Allows to use and char in the html-output and prevents XSS attacks.
+	 * Some entities are allowed and get NOT escaped: -> prevented by 4th param = doubleencode=false
+	 * - &# some translations (AFAIK: the arabic ones) need this;
+	 * -   < > for convenience -> should not happen anymore, as we do not doubleencode anymore (20101020)
+	 *
+	 * @param string $str string to escape
+	 * @param boolean $double_encoding =false do we want double encoding or not, default no
+	 * @return string
+	 */
+	static function htmlspecialchars($str, $double_encoding=false)
+	{
+		return htmlspecialchars($str,ENT_COMPAT,Translation::charset(),$double_encoding);
+	}
+
+	/**
+	 * allows to show and select one item from an array
+	 *
+	 * @param string $name	string with name of the submitted var which holds the key of the selected item form array
+	 * @param string|array $key key(s) of already selected item(s) from $arr, eg. '1' or '1,2' or array with keys
+	 * @param array $arr array with items to select, eg. $arr = array ( 'y' => 'yes','n' => 'no','m' => 'maybe');
+	 * @param boolean $no_lang NOT run the labels of the options through lang(), default false=use lang()
+	 * @param string $options additional options (e.g. 'width')
+	 * @param int $multiple number of lines for a multiselect, default 0 = no multiselect, < 0 sets size without multiple
+	 * @param boolean $enhanced Use enhanced selectbox with search.  Null for default yes if more than 12 options.
+	 * @return string to set for a template or to echo into html page
+	 */
+	static function select($name, $key, $arr=0,$no_lang=false,$options='',$multiple=0,$enhanced=null)
+	{
+		if(is_null($enhanced)) $enhanced = false;	//disabled by default (count($arr) > self::SELECT_ENHANCED_ROW_COUNT);
+
+		if (!is_array($arr))
+		{
+			$arr = array('no','yes');
+		}
+		if ((int)$multiple > 0)
+		{
+			$options .= ' multiple="1" size="'.(int)$multiple.'"';
+			if (substr($name,-2) != '[]')
+			{
+				$name .= '[]';
+			}
+		}
+		elseif($multiple < 0)
+		{
+			$options .= ' size="'.abs($multiple).'"';
+		}
+		// fix width for MSIE < 9 in/for selectboxes
+		if (Header\UserAgent::type() == 'msie' && Header\UserAgent::version() < 9)
+		{
+			if (stripos($options,'onfocus="') === false)
+			{
+				$options .= ' onfocus="window.dropdown_menu_hack(this);" ';
+			}
+			else
+			{
+				$options = str_ireplace('onfocus="','onfocus="window.dropdown_menu_hack(this);',$options);
+			}
+		}
+		$out = "\n";
+
+		if($enhanced) {
+			egw_framework::validate_file('/phpgwapi/js/jquery/chosen/chosen.jquery.js');
+			egw_framework::includeCSS('/phpgwapi/js/jquery/chosen/chosen.css',null,false);
+			$out .= "\n";
+		}
+		return $out;
+	}
+
+	/**
+	 * emulating a multiselectbox using checkboxes
+	 *
+	 * Unfortunaly this is not in all aspects like a multi-selectbox, eg. you cant select options via javascript
+	 * in the same way. Therefor I made it an extra function.
+	 *
+	 * @param string $name	string with name of the submitted var which holds the key of the selected item form array
+	 * @param string|array $key key(s) of already selected item(s) from $arr, eg. '1' or '1,2' or array with keys
+	 * @param array $arr array with items to select, eg. $arr = array ( 'y' => 'yes','n' => 'no','m' => 'maybe');
+	 * @param boolean $no_lang NOT run the labels of the options through lang(), default false=use lang()
+	 * @param string $options additional options (e.g. 'width')
+	 * @param int $multiple number of lines for a multiselect, default 3
+	 * @param boolean $selected_first show the selected items before the not selected ones, default true
+	 * @param string $style ='' extra style settings like "width: 100%", default '' none
+	 * @return string to set for a template or to echo into html page
+	 */
+	static function checkbox_multiselect($name, $key, $arr=0,$no_lang=false,$options='',$multiple=3,$selected_first=true,$style='',$enhanced = null)
+	{
+		//echo "

checkbox_multiselect('$name',".array2string($key).",".array2string($arr).",$no_lang,'$options',$multiple,$selected_first,'$style')

\n"; + if(is_null($enhanced)) $enhanced = (count($arr) > self::SELECT_ENHANCED_ROW_COUNT); + + if (!is_array($arr)) + { + $arr = array('no','yes'); + } + if ((int)$multiple <= 0) $multiple = 1; + + if (substr($name,-2) != '[]') + { + $name .= '[]'; + } + $base_name = substr($name,0,-2); + + if($enhanced) return self::select($name, $key, $arr,$no_lang,$options." style=\"$style\" ",$multiple,$enhanced); + + if (!is_array($key)) + { + // explode on ',' only if multiple values expected and the key contains just numbers and commas + $key = preg_match('/^[,0-9]+$/',$key) ? explode(',',$key) : array($key); + } + $html = ''; + $options_no_id = preg_replace('/id="[^"]+"/i','',$options); + + if ($selected_first) + { + $selected = $not_selected = array(); + foreach($arr as $val => $label) + { + if (in_array((string)$val,$key)) + { + $selected[$val] = $label; + } + else + { + $not_selected[$val] = $label; + } + } + $arr = $selected + $not_selected; + } + $max_len = 0; + foreach($arr as $val => $label) + { + if (is_array($label)) + { + $title = $label['title']; + $label = $label['label']; + } + else + { + $title = ''; + } + if ($label && !$no_lang) $label = lang($label); + if ($title && !$no_lang) $title = lang($title); + + if (strlen($label) > $max_len) $max_len = strlen($label); + + $html .= self::label(self::checkbox($name,in_array((string)$val,$key),$val,$options_no_id. + ' id="'.$base_name.'['.$val.']'.'"').self::htmlspecialchars($label), + $base_name.'['.$val.']','',($title ? 'title="'.self::htmlspecialchars($title).'" ':''))."
\n"; + } + if ($style && substr($style,-1) != ';') $style .= '; '; + if (strpos($style,'height')===false) $style .= 'height: '.(1.7*$multiple).'em; '; + if (strpos($style,'width')===false) $style .= 'width: '.(4+$max_len*($max_len < 15 ? 0.65 : 0.6)).'em; '; + $style .= 'background-color: white; overflow: auto; border: lightgray 2px inset; text-align: left;'; + + return self::div($html,$options,'',$style); + } + + /** + * generates an option-tag for a selectbox + * + * @param string $value value + * @param string $label label + * @param mixed $selected value or array of values of options to mark as selected + * @param boolean $no_lang NOT running the label through lang(), default false=use lang() + * @param string $extra extra text, e.g.: style="", default: '' + * @return string html + */ + static function select_option($value,$label,$selected,$no_lang=0,$title='',$extra='') + { + // the following compares strict as strings, to archive: '0' == 0 != '' + // the first non-strict search via array_search, is for performance reasons, to not always search the whole array with php + if (($found = ($key = array_search($value,$selected)) !== false) && (string) $value !== (string) $selected[$key]) + { + $found = false; + foreach($selected as $sel) + { + if (($found = (((string) $value) === ((string) $selected[$key])))) break; + } + unset($sel); + } + return '\n"; + } + + /** + * generates a div-tag + * + * @param string $content of a div, or '' to generate only the opening tag + * @param string $options to include in the tag, default ''=none + * @param string $class css-class attribute, default ''=none + * @param string $style css-styles attribute, default ''=none + * @return string html + */ + static function div($content,$options='',$class='',$style='') + { + if ($class) $options .= ' class="'.$class.'"'; + if ($style) $options .= ' style="'.$style.'"'; + + return "
\n".($content ? "$content
\n" : ''); + } + + /** + * generate one or more hidden input tag(s) + * + * @param array|string $vars var-name or array with name / value pairs + * @param string $value value if $vars is no array, default '' + * @param boolean $ignore_empty if true all empty, zero (!) or unset values, plus filer=none + * @param string html + */ + static function input_hidden($vars,$value='',$ignore_empty=True) + { + if (!is_array($vars)) + { + $vars = array( $vars => $value ); + } + foreach($vars as $name => $value) + { + if (is_array($value)) + { + $value = json_encode($value); + } + if (!$ignore_empty || $value && !($name == 'filter' && $value == 'none')) // dont need to send all the empty vars + { + $html .= "\n"; + } + } + return $html; + } + + /** + * generate a textarea tag + * + * @param string $name name attr. of the tag + * @param string $value default + * @param boolean $ignore_empty if true all empty, zero (!) or unset values, plus filer=none + * @param boolean $double_encoding =false do we want double encoding or not, default no + * @param string html + */ + static function textarea($name,$value='',$options='',$double_encoding=false) + { + return "\n"; + } + + /** + * Checks if HTMLarea (or an other richtext editor) is availible for the used browser + * + * @return boolean + */ + static function htmlarea_availible() + { + // this one is for testing how it will turn out, if you do not have the device or agent ready at your fingertips + // if (stripos($_SERVER[HTTP_USER_AGENT],'mozilla') !== false) return false; + + // CKeditor will doublecheck availability for us, but its fallback does not look nice, and you will get + // no conversion of html content to plain text, so we provide a check for known USER_AGENTS to fail the test + return true; + } + + /** + * compability static function for former used htmlarea. Please use static function fckeditor now! + * + * creates a textarea inputfield for the htmlarea js-widget (returns the necessary html and js) + */ + static function htmlarea($name,$content='',$style='',$base_href=''/*,$plugins='',$custom_toolbar='',$set_width_height_in_config=false*/) + { + /*if (!self::htmlarea_availible()) + { + return self::textarea($name,$content,'style="'.$style.'"'); + }*/ + return self::fckEditor($name, $content, $style, array('toolbar_expanded' =>'true'), '400px', '100%', $base_href); + } + + /** + * this static function is a wrapper for fckEditor to create some reuseable layouts + * + * @param string $_name name and id of the input-field + * @param string $_content of the tinymce (will be run through htmlspecialchars !!!), default '' + * @param string $_mode display mode of the tinymce editor can be: simple, extended or advanced + * @param array $_options (toolbar_expanded true/false) + * @param string $_height ='400px' + * @param string $_width ='100%' + * @param string $_start_path ='' if passed activates the browser for image at absolute path passed + * @param boolean $_purify =true run $_content through htmlpurifier before handing it to fckEditor + * @param mixed (boolean/string) $_focusToBody=false USED only for CKEDIOR true means yes, focus on top, you may specify TOP or BOTTOM (to focus on the end of the editor area) + * @param string $_executeJSAfterInit ='' Javascript to be executed after InstanceReady of CKEditor + * @return string the necessary html for the textarea + */ + static function fckEditor($_name, $_content, $_mode, $_options=array('toolbar_expanded' =>'true'), + $_height='400px', $_width='100%',$_start_path='',$_purify=true, $_focusToBody=false, $_executeJSAfterInit='') + { + if (!self::htmlarea_availible() || $_mode == 'ascii') + { + return self::textarea($_name,$_content,'style="width: '.$_width.'; height: '.$_height.';" id="'.htmlspecialchars($_name).'"'); + } + + //include the ckeditor js file + egw_framework::validate_file('ckeditor','ckeditor','phpgwapi'); + + // run content through htmlpurifier + if ($_purify && !empty($_content)) + $_content = self::purify($_content); + + // By default the editor start expanded + $expanded = isset($_options['toolbar_expanded']) ? + $_options['toolbar_expanded'] == 'true' : true; + + //Get the height in pixels from the pixels parameter + $pxheight = (strpos('px', $_height) === false) ? + (empty($_height) ? 400 : $_height) : str_replace('px', '', $_height); + + // User preferences + $font = $GLOBALS['egw_info']['user']['preferences']['common']['rte_font']; + $font_size = egw_ckeditor_config::font_size_from_prefs(); + $font_span = ''; + if (empty($font) && empty($font_size)) $font_span = ''; + + // we need to enable double encoding here, as ckEditor has to undo one level of encoding + // otherwise < and > chars eg. from html markup entered in regular (not source) input, will turn into html! + //error_log(__METHOD__.__LINE__.' '.Header\UserAgent::type().','.Header\UserAgent::version()); + return self::textarea($_name,$_content,'id="'.htmlspecialchars($_name).'"',true). // true = double encoding +' + +'; + } + + /** + * this static function is a wrapper for tinymce to create some reuseable layouts + * + * Please note: if you did not run init_tinymce already you this static function need to be called before the call to phpgw_header() !!! + * + * @param string $_name name and id of the input-field + * @param string $_mode display mode of the tinymce editor can be: simple, extended or advanced + * @param string $_content ='' of the tinymce (will be run through htmlspecialchars !!!), default '' + * @param string $_height ='400px' + * @param string $_width ='100%' + * @param boolean $_purify =true + * @param string $_border ='0px' NOT used for CKEditor + * @param mixed (boolean/string) $_focusToBody=false USED only for CKEDIOR true means yes, focus on top, you may specify TOP or BOTTOM (to focus on the end of the editor area) + * @param string $_executeJSAfterInit ='' Javascript to be executed after InstanceReady of CKEditor + * @return string the necessary html for the textarea + */ + static function fckEditorQuick($_name, $_mode, $_content='', $_height='400px', $_width='100%',$_purify=true, $_border='0px',$_focusToBody=false,$_executeJSAfterInit='') + { + if (!self::htmlarea_availible() || $_mode == 'ascii') + { + //TODO: use self::textarea + return ""; + } + else + { + return self::fckEditor($_name, $_content, $_mode, array(), $_height, $_width,'',$_purify,$_focusToBody,$_executeJSAfterInit); + } + } + + /** + * represents html's input tag + * + * @param string $name name + * @param string $value default value of the field + * @param string $type type, default ''=not specified = text + * @param string $options attributes for the tag, default ''=none + */ + static function input($name,$value='',$type='',$options='' ) + { + switch ((string)$type) + { + case ''; + break; + default: + $type = 'type="'.htmlspecialchars($type).'"'; + } + return "\n"; + } + + static protected $default_background_images = array( + 'save' => '/save(&|\]|$)/', + 'apply' => '/apply(&|\]|$)/', + 'cancel' => '/cancel(&|\]|$)/', + 'delete' => '/delete(&|\]|$)/', + 'edit' => '/edit(&|\]|$)/', + 'next' => '/(next|continue)(&|\]|$)/', + 'finish' => '/finish(&|\]|$)/', + 'back' => '/(back|previous)(&|\]|$)/', + 'copy' => '/copy(&|\]|$)/', + 'more' => '/more(&|\]|$)/', + 'check' => '/(yes|check)(&|\]|$)/', + 'cancelled' => '/no(&|\]|$)/', + 'ok' => '/ok(&|\]|$)/', + 'close' => '/close(&|\]|$)/', + 'add' => '/(add(&|\]|$)|create)/', // customfields use create* + ); + + static protected $default_classes = array( + 'et2_button_cancel' => '/cancel(&|\]|$)/', // yellow + 'et2_button_question' => '/(yes|no)(&|\]|$)/', // yellow + 'et2_button_delete' => '/delete(&|\]|$)/' // red + ); + + /** + * represents html's button (input type submit or input type button or image) + * + * @param string $name name + * @param string $label label of the button + * @param string $onClick javascript to call, when button is clicked + * @param boolean $no_lang NOT running the label through lang(), default false=use lang() + * @param string $options attributes for the tag, default ''=none + * @param string $image to show instead of the label, default ''=none + * @param string $app app to search the image in + * @param string $buttontype which type of html button (button|submit), default ='submit' + * @return string html + */ + static function submit_button($name,$label,$onClick='',$no_lang=false,$options='',$image='',$app='phpgwapi', $buttontype='submit') + { + // workaround for idots and IE button problem (wrong cursor-image) + if (Header\UserAgent::type() == 'msie') + { + $options .= ' style="cursor: pointer;"'; + } + // add et2_classes to "old" buttons + $classes = array('et2_button'); + + if ($image != '') + { + $image = str_replace(array('.gif','.GIF','.png','.PNG'),'',$image); + + if (!($path = Image::find($app, $image))) + { + $path = $image; // name may already contain absolut path + } + $image = ' src="'.$path.'"'; + $classes[] = 'image_button'; + } + if (!$no_lang) + { + $label = lang($label); + } + if (($accesskey = @strstr($label,'&')) && $accesskey[1] != ' ' && + (($pos = strpos($accesskey,';')) === false || $pos > 5)) + { + $label_u = str_replace('&'.$accesskey[1],''.$accesskey[1].'',$label); + $label = str_replace('&','',$label); + $options .= ' accesskey="'.$accesskey[1].'" '.$options; + } + else + { + $accesskey = ''; + $label_u = $label; + } + if ($onClick) $options .= ' onclick="'.str_replace('"','\\"',$onClick).'"'; + + // add default background-image to get et2 like buttons + foreach(self::$default_background_images as $img => $reg_exp) + { + if (preg_match($reg_exp, $name) && ($url = Image::find($GLOBALS['egw_info']['flags']['currentapp'], $img))) + { + $options .= ' style="background-image: url('.$url.');"'; + $classes[] = 'et2_button_with_image'; + break; + } + } + // add default class for cancel, delete or yes/no buttons + foreach(self::$default_classes as $class => $reg_exp) + { + if (preg_match($reg_exp, $name)) + { + $classes[] = $class; + break; + } + } + if (strpos($options, 'class="') !== false) + { + $options = str_replace('class="', 'class="'.implode(' ', $classes).' ', $options); + } + else + { + $options .= ' class="'.implode(' ', $classes).'"'; + } + + return ''; + } + + /** + * creates an absolut link + the query / get-variables + * + * Example link('/index.php?menuaction=infolog.uiinfolog.get_list',array('info_id' => 123)) + * gives 'http://domain/phpgw-path/index.php?menuaction=infolog.uiinfolog.get_list&info_id=123' + * + * @param string $_url egw-relative link, may include query / get-vars + * @param array|string $vars query or array ('name' => 'value', ...) with query + * @return string absolut link already run through $phpgw->link + */ + static function link($_url,$vars='') + { + //echo "

html::link(url='$url',vars='"; print_r($vars); echo "')

\n"; + if (!is_array($vars)) + { + parse_str($vars,$vars); + } + list($url,$v) = explode('?', $_url); // url may contain additional vars + if ($v) + { + parse_str($v,$v); + $vars += $v; + } + return egw::link($url,$vars); + } + + /** + * represents html checkbox + * + * @param string $name name + * @param boolean $checked box checked on display + * @param string $value value the var should be set to, default 'True' + * @param string $options attributes for the tag, default ''=none + * @return string html + */ + static function checkbox($name,$checked=false,$value='True',$options='') + { + return '\n"; + } + + /** + * represents a html form + * + * @param string $content of the form, if '' only the opening tag gets returned + * @param array $hidden_vars array with name-value pairs for hidden input fields + * @param string $_url eGW relative URL, will be run through the link function, if empty the current url is used + * @param string|array $url_vars parameters for the URL, send to link static function too + * @param string $name name of the form, defaul ''=none + * @param string $options attributes for the tag, default ''=none + * @param string $method method of the form, default 'POST' + * @return string html + */ + static function form($content,$hidden_vars,$_url,$url_vars='',$name='',$options='',$method='POST') + { + $url = $_url ? self::link($_url, $url_vars) : $_SERVER['PHP_SELF'].'?'.$_SERVER['QUERY_STRING']; + $html = "
\n"; + $html .= self::input_hidden($hidden_vars); + + if ($content) + { + $html .= $content; + $html .= "
\n"; + } + return $html; + } + + /** + * represents a html form with one button + * + * @param string $name name of the button + * @param string $label label of the button + * @param array $hidden_vars array with name-value pairs for hidden input fields + * @param string $url eGW relative URL, will be run through the link function + * @param string|array $url_vars parameters for the URL, send to link static function too + * @param string $options attributes for the tag, default ''=none + * @param string $form_name name of the form, defaul ''=none + * @param string $method method of the form, default 'POST' + * @return string html + */ + static function form_1button($name,$label,$hidden_vars,$url,$url_vars='',$form_name='',$method='POST') + { + return self::form(self::submit_button($name,$label),$hidden_vars,$url,$url_vars,$form_name,' style="display: inline-block"',$method); + } + + const THEAD = 1; + const TFOOT = 2; + const TBODY = 3; + static $part2tag = array( + self::THEAD => 'thead', + self::TFOOT => 'tfoot', + self::TBODY => 'tbody', + ); + + /** + * creates table from array of rows + * + * abstracts the html stuff for the table creation + * Example: $rows = array ( + * 'h1' => array( // optional header row(s) + * ), + * 'f1' => array( // optional footer row(s) + * ), + * '1' => array( + * 1 => 'cell1', '.1' => 'colspan=3', + * 2 => 'cell2', + * 3 => 'cell3', '.3' => 'width="10%"' + * ),'.1' => 'BGCOLOR="#0000FF"' ); + * table($rows,'width="100%"') = '
cell1cell2cell3
' + * + * @param array $rows with rows, each row is an array of the cols + * @param string $options options for the table-tag + * @param boolean $no_table_tr dont return the table- and outmost tr-tabs, default false=return table+tr + * @return string with html-code of the table + */ + static function table($rows,$options = '',$no_table_tr=False) + { + $html = $no_table_tr ? '' : "\n"; + + $part = 0; + foreach($rows as $key => $row) + { + if (!is_array($row)) + { + continue; // parameter + } + // get the current part from the optional 'h' or 'f' prefix of the key + $p = $key[0] == 'h' ? html::THEAD : ($key[0] == 'f' ? html::TFOOT : html::TBODY); + if ($part < $p && ($part || $p < self::TBODY)) // add only allowed and neccessary transitions + { + if ($part) $html .= '\n"; + $html .= '<'.self::$part2tag[$part=$p].">\n"; + } + $html .= $no_table_tr && $key == 1 ? '' : "\t\n"; + + foreach($row as $key => $cell) + { + if ($key[0] == '.') + { + continue; // parameter + } + $table_pos = strpos($cell,'$cell\n"; + } + } + $html .= "\t\n"; + } + if (!is_array($rows)) + { + echo "

".function_backtrace()."

\n"; + } + if ($part) // close current part + { + $html .= "\n"; + } + $html .= "
\n"; + + if ($no_table_tr) + { + $html = substr($html,0,-16); + } + return $html; + } + + /** + * changes a selectbox to submit the form if it gets changed, to be used with the sbox-class + * + * @param string $sbox html with the select-box + * @param boolean $no_script if true generate a submit-button if javascript is off + * @return string html + */ + static function sbox_submit( $sbox,$no_script=false ) + { + $html = str_replace('').''; + } + return $html; + } + + /** + * html-widget showing progessbar with a view div's (html4 only, textual percentage otherwise) + * + * @param mixed $_percent percent-value, gets casted to int + * @param string $_title title for the progressbar, default ''=the percentage itself + * @param string $options attributes for the outmost div (may include onclick="...") + * @param string $width width, default 30px + * @param string $color color, default '#D00000' (dark red) + * @param string $height height, default 5px + * @return string html + */ + static function progressbar($_percent, $_title='',$options='',$width='',$color='',$height='' ) + { + $percent = (int)$_percent; + if (!$width) $width = '30px'; + if (!$height)$height= '5px'; + if (!$color) $color = '#D00000'; + $title = $_title ? self::htmlspecialchars($_title) : $percent.'%'; + + if (self::$netscape4) + { + return $title; + } + return '
'.$title.'
'."\n\t". + '
'."\n
\n"; + } + + /** + * representates a html img tag, output a picture + * + * If the name ends with a '%' and the rest is numeric, a progressionbar is shown instead of an image. + * The vfs:/ pseudo protocoll allows to access images in the vfs, eg. vfs:/home/ralf/me.png + * Instead of a name you specify an array with get-vars, it is passed to eGW's link function. + * This way session-information gets passed, eg. $name=array('menuaction'=>'myapp.class.image','id'=>123). + * + * @param string $app app-name to search the image + * @param string|array $name image-name or URL (incl. vfs:/) or array with get-vars + * @param string $title tooltip, default '' = none + * @param string $options further options for the tag, default '' = none + * @return string the html + */ + static function image( $app,$name,$title='',$options='' ) + { + if (is_array($name)) // menuaction and other get-vars + { + $name = $GLOBALS['egw']->link('/index.php',$name); + } + if (substr($name,0,5) == 'vfs:/') // vfs pseudo protocoll + { + $name = egw::link(Vfs::download_url(substr($name,4))); + } + if ($name[0] == '/' || substr($name,0,7) == 'http://' || substr($name,0,8) == 'https://' || stripos($name,'etemplate/thumbnail.php') ) + { + if (!($name[0] == '/' || substr($name,0,7) == 'http://' || substr($name,0,8) == 'https://')) $name = '/'.$name; + $url = $name; + } + else // no URL, so try searching the image + { + $name = str_replace(array('.gif','.GIF','.png','.PNG'),'',$name); + + if (!($url = Image::find($app,$name))) + { + $url = $name; // name may already contain absolut path + } + if($GLOBALS['egw_info']['server']['webserver_url']) + { + list(,$path) = explode($GLOBALS['egw_info']['server']['webserver_url'],$url); + + if (!is_null($path)) $path = EGW_SERVER_ROOT.$path; + } + else + { + $path = EGW_SERVER_ROOT.$url; + } + + if (is_null($path) || (!@is_readable($path) && stripos($path,'webdav.php')===false)) + { + // if the image-name is a percentage, use a progressbar + if (substr($name,-1) == '%' && is_numeric($percent = substr($name,0,-1))) + { + return self::progressbar($percent,$title); + } + return $title; + } + } + if ($title) + { + $options .= ' title="'.self::htmlspecialchars($title).'"'; + } + + // This block makes pngfix.js useless, adding a check on disable_pngfix to have pngfix.js do its thing + if (Header\UserAgent::type() == 'msie' && Header\UserAgent::version() < 7.0 && substr($url,-4) == '.png' && ($GLOBALS['egw_info']['user']['preferences']['common']['disable_pngfix'] || !isset($GLOBALS['egw_info']['user']['preferences']['common']['disable_pngfix']))) + { + $extra_styles = "display: inline-block; filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(src='$url',sizingMethod='image'); width: 1px; height: 1px;"; + if (false!==strpos($options,'style="')) + { + $options = str_replace('style="','style="'.$extra_styles, $options); + } + else + { + $options .= ' style="'.$extra_styles.'"'; + } + return ""; + } + return ""; + } + + /** + * representates a html link + * + * @param string $content of the link, if '' only the opening tag gets returned + * @param string $url eGW relative URL, will be run through the link function + * @param string|array $vars parameters for the URL, send to link static function too + * @param string $options attributes for the tag, default ''=none + * @return string the html + */ + static function a_href( $content,$url,$vars='',$options='') + { + if (is_array($url)) + { + $vars = $url; + $url = '/index.php'; + } + elseif (strpos($url,'/')===false && + count(explode('.',$url)) >= 3 && + !(strpos($url,'mailto:')!==false || + strpos($url,'://')!==false || + strpos($url,'javascript:')!==false)) + { + $url = "/index.php?menuaction=$url"; + } + if ($url[0] == '/') // link relative to eGW + { + $url = self::link($url,$vars); + } + //echo "

html::a_href('".self::htmlspecialchars($content)."','$url',".print_r($vars,True).") = ".self::link($url,$vars)."

"; + return ''.$content.''; + } + + /** + * representates a hr tag (horizontal rule) + * + * @param string $width default ''=none given + * @param string $options attributes for the tag, default ''=none + * @return string the html + */ + static function hr($width='',$options='') + { + if ($width) $options .= " width=\"$width\""; + + return "
\n"; + } + + /** + * formats option-string for most of the above functions + * + * Example: formatOptions('100%,,1','width,height,border') = ' width="100%" border="1"' + * + * @param mixed $options String (or Array) with option-values eg. '100%,,1' + * @param mixed $names String (or Array) with the option-names eg. 'WIDTH,HEIGHT,BORDER' + * @return string with options/attributes + */ + static function formatOptions($options,$names) + { + if (!is_array($options)) $options = explode(',',$options); + if (!is_array($names)) $names = explode(',',$names); + + foreach($options as $n => $val) + { + if ($val != '' && $names[$n] != '') + { + $html .= ' '.strtolower($names[$n]).'="'.$val.'"'; + } + } + return $html; + } + + /** + * html style tag (incl. type) + * + * @param string $styles css-style definitions + * @return string html + */ + static function style($styles) + { + return $styles ? "" : ''; + } + + /** + * html label tag + * + * @param string $content the label + * @param string $id for the for attribute, default ''=none + * @param string $accesskey accesskey, default ''=none + * @param string $options attributes for the tag, default ''=none + * @return string the html + */ + static function label($content,$id='',$accesskey='',$options='') + { + if ($id != '') + { + $id = " for=\"$id\""; + } + if ($accesskey != '') + { + $accesskey = " accesskey=\"$accesskey\""; + } + return "$content"; + } + + /** + * html fieldset, eg. groups a group of radiobuttons + * + * @param string $content the content + * @param string $legend legend / label of the fieldset, default ''=none + * @param string $options attributes for the tag, default ''=none + * @return string the html + */ + static function fieldset($content,$legend='',$options='') + { + $html = "
".($legend ? ''.self::htmlspecialchars($legend).'' : '')."\n"; + + if ($content) + { + $html .= $content; + $html .= "\n
\n"; + } + return $html; + } + + /** + * tree widget using dhtmlXtree + * + * Code inspired by Lars's Felamimail uiwidgets::createFolderTree() + * + * @author Lars Kneschke original code in felamimail + * @param array $_folders array of folders: pairs path => node (string label or array with keys: label, (optional) image, (optional) title, (optional) checked) + * @param string $_selected path of selected folder + * @param mixed $_topFolder =false node of topFolder or false for none + * @param string $_onNodeSelect ='alert' js static function to call if node gets selected + * @param string $tree ='foldertree' id of the div and name of the variable containing the tree object + * @param string $_divClass ='' css class of the div + * @param string $_leafImage ='' default image of a leaf-node, ''=default of foldertree, set it eg. 'folderClosed.gif' to show leafs as folders + * @param boolean|string $_onCheckHandler =false string with handler-name to display a checkbox for each folder, or false (default), 'null' switches checkboxes on without an handler! + * @param string $delimiter ='/' path-delimiter, default / + * @param string $folderImageDir =null string path to the tree menu images, null uses default path + * @param string|array $autoLoading =null EGw relative path or array with get parameter, both send through egw::link + * @param string $dataMode ='JSON' data type for autoloading: XML, JSON, CSV + * @param boolean $dragndrop =false true to enable drag-n-drop (must be before autoloading get enabled!) + * + * @return string the html code, to be added into the template + */ + static function tree($_folders,$_selected,$_topFolder=false,$_onNodeSelect="null",$tree='foldertree',$_divClass='', + $_leafImage='',$_onCheckHandler=false,$delimiter='/',$folderImageDir=null,$autoLoading=null,$dataMode='JSON', + $dragndrop=false) + { + $webserver_url = $GLOBALS['egw_info']['server']['webserver_url']; + if (empty($folderImageDir)) + { + $folderImageDir = $webserver_url.'/phpgwapi/templates/default/images'; + } + // check if we have template-set specific image path + $image_path = $folderImageDir; + if ($webserver_url && $webserver_url != '/') + { + list(,$image_path) = explode($webserver_url, $image_path, 2); + } + $templated_path = strtr($image_path, array( + '/phpgwapi/templates/default' => $GLOBALS['egw']->framework->template_dir, + '/default/' => '/'.$GLOBALS['egw']->framework->template.'/', + )); + if (file_exists(EGW_SERVER_ROOT.$templated_path.'/dhtmlxtree')) + { + $folderImageDir = ($webserver_url != '/' ? $webserver_url : '').$templated_path; + //error_log(__METHOD__."() setting templated image-path: $folderImageDir"); + } + + static $tree_initialised=false; + if (!$tree_initialised) + { + egw_framework::includeCSS('/phpgwapi/js/dhtmlxtree/codebase/dhtmlxtree.css'); + egw_framework::validate_file('/phpgwapi/js/dhtmlxtree/codebase/dhtmlxcommon.js'); + egw_framework::validate_file('/phpgwapi/js/dhtmlxtree/sources/dhtmlxtree.js'); + if ($autoLoading && $dataMode != 'XML') egw_framework::validate_file('/phpgwapi/js/dhtmlxtree/sources/ext/dhtmlxtree_json.js'); + $tree_initialised = true; + if (!$_folders && !$autoLoading) return null; + } + $html = self::div("\n",'id="'.$tree.'"',$_divClass).$html; + $html .= "\n"; + } + } + + $top = 0; + if ($_topFolder) + { + $top = '--topfolder--'; + $topImage = ''; + if (is_array($_topFolder)) + { + $label = $_topFolder['label']; + if (isset($_topFolder['image'])) + { + $topImage = $_topFolder['image']; + } + } + else + { + $label = $_topFolder; + } + $html .= "\n$tree.insertNewItem(0,'$top','".addslashes($label)."',$_onNodeSelect,'$topImage','$topImage','$topImage','CHILD,TOP');\n"; + + if (is_array($_topFolder) && isset($_topFolder['title'])) + { + $html .= "$tree.setItemText('$top','".addslashes($label)."','".addslashes($_topFolder['title'])."');\n"; + } + } + if (is_string($_folders)) + { + switch($dataMode) + { + case 'JSON': + $html .= "$tree.loadJSONObject($_folders);\n"; break; + case 'XML': + $html .= "$tree.loadXMLString('$_folders');\n"; break; + } + } + else + { + // evtl. remove leading delimiter + if ($_selected[0] == $delimiter) $_selected = substr($_selected,1); + + $n = 0; + foreach($_folders as $path => $data) + { + if (!is_array($data)) + { + $data = array('label' => $data); + } + $image1 = $image2 = $image3 = '0'; + + // if _leafImage given, set it only for leaves, not for folders containing children + if ($_leafImage) + { + $image1 = $image2 = $image3 = "'".$_leafImage."'"; + if (($next_item = array_slice($_folders, $n+1, 1, true))) + { + list($next_path) = each($next_item); + if (substr($next_path,0,strlen($path)+1) == $path.'/') + { + $image1 = $image2 = $image3 = '0'; + } + } + } + if (isset($data['image'])) + { + $image1 = $image2 = $image3 = "'".$data['image']."'"; + } + // evtl. remove leading delimiter + if ($path[0] == $delimiter) $path = substr($path,1); + $folderParts = explode($delimiter,$path); + + //get rightmost folderpart + $label = array_pop($folderParts); + if (isset($data['label'])) $label = $data['label']; + + // the rest of the array is the name of the parent + $parentName = implode((array)$folderParts,$delimiter); + if(empty($parentName)) $parentName = $top; + + $entryOptions = !isset($data['child']) || $data['child'] ? 'CHILD' : ''; + if ($_onCheckHandler && $_selected) // check selected items on multi selection + { + if (!is_array($_selected)) $_selected = explode(',',$_selected); + if (array_search("$path",$_selected)!==false) $entryOptions .= ',CHECKED'; + //echo "

path=$path, _selected=".print_r($_selected,true).": $entryOptions

\n"; + } + // highlight current item + elseif ((string)$_selected === (string)$path) + { + $entryOptions .= ',SELECT'; + } + $html .= "$tree.insertNewItem('".addslashes($parentName)."','".addslashes($path)."','".addslashes($label). + "',$_onNodeSelect,$image1,$image2,$image3,'$entryOptions');\n"; + if (isset($data['title'])) + { + $html .= "$tree.setItemText('".addslashes($path)."','".addslashes($label)."','".addslashes($data['title'])."');\n"; + } + ++$n; + } + } + $html .= "$tree.closeAllItems(0);\n"; + if ($_selected) + { + foreach(is_array($_selected)?$_selected:array($_selected) as $path) + { + $html .= "$tree.openItem('".addslashes($path)."');\n"; + } + } + else + { + $html .= "$tree.openItem('$top');\n"; + } + $html .= "});"; + $html .= "\n"; + + return $html; + } + + /** + * Runs HTMLPurifier over supplied html to remove malicious code + * + * @param string $html + * @param array|string $config =null - config to influence the behavior of current purifying engine + * @param array|string $spec =null - spec to influence the behavior of current purifying engine + * The $spec argument can be used to disallow an otherwise legal attribute for an element, + * or to restrict the attribute's values + * @param boolean $_force =null - force the config passed to be used without merging to the default + */ + static function purify($html,$config=null,$spec=array(),$_force=false) + { + $defaultConfig = array('valid_xhtml'=>1,'safe'=>1); + + if (empty($html)) return $html; // no need to process further + if (!empty($config) && is_string($config)) + { + //error_log(__METHOD__.__LINE__.$config); + $config = json_decode($config,true); + if (is_null($config)) error_log(__METHOD__.__LINE__." decoding of config failed; standard will be applied"); + } + + // User preferences + $font = $GLOBALS['egw_info']['user']['preferences']['common']['rte_font']; + $font_size = $GLOBALS['egw_info']['user']['preferences']['common']['rte_font_size']; + + // Check for "blank" = just user preference span - for some reason we can't match on the entity, so approximate + $regex = '#^.?$#us'; + if(preg_match($regex,$html)) + { + return ''; + } + $htmLawed = new egw_htmLawed(); + if (is_array($config) && $_force===false) $config = array_merge($defaultConfig, $config); + if (empty($config)) $config = $defaultConfig; + //error_log(__METHOD__.__LINE__.array2string($config)); + return $htmLawed->egw_htmLawed($html,$config,$spec); + } +} diff --git a/api/src/Image.php b/api/src/Image.php index d5f04abe66..dcb3d3712f 100644 --- a/api/src/Image.php +++ b/api/src/Image.php @@ -11,9 +11,6 @@ namespace EGroupware\Api; -// explicitly import old phpgwapi classes used: -use html; - /** * Finding template specific images * @@ -34,7 +31,7 @@ class Image */ static function find($app,$image,$extension='',$_svg=false) { - $svg = html::$ua_mobile ? null : $_svg; // ATM we use svg icons only for mobile theme + $svg = Header\UserAgent::mobile() ? null : $_svg; // ATM we use svg icons only for mobile theme static $image_map_no_svg = null, $image_map_svg = null; if (is_null($svg)) $svg = self::svg_usable (); if ($svg) @@ -114,7 +111,7 @@ class Image */ public static function svg_usable() { - return html::$user_agent !== 'msie' || html::$ua_version >= 9; + return Header\UserAgent::type() !== 'msie' || Header\UserAgent::version() >= 9; } /** @@ -160,7 +157,7 @@ class Image $app_map =& $map[$app]; if (true) $app_map = array(); $imagedirs = array(); - if (html::$ua_mobile) + if (Header\UserAgent::mobile()) { $imagedirs[] = '/'.$app.'/templates/mobile/images'; } diff --git a/api/src/Link.php b/api/src/Link.php index 401b5bf7c5..81537b9d69 100644 --- a/api/src/Link.php +++ b/api/src/Link.php @@ -18,7 +18,6 @@ namespace EGroupware\Api; // explicitly reference classes still in phpgwapi -use html; use egw; // on_shutdown use egw_json_response; @@ -202,7 +201,8 @@ class Link extends Link\Storage { // FireFox 36 can not display pdf with it's internal viewer in an iframe used by mobile theme/template for popups // same is true for all mobile devices - if (html::$user_agent == 'firefox' && $GLOBALS['egw_info']['user']['preferences']['common']['theme'] == 'mobile' || html::$ua_mobile) + if (Header\UserAgent::type() == 'firefox' && $GLOBALS['egw_info']['user']['preferences']['common']['theme'] == 'mobile' || + Header\UserAgent::mobile()) { unset(self::$app_register['home']['mime']['application/pdf']); } diff --git a/api/src/Session.php b/api/src/Session.php index 682968ecab..dd6d2fc363 100644 --- a/api/src/Session.php +++ b/api/src/Session.php @@ -25,7 +25,6 @@ namespace EGroupware\Api; // explicitly reference classes still in phpgwapi use egw_mailer; use egw_digest_auth; // egw_digest_auth::parse_digest -use html; // html::$ua_mobile /** * Create, verifies or destroys an EGroupware session @@ -1245,7 +1244,7 @@ class Session // if we are installed in iOS as web-app, we must not set a cookietime==0 (session-cookie), // as every change between apps will cause the cookie to get lost static $is_iOS = null; - if (!$cookietime && !isset($is_iOS)) $is_iOS = (bool)preg_match('/^(iPhone|iPad|iPod)/i', html::$ua_mobile); + if (!$cookietime && !isset($is_iOS)) $is_iOS = (bool)preg_match('/^(iPhone|iPad|iPod)/i', Header\UserAgent::mobile()); if(!headers_sent()) // gives only a warning, but can not send the cookie anyway { diff --git a/api/src/Storage/Tracking.php b/api/src/Storage/Tracking.php index f63ff2ced6..18fdeeb0bc 100644 --- a/api/src/Storage/Tracking.php +++ b/api/src/Storage/Tracking.php @@ -16,7 +16,6 @@ namespace EGroupware\Api\Storage; use EGroupware\Api; // explicitly reference classes still in phpgwapi or otherwise outside api -use html; use notifications; /** @@ -1057,7 +1056,7 @@ abstract class Tracking if ($html_mail) { - if (!$this->html_content_allow) $line = html::htmlspecialchars($line); // XSS + if (!$this->html_content_allow) $line = Api\Html::htmlspecialchars($line); // XSS $color = $modified ? 'red' : false; $size = '110%'; @@ -1080,7 +1079,7 @@ abstract class Tracking // Only Convert nl2br on non-html content if (strpos($data, 'html_content_allow ? $data : html::htmlspecialchars($data)); + $data = nl2br($this->html_content_allow ? $data : Api\Html::htmlspecialchars($data)); $this->html_content_allow = true; // to NOT do htmlspecialchars again } break; @@ -1108,15 +1107,15 @@ abstract class Tracking if ($type == 'link') { // the link is often too long for html boxes chunk-split allows to break lines if needed - $content .= html::a_href(chunk_split(rawurldecode($data),40,'​'),$data,'','target="_blank"'); + $content .= Api\Html::a_href(chunk_split(rawurldecode($data),40,'​'),$data,'','target="_blank"'); } elseif ($this->html_content_allow) { - $content .= html::activate_links($data); + $content .= Api\Html::activate_links($data); } else { - $content .= html::htmlspecialchars($data); + $content .= Api\Html::htmlspecialchars($data); } } else diff --git a/mail/inc/class.mail_compose.inc.php b/mail/inc/class.mail_compose.inc.php index 27e4bc61d7..569a5b3d35 100644 --- a/mail/inc/class.mail_compose.inc.php +++ b/mail/inc/class.mail_compose.inc.php @@ -230,7 +230,7 @@ class mail_compose } if (html::$ua_mobile) { - foreach ($actions as $key => $action) + foreach (array_keys($actions) as $key) { if (!in_array($key, array('send','button[saveAsDraft]','uploadForCompose' ))) { $actions[$key]['toolbarDefault'] = false; @@ -548,7 +548,7 @@ class mail_compose $suppressSigOnTop = true; if (stripos($content['mail_htmltext'],'
')!==false)
 			{
-				$contentArr = html::splithtmlByPRE($content['mail_htmltext']);
+				$contentArr = translation::splithtmlByPRE($content['mail_htmltext']);
 				if (is_array($contentArr))
 				{
 					foreach ($contentArr as $k =>&$elem)
@@ -1171,7 +1171,7 @@ class mail_compose
 			foreach((array)$addr_content as $key => $value) {
 				if ($value=="NIL@NIL") continue;
 				if ($destination=='replyto' && str_replace('"','',$value) ==
-					str_replace('"','',$identities[$presetId ? $presetId : $this->mail_bo->getDefaultIdentity()]))
+					str_replace('"','',$identities[$this->mail_bo->getDefaultIdentity()]))
 				{
 					// preserve/restore the value to content.
 					$content[strtolower($destination)][]=$value;
@@ -1539,6 +1539,7 @@ class mail_compose
 			}
 			catch (Exception $e)
 			{
+				unset($e);
 				// fail silently
 				$this->sessionData['mailaccount'] = $mail_bo->profileID;
 			}
@@ -2123,9 +2124,9 @@ class mail_compose
 				}
 
 				// add line breaks to $bodyParts
-				$newBody = translation::convert_jsonsafe($bodyParts[$i]['body'],$bodyParts[$i]['charSet']);
+				$newBody2 = translation::convert_jsonsafe($bodyParts[$i]['body'],$bodyParts[$i]['charSet']);
 				#error_log( "GetReplyData (Plain) CharSet:".mb_detect_encoding($bodyParts[$i]['body'] . 'a' , strtoupper($bodyParts[$i]['charSet']).','.strtoupper($this->displayCharset).',UTF-8, ISO-8859-1'));
-				$newBody = mail_ui::resolve_inline_images($newBody, $_folder, $_uid, $_partID, 'plain');
+				$newBody = mail_ui::resolve_inline_images($newBody2, $_folder, $_uid, $_partID, 'plain');
 				$this->sessionData['body'] .= "\r\n";
 				// create body new, with good line breaks and indention
 				foreach(explode("\n",$newBody) as $value) {
@@ -2153,7 +2154,7 @@ class mail_compose
 		return $this->sessionData;
 
 	}
-	
+
 	/**
 	 * HTML cleanup
 	 *
diff --git a/phpgwapi/inc/class.egw_htmLawed.inc.php b/phpgwapi/inc/class.egw_htmLawed.inc.php
index 8a406bcbd7..4bfd4ce27f 100644
--- a/phpgwapi/inc/class.egw_htmLawed.inc.php
+++ b/phpgwapi/inc/class.egw_htmLawed.inc.php
@@ -110,14 +110,109 @@ class egw_htmLawed
 		if (empty($Spec)) $Spec = $this->Spec;
 		// If we are processing mails, we take out stuff in  tags and
 		// put it back in after purifying; styles are processed for known security risks
-		// in html::getStyles
-		$styles='';
+		// in self::getStyles
 		// we allow filtered style sections now throughout egroupware
-		/*if ($Config['hook_tag'] =="hl_email_tag_transform")*/ $styles = html::getStyles($html2check);
+		/*if ($Config['hook_tag'] =="hl_email_tag_transform")*/ $styles = self::getStyles($html2check);
 		//error_log(__METHOD__.__LINE__.array2string($styles));
 
 		return ($styles?$styles:'').htmLawed($html2check, $Config, $Spec);
 	}
+
+	/**
+	 * get all style tag definitions,  of the html passed in
+	 * and remove it from input
+	 * @author Leithoff, Klaus
+	 * @param string html
+	 * @return string the style css
+	 */
+	static function getStyles(&$html)
+	{
+		$ct=0;
+		$newStyle = null;
+		if (stripos($html,'(.+)#isU', $html, $newStyle);
+		if ($ct>0)
+		{
+			//error_log(__METHOD__.__LINE__.array2string($newStyle[0]));
+			$style2buffer = implode('',$newStyle[0]);
+			// only replace what we have found, we use it here, as we use the same routine in Translation::replaceTagsCompletley
+			// no need to do the extra routine
+			$html = str_ireplace($newStyle[0],'',$html);
+		}
+		if ($style2buffer)
+		{
+			//error_log(__METHOD__.__LINE__.array2string($style2buffer));
+			$test = json_encode($style2buffer);
+			//error_log(__METHOD__.__LINE__.'#'.$test.'# ->'.strlen($style2buffer).' Error:'.json_last_error());
+			//if (json_last_error() != JSON_ERROR_NONE && strlen($style2buffer)>0)
+			if ($test=="null" && strlen($style2buffer)>0)
+			{
+				// this should not be needed, unless something fails with charset detection/ wrong charset passed
+				error_log(__METHOD__.__LINE__.' Found Invalid sequence for utf-8 in CSS:'.$style2buffer.' Carset Detected:'.Translation::detect_encoding($style2buffer));
+				$style2buffer = utf8_encode($style2buffer);
+			}
+		}
+		$style .= $style2buffer;
+		// clean out comments and stuff
+		$search = array(
+			'@url\(http:\/\/[^\)].*?\)@si',  // url calls e.g. in style definitions
+//			'@@',   // Strip multi-line comments including CDATA
+//			'@ in stylesheet are outdated, and ck-editor does not understand it, we remove it
+		$css_no_comment = str_replace(array(':',''),array(': ','',''),$css);
+		//error_log(__METHOD__.__LINE__.$css);
+		// we already removed what we have found, above, as we used pretty much the same routine as in Translation::replaceTagsCompletley
+		// no need to do the extra routine
+		// TODO: we may have to strip urls and maybe comments and ifs
+		//if (stripos($html,'style')!==false) Translation::replaceTagsCompletley($html,'style'); // clean out empty or pagewide style definitions / left over tags
+		return $css_no_comment;
+	}
+
+	/**
+	 * Runs HTMLPurifier over supplied html to remove malicious code
+	 *
+	 * @param string $html
+	 * @param array|string $config =null - config to influence the behavior of current purifying engine
+	 * @param array|string $spec =null - spec to influence the behavior of current purifying engine
+	 *		The $spec argument can be used to disallow an otherwise legal attribute for an element,
+	 *		or to restrict the attribute's values
+	 * @param boolean $_force =null - force the config passed to be used without merging to the default
+	 */
+	static function purify($html,$config=null,$spec=array(),$_force=false)
+	{
+		$defaultConfig = array('valid_xhtml'=>1,'safe'=>1);
+
+		if (empty($html)) return $html;	// no need to process further
+		if (!empty($config) && is_string($config))
+		{
+			//error_log(__METHOD__.__LINE__.$config);
+			$config = json_decode($config,true);
+			if (is_null($config)) error_log(__METHOD__.__LINE__." decoding of config failed; standard will be applied");
+		}
+
+		// User preferences
+		$font = $GLOBALS['egw_info']['user']['preferences']['common']['rte_font'];
+		$font_size = $GLOBALS['egw_info']['user']['preferences']['common']['rte_font_size'];
+
+		// Check for "blank" = just user preference span - for some reason we can't match on the entity, so approximate
+		$regex = '#^.?$#us';
+		if(preg_match($regex,$html))
+		{
+			return '';
+		}
+		$htmLawed = new egw_htmLawed();
+		if (is_array($config) && $_force===false) $config = array_merge($defaultConfig, $config);
+		if (empty($config)) $config = $defaultConfig;
+		//error_log(__METHOD__.__LINE__.array2string($config));
+		return $htmLawed->egw_htmLawed($html,$config,$spec);
+	}
 }
 
 /**
@@ -237,8 +332,8 @@ function hl_my_tag_transform($element, $attribute_array=0)
 function hl_email_tag_transform($element, $attribute_array=0)
 {
 	//error_log(__METHOD__.__LINE__.$element.array2string($attribute_array));
-	static $lastelement;
-	static $throwawaycounter;
+	static $lastelement = null;
+	static $throwawaycounter = null;
 	if (is_null($lastelement)) $lastelement='';
 	if (is_null($throwawaycounter)) $throwawaycounter = 0;
 	//if ($throwawaycounter>1) error_log(__METHOD__.__LINE__.' '.$throwawaycounter.$element.array2string($attribute_array));
diff --git a/phpgwapi/inc/class.html.inc.php b/phpgwapi/inc/class.html.inc.php
index c0244f9521..b51f61eaa8 100644
--- a/phpgwapi/inc/class.html.inc.php
+++ b/phpgwapi/inc/class.html.inc.php
@@ -1,139 +1,144 @@
  complete rewrite in 6/2006 and earlier modifications
  * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
  * @author RalfBecker-AT-outdoor-training.de
- * @copyright 2001-2014 by RalfBecker@outdoor-training.de
+ * @copyright 2001-2016 by RalfBecker@outdoor-training.de
  * @package api
  * @subpackage html
  * @version $Id$
  */
 
+use EGroupware\Api;
+
 /**
  * Generates html with methods representing html-tags or higher widgets
  *
- * The class has only static methods now, so there's no need to instanciate as object anymore!
+ * @deprecated use methods of Api\Html, Api\Api\Header\Content or Api\Api\Header\UserAgent classes
  */
-class html
+class html extends Api\Html
 {
 	/**
-	 * user-agent: 'firefox', 'msie', 'safari' (incl. iPhone), 'chrome', 'opera', 'konqueror', 'mozilla'
-	 * @var string
+	 * @deprecated use Api\Api\Header\UserAgent::type()
 	 */
 	static $user_agent;
 	/**
-	 * User agent is mobile browser: "iphone", "ipod", "ipad", "android", "symbianos", "blackberry", "kindle", "opera mobi", "windows phone"
-	 * @var string with name of mobile browser or null, if not mobile browser
+	 * @deprecated use Api\Api\Header\UserAgent::mobile()
 	 */
 	static $ua_mobile;
 	/**
-	 * version of user-agent as specified by browser
-	 * @var string
+	 * @deprecated use Api\Api\Header\UserAgent::version()
 	 */
 	static $ua_version;
 	/**
-	 * what attribute to use for the title of an image: 'title' for everything but netscape4='alt'
-	 * @var string
+	 * @deprecated seriously Netscape4 ;-)
 	 */
 	static $netscape4;
-	static private $prefered_img_title;
 	/**
-	 * charset used by the page, as returned by $GLOBALS['egw']->translation->charset()
-	 * @var string
+	 * @deprecated seriously Netscape4 ;-)
+	 */
+	static private $prefered_img_title = 'title';
+	/**
+	 * @deprecated use Api\Translation::charset()
 	 */
 	static $charset;
-	/**
-	 * URL (NOT path) of the js directory in the api
-	 * @var string
-	 */
-	static $api_js_url;
 
 	/**
-	 * Automatically turn on enhanced selectboxes if there's more than this many options
+	 * Output content headers for user-content, mitigating risk of javascript or html
+	 *
+	 * Mitigate risk of serving javascript or css from our domain,
+	 * which will get around same origin policy and CSP!
+	 *
+	 * Mitigate risk of html downloads by using CSP or force download for IE
+	 *
+	 * @param resource|string &$content content might be changed by this call
+	 * @param string $path filename or path for content-disposition header
+	 * @param string &$mime ='' mimetype or '' (default) to detect it from filename, using mime_magic::filename2mime()
+	 *	on return used, maybe changed, mime-type
+	 * @param int $length =0 content length, default 0 = skip that header
+	 *  on return changed size
+	 * @param boolean $nocache =true send headers to disallow browser/proxies to cache the download
+	 * @param boolean $force_download =true send content-disposition attachment header
+	 * @param boolean $no_content_type =false do not send actual content-type and content-length header, just content-disposition
+	 * @deprecated use Api\Api\Header\Content::safe()
 	 */
-	const SELECT_ENHANCED_ROW_COUNT = 12;
-
-	/**
-	 * initialise our static vars
-	 */
-	static function _init_static()
+	public static function safe_content_header(&$content, $path, &$mime='', &$length=0, $nocache=true, $force_download=true, $no_content_type=false)
 	{
-		// should be Ok for all HTML 4 compatible browsers
-		$parts = $all_parts = null;
-		if(!preg_match('/compatible; ([a-z]+)[\/ ]+([0-9.]+)/i',$_SERVER['HTTP_USER_AGENT'],$parts))
-		{
-			preg_match_all('/([a-z]+)\/([0-9.]+)/i',$_SERVER['HTTP_USER_AGENT'],$all_parts,PREG_SET_ORDER);
-			$parts = array_pop($all_parts);
-			foreach($all_parts as $p)
-			{
-				if ($p[1] == 'Chrome' && $parts[1] != 'Edge')
-				{
-					$parts = $p;
-					break;
-				}
-			}
-		}
-		list(,self::$user_agent,self::$ua_version) = $parts;
-		if ((self::$user_agent = strtolower(self::$user_agent)) == 'version') self::$user_agent = 'opera';
-		// IE no longer reports MSIE, but "Trident/7.0; rv:11.0"
-		if (self::$user_agent=='trident')
-		{
-			self::$user_agent='msie';
-			$matches = null;
-			self::$ua_version = preg_match('|Trident/[0-9.]+; rv:([0-9.]+)|i', $_SERVER['HTTP_USER_AGENT'], $matches) ?
-				$matches[1] : 11.0;
-		}
-		// iceweasel is based on mozilla and we treat it like as firefox
-		if (self::$user_agent == 'iceweasel')
-		{
-			self::$user_agent = 'firefox';
-		}
-		// MS Edge sometimes reports just "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
-		if (self::$user_agent == 'mozilla' && self::$ua_version == '5.0')
-		{
-			self::$user_agent = 'edge';
-			self::$ua_version = '12.0';
-		}
-		self::$ua_mobile = preg_match('/(iPhone|iPod|iPad|Android|SymbianOS|Blackberry|Kindle|Opera Mobi|Windows Phone)/i',
-			$_SERVER['HTTP_USER_AGENT'], $matches) ? strtolower($matches[1]) : null;
-
-		self::$netscape4 = self::$user_agent == 'mozilla' && self::$ua_version < 5;
-		self::$prefered_img_title = self::$netscape4 ? 'alt' : 'title';
-		//error_log("HTTP_USER_AGENT='$_SERVER[HTTP_USER_AGENT]', UserAgent: '".self::$user_agent."', Version: '".self::$ua_version."', isMobile=".array2string(self::$ua_mobile).", img_title: '".self::$prefered_img_title."'");
-
-		if ($GLOBALS['egw']->translation)
-		{
-			self::$charset = $GLOBALS['egw']->translation->charset();
-		}
-		self::$api_js_url = $GLOBALS['egw_info']['server']['webserver_url'].'/phpgwapi/js';
+		Api\Api\Header\Content::safe($content, $path, $mime, $length, $nocache, $force_download, $no_content_type);
 	}
 
 	/**
-	* Created an input-field with an attached color-picker
-	*
-	* Please note: it need to be called before the call to egw_header() !!!
-	*
-	* @param string $name the name of the input-field
-	* @param string $value ='' the actual value for the input-field, default ''
-	* @param string $title ='' tooltip/title for the picker-activation-icon
-	* @param string $options ='' options for input
-	* @return string the html
-	*/
+	 * Output content-type headers for file downloads
+	 *
+	 * This function should only be used for non-user supplied content!
+	 * For uploaded files, mail attachmentes, etc, you have to use safe_content_header!
+	 *
+	 * @author Miles Lott originally in browser class
+	 * @param string $fn filename
+	 * @param string $mime ='' mimetype or '' (default) to detect it from filename, using mime_magic::filename2mime()
+	 * @param int $length =0 content length, default 0 = skip that header
+	 * @param boolean $nocache =true send headers to disallow browser/proxies to cache the download
+	 * @param boolean $forceDownload =true send headers to handle as attachment/download
+	 * @deprecated use Api\Api\Header\Content::type()
+	 */
+	public static function content_header($fn,$mime='',$length=0,$nocache=True,$forceDownload=true)
+	{
+		Api\Api\Header\Content::type($fn, $mime, $length, $nocache, $forceDownload);
+	}
+
+	/**
+	 * Output content-disposition header for file downloads
+	 *
+	 * @param string $fn filename
+	 * @param boolean $forceDownload =true send headers to handle as attachment/download
+	 * @deprecated use Api\Api\Header\Content::disposition()
+	 */
+	public static function content_disposition_header($fn,$forceDownload=true)
+	{
+		Api\Api\Header\Content::disposition($fn, $forceDownload);
+	}
+
+	/**
+	 * Created an input-field with an attached color-picker
+	 *
+	 * @param string $name the name of the input-field
+	 * @param string $value ='' the actual value for the input-field, default ''
+	 * @param string $title ='' tooltip/title for the picker-activation-icon
+	 * @param string $options ='' options for input
+	 * @deprecated use html5 input type="color"
+	 * @return string the html
+	 */
 	static function inputColor($name,$value='',$title='',$options='')
 	{
-		$options .= ' id="'.htmlspecialchars($id=str_replace(array('[',']'),array('_',''),$name).'_colorpicker').'"';
-		$onclick = "javascript:egw_openWindowCentered2('".self::$api_js_url.'/colorpicker/select_color.html?id='.urlencode($id)."&color='+encodeURIComponent(document.getElementById('$id').value),'colorPicker',240,187);";
-		if (preg_match('/^#[0-9A-F]{6}$/i',$value))
-		{
-			$options .= ' style="background-color: '.$value.'"';
-		}
-		$options .= ' onChange="this.style.backgroundColor=this.value.match(/^(#[0-9A-F]+|[a-z]+)$/i)?this.value:\'#FFFFFF\'+(this.value=\'\')"';
-		return self::input($name, $value, 'text', $options.' size="7" maxsize="7"').' '.
-			''.
-			'";
+		return self::input($name, $value, 'color', $options.' size="7" maxsize="7"').
+			($title ? ' title="'.self::htmlspecialchars($title).'"' : '');
+	}
+
+	/**
+	 * representates a b tab (bold)
+	 *
+	 * @param string $content of the link, if '' only the opening tag gets returned
+	 * @deprecated use css
+	 * @return string the html
+	 */
+	static function bold($content)
+	{
+		return ''.$content.'';
+	}
+
+	/**
+	 * representates a i tab (bold)
+	 *
+	 * @param string $content of the link, if '' only the opening tag gets returned
+	 * @deprecated use css
+	 * @return string the html
+	 */
+	static function italic($content)
+	{
+		return ''.$content.'';
 	}
 
 	/**
@@ -145,6 +150,7 @@ class html
 	*  title (string) gives extra title-row, width (int,'auto') , padding (int), above (bool), bgcolor (color), bgimg (URL)
 	*  For a complete list and description see http://www.walterzorn.com/tooltip/tooltip_e.htm
 	* @param boolean $return_as_attributes true to return array(onmouseover, onmouseout) attributes
+	* @deprecated use something else ;-)
 	* @return string|array to be included in any tag, like 'Ralf').'>Text with tooltip

' */ static function tooltip($text,$do_lang=False,$options=False, $return_as_attributes=false) @@ -184,1016 +190,6 @@ class html ' onmouseover="'.self::htmlspecialchars($ttip).'" onmouseout="'.$untip.'"'; } - /** - * activates URLs in a text, URLs get replaced by html-links - * - * @param string $content text containing URLs - * @return string html with activated links - */ - static function activate_links($content) - { - if (!$content || strlen($content) < 20) return $content; // performance - - // Exclude everything which is already a link - $NotAnchor = '(?\\1 AT \\2 DOT \\3', - "$1 AT $2 DOT $3", - $content); - - // First match things beginning with http:// (or other protocols) - $optBracket0 = '(<|<)'; - $Protocol = '(http:\/\/|(ftp:\/\/|https:\/\/))'; // only http:// gets removed, other protocolls are shown - $Domain = '([\w-]+\.[\w-.]+)'; - $Subdir = '([\w\-\.,@?^=%&;:\/~\+#]*[\w\-\@?^=%&\/~\+#])?'; - $optBracket = ''; - $optBracket = '(>|>)'; - $Expr = '/' .$optBracket0. $NotAnchor . $Protocol . $Domain . $Subdir . $optBracket . '/i'; - // use preg_replace_callback as we experienced problems with https links - $result2 = preg_replace_callback( $Expr, function ($match) { - //error_log(__METHOD__.__LINE__.array2string($match)); - return $match[1]."".$match[4].$match[5]."".$match[6]; - }, $result ); -//error_log(__METHOD__.__LINE__.array2string($Expr)); -//error_log(__METHOD__.__LINE__.array2string($result2)); - // First match things beginning with http:// (or other protocols) - $Protocol = '(http:\/\/|(ftp:\/\/|https:\/\/))'; // only http:// gets removed, other protocolls are shown - $Domain = '([\w-]+\.[\w-.]+)'; - $Subdir = '([\w\-\.,@?^=%&;:\/~\+#]*[\w\-\@?^=%&\/~\+#])?'; - $Expr = '/' . $NotAnchor . $Protocol . $Domain . $Subdir . '/i'; - // use preg_replace_callback as we experienced problems with https links - $result3 = preg_replace_callback( $Expr, function ($match) { - //error_log(__METHOD__.__LINE__.array2string($match)); - return "".$match[3].$match[4].""; - }, $result2 ); -//error_log(__METHOD__.__LINE__.array2string($Expr)); -//error_log(__METHOD__.__LINE__.array2string($result3)); - // Now match things beginning with www. - $optBracket0 = '(<|<)?'; - $NotHTTP = '(?)'; // avoid running again on http://www links already handled above - $Domain2 = 'www(\.[\w-.]+)'; - $Subdir2 = '([\w\-\.,@?^=%&:\/~\+#]*[\w\-\@?^=%&\/~\+#])?'; - $optBracket = '(>|>)?'; - $Expr = '/' .$optBracket0. $NotAnchor . $NotHTTP . $Domain2 . $Subdir2 .$optBracket. '/i'; - //$Expr = '/' . $NotAnchor . $NotHTTP . $Domain . $Subdir . $optBracket . '/i'; - - $result4 = preg_replace( $Expr, "$1www$2$3$4$5$6", $result3 ); - //return preg_replace( $Expr, "www$1$2$3 ", $result ); -//error_log(__METHOD__.__LINE__.array2string($Expr)); -//error_log(__METHOD__.__LINE__.array2string($result4)); - return $result4; - } - - /** - * escapes chars with special meaning in html as entities - * - * Allows to use and char in the html-output and prevents XSS attacks. - * Some entities are allowed and get NOT escaped: -> prevented by 4th param = doubleencode=false - * - &# some translations (AFAIK: the arabic ones) need this; - * -   < > for convenience -> should not happen anymore, as we do not doubleencode anymore (20101020) - * - * @param string $str string to escape - * @param boolean $double_encoding =false do we want double encoding or not, default no - * @return string - */ - static function htmlspecialchars($str, $double_encoding=false) - { - //if (!is_scalar($str) && !is_null($str)) error_log(__METHOD__.'('.array2string($str).') '.function_backtrace()); - // as EGroupware supports only utf-8 we should not need to worry about wrong charsets - //if (is_array($str)) error_log(__METHOD__.__LINE__.' string expected -> array given:'.array2string($str).'->'.function_backtrace()); - return htmlspecialchars($str,ENT_COMPAT,self::$charset,$double_encoding); - // we need '&#' unchanged, so we translate it back -> this is provided by 4th param = false -> do not doubleencode - //$str = str_replace(array('&#','&nbsp;','&lt;','&gt;'),array('&#',' ','<','>'),$str); - - //return $str; - } - - /** - * allows to show and select one item from an array - * - * @param string $name string with name of the submitted var which holds the key of the selected item form array - * @param string|array $key key(s) of already selected item(s) from $arr, eg. '1' or '1,2' or array with keys - * @param array $arr array with items to select, eg. $arr = array ( 'y' => 'yes','n' => 'no','m' => 'maybe'); - * @param boolean $no_lang NOT run the labels of the options through lang(), default false=use lang() - * @param string $options additional options (e.g. 'width') - * @param int $multiple number of lines for a multiselect, default 0 = no multiselect, < 0 sets size without multiple - * @param boolean $enhanced Use enhanced selectbox with search. Null for default yes if more than 12 options. - * @return string to set for a template or to echo into html page - */ - static function select($name, $key, $arr=0,$no_lang=false,$options='',$multiple=0,$enhanced=null) - { - if(is_null($enhanced)) $enhanced = false; //disabled by default (count($arr) > self::SELECT_ENHANCED_ROW_COUNT); - - if (!is_array($arr)) - { - $arr = array('no','yes'); - } - if ((int)$multiple > 0) - { - $options .= ' multiple="1" size="'.(int)$multiple.'"'; - if (substr($name,-2) != '[]') - { - $name .= '[]'; - } - } - elseif($multiple < 0) - { - $options .= ' size="'.abs($multiple).'"'; - } - // fix width for MSIE < 9 in/for selectboxes - if (self::$user_agent == 'msie' && self::$ua_version < 9) - { - if (stripos($options,'onfocus="') === false) - { - $options .= ' onfocus="window.dropdown_menu_hack(this);" '; - } - else - { - $options = str_ireplace('onfocus="','onfocus="window.dropdown_menu_hack(this);',$options); - } - } - $out = "\n"; - - if($enhanced) { - egw_framework::validate_file('/phpgwapi/js/jquery/chosen/chosen.jquery.js'); - egw_framework::includeCSS('/phpgwapi/js/jquery/chosen/chosen.css',null,false); - $out .= "\n"; - } - return $out; - } - - /** - * emulating a multiselectbox using checkboxes - * - * Unfortunaly this is not in all aspects like a multi-selectbox, eg. you cant select options via javascript - * in the same way. Therefor I made it an extra function. - * - * @param string $name string with name of the submitted var which holds the key of the selected item form array - * @param string|array $key key(s) of already selected item(s) from $arr, eg. '1' or '1,2' or array with keys - * @param array $arr array with items to select, eg. $arr = array ( 'y' => 'yes','n' => 'no','m' => 'maybe'); - * @param boolean $no_lang NOT run the labels of the options through lang(), default false=use lang() - * @param string $options additional options (e.g. 'width') - * @param int $multiple number of lines for a multiselect, default 3 - * @param boolean $selected_first show the selected items before the not selected ones, default true - * @param string $style ='' extra style settings like "width: 100%", default '' none - * @return string to set for a template or to echo into html page - */ - static function checkbox_multiselect($name, $key, $arr=0,$no_lang=false,$options='',$multiple=3,$selected_first=true,$style='',$enhanced = null) - { - //echo "

checkbox_multiselect('$name',".array2string($key).",".array2string($arr).",$no_lang,'$options',$multiple,$selected_first,'$style')

\n"; - if(is_null($enhanced)) $enhanced = (count($arr) > self::SELECT_ENHANCED_ROW_COUNT); - - if (!is_array($arr)) - { - $arr = array('no','yes'); - } - if ((int)$multiple <= 0) $multiple = 1; - - if (substr($name,-2) != '[]') - { - $name .= '[]'; - } - $base_name = substr($name,0,-2); - - if($enhanced) return self::select($name, $key, $arr,$no_lang,$options." style=\"$style\" ",$multiple,$enhanced); - - if (!is_array($key)) - { - // explode on ',' only if multiple values expected and the key contains just numbers and commas - $key = preg_match('/^[,0-9]+$/',$key) ? explode(',',$key) : array($key); - } - $html = ''; - $options_no_id = preg_replace('/id="[^"]+"/i','',$options); - - if ($selected_first) - { - $selected = $not_selected = array(); - foreach($arr as $val => $label) - { - if (in_array((string)$val,$key)) - { - $selected[$val] = $label; - } - else - { - $not_selected[$val] = $label; - } - } - $arr = $selected + $not_selected; - } - $max_len = 0; - foreach($arr as $val => $label) - { - if (is_array($label)) - { - $title = $label['title']; - $label = $label['label']; - } - else - { - $title = ''; - } - if ($label && !$no_lang) $label = lang($label); - if ($title && !$no_lang) $title = lang($title); - - if (strlen($label) > $max_len) $max_len = strlen($label); - - $html .= self::label(self::checkbox($name,in_array((string)$val,$key),$val,$options_no_id. - ' id="'.$base_name.'['.$val.']'.'"').self::htmlspecialchars($label), - $base_name.'['.$val.']','',($title ? 'title="'.self::htmlspecialchars($title).'" ':''))."
\n"; - } - if ($style && substr($style,-1) != ';') $style .= '; '; - if (strpos($style,'height')===false) $style .= 'height: '.(1.7*$multiple).'em; '; - if (strpos($style,'width')===false) $style .= 'width: '.(4+$max_len*($max_len < 15 ? 0.65 : 0.6)).'em; '; - $style .= 'background-color: white; overflow: auto; border: lightgray 2px inset; text-align: left;'; - - return self::div($html,$options,'',$style); - } - - /** - * generates an option-tag for a selectbox - * - * @param string $value value - * @param string $label label - * @param mixed $selected value or array of values of options to mark as selected - * @param boolean $no_lang NOT running the label through lang(), default false=use lang() - * @param string $extra extra text, e.g.: style="", default: '' - * @return string html - */ - static function select_option($value,$label,$selected,$no_lang=0,$title='',$extra='') - { - // the following compares strict as strings, to archive: '0' == 0 != '' - // the first non-strict search via array_search, is for performance reasons, to not always search the whole array with php - if (($found = ($key = array_search($value,$selected)) !== false) && (string) $value !== (string) $selected[$key]) - { - $found = false; - foreach($selected as $sel) - { - if (($found = (((string) $value) === ((string) $selected[$key])))) break; - } - unset($sel); - } - return '\n"; - } - - /** - * generates a div-tag - * - * @param string $content of a div, or '' to generate only the opening tag - * @param string $options to include in the tag, default ''=none - * @param string $class css-class attribute, default ''=none - * @param string $style css-styles attribute, default ''=none - * @return string html - */ - static function div($content,$options='',$class='',$style='') - { - if ($class) $options .= ' class="'.$class.'"'; - if ($style) $options .= ' style="'.$style.'"'; - - return "
\n".($content ? "$content
\n" : ''); - } - - /** - * generate one or more hidden input tag(s) - * - * @param array|string $vars var-name or array with name / value pairs - * @param string $value value if $vars is no array, default '' - * @param boolean $ignore_empty if true all empty, zero (!) or unset values, plus filer=none - * @param string html - */ - static function input_hidden($vars,$value='',$ignore_empty=True) - { - if (!is_array($vars)) - { - $vars = array( $vars => $value ); - } - foreach($vars as $name => $value) - { - if (is_array($value)) - { - $value = json_encode($value); - } - if (!$ignore_empty || $value && !($name == 'filter' && $value == 'none')) // dont need to send all the empty vars - { - $html .= "\n"; - } - } - return $html; - } - - /** - * generate a textarea tag - * - * @param string $name name attr. of the tag - * @param string $value default - * @param boolean $ignore_empty if true all empty, zero (!) or unset values, plus filer=none - * @param boolean $double_encoding =false do we want double encoding or not, default no - * @param string html - */ - static function textarea($name,$value='',$options='',$double_encoding=false) - { - return "\n"; - } - - /** - * Checks if HTMLarea (or an other richtext editor) is availible for the used browser - * - * @return boolean - */ - static function htmlarea_availible() - { - // this one is for testing how it will turn out, if you do not have the device or agent ready at your fingertips - // if (stripos($_SERVER[HTTP_USER_AGENT],'mozilla') !== false) return false; - - // CKeditor will doublecheck availability for us, but its fallback does not look nice, and you will get - // no conversion of html content to plain text, so we provide a check for known USER_AGENTS to fail the test - return true; - } - - /** - * compability static function for former used htmlarea. Please use static function fckeditor now! - * - * creates a textarea inputfield for the htmlarea js-widget (returns the necessary html and js) - */ - static function htmlarea($name,$content='',$style='',$base_href=''/*,$plugins='',$custom_toolbar='',$set_width_height_in_config=false*/) - { - /*if (!self::htmlarea_availible()) - { - return self::textarea($name,$content,'style="'.$style.'"'); - }*/ - return self::fckEditor($name, $content, $style, array('toolbar_expanded' =>'true'), '400px', '100%', $base_href); - } - - /** - * this static function is a wrapper for fckEditor to create some reuseable layouts - * - * @param string $_name name and id of the input-field - * @param string $_content of the tinymce (will be run through htmlspecialchars !!!), default '' - * @param string $_mode display mode of the tinymce editor can be: simple, extended or advanced - * @param array $_options (toolbar_expanded true/false) - * @param string $_height ='400px' - * @param string $_width ='100%' - * @param string $_start_path ='' if passed activates the browser for image at absolute path passed - * @param boolean $_purify =true run $_content through htmlpurifier before handing it to fckEditor - * @param mixed (boolean/string) $_focusToBody=false USED only for CKEDIOR true means yes, focus on top, you may specify TOP or BOTTOM (to focus on the end of the editor area) - * @param string $_executeJSAfterInit ='' Javascript to be executed after InstanceReady of CKEditor - * @return string the necessary html for the textarea - */ - static function fckEditor($_name, $_content, $_mode, $_options=array('toolbar_expanded' =>'true'), - $_height='400px', $_width='100%',$_start_path='',$_purify=true, $_focusToBody=false, $_executeJSAfterInit='') - { - if (!self::htmlarea_availible() || $_mode == 'ascii') - { - return self::textarea($_name,$_content,'style="width: '.$_width.'; height: '.$_height.';" id="'.htmlspecialchars($_name).'"'); - } - - //include the ckeditor js file - egw_framework::validate_file('ckeditor','ckeditor','phpgwapi'); - - // run content through htmlpurifier - if ($_purify && !empty($_content)) - $_content = self::purify($_content); - - // By default the editor start expanded - $expanded = isset($_options['toolbar_expanded']) ? - $_options['toolbar_expanded'] == 'true' : true; - - //Get the height in pixels from the pixels parameter - $pxheight = (strpos('px', $_height) === false) ? - (empty($_height) ? 400 : $_height) : str_replace('px', '', $_height); - - // User preferences - $font = $GLOBALS['egw_info']['user']['preferences']['common']['rte_font']; - $font_size = egw_ckeditor_config::font_size_from_prefs(); - $font_span = ''; - if (empty($font) && empty($font_size)) $font_span = ''; - - // we need to enable double encoding here, as ckEditor has to undo one level of encoding - // otherwise < and > chars eg. from html markup entered in regular (not source) input, will turn into html! - //error_log(__METHOD__.__LINE__.' '.self::$user_agent.','.self::$ua_version); - return self::textarea($_name,$_content,'id="'.htmlspecialchars($_name).'"',true). // true = double encoding -' - -'; - } - - /** - * this static function is a wrapper for tinymce to create some reuseable layouts - * - * Please note: if you did not run init_tinymce already you this static function need to be called before the call to phpgw_header() !!! - * - * @param string $_name name and id of the input-field - * @param string $_mode display mode of the tinymce editor can be: simple, extended or advanced - * @param string $_content ='' of the tinymce (will be run through htmlspecialchars !!!), default '' - * @param string $_height ='400px' - * @param string $_width ='100%' - * @param boolean $_purify =true - * @param string $_border ='0px' NOT used for CKEditor - * @param mixed (boolean/string) $_focusToBody=false USED only for CKEDIOR true means yes, focus on top, you may specify TOP or BOTTOM (to focus on the end of the editor area) - * @param string $_executeJSAfterInit ='' Javascript to be executed after InstanceReady of CKEditor - * @return string the necessary html for the textarea - */ - static function fckEditorQuick($_name, $_mode, $_content='', $_height='400px', $_width='100%',$_purify=true, $_border='0px',$_focusToBody=false,$_executeJSAfterInit='') - { - if (!self::htmlarea_availible() || $_mode == 'ascii') - { - //TODO: use self::textarea - return ""; - } - else - { - return self::fckEditor($_name, $_content, $_mode, array(), $_height, $_width,'',$_purify,$_focusToBody,$_executeJSAfterInit); - } - } - - /** - * represents html's input tag - * - * @param string $name name - * @param string $value default value of the field - * @param string $type type, default ''=not specified = text - * @param string $options attributes for the tag, default ''=none - */ - static function input($name,$value='',$type='',$options='' ) - { - switch ((string)$type) - { - case 'color': - return self::inputColor($name, $value, '', $options); - case ''; - break; - default: - $type = 'type="'.htmlspecialchars($type).'"'; - } - return "\n"; - } - - static protected $default_background_images = array( - 'save' => '/save(&|\]|$)/', - 'apply' => '/apply(&|\]|$)/', - 'cancel' => '/cancel(&|\]|$)/', - 'delete' => '/delete(&|\]|$)/', - 'edit' => '/edit(&|\]|$)/', - 'next' => '/(next|continue)(&|\]|$)/', - 'finish' => '/finish(&|\]|$)/', - 'back' => '/(back|previous)(&|\]|$)/', - 'copy' => '/copy(&|\]|$)/', - 'more' => '/more(&|\]|$)/', - 'check' => '/(yes|check)(&|\]|$)/', - 'cancelled' => '/no(&|\]|$)/', - 'ok' => '/ok(&|\]|$)/', - 'close' => '/close(&|\]|$)/', - 'add' => '/(add(&|\]|$)|create)/', // customfields use create* - ); - - static protected $default_classes = array( - 'et2_button_cancel' => '/cancel(&|\]|$)/', // yellow - 'et2_button_question' => '/(yes|no)(&|\]|$)/', // yellow - 'et2_button_delete' => '/delete(&|\]|$)/' // red - ); - - /** - * represents html's button (input type submit or input type button or image) - * - * @param string $name name - * @param string $label label of the button - * @param string $onClick javascript to call, when button is clicked - * @param boolean $no_lang NOT running the label through lang(), default false=use lang() - * @param string $options attributes for the tag, default ''=none - * @param string $image to show instead of the label, default ''=none - * @param string $app app to search the image in - * @param string $buttontype which type of html button (button|submit), default ='submit' - * @return string html - */ - static function submit_button($name,$label,$onClick='',$no_lang=false,$options='',$image='',$app='phpgwapi', $buttontype='submit') - { - // workaround for idots and IE button problem (wrong cursor-image) - if (self::$user_agent == 'msie') - { - $options .= ' style="cursor: pointer;"'; - } - // add et2_classes to "old" buttons - $classes = array('et2_button'); - - if ($image != '') - { - $image = str_replace(array('.gif','.GIF','.png','.PNG'),'',$image); - - if (!($path = common::image($app,$image))) - { - $path = $image; // name may already contain absolut path - } - $image = ' src="'.$path.'"'; - $classes[] = 'image_button'; - } - if (!$no_lang) - { - $label = lang($label); - } - if (($accesskey = @strstr($label,'&')) && $accesskey[1] != ' ' && - (($pos = strpos($accesskey,';')) === false || $pos > 5)) - { - $label_u = str_replace('&'.$accesskey[1],''.$accesskey[1].'',$label); - $label = str_replace('&','',$label); - $options .= ' accesskey="'.$accesskey[1].'" '.$options; - } - else - { - $accesskey = ''; - $label_u = $label; - } - if ($onClick) $options .= ' onclick="'.str_replace('"','\\"',$onClick).'"'; - - // add default background-image to get et2 like buttons - foreach(self::$default_background_images as $img => $reg_exp) - { - if (preg_match($reg_exp, $name) && ($url = common::image($GLOBALS['egw_info']['flags']['currentapp'], $img))) - { - $options .= ' style="background-image: url('.$url.');"'; - $classes[] = 'et2_button_with_image'; - break; - } - } - // add default class for cancel, delete or yes/no buttons - foreach(self::$default_classes as $class => $reg_exp) - { - if (preg_match($reg_exp, $name)) - { - $classes[] = $class; - break; - } - } - if (strpos($options, 'class="') !== false) - { - $options = str_replace('class="', 'class="'.implode(' ', $classes).' ', $options); - } - else - { - $options .= ' class="'.implode(' ', $classes).'"'; - } - - return ''; - } - - /** - * creates an absolut link + the query / get-variables - * - * Example link('/index.php?menuaction=infolog.uiinfolog.get_list',array('info_id' => 123)) - * gives 'http://domain/phpgw-path/index.php?menuaction=infolog.uiinfolog.get_list&info_id=123' - * - * @param string $_url egw-relative link, may include query / get-vars - * @param array|string $vars query or array ('name' => 'value', ...) with query - * @return string absolut link already run through $phpgw->link - */ - static function link($_url,$vars='') - { - //echo "

html::link(url='$url',vars='"; print_r($vars); echo "')

\n"; - if (!is_array($vars)) - { - parse_str($vars,$vars); - } - list($url,$v) = explode('?', $_url); // url may contain additional vars - if ($v) - { - parse_str($v,$v); - $vars += $v; - } - return egw::link($url,$vars); - } - - /** - * represents html checkbox - * - * @param string $name name - * @param boolean $checked box checked on display - * @param string $value value the var should be set to, default 'True' - * @param string $options attributes for the tag, default ''=none - * @return string html - */ - static function checkbox($name,$checked=false,$value='True',$options='') - { - return '\n"; - } - - /** - * represents a html form - * - * @param string $content of the form, if '' only the opening tag gets returned - * @param array $hidden_vars array with name-value pairs for hidden input fields - * @param string $_url eGW relative URL, will be run through the link function, if empty the current url is used - * @param string|array $url_vars parameters for the URL, send to link static function too - * @param string $name name of the form, defaul ''=none - * @param string $options attributes for the tag, default ''=none - * @param string $method method of the form, default 'POST' - * @return string html - */ - static function form($content,$hidden_vars,$_url,$url_vars='',$name='',$options='',$method='POST') - { - $url = $_url ? self::link($_url, $url_vars) : $_SERVER['PHP_SELF'].'?'.$_SERVER['QUERY_STRING']; - $html = "
\n"; - $html .= self::input_hidden($hidden_vars); - - if ($content) - { - $html .= $content; - $html .= "
\n"; - } - return $html; - } - - /** - * represents a html form with one button - * - * @param string $name name of the button - * @param string $label label of the button - * @param array $hidden_vars array with name-value pairs for hidden input fields - * @param string $url eGW relative URL, will be run through the link function - * @param string|array $url_vars parameters for the URL, send to link static function too - * @param string $options attributes for the tag, default ''=none - * @param string $form_name name of the form, defaul ''=none - * @param string $method method of the form, default 'POST' - * @return string html - */ - static function form_1button($name,$label,$hidden_vars,$url,$url_vars='',$form_name='',$method='POST') - { - return self::form(self::submit_button($name,$label),$hidden_vars,$url,$url_vars,$form_name,' style="display: inline-block"',$method); - } - - const THEAD = 1; - const TFOOT = 2; - const TBODY = 3; - static $part2tag = array( - self::THEAD => 'thead', - self::TFOOT => 'tfoot', - self::TBODY => 'tbody', - ); - - /** - * creates table from array of rows - * - * abstracts the html stuff for the table creation - * Example: $rows = array ( - * 'h1' => array( // optional header row(s) - * ), - * 'f1' => array( // optional footer row(s) - * ), - * '1' => array( - * 1 => 'cell1', '.1' => 'colspan=3', - * 2 => 'cell2', - * 3 => 'cell3', '.3' => 'width="10%"' - * ),'.1' => 'BGCOLOR="#0000FF"' ); - * table($rows,'width="100%"') = '
cell1cell2cell3
' - * - * @param array $rows with rows, each row is an array of the cols - * @param string $options options for the table-tag - * @param boolean $no_table_tr dont return the table- and outmost tr-tabs, default false=return table+tr - * @return string with html-code of the table - */ - static function table($rows,$options = '',$no_table_tr=False) - { - $html = $no_table_tr ? '' : "\n"; - - $part = 0; - foreach($rows as $key => $row) - { - if (!is_array($row)) - { - continue; // parameter - } - // get the current part from the optional 'h' or 'f' prefix of the key - $p = $key[0] == 'h' ? html::THEAD : ($key[0] == 'f' ? html::TFOOT : html::TBODY); - if ($part < $p && ($part || $p < self::TBODY)) // add only allowed and neccessary transitions - { - if ($part) $html .= '\n"; - $html .= '<'.self::$part2tag[$part=$p].">\n"; - } - $html .= $no_table_tr && $key == 1 ? '' : "\t\n"; - - foreach($row as $key => $cell) - { - if ($key[0] == '.') - { - continue; // parameter - } - $table_pos = strpos($cell,'$cell\n"; - } - } - $html .= "\t\n"; - } - if (!is_array($rows)) - { - echo "

".function_backtrace()."

\n"; - } - if ($part) // close current part - { - $html .= "\n"; - } - $html .= "
\n"; - - if ($no_table_tr) - { - $html = substr($html,0,-16); - } - return $html; - } - - /** - * changes a selectbox to submit the form if it gets changed, to be used with the sbox-class - * - * @param string $sbox html with the select-box - * @param boolean $no_script if true generate a submit-button if javascript is off - * @return string html - */ - static function sbox_submit( $sbox,$no_script=false ) - { - $html = str_replace('').''; - } - return $html; - } - - /** - * html-widget showing progessbar with a view div's (html4 only, textual percentage otherwise) - * - * @param mixed $_percent percent-value, gets casted to int - * @param string $_title title for the progressbar, default ''=the percentage itself - * @param string $options attributes for the outmost div (may include onclick="...") - * @param string $width width, default 30px - * @param string $color color, default '#D00000' (dark red) - * @param string $height height, default 5px - * @return string html - */ - static function progressbar($_percent, $_title='',$options='',$width='',$color='',$height='' ) - { - $percent = (int)$_percent; - if (!$width) $width = '30px'; - if (!$height)$height= '5px'; - if (!$color) $color = '#D00000'; - $title = $_title ? self::htmlspecialchars($_title) : $percent.'%'; - - if (self::$netscape4) - { - return $title; - } - return '
'.$title.'
'."\n\t". - '
'."\n
\n"; - } - - /** - * representates a html img tag, output a picture - * - * If the name ends with a '%' and the rest is numeric, a progressionbar is shown instead of an image. - * The vfs:/ pseudo protocoll allows to access images in the vfs, eg. vfs:/home/ralf/me.png - * Instead of a name you specify an array with get-vars, it is passed to eGW's link function. - * This way session-information gets passed, eg. $name=array('menuaction'=>'myapp.class.image','id'=>123). - * - * @param string $app app-name to search the image - * @param string|array $name image-name or URL (incl. vfs:/) or array with get-vars - * @param string $title tooltip, default '' = none - * @param string $options further options for the tag, default '' = none - * @return string the html - */ - static function image( $app,$name,$title='',$options='' ) - { - if (is_array($name)) // menuaction and other get-vars - { - $name = $GLOBALS['egw']->link('/index.php',$name); - } - if (substr($name,0,5) == 'vfs:/') // vfs pseudo protocoll - { - $name = egw::link(egw_vfs::download_url(substr($name,4))); - } - if ($name[0] == '/' || substr($name,0,7) == 'http://' || substr($name,0,8) == 'https://' || stripos($name,'etemplate/thumbnail.php') ) - { - if (!($name[0] == '/' || substr($name,0,7) == 'http://' || substr($name,0,8) == 'https://')) $name = '/'.$name; - $url = $name; - } - else // no URL, so try searching the image - { - $name = str_replace(array('.gif','.GIF','.png','.PNG'),'',$name); - - if (!($url = $GLOBALS['egw']->common->image($app,$name))) - { - $url = $name; // name may already contain absolut path - } - if($GLOBALS['egw_info']['server']['webserver_url']) - { - list(,$path) = explode($GLOBALS['egw_info']['server']['webserver_url'],$url); - - if (!is_null($path)) $path = EGW_SERVER_ROOT.$path; - } - else - { - $path = EGW_SERVER_ROOT.$url; - } - - if (is_null($path) || (!@is_readable($path) && stripos($path,'webdav.php')===false)) - { - // if the image-name is a percentage, use a progressbar - if (substr($name,-1) == '%' && is_numeric($percent = substr($name,0,-1))) - { - return self::progressbar($percent,$title); - } - return $title; - } - } - if ($title) - { - $options .= ' '.self::$prefered_img_title.'="'.self::htmlspecialchars($title).'"'; - } - - // This block makes pngfix.js useless, adding a check on disable_pngfix to have pngfix.js do its thing - if (self::$user_agent == 'msie' && self::$ua_version < 7.0 && substr($url,-4) == '.png' && ($GLOBALS['egw_info']['user']['preferences']['common']['disable_pngfix'] || !isset($GLOBALS['egw_info']['user']['preferences']['common']['disable_pngfix']))) - { - $extra_styles = "display: inline-block; filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(src='$url',sizingMethod='image'); width: 1px; height: 1px;"; - if (false!==strpos($options,'style="')) - { - $options = str_replace('style="','style="'.$extra_styles, $options); - } - else - { - $options .= ' style="'.$extra_styles.'"'; - } - return ""; - } - return ""; - } - - /** - * representates a html link - * - * @param string $content of the link, if '' only the opening tag gets returned - * @param string $url eGW relative URL, will be run through the link function - * @param string|array $vars parameters for the URL, send to link static function too - * @param string $options attributes for the tag, default ''=none - * @return string the html - */ - static function a_href( $content,$url,$vars='',$options='') - { - if (is_array($url)) - { - $vars = $url; - $url = '/index.php'; - } - elseif (strpos($url,'/')===false && - count(explode('.',$url)) >= 3 && - !(strpos($url,'mailto:')!==false || - strpos($url,'://')!==false || - strpos($url,'javascript:')!==false)) - { - $url = "/index.php?menuaction=$url"; - } - if ($url[0] == '/') // link relative to eGW - { - $url = self::link($url,$vars); - } - //echo "

html::a_href('".self::htmlspecialchars($content)."','$url',".print_r($vars,True).") = ".self::link($url,$vars)."

"; - return ''.$content.''; - } - - /** - * representates a b tab (bold) - * - * @param string $content of the link, if '' only the opening tag gets returned - * @return string the html - */ - static function bold($content) - { - return ''.$content.''; - } - - /** - * representates a i tab (bold) - * - * @param string $content of the link, if '' only the opening tag gets returned - * @return string the html - */ - static function italic($content) - { - return ''.$content.''; - } - - /** - * representates a hr tag (horizontal rule) - * - * @param string $width default ''=none given - * @param string $options attributes for the tag, default ''=none - * @return string the html - */ - static function hr($width='',$options='') - { - if ($width) $options .= " width=\"$width\""; - - return "
\n"; - } - - /** - * formats option-string for most of the above functions - * - * Example: formatOptions('100%,,1','width,height,border') = ' width="100%" border="1"' - * - * @param mixed $options String (or Array) with option-values eg. '100%,,1' - * @param mixed $names String (or Array) with the option-names eg. 'WIDTH,HEIGHT,BORDER' - * @return string with options/attributes - */ - static function formatOptions($options,$names) - { - if (!is_array($options)) $options = explode(',',$options); - if (!is_array($names)) $names = explode(',',$names); - - foreach($options as $n => $val) - { - if ($val != '' && $names[$n] != '') - { - $html .= ' '.strtolower($names[$n]).'="'.$val.'"'; - } - } - return $html; - } - /** * returns simple stylesheet (incl. " : ''; - } + self::$user_agent = Api\Header\UserAgent::type(); + self::$ua_version = Api\Header\UserAgent::version(); + self::$ua_mobile = Api\Header\UserAgent::mobile(); + self::$netscape4 = self::$user_agent == 'mozilla' && self::$ua_version < 5; + self::$prefered_img_title = self::$netscape4 ? 'alt' : 'title'; + //error_log("HTTP_USER_AGENT='$_SERVER[HTTP_USER_AGENT]', UserAgent: '".self::$user_agent."', Version: '".self::$ua_version."', isMobile=".array2string(self::$ua_mobile).", img_title: '".self::$prefered_img_title."'"); - /** - * html label tag - * - * @param string $content the label - * @param string $id for the for attribute, default ''=none - * @param string $accesskey accesskey, default ''=none - * @param string $options attributes for the tag, default ''=none - * @return string the html - */ - static function label($content,$id='',$accesskey='',$options='') - { - if ($id != '') - { - $id = " for=\"$id\""; - } - if ($accesskey != '') - { - $accesskey = " accesskey=\"$accesskey\""; - } - return "$content"; - } - - /** - * html fieldset, eg. groups a group of radiobuttons - * - * @param string $content the content - * @param string $legend legend / label of the fieldset, default ''=none - * @param string $options attributes for the tag, default ''=none - * @return string the html - */ - static function fieldset($content,$legend='',$options='') - { - $html = "
".($legend ? ''.self::htmlspecialchars($legend).'' : '')."\n"; - - if ($content) - { - $html .= $content; - $html .= "\n
\n"; - } - return $html; - } - - /** - * tree widget using dhtmlXtree - * - * Code inspired by Lars's Felamimail uiwidgets::createFolderTree() - * - * @author Lars Kneschke original code in felamimail - * @param array $_folders array of folders: pairs path => node (string label or array with keys: label, (optional) image, (optional) title, (optional) checked) - * @param string $_selected path of selected folder - * @param mixed $_topFolder =false node of topFolder or false for none - * @param string $_onNodeSelect ='alert' js static function to call if node gets selected - * @param string $tree ='foldertree' id of the div and name of the variable containing the tree object - * @param string $_divClass ='' css class of the div - * @param string $_leafImage ='' default image of a leaf-node, ''=default of foldertree, set it eg. 'folderClosed.gif' to show leafs as folders - * @param boolean|string $_onCheckHandler =false string with handler-name to display a checkbox for each folder, or false (default), 'null' switches checkboxes on without an handler! - * @param string $delimiter ='/' path-delimiter, default / - * @param string $folderImageDir =null string path to the tree menu images, null uses default path - * @param string|array $autoLoading =null EGw relative path or array with get parameter, both send through egw::link - * @param string $dataMode ='JSON' data type for autoloading: XML, JSON, CSV - * @param boolean $dragndrop =false true to enable drag-n-drop (must be before autoloading get enabled!) - * - * @return string the html code, to be added into the template - */ - static function tree($_folders,$_selected,$_topFolder=false,$_onNodeSelect="null",$tree='foldertree',$_divClass='', - $_leafImage='',$_onCheckHandler=false,$delimiter='/',$folderImageDir=null,$autoLoading=null,$dataMode='JSON', - $dragndrop=false) - { - $webserver_url = $GLOBALS['egw_info']['server']['webserver_url']; - if (empty($folderImageDir)) - { - $folderImageDir = $webserver_url.'/phpgwapi/templates/default/images'; - } - // check if we have template-set specific image path - $image_path = $folderImageDir; - if ($webserver_url && $webserver_url != '/') - { - list(,$image_path) = explode($webserver_url, $image_path, 2); - } - $templated_path = strtr($image_path, array( - '/phpgwapi/templates/default' => $GLOBALS['egw']->framework->template_dir, - '/default/' => '/'.$GLOBALS['egw']->framework->template.'/', - )); - if (file_exists(EGW_SERVER_ROOT.$templated_path.'/dhtmlxtree')) - { - $folderImageDir = ($webserver_url != '/' ? $webserver_url : '').$templated_path; - //error_log(__METHOD__."() setting templated image-path: $folderImageDir"); - } - - static $tree_initialised=false; - if (!$tree_initialised) - { - egw_framework::includeCSS('/phpgwapi/js/dhtmlxtree/codebase/dhtmlxtree.css'); - egw_framework::validate_file('/phpgwapi/js/dhtmlxtree/codebase/dhtmlxcommon.js'); - egw_framework::validate_file('/phpgwapi/js/dhtmlxtree/sources/dhtmlxtree.js'); - if ($autoLoading && $dataMode != 'XML') egw_framework::validate_file('/phpgwapi/js/dhtmlxtree/sources/ext/dhtmlxtree_json.js'); - $tree_initialised = true; - if (!$_folders && !$autoLoading) return null; - } - $html = self::div("\n",'id="'.$tree.'"',$_divClass).$html; - $html .= "\n"; - } - } - - $top = 0; - if ($_topFolder) - { - $top = '--topfolder--'; - $topImage = ''; - if (is_array($_topFolder)) - { - $label = $_topFolder['label']; - if (isset($_topFolder['image'])) - { - $topImage = $_topFolder['image']; - } - } - else - { - $label = $_topFolder; - } - $html .= "\n$tree.insertNewItem(0,'$top','".addslashes($label)."',$_onNodeSelect,'$topImage','$topImage','$topImage','CHILD,TOP');\n"; - - if (is_array($_topFolder) && isset($_topFolder['title'])) - { - $html .= "$tree.setItemText('$top','".addslashes($label)."','".addslashes($_topFolder['title'])."');\n"; - } - } - if (is_string($_folders)) - { - switch($dataMode) - { - case 'JSON': - $html .= "$tree.loadJSONObject($_folders);\n"; break; - case 'XML': - $html .= "$tree.loadXMLString('$_folders');\n"; break; - } - } - else - { - // evtl. remove leading delimiter - if ($_selected[0] == $delimiter) $_selected = substr($_selected,1); - - $n = 0; - foreach($_folders as $path => $data) - { - if (!is_array($data)) - { - $data = array('label' => $data); - } - $image1 = $image2 = $image3 = '0'; - - // if _leafImage given, set it only for leaves, not for folders containing children - if ($_leafImage) - { - $image1 = $image2 = $image3 = "'".$_leafImage."'"; - if (($next_item = array_slice($_folders, $n+1, 1, true))) - { - list($next_path) = each($next_item); - if (substr($next_path,0,strlen($path)+1) == $path.'/') - { - $image1 = $image2 = $image3 = '0'; - } - } - } - if (isset($data['image'])) - { - $image1 = $image2 = $image3 = "'".$data['image']."'"; - } - // evtl. remove leading delimiter - if ($path[0] == $delimiter) $path = substr($path,1); - $folderParts = explode($delimiter,$path); - - //get rightmost folderpart - $label = array_pop($folderParts); - if (isset($data['label'])) $label = $data['label']; - - // the rest of the array is the name of the parent - $parentName = implode((array)$folderParts,$delimiter); - if(empty($parentName)) $parentName = $top; - - $entryOptions = !isset($data['child']) || $data['child'] ? 'CHILD' : ''; - if ($_onCheckHandler && $_selected) // check selected items on multi selection - { - if (!is_array($_selected)) $_selected = explode(',',$_selected); - if (array_search("$path",$_selected)!==false) $entryOptions .= ',CHECKED'; - //echo "

path=$path, _selected=".print_r($_selected,true).": $entryOptions

\n"; - } - // highlight current item - elseif ((string)$_selected === (string)$path) - { - $entryOptions .= ',SELECT'; - } - $html .= "$tree.insertNewItem('".addslashes($parentName)."','".addslashes($path)."','".addslashes($label). - "',$_onNodeSelect,$image1,$image2,$image3,'$entryOptions');\n"; - if (isset($data['title'])) - { - $html .= "$tree.setItemText('".addslashes($path)."','".addslashes($label)."','".addslashes($data['title'])."');\n"; - } - ++$n; - } - } - $html .= "$tree.closeAllItems(0);\n"; - if ($_selected) - { - foreach(is_array($_selected)?$_selected:array($_selected) as $path) - { - $html .= "$tree.openItem('".addslashes($path)."');\n"; - } - } - else - { - $html .= "$tree.openItem('$top');\n"; - } - $html .= "});"; - $html .= "\n"; - - return $html; - } - - /** - * Runs HTMLPurifier over supplied html to remove malicious code - * - * @param string $html - * @param array|string $config =null - config to influence the behavior of current purifying engine - * @param array|string $spec =null - spec to influence the behavior of current purifying engine - * The $spec argument can be used to disallow an otherwise legal attribute for an element, - * or to restrict the attribute's values - * @param boolean $_force =null - force the config passed to be used without merging to the default - */ - static function purify($html,$config=null,$spec=array(),$_force=false) - { - $defaultConfig = array('valid_xhtml'=>1,'safe'=>1); - - if (empty($html)) return $html; // no need to process further - if (!empty($config) && is_string($config)) - { - //error_log(__METHOD__.__LINE__.$config); - $config = json_decode($config,true); - if (is_null($config)) error_log(__METHOD__.__LINE__." decoding of config failed; standard will be applied"); - } - - // User preferences - $font = $GLOBALS['egw_info']['user']['preferences']['common']['rte_font']; - $font_size = $GLOBALS['egw_info']['user']['preferences']['common']['rte_font_size']; - - // Check for "blank" = just user preference span - for some reason we can't match on the entity, so approximate - $regex = '#^.?$#us'; - if(preg_match($regex,$html)) - { - return ''; - } - $htmLawed = new egw_htmLawed(); - if (is_array($config) && $_force===false) $config = array_merge($defaultConfig, $config); - if (empty($config)) $config = $defaultConfig; - //error_log(__METHOD__.__LINE__.array2string($config)); - return $htmLawed->egw_htmLawed($html,$config,$spec); - } - - /** - * Output content headers for user-content, mitigating risk of javascript or html - * - * Mitigate risk of serving javascript or css from our domain, - * which will get around same origin policy and CSP! - * - * Mitigate risk of html downloads by using CSP or force download for IE - * - * @param resource|string &$content content might be changed by this call - * @param string $path filename or path for content-disposition header - * @param string &$mime ='' mimetype or '' (default) to detect it from filename, using mime_magic::filename2mime() - * on return used, maybe changed, mime-type - * @param int $length =0 content length, default 0 = skip that header - * on return changed size - * @param boolean $nocache =true send headers to disallow browser/proxies to cache the download - * @param boolean $force_download =true send content-disposition attachment header - * @param boolean $no_content_type =false do not send actual content-type and content-length header, just content-disposition - */ - public static function safe_content_header(&$content, $path, &$mime='', &$length=0, $nocache=true, $force_download=true, $no_content_type=false) - { - // change old/aliased mime-types to new one, eg. image/pdf to application/pdf - $mime = mime_magic::fix_mime_type($mime); - - // mitigate risk of serving javascript or css via webdav from our domain, - // which will get around same origin policy and CSP - list($type, $subtype) = explode('/', strtolower($mime)); - if (!$force_download && in_array($type, array('application', 'text')) && - in_array($subtype, array('javascript', 'x-javascript', 'ecmascript', 'jscript', 'vbscript', 'css'))) - { - // unfortunatly only Chrome and IE >= 8 allow to switch content-sniffing off with X-Content-Type-Options: nosniff - if (html::$user_agent == 'chrome' || html::$user_agent == 'msie' && html::$ua_version >= 8) - { - $mime = 'text/plain'; - header('X-Content-Type-Options: nosniff'); // stop IE & Chrome from content-type sniffing - } - // for the rest we change mime-type to text/html and let code below handle it safely - // this stops Safari and Firefox from using it as src attribute in a script tag - // but only for "real" browsers, we dont want to modify data for our WebDAV clients - elseif (isset($_SERVER['HTTP_REFERER'])) - { - $mime = 'text/html'; - if (is_resource($content)) - { - $data = fread($content, $length); - fclose($content); - $content =& $data; - unset($data); - } - $content = '
'.$content;
-				$length += 5;
-			}
-		}
-		// mitigate risk of html downloads by using CSP or force download for IE
-		if (!$force_download && in_array($mime, array('text/html', 'application/xhtml+xml')))
-		{
-			// use CSP only for current user-agents/versions I was able to positivly test
-			if (html::$user_agent == 'chrome' && html::$ua_version >= 24 ||
-				// mobile FF 24 on Android does NOT honor CSP!
-				html::$user_agent == 'firefox' && !html::$ua_mobile && html::$ua_version >= 24 ||
-				html::$user_agent == 'safari' && !html::$ua_mobile && html::$ua_version >= 536 ||	// OS X
-				html::$user_agent == 'safari' && html::$ua_mobile && html::$ua_version >= 9537)	// iOS 7
-			{
-				$csp = "script-src 'none'";	// forbid to execute any javascript
-				header("Content-Security-Policy: $csp");
-				header("X-Webkit-CSP: $csp");	// Chrome: <= 24, Safari incl. iOS
-				//header("X-Content-Security-Policy: $csp");	// FF <= 22
-				//error_log(__METHOD__."('$options[path]') ".html::$user_agent.'/'.html::$ua_version.(html::$ua_mobile?'/mobile':'').": using Content-Security-Policy: $csp");
-			}
-			else	// everything else get's a Content-dispostion: attachment, to be on save side
-			{
-				//error_log(__METHOD__."('$options[path]') ".html::$user_agent.'/'.html::$ua_version.(html::$ua_mobile?'/mobile':'').": using Content-disposition: attachment");
-				$force_download = true;
-			}
-		}
-		if ($no_content_type)
-		{
-			if ($force_download) self::content_disposition_header(egw_vfs::basename($path), $force_download);
-		}
-		else
-		{
-			self::content_header(egw_vfs::basename($path), $mime, $length, $nocache, $force_download);
-		}
-	}
-
-	/**
-	 * Output content-type headers for file downloads
-	 *
-	 * This function should only be used for non-user supplied content!
-	 * For uploaded files, mail attachmentes, etc, you have to use safe_content_header!
-	 *
-	 * @author Miles Lott originally in browser class
-	 * @param string $fn filename
-	 * @param string $mime ='' mimetype or '' (default) to detect it from filename, using mime_magic::filename2mime()
-	 * @param int $length =0 content length, default 0 = skip that header
-	 * @param boolean $nocache =true send headers to disallow browser/proxies to cache the download
-	 * @param boolean $forceDownload =true send headers to handle as attachment/download
-	 */
-	public static function content_header($fn,$mime='',$length=0,$nocache=True,$forceDownload=true)
-	{
-		// if no mime-type is given or it's the default binary-type, guess it from the extension
-		if(empty($mime) || $mime == 'application/octet-stream')
-		{
-			$mime = mime_magic::filename2mime($fn);
-		}
-		if($fn)
-		{
-			// Show this for all
-			self::content_disposition_header($fn,$forceDownload);
-			header('Content-type: '.$mime);
-
-			if($length)
-			{
-				header('Content-length: '.$length);
-			}
-
-			if($nocache)
-			{
-				header('Pragma: no-cache');
-				header('Pragma: public');
-				header('Expires: 0');
-				header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
-			}
-		}
-	}
-
-	/**
-	 * Output content-disposition header for file downloads
-	 *
-	 * @param string $fn filename
-	 * @param boolean $forceDownload =true send headers to handle as attachment/download
-	 */
-	public static function content_disposition_header($fn,$forceDownload=true)
-	{
-		if ($forceDownload)
-		{
-			$attachment = ' attachment;';
-		}
-		else
-		{
-			$attachment = ' inline;';
-		}
-
-		header('Content-disposition:'.$attachment.' filename="'.translation::to_ascii($fn).'"; filename*=utf-8\'\''.rawurlencode($fn));
-	}
-
-	/**
-	 * split html by PRE tag, return array with all content pre-sections isolated in array elements
-	 * @author Leithoff, Klaus
-	 * @param string html
-	 * @return mixed array of parts or unaffected html
-	 */
-	static function splithtmlByPRE($html)
-	{
-		$searchFor = '
',$pos);
-			$length = $endofpre-$pos+6;
-			$html2ret[] = substr($html,$pos,$length);
-			$searchFor = '
 stuff  of the html passed in
-	 * and remove it from input
-	 * @author Leithoff, Klaus
-	 * @param string html
-	 * @return string the style css
-	 */
-	static function getStyles(&$html)
-	{
-		$ct=0;
-		$newStyle = null;
-		if (stripos($html,'(.+)#isU', $html, $newStyle);
-		if ($ct>0)
-		{
-			//error_log(__METHOD__.__LINE__.array2string($newStyle[0]));
-			$style2buffer = implode('',$newStyle[0]);
-			// only replace what we have found, we use it here, as we use the same routine in translation::replaceTagsCompletley
-			// no need to do the extra routine
-			$html = str_ireplace($newStyle[0],'',$html);
-		}
-		if ($style2buffer)
-		{
-			//error_log(__METHOD__.__LINE__.array2string($style2buffer));
-			$test = json_encode($style2buffer);
-			//error_log(__METHOD__.__LINE__.'#'.$test.'# ->'.strlen($style2buffer).' Error:'.json_last_error());
-			//if (json_last_error() != JSON_ERROR_NONE && strlen($style2buffer)>0)
-			if ($test=="null" && strlen($style2buffer)>0)
-			{
-				// this should not be needed, unless something fails with charset detection/ wrong charset passed
-				error_log(__METHOD__.__LINE__.' Found Invalid sequence for utf-8 in CSS:'.$style2buffer.' Carset Detected:'.translation::detect_encoding($style2buffer));
-				$style2buffer = utf8_encode($style2buffer);
-			}
-		}
-		$style .= $style2buffer;
-		// clean out comments and stuff
-		$search = array(
-			'@url\(http:\/\/[^\)].*?\)@si',  // url calls e.g. in style definitions
-//			'@@',   // Strip multi-line comments including CDATA
-//			'@ in stylesheet are outdated, and ck-editor does not understand it, we remove it
-		$css_no_comment = str_replace(array(':',''),array(': ','',''),$css);
-		//error_log(__METHOD__.__LINE__.$css);
-		// we already removed what we have found, above, as we used pretty much the same routine as in translation::replaceTagsCompletley
-		// no need to do the extra routine
-		// TODO: we may have to strip urls and maybe comments and ifs
-		//if (stripos($html,'style')!==false) translation::replaceTagsCompletley($html,'style'); // clean out empty or pagewide style definitions / left over tags
-		return $css_no_comment;
+		self::$charset = Api\Translation::charset();
 	}
 }
 html::_init_static();
diff --git a/phpgwapi/inc/class.translation.inc.php b/phpgwapi/inc/class.translation.inc.php
index 42dc1068c1..bad8ecca4a 100644
--- a/phpgwapi/inc/class.translation.inc.php
+++ b/phpgwapi/inc/class.translation.inc.php
@@ -279,7 +279,7 @@ class translation extends Api\Translation
 		{
 			if (stripos($_html,'
')!==false)
 			{
-				$contentArr = html::splithtmlByPRE($_html);
+				$contentArr = self::splithtmlByPRE($_html);
 				foreach ($contentArr as $k =>&$elem)
 				{
 					if (stripos($elem,'
')===false)
@@ -349,7 +349,7 @@ class translation extends Api\Translation
 		{
 			if (stripos($_html,'
')!==false)
 			{
-				$contentArr = html::splithtmlByPRE($_html);
+				$contentArr = self::splithtmlByPRE($_html);
 				foreach ($contentArr as $k =>&$elem)
 				{
 					if (stripos($elem,'
')===false)
@@ -458,4 +458,43 @@ class translation extends Api\Translation
 			return implode("\r\n",$asciiTextBuff);
 		}
 	}
+
+	/**
+	 * split html by PRE tag, return array with all content pre-sections isolated in array elements
+	 * @author Leithoff, Klaus
+	 * @param string html
+	 * @return mixed array of parts or unaffected html
+	 */
+	static function splithtmlByPRE($html)
+	{
+		$searchFor = '
',$pos);
+			$length = $endofpre-$pos+6;
+			$html2ret[] = substr($html,$pos,$length);
+			$searchFor = '
_>+Z^fq|Pr
z2P6VAgMlTf;H2m3sa|*2e%?0kyKmGYj$0A4PES~Rb(cnS0KY|_u4fZx+k*uTG6_{G
z2>}i|66Hd3a;t;`OtxN5)aGDd@}IPNt1ZvI1_Mp48N2M6TWTCutk!0-bP7   *
-\**************************************************************************/
-
-htmlArea v3.0 - Copyright (c) 2002-2004 interactivetools.com, inc.
-This copyright notice MUST stay intact for use (see license.txt).
-
-Portions (c) dynarch.com, 2003-2004
-
-$Id$
--->
-Select Color
-
-
-
-
-
-
- - - - - - -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -