1<?php
2/**
3 * EGroupware - Framework for Ajax based templates: jdots & Pixelegg
4 *
5 * @link http://www.stylite.de
6 * @package api
7 * @subpackage framework
8 * @author Andreas Stöckel <as@stylite.de>
9 * @author Ralf Becker <rb@stylite.de>
10 * @author Nathan Gray <ng@stylite.de>
11 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
12 */
13
14namespace EGroupware\Api\Framework;
15
16use EGroupware\Api;
17
18/**
19* Stylite jdots template
20*/
21abstract class Ajax extends Api\Framework
22{
23	/**
24	 * Appname used to include javascript code
25	 */
26	const JS_INCLUDE_APP = '';
27	/**
28	 * Appname used for everything else
29	 */
30	const APP = '';
31
32	/**
33	 * Minimum width of sidebar eg. from German 2-letter daynames in Calendar
34	 * or Calendar's navigation buttons need to be seen
35	 *
36	 * Need to be changed in js/fw_[template].js.
37	 */
38	const MIN_SIDEBAR_WIDTH = 245;
39	/**
40	 * Default width need to be tested with English 3-letter day-names and Pixelegg template in Calendar
41	 *
42	 * Need to be changed in js/egw_fw.js around line 1536 too!
43	 */
44	const DEFAULT_SIDEBAR_WIDTH = 255;
45	/**
46	 * Whether javascript:egw_link_handler calls (including given app) should be returned by the "link" function
47	 * or just the link
48	 *
49	 * @var string
50	 */
51	private static $link_app;
52
53	/**
54	 * Constructor
55	 *
56	 * @param string $template = '' name of the template
57	 */
58	function __construct($template=self::APP)
59	{
60		parent::__construct($template);		// call the constructor of the extended class
61
62		$this->template_dir = '/'.$template;		// we are packaged as an application
63	}
64
65	/**
66	 * Check if current user agent is supported
67	 *
68	 * Currently we do NOT support:
69	 * - iPhone, iPad, Android, SymbianOS due to iframe scrolling problems of Webkit
70	 * - IE < 7
71	 *
72	 * @return boolean
73	 */
74	public static function is_supported_user_agent()
75	{
76		if (Api\Header\UserAgent::type() == 'msie' && Api\Header\UserAgent::version() < 7)
77		{
78			return false;
79		}
80		return true;
81	}
82
83	/**
84	 * Reads an returns the width of the sidebox or false if the width is not set
85	 */
86	private static function get_sidebar_width($app)
87	{
88		$width = self::DEFAULT_SIDEBAR_WIDTH;
89
90		//Check whether the width had been stored explicitly for the jdots template, use that value
91		if ($GLOBALS['egw_info']['user']['preferences'][$app]['jdotssideboxwidth'])
92		{
93			$width = (int)$GLOBALS['egw_info']['user']['preferences'][$app]['jdotssideboxwidth'];
94//				error_log(__METHOD__.__LINE__."($app):$width --> reading jdotssideboxwidth");
95		}
96		//Otherwise use the legacy "idotssideboxwidth" value
97		else if ($GLOBALS['egw_info']['user']['preferences'][$app]['idotssideboxwidth'])
98		{
99			$width = (int)$GLOBALS['egw_info']['user']['preferences'][$app]['idotssideboxwidth'];
100//				error_log(__METHOD__.__LINE__."($app):$width --> reading idotssideboxwidth");
101		}
102
103		//Width may not be smaller than MIN_SIDEBAR_WIDTH
104		if ($width < self::MIN_SIDEBAR_WIDTH)
105			$width = self::MIN_SIDEBAR_WIDTH;
106
107		return $width;
108	}
109
110	/**
111	 * Returns the global width of the sidebox. If the app_specific_sidebar_width had been switched
112	 * on, the default width will be returned
113	 */
114	private static function get_global_sidebar_width()
115	{
116		return self::DEFAULT_SIDEBAR_WIDTH;
117	}
118
119
120
121	/**
122	 * Extract applicaton name from given url (incl. GET parameters)
123	 *
124	 * @param string $url
125	 * @return string appname or NULL if it could not be detected (eg. constructing javascript urls)
126	 */
127	public static function app_from_url($url)
128	{
129		$matches = null;
130		if (preg_match('/menuaction=([a-z0-9_-]+)\./i',$url,$matches))
131		{
132			return $matches[1];
133		}
134		if ($GLOBALS['egw_info']['server']['webserver_url'] &&
135			($webserver_path = parse_url($GLOBALS['egw_info']['server']['webserver_url'],PHP_URL_PATH)))
136		{
137			list(,$url) = explode($webserver_path, parse_url($url,PHP_URL_PATH),2);
138		}
139		if (preg_match('/\/([^\/]+)\/([^\/]+\.php)?(\?|\/|$)/',$url,$matches))
140		{
141			return $matches[1];
142		}
143		//error_log(__METHOD__."('$url') could NOT detect application!");
144		return null;
145	}
146
147	/**
148	 * Link url generator
149	 *
150	 * @param string $url The url the link is for
151	 * @param string|array	$extravars	Extra params to be passed to the url
152	 * @param string $link_app = null if appname or true, some templates generate a special link-handler url
153	 * @return string	The full url after processing
154	 */
155	static function link($url = '', $extravars = '', $link_app=null)
156	{
157		if (is_null($link_app)) $link_app = self::$link_app;
158		$link = parent::link($url, $extravars);
159
160		// $link_app === true --> detect application, otherwise use given application
161		if ($link_app && (is_string($link_app) || ($link_app = self::app_from_url($link))))
162		{
163			// Link gets handled in JS, so quotes need slashes as well as url-encoded
164			// encoded ampersands in get parameters (%26) need to be encoded twise,
165			// so they are still encoded when assigned to window.location
166			$link_with_slashes = str_replace(array('%27','%26'), array('\%27','%2526'), $link);
167
168			//$link = "javascript:window.egw_link_handler?egw_link_handler('$link','$link_app'):parent.egw_link_handler('$link','$link_app');";
169			$link = "javascript:egw_link_handler('$link_with_slashes','$link_app')";
170		}
171		return $link;
172	}
173
174	/**
175	 * Returns the html-header incl. the opening body tag
176	 *
177	 * @param array $extra = array() extra attributes passed as data-attribute to egw.js
178	 * @return string with Api\Html
179	 */
180	function header(array $extra=array())
181	{
182		// make sure header is output only once
183		if (self::$header_done) return '';
184		self::$header_done = true;
185
186		$this->send_headers();
187
188		// catch error echo'ed before the header, ob_start'ed in the header.inc.php
189		$content = ob_get_contents();
190		ob_end_clean();
191		//error_log(__METHOD__.'('.array2string($extra).') called from:'.function_backtrace());
192
193		// the instanciation of the template has to be here and not in the constructor,
194		// as the old template class has problems if restored from the session (php-restore)
195		// todo: check if this is still true
196		$this->tpl = new Template(EGW_SERVER_ROOT.$this->template_dir);
197		if (Api\Header\UserAgent::mobile() || $GLOBALS['egw_info']['user']['preferences']['common']['theme'] == 'mobile')
198		{
199			$this->tpl->set_file(array('_head' => 'head_mobile.tpl'));
200		}
201		else
202		{
203			$this->tpl->set_file(array('_head' => 'head.tpl'));
204		}
205		$this->tpl->set_block('_head','head');
206		$this->tpl->set_block('_head','framework');
207
208		// should we draw the framework, or just a header
209		$do_framework = isset($_GET['cd']) && $_GET['cd'] === 'yes';
210
211		// load clientside link registry to framework only
212		if (!isset($GLOBALS['egw_info']['flags']['js_link_registry']))
213		{
214			$GLOBALS['egw_info']['flags']['js_link_registry'] = $do_framework;
215		}
216		// Loader
217		$this->tpl->set_var('loader_text', lang('please wait...'));
218
219		if ($do_framework)
220		{
221			//echo __METHOD__.__LINE__.' do framework ...'.'<br>';
222			// framework javascript classes only need for framework
223			if (Api\Header\UserAgent::mobile() || $GLOBALS['egw_info']['user']['preferences']['common']['theme'] == 'mobile')
224			{
225				self::includeJS('.', 'fw_mobile', static::JS_INCLUDE_APP);
226			}
227			else
228			{
229				self::includeJS('.', 'fw_'.static::APP, static::JS_INCLUDE_APP);
230			}
231			Api\Cache::unsetSession(__CLASS__,'sidebox_md5');	// sideboxes need to be send again
232
233			$extra['navbar-apps'] = $this->get_navbar_apps($_SERVER['REQUEST_URI']);
234		}
235		// for an url WITHOUT cd=yes --> load framework if not yet loaded:
236		// - if top has framework object, we are all right
237		// - if not we need to check if we have an opener (are a popup window)
238		// - as popups can open further popups, we need to decend all the way down until we find a framework
239		// - only if we cant find a framework in all openers, we redirect to create a new framework
240		if(!$do_framework)
241		{
242			// fetch sidebox from application and set it in extra data, if we are no popup
243			if (!$GLOBALS['egw_info']['flags']['nonavbar'])
244			{
245				$this->do_sidebox();
246			}
247			// for remote manual never check/create framework
248			if (!in_array($GLOBALS['egw_info']['flags']['currentapp'], array('manual', 'login', 'logout', 'sitemgr')))
249			{
250				if (empty($GLOBALS['egw_info']['flags']['java_script'])) $GLOBALS['egw_info']['flags']['java_script']='';
251				// eT2 sets $GLOBALS['egw_info']['flags']['nonavbar'] === 'popup' for popups, Etemplate::exec($outputmode === 2)
252				$extra['check-framework'] = $_GET['cd'] !== 'no' && $GLOBALS['egw_info']['flags']['nonavbar'] !== 'popup';
253			}
254		}
255		// allow apps to load JavaScript or CSS files, knowing we're loading the framework or not
256		Api\Hooks::process(array(
257			'location' => 'framework_header',
258			'popup' => !$do_framework,
259			'extra' => &$extra,
260		), [], true);
261
262		$this->tpl->set_var($this->_get_header($extra));
263		$content = $this->tpl->fp('out','head').$content;
264
265		if (!$do_framework)
266		{
267			return $content;
268		}
269
270		// topmenu
271		$vars = $this->_get_navbar($apps = $this->_get_navbar_apps());
272		$this->tpl->set_var($this->topmenu($vars,$apps));
273
274		// hook after_navbar (eg. notifications)
275		$this->tpl->set_var('hook_after_navbar',$this->_get_after_navbar());
276
277		//Global sidebar width
278		$this->tpl->set_var('sidebox_width', self::get_global_sidebar_width());
279		$this->tpl->set_var('sidebox_min_width', self::MIN_SIDEBAR_WIDTH);
280
281		// add framework div's
282		$this->tpl->set_var($this->_get_footer());
283		$content .= $this->tpl->fp('out','framework');
284		$content .= self::footer(false);
285
286		echo $content;
287		exit();
288	}
289
290	private $topmenu_items;
291	private $topmenu_info_items;
292
293	/**
294	 * Compile entries for topmenu:
295	 * - regular items: links
296	 * - info items
297	 *
298	 * @param array $vars
299	 * @param array $apps
300	 * @return array
301	 */
302	function topmenu(array $vars,array $apps)
303	{
304		$this->topmenu_items = $this->topmenu_info_items = array();
305
306		parent::topmenu($vars,$apps);
307		$vars['topmenu_items'] = "<ul>\n<li>".implode("</li>\n<li>",$this->topmenu_items)."</li>\n</ul>";
308		$vars['topmenu_info_items'] = '';
309		foreach($this->topmenu_info_items as $id => $item)
310		{
311			$vars['topmenu_info_items'] .= '<div class="topmenu_info_item"'.
312				(is_numeric($id) ? '' : ' id="topmenu_info_'.$id.'"').'>'.$item."</div>\n";
313		}
314		$this->topmenu_items = $this->topmenu_info_items = null;
315
316		return $vars;
317	}
318
319	/**
320	* called by hooks to add an icon in the topmenu info location
321	*
322	* @param string $id unique element id
323	* @param string $icon_src src of the icon image. Make sure this nog height then 18pixels
324	* @param string $iconlink where the icon links to
325	* @param booleon $blink set true to make the icon blink
326	* @param mixed $tooltip string containing the tooltip Api\Html, or null of no tooltip
327	* @todo implement in a reasonable way for jdots
328	* @return void
329	*/
330	function topmenu_info_icon($id,$icon_src,$iconlink,$blink=false,$tooltip=null)
331	{
332		unset($id,$icon_src,$iconlink,$blink,$tooltip);	// not used
333		// not yet implemented, only used in admin/inc/hook_topmenu_info.inc.php to notify about pending updates
334	}
335
336	/**
337	* Add menu items to the topmenu template class to be displayed
338	*
339	* @param array $app application data
340	* @param mixed $alt_label string with alternative menu item label default value = null
341	* @param string $urlextra string with alternate additional code inside <a>-tag
342	* @access protected
343	* @return void
344	*/
345	function _add_topmenu_item(array $app_data,$alt_label=null)
346	{
347		switch($app_data['name'])
348		{
349			case 'manual':
350				$app_data['url'] = "javascript:callManual();";
351				break;
352
353			default:
354				if (Api\Header\UserAgent::mobile() || $GLOBALS['egw_info']['user']['preferences']['common']['theme'] == 'mobile')
355				{
356					break;
357				}
358				if (strpos($app_data['url'],'logout.php') === false && substr($app_data['url'], 0, 11) != 'javascript:')
359				{
360					$app_data['url'] = "javascript:egw_link_handler('".$app_data['url']."','".
361						(isset($GLOBALS['egw_info']['user']['apps'][$app_data['name']]) ?
362							$app_data['name'] : 'about')."')";
363				}
364		}
365		$id = $app_data['id'] ? $app_data['id'] : ($app_data['name'] ? $app_data['name'] : $app_data['title']);
366		$title =  htmlspecialchars($alt_label ? $alt_label : $app_data['title']);
367		$this->topmenu_items[] = '<a id="topmenu_' . $id . '" href="'.htmlspecialchars($app_data['url']).'" title="'.$app_data['title'].'">'.$title.'</a>';
368	}
369
370	/**
371	 * Add info items to the topmenu template class to be displayed
372	 *
373	 * @param string $content Api\Html of item
374	 * @param string $id = null
375	 * @access protected
376	 * @return void
377	 */
378	function _add_topmenu_info_item($content, $id=null)
379	{
380		if(strpos($content,'menuaction=admin.admin_accesslog.sessions') !== false)
381		{
382			$content = preg_replace('/href="([^"]+)"/',"href=\"javascript:egw_link_handler('\\1','admin')\"",$content);
383		}
384		if ($id)
385		{
386			$this->topmenu_info_items[$id] = $content;
387		}
388		else
389		{
390			$this->topmenu_info_items[] = $content;
391		}
392	}
393
394	/**
395	 * Change timezone
396	 *
397	 * @param string $tz
398	 */
399	static function ajax_tz_selection($tz)
400	{
401		Api\DateTime::setUserPrefs($tz);	// throws exception, if tz is invalid
402
403		$GLOBALS['egw']->preferences->read_repository();
404		$GLOBALS['egw']->preferences->add('common','tz',$tz);
405		$GLOBALS['egw']->preferences->save_repository();
406	}
407
408	/**
409	 * Flag if do_sidebox() was called
410	 *
411	 * @var boolean
412	 */
413	protected $sidebox_done = false;
414
415	/**
416	 * Returns the Api\Html from the body-tag til the main application area (incl. opening div tag)
417	 *
418	 * jDots does NOT use a navbar, but it tells us that application might want a sidebox!
419	 *
420	 * @return string
421	 */
422	function navbar()
423	{
424		$header = '';
425		if (!self::$header_done)
426		{
427			$header = $this->header();
428		}
429		$GLOBALS['egw_info']['flags']['nonavbar'] = false;
430
431		if (!$this->sidebox_done && self::$header_done)
432		{
433			$this->do_sidebox();
434			return $header.'<span id="late-sidebox" data-setSidebox="'.htmlspecialchars(json_encode(self::$extra['setSidebox'])).'"/>';
435		}
436
437		return $header;
438	}
439
440	/**
441	 * Set sidebox content in self::$data['setSidebox']
442	 *
443	 * We store in the session the md5 of each sidebox menu already send to client.
444	 * If the framework get reloaded, that list gets cleared in header();
445	 * Most apps never change sidebox, so we not even need to generate it more then once.
446	 */
447	function do_sidebox()
448	{
449		$this->sidebox_done = true;
450
451		$app = $GLOBALS['egw_info']['flags']['currentapp'];
452
453		// only send admin sidebox, for admin index url (when clicked on admin),
454		// not for other admin pages, called eg. from sidebox menu of other apps
455		// --> that way we always stay in the app, and NOT open admin sidebox for an app tab!!!
456		if ($app == 'admin' && substr($_SERVER['PHP_SELF'],-16) != '/admin/index.php' &&
457			$_GET['menuaction'] != 'admin.admin_ui.index')
458		{
459			//error_log(__METHOD__."() app=$app, menuaction=$_GET[menuaction], PHP_SELF=$_SERVER[PHP_SELF] --> sidebox request ignored");
460			return;
461		}
462		$md5_session =& Api\Cache::getSession(__CLASS__,'sidebox_md5');
463
464		//Set the sidebox content
465		$sidebox = $this->get_sidebox($app);
466		$md5 = md5(json_encode($sidebox));
467
468		if ($md5_session[$app] !== $md5)
469		{
470			//error_log(__METHOD__."() header changed md5_session[$app]!=='$md5' --> setting it on self::\$extra[setSidebox]");
471			$md5_session[$app] = $md5;	// update md5 in session
472			self::$extra['setSidebox'] = array($app, $sidebox, $md5);
473		}
474		//else error_log(__METHOD__."() md5_session[$app]==='$md5' --> nothing to do");
475	}
476
477	/**
478	 * Return true if we are rendering the top-level EGroupware window
479	 *
480	 * A top-level EGroupware window has a navbar: eg. no popup and for a framed template (jdots) only frameset itself
481	 *
482	 * @return boolean $consider_navbar_not_yet_called_as_true=true ignored by jdots, we only care for cd=yes GET param
483	 * @return boolean
484	 */
485	public function isTop($consider_navbar_not_yet_called_as_true=true)
486	{
487		unset($consider_navbar_not_yet_called_as_true);	// not used
488		return isset($_GET['cd']) && $_GET['cd'] === 'yes';
489	}
490
491	/**
492	 * Array containing sidebox menus by applications and menu-name
493	 *
494	 * @var array
495	 */
496	protected $sideboxes;
497
498	/**
499	 * Should calls the first call to self::sidebox create an opened menu
500	 *
501	 * @var boolean
502	 */
503	protected $sidebox_menu_opened = true;
504
505	/**
506	 * Callback for sideboxes hooks, collects the data in a private var
507	 *
508	 * @param string $appname
509	 * @param string $menu_title
510	 * @param array $file
511	 * @param string $type = null 'admin', 'preferences', 'favorites', ...
512	 */
513	public function sidebox($appname,$menu_title,$file,$type=null)
514	{
515		if (!isset($file['menuOpened'])) $file['menuOpened'] = (boolean)$this->sidebox_menu_opened;
516		//error_log(__METHOD__."('$appname', '$menu_title', file[menuOpened]=$file[menuOpened], ...) this->sidebox_menu_opened=$this->sidebox_menu_opened");
517		$this->sidebox_menu_opened = false;
518
519		// fix app admin menus to use admin.admin_ui.index loader
520		if (($type == 'admin' || $menu_title == lang('Admin')) && $appname != 'admin')
521		{
522			foreach($file as &$link)
523			{
524				$ajax = null;
525				preg_match('/ajax=(true|false)/', $link, $ajax);
526				$link = preg_replace("/^(javascript:egw_link_handler\(')(.*)menuaction=([^&]+)(.*)(','[^']+'\))$/",
527					'$1$2menuaction=admin.admin_ui.index&load=$3$4&ajax=' . ($ajax[1] ? $ajax[1] : 'true') .'\',\'admin\')', $link);
528			}
529		}
530
531		$this->sideboxes[$appname][$menu_title] = $file;
532	}
533
534	/**
535	 * Return sidebox data for an application
536	 *
537	 * @param $appname
538	 * @return array of array(
539	 * 		'menu_name' => (string),	// menu name, currently md5(title)
540	 * 		'title'     => (string),	// translated title to display
541	 * 		'opened'    => (boolean),	// menu opend or closed
542	 *  	'entries'   => array(
543	 *			array(
544	 *				'lang_item' => translated menu item or Api\Html, i item_link === false
545	 * 				'icon_or_star' => url of bullet images, or false for none
546	 *  			'item_link' => url or false (lang_item contains complete html)
547	 *  			'target' => target attribute fragment, ' target="..."'
548	 *			),
549	 *			// more entries
550	 *		),
551	 * 	),
552	 *	array (
553	 *		// next menu
554	 *	)
555	 */
556	public function get_sidebox($appname)
557	{
558		if (!isset($this->sideboxes[$appname]))
559		{
560			self::$link_app = $appname;
561			// allow other apps to hook into sidebox menu of an app, hook-name: sidebox_$appname
562			$this->sidebox_menu_opened = true;
563			Api\Hooks::process('sidebox_'.$appname,array($appname),true);	// true = call independent of app-permissions
564
565			// calling the old hook
566			$this->sidebox_menu_opened = true;
567			Api\Hooks::single('sidebox_menu',$appname);
568			self::$link_app = null;
569
570			// allow other apps to hook into sidebox menu of every app: sidebox_all
571			Api\Hooks::process('sidebox_all',array($GLOBALS['egw_info']['flags']['currentapp']),true);
572		}
573		//If there still is no sidebox content, return null here
574		if (!isset($this->sideboxes[$appname]))
575		{
576			return null;
577		}
578
579		$data = array();
580		$sendToBottom = array();
581		foreach($this->sideboxes[$appname] as $menu_name => &$file)
582		{
583			$current_menu = array(
584				'menu_name' => md5($menu_name),	// can contain Api\Html tags and javascript!
585				'title' => $menu_name,
586				'entries' => array(),
587				'opened' => (boolean)$file['menuOpened'],
588			);
589			foreach($file as $item_text => $item_link)
590			{
591				if ($item_text === 'menuOpened' || $item_text === 'sendToBottom' ||// flag, not menu entry
592					$item_text === '_NewLine_' || $item_link === '_NewLine_')
593				{
594					continue;
595				}
596				if (strtolower($item_text) == 'grant access' && $GLOBALS['egw_info']['server']['deny_user_grants_access'])
597				{
598					continue;
599				}
600
601				$var = array();
602				$var['icon_or_star'] = $GLOBALS['egw_info']['server']['webserver_url'] . $this->template_dir.'/images/bullet.svg';
603				$var['target'] = '';
604				if(is_array($item_link))
605				{
606					if(isset($item_link['icon']))
607					{
608						$app = isset($item_link['app']) ? $item_link['app'] : $appname;
609						$var['icon_or_star'] = $item_link['icon'] ? Api\Image::find($app,$item_link['icon']) : False;
610					}
611					$var['lang_item'] = isset($item_link['no_lang']) && $item_link['no_lang'] ? $item_link['text'] : lang($item_link['text']);
612					$var['item_link'] = $item_link['link'];
613					if ($item_link['target'])
614					{
615						// we only support real targets not Api\Html markup with target in it
616						if (strpos($item_link['target'], 'target=') === false &&
617							strpos($item_link['target'], '"') === false)
618						{
619							$var['target'] = $item_link['target'];
620						}
621					}
622					if ($item_link['disableIfNoEPL'] && !$GLOBALS['egw_info']['apps']['stylite'])
623					{
624						$var['disableIfNoEPL'] = true;
625					}
626				}
627				else
628				{
629					$var['lang_item'] = lang($item_text);
630					$var['item_link'] = $item_link;
631				}
632				$current_menu['entries'][] = $var;
633			}
634
635			if ($file['sendToBottom'])
636			{
637				$sendToBottom[] = $current_menu;
638			}
639			else
640			{
641				$data[] = $current_menu;
642			}
643		}
644		return array_merge($data, $sendToBottom);
645	}
646
647	/**
648	 * Ajax callback which is called whenever a previously opened tab is closed or
649	 * opened.
650	 *
651	 * @param $tablist is an array which contains each tab as an associative array
652	 *   with the keys 'appName' and 'active'
653	 */
654	public static function ajax_tab_changed_state($tablist)
655	{
656		$tabs = array();
657		foreach($tablist as $data)
658		{
659			$tabs[] = $data['appName'];
660			if ($data['active']) $active = $data['appName'];
661		}
662		// send app a notification, that it's tab got closed
663		// used eg. in phpFreeChat to leave the chat
664		if (($old_tabs = Api\Cache::getSession(__CLASS__, 'open_tabs')))
665		{
666			foreach(array_diff(explode(',',$old_tabs),$tabs) as $app)
667			{
668				//error_log("Tab '$app' closed, old_tabs=$old_tabs");
669				Api\Hooks::single(array(
670					'location' => 'tab_closed',
671					'app' => $app,
672				), $app);
673			}
674		}
675		$open = implode(',',$tabs);
676
677		if ($open != $GLOBALS['egw_info']['user']['preferences']['common']['open_tabs'] ||
678			$active != $GLOBALS['egw_info']['user']['preferences']['common']['active_tab'])
679		{
680			//error_log(__METHOD__.'('.array2string($tablist).") storing common prefs: open_tabs='$tabs', active_tab='$active'");
681			Api\Cache::setSession(__CLASS__, 'open_tabs', $open);
682			$GLOBALS['egw']->preferences->read_repository();
683			$GLOBALS['egw']->preferences->add('common', 'open_tabs', $open);
684			$GLOBALS['egw']->preferences->add('common', 'active_tab', $active);
685			$GLOBALS['egw']->preferences->save_repository(true);
686		}
687	}
688
689	/**
690	 * Return sidebox data for an application
691	 *
692	 * Format see get_sidebox()
693	 *
694	 * @param $appname
695	 */
696	public function ajax_sidebox($appname, $md5)
697	{
698		// dont block session, while we read sidebox, they are not supposed to change something in the session
699		$GLOBALS['egw']->session->commit_session();
700
701		$response = Api\Json\Response::get();
702		$sidebox = $this->get_sidebox($appname);
703		$encoded = json_encode($sidebox);
704		$new_md5 = md5($encoded);
705
706		$response_array = array();
707		$response_array['md5'] = $new_md5;
708
709		if ($new_md5 != $md5)
710		{
711			//TODO: Add some proper solution to be able to attach the already
712			//JSON data to the response in order to gain some performace improvements.
713			$response_array['data'] = $sidebox;
714		}
715
716		$response->data($response_array);
717	}
718
719	/**
720	 * Stores the width of the sidebox menu depending on the sidebox menu settings
721	 * @param $appname the name of the application
722	 * @param $width the width set
723	 */
724	public static function ajax_sideboxwidth($appname, $width)
725	{
726		//error_log(__METHOD__."($appname, $width)");
727		//Check whether the supplied parameters are valid
728		if (is_int($width) && $GLOBALS['egw_info']['user']['apps'][$appname])
729		{
730			self::set_sidebar_width($appname, $width);
731		}
732	}
733
734	/**
735	 * Stores the user defined sorting of the applications inside the preferences
736	 *
737	 * @param array $apps
738	 */
739	public static function ajax_appsort(array $apps)
740	{
741		$order = array();
742		$i = 0;
743
744		//Parse the "$apps" array for valid content (security)
745		foreach($apps as $app)
746		{
747			//Check whether the app really exists and add it to the $app_arr var
748			if ($GLOBALS['egw_info']['user']['apps'][$app])
749			{
750				$order[$app] = $i;
751				$i++;
752			}
753		}
754
755		//Store the order array inside the common user Api\Preferences
756		$GLOBALS['egw']->preferences->read_repository();
757		$GLOBALS['egw']->preferences->add('common', 'user_apporder', serialize($order));
758		$GLOBALS['egw']->preferences->save_repository(true);
759	}
760
761	/**
762	 * Prepare an array with apps used to render the navbar
763	 *
764	 * @return array of array(
765	 *  'name'  => app / directory name
766	 * 	'title' => translated application title
767	 *  'url'   => url to call for index
768	 *  'icon'  => icon name
769	 *  'icon_app' => application of icon
770	 *  'icon_hover' => hover-icon, if used by template
771	 *  'target'=> ' target="..."' attribute fragment to open url in target, popup or ''
772	 * )
773	 */
774	public function navbar_apps()
775	{
776		$apps = parent::_get_navbar_apps();
777
778		//Add its sidebox width to each app
779		foreach ($apps as $app => &$data)
780		{
781			$data['sideboxwidth'] = self::get_sidebar_width($app);
782			// overwrite icon with svg, if supported by browser
783			unset($data['icon_hover']);	// not used in jdots
784		}
785
786		unset($apps['logout']);	// never display it
787		if (isset($apps['about'])) $apps['about']['noNavbar'] = true;
788		if (isset($apps['preferences'])) $apps['preferences']['noNavbar'] = true;
789		if (isset($apps['manual'])) $apps['manual']['noNavbar'] = true;
790		if (isset($apps['home'])) $apps['home']['noNavbar'] = true;
791
792		// no need for website icon, if we have sitemgr
793		if (isset($apps['sitemgr']) && isset($apps['sitemgr-link']))
794		{
795			unset($apps['sitemgr-link']);
796		}
797
798		return $apps;
799	}
800
801	/**
802	 * Prepare an array with apps used to render the navbar
803	 *
804	 * @param string $url contains the current url on the client side. It is used to
805	 *  determine whether the default app/home should be opened on the client
806	 *  or whether a specific application-url has been given.
807	 *
808	 * @return array of array(
809	 *  'name'  => app / directory name
810	 * 	'title' => translated application title
811	 *  'url'   => url to call for index
812	 *  'icon'  => icon name
813	 *  'icon_app' => application of icon
814	 *  'icon_hover' => hover-icon, if used by template
815	 *  'target'=> ' target="..."' attribute fragment to open url in target, popup or ''
816	 *  'opened' => unset or false if the tab should not be opened, otherwise the numeric position in the tab list
817	 *  'active' => true if this tab should be the active one when it is restored, otherwise unset or false
818	 *  'openOnce' => unset or the url which will be opened when the tab is restored
819	 * )
820	 */
821	protected function get_navbar_apps($url)
822	{
823		$apps = $this->navbar_apps();
824
825		// open tab for default app, if no other tab is set
826		if (!($default_app = $GLOBALS['egw_info']['user']['preferences']['common']['default_app']))
827		{
828			$default_app = 'calendar';
829		}
830		if (isset($apps[$default_app]))
831		{
832			$apps[$default_app]['isDefault'] = true;
833		}
834
835		// check if user called a specific url --> open it as active tab
836		$last_direct_url =& Api\Cache::getSession(__CLASS__, 'last_direct_url');
837		if ($last_direct_url)
838		{
839			$url = $last_direct_url;
840			$active_tab = self::app_from_url($last_direct_url);
841		}
842		else if (strpos($url, 'menuaction') > 0)
843		{
844			// Coming in with a specific URL, save it and redirect to index.php
845			// so reloads work nicely, but strip cd=yes or we'll get the framework again
846			$last_direct_url = preg_replace('/[&?]cd=yes/','',$url);
847			Api\Framework::redirect_link('/index.php?cd=yes');
848		}
849		else
850		{
851			$active_tab = $GLOBALS['egw_info']['user']['preferences']['common']['active_tab'];
852			if (!$active_tab) $active_tab = $default_app;
853		}
854
855		//self::app_from_url might return an application the user has no rights
856		//for or may return an application that simply does not exist. So check first
857		//whether the $active_tab really exists in the $apps array.
858		if ($active_tab && array_key_exists($active_tab, $apps))
859		{
860			// Do not remove cd=yes if it's an ajax=true app
861			if (strpos( $apps[$active_tab]['url'],'ajax=true') !== False)
862			{
863				$url = preg_replace('/[&?]cd=yes/','',$url);
864			}
865			if($last_direct_url)
866			{
867				$apps[$active_tab]['openOnce'] = $url;
868			}
869			$store_prefs = true;
870		}
871
872		// if we have the open tabs in the session, use it instead the maybe forced common prefs open_tabs
873		if (!($open_tabs = Api\Cache::getSession(__CLASS__, 'open_tabs')))
874		{
875			$open_tabs = $GLOBALS['egw_info']['user']['preferences']['common']['open_tabs'];
876		}
877		$open_tabs = $open_tabs ? explode(',',$open_tabs) : array();
878		if ($active_tab && !in_array($active_tab,$open_tabs))
879		{
880			$open_tabs[] = $active_tab;
881			$store_prefs = true;
882		}
883		if ($store_prefs)
884		{
885			$GLOBALS['egw']->preferences->read_repository();
886			$GLOBALS['egw']->preferences->add('common', 'open_tabs', implode(',',$open_tabs));
887			$GLOBALS['egw']->preferences->add('common', 'active_tab', $active_tab);
888			$GLOBALS['egw']->preferences->save_repository(true);
889		}
890
891		//error_log(__METHOD__."('$url') url_tab='$url_tab', active_tab=$active_tab, open_tabs=".array2string($open_tabs));
892		// Restore Tabs
893		foreach($open_tabs as $n => $app)
894		{
895			if (isset($apps[$app]))		// user might no longer have app rights
896			{
897				$apps[$app]['opened'] = $n;
898				if ($app == $active_tab)
899				{
900					$apps[$app]['active'] = true;
901				}
902			}
903		}
904		return array_values($apps);
905	}
906
907	/**
908	 * Have we output the footer
909	 *
910	 * @var boolean
911	 */
912	static private $footer_done;
913
914	/**
915	 * Returns the Api\Html from the closing div of the main application area to the closing html-tag
916	 *
917	 * @param boolean $no_framework = true
918	 * @return string
919	 */
920	function footer($no_framework=true)
921	{
922		//error_log(__METHOD__."($no_framework) footer_done=".array2string(self::$footer_done).' '.function_backtrace());
923		if (self::$footer_done) return;	// prevent (multiple) footers
924		self::$footer_done = true;
925
926		if (!isset($GLOBALS['egw_info']['flags']['nofooter']) || !$GLOBALS['egw_info']['flags']['nofooter'])
927		{
928			if ($no_framework && $GLOBALS['egw_info']['user']['preferences']['common']['show_generation_time'])
929			{
930				$vars = $this->_get_footer();
931				$footer = "\n".$vars['page_generation_time']."\n";
932			}
933		}
934		return $footer.
935			$GLOBALS['egw_info']['flags']['need_footer']."\n".	// eg. javascript, which need to be at the end of the page
936			"</body>\n</html>\n";
937	}
938
939	/**
940	 * Return javascript (eg. for onClick) to open manual with given url
941	 *
942	 * @param string $url
943	 * @return string
944	 */
945	function open_manual_js($url)
946	{
947		return "callManual('$url')";
948	}
949
950	/**
951	 * JSON reponse object
952	 *
953	 * If set output is requested for an ajax response --> no header, navbar or footer
954	 *
955	 * @var Api\Json\Response
956	 */
957	public $response;
958
959	/**
960	 * Run a link via ajax, returning content via egw_json_response->data()
961	 *
962	 * This behavies like /index.php, but returns the content via json.
963	 *
964	 * @param string $link
965	 */
966	public static function ajax_exec($link)
967	{
968		$parts = parse_url($link);
969		$_SERVER['REQUEST_URI'] = $_SERVER['SCRIPT_NAME'] = $parts['path'];
970		if ($parts['query'])
971		{
972			$_SERVER['REQUEST_URI'] = '?'.$parts['query'];
973			parse_str($parts['query'],$_GET);
974			$_REQUEST = $_GET;	// some apps use $_REQUEST to check $_GET or $_POST
975		}
976
977		if (!isset($_GET['menuaction']))
978		{
979			throw new Api\Exception\WrongParameter(__METHOD__."('$link') no menuaction set!");
980		}
981		// set session action
982		$GLOBALS['egw']->session->set_action('Ajax: '.$_GET['menuaction']);
983
984		list($app,$class,$method) = explode('.',$_GET['menuaction']);
985
986		if (!isset($GLOBALS['egw_info']['user']['apps'][$app]))
987		{
988			throw new Api\Exception\NoPermission\App($app);
989		}
990		$GLOBALS['egw_info']['flags']['currentapp'] = $app;
991
992		$GLOBALS['egw']->framework->response = Api\Json\Response::get();
993
994		$GLOBALS[$class] = $obj = CreateObject($app.'.'.$class);
995
996		if(!is_array($obj->public_functions) || !$obj->public_functions[$method])
997		{
998			throw new Api\Exception\NoPermission("Bad menuaction {$_GET['menuaction']}, not listed in public_functions!");
999		}
1000		// dont send header and footer
1001		self::$header_done = self::$footer_done = true;
1002
1003		// flag to indicate target of output e.g. _tab
1004		if ($_GET['fw_target'])
1005		{
1006			Api\Cache::unsetSession(__CLASS__,'sidebox_md5');	// sideboxes need to be send again
1007			$GLOBALS['egw']->framework->set_extra('fw','target',$_GET['fw_target']);
1008		}
1009
1010		// need to call do_sidebox, as header() with $header_done does NOT!
1011		$GLOBALS['egw']->framework->do_sidebox();
1012
1013		// send Api\Preferences, so we dont need to request them in a second ajax request
1014		$GLOBALS['egw']->framework->response->call('egw.set_preferences',
1015			(array)$GLOBALS['egw_info']['user']['preferences'][$app], $app);
1016
1017		// call application menuaction
1018		ob_start();
1019		$obj->$method();
1020		$output .= ob_get_contents();
1021		ob_end_clean();
1022
1023		// add registered css and javascript to the response
1024		self::include_css_js_response();
1025
1026		// add output if present
1027		if ($output)
1028		{
1029			$GLOBALS['egw']->framework->response->data($output);
1030		}
1031	}
1032
1033	/**
1034	 * Apps available for mobile, if admin did not configured something else
1035	 * (needs to kept in sync with list in phpgwapi/js/framework/fw_mobile.js!)
1036	 *
1037	 * Constant is read by admin_hooks::config to set default for fw_mobile_app_list.
1038	 */
1039	const DEFAULT_MOBILE_APPS = 'calendar,infolog,timesheet,resources,addressbook,projectmanager,tracker,mail,filemanager';
1040}
1041