1<?php
2/**
3 * EGroupware API - framework baseclass
4 *
5 * @link http://www.egroupware.org
6 * @author Ralf Becker <RalfBecker-AT-outdoor-training.de> rewrite in 12/2006
7 * @author Pim Snel <pim@lingewoud.nl> author of the idots template set
8 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
9 * @package api
10 * @subpackage framework
11 * @access public
12 */
13
14namespace EGroupware\Api;
15
16use EGroupware\Api\Header\ContentSecurityPolicy;
17
18/**
19 * Framework: virtual base class for all template sets
20 *
21 * This class creates / renders the eGW framework:
22 *  a) Html header
23 *  b) navbar
24 *  c) sidebox menu
25 *  d) main application area
26 *  e) footer
27 * It replaces several methods in the common class and the diverse templates.
28 *
29 * Existing apps either set $GLOBALS['egw_info']['flags']['noheader'] and call common::egw_header() and
30 * (if $GLOBALS['egw_info']['flags']['nonavbar'] is true) parse_navbar() or it's done by the header.inc.php include.
31 * The app's hook_sidebox then calls the public function display_sidebox().
32 * And the app calls common::egw_footer().
33 *
34 * This are the authors (and their copyrights) of the original egw_header, egw_footer methods of the common class:
35 * This file written by Dan Kuykendall <seek3r@phpgroupware.org>
36 * and Joseph Engo <jengo@phpgroupware.org>
37 * and Mark Peters <skeeter@phpgroupware.org>
38 * and Lars Kneschke <lkneschke@linux-at-work.de>
39 * Copyright (C) 2000, 2001 Dan Kuykendall
40 * Copyright (C) 2003 Lars Kneschke
41 */
42abstract class Framework extends Framework\Extra
43{
44	/**
45	 * Name of the template set, eg. 'idots'
46	 *
47	 * @var string
48	 */
49	var $template;
50
51	/**
52	 * Path relative to EGW_SERVER_ROOT for the template directory
53	 *
54	 * @var string
55	 */
56	var $template_dir;
57
58	/**
59	 * Application specific template directories to try in given order for CSS
60	 *
61	 * @var string
62	 */
63	var $template_dirs = array();
64
65	/**
66	* true if $this->header() was called
67	*
68	* @var boolean
69	*/
70	static $header_done = false;
71	/**
72	* true if $this->navbar() was called
73	*
74	* @var boolean
75	*/
76	static $navbar_done = false;
77
78	/**
79	 * Constructor
80	 *
81	 * The constructor instanciates the class in $GLOBALS['egw']->framework, from where it should be used
82	 */
83	function __construct($template)
84	{
85		$this->template = $template;
86
87		if (!isset($GLOBALS['egw']->framework))
88		{
89			$GLOBALS['egw']->framework = $this;
90		}
91		$this->template_dir = '/api/templates/'.$template;
92
93		$this->template_dirs[] = $template;
94		$this->template_dirs[] = 'default';
95	}
96
97	/**
98	 * Factory method to instanciate framework object
99	 *
100	 * @return self
101	 */
102	public static function factory()
103	{
104		// we prefer Pixelegg template, if it is available
105		if (file_exists(EGW_SERVER_ROOT.'/pixelegg') &&
106			(empty($GLOBALS['egw_info']['flags']['deny_mobile']) && Header\UserAgent::mobile() ||
107			$GLOBALS['egw_info']['user']['preferences']['common']['theme'] == 'mobile' ||
108			empty($GLOBALS['egw_info']['server']['template_set'])) ||
109			// change old idots and jerryr to our standard template (pixelegg)
110			in_array($GLOBALS['egw_info']['server']['template_set'], array('idots', 'jerryr')))
111		{
112			$GLOBALS['egw_info']['server']['template_set'] = 'pixelegg';
113		}
114		// then jdots aka Stylite template
115		if (file_exists(EGW_SERVER_ROOT.'/jdots') && empty($GLOBALS['egw_info']['server']['template_set']))
116		{
117			$GLOBALS['egw_info']['server']['template_set'] = 'jdots';
118		}
119		// eg. "default" is only used for login at the moment
120		if (!class_exists($class=$GLOBALS['egw_info']['server']['template_set'].'_framework'))
121		{
122			$class = __CLASS__.'\\Minimal';
123		}
124		return new $class($GLOBALS['egw_info']['server']['template_set']);
125	}
126
127	/**
128	 * Check if we have a valid and installed EGroupware template
129	 *
130	 * Templates are installed in their own directory and contain a setup/setup.inc.php file
131	 *
132	 * @param string $template
133	 * @return boolean
134	 */
135	public static function validTemplate($template)
136	{
137		return preg_match('/^[A-Z0-9_-]+$/i', $template) &&
138			file_exists(EGW_SERVER_ROOT.'/'.$template) &&
139			file_exists($file=EGW_SERVER_ROOT.'/'.$template.'/setup/setup.inc.php') &&
140			include_once($file) && !empty($GLOBALS['egw_info']['template'][$template]);
141	}
142
143	/**
144	 * Send HTTP headers: Content-Type and Content-Security-Policy
145	 */
146	public function send_headers()
147	{
148		// add a content-type header to overwrite an existing default charset in apache (AddDefaultCharset directiv)
149		header('Content-type: text/html; charset='.Translation::charset());
150
151		Header\ContentSecurityPolicy::send();
152
153		// allow client-side to detect first load aka just logged in
154		$reload_count =& Cache::getSession(__CLASS__, 'framework-reload');
155		self::$extra['framework-reload'] = (int)(bool)$reload_count++;
156	}
157
158	/**
159	 * Constructor for static variables
160	 */
161	public static function init_static()
162	{
163		self::$js_include_mgr = new Framework\IncludeMgr(array(
164			// We need LABjs, but putting it through Framework\IncludeMgr causes it to re-load itself
165			//'/api/js/labjs/LAB.src.js',
166
167			// allways load jquery (not -ui) first
168			'/vendor/bower-asset/jquery/dist/jquery.js',
169			'/api/js/jquery/jquery.noconflict.js',
170			// always include javascript helper functions
171			'/api/js/jsapi/jsapi.js',
172			'/api/js/jsapi/egw.js',
173		));
174	}
175
176	/**
177	 * Link url generator
178	 *
179	 * @param string $url	The url the link is for
180	 * @param string|array	$extravars	Extra params to be passed to the url
181	 * @param string $link_app =null if appname or true, some templates generate a special link-handler url
182	 * @return string	The full url after processing
183	 */
184	static function link($url, $extravars = '', $link_app=null)
185	{
186		unset($link_app);	// not used by required by function signature
187		return $GLOBALS['egw']->session->link($url, $extravars);
188	}
189
190	/**
191	 * Get a full / externally usable URL from an EGroupware link
192	 *
193	 * @param string $link
194	 */
195	static function getUrl($link)
196	{
197		return Header\Http::fullUrl($link);
198	}
199
200	/**
201	 * Handles redirects under iis and apache, it does NOT return (calls exit)
202	 *
203	 * This function handles redirects under iis and apache it assumes that $phpgw->link() has already been called
204	 *
205	 * @param string $url url to redirect to
206	 * @param string $link_app =null appname to redirect for, default currentapp
207	 */
208	static function redirect($url, $link_app=null)
209	{
210		// Determines whether the current output buffer should be flushed
211		$do_flush = true;
212
213		if (Json\Response::isJSONResponse() || Json\Request::isJSONRequest())
214		{
215			Json\Response::get()->redirect($url, false, $link_app);
216
217			// check if we have a message, in which case send it along too
218			$extra = self::get_extra();
219			if ($extra['message'])
220			{
221				Json\Response::get()->apply('egw.message', $extra['message']);
222			}
223
224			// If we are in a json request, we should not flush the current output!
225			$do_flush = false;
226		}
227		else
228		{
229			$file = $line = null;
230			if (headers_sent($file,$line))
231			{
232				throw new Exception\AssertionFailed(__METHOD__."('".htmlspecialchars($url)."') can NOT redirect, output already started at $file line $line!");
233			}
234			if ($GLOBALS['egw']->framework instanceof Framework\Ajax && !empty($link_app))
235			{
236				self::set_extra('egw', 'redirect', array($url, $link_app));
237				$GLOBALS['egw']->framework->render('');
238			}
239			else
240			{
241				Header("Location: $url");
242				print("\n\n");
243			}
244		}
245
246		if ($do_flush)
247		{
248			@ob_flush(); flush();
249		}
250
251		// commit session (if existing), to fix timing problems sometimes preventing session creation ("Your session can not be verified")
252		if (isset($GLOBALS['egw']->session)) $GLOBALS['egw']->session->commit_session();
253
254		// run egw destructor now explicit, in case a (notification) email is send via Egw::on_shutdown(),
255		// as stream-wrappers used by Horde Smtp fail when PHP is already in destruction
256		$GLOBALS['egw']->__destruct();
257		exit;
258	}
259
260	/**
261	 * Redirects direct to a generated link
262	 *
263	 * @param string $url	The url the link is for
264	 * @param string|array	$extravars	Extra params to be passed to the url
265	 * @param string $link_app =null if appname or true, some templates generate a special link-handler url
266	 * @return string	The full url after processing
267	 */
268	static function redirect_link($url, $extravars='', $link_app=null)
269	{
270		self::redirect(self::link($url, $extravars), $link_app);
271	}
272
273	/**
274	 * Renders an applicaton page with the complete eGW framework (header, navigation and menu)
275	 *
276	 * This is the (new) prefered way to render a page in eGW!
277	 *
278	 * @param string $content Html of the main application area
279	 * @param string $app_header =null application header, default what's set in $GLOBALS['egw_info']['flags']['app_header']
280	 * @param string $navbar =null show the navigation, default !$GLOBALS['egw_info']['flags']['nonavbar'], false gives a typical popu
281	 *
282	 */
283	function render($content,$app_header=null,$navbar=null)
284	{
285		if (!is_null($app_header)) $GLOBALS['egw_info']['flags']['app_header'] = $app_header;
286		if (!is_null($navbar)) $GLOBALS['egw_info']['flags']['nonavbar'] = !$navbar;
287
288		echo $this->header();
289
290		if (!isset($GLOBALS['egw_info']['flags']['nonavbar']) || !$GLOBALS['egw_info']['flags']['nonavbar'])
291		{
292			echo $this->navbar();
293		}
294		echo $content;
295
296		echo $this->footer();
297	}
298
299	/**
300	 * Returns the html-header incl. the opening body tag
301	 *
302	 * @return string with Html
303	 */
304	abstract function header(array $extra=array());
305
306	/**
307	 * Returns the Html from the body-tag til the main application area (incl. opening div tag)
308	 *
309	 * If header has NOT been called, also return header content!
310	 * No need to manually call header, this allows to postpone header so navbar / sidebox can include JS or CSS.
311	 *
312	 * @return string with Html
313	 */
314	abstract function navbar();
315
316	/**
317	 * Return true if we are rendering the top-level EGroupware window
318	 *
319	 * A top-level EGroupware window has a navbar: eg. no popup and for a framed template (jdots) only frameset itself
320	 *
321	 * @return boolean $consider_navbar_not_yet_called_as_true=true
322	 * @return boolean
323	 */
324	abstract function isTop($consider_navbar_not_yet_called_as_true=true);
325
326	/**
327	 * Returns the content of one sidebox
328	 *
329	 * @param string $appname
330	 * @param string $menu_title
331	 * @param array $file
332	 * @param string $type =null 'admin', 'preferences', 'favorites', ...
333	 */
334	abstract function sidebox($appname,$menu_title,$file,$type=null);
335
336	/**
337	 * Returns the Html from the closing div of the main application area to the closing html-tag
338	 *
339	 * @return string
340	 */
341	abstract function footer();
342
343	/**
344	 * Displays the login screen
345	 *
346	 * @param string $extra_vars for login url
347	 * @param string $change_passwd =null string with message to render input fields for password change
348	 */
349	function login_screen($extra_vars, $change_passwd=null)
350	{
351		(new Framework\Login($this))->screen($extra_vars, $change_passwd);
352	}
353
354	/**
355	 * displays a login denied message
356	 */
357	function denylogin_screen()
358	{
359		(new Framework\Login($this))->deny_screen();
360	}
361
362	/**
363	 * Calculate page-generation- and session-restore times
364	 *
365	 * @return array values for keys 'page_generation_time' and 'session_restore_time', if display is an
366	 */
367	public static function get_page_generation_time()
368	{
369		$times = array(
370			'page_generation_time' => sprintf('%4.2lf', microtime(true) - $GLOBALS['egw_info']['flags']['page_start_time']),
371		);
372		if ($GLOBALS['egw_info']['flags']['session_restore_time'])
373		{
374			$times['session_restore_time'] = sprintf('%4.2lf', $GLOBALS['egw_info']['flags']['session_restore_time']);
375		}
376		return $times;
377	}
378
379	/**
380	 * Get footer as array to eg. set as vars for a template (from idots' head.inc.php)
381	 *
382	 * @return array
383	 */
384	public function _get_footer()
385	{
386		$var = Array(
387			'img_root'       => $GLOBALS['egw_info']['server']['webserver_url'] . $this->template_dir.'/images',
388			'version'        => $GLOBALS['egw_info']['server']['versions']['api']
389		);
390		$var['page_generation_time'] = '';
391		if($GLOBALS['egw_info']['user']['preferences']['common']['show_generation_time'])
392		{
393			$times = self::get_page_generation_time();
394
395			$var['page_generation_time'] = '<div class="pageGenTime" id="divGenTime_'.$GLOBALS['egw_info']['flags']['currentapp'].'"><span>'.
396				lang('Page was generated in %1 seconds', $times['page_generation_time']);
397
398			if (isset($times['session_restore_time']))
399			{
400				$var['page_generation_time'] .= ' '.lang('(session restored in %1 seconds)',
401					$times['session_restore_time']);
402			}
403			$var['page_generation_time'] .= '</span></div>';
404		}
405		if (empty($GLOBALS['egw_info']['server']['versions']['maintenance_release']))
406		{
407			$GLOBALS['egw_info']['server']['versions']['maintenance_release'] = self::api_version();
408		}
409		$var['powered_by'] = '<a href="http://www.egroupware.org/" class="powered_by" target="_blank">'.
410			lang('Powered by').' EGroupware '.
411			$GLOBALS['egw_info']['server']['versions']['maintenance_release'].'</a>';
412
413		return $var;
414	}
415
416	/**
417	 * Body tags for onLoad, onUnload and onResize
418	 *
419	 * @deprecated since 14.1 use app.js et2_ready method instead to execute code or bind a handler (CSP will stop onXXX attributes!)
420	 * @var array
421	 */
422	protected static $body_tags = array();
423
424	/**
425	 * Adds on(Un)Load= attributes to the body tag of a page
426	 *
427	 * Can only be set via egw_framework::set_on* methods.
428	 *
429	 * @deprecated since 14.1 use app.js et2_ready method instead to execute code or bind a handler (CSP will stop onXXX attributes!)
430	 * @returns string the attributes to be used
431	 */
432	static public function _get_body_attribs()
433	{
434		$js = '';
435		foreach(self::$body_tags as $what => $data)
436		{
437			if (!empty($data))
438			{
439				if($what == 'onLoad')
440				{
441					$js .= 'onLoad="egw_LAB.wait(function() {'. htmlspecialchars($data).'})"';
442					continue;
443				}
444				$js .= ' '.$what.'="' . htmlspecialchars($data) . '"';
445			}
446		}
447		return $js;
448	}
449
450	protected static $body_classes = [];
451
452	/**
453	 * Set a CSS class on the body tag
454	 *
455	 * @param string $class =null
456	 * @return array with all currently set css classes
457	 */
458	public static function bodyClass($class=null)
459	{
460		if (!empty($class))
461		{
462			self::$body_classes[] = $class;
463		}
464		return self::$body_classes;
465	}
466
467	/**
468	 * Get class attribute for body tag
469	 *
470	 * @return string
471	 */
472	protected static function bodyClassAttribute()
473	{
474		return self::$body_classes ? ' class="'.htmlspecialchars(implode(' ', self::$body_classes)).'"' : '';
475	}
476
477	/**
478	 * Get header as array to eg. set as vars for a template (from idots' head.inc.php)
479	 *
480	 * @param array $extra =array() extra attributes passed as data-attribute to egw.js
481	 * @return array
482	 */
483	protected function _get_header(array $extra=array())
484	{
485		// display password expires in N days message once per session
486		$message = null;
487		if ($GLOBALS['egw_info']['flags']['currentapp'] != 'login' &&
488			Auth::check_password_change($message) !== true)
489		{
490			self::message($message, 'info');
491		}
492
493		// get used language code (with a little xss check, if someone tries to sneak something in)
494		if (preg_match('/^[a-z]{2}(-[a-z]{2})?$/',$GLOBALS['egw_info']['user']['preferences']['common']['lang']))
495		{
496			$lang_code = $GLOBALS['egw_info']['user']['preferences']['common']['lang'];
497		}
498		// IE specific fixes
499		if (Header\UserAgent::type() == 'msie')
500		{
501			// tell IE to use it's own mode, not old compatibility modes (set eg. via group policy for all intranet sites)
502			// has to be before any other header tags, but meta and title!!!
503			$pngfix = '<meta http-equiv="X-UA-Compatible" content="IE=edge" />'."\n";
504		}
505
506		$app = $GLOBALS['egw_info']['flags']['currentapp'];
507		$app_title = isset($GLOBALS['egw_info']['apps'][$app]) ? $GLOBALS['egw_info']['apps'][$app]['title'] : lang($app);
508		$app_header = $GLOBALS['egw_info']['flags']['app_header'] ? $GLOBALS['egw_info']['flags']['app_header'] : $app_title;
509		$site_title = strip_tags($GLOBALS['egw_info']['server']['site_title'].' ['.($app_header ? $app_header : $app_title).']');
510
511		// send appheader to clientside
512		$extra['app-header'] = $app_header;
513
514		if($GLOBALS['egw_info']['flags']['currentapp'] != 'wiki') $robots ='<meta name="robots" content="none" />';
515
516		$var['favicon_file'] = self::get_login_logo_or_bg_url('favicon_file', 'favicon.ico');
517
518		if ($GLOBALS['egw_info']['flags']['include_wz_tooltip'] &&
519			file_exists(EGW_SERVER_ROOT.($wz_tooltip = '/phpgwapi/js/wz_tooltip/wz_tooltip.js')))
520		{
521			$include_wz_tooltip = '<script src="'.$GLOBALS['egw_info']['server']['webserver_url'].
522				$wz_tooltip.'?'.filemtime(EGW_SERVER_ROOT.$wz_tooltip).'" type="text/javascript"></script>';
523		}
524		return $this->_get_css()+array(
525			'img_icon'			=> $var['favicon_file'],
526			'img_shortcut'		=> $var['favicon_file'],
527			'pngfix'        	=> $pngfix,
528			'lang_code'			=> $lang_code,
529			'charset'       	=> Translation::charset(),
530			'website_title' 	=> $site_title,
531			'body_tags'         => self::_get_body_attribs().self::bodyClassAttribute(),
532			'java_script'   	=> self::_get_js($extra),
533			'meta_robots'		=> $robots,
534			'dir_code'			=> lang('language_direction_rtl') != 'rtl' ? '' : ' dir="rtl"',
535			'include_wz_tooltip'=> $include_wz_tooltip,
536			'webserver_url'     => $GLOBALS['egw_info']['server']['webserver_url'],
537		);
538	}
539
540	/**
541	 * Get navbar as array to eg. set as vars for a template (from idots' navbar.inc.php)
542	 *
543	 * @param array $apps navbar apps from _get_navbar_apps
544	 * @return array
545	 */
546	protected function _get_navbar($apps)
547	{
548		$var['img_root'] = $GLOBALS['egw_info']['server']['webserver_url'] . '/phpgwapi/templates/'.$this->template.'/images';
549
550		if(isset($GLOBALS['egw_info']['flags']['app_header']))
551		{
552			$var['current_app_title'] = $GLOBALS['egw_info']['flags']['app_header'];
553		}
554		else
555		{
556			$var['current_app_title']=$apps[$GLOBALS['egw_info']['flags']['currentapp']]['title'];
557		}
558		$var['currentapp'] = $GLOBALS['egw_info']['flags']['currentapp'];
559
560		// quick add selectbox
561		$var['quick_add'] = $this->_get_quick_add();
562
563		$var['user_info'] = $this->_user_time_info();
564
565		if($GLOBALS['egw_info']['user']['account_lastpwd_change'] == 0)
566		{
567			$api_messages = lang('You are required to change your password during your first login').'<br />'.
568				lang('Click this image on the navbar: %1','<img src="'.Image::find('preferences','navbar.gif').'">');
569		}
570		elseif($GLOBALS['egw_info']['server']['change_pwd_every_x_days'] && $GLOBALS['egw_info']['user']['account_lastpwd_change'] < time() - (86400*$GLOBALS['egw_info']['server']['change_pwd_every_x_days']))
571		{
572			$api_messages = lang('it has been more then %1 days since you changed your password',$GLOBALS['egw_info']['server']['change_pwd_every_x_days']);
573		}
574
575		$var['logo_header'] = $var['logo_file'] = self::get_login_logo_or_bg_url('login_logo_file', 'logo');
576
577		if ($GLOBALS['egw_info']['server']['login_logo_header'])
578		{
579			$var['logo_header'] = self::get_login_logo_or_bg_url('login_logo_header', 'logo');
580		}
581
582		$var['logo_url'] = $GLOBALS['egw_info']['server']['login_logo_url']?$GLOBALS['egw_info']['server']['login_logo_url']:'http://www.egroupware.org';
583
584		if (substr($var['logo_url'],0,4) != 'http')
585		{
586			$var['logo_url'] = 'http://'.$var['logo_url'];
587		}
588		$var['logo_title'] = $GLOBALS['egw_info']['server']['login_logo_title']?$GLOBALS['egw_info']['server']['login_logo_title']:'www.egroupware.org';
589
590		return $var;
591	}
592
593	/**
594	 * Get login logo or background image base on requested config type
595	 *
596	 * @param type $type config type to fetch. e.g.: "login_logo_file"
597	 * @param type $find_type type of image to search on as alternative option. e.g.: "logo"
598	 *
599	 * @return string returns full url of the image
600	 */
601	static function get_login_logo_or_bg_url ($type, $find_type)
602	{
603		$url = is_array($GLOBALS['egw_info']['server'][$type]) ?
604			$GLOBALS['egw_info']['server'][$type][0] :
605			$GLOBALS['egw_info']['server'][$type];
606
607		if (substr($url, 0, 4) == 'http' ||
608			$url[0] == '/')
609		{
610			return $url;
611		}
612		else
613		{
614			return Image::find('api',$url ? $url : $find_type, '', null);
615		}
616	}
617
618	/**
619	 * Returns Html with user and time
620	 *
621	 * @return void
622	 */
623	protected static function _user_time_info()
624	{
625		$now = new DateTime();
626		$user_info = '<span>'.lang($now->format('l')) . ' ' . $now->format(true).'</span>';
627
628		$user_tzs = DateTime::getUserTimezones();
629		if (count($user_tzs) > 1)
630		{
631			$tz = $GLOBALS['egw_info']['user']['preferences']['common']['tz'];
632			$user_info .= Html::form(Html::select('tz',$tz,$user_tzs,true),array(),
633				'/index.php','','tz_selection',' style="display: inline;"','GET');
634		}
635		return $user_info;
636	}
637
638	/**
639	 * Returns user avatar menu
640	 *
641	 * @return string
642	 */
643	protected static function _user_avatar_menu()
644	{
645		$stat = array_pop(Hooks::process('framework_avatar_stat'));
646
647		return '<span title="'.Accounts::format_username().'" class="avatar"><img src="'.Egw::link('/api/avatar.php', array(
648								'account_id' => $GLOBALS['egw_info']['user']['account_id'],
649							)).'"/>'.(!empty($stat) ?
650				'<span class="fw_avatar_stat '.$stat['class'].'" title="'.$stat['title'].'">'.$stat['body'].'</span>' : '').'</span>';
651	}
652
653	/**
654	 * Returns logout menu
655	 *
656	 * @return string
657	 */
658	protected static function _logout_menu()
659	{
660		return '<a href="'.Egw::link('/logout.php').'" title="'.lang("Logout").'" ></a>';
661	}
662
663	/**
664	 * Returns print menu
665	 *
666	 * @return string
667	 */
668	protected static function _print_menu()
669	{
670		return '<span title="'.lang("Print current view").'"</span>';
671	}
672
673
674	/**
675	 * Prepare the current users
676	 *
677	 * @return array
678	 */
679	protected static function _current_users()
680	{
681	   if( $GLOBALS['egw_info']['user']['apps']['admin'] && $GLOBALS['egw_info']['user']['preferences']['common']['show_currentusers'])
682	   {
683		   return [
684			   'name' => 'current_user',
685			   'title' => lang('Current users').':'.$GLOBALS['egw']->session->session_count(),
686			   'url' => self::link('/index.php','menuaction=admin.admin_accesslog.sessions&ajax=true')
687		   ];
688	   }
689	}
690
691	/**
692	 * Prepare the quick add selectbox
693	 *
694	 * @return string
695	 */
696	protected static function _get_quick_add()
697	{
698		return '<span id="quick_add" title="'.lang('Quick add').'"></span>';
699	}
700
701	/**
702	 * Prepare notification signal (blinking bell)
703	 *
704	 * @return string
705	 */
706	protected static function _get_notification_bell()
707	{
708		return Html::image('notifications', 'notificationbell', lang('notifications'),
709			'id="notificationbell" style="display: none"');
710	}
711
712	/**
713	 * Get context to use with file_get_context or fopen to use our proxy settings from setup
714	 *
715	 * @param string $username =null username for regular basic Auth
716	 * @param string $password =null password --------- " ----------
717	 * @param array $opts =array() further params for http(s) context, eg. array('timeout' => 123)
718	 * @return resource|null context to use with file_get_context/fopen or null if no proxy configured
719	 */
720	public static function proxy_context($username=null, $password=null, array $opts = array())
721	{
722		$opts += array(
723			'method' => 'GET',
724		);
725		if (!empty($GLOBALS['egw_info']['server']['httpproxy_server']))
726		{
727			$opts += array (
728				'proxy'  => 'tcp://'.$GLOBALS['egw_info']['server']['httpproxy_server'].':'.
729					($GLOBALS['egw_info']['server']['httpproxy_port'] ? $GLOBALS['egw_info']['server']['httpproxy_port'] : 8080),
730				'request_fulluri' => true,
731			);
732			// proxy authentication
733			if (!empty($GLOBALS['egw_info']['server']['httpproxy_server_username']))
734			{
735				$opts['header'][] = 'Proxy-Authorization: Basic '.base64_encode($GLOBALS['egw_info']['server']['httpproxy_server_username'].':'.
736					$GLOBALS['egw_info']['server']['httpproxy_server_password']);
737			}
738		}
739		// optional authentication
740		if (isset($username))
741		{
742			$opts['header'][] = 'Authorization: Basic '.base64_encode($username.':'.$password);
743		}
744		return stream_context_create(array(
745			'http' => $opts,
746			'https' => $opts,
747		));
748	}
749
750	/**
751	 * Get API version from changelog or database, whichever is bigger
752	 *
753	 * @param string &$changelog on return path to changelog
754	 * @return string
755	 */
756	public static function api_version(&$changelog=null)
757	{
758		return Framework\Updates::api_version($changelog);
759	}
760
761	/**
762	 * Get the link to an application's index page
763	 *
764	 * @param string $app
765	 * @return string
766	 */
767	public static function index($app)
768	{
769		$data =& $GLOBALS['egw_info']['user']['apps'][$app];
770		if (!isset($data))
771		{
772			throw new Exception\WrongParameter("'$app' not a valid app for this user!");
773		}
774		$index = '/'.$app.'/index.php';
775		if (isset($data['index']))
776		{
777			if (preg_match('|^https?://|', $data['index']))
778			{
779				return $data['index'];
780			}
781			if ($data['index'][0] == '/')
782			{
783				$index = $data['index'];
784			}
785			else
786			{
787				$index = '/index.php?menuaction='.$data['index'];
788			}
789		}
790		return self::link($index,$GLOBALS['egw_info']['flags']['params'][$app]);
791	}
792
793	/**
794	 * Used internally to store unserialized value of $GLOBALS['egw_info']['user']['preferences']['common']['user_apporder']
795	 */
796	private static $user_apporder = array();
797
798	/**
799	 * Internal usort callback function used to sort an array according to the
800	 * user sort order
801	 */
802	private static function _sort_apparray($a, $b)
803	{
804		//Unserialize the user_apporder array
805		$arr = self::$user_apporder;
806
807		$ind_a = isset($arr[$a['name']]) ? $arr[$a['name']] : null;
808		$ind_b = isset($arr[$b['name']]) ? $arr[$b['name']] : null;
809
810		if ($ind_a == $ind_b)
811			return 0;
812
813		if ($ind_a == null)
814			return -1;
815
816		if ($ind_b == null)
817			return 1;
818
819		return $ind_a > $ind_b ? 1 : -1;
820	}
821
822	/**
823	 * Prepare an array with apps used to render the navbar
824	 *
825	 * This is similar to the former common::navbar() method - though it returns the vars and does not place them in global scope.
826	 *
827	 * @return array
828	 */
829	protected static function _get_navbar_apps()
830	{
831		$first = key($GLOBALS['egw_info']['user']['apps']);
832		if(is_array($GLOBALS['egw_info']['user']['apps']['admin']) && $first != 'admin')
833		{
834			$newarray['admin'] = $GLOBALS['egw_info']['user']['apps']['admin'];
835			foreach($GLOBALS['egw_info']['user']['apps'] as $index => $value)
836			{
837				if($index != 'admin')
838				{
839					$newarray[$index] = $value;
840				}
841			}
842			$GLOBALS['egw_info']['user']['apps'] = $newarray;
843			reset($GLOBALS['egw_info']['user']['apps']);
844		}
845		unset($index);
846		unset($value);
847		unset($newarray);
848
849		$apps = array();
850		foreach($GLOBALS['egw_info']['user']['apps'] as $app => $data)
851		{
852			if (is_long($app))
853			{
854				continue;
855			}
856
857			if ($app == 'preferences' || $GLOBALS['egw_info']['apps'][$app]['status'] != 2 && $GLOBALS['egw_info']['apps'][$app]['status'] != 3)
858			{
859				$apps[$app]['title'] = $GLOBALS['egw_info']['apps'][$app]['title'];
860				$apps[$app]['url']   = self::index($app);
861				$apps[$app]['name']  = $app;
862
863				// create popup target
864				if ($data['status'] == 4)
865				{
866					$apps[$app]['target'] = ' target="'.$app.'" onClick="'."if (this != '') { window.open(this+'".
867						(strpos($apps[$app]['url'],'?') !== false ? '&' : '?').
868						"referer='+encodeURIComponent(location),this.target,'width=800,height=600,scrollbars=yes,resizable=yes'); return false; } else { return true; }".'"';
869				}
870				elseif(isset($GLOBALS['egw_info']['flags']['navbar_target']) && $GLOBALS['egw_info']['flags']['navbar_target'])
871				{
872					$apps[$app]['target'] = 'target="' . $GLOBALS['egw_info']['flags']['navbar_target'] . '"';
873				}
874				else
875				{
876					$apps[$app]['target'] = '';
877				}
878
879				// take status flag into account as we might use it on client-side.
880				// for instance: applications with status 5 will run in background
881				$apps[$app]['status'] = $data['status'];
882
883				if (!empty($data['icon']) && preg_match('#^(https?://|/)#', $data['icon']))
884				{
885					$icon_url =  $data['icon'];
886				}
887				else
888				{
889					$icon = isset($data['icon']) ?  $data['icon'] : 'navbar';
890					$icon_app = isset($data['icon_app']) ? $data['icon_app'] : $app;
891					$icon_url = Image::find($icon_app,Array($icon,'nonav'),'');
892				}
893				$apps[$app]['icon']  = $apps[$app]['icon_hover'] = $icon_url;
894			}
895		}
896
897		//Sort the applications accordingly to their user sort setting
898		if ($GLOBALS['egw_info']['user']['preferences']['common']['user_apporder'])
899		{
900			//Sort the application array using the user_apporder array as sort index
901			self::$user_apporder =
902				unserialize($GLOBALS['egw_info']['user']['preferences']['common']['user_apporder']);
903			uasort($apps, __CLASS__.'::_sort_apparray');
904		}
905
906		if ($GLOBALS['egw_info']['flags']['currentapp'] == 'preferences' || $GLOBALS['egw_info']['flags']['currentapp'] == 'about')
907		{
908			$app = $app_title = 'EGroupware';
909		}
910		else
911		{
912			$app = $GLOBALS['egw_info']['flags']['currentapp'];
913			$app_title = $GLOBALS['egw_info']['apps'][$app]['title'];
914		}
915
916		if ($GLOBALS['egw_info']['user']['apps']['preferences'])	// Preferences last
917		{
918			$prefs = $apps['preferences'];
919			unset($apps['preferences']);
920			$apps['preferences'] = $prefs;
921		}
922
923		// We handle this here because its special
924		$apps['about']['title'] = 'EGroupware';
925		$apps['about']['url']   = self::link('/about.php');
926		$apps['about']['icon']  = $apps['about']['icon_hover'] = Image::find('api',Array('about','nonav'));
927		$apps['about']['name']  = 'about';
928
929		$apps['logout']['title'] = lang('Logout');
930		$apps['logout']['name']  = 'logout';
931		$apps['logout']['url']   = self::link('/logout.php');
932		$apps['logout']['icon']  = $apps['logout']['icon_hover'] = Image::find('api',Array('logout','nonav'));
933
934		return $apps;
935	}
936
937	/**
938	 * Used by template headers for including CSS in the header
939	 *
940	 * 'app_css'   - css styles from a) the menuaction's css-method and b) the $GLOBALS['egw_info']['flags']['css']
941	 * 'file_css'  - link tag of the app.css file of the current app
942	 * 'theme_css' - url of the theme css file
943	 * 'print_css' - url of the print css file
944	 *
945	 * @author Dave Hall (*based* on verdilak? css inclusion code)
946	 * @return array with keys 'app_css' from the css method of the menuaction-class and 'file_css' (app.css file of the application)
947	 */
948	public function _get_css()
949	{
950		$app_css = '';
951		if (isset($GLOBALS['egw_info']['flags']['css']))
952		{
953			$app_css = $GLOBALS['egw_info']['flags']['css'];
954		}
955
956		if (self::$load_default_css)
957		{
958			// For mobile user-agent we prefer mobile theme over selected one with a final fallback to theme named as template
959			$themes_to_check = array();
960			if (Header\UserAgent::mobile() || $GLOBALS['egw_info']['user']['preferences']['common']['theme'] == 'fw_mobile')
961			{
962				$themes_to_check[] = $this->template_dir.'/mobile/'.$GLOBALS['egw_info']['user']['preferences']['common']['theme'].'.css';
963				$themes_to_check[] = $this->template_dir.'/mobile/fw_mobile.css';
964			}
965			$themes_to_check[] = $this->template_dir.'/css/'.$GLOBALS['egw_info']['user']['preferences']['common']['theme'].'.css';
966			$themes_to_check[] = $this->template_dir.'/css/'.$this->template.'.css';
967			foreach($themes_to_check as $theme_css)
968			{
969				if (file_exists(EGW_SERVER_ROOT.$theme_css)) break;
970			}
971			$debug_minify = $GLOBALS['egw_info']['server']['debug_minify'] === 'True';
972			if (!$debug_minify && file_exists(EGW_SERVER_ROOT.($theme_min_css = str_replace('.css', '.min.css', $theme_css))))
973			{
974				//error_log(__METHOD__."() Framework\CssIncludes::get()=".array2string(Framework\CssIncludes::get()));
975				self::includeCSS($theme_min_css);
976
977				// Global category styles
978				if (basename($_SERVER['PHP_SELF']) !== 'login.php')
979				{
980					Categories::css(Categories::GLOBAL_APPNAME);
981				}
982			}
983			else
984			{
985				// Load these first
986				// Cascade should go:
987				//  Libs < etemplate2 < framework/theme < app < print
988				// Enhanced selectboxes (et1)
989				self::includeCSS('/api/js/jquery/chosen/chosen.css');
990
991				// eTemplate2 uses jQueryUI, so load it first so et2 can override if needed
992				self::includeCSS("/vendor/bower-asset/jquery-ui/themes/redmond/jquery-ui.css");
993
994				// eTemplate2 - load in top so sidebox has styles too
995				self::includeCSS('/api/templates/default/etemplate2.css');
996
997				// Category styles
998				if (basename($_SERVER['PHP_SELF']) !== 'login.php')
999				{
1000					Categories::css(Categories::GLOBAL_APPNAME);
1001				}
1002
1003				self::includeCSS($theme_css);
1004
1005				// sending print css last, so it can overwrite anything
1006				$print_css = $this->template_dir.'/print.css';
1007				if(!file_exists(EGW_SERVER_ROOT.$print_css))
1008				{
1009					$print_css = '/api/templates/default/print.css';
1010				}
1011				self::includeCSS($print_css);
1012			}
1013			// search for app specific css file, so it can customize the theme
1014			self::includeCSS($GLOBALS['egw_info']['flags']['currentapp'], 'app-'.$GLOBALS['egw_info']['user']['preferences']['common']['theme']) ||
1015				self::includeCSS($GLOBALS['egw_info']['flags']['currentapp'], 'app');
1016		}
1017		return array(
1018			'app_css'   => $app_css,
1019			'css_file'  => Framework\CssIncludes::tags(),
1020		);
1021	}
1022
1023	/**
1024	 * Used by the template headers for including javascript in the header
1025	 *
1026	 * The method is included here to make it easier to change the js support
1027	 * in eGW.  One change then all templates will support it (as long as they
1028	 * include a call to this method).
1029	 *
1030	 * @param array $extra =array() extra data to pass to egw.js as data-parameter
1031	 * @return string the javascript to be included
1032	 */
1033	public static function _get_js(array $extra=array())
1034	{
1035		$java_script = '';
1036
1037		/* this flag is for all javascript code that has to be put before other jscode.
1038		Think of conf vars etc...  (pim@lingewoud.nl) */
1039		if (isset($GLOBALS['egw_info']['flags']['java_script_thirst']))
1040		{
1041			$java_script .= $GLOBALS['egw_info']['flags']['java_script_thirst'] . "\n";
1042		}
1043		// add configuration, link-registry, images, user-data and -perferences for non-popup windows
1044		// specifying etag in url to force reload, as we send expires header
1045		if ($GLOBALS['egw_info']['flags']['js_link_registry'] || isset($_GET['cd']) && $_GET['cd'] === 'popup')
1046		{
1047			self::includeJS('/api/config.php', array(
1048				'etag' => md5(json_encode(Config::clientConfigs()).Link::json_registry()),
1049			));
1050			self::includeJS('/api/images.php', array(
1051				'template' => $GLOBALS['egw_info']['server']['template_set'],
1052				'etag' => md5(json_encode(Image::map($GLOBALS['egw_info']['server']['template_set'])))
1053			));
1054			self::includeJS('/api/user.php', array(
1055				'user' => $GLOBALS['egw_info']['user']['account_lid'],
1056				'lang' => $GLOBALS['egw_info']['user']['preferences']['common']['lang'],
1057				// add etag on url, so we can set an expires header
1058				'etag' => md5(json_encode($GLOBALS['egw_info']['user']['preferences']['common']).
1059					$GLOBALS['egw']->accounts->json($GLOBALS['egw_info']['user']['account_id'])),
1060			));
1061		}
1062
1063		$extra['url'] = $GLOBALS['egw_info']['server']['webserver_url'];
1064		$extra['include'] = array_map(function($str){return substr($str,1);}, self::get_script_links(true), array(1));
1065		$extra['app'] = $GLOBALS['egw_info']['flags']['currentapp'];
1066
1067		// Load LABjs ONCE here
1068		$java_script .= '<script type="text/javascript" src="'.$GLOBALS['egw_info']['server']['webserver_url'].
1069				'/api/js/labjs/LAB.src.js?'.filemtime(EGW_SERVER_ROOT.'/api/js/labjs/LAB.src.js')."\"></script>\n".
1070			'<script type="text/javascript" src="'.$GLOBALS['egw_info']['server']['webserver_url'].
1071				'/api/js/jsapi/egw.js?'.filemtime(EGW_SERVER_ROOT.'/api/js/jsapi/egw.js').'" id="egw_script_id"';
1072
1073		// add values of extra parameter and class var as data attributes to script tag of egw.js
1074		foreach($extra+self::$extra as $name => $value)
1075		{
1076			if (is_array($value)) $value = json_encode($value);
1077			// we need to double encode (Html::htmlspecialchars( , TRUE)), as otherwise we get invalid json, eg. for quotes
1078			$java_script .= ' data-'.$name."=\"". Html::htmlspecialchars($value, true)."\"";
1079		}
1080		$java_script .= "></script>\n";
1081
1082		if(@isset($_GET['menuaction']))
1083		{
1084			list(, $class) = explode('.',$_GET['menuaction']);
1085			if(is_array($GLOBALS[$class]->public_functions) &&
1086				$GLOBALS[$class]->public_functions['java_script'])
1087			{
1088				$java_script .= $GLOBALS[$class]->java_script();
1089			}
1090		}
1091		if (isset($GLOBALS['egw_info']['flags']['java_script']))
1092		{
1093			// Strip out any script tags, this needs to be executed as anonymous function
1094			$GLOBALS['egw_info']['flags']['java_script'] = preg_replace(array('/(<script[^>]*>)([^<]*)/is','/<\/script>/'),array('$2',''),$GLOBALS['egw_info']['flags']['java_script']);
1095			if(trim($GLOBALS['egw_info']['flags']['java_script']) != '')
1096			{
1097				$java_script .= '<script type="text/javascript">window.egw_LAB.wait(function() {'.$GLOBALS['egw_info']['flags']['java_script'] . "});</script>\n";
1098			}
1099		}
1100
1101		return $java_script;
1102	}
1103
1104	/**
1105	 * List available themes
1106	 *
1107	 * Themes are css file in the template directory
1108	 *
1109	 * @param string $themes_dir ='css'
1110	 */
1111	function list_themes()
1112	{
1113		$list = array();
1114		if (file_exists($file=EGW_SERVER_ROOT.$this->template_dir.'/setup/setup.inc.php') &&
1115			(include $file) && isset($GLOBALS['egw_info']['template'][$this->template]['themes']))
1116		{
1117			$list = $GLOBALS['egw_info']['template'][$this->template]['themes'];
1118		}
1119		if (($dh = @opendir(EGW_SERVER_ROOT.$this->template_dir.'/css')))
1120		{
1121			while (($file = readdir($dh)))
1122			{
1123				if (preg_match('/'."\.css$".'/i', $file))
1124				{
1125					list($name) = explode('.',$file);
1126					if (!isset($list[$name])) $list[$name] = ucfirst ($name);
1127				}
1128			}
1129			closedir($dh);
1130		}
1131		return $list;
1132	}
1133
1134	/**
1135	 * List available templates
1136	 *
1137	 * @param boolean $full_data =false true: value is array with values for keys 'name', 'title', ...
1138	 * @returns array alphabetically sorted list of templates
1139	 */
1140	static function list_templates($full_data=false)
1141	{
1142		$list = array('pixelegg'=>null);
1143		// templates packaged like apps in own directories (containing as setup/setup.inc.php file!)
1144		$dr = dir(EGW_SERVER_ROOT);
1145		while (($entry=$dr->read()))
1146		{
1147			if ($entry != '..' && !isset($GLOBALS['egw_info']['apps'][$entry]) && is_dir(EGW_SERVER_ROOT.'/'.$entry) &&
1148				file_exists($f = EGW_SERVER_ROOT . '/' . $entry .'/setup/setup.inc.php'))
1149			{
1150				include($f);
1151				if (isset($GLOBALS['egw_info']['template'][$entry]))
1152				{
1153					$list[$entry] = $full_data ? $GLOBALS['egw_info']['template'][$entry] :
1154						$GLOBALS['egw_info']['template'][$entry]['title'];
1155				}
1156			}
1157		}
1158		$dr->close();
1159
1160		return array_filter($list);
1161	}
1162
1163	/**
1164	* Compile entries for topmenu:
1165	* - regular items: links
1166	* - info items
1167	*
1168	* @param array $vars
1169	* @param array $apps
1170	*/
1171	function topmenu(array $vars,array $apps)
1172	{
1173		// array of topmenu info items (orders of the items matter)
1174		$topmenu_info_items = [
1175			'user_avatar' => $this->_user_avatar_menu(),
1176			'logout' => (Header\UserAgent::mobile() || $GLOBALS['egw_info']['user']['preferences']['common']['theme'] == 'fw_mobile') ? self::_logout_menu() : null,
1177			'update' => ($update = Framework\Updates::notification()) ? $update : null,
1178			'notifications' => ($GLOBALS['egw_info']['user']['apps']['notifications']) ? self::_get_notification_bell() : null,
1179			'quick_add' => $vars['quick_add'],
1180			'print_title' => $this->_print_menu()
1181		];
1182
1183		// array of topmenu items (orders of the items matter)
1184		$topmenu_items = [
1185			0 => (is_array(($current_user = $this->_current_users()))) ? $current_user : null,
1186		];
1187
1188		// Home should be at the top before preferences
1189		if($GLOBALS['egw_info']['user']['apps']['home'] && isset($apps['home']))
1190		{
1191			$this->_add_topmenu_item($apps['home']);
1192		}
1193
1194		// array of topmenu preferences items (orders of the items matter)
1195		$topmenu_preferences = ['prefs', 'acl', 'cats', 'security'];
1196
1197		// set topmenu preferences items
1198		if($GLOBALS['egw_info']['user']['apps']['preferences'])
1199		{
1200			foreach ($topmenu_preferences as $prefs)
1201			{
1202				$this->add_preferences_topmenu($prefs);
1203			}
1204		}
1205
1206		// call topmenu info items hooks
1207		Hooks::process('topmenu_info',array(),true);
1208
1209		// Add extra items added by hooks
1210		foreach(self::$top_menu_extra as $extra_item) {
1211			if ($extra_item['name'] == 'search')
1212			{
1213				$topmenu_info_items['search'] = '<a href="'.$extra_item['url'].'" title="'.$extra_item['title'].'"></a>';
1214			}
1215			else
1216			{
1217				array_push($topmenu_items, $extra_item);
1218			}
1219		}
1220		// push logout as the last item in topmenu items list
1221		array_push($topmenu_items, $apps['logout']);
1222
1223		// set topmenu info items
1224		foreach ($topmenu_info_items as $id => $content)
1225		{
1226			if (!$content || (in_array($id, ['search', 'quick_add', 'update']) && (Header\UserAgent::mobile() || $GLOBALS['egw_info']['user']['preferences']['common']['theme'] == 'fw_mobile')))
1227			{
1228				continue;
1229			}
1230			$this->_add_topmenu_info_item($content, $id);
1231		}
1232		// set topmenu items
1233		foreach ($topmenu_items as $item)
1234		{
1235			if ($item) $this->_add_topmenu_item($item);
1236		}
1237	}
1238
1239	/**
1240	 * Add Preferences link to topmenu using settings-hook to know if an app supports Preferences
1241	 */
1242	protected function add_preferences_topmenu($type='prefs')
1243	{
1244		static $memberships=null;
1245		if (!isset($memberships)) $memberships = $GLOBALS['egw']->accounts->memberships($GLOBALS['egw_info']['user']['account_id'], true);
1246		static $types = array(
1247			'prefs' => array(
1248				'title' => 'Preferences',
1249				'hook'  => 'settings',
1250			),
1251			'acl' => array(
1252				'title' => 'Access',
1253				'hook'  => 'acl_rights',
1254			),
1255			'cats' => array(
1256				'title' => 'Categories',
1257				'hook' => 'categories',
1258				'run_hook' => true,	// acturally run hook, not just look it's implemented
1259			),
1260			'security' => array(
1261				'title' => 'Security & Password',
1262				'hook' => 'preferences_security',
1263			),
1264		);
1265		if (!$GLOBALS['egw_info']['user']['apps']['preferences'] || $GLOBALS['egw_info']['server']['deny_'.$type] &&
1266			array_intersect($memberships, (array)$GLOBALS['egw_info']['server']['deny_'.$type]) &&
1267			!$GLOBALS['egw_info']['user']['apps']['admin'])
1268		{
1269			return;	// user has no access to Preferences app
1270		}
1271		if (isset($types[$type]['run_hook']))
1272		{
1273			$apps = Hooks::process($types[$type]['hook']);
1274			// as all apps answer, we need to remove none-true responses
1275			foreach($apps as $app => $val)
1276			{
1277				if (!$val) unset($apps[$app]);
1278			}
1279		}
1280		else
1281		{
1282			$apps = Hooks::implemented($types[$type]['hook']);
1283		}
1284		// allways display password in topmenu, if user has rights to change it
1285		switch ($type)
1286		{
1287			case 'security':
1288				if ($apps || $GLOBALS['egw_info']['server']['2fa_required'] !== 'disabled' ||
1289					!$GLOBALS['egw']->acl->check('nopasswordchange', 1))
1290				{
1291					$this->_add_topmenu_item(array(
1292						'id'    => 'password',
1293						'name'  => 'preferences',
1294						'title' => lang($types[$type]['title']),
1295						'url'   => "javascript:egw.open_link('".
1296							self::link('/index.php?menuaction=preferences.preferences_password.change')."','_blank','850x580')",
1297					));
1298				}
1299				break;
1300
1301			default:
1302				$this->_add_topmenu_item(array(
1303					'id' => $type,
1304					'name' => 'preferences',
1305					'title' => lang($types[$type]['title']),
1306					'url' => "javascript:egw.show_preferences(\"$type\",".json_encode($apps).')',
1307				));
1308		}
1309	}
1310
1311	/**
1312	* Add menu items to the topmenu template class to be displayed
1313	*
1314	* @param array $app application data
1315	* @param mixed $alt_label string with alternative menu item label default value = null
1316	* @param string $urlextra string with alternate additional code inside <a>-tag
1317	* @access protected
1318	* @return void
1319	*/
1320	abstract function _add_topmenu_item(array $app_data,$alt_label=null);
1321
1322	/**
1323	* Add info items to the topmenu template class to be displayed
1324	*
1325	* @param string $content Html of item
1326	* @param string $id =null
1327	* @access protected
1328	* @return void
1329	*/
1330	abstract function _add_topmenu_info_item($content, $id=null);
1331
1332	static $top_menu_extra = array();
1333
1334	/**
1335	* Called by hooks to add an entry in the topmenu location.
1336	* Extra entries will be added just before Logout.
1337	*
1338	* @param string $id unique element id
1339	* @param string $url Address for the entry to link to
1340	* @param string $title Text displayed for the entry
1341	* @param string $target Optional, so the entry can open in a new page or popup
1342	* @access public
1343	* @return void
1344	*/
1345	public static function add_topmenu_item($id,$url,$title,$target = '')
1346	{
1347		$entry['name'] = $id;
1348		$entry['url'] = $url;
1349		$entry['title'] = $title;
1350		$entry['target'] = $target;
1351
1352		self::$top_menu_extra[$id] = $entry;
1353	}
1354
1355	/**
1356	* called by hooks to add an icon in the topmenu info location
1357	*
1358	* @param string $id unique element id
1359	* @param string $icon_src src of the icon image. Make sure this nog height then 18pixels
1360	* @param string $iconlink where the icon links to
1361	* @param booleon $blink set true to make the icon blink
1362	* @param mixed $tooltip string containing the tooltip html, or null of no tooltip
1363	* @access public
1364	* @return void
1365	*/
1366	abstract function topmenu_info_icon($id,$icon_src,$iconlink,$blink=false,$tooltip=null);
1367
1368	/**
1369	 * Call and return content of 'after_navbar' hook
1370	 *
1371	 * @return string
1372	 */
1373	protected function _get_after_navbar()
1374	{
1375		ob_start();
1376		Hooks::process('after_navbar',null,true);
1377		$content = ob_get_contents();
1378		ob_end_clean();
1379
1380		return $content;
1381	}
1382
1383	/**
1384	 * Return javascript (eg. for onClick) to open manual with given url
1385	 *
1386	 * @param string $url
1387	 */
1388	abstract function open_manual_js($url);
1389
1390	/**
1391	 * Methods to add javascript to framework
1392	 */
1393
1394	/**
1395	 * The include manager manages including js files and their dependencies
1396	 */
1397	protected static $js_include_mgr;
1398
1399	/**
1400	* Checks to make sure a valid package and file name is provided
1401	*
1402	* Example call syntax:
1403	* a) Api\Framework::includeJS('jscalendar','calendar')
1404	*    --> /phpgwapi/js/jscalendar/calendar.js
1405	* b) Api\Framework::includeJS('/phpgwapi/inc/calendar-setup.js',array('lang'=>'de'))
1406	*    --> /phpgwapi/inc/calendar-setup.js?lang=de
1407	*
1408	* @param string $package package or complete path (relative to EGW_SERVER_ROOT) to be included
1409	* @param string|array $file =null file to be included - no ".js" on the end or array with get params
1410	* @param string $app ='phpgwapi' application directory to search - default = phpgwapi
1411	* @param boolean $append =true should the file be added
1412	*/
1413	static function includeJS($package, $file=null, $app='phpgwapi', $append=true)
1414	{
1415		self::$js_include_mgr->include_js_file($package, $file, $app, $append);
1416	}
1417
1418	/**
1419	 * Set or return all javascript files set via validate_file, optionally clear all files
1420	 *
1421	 * @param array $files =null array with pathes relative to EGW_SERVER_ROOT, eg. /api/js/jquery/jquery.js
1422	 * @param boolean $clear_files =false true clear files after returning them
1423	 * @return array with pathes relative to EGW_SERVER_ROOT
1424	 */
1425	static function js_files(array $files=null, $clear_files=false)
1426	{
1427		if (isset($files) && is_array($files))
1428		{
1429			self::$js_include_mgr->include_files($files);
1430		}
1431		return self::$js_include_mgr->get_included_files($clear_files);
1432	}
1433
1434	/**
1435	 * Used for generating the list of external js files to be included in the head of a page
1436	 *
1437	 * NOTE: This method should only be called by the template class.
1438	 * The validation is done when the file is added so we don't have to worry now
1439	 *
1440	 * @param boolean $return_pathes =false false: return Html script tags, true: return array of file pathes relative to webserver_url
1441	 * @param boolean $clear_files =false true clear files after returning them
1442	 * @return string|array see $return_pathes parameter
1443	 */
1444	static public function get_script_links($return_pathes=false, $clear_files=false)
1445	{
1446		$to_include = Framework\Bundle::js_includes(self::$js_include_mgr->get_included_files($clear_files));
1447
1448		if ($return_pathes)
1449		{
1450			return $to_include;
1451		}
1452		$start = '<script type="text/javascript" src="'. $GLOBALS['egw_info']['server']['webserver_url'];
1453		$end = '">'."</script>\n";
1454		return "\n".$start.implode($end.$start, $to_include).$end;
1455	}
1456
1457	/**
1458	 *
1459	 * @var boolean
1460	 */
1461	protected static $load_default_css = true;
1462
1463	/**
1464	 * Include a css file, either speicified by it's path (relative to EGW_SERVER_ROOT) or appname and css file name
1465	 *
1466	 * @param string $app path (relative to EGW_SERVER_ROOT) or appname (if !is_null($name))
1467	 * @param string $name =null name of css file in $app/templates/{default|$this->template}/$name.css
1468	 * @param boolean $append =true true append file, false prepend (add as first) file used eg. for template itself
1469	 * @param boolean $no_default_css =false true do NOT load any default css, only what app explicitly includes
1470	 * @return boolean false: css file not found, true: file found
1471	 */
1472	public static function includeCSS($app, $name=null, $append=true, $no_default_css=false)
1473	{
1474		if ($no_default_css)
1475		{
1476			self::$load_default_css = false;
1477		}
1478		//error_log(__METHOD__."('$app', '$name', append=$append, no_default=$no_default_css) ".function_backtrace());
1479		return Framework\CssIncludes::add($app, $name, $append, $no_default_css);
1480	}
1481
1482	/**
1483	 * Add registered CSS and javascript to ajax response
1484	 */
1485	public static function include_css_js_response()
1486	{
1487		$response = Json\Response::get();
1488		$app = $GLOBALS['egw_info']['flags']['currentapp'];
1489
1490		// try to add app specific css file
1491		self::includeCSS($app, 'app-'.$GLOBALS['egw_info']['user']['preferences']['common']['theme']) ||
1492			self::includeCSS($app,'app');
1493
1494		// add all css files from Framework::includeCSS()
1495		$query = null;
1496		//error_log(__METHOD__."() Framework\CssIncludes::get()=".array2string(Framework\CssIncludes::get()));
1497		foreach(Framework\CssIncludes::get() as $path)
1498		{
1499			unset($query);
1500			list($path,$query) = explode('?',$path,2);
1501			$path .= '?'. ($query ? $query : filemtime(EGW_SERVER_ROOT.$path));
1502			$response->includeCSS($GLOBALS['egw_info']['server']['webserver_url'].$path);
1503		}
1504
1505		// try to add app specific js file
1506		self::includeJS('.', 'app', $app);
1507
1508		// add all js files from Framework::includeJS()
1509		$files = Framework\Bundle::js_includes(self::$js_include_mgr->get_included_files());
1510		foreach($files as $path)
1511		{
1512			$response->includeScript($GLOBALS['egw_info']['server']['webserver_url'].$path);
1513		}
1514	}
1515
1516	/**
1517	 * Set a preference via ajax
1518	 *
1519	 * @param string $app
1520	 * @param string $name
1521	 * @param string $value
1522	 */
1523	public static function ajax_set_preference($app, $name, $value)
1524	{
1525		$GLOBALS['egw']->preferences->read_repository();
1526		if ((string)$value === '')
1527		{
1528			$GLOBALS['egw']->preferences->delete($app, $name);
1529		}
1530		else
1531		{
1532			$GLOBALS['egw']->preferences->add($app, $name, $value);
1533		}
1534		$GLOBALS['egw']->preferences->save_repository(True);
1535	}
1536
1537	/**
1538	 * Get Preferences of a certain application via ajax
1539	 *
1540	 * @param string $app
1541	 */
1542	public static function ajax_get_preference($app)
1543	{
1544		// dont block session, while we read preferences, they are not supposed to change something in the session
1545		$GLOBALS['egw']->session->commit_session();
1546
1547		if (preg_match('/^[a-z0-9_]+$/i', $app))
1548		{
1549			// send etag header, if we are directly called (not via jsonq!)
1550			if (strpos($_GET['menuaction'], __FUNCTION__) !== false)
1551			{
1552				$etag = '"'.$app.'-'.md5(json_encode($GLOBALS['egw_info']['user']['preferences'][$app])).'"';
1553				if (isset($_SERVER['HTTP_IF_NONE_MATCH']) && $_SERVER['HTTP_IF_NONE_MATCH'] == $etag)
1554				{
1555					header("HTTP/1.1 304 Not Modified");
1556					exit();
1557				}
1558				header('ETag: '.$etag);
1559			}
1560			$response = Json\Response::get();
1561			$response->call('egw.set_preferences', (array)$GLOBALS['egw_info']['user']['preferences'][$app], $app);
1562		}
1563	}
1564
1565	/**
1566	 * Create or delete a favorite for multiple users
1567	 *
1568	 * Need to be in egw_framework to be called with .template postfix from json.php!
1569	 *
1570	 * @param string $app Current application, needed to save preference
1571	 * @param string $name Name of the favorite
1572	 * @param string $action "add" or "delete"
1573	 * @param boolean|int|string $group ID of the group to create the favorite for, or 'all' for all users
1574	 * @param array $filters =array() key => value pairs for the filter
1575	 * @return boolean Success
1576	 */
1577	public static function ajax_set_favorite($app, $name, $action, $group, $filters = array())
1578	{
1579		return Framework\Favorites::set_favorite($app, $name, $action, $group, $filters);
1580	}
1581
1582	/**
1583	 * Get a cachable list of users for the client
1584	 *
1585	 * The account source takes care of access and filtering according to preference
1586	 */
1587	public static function ajax_user_list()
1588	{
1589		$list = array('accounts' => array(),'groups' => array(), 'owngroups' => array());
1590		if($GLOBALS['egw_info']['user']['preferences']['common']['account_selection'] == 'primary_group')
1591		{
1592			$list['accounts']['filter']['group'] = $GLOBALS['egw_info']['user']['account_primary_group'];
1593		}
1594		$contact_obj = new Contacts();
1595		foreach($list as $type => &$accounts)
1596		{
1597			$options = array('account_type' => $type) + $accounts;
1598			$key_pair = Accounts::link_query('',$options);
1599			$accounts = array();
1600			foreach($key_pair as $account_id => $name)
1601			{
1602				$contact = $contact_obj->read('account:'.$account_id, true);
1603				$accounts[] = array('value' => $account_id, 'label' => $name, 'icon' => self::link('/api/avatar.php', array(
1604						'contact_id' => $contact['id'],
1605						'etag' => $contact['etag']
1606					)));
1607			}
1608		}
1609
1610		Json\Response::get()->data($list);
1611		return $list;
1612	}
1613
1614	/**
1615	 * Get certain account-data of given account-id(s)
1616	 *
1617	 * @param string|array $_account_ids
1618	 * @param string $_field ='account_email'
1619	 * @param boolean $_resolve_groups =false true: return attribute for all members, false return attribute for group itself
1620	 * @return array account_id => data pairs
1621	 */
1622	public static function ajax_account_data($_account_ids, $_field, $_resolve_groups=false)
1623	{
1624		$list = array();
1625		foreach((array)$_account_ids as $account_id)
1626		{
1627			foreach($account_id < 0 && $_resolve_groups ?
1628				$GLOBALS['egw']->accounts->members($account_id, true) : array($account_id) as $account_id)
1629			{
1630				// Make sure name is formatted according to preference
1631				if($_field == 'account_fullname')
1632				{
1633					$list[$account_id] = Accounts::format_username(
1634						$GLOBALS['egw']->accounts->id2name($account_id, 'account_lid'),
1635						$GLOBALS['egw']->accounts->id2name($account_id, 'account_firstname'),
1636						$GLOBALS['egw']->accounts->id2name($account_id, 'account_lastname'),
1637						$account_id
1638					);
1639				}
1640				else
1641				{
1642					$list[$account_id] = $GLOBALS['egw']->accounts->id2name($account_id, $_field);
1643				}
1644			}
1645		}
1646
1647		Json\Response::get()->data($list);
1648		return $list;
1649	}
1650}
1651// Init all static variables
1652Framework::init_static();
1653