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