From 8315cbfee0e485fbcf70e831d3c8671da5263031 Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Tue, 26 Apr 2016 14:38:08 +0000 Subject: [PATCH] move egw and applications class to api including (common_)functions.inc.php --- api/src/Egw.php | 601 +++++++ api/src/Egw/Applications.php | 109 ++ api/src/Egw/Base.php | 125 ++ api/src/autoload.php | 104 ++ api/src/loader.php | 143 ++ api/src/loader/common.php | 417 +++++ api/src/loader/exception.php | 195 ++ api/src/loader/security.php | 332 ++++ header.inc.php.template | 35 +- phpgwapi/inc/class.applications.inc.php | 109 +- phpgwapi/inc/class.egw.inc.php | 679 +------ phpgwapi/inc/common_functions.inc.php | 1986 +-------------------- phpgwapi/inc/deprecated_functions.inc.php | 931 ++++++++++ phpgwapi/inc/functions.inc.php | 137 +- 14 files changed, 2982 insertions(+), 2921 deletions(-) create mode 100644 api/src/Egw.php create mode 100755 api/src/Egw/Applications.php create mode 100644 api/src/Egw/Base.php create mode 100755 api/src/autoload.php create mode 100644 api/src/loader.php create mode 100755 api/src/loader/common.php create mode 100755 api/src/loader/exception.php create mode 100755 api/src/loader/security.php create mode 100755 phpgwapi/inc/deprecated_functions.inc.php diff --git a/api/src/Egw.php b/api/src/Egw.php new file mode 100644 index 0000000000..a16e79e240 --- /dev/null +++ b/api/src/Egw.php @@ -0,0 +1,601 @@ +ldap) + * { + * $GLOBALS['egw']->ldap = Api\Ldap::factory(); + * } + * You can now simply use $GLOBALS['egw']->ldap, and the egw class instanciates it for you on demand. + */ +class Egw extends Egw\Base +{ + /** + * Turn on debug mode. Will output additional data for debugging purposes. + * @var string + * @access public + */ + var $debug = 0; // This will turn on debugging information. + /** + * Instance of the account object + * + * @var Accounts + */ + var $accounts; + + /** + * Constructor: Instantiates the sub-classes + * + * @author RalfBecker@outdoor-training.de + * @param array $domain_names array with valid egw-domain names + */ + function __construct($domain_names=null) + { + $GLOBALS['egw'] =& $this; // we need to be immediately available there for the other classes we instantiate + $this->setup($domain_names,True); + } + + /** + * Called every time the constructor is called. Also called by sessions to ensure the correct db, + * in which case we do not recreate the session object. + * @author RalfBecker@outdoor-training.de (moved to setup() by milos@groupwhere.org + * @param array $domain_names array with valid egw-domain names + * @param boolean $createsessionobject True to create the session object (default=True) + */ + function setup($domain_names,$createsessionobject=True) + { + // create the DB-object + // as SiteMgr, Wiki, KnowledgeBase and probably more still use eg next_record(), we stick with Db\Deprecated for now + $this->db = new Db\Deprecated($GLOBALS['egw_info']['server']); + if ($this->debug) + { + $this->db->Debug = 1; + } + $this->db->set_app(Db::API_APPNAME); + + // check if eGW is already setup, if not redirect to setup/ + try { + $this->db->connect(); + $num_config = $this->db->select(Config::TABLE,'COUNT(config_name)',false,__LINE__,__FILE__)->fetchColumn(); + } + catch(Db\Exception\Connection $e) { + // ignore exception, get handled below + } + catch(Db\Exception\InvalidSql $e1) { + unset($e1); // not used + try { + $phpgw_config = $this->db->select('phpgw_config','COUNT(config_name)',false,__LINE__,__FILE__)->fetchColumn(); + } + catch (Db\Exception\InvalidSql $e2) { + unset($e2); // not used + // ignor error, get handled below + } + } + if (!$num_config) + { + // we check for the old table too, to not scare updating users ;-) + if ($phpgw_config) + { + throw new Exception('You need to update EGroupware before you can continue using it.',999); + } + if ($e) + { + throw new Db\Exception\Setup('Connection with '.$e->getMessage()."\n\n". + 'Maybe you not created a database for EGroupware yet.',999); + } + throw new Db\Exception\Setup('It appears that you have not created the database tables for EGroupware.',999); + } + // Set the DB's client charset if a system-charset is set and some other values needed by egw_cache (used in Config::read) + foreach($this->db->select(Config::TABLE,'config_name,config_value',array( + 'config_app' => 'phpgwapi', + 'config_name' => array('system_charset','install_id','temp_dir'), + ),__LINE__,__FILE__) as $row) + { + $GLOBALS['egw_info']['server'][$row['config_name']] = $row['config_value']; + } + if ($GLOBALS['egw_info']['server']['system_charset'] && $GLOBALS['egw_info']['server']['system_charset'] != 'utf-8') + { + $this->db->Link_ID->SetCharSet($GLOBALS['egw_info']['server']['system_charset']); + } + // load up the $GLOBALS['egw_info']['server'] array + $GLOBALS['egw_info']['server'] += Config::read('phpgwapi'); + + // if no server timezone set, use date_default_timezone_get() to determine it once + // it fills to log with deprecated warnings under 5.3 otherwise + if (empty($GLOBALS['egw_info']['server']['server_timezone']) || + $GLOBALS['egw_info']['server']['server_timezone'] == 'System/Localtime') // treat invalid tz like empty! + { + try + { + $tz = new DateTimeZone(date_default_timezone_get()); + Config::save_value('server_timezone',$GLOBALS['egw_info']['server']['server_timezone'] = $tz->getName(),'phpgwapi'); + error_log(__METHOD__."() stored server_timezone=".$GLOBALS['egw_info']['server']['server_timezone']); + } + catch(Exception $e) + { + // do nothing if new DateTimeZone fails (eg. 'System/Localtime' returned), specially do NOT store it! + error_log(__METHOD__."() NO valid 'date.timezone' set in your php.ini!"); + } + } + date_default_timezone_set($GLOBALS['egw_info']['server']['server_timezone']); + + // if phpgwapi exists we prefer accounts and egw_session, as they have some deprecated methods + if (file_exists(EGW_SERVER_ROOT.'/phpgwapi')) + { + $this->accounts = new accounts(); + /* Do not create the session object if called by the sessions class. This way + * we ensure the correct db based on the user domain. + */ + if($createsessionobject) + { + $this->session = new egw_session($domain_names); + } + } + else + { + $this->accounts = new Accounts(); + /* Do not create the session object if called by the sessions class. This way + * we ensure the correct db based on the user domain. + */ + if($createsessionobject) + { + $this->session = new Session($domain_names); + } + } + // setup the other subclasses + $this->acl = new Acl(); + $this->preferences = new Preferences(); + $this->applications = new Egw\Applications(); + + if ($GLOBALS['egw_info']['flags']['currentapp'] != 'login' && $GLOBALS['egw_info']['flags']['currentapp'] != 'logout') + { + $this->verify_session(); + $this->applications->read_installed_apps(); // to get translated app-titles, has to be after verify_session + + $this->define_egw_constants(); + + $this->check_app_rights(); + + $this->load_optional_classes(); + } + else // set the defines for login, in case it's more then just login + { + $this->define_egw_constants(); + } + } + + /** + * __wakeup function gets called by php while unserializing the egw-object, eg. reconnects to the DB + * + * @author RalfBecker@outdoor-training.de + */ + function __wakeup() + { + $GLOBALS['egw'] =& $this; // we need to be immediately available there for the other classes we instantiate + // for the migration: reference us to the old phpgw object + $GLOBALS['phpgw'] =& $this; + + if ($GLOBALS['egw_info']['server']['system_charset']) + { + $this->db->Link_ID->SetCharSet($GLOBALS['egw_info']['server']['system_charset']); + } + // restoring server timezone, to avoid warnings under php5.3 + if (!empty($GLOBALS['egw_info']['server']['server_timezone'])) + { + date_default_timezone_set($GLOBALS['egw_info']['server']['server_timezone']); + } + + $this->define_egw_constants(); + } + + /** + * wakeup2 function needs to be called after unserializing the egw-object + * + * It adapts the restored object/enviroment to the changed (current) application / page-request + * + * @author RalfBecker@outdoor-training.de + */ + function wakeup2() + { + // do some application specific stuff, need to be done as we are different (current) app now + if (isset($this->template)) + { + $this->template->set_root(EGW_APP_TPL); + } + // init the translation class, necessary as own wakeup would run before our's + Translation::init(isset($GLOBALS['egw_info']['flags']['load_translations']) ? $GLOBALS['egw_info']['flags']['load_translations'] : true); + + $this->unset_datetime(); + + // verify the session + $GLOBALS['egw']->verify_session(); + $GLOBALS['egw']->check_app_rights(); + + $this->load_optional_classes(); + } + + /** + * Unsetting datetime object, so time gets updated + */ + function unset_datetime() + { + unset($this->datetime); + } + + /** + * load optional classes by mentioning them in egw_info[flags][enable_CLASS_class] => true + * + * Also loads the template-class if not egw_info[flags][disable_Template_class] is set + * + * Maybe the whole thing should be depricated ;-) + */ + function load_optional_classes() + { + // output the header unless the developer turned it off + if (!@$GLOBALS['egw_info']['flags']['noheader']) + { + echo $GLOBALS['egw']->framework->header(); + + if (!$GLOBALS['egw_info']['flags']['nonavbar']) + { + echo $GLOBALS['egw']->framework->navbar(); + } + } + + // Load the (depricated) app include files if they exists + if (EGW_APP_INC != "" && ! preg_match ('/phpgwapi/i', EGW_APP_INC) && + file_exists(EGW_APP_INC . '/functions.inc.php') && !isset($_GET['menuaction'])) + { + include(EGW_APP_INC . '/functions.inc.php'); + } + if (!@$GLOBALS['egw_info']['flags']['noheader'] && !@$GLOBALS['egw_info']['flags']['noappheader'] && + file_exists(EGW_APP_INC . '/header.inc.php') && !isset($_GET['menuaction'])) + { + include(EGW_APP_INC . '/header.inc.php'); + } + } + + /** + * Verfiy there is a valid session + * + * One can specify a callback, which gets called if there's no valid session. If the callback returns true, the parameter + * containst account-details (in keys login, passwd and passwd_type) to automatic create an (anonymous session) + * + * It also checks if enforce_ssl is set in the DB and redirects to the https:// version of the site. + * + * If there is no valid session and none could be automatic created, the function will redirect to login and NOT return + */ + function verify_session() + { + if($GLOBALS['egw_info']['server']['enforce_ssl'] === 'redirect' && !$_SERVER['HTTPS']) + { + Header('Location: https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI']); + exit; + } + // check if we have a session, if not try to automatic create one + if ($this->session->verify()) return true; + + $account = null; + if (($account_callback = $GLOBALS['egw_info']['flags']['autocreate_session_callback']) && is_callable($account_callback) && + ($sessionid = call_user_func_array($account_callback,array(&$account))) === true) // $account_call_back returns true, false or a session-id + { + $sessionid = $this->session->create($account); + } + if (!$sessionid) + { + //echo "

account_callback='$account_callback', account=".print_r($account,true).", sessionid=$sessionid

\n"; exit; + // we forward to the same place after the re-login + if ($GLOBALS['egw_info']['server']['webserver_url'] && $GLOBALS['egw_info']['server']['webserver_url'] != '/' && + ($webserver_path = parse_url($GLOBALS['egw_info']['server']['webserver_url'],PHP_URL_PATH)) && $webserver_path != '/') + { + // we have to use only path component, to cope with domains like http://egroupware.domain.com and /egroupware + list(,$relpath) = explode($webserver_path,parse_url($_SERVER['PHP_SELF'],PHP_URL_PATH),2); + } + else // the webserver-url is empty or just a slash '/' (eGW is installed in the docroot and no domain given) + { + $matches = null; + if (preg_match('/^https?:\/\/[^\/]*\/(.*)$/',$relpath=$_SERVER['PHP_SELF'],$matches)) + { + $relpath = $matches[1]; + } + } + // this removes the sessiondata if its saved in the URL + $query = preg_replace('/[&]?sessionid(=|%3D)[^&]+&kp3(=|%3D)[^&]+&domain=.*$/','',$_SERVER['QUERY_STRING']); + if ($GLOBALS['egw_info']['server']['http_auth_types']) + { + $redirect = '/phpgwapi/ntlm/index.php?'; + } + else + { + $redirect = '/login.php?'; + // only add "your session could not be verified", if a sessionid is given (cookie or on url) + if (Session::get_sessionid()) $redirect .= 'cd=10&'; + } + if ($relpath) $redirect .= 'phpgw_forward='.urlencode($relpath.(!empty($query) ? '?'.$query : '')); + self::redirect_link($redirect); + } + } + + /** + * Verify the user has rights for the requested app + * + * If the user has no rights for the app (eg. called via URL) he get a permission denied page (this function does NOT return) + * + * @throws Exception\Redirect for anonymous user accessing something he has no rights to + * @throws Exception\NoPermission\Admin + * @throws Exception\NoPermission\App + */ + function check_app_rights() + { + $this->currentapp = $GLOBALS['egw_info']['flags']['currentapp']; // some apps change it later + + if (!in_array($GLOBALS['egw_info']['flags']['currentapp'], array('api', 'home'))) // give everyone implicit home rights + { + // This will need to use ACL in the future + if (!$GLOBALS['egw_info']['user']['apps'][$currentapp = $GLOBALS['egw_info']['flags']['currentapp']] || + ($GLOBALS['egw_info']['flags']['admin_only'] && !$GLOBALS['egw_info']['user']['apps']['admin'])) + { + // present a login page, if anon user has no right for an application + if ($this->session->session_flags == 'A') + { + // need to destroy a basic auth session here, because it will only be available on current url + if (($sessionid = Session::get_sessionid(true))) + { + $GLOBALS['egw']->session->destroy($sessionid); + } + throw new Exception\Redirect(egw::link('/logout.php')); + } + if ($currentapp == 'admin' || $GLOBALS['egw_info']['flags']['admin_only']) + { + throw new Exception\NoPermission\Admin(); + } + throw new Exception\NoPermission\App($currentapp); + } + } + } + + /** + * create all the defines / constants of the eGW-environment (plus the deprecated phpgw ones) + */ + function define_egw_constants() + { + define('EGW_ACL_READ',1); + define('EGW_ACL_ADD',2); + define('EGW_ACL_EDIT',4); + define('EGW_ACL_DELETE',8); + define('EGW_ACL_PRIVATE',16); + define('EGW_ACL_GROUP_MANAGERS',32); + define('EGW_ACL_CUSTOM_1',64); + define('EGW_ACL_CUSTOM_2',128); + define('EGW_ACL_CUSTOM_3',256); + // and the old ones + define('PHPGW_ACL_READ',1); + define('PHPGW_ACL_ADD',2); + define('PHPGW_ACL_EDIT',4); + define('PHPGW_ACL_DELETE',8); + define('PHPGW_ACL_PRIVATE',16); + define('PHPGW_ACL_GROUP_MANAGERS',32); + define('PHPGW_ACL_CUSTOM_1',64); + define('PHPGW_ACL_CUSTOM_2',128); + define('PHPGW_ACL_CUSTOM_3',256); + // A few hacker resistant constants that will be used throught the program + define('EGW_TEMPLATE_DIR', $this->common->get_tpl_dir('phpgwapi')); + define('EGW_IMAGES_DIR', $this->common->get_image_path('phpgwapi')); + define('EGW_IMAGES_FILEDIR', $this->common->get_image_dir('phpgwapi')); + define('EGW_APP_ROOT', $this->common->get_app_dir()); + define('EGW_APP_INC', $this->common->get_inc_dir()); + define('EGW_APP_TPL', $this->common->get_tpl_dir()); + define('EGW_IMAGES', $this->common->get_image_path()); + define('EGW_APP_IMAGES_DIR', $this->common->get_image_dir()); + // and the old ones + define('PHPGW_TEMPLATE_DIR',EGW_TEMPLATE_DIR); + define('PHPGW_IMAGES_DIR',EGW_IMAGES_DIR); + define('PHPGW_IMAGES_FILEDIR',EGW_IMAGES_FILEDIR); + define('PHPGW_APP_ROOT',EGW_APP_ROOT); + define('PHPGW_APP_INC',EGW_APP_INC); + define('PHPGW_APP_TPL',EGW_APP_TPL); + define('PHPGW_IMAGES',EGW_IMAGES); + define('PHPGW_APP_IMAGES_DIR',EGW_APP_IMAGES_DIR); + } + + /** + * force the session cache to be re-created, because some of it's data changed + * + * Needs to be called if user-preferences, system-config or enabled apps of the current user have been changed and + * the change should have immediate effect + */ + static function invalidate_session_cache() + { + unset($_SESSION['egw_info_cache']); + unset($_SESSION['egw_object_cache']); + } + + /** + * run string through htmlspecialchars and stripslashes + * + * @param string $s + * @return string The string with html special characters replaced with entities + */ + static function strip_html($s) + { + return htmlspecialchars(stripslashes($s)); + } + + /** + * Link url generator + * + * @param string $url url link is for + * @param string|array $extravars ='' extra params to be added to url + * @param string $link_app =null if appname or true, some templates generate a special link-handler url + * @return string The full url after processing + */ + static function link($url, $extravars = '', $link_app=null) + { + return $GLOBALS['egw']->framework->link($url, $extravars, $link_app); + } + + /** + * Redirects direct to a generated link + * + * @param string $url url link is for + * @param string|array $extravars ='' extra params to be added to url + * @param string $link_app =null if appname or true, some templates generate a special link-handler url + * @return string The full url after processing + */ + static function redirect_link($url, $extravars='', $link_app=null) + { + return $GLOBALS['egw']->framework->redirect_link($url, $extravars, $link_app); + } + + /** + * Handles redirects under iis and apache, it does NOT return (calls exit) + * + * This function handles redirects under iis and apache it assumes that $phpgw->link() has already been called + * + * @param string $url url to redirect to + * @param string $link_app =null appname to redirect for, default currentapp + */ + static function redirect($url, $link_app=null) + { + Framework::redirect($url, $link_app); + } + + /** + * Shortcut to translation class + * + * This function is a basic wrapper to Translation::translate() + * + * @deprecated only used in the old timetracker + * @param string The key for the phrase + * @see Translation::translate() + */ + static function lang($key,$args=null) + { + if (!is_array($args)) + { + $args = func_get_args(); + array_shift($args); + } + return Translation::translate($key,$args); + } + + /** + * registered shutdown callbacks and optional arguments + * + * @var array + */ + private static $shutdown_callbacks = array(); + + /** + * Register a callback to run on shutdown AFTER output send to user + * + * Allows eg. static classes (no destructor) to run on shutdown AND + * garanties to run AFTER output send to user. + * + * @param callable $callback use array($classname, $method) for static methods + * @param array $args =array() + */ + public static function on_shutdown($callback, array $args=array()) + { + array_unshift($args, $callback); + + // prepend new callback, to run them in oposite order they are registered + array_unshift(self::$shutdown_callbacks, $args); + } + + /** + * Shutdown handler running all registered on_shutdown callbacks and then disconnecting from db + */ + function __destruct() + { + if (!defined('EGW_SHUTDOWN')) + { + define('EGW_SHUTDOWN',True); + + // send json response BEFORE flushing output + if (Json\Request::isJSONRequest()) + { + Json\Response::sendResult(); + } + + // run all on_shutdown callbacks with session in their name (eg. egw_link::save_session_cache), do NOT stop on exceptions + foreach(self::$shutdown_callbacks as $n => $data) + { + try { + //error_log(__METHOD__."() running ".array2string($data)); + $callback = array_shift($data); + if (!is_array($callback) || strpos($callback[1], 'session') === false) continue; + call_user_func_array($callback, $data); + } + catch (\Exception $ex) { + _egw_log_exception($ex); + } + unset(self::$shutdown_callbacks[$n]); + } + // now we can close the session + // without closing the session fastcgi_finish_request() will NOT send output to user + if (isset($GLOBALS['egw']->session) && is_object($GLOBALS['egw']->session)) $GLOBALS['egw']->session->commit_session(); + + // flush all output to user + /* does NOT work on Apache :-( + for($i = 0; ob_get_level() && $i < 10; ++$i) + { + ob_end_flush(); + } + flush();*/ + // working for fastCGI :-) + if (function_exists('fastcgi_finish_request') && substr($_SERVER['PHP_SELF'], -32) != '/phpgwapi/cron/asyncservices.php') + { + fastcgi_finish_request(); + } + + // run all on_shutdown, do NOT stop on exceptions + foreach(self::$shutdown_callbacks as $data) + { + try { + //error_log(__METHOD__."() running ".array2string($data)); + $callback = array_shift($data); + call_user_func_array($callback, $data); + } + catch (\Exception $ex) { + _egw_log_exception($ex); + } + } + // call the asyncservice check_run function if it is not explicitly set to cron-only + if (!$GLOBALS['egw_info']['server']['asyncservice']) // is default + { + $async = new Asyncservice(); + $async->fallback(); + } + $this->db->disconnect(); + } + } +} diff --git a/api/src/Egw/Applications.php b/api/src/Egw/Applications.php new file mode 100755 index 0000000000..dd3c08bf71 --- /dev/null +++ b/api/src/Egw/Applications.php @@ -0,0 +1,109 @@ + + * Copyright (C) 2001 Mark Peters + * @license http://opensource.org/licenses/lgpl-license.php LGPL - GNU Lesser General Public License + * @package api + * @subpackage egw + * @version $Id$ + */ + +namespace EGroupware\Api\Egw; + +/** + * Application (sub-)object of Egw-object used to load $GLOBALS['egw_info'](['user'])['apps'] + */ +class Applications +{ + var $account_id; + var $data = Array(); + /** + * Reference to the global db class + * + * @var egw_db + */ + var $db; + var $table_name = 'egw_applications'; + + /**************************************************************************\ + * Standard constructor for setting $this->account_id * + \**************************************************************************/ + + /** + * standard constructor for setting $this->account_id + * + * @param $account_id account id + */ + function __construct($account_id = '') + { + if (is_object($GLOBALS['egw_setup']) && is_object($GLOBALS['egw_setup']->db)) + { + $this->db = $GLOBALS['egw_setup']->db; + } + else + { + $this->db = $GLOBALS['egw']->db; + } + + $this->account_id = get_account_id($account_id); + } + + /** + * Get applications of user + * + * Used to populate $GLOBALS['egw_info']['user']['apps'] in Api\Session + */ + function read_repository() + { + if (!isset($GLOBALS['egw_info']['apps']) || !is_array($GLOBALS['egw_info']['apps'])) + { + $this->read_installed_apps(); + } + $this->data = Array(); + if(!$this->account_id) + { + return False; + } + $apps = $GLOBALS['egw']->acl->get_user_applications($this->account_id); + foreach(array_keys($GLOBALS['egw_info']['apps']) as $app) + { + if (isset($apps[$app]) && $apps[$app]) + { + $this->data[$app] =& $GLOBALS['egw_info']['apps'][$app]; + } + } + return $this->data; + } + + /** + * populate array with a list of installed apps + * + */ + function read_installed_apps() + { + foreach($this->db->select($this->table_name,'*',false,__LINE__,__FILE__,false,'ORDER BY app_order ASC') as $row) + { + $title = $app_name = $row['app_name']; + + if (@is_array($GLOBALS['egw_info']['user']['preferences']) && ($t = lang($app_name)) != $app_name.'*') + { + $title = $t; + } + $GLOBALS['egw_info']['apps'][$app_name] = Array( + 'title' => $title, + 'name' => $app_name, + 'enabled' => True, + 'status' => $row['app_enabled'], + 'id' => (int)$row['app_id'], + 'order' => (int)$row['app_order'], + 'version' => $row['app_version'], + 'index' => $row['app_index'], + 'icon' => $row['app_icon'], + 'icon_app'=> $row['app_icon_app'], + ); + } + } +} diff --git a/api/src/Egw/Base.php b/api/src/Egw/Base.php new file mode 100644 index 0000000000..7fe105d29c --- /dev/null +++ b/api/src/Egw/Base.php @@ -0,0 +1,125 @@ +ldap) + * { + * $GLOBALS['egw']->ldap = Api\Ldap::factory(); + * } + * You can now simply use $GLOBALS['egw']->ldap, and the egw class instanciates it for you on demand. + */ +class Base +{ + /** + * Instance of the db-object + * + * @var Api\Db + */ + var $db; + /** + * Current app at the instancation of the class + * + * @var string + */ + var $currentapp; + /** + * Global ADOdb object, need to be defined here, to not call magic __get method + * + * @var ADOConnection + */ + var $ADOdb; + + /** + * Classes which get instanciated in a different name + * + * @var array + */ + static $sub_objects = array( + 'log' => 'errorlog', + 'link' => 'bolink', // depricated use static egw_link methods + 'datetime' => 'egw_datetime', + 'template' => 'Template', + 'session' => 'egw_session', // otherwise $GLOBALS['egw']->session->appsession() fails + // classes moved to new api dir + 'framework' => true, // special handling in __get() + 'ldap' => true, + 'auth' => 'EGroupware\\Api\\Auth', + ); + + /** + * Magic function to check if a sub-object is set + * + * @param string $name + * @return boolean + */ + function __isset($name) + { + //error_log(__METHOD__."($name)"); + return isset($this->$name); + } + + /** + * Magic function to return a sub-object + * + * @param string $name + * @return mixed + */ + function __get($name) + { + //error_log(__METHOD__."($name)".function_backtrace()); + + if ($name == 'js') $name = 'framework'; // javascript class is integrated now into framework + + if (isset($this->$name)) + { + return $this->$name; + } + + if (!isset(self::$sub_objects[$name]) && !class_exists($name)) + { + if ($name != 'ADOdb') error_log(__METHOD__.": There's NO $name object! ".function_backtrace()); + return null; + } + switch($name) + { + case 'framework': + return $this->framework = Api\Framework::factory(); + case 'template': // need to be instancated for the current app + if (!($tpl_dir = common::get_tpl_dir($this->currentapp))) + { + return null; + } + return $this->template = new Api\Framework\Template($tpl_dir); + case 'ldap': + return $this->ldap = Api\Ldap::factory(false); + default: + $class = isset(self::$sub_objects[$name]) ? self::$sub_objects[$name] : $name; + break; + } + return $this->$name = new $class(); + } +} diff --git a/api/src/autoload.php b/api/src/autoload.php new file mode 100755 index 0000000000..3b84fa237b --- /dev/null +++ b/api/src/autoload.php @@ -0,0 +1,104 @@ + + * @package api + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @version $Id$ + */ + +// this is only neccessary, if header.inc.php is not included, but api/src/autoload.php directly +if (!defined('EGW_SERVER_ROOT')) +{ + define('EGW_SERVER_ROOT', dirname(dirname(__DIR__))); + define('EGW_INCLUDE_ROOT', EGW_SERVER_ROOT); + define('EGW_API_INC', __DIR__); +} + +/** + * New PSR-4 autoloader for EGroupware + * + * class_exists('\\EGroupware\\Api\\Vfs'); // /api/src/Vfs.php + * class_exists('\\EGroupware\\Api\\Vfs\\Dav\\Directory'); // /api/src/Vfs/Dav/Directory.php + * class_exists('\\EGroupware\\Api\\Vfs\\Sqlfs\\StreamWrapper'); // /api/src/Vfs/Sqlfs/StreamWrapper.php + * class_exists('\\EGroupware\\Api\\Vfs\\Sqlfs\\Utils'); // /api/src/Vfs/Sqlfs/Utils.php + * class_exists('\\EGroupware\\Stylite\\Versioning\\StreamWrapper'); // /stylite/src/Versioning/StreamWrapper.php + * class_exists('\\EGroupware\\Calendar\\Ui'); // /calendar/src/Ui.php + * class_exists('\\EGroupware\\Calendar\\Ui\\Lists'); // /calendar/src/Ui/Lists.php + * class_exists('\\EGroupware\\Calendar\\Ui\\Views'); // /calendar/src/Ui/Views.php + */ +spl_autoload_register(function($class) +{ + $parts = explode('\\', $class); + if (array_shift($parts) != 'EGroupware') return; // not our prefix + + $app = lcfirst(array_shift($parts)); + $base = EGW_INCLUDE_ROOT.'/'.$app.'/src/'; + $path = $base.implode('/', $parts).'.php'; + + if (file_exists($path)) + { + require_once $path; + //error_log("PSR4_autoload('$class') --> require_once($path) --> class_exists('$class')=".array2string(class_exists($class,false))); + } +}); + +/** + * Old autoloader for EGroupware understanding the following naming schema: + * + * 1. new (prefered) nameing schema: app_class_something loading app/inc/class.class_something.inc.php + * 2. API classes: classname loading phpgwapi/inc/class.classname.inc.php + * 2a.API classes containing multiple classes per file eg. egw_exception* in class.egw_exception.inc.php + * 3. eTemplate classes: classname loading etemplate/inc/class.classname.inc.php + * + * @param string $class name of class to load + */ +spl_autoload_register(function($class) +{ + // fixing warnings generated by php 5.3.8 is_a($obj) trying to autoload huge strings + if (strlen($class) > 64 || strpos($class, '.') !== false) return; + + $components = explode('_',$class); + $app = array_shift($components); + // classes using the new naming schema app_class_name, eg. admin_cmd + if (file_exists($file = EGW_INCLUDE_ROOT.'/'.$app.'/inc/class.'.$class.'.inc.php') || + // classes using the new naming schema app_class_name, eg. admin_cmd + isset($components[0]) && file_exists($file = EGW_INCLUDE_ROOT.'/'.$app.'/inc/class.'.$app.'_'.$components[0].'.inc.php') || + // classes with an underscore in their name + !isset($GLOBALS['egw_info']['apps'][$app]) && isset($GLOBALS['egw_info']['apps'][$app . '_' . $components[0]]) && + file_exists($file = EGW_INCLUDE_ROOT.'/'.$app.'_'.$components[0].'/inc/class.'.$class.'.inc.php') || + // eGW api classes using the old naming schema, eg. html + file_exists($file = EGW_API_INC.'/class.'.$class.'.inc.php') || + // eGW api classes containing multiple classes in on file, eg. egw_exception + isset($components[0]) && file_exists($file = EGW_API_INC.'/class.'.$app.'_'.$components[0].'.inc.php') || + // eGW eTemplate classes using the old naming schema, eg. etemplate + file_exists($file = EGW_INCLUDE_ROOT.'/etemplate/inc/class.'.$class.'.inc.php') || + // include PEAR and PSR0 classes from include_path + // need to use include (not include_once) as eg. a previous included EGW_API_INC/horde/Horde/String.php causes + // include_once('Horde/String.php') to return true, even if the former was included with an absolute path + // only use include_path, if no Composer vendor directory exists + !isset($GLOBALS['egw_info']['apps'][$app]) && !file_exists(EGW_SERVER_ROOT.'/vendor') && + @include($file = strtr($class, array('_'=>'/','\\'=>'/')).'.php')) + { + include_once($file); + //if (!class_exists($class, false) && !interface_exists($class, false)) error_log("autoloading class $class by include_once($file) failed!"); + } + // allow apps to define an own autoload method + elseif (isset($GLOBALS['egw_info']['flags']['autoload']) && is_callable($GLOBALS['egw_info']['flags']['autoload'])) + { + call_user_func($GLOBALS['egw_info']['flags']['autoload'],$class); + } +}); + +// if we have a Composer vendor directory, also load it's autoloader, to allow manage our requirements with Composer +if (file_exists(EGW_SERVER_ROOT.'/vendor')) +{ + require_once EGW_SERVER_ROOT.'/vendor/autoload.php'; +} diff --git a/api/src/loader.php b/api/src/loader.php new file mode 100644 index 0000000000..861801422a --- /dev/null +++ b/api/src/loader.php @@ -0,0 +1,143 @@ + + * @package api + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @version $Id$ + */ + +use EGroupware\Api\Session; +use EGroupware\Api\Egw; + +// E_STRICT in PHP 5.4 gives various strict warnings in working code, which can NOT be easy fixed in all use-cases :-( +// Only variables should be assigned by reference, eg. soetemplate::tree_walk() +// Declaration of should be compatible with , varios places where method parameters change +// --> switching it off for now, as it makes error-log unusable +error_reporting(E_ALL & ~E_NOTICE & ~E_STRICT); +if (function_exists('get_magic_quotes_runtime') && get_magic_quotes_runtime()) +{ + set_magic_quotes_runtime(false); +} + +$egw_min_php_version = '5.4'; +if (!function_exists('version_compare') || version_compare(PHP_VERSION,$egw_min_php_version) < 0) +{ + die("EGroupware requires PHP $egw_min_php_version or greater.
Please contact your System Administrator to upgrade PHP!"); +} + +if (!defined('EGW_API_INC')) define('EGW_API_INC',PHPGW_API_INC); // this is to support the header upgrade + +/* Make sure the header.inc.php is current. */ +if (!isset($GLOBALS['egw_domain']) || $GLOBALS['egw_info']['server']['versions']['header'] < $GLOBALS['egw_info']['server']['versions']['current_header']) +{ + echo '
You need to update your header.inc.php file to version '. + $GLOBALS['egw_info']['server']['versions']['current_header']. + ' by running setup/headeradmin.
'; + exit; +} + +/* Make sure the developer is following the rules. */ +if (!isset($GLOBALS['egw_info']['flags']['currentapp'])) +{ + echo "

!!! YOU DO NOT HAVE YOUR \$GLOBALS['egw_info']['flags']['currentapp'] SET !!!
\n"; + echo '!!! PLEASE CORRECT THIS SITUATION !!!

'; +} + +require_once(__DIR__.'/loader/common.php'); + +// init eGW's sessions-handler and check if we can restore the eGW enviroment from the php-session +if (Session::init_handler()) +{ + if ($GLOBALS['egw_info']['flags']['currentapp'] != 'login' && $GLOBALS['egw_info']['flags']['currentapp'] != 'logout') + { + if (is_array($_SESSION[Session::EGW_INFO_CACHE]) && $_SESSION[Session::EGW_OBJECT_CACHE] && $_SESSION[Session::EGW_REQUIRED_FILES]) + { + // marking the context as restored from the session, used by session->verify to not read the data from the db again + $GLOBALS['egw_info']['flags']['restored_from_session'] = true; + + // restoring the egw_info-array + $GLOBALS['egw_info'] = array_merge($_SESSION[Session::EGW_INFO_CACHE],array('flags' => $GLOBALS['egw_info']['flags'])); + + // include required class-definitions + if (is_array($_SESSION[Session::EGW_REQUIRED_FILES])) // all classes, which can not be autoloaded + { + foreach($_SESSION[Session::EGW_REQUIRED_FILES] as $file) + { + require_once($file); + } + } + $GLOBALS['egw'] = unserialize($_SESSION[Session::EGW_OBJECT_CACHE]); + + if (is_object($GLOBALS['egw']) && ($GLOBALS['egw'] instanceof Egw)) // only egw object has wakeup2, setups egw_minimal eg. has not! + { + $GLOBALS['egw']->wakeup2(); // adapt the restored egw-object/enviroment to this request (eg. changed current app) + + $GLOBALS['egw_info']['flags']['session_restore_time'] = microtime(true) - $GLOBALS['egw_info']['flags']['page_start_time']; + if (is_object($GLOBALS['egw']->translation)) return; // exit this file, as the rest of the file creates a new egw-object and -enviroment + } + // egw object could NOT be restored from the session, create a new one + unset($GLOBALS['egw']); + $GLOBALS['egw_info'] = array('flags'=>$GLOBALS['egw_info']['flags']); + unset($GLOBALS['egw_info']['flags']['restored_from_session']); + unset($_SESSION[Session::EGW_INFO_CACHE]); + unset($_SESSION[Session::EGW_REQUIRED_FILES]); + unset($_SESSION[Session::EGW_OBJECT_CACHE]); + } + } + else // destroy the session-cache if called by login or logout + { + unset($_SESSION[Session::EGW_INFO_CACHE]); + unset($_SESSION[Session::EGW_REQUIRED_FILES]); + unset($_SESSION[Session::EGW_OBJECT_CACHE]); + } +} + +/****************************************************************************\ +* Multi-Domain support * +\****************************************************************************/ + +$GLOBALS['egw_info']['user']['domain'] = Session::search_instance( + isset($_POST['login']) ? $_POST['login'] : (isset($_SERVER['PHP_AUTH_USER']) ? $_SERVER['PHP_AUTH_USER'] : $_SERVER['REMOTE_USER']), + Session::get_request('domain'),$GLOBALS['egw_info']['server']['default_domain'], + array($_SERVER['HTTP_HOST'], $_SERVER['SERVER_NAME']),$GLOBALS['egw_domain']); + +$GLOBALS['egw_info']['server']['db_host'] = $GLOBALS['egw_domain'][$GLOBALS['egw_info']['user']['domain']]['db_host']; +$GLOBALS['egw_info']['server']['db_port'] = $GLOBALS['egw_domain'][$GLOBALS['egw_info']['user']['domain']]['db_port']; +$GLOBALS['egw_info']['server']['db_name'] = $GLOBALS['egw_domain'][$GLOBALS['egw_info']['user']['domain']]['db_name']; +$GLOBALS['egw_info']['server']['db_user'] = $GLOBALS['egw_domain'][$GLOBALS['egw_info']['user']['domain']]['db_user']; +$GLOBALS['egw_info']['server']['db_pass'] = $GLOBALS['egw_domain'][$GLOBALS['egw_info']['user']['domain']]['db_pass']; +$GLOBALS['egw_info']['server']['db_type'] = $GLOBALS['egw_domain'][$GLOBALS['egw_info']['user']['domain']]['db_type']; + +// the egw-object instanciates all sub-classes (eg. $GLOBALS['egw']->db) and the egw_info array +$GLOBALS['egw'] = new Egw(array_keys($GLOBALS['egw_domain'])); + +// store domain config user&pw as a hash (originals get unset) +$GLOBALS['egw_info']['server']['config_hash'] = Session::user_pw_hash($GLOBALS['egw_domain'][$GLOBALS['egw_info']['user']['domain']]['config_user'], + $GLOBALS['egw_domain'][$GLOBALS['egw_info']['user']['domain']]['config_passwd'],true); + +if ($GLOBALS['egw_info']['flags']['currentapp'] != 'login' && !$GLOBALS['egw_info']['server']['show_domain_selectbox']) +{ + unset($GLOBALS['egw_domain']); // we kill this for security reasons + unset($GLOBALS['egw_info']['server']['header_admin_user']); + unset($GLOBALS['egw_info']['server']['header_admin_password']); +} + +// saving the the egw_info array and the egw-object in the session +if ($GLOBALS['egw_info']['flags']['currentapp'] != 'login') +{ + $_SESSION[Session::EGW_INFO_CACHE] = $GLOBALS['egw_info']; + unset($_SESSION[Session::EGW_INFO_CACHE]['flags']); // dont save the flags, they change on each request + + $_SESSION[Session::EGW_OBJECT_CACHE] = serialize($GLOBALS['egw']); +} diff --git a/api/src/loader/common.php b/api/src/loader/common.php new file mode 100755 index 0000000000..46aee5e66c --- /dev/null +++ b/api/src/loader/common.php @@ -0,0 +1,417 @@ + + * @package api + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @version $Id$ + */ + +use EGroupware\Api; + +require_once dirname(__DIR__).'/autoload.php'; + +/** +* applies stripslashes recursivly on each element of an array +* +* @param array &$var +* @return array +*/ +function array_stripslashes($var) +{ + if (!is_array($var)) + { + return stripslashes($var); + } + foreach($var as $key => $val) + { + $var[$key] = is_array($val) ? array_stripslashes($val) : stripslashes($val); + } + return $var; +} + +/** + * Return the number of bytes of a string, independent of mbstring.func_overload + * AND the availability of mbstring + * + * @param string $str + * @return int + */ +function bytes($str) +{ + static $func_overload = null; + + if (is_null($func_overload)) $func_overload = extension_loaded('mbstring') ? ini_get('mbstring.func_overload') : 0; + + return $func_overload & 2 ? mb_strlen($str,'ascii') : strlen($str); +} + +/** + * mbstring.func_overload safe substr + * + * @param string $data + * @param int $offset + * @param int $len + * @return string + */ +function cut_bytes(&$data,$offset,$len=null) +{ + static $func_overload = null; + + if (is_null($func_overload)) $func_overload = extension_loaded('mbstring') ? ini_get('mbstring.func_overload') : 0; + + if (is_null($len)) + { + return $func_overload & 2 ? mb_substr($data,$offset,bytes($data),'ascii') : substr($data,$offset); + } + return $func_overload & 2 ? mb_substr($data,$offset,$len,'ascii') : substr($data,$offset,$len); +} + +if (!function_exists('imap_rfc822_parse_adrlist')) +{ + /** + * parses a (comma-separated) address string + * + * Examples: + * - Joe Doe + * - "Doe, Joe" + * - "\'Joe Doe\'" // actually not necessary to quote + * - postmaster@example.com + * - root + * - "Joe on its way Down Under :-\)" + * - "Giant; \"Big\" Box" + * - sysservices@example.net // this is wrong, because @ need to be quoted + * + * Invalid addresses, if detected, set host to '.SYNTAX-ERROR.' + * + * @param string $address - A string containing addresses + * @param string $default_host - The default host name + * @return array of objects. The objects properties are: + * mailbox - the mailbox name (username) + * host - the host name + * personal - the personal name + * adl - at domain source route + */ + function imap_rfc822_parse_adrlist($address, $default_host) + { + $addresses = array(); + $pending = ''; + foreach(explode(',', $address) as $part) + { + $trimmed = trim(($pending ? $pending.',' : '').$part); + if (($trimmed[0] == '"' && substr($trimmed, -1) != '>')||strpos($part, '@')===false) + { + $pending .= ($pending ? $pending.',' : '').$part; + continue; + } + $pending = ''; + $matches = $personal = $mailbox = $host = null; + if (preg_match('/^(.*)<([^>@]+)(@([^>]+))?>$/', $trimmed, $matches)) + { + $personal = trim($matches[1]); + $mailbox = $matches[2]; + $host = $matches[4]; + } + elseif (strpos($trimmed, '@') !== false) + { + list($mailbox, $host) = explode('@', $trimmed); + } + else + { + $mailbox = $trimmed; + } + if ($personal[0] == '"' && substr($personal, -1) == '"') + { + $personal = str_replace('\\', '', substr($personal, 1, -1)); + } + if (empty($host)) $host = $default_host; + + $addresses[] = (object)array_diff(array( + 'mailbox' => $mailbox, + 'host' => $host, + 'personal' => $personal, + ), array(null, '')); + } + return $addresses; + } +} + +if (!function_exists('imap_rfc822_write_address')) +{ + /** + * Returns a properly formatted email address given the mailbox, host, and personal info + * @param string $mailbox - The mailbox name, see imap_open() for more information + * @param string $host - The email host part + * @param string $personal - The name of the account owner + * @return string properly formatted email address as defined in » RFC2822. + */ + function imap_rfc822_write_address($mailbox, $host, $personal) + { + if (is_array($personal)) $personal = implode(' ', $personal); + + //if (!preg_match('/^[!#$%&\'*+/0-9=?A-Z^_`a-z{|}~-]+$/u', $personal)) // that's how I read the rfc(2)822 + if ($personal && !preg_match('/^[0-9A-Z -]*$/iu', $personal)) // but quoting is never wrong, so quote more then necessary + { + $personal = '"'.str_replace(array('\\', '"'),array('\\\\', '\\"'), $personal).'"'; + } + return ($personal ? $personal.' <' : '').$mailbox.($host ? '@'.$host : '').($personal ? '>' : ''); + } +} + +if (!function_exists('imap_mime_header_decode')) +{ + /** + * Decodes MIME message header extensions that are non ASCII text (RFC2047) + * + * Uses Horde_Mime::decode() and therefore always returns only a single array element! + * + * @param string $text + * @return array with single object with attribute text already in our internal encoding and charset + * @deprecated use Horde_Mime::decode() + */ + function imap_mime_header_decode($text) + { + return array((object)array( + 'text' => Horde_Mime::decode($text), + 'charset' => Api\Translation::charset(), // is already in our internal encoding! + )); + } +} + +if (!function_exists('mb_strlen')) +{ + /** + * Number of characters in a string + * + * @param string $str + * @return int + */ + function mb_strlen($str) + { + return strlen($str); + } +} + +if (!function_exists('mb_substr')) +{ + /** + * Return part of a string + * + * @param string $data + * @param int $offset + * @param int $len + * @return string + */ + function mb_substr(&$data, $offset, $len=null) + { + return is_null($len) ? substr($data, $offset) : substr($data, $offset, $len); + } +} + +/** + * Format array or other types as (one-line) string, eg. for error_log statements + * + * @param mixed $var variable to dump + * @return string + */ +function array2string($var) +{ + switch (($type = gettype($var))) + { + case 'boolean': + return $var ? 'TRUE' : 'FALSE'; + case 'string': + return "'$var'"; + case 'integer': + case 'double': + case 'resource': + return $var; + case 'NULL': + return 'NULL'; + case 'object': + case 'array': + return str_replace(array("\n",' '/*,'Array'*/),'',print_r($var,true)); + } + return 'UNKNOWN TYPE!'; +} + +/** + * Check if a given extension is loaded or load it if possible (requires sometimes disabled or unavailable dl function) + * + * @param string $extension + * @param boolean $throw =false should we throw an exception, if $extension could not be loaded, default false = no + * @return boolean true if loaded now, false otherwise + */ +function check_load_extension($extension,$throw=false) +{ + if (!defined('PHP_SHLIB_PREFIX')) + { + define('PHP_SHLIB_PREFIX',PHP_SHLIB_SUFFIX == 'dll' ? 'php_' : ''); + } + // we check for the existens of 'dl', as multithreaded webservers dont have it and some hosters disable it !!! + $loaded = extension_loaded($extension) || function_exists('dl') && @dl($dl=PHP_SHLIB_PREFIX.$extension.'.'.PHP_SHLIB_SUFFIX); + + if (!$loaded && $throw) + { + throw new Exception ("PHP extension '$extension' not loaded AND can NOT be loaded via dl('$dl')!"); + } + return $loaded; +} + +// include deprecated global functions, if phpgwapi is installed +if (file_exists(EGW_SERVER_ROOT.'/phpgwapi')) +{ + include_once EGW_SERVER_ROOT.'/phpgwapi/inc/deprecated_functions.inc.php'; +} + +/** + * Return a properly formatted account_id. + * + * @author skeeter + * This function will return a properly formatted account_id. This can take either a name or an account_id as paramters. If a name is provided it will return the associated id. + * $account_id = get_account_id($accountid); + * @param int/string $account_id either a name or an id + * @param int/string $default_id either a name or an id + * @return int account_id + */ +function get_account_id($account_id = '',$default_id = '') +{ + if (gettype($account_id) == 'integer') + { + return $account_id; + } + elseif ($account_id == '') + { + if ($default_id == '') + { + return (isset($GLOBALS['egw_info']['user']['account_id'])?$GLOBALS['egw_info']['user']['account_id']:0); + } + elseif (is_string($default_id)) + { + return $GLOBALS['egw']->accounts->name2id($default_id); + } + return (int)$default_id; + } + elseif (is_string($account_id)) + { + if($GLOBALS['egw']->accounts->exists((int)$account_id) == True) + { + return (int)$account_id; + } + else + { + return $GLOBALS['egw']->accounts->name2id($account_id); + } + } +} + +/** + * print an array or object as pre-formatted html + * + * @param mixed $array + * @param boolean $print =true print or return the content + * @return string if !$print + */ +function _debug_array($array,$print=True) +{ + $output = '
'.print_r($array,true)."
\n"; + + if ($print) + { + echo $output; + } + else + { + return $output; + } +} + +/** + * backtrace of the calling functions for php4.3+ else menuaction/scriptname + * + * @author RalfBecker-AT-outdoor-training.de + * @param int $remove =0 number of levels to remove + * @return string function-names separated by slashes (beginning with the calling function not this one) + */ +function function_backtrace($remove=0) +{ + if (function_exists('debug_backtrace')) + { + $backtrace = debug_backtrace(); + //echo "function_backtrace($remove)
".print_r($backtrace,True)."
\n"; + foreach($backtrace as $n => $level) + { + if ($remove-- < 0) + { + $ret[] = (isset($level['class'])?$level['class'].$level['type']:'').$level['function']. + ($n > 0 && isset($backtrace[$n-1]['line']) ? ':'.$backtrace[$n-1]['line'] : ''). // add line number of call + (!$level['class'] && !is_object($level['args'][0]) && $level['function'] != 'unserialize' ? + '('.substr(str_replace(EGW_SERVER_ROOT,'',(string)$level['args'][0]),0,64).')' : ''); + } + } + if (is_array($ret)) + { + return implode(' / ',$ret); + } + } + return $_GET['menuaction'] ? $_GET['menuaction'] : str_replace(EGW_SERVER_ROOT,'',$_SERVER['SCRIPT_FILENAME']); +} + +if (!function_exists('lang') || defined('NO_LANG')) // setup declares an own version +{ + /** + * function to handle multilanguage support + * + * @param string $key message in englich with %1, %2, ... placeholders + * @param string $vars =null multiple values to replace the placeholders + * @return string translated message with placeholders replaced + */ + function lang($key,$vars=null) + { + if(!is_array($vars)) + { + $vars = func_get_args(); + array_shift($vars); // remove $key + } + return Api\Translation::translate($key,$vars); + } +} + +require_once __DIR__.'/security.php'; +require_once __DIR__.'/exception.php'; + +/** + * Public functions to be compatible with the exiting eGW framework + */ +if (!function_exists('parse_navbar')) +{ + /** + * echo's out the navbar + * + * @deprecated use $GLOBALS['egw']->framework->navbar() or $GLOBALS['egw']->framework::render() + */ + function parse_navbar() + { + echo $GLOBALS['egw']->framework->navbar(); + } +} + +if (!function_exists('display_sidebox')) +{ + /** + * echo's out a sidebox menu + * + * @deprecated use $GLOBALS['egw']->framework->sidebox() + */ + function display_sidebox($appname,$menu_title,$_file) + { + $file = str_replace('preferences.uisettings.index', 'preferences.preferences_settings.index', $_file); + $GLOBALS['egw']->framework->sidebox($appname,$menu_title,$file); + } +} diff --git a/api/src/loader/exception.php b/api/src/loader/exception.php new file mode 100755 index 0000000000..ebe594d5de --- /dev/null +++ b/api/src/loader/exception.php @@ -0,0 +1,195 @@ + + * @package api + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @version $Id$ + */ + +use EGroupware\Api; + +/** + * Translate message only if translation object is already loaded + * + * This function is usefull for exception handlers or early stages of the initialisation of the egw object, + * as calling lang would try to load the translations, evtl. cause more errors, eg. because there's no db-connection. + * + * @param string $key message in englich with %1, %2, ... placeholders + * @param string $vars =null multiple values to replace the placeholders + * @return string translated message with placeholders replaced + */ +function try_lang($key,$vars=null) +{ + static $varnames = array('%1','%2','%3','%4'); + + if(!is_array($vars)) + { + $vars = func_get_args(); + array_shift($vars); // remove $key + } + return class_exists('EGroupware\Api\Translations',false) ? Api\Translation::translate($key,$vars) : str_replace($varnames,$vars,$key); +} + +/** + * Clasify exception for a headline and log it to error_log, if not running as cli + * + * @param Exception|Error $e + * @param string &$headline + */ +function _egw_log_exception($e,&$headline=null) +{ + $trace = explode("\n", $e->getTraceAsString()); + if ($e instanceof Api\Exception\NoPermission) + { + $headline = try_lang('Permission denied!'); + } + elseif ($e instanceof Api\Db\Exception) + { + $headline = try_lang('Database error'); + } + elseif ($e instanceof Api\Exception\WrongUserinput) + { + $headline = ''; // message contains the whole message, it's usually no real error but some input validation + } + elseif ($e instanceof egw_exception_warning) + { + $headline = 'PHP Warning'; + array_shift($trace); + } + else + { + $headline = try_lang('An error happened'); + } + // log exception to error log, if not running as cli, + // which outputs the error_log to stderr and therefore output it twice to the user + if(isset($_SERVER['HTTP_HOST']) || $GLOBALS['egw_info']['flags']['no_exception_handler'] !== 'cli') + { + error_log($headline.($e instanceof egw_exception_warning ? ': ' : ' ('.get_class($e).'): ').$e->getMessage()); + foreach($trace as $line) + { + error_log($line); + } + error_log('# Instance='.$GLOBALS['egw_info']['user']['domain'].', User='.$GLOBALS['egw_info']['user']['account_lid']. + ', Request='.$_SERVER['REQUEST_METHOD'].' '.($_SERVER['HTTPS']?'https://':'http://').$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI']. + ', User-agent='.$_SERVER['HTTP_USER_AGENT']); + } +} + +/** + * Fail a little bit more gracefully then an uncought exception + * + * Does NOT return + * + * @param Exception|Error $e + */ +function egw_exception_handler($e) +{ + // handle redirects without logging + if (is_a($e, 'egw_exception_redirect')) + { + egw::redirect($e->url, $e->app); + } + // logging all exceptions to the error_log (if not cli) and get headline + $headline = null; + _egw_log_exception($e,$headline); + + // exception handler for cli (command line interface) clients, no html, no logging + if(!isset($_SERVER['HTTP_HOST']) || $GLOBALS['egw_info']['flags']['no_exception_handler'] == 'cli') + { + echo ($headline ? $headline.': ' : '').$e->getMessage()."\n"; + if ($GLOBALS['egw_info']['server']['exception_show_trace']) + { + echo $e->getTraceAsString()."\n"; + } + exit($e->getCode() ? $e->getCode() : 9999); // allways give a non-zero exit code + } + // regular GUI exception + if (!isset($GLOBALS['egw_info']['flags']['no_exception_handler'])) + { + header('HTTP/1.1 500 '.$headline); + $message = '

'.Api\Html::htmlspecialchars($headline)."

\n". + '
'.Api\Html::htmlspecialchars($e->getMessage())."\n\n";
+
+		// only show trace (incl. function arguments) if explicitly enabled, eg. on a development system
+		if ($GLOBALS['egw_info']['server']['exception_show_trace'])
+		{
+			$message .= Api\Html::htmlspecialchars($e->getTraceAsString());
+		}
+		$message .= "
\n"; + if (is_a($e, 'EGroupware\Api\Db\Exception\Setup')) + { + $setup_dir = str_replace(array('home/index.php','index.php'),'setup/',$_SERVER['PHP_SELF']); + $message .= 'Run setup to install or configure EGroupware.'; + } + elseif (is_object($GLOBALS['egw']) && isset($GLOBALS['egw']->session) && method_exists($GLOBALS['egw'],'link')) + { + $message .= '

'.try_lang('Click here to resume your eGroupWare Session.').'

'; + } + if (is_object($GLOBALS['egw']) && isset($GLOBALS['egw']->framework)) + { + $GLOBALS['egw']->framework->render($message,$headline); + } + else + { + echo "\n\n".Api\Html::htmlspecialchars($headline)."\n\n\n$message\n\n\n"; + } + } + // exception handler sending message back to the client as basic auth message + elseif($GLOBALS['egw_info']['flags']['no_exception_handler'] == 'basic_auth') + { + $error = str_replace(array("\r", "\n"), array('', ' | '), $e->getMessage()); + header('WWW-Authenticate: Basic realm="'.$headline.' '.$error.'"'); + header('HTTP/1.1 401 Unauthorized'); + header('X-WebDAV-Status: 401 Unauthorized', true); + } + exit; +} + +if (!isset($GLOBALS['egw_info']['flags']['no_exception_handler']) || $GLOBALS['egw_info']['flags']['no_exception_handler'] !== true) +{ + set_exception_handler('egw_exception_handler'); +} + +/** + * Fail a little bit more gracefully then a catchable fatal error, by throwing an exception + * + * @param int $errno level of the error raised: E_* constants + * @param string $errstr error message + * @param string $errfile filename that the error was raised in + * @param int $errline line number the error was raised at + * @link http://www.php.net/manual/en/function.set-error-handler.php + * @throws ErrorException + */ +function egw_error_handler ($errno, $errstr, $errfile, $errline) +{ + switch ($errno) + { + case E_RECOVERABLE_ERROR: + case E_USER_ERROR: + throw new ErrorException($errstr, $errno, 0, $errfile, $errline); + + case E_WARNING: + case E_USER_WARNING: + // skip message for warnings supressed via @-error-control-operator (eg. @is_dir($path)) + // can be commented out to get suppressed warnings too! + if (error_reporting()) + { + _egw_log_exception(new egw_exception_warning($errstr.' in '.$errfile.' on line '.$errline)); + } + break; + } +} + +/** + * Used internally to trace warnings + */ +class egw_exception_warning extends Exception {} + +// install our error-handler only for catchable fatal errors and warnings +// following error types cannot be handled with a user defined function: E_ERROR, E_PARSE, E_CORE_ERROR, E_CORE_WARNING, E_COMPILE_ERROR, E_COMPILE_WARNING +set_error_handler('egw_error_handler', E_RECOVERABLE_ERROR|E_USER_ERROR|E_WARNING|E_USER_WARNING); diff --git a/api/src/loader/security.php b/api/src/loader/security.php new file mode 100755 index 0000000000..29a27cae20 --- /dev/null +++ b/api/src/loader/security.php @@ -0,0 +1,332 @@ + + * @package api + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @version $Id$ + */ + +use EGroupware\Api; + +/** + * check $_REQUEST data for XSS, vars containing script tags are moved to $GLOBALS['egw_unset_vars'] + * + * @internal + * @param array &$var reference of array to check + * @param string $name ='' name of the array + */ +function _check_script_tag(&$var,$name='') +{ + static $preg=null; + //old: '/<\/?[^>]*\b(iframe|script|javascript|on(before)?(abort|blur|change|click|dblclick|error|focus|keydown|keypress|keyup|load|mousedown|mousemove|mouseout|mouseover|mouseup|reset|select|submit|unload))\b[^>]*>/i'; + if (!isset($preg)) $preg = + // forbidden tags like iframe or script + '/(<(\s*\/)?\s*(iframe|script|object|embed|math|meta)[^a-z0-9]|'. + // on* attributes + '<[^>]*on(before)?(abort|blur|change|click|dblclick|error|focus|keydown|keypress|keyup|load|mouse[^=]+|reset|select|submit|unload|resize|propertychange|page[^=]*|scroll|readystatechange|start|popstate|form[^=]+|input)\s*=|'. + // ="javascript:*" diverse javascript attribute value + '<[^>]+(href|src|dynsrc|lowsrc|background|style|poster|action)\s*=\s*("|\')?[^"\']*javascript|'. + // benavior:url and expression in style attribute + '<[^>]+style\s*=\s*("|\')[^>]*(behavior\s*:\s*url|expression)\s*\()/i'; + if (is_array($var)) + { + foreach($var as $key => $val) + { + if (is_array($val)) + { + _check_script_tag($var[$key],$name.'['.$key.']'); + } + elseif(strpos($val, '<') !== false) // speedup: ignore everything without < + { + if (preg_match($preg,$val)) + { + // special handling for $_POST[json_data], to decend into it's decoded content, fixing json direct might break json syntax + if ($name == '_POST' && $key == 'json_data' && ($json_data = json_decode($val, true))) + { + _check_script_tag($json_data, $name.'[json_data]'); + $_REQUEST[$key] = $var[$key] = json_encode($json_data); + continue; + } + error_log(__FUNCTION__."(,$name) ${name}[$key] = ".$var[$key]); + $GLOBALS['egw_unset_vars'][$name.'['.$key.']'] = $var[$key]; + // attempt to clean the thing + $var[$key] = $val = Api\Html\HtmLawed::purify($val); + // check if we succeeded, if not drop the var anyway, keep the egw_unset_var in any case + if (preg_match($preg,$val)) + { + error_log("*** _check_script_tag($name): unset(${name}[$key]) with value $val***"); + unset($var[$key]); + } + } + } + } + // in case some stupid old code expects the array-pointer to be at the start of the array + reset($var); + } +} + +/* some _check_script_tag tests, should be commented out by default +if (isset($_SERVER['SCRIPT_FILENAME']) && $_SERVER['SCRIPT_FILENAME'] == __FILE__) // some tests +{ + if (!defined('EGW_INCLUDE_ROOT')) + { + define(EGW_INCLUDE_ROOT, realpath(dirname(__FILE__).'/../..')); + define(EGW_API_INC, realpath(dirname(__FILE__))); + } + + $total = $num_failed = 0; + $patterns = array( + // pattern => true: should fail, false: should not fail + '< script >alert(1)< / script >' => true, + 'blah' => true, + 'Click Me' => true, + // from https://www.acunetix.com/websitesecurity/cross-site-scripting/ + '' => true, + '' => true, + '