true ); /** * Main UI - generates the container, and aggregates all * the portlets from the applications */ public function index() { self::setup_default_home(); // CSS for Gridster grid layout Framework::includeCSS('/vendor/npm-asset/gridster/dist/jquery.gridster.css'); $template = new Etemplate('home.index'); // Get a list of portlets $content = array( 'portlets' => $this->get_user_portlets($template) ); $template->setElementAttribute('home.index','actions',$this->get_actions()); $GLOBALS['egw_info']['flags']['app_header'] = lang('home'); $GLOBALS['egw_info']['flags']['currentapp'] = 'home'; // home screen message Api\Translation::add_app('mainscreen'); $greeting = Api\Translation::translate('mainscreen_message',false,''); if($greeting == 'mainscreen_message'|| empty($greeting)) { Api\Translation::add_app('mainscreen','en'); // trying the en one $greeting = Api\Translation::translate('mainscreen_message',false,''); } if(!($greeting == 'mainscreen_message'|| empty($greeting))) { $content['mainscreen_message'] = $greeting; } $template->exec('home.home_ui.index', $content); // Now run the portlets themselves $attrs = array(); foreach($content['portlets'] as $portlet => $p_data) { $id = $p_data['id']; if(!$id) { continue; } try { $portlet = $this->get_portlet($id, $p_data, $content, $attrs, true); } catch (Exception $e) { error_log($e); } } } /** * Get a list of actions on the whole home page. Each portlet also has * its own actions * * @return array of actions */ protected function get_actions() { $portlets = $this->get_portlet_list(); $add_portlets = $portlets; $change_for_add = function(&$add_portlets) use (&$change_for_add) { foreach($add_portlets as $id => &$add) { if(is_array($add['children'])) { $change_for_add($add['children']); } if($id && !$add['children']) { $add['id'] = 'add_' . $id; $add['class'] = 'add_'.$id; } } }; $change_for_add($add_portlets); $actions = array( 'add' => array( 'type' => 'popup', 'caption' => 'Add', 'onExecute' => 'javaScript:app.home.add', 'children' => $add_portlets ), // Favorites are sortable which needs special handling ); // Add all known portlets as drop actions too. If there are multiple matches, there will be a menu $drop_execute = 'javaScript:app.home.add_from_drop'; foreach($portlets as $app => &$children) { // Home portlets - uses link system, so all apps that support that are accepted if(!$children['children']) { $children['class'] = $app; $children['onExecute'] = $drop_execute; $children['type'] = 'drop'; $actions["drop_$app"] = $children; } else { foreach($children['children'] as $portlet => $app_portlet) { if(!is_array($app_portlet)) continue; $app_portlet['class'] = $portlet; $app_portlet['id'] = 'drop_' . $app_portlet['id']; $app_portlet['onExecute'] = $drop_execute; $app_portlet['acceptedTypes'] = $app; $app_portlet['type'] = 'drop'; $actions["drop_$portlet"] = $app_portlet; } } } // For admins, add the ability to set current home as a default self::create_default_actions($actions); return $actions; } /** * Get a list of the user's portlets, and their associated values & settings, for display * * Actual portlet content is provided by each portlet. * @param template etemplate so attributes can be set */ protected function get_user_portlets(Etemplate &$template) { $portlets = array(); foreach((array)$GLOBALS['egw_info']['user']['preferences']['home']as $id => $context) { if(strpos($id,'portlet_') !== 0 || // Not a portlet in_array($id, array_keys($GLOBALS['egw_info']['user']['apps'])) || // Some other app put it's pref in here !is_array($context) // Not a valid portlet (probably user deleted a default) ) continue; $classname = $context['class']; // Avoid portlets for apps user can't use (eg. from defaults/forced) list($app,$other) = explode('_',$classname); if(!$GLOBALS['egw_info']['apps'][$app]) $app .='_'.$other; if(!$GLOBALS['egw_info']['user']['apps'][$app]) { continue; } $portlet = new $classname($context); $desc = $portlet->get_description(); $portlet_content = array( 'id' => $id, 'type' => $portlet->get_type() ) + $desc + $context; // Get settings // Exclude common attributes changed through UI and settings lacking a type $settings = $portlet->get_properties(); foreach($settings as $key => $setting) { if(is_array($setting) && !array_key_exists('type', $setting)) { unset($settings[$key]); } } $settings += $context; foreach(home_portlet::$common_attributes as $attr) { unset($settings[$attr]); } $portlet_content['settings'] = $settings; if(method_exists($portlet, "get_value")) { $portlet_content['value'] = $portlet->get_value(); } // Set actions // Must be after settings so actions can take settings into account $actions = $portlet->get_actions(); // Add in default for admins self::create_default_actions($actions, $id); $template->setElementAttribute("portlets[" . count($portlets) . "[$id]", 'actions', $actions); $portlets[] = $portlet_content; } // Add in legacy HTML home bits // TODO: DOM IDs still collide $this->get_legacy_portlets($portlets, $attributes); return $portlets; } /** * Load the needed info for one portlet, given the context * * @param context Array Settings to customize the portlet instance (size, entry, etc) * These are specific values for the portlet's properties. * @param content String HTML fragment to be displayed - will be set by the portlet * @param attributes Array Settings that can be customized on a per-portlet basis - will be set * @param full_exec Boolean If set, the portlet etemplates should use mode 2, if not use mode -1 * @return home_portlet The portlet object that created the content */ protected function get_portlet($id, &$context, &$content, &$attributes, $full_exec = false) { if(!$context['class']) $context['class'] = 'home_link_portlet'; // This should be set already, but just in case the execution path // is different from normal... if(Api\Json\Response::isJSONResponse()) { $GLOBALS['egw']->framework->response = Api\Json\Response::get(); } $classname = $context['class']; $portlet = new $classname($context, $full_exec); $desc = $portlet->get_description(); // Pre-set up etemplate so it only needs done once $etemplate = new Etemplate(); // Exclude common attributes changed through UI and settings lacking a type $settings = $portlet->get_properties(); foreach($settings as $key => $setting) { if(is_array($setting) && !array_key_exists('type',$setting)) unset($settings[$key]); } $settings += $context; foreach(home_portlet::$common_attributes as $attr) { unset($settings[$attr]); } $attributes = array( 'title' => $desc['title'], 'color' => $settings['color'] ?: "", 'settings' => $settings, 'actions' => $portlet->get_actions(), ); // Add in default settings self::create_default_actions($attributes['actions'], $id); // Set any provided common attributes (size, etc) foreach(home_portlet::$common_attributes as $name) { if(array_key_exists($name, $context)) { $attributes[$name] = $context[$name]; } } foreach($attributes as $attr => $value) { $etemplate->setElementAttribute($id, $attr, $value); } // Make sure custom javascript is loaded $appname = $context['appname']; if(!$appname) { list($app) = explode('_',$classname); if($GLOBALS['egw_info']['apps'][$app]) { $appname = $app; } } //Framework::includeJS('', $classname, $appname ? $appname : 'home'); if($full_exec) { $content = $portlet->exec($id, $etemplate, $full_exec ? 2 : -1); Etemplate::reset_request(); } return $portlet; } /** * Get a list of pre-etemplate2 home hook content according to the individual * application preferences. If we find a preference that indicates the user * wants some content, we make a portlet for that app using the home_legacy_portlet, * which fetches content from the home hook. */ protected function get_legacy_portlets(&$content, &$attributes) { $sorted_apps = array_keys($GLOBALS['egw_info']['user']['apps']); $portal_oldvarnames = array('mainscreen_showevents', 'homeShowEvents','homeShowLatest','mainscreen_showmail','mainscreen_showbirthdays','mainscreen_show_new_updated', 'homepage_display'); foreach($sorted_apps as $appname) { // If there's already [new] settings, or no preference, skip it if($content[$appname]) continue; $no_pref = true; foreach($portal_oldvarnames as $varcheck) { $thisd = $GLOBALS['egw_info']['user']['preferences'][$appname][$varcheck]; if($thisd) { $no_pref = false; break; } } if($no_pref || !Api\Hooks::exists('home', $appname)) { continue; } $context = array( 'class' => 'home_legacy_portlet', 'app' => $appname, 'type' => 'et2-portlet', 'width' => 8, 'height' => 3 ); $_content = ''; $_attributes = array(); $this->get_portlet($appname, $context, $_content, $_attributes); if(trim($_content)) { $content[$appname] = $_content; $attributes[$appname] = $_attributes; } } } /** * Get a list of all available portlets for add menu */ protected function get_portlet_list() { $list = Api\Cache::getTree('home', 'portlet_classes', function() { $list = array(); $classes = array(); // Ignore some problem files and base classes that shouldn't be options $ignore = array( '.','..', 'class.home_portlet.inc.php', 'class.home_legacy_portlet.inc.php', 'class.home_favorite_portlet.inc.php' ); // Look through all known classes for portlets - for now, they need 'portlet' in the file name foreach(array_keys($GLOBALS['egw_info']['apps']) as $appname) { if(in_array($appname, array('phpgwapi', 'felamimail'))) continue; $files = (array)@scandir(EGW_SERVER_ROOT . '/'.$appname .'/inc/'); if(!$files) continue; foreach($files as $entry) { if (!in_array($entry, $ignore) && substr($entry,-8) == '.inc.php' && strpos($entry,'portlet')) { list(,$classname) = explode('.', $entry); if(class_exists($classname) && in_array('home_portlet', class_parents($classname, false))) { $classes[$appname][] = $classname; } else { error_log("Could not load $classname from $entry"); } } } if(!$classes[$appname]) continue; // Build 'Add' actions for each discovered portlet. // Portlets from other apps go in sub-actions $add_to =& $list; if($classes[$appname] && $appname != 'home') { $list[$appname] = array( 'caption' => lang($appname), ); $add_to =& $list[$appname]['children']; } foreach($classes[$appname] as $portlet) { $context = ['id' => $portlet]; $instance = new $portlet($context); $desc = $instance->get_description(); $add_to[$portlet] = array( 'id' => $portlet, 'caption' => $desc['displayName'], 'hint' => $desc['description'], 'onExecute' => 'javaScript:app.home.add', 'acceptedTypes' => $instance->accept_drop(), 'allowOnMultiple' => $instance->accept_multiple() ); } } return $list; }, array(), 60); // Filter list by current user's permissions foreach(array_keys($list) as $appname) { if(in_array($appname, array_keys($GLOBALS['egw_info']['apps']))) { if(!in_array($appname, array_keys($GLOBALS['egw_info']['user']['apps']))) { unset($list[$appname]); } } } return $list; } /** * Create an action to set the portlet as default * * @param Array $actions Existing action list * @param String $portlet_id Provide the ID to have the checkbox set */ protected static function create_default_actions(&$actions, $portlet_id = null) { if($GLOBALS['egw_info']['user']['apps']['admin']) { $actions['add_default'] = array( 'type' => 'popup', 'caption' => 'Set as default', 'onExecute' => 'javaScript:app.home.set_default', 'group' => 'Admins', 'icon' => 'preference' ); // Customize for the given portlet if($portlet_id !== null) { foreach(array('forced','group','default') as $location) { $loc = $GLOBALS['egw']->preferences->$location; if($loc['home'][$portlet_id]) { // If it's forced, no point in setting default if($location == 'forced') { unset($actions['add_default']); } // If it's a group, we'd like to know which if($location == 'group') { $options = array('account_type' => 'groups'); $groups = Api\Accounts::link_query('',$options); foreach(array_keys($groups) as $gid) { $prefs = new Api\Preferences($gid); $prefs->read_repository(); if (isset($prefs->user['home'][$portlet_id])) { $location = $gid; break; } } } $actions['remove_default_'.$location] = array( 'type' => 'popup', 'caption' => lang('Remove as default for %1',is_numeric($location) ? Api\Accounts::id2name($location,'account_fullname') : $location), 'onExecute' => 'javaScript:app.home.set_default', 'group' => 'Admins', 'portlet_group' => $location ); } } } } // Change action for forced if($portlet_id && $GLOBALS['egw']->preferences->forced['home'][$portlet_id]) { // No one can remove it $actions['remove_portlet']['enabled'] = false; $actions['remove_portlet']['caption'] .= ' ('.lang('Forced') .')'; // Non-admins can't edit it if($actions['edit_settings'] && !$GLOBALS['egw_info']['user']['apps']['admin']) { $actions['edit_settings']['enabled'] = false; $actions['edit_settings']['visible'] = false; } } } /** * Update the settings for a particular portlet, and give updated content * * @param portlet_id String Unique ID (for the user) for a portlet * @param values Array List of property => value pairs * @param boolean|int|String $group False for current user, ID of the group to create the favorite for, or 'all' for all users * */ public function ajax_set_properties($portlet_id, $attributes, $values, $group = false) { //error_log(__METHOD__ . "($portlet_id, " .array2string($attributes).','.array2string($values).",$group)"); if(!$attributes) { $attributes = array(); } if(!$GLOBALS['egw_info']['user']['apps']['admin']) { if($group == 'forced') { // Quietly reject return; } // Not an admin, can only override. $group = false; } if($group && $GLOBALS['egw_info']['user']['apps']['admin']) { $prefs = new Api\Preferences(is_numeric($group) ? $group : $GLOBALS['egw_info']['user']['account_id']); } else { $prefs = $GLOBALS['egw']->preferences; } $type = is_numeric($group) ? "user" : $group; $prefs->read_repository(); $response = Api\Json\Response::get(); if($values =='~reload~') { $full_exec = true; $values = array(); } if($values == '~remove~') { // Already removed client side, needs to be removed permanently $default = $prefs->default_prefs('home',$portlet_id) || $prefs->group['home'][$portlet_id]; if($default) { // Can't delete forced - not a UI option though if(!$GLOBALS['egw']->preferences->forced['home'][$portlet_id]) { // Set a flag to override default instead of just delete $GLOBALS['egw']->preferences->add('home',$portlet_id, 'deleted'); $GLOBALS['egw']->preferences->save_repository(); } } else { $prefs->delete('home', $portlet_id); $response->data([]); } } else { $pref_data = $prefs->read(); $portlets = $pref_data['home']; // Remove some constant stuff that winds up here unset($values['edit_template']); unset($values['readonly']); unset($values['disabled']);unset($values['no_lang']); unset($values['actions']); unset($values['statustext']); unset($values['type']);unset($values['label']);unset($values['status']); unset($values['value']);unset($values['align']); // Get portlet settings, and merge new with old $context = array_merge((array)$portlets[$portlet_id], $values); $context['group'] = $group; // Handle add IDs $classname =& $context['class']; if(strpos($classname,'add_') == 0 && !class_exists($classname)) { $add = true; $classname = substr($classname, 4); } $content = null; $portlet = $this->get_portlet("home-index_portlets_" . $portlet_id, $context, $content, $attributes, $full_exec); $context['class'] = get_class($portlet); foreach($portlet->get_properties() as $property) { if($values[$property['name']]) { $context[$property['name']] = $values[$property['name']]; } elseif($portlets[$portlet_id][$property['name']]) { $context[$property['name']] = $portlets[$portlet_id][$property['name']]; } } // Update client side $update = array('attributes' => $attributes); // New portlet? Flag going straight to edit mode if($add) { $update['edit_settings'] = true; } // Send this back to the portlet widget $response->data($update); // Store for preference update $prefs->add('home', $portlet_id, $context, $type); } // Save updated preferences $prefs->save_repository(True,$type); } /** * Set the selected portlets as default for a group * * @param String $action 'add' or 'delete' * @param String[] $portlet_ids * @param int|String $group Group ID or 'default' or 'forced' */ public static function ajax_set_default($action, $portlet_ids, $group) { // Admins only if(!$GLOBALS['egw_info']['apps']['admin']) return; // Load the appropriate group if($group) { $prefs = new Api\Preferences(is_numeric($group) ? $group : $GLOBALS['egw_info']['user']['account_id']); } else { $prefs = $GLOBALS['egw']->preferences; } $prefs->read_repository(); $type = is_numeric($group) ? "user" : $group; if($action == 'add') { foreach($portlet_ids as $id) { Api\Json\Response::get()->call('egw.message', lang("Set default")); // Current user is setting the default, copy their settings $settings = $GLOBALS['egw_info']['user']['preferences']['home'][$id]; $settings['group'] = $group; $prefs->add('home',$id,$settings,$type); // Remove user's copy $GLOBALS['egw']->preferences->delete('home',$id); $GLOBALS['egw']->preferences->save_repository(true); } } else if ($action == "delete") { foreach($portlet_ids as $id) { Api\Json\Response::get()->call('egw.message', lang("Removed default")); $prefs->delete('home',$id, $type); } } $prefs->save_repository(false,$type); // Update Api\Preferences client side for consistency $prefs = $GLOBALS['egw']->preferences; $pref = $prefs->read_repository(); Api\Json\Response::get()->call('egw.set_preferences', (array)$pref['home'], 'home'); } /** * Name of config var to store installed default home screen */ const HOME_VERSION = 'home_version'; /** * Current version of home screen, there need to be a function matching that number! */ const CURRENT_HOME_VERSION = '14.3'; /** * Setup default home screen for 14.2 */ public static function setup_default_home_14_3() { $preferences = $GLOBALS['egw']->preferences; $preferences->read_repository(); $lang = $preferences->default['common']['lang']; if (empty($lang)) $lang = 'en'; Api\Translation::add_app('calendar', $lang); $weekview = lang('Weekview'); if ($GLOBALS['egw_info']['apps']['news_admin']) { $cats = new Api\Categories('', 'news_admin'); $cat_id_egw_org_news = $cats->name2id('egroupware.org'); } $app_prefs = array( 'home' => array( // current users week favorite 'portlet_setup142w' => array( 'id' => 'portlet_setup-142wp', 'class' => 'calendar_favorite_portlet', 'row' => 7, 'col' => 1, 'width' => 7, 'height' => 5, 'color' => '', 'favorite' => array( 'name' => $weekview, 'group' => 'default', 'state' => array( 'cat_id' => '0', 'filter' => 'default', 'owner' => 0, // current user 'sortby' => 'user', 'planner_days' => '0', 'view' => 'week', 'listview_days' => '', ), ), 'title' => $weekview, 'group' => 'default', 'appname' => 'calendar', ), // egroupware.org news 'portlet_setup142n' => array( 'row' => 7, 'col' => 8, 'width' => 8, 'height' => 5, 'color' => '', 'favorite' => array( 'name' => 'www.egroupware.org', 'group' => 'default', 'state' => array( 'selected' => array(), 'col_filter' => array( 'visible' => 'now', 'news_lang' => $lang == 'de' ? 'de' : 'en', 'news_submittedby' => '' ), 'filter' => $cat_id_egw_org_news, 'filter2' => 'content', 'search' => '', 'sort' => array( 'id' => 'news_date', 'asc' => false ), ), ), 'id' => 'portlet_setup142n', 'class' => 'news_admin_favorite_portlet', 'title' => 'www.egroupware.org', 'group' => 'default', 'appname' => 'news_admin' ), ), ); if (empty($cat_id_egw_org_news)) { unset($app_prefs['home']['portlet_setup142n']); } foreach($app_prefs as $app => $prefs) { foreach($prefs as $name => $value) { Api\Preferences::delete_preference($app, $name, 'default'); $preferences->add($app, $name, $value, 'default'); } } // remove tutorial from home, as it is now in sidebox Api\Preferences::delete_preference('home', 'portlet_setup142t', 'default'); $preferences->delete('home', 'portlet_setup142t', 'default'); // assigning saved preferences to egw_info, which is used for this request $GLOBALS['egw_info']['user']['preferences'] = $preferences->save_repository(null, 'default'); } /** * Check and if necessary setup default home screen * * Version of home-screen is stored in api config self::HOME_VERSION */ public static function setup_default_home() { switch($GLOBALS['egw_info']['server'][self::HOME_VERSION]) { case self::CURRENT_HOME_VERSION: return; // already up to date --> nothing to do default: call_user_func(array(__CLASS__, 'setup_default_home_'.str_replace('.', '_', self::CURRENT_HOME_VERSION))); Api\Config::save_value(self::HOME_VERSION, self::CURRENT_HOME_VERSION, 'phpgwapi'); break; } } }