SetCookieDomain('my.domain.com'); $session->SetCookiePath('/myscript'); $session->SetCookieName('my_sid_cookie'); $session->SetGETName('sid'); $session->InitSession(); ... //link output: echo "NeedQueryString() ? 'sid='.$session->SID : '' ) .">My Link"; */ class Session extends kBase { const smAUTO = 1; const smCOOKIES_ONLY = 2; const smGET_ONLY = 3; const smCOOKIES_AND_GET = 4; const PURPOSE_TRANSPORT = 1; const PURPOSE_STORAGE = 2; const PURPOSE_REFERENCE = 3; var $Checkers; var $Mode; var $OriginalMode = null; var $GETName = 'sid'; var $CookiesEnabled = true; var $CookieName = 'sid'; var $CookieDomain; var $CookiePath; var $CookieSecure = 0; var $SessionTimeout = 3600; var $Expiration; var $SID; var $CachedSID; var $SessionSet = false; /** * Session ID is used from GET * * @var bool */ var $_fromGet = false; /** * Enter description here... * * @var SessionStorage * @access protected */ protected $Storage; var $CachedNeedQueryString = null; /** * Session Data array * * @var Params */ var $Data; /** * Names of optional session keys with their optional values (which does not need to be always stored) * * @var Array */ var $OptionalData = Array (); /** * Session expiration mark * * @var bool */ var $expired = false; /** * Creates session * * @param int $mode * @access public */ public function __construct($mode = self::smAUTO) { parent::__construct(); $this->SetMode($mode); } function SetMode($mode) { $this->Mode = $mode; $this->CachedNeedQueryString = null; $this->CachedSID = null; } function SetCookiePath($path) { $this->CookiePath = str_replace(' ', '%20', $path); } /** * Setting cookie domain. Set false for local domains, because they don't contain dots in their names. * * @param string $domain */ function SetCookieDomain($domain) { // 1. localhost or other like it without "." in domain name if (!substr_count($domain, '.')) { // don't use cookie domain at all $this->CookieDomain = false; return ; } // 2. match using predefined cookie domains from configuration $cookie_domains = $this->Application->ConfigValue('SessionCookieDomains'); if ($cookie_domains) { $cookie_domains = array_map('trim', explode("\n", $cookie_domains)); foreach ($cookie_domains as $cookie_domain) { if (ltrim($cookie_domain, '.') == $domain) { $this->CookieDomain = $cookie_domain; // as defined in configuration return ; } } } // 3. only will execute, when none of domains were matched at previous step $this->CookieDomain = $this->_autoGuessDomain($domain); } /** * Auto-guess cookie domain based on $_SERVER['HTTP_HOST'] * * @param $domain * @return string */ function _autoGuessDomain($domain) { static $cache = Array (); if (!array_key_exists($domain, $cache)) { switch ( substr_count($domain, '.') ) { case 2: // 3rd level domain (3 parts) $cache[$domain] = substr($domain, strpos($domain, '.')); // with leading "." break; case 1: // 2rd level domain (2 parts) $cache[$domain] = '.' . $domain; // with leading "." break; default: // more then 3rd level $cache[$domain] = ltrim($domain, '.'); // without leading "." break; } } return $cache[$domain]; } function SetGETName($get_name) { $this->GETName = $get_name; } function SetCookieName($cookie_name) { $this->CookieName = $cookie_name; } function InitStorage($special) { $this->Storage = $this->Application->recallObject('SessionStorage.'.$special); $this->Storage->setSession($this); } public function Init($prefix, $special) { parent::Init($prefix, $special); if ( php_sapi_name() == 'cli' ) { $this->SetMode(self::smGET_ONLY); } $this->CheckIfCookiesAreOn(); if ($this->CookiesEnabled) $_COOKIE['cookies_on'] = 1; $this->Checkers = Array(); $this->InitStorage($special); $this->Data = new Params(); $tmp_sid = $this->GetPassedSIDValue(); $check = $this->Check(); if ($this->Application->isAdmin) { // 1. Front-End session may not be created (SID is present, but no data in database). // Check expiration LATER from kApplication::Init, because template, used in session // expiration redirect should be retrieved from mod-rewrite url first. // 2. Admin sessions are always created, so case when SID is present, // but session in database isn't is 100% session expired. Check expiration // HERE because Session::SetSession will create missing session in database // and when Session::ValidateExpired will be called later from kApplication::Init // it won't consider such session as expired !!! $this->ValidateExpired(); } if ($check) { $this->SID = $this->GetPassedSIDValue(); $this->Refresh(); $this->LoadData(); } else { $this->SetSession(); } if (!is_null($this->OriginalMode)) $this->SetMode($this->OriginalMode); } function ValidateExpired() { if (defined('IS_INSTALL') && IS_INSTALL) { return ; } // $this->DeleteExpired(); // called from u:OnDeleteExpiredSessions scheduled task now if ($this->expired || ($this->CachedSID && !$this->_fromGet && !$this->SessionSet)) { $this->RemoveSessionCookie(); // true was here to force new session creation, but I (kostja) used // RemoveCookie a line above, to avoid redirect loop with expired sid // not being removed setSession with true was used before, to set NEW // session cookie $this->SetSession(); // case #1: I've OR other site visitor expired my session // case #2: I have no session in database, but SID is present $this->expired = false; $this->Application->HandleEvent(new kEvent('u:OnSessionExpire')); } } /** * Helper method for detecting cookie availability * * @return bool */ function _checkCookieReferer() { // removing /admin for compatability with in-portal (in-link/admin/add_link.php) $path = preg_replace('/admin[\/]{0,1}$/', '', $this->CookiePath); $reg = '#^'.preg_quote(PROTOCOL.ltrim($this->CookieDomain, '.').$path).'#'; return preg_match($reg, getArrayValue($_SERVER, 'HTTP_REFERER') ); } function CheckIfCookiesAreOn() { if ( $this->Mode == self::smGET_ONLY ) { //we don't need to bother checking if we would not use it $this->CookiesEnabled = false; return false; } /** @var kHTTPQuery $http_query */ $http_query = $this->Application->recallObject('HTTPQuery'); $cookies_on = array_key_exists('cookies_on', $http_query->Cookie); // not good here $get_sid = getArrayValue($http_query->Get, $this->GETName); if ( ($this->Application->HttpQuery->IsHTTPSRedirect() && $get_sid) || $this->getFlashSID() ) { // Redirect from http to https on different domain OR flash uploader $this->OriginalMode = $this->Mode; $this->SetMode(self::smGET_ONLY); } if ( !$cookies_on || $this->Application->HttpQuery->IsHTTPSRedirect() || $this->getFlashSID() ) { //If referer is our server, but we don't have our cookies_on, it's definetly off $is_install = defined('IS_INSTALL') && IS_INSTALL; if ( !$is_install && $this->_checkCookieReferer() && !$this->Application->GetVar('admin') && !$this->Application->HttpQuery->IsHTTPSRedirect() ) { $this->CookiesEnabled = false; } else { //Otherwise we still suppose cookies are on, because may be it's the first time user visits the site //So we send cookies on to get it next time (when referal will tell us if they are realy off $this->SetCookie('cookies_on', 1, adodb_mktime() + 31104000); //one year should be enough } } else { $this->CookiesEnabled = true; } return $this->CookiesEnabled; } /** * Sets cookie for current site using path and domain * * @param string $name * @param mixed $value * @param int $expires */ function SetCookie($name, $value, $expires = null) { if (isset($expires) && $expires < adodb_mktime()) { unset($this->Application->HttpQuery->Cookie[$name]); } else { $this->Application->HttpQuery->Cookie[$name] = $value; } $old_style_domains = Array ( // domain like in pre 5.1.0 versions '.' . SERVER_NAME, // auto-guessed domain (when user specified other domain in configuration variable) $this->_autoGuessDomain(SERVER_NAME) ); foreach ($old_style_domains as $old_style_domain) { if ($this->CookieDomain != $old_style_domain) { // new style cookie domain -> delete old style cookie to prevent infinite redirect setcookie($name, $value, adodb_mktime() - 3600, $this->CookiePath, $old_style_domain, $this->CookieSecure, true); } } setcookie($name, $value, $expires, $this->CookiePath, $this->CookieDomain, $this->CookieSecure, true); } function Check() { // don't check referer here, because it doesn't provide any security option and can be easily falsified $sid = $this->GetPassedSIDValue(); if (empty($sid)) { return false; } //try to load session by sid, if everything is fine $result = $this->LoadSession($sid); $this->SessionSet = $result; // fake front-end session will given "false" here return $result; } function LoadSession($sid) { if( $this->Storage->LocateSession($sid) ) { // if we have session with such SID - get its expiration $this->Expiration = $this->Storage->GetExpiration(); // If session has expired if ($this->Expiration < adodb_mktime()) { // when expired session is loaded, then SID is // not assigned, but used in Destroy method $this->SID = $sid; $this->Destroy(); $this->expired = true; // when Destory methods calls SetSession inside and new session get created return $this->SessionSet; } // Otherwise it's ok return true; } else { // fake or deleted due to expiration SID if (!$this->_fromGet) { $this->expired = true; } return false; } } function getFlashSID() { /** @var kHTTPQuery $http_query */ $http_query = $this->Application->recallObject('HTTPQuery'); return getArrayValue($http_query->Post, 'flashsid'); } function GetPassedSIDValue($use_cache = 1) { if (!empty($this->CachedSID) && $use_cache) { return $this->CachedSID; } // flash sid overrides regular sid $get_sid = $this->getFlashSID(); if (!$get_sid) { /** @var kHTTPQuery $http_query */ $http_query = $this->Application->recallObject('HTTPQuery'); $get_sid = getArrayValue($http_query->Get, $this->GETName); } $sid_from_get = $get_sid ? true : false; if ($this->Application->GetVar('admin') == 1 && $get_sid) { $sid = $get_sid; } else { switch ($this->Mode) { case self::smAUTO: //Cookies has the priority - we ignore everything else $sid = $this->CookiesEnabled ? $this->GetSessionCookie() : $get_sid; if ($this->CookiesEnabled) { $sid_from_get = false; } break; case self::smCOOKIES_ONLY: $sid = $this->GetSessionCookie(); break; case self::smGET_ONLY: $sid = $get_sid; break; case self::smCOOKIES_AND_GET: $cookie_sid = $this->GetSessionCookie(); //both sids should match if cookies are enabled if (!$this->CookiesEnabled || ($cookie_sid == $get_sid)) { $sid = $get_sid; //we use get here just in case cookies are disabled } else { $sid = ''; $sid_from_get = false; } break; } } $this->CachedSID = $sid; $this->_fromGet = $sid_from_get; return $this->CachedSID; } /** * Returns session id * * @param integer $purpose Purpose. * * @return integer * @throws RuntimeException When unknown purpose is given. */ public function GetID($purpose = self::PURPOSE_TRANSPORT) { if ( $purpose === self::PURPOSE_TRANSPORT ) { return $this->SID; } if ( $purpose === self::PURPOSE_STORAGE ) { return $this->Storage->createSignatureFromSID($this->SID); } if ( $purpose === self::PURPOSE_REFERENCE ) { return $this->Storage->GetID(); } throw new RuntimeException('The "' . $purpose . '" purpose is not supported.'); } /** * Generates new session id * * @return string */ protected function GenerateSID() { $promise = SecurityGenerator::generateBytes(16); $promise->asSignature()->resolveForPersisting(TABLE_PREFIX . 'UserSessions', 'SessionKey'); $new_sid = $promise->asValue()->resolve(); $this->setSID($new_sid); return $this->SID; } /** * Set's new session id * * @param int $new_sid * @access private */ function setSID($new_sid) { $this->SID /*= $this->CachedSID*/ = $new_sid; // don't set cached sid here $this->Application->SetVar($this->GETName,$new_sid); } function NeedSession() { $data = $this->Data->GetParams(); $data_keys = array_keys($data); $optional_keys = array_keys($this->OptionalData); $real_keys = array_diff($data_keys, $optional_keys); return $real_keys ? true : false; } function SetSession($force = false) { if ( $this->SessionSet && !$force ) { return true; } $this->Expiration = adodb_mktime() + $this->SessionTimeout; if ( !$force && /*!$this->Application->isAdmin &&*/ !$this->Application->GetVar('admin') && !$this->NeedSession() ) { // don't create session (in db) on Front-End, when sid is present (GPC), but data in db isn't if ( $this->_fromGet ) { // set sid, that was given in GET $this->setSID($this->GetPassedSIDValue()); } else { // re-generate sid only, when cookies are used $this->GenerateSID(); } $this->Storage->StoreSession(false); return false; } if ( !$this->SID || $force ) { $this->GenerateSID(); } switch ( $this->Mode ) { case self::smAUTO: if ( $this->CookiesEnabled ) { $this->SetSessionCookie(); } break; case self::smGET_ONLY: break; case self::smCOOKIES_ONLY: case self::smCOOKIES_AND_GET: $this->SetSessionCookie(); break; } $this->Storage->StoreSession(); if ( $this->Application->isAdmin || $this->Special == 'admin' ) { $this->StoreVar('admin', 1); } $this->SessionSet = true; // should be called before SaveData, because SaveData will try to SetSession again if ( $this->Special != '' ) { // front-session called from admin or otherwise, then save it's data $this->SaveData(); } $this->Application->resetCounters('UserSessions'); return true; } /** * Returns SID from cookie. * * Use 2 cookies to have 2 expiration: * - 1. for normal expiration when browser is not closed (30 minutes by default), configurable * - 2. for advanced expiration when browser is closed * * @return int */ function GetSessionCookie() { $keep_session_on_browser_close = $this->Application->ConfigValue('KeepSessionOnBrowserClose'); if (isset($this->Application->HttpQuery->Cookie[$this->CookieName]) && ( $keep_session_on_browser_close || ( !$keep_session_on_browser_close && isset($this->Application->HttpQuery->Cookie[$this->CookieName.'_live']) && $this->Application->HttpQuery->Cookie[$this->CookieName] == $this->Application->HttpQuery->Cookie[$this->CookieName.'_live'] ) ) ) { return $this->Application->HttpQuery->Cookie[$this->CookieName]; } return false; } /** * Updates SID in cookie with new value * */ function SetSessionCookie() { $this->SetCookie($this->CookieName, $this->SID, $this->Expiration); $this->SetCookie($this->CookieName.'_live', $this->SID); $_COOKIE[$this->CookieName] = $this->SID; // for compatibility with in-portal } function RemoveSessionCookie() { $this->SetCookie($this->CookieName, ''); $this->SetCookie($this->CookieName.'_live', ''); $_COOKIE[$this->CookieName] = null; // for compatibility with in-portal } /** * Refreshes session expiration time * * @access private */ function Refresh() { if ($this->Application->GetVar('skip_session_refresh')) { return ; } if ($this->CookiesEnabled) { // we need to refresh the cookie $this->SetSessionCookie(); } $this->Storage->UpdateSession(); } function Destroy() { $this->Storage->DeleteSession(); $this->Data = new Params(); $this->SID = $this->CachedSID = ''; $this->SessionSet = false; if ($this->CookiesEnabled) { $this->SetSessionCookie(); //will remove the cookie due to value (sid) is empty } $this->SetSession(true); //will create a new session, true to force } function NeedQueryString($use_cache = 1) { if ( $this->CachedNeedQueryString !== null && $use_cache ) { return $this->CachedNeedQueryString; } $result = false; switch ($this->Mode) { case self::smAUTO: if ( !$this->CookiesEnabled && PHP_SAPI !== 'cli' ) { $result = true; } break; /*case self::smCOOKIES_ONLY: break;*/ case self::smGET_ONLY: case self::smCOOKIES_AND_GET: if ( PHP_SAPI !== 'cli' ) { $result = true; } break; } $this->CachedNeedQueryString = $result; return $result; } function LoadData() { $this->Data->AddParams( $this->Storage->LoadData() ); } /** * Returns information about session contents * * @param bool $include_optional * @return array * @access public */ public function getSessionData($include_optional = true) { $session_data = $this->Data->GetParams(); ksort($session_data); foreach ($session_data as $session_key => $session_value) { if ( kUtil::IsSerialized($session_value) ) { $session_data[$session_key] = unserialize($session_value); } } if ( !$include_optional ) { $optional_keys = array_keys($this->OptionalData); foreach ($session_data as $session_key => $session_value) { if ( in_array($session_key, $optional_keys) ) { unset($session_data[$session_key]); } } } return $session_data; } /** * Returns real session data, that was saved * * @param Array $session_data * @return Array * @access protected */ protected function _getRealSessionData($session_data) { $data_keys = array_keys($session_data); $optional_keys = array_keys($this->OptionalData); $real_keys = array_diff($data_keys, $optional_keys); if ( !$real_keys ) { return Array (); } $ret = Array (); foreach ($real_keys as $real_key) { $ret[$real_key] = $session_data[$real_key]; } return $ret; } function PrintSession($comment = '') { if ( defined('DEBUG_MODE') && $this->Application->isDebugMode() && kUtil::constOn('DBG_SHOW_SESSIONDATA') ) { // dump session data $this->Application->Debugger->appendHTML('SessionStorage [' . ($this->RecallVar('admin') == 1 ? 'Admin' : 'Front-End') . '] (' . $comment . '):'); $session_data = $this->getSessionData(); $this->Application->Debugger->dumpVars($session_data); if ( !$this->RecallVar('admin') ) { // dump real keys (only for front-end) $real_session_data = $this->_getRealSessionData($session_data); if ( $real_session_data ) { $this->Application->Debugger->appendHTML('Real Keys:'); $this->Application->Debugger->dumpVars($real_session_data); } } } if ( defined('DEBUG_MODE') && $this->Application->isDebugMode() && kUtil::constOn('DBG_SHOW_PERSISTENTDATA') ) { // dump persistent session data if ( $this->Storage->PersistentVars ) { $this->Application->Debugger->appendHTML('Persistant Session:'); $session_data = $this->Storage->PersistentVars; ksort($session_data); foreach ($session_data as $session_key => $session_value) { if ( kUtil::IsSerialized($session_value) ) { $session_data[$session_key] = unserialize($session_value); } } $this->Application->Debugger->dumpVars($session_data); } } } function SaveData($params = Array ()) { if (!$this->SetSession()) { // call it here - it may be not set before, because there was no need; if there is a need, it will be set here return; } if (!$this->Application->GetVar('skip_last_template') && $this->Application->GetVar('ajax') != 'yes') { $this->SaveLastTemplate( $this->Application->GetVar('t'), $params ); } $this->PrintSession('after save'); $this->Storage->SaveData(); } /** * Save last template * * @param string $t * @param Array $params */ function SaveLastTemplate($t, $params = Array ()) { $wid = $this->Application->GetVar('m_wid'); $last_env = $this->getLastTemplateENV($t, array('m_opener' => 'u')); $last_template = basename($_SERVER['PHP_SELF']) . '|' . $last_env; $this->StoreVar(rtrim('last_template_' . $wid, '_'), $last_template); // prepare last_template for opener stack, module & session could be added later $last_env = $this->getLastTemplateENV($t); $last_template = basename($_SERVER['PHP_SELF']) . '|' . $last_env; // save last_template in persistent session if (!$wid) { if ($this->Application->isAdmin) { // only for main window, not popups, not login template, not temp mode (used in adm:MainFrameLink tag) $temp_mode = false; $passed = explode(',', $this->Application->GetVar('passed')); foreach ($passed as $passed_prefix) { if ($this->Application->GetVar($passed_prefix.'_mode')) { $temp_mode = true; break; } } if (!$temp_mode) { if ( $this->Application->GetVarDirect('section', 'Get') !== false ) { // check directly in GET, because LinkVar (session -> request) used on these vars $last_template .= '§ion='.$this->Application->GetVar('section').'&module='.$this->Application->GetVar('module'); } $this->StorePersistentVar('last_template_popup', $last_template); } } elseif ($this->Application->GetVar('admin')) { // admin checking by session data to prevent recursive session save static $admin_saved = null; if (!$this->RecallVar('admin') && !isset($admin_saved)) { // bug: we get recursion in this place, when cookies are disabled in browser and we are browsing // front-end in admin's frame (front-end session is initialized using admin's sid and they are // mixed together) $admin_saved = true; /** @var Session $admin_session */ $admin_session = $this->Application->recallObject('Session.admin'); // save to admin last_template too, because when F5 is pressed in frameset Front-End frame should reload as well $admin_session->StoreVar('last_template_popup', '../' . $last_template); $admin_session->StorePersistentVar('last_template_popup', '../' . $last_template); $admin_session->SaveData( Array ('save_last_template' => false) ); } else { // don't allow admin=1 & editing_mode=* to get in admin last_template $last_template = preg_replace('/&(admin|editing_mode)=[\d]/', '', $last_template); } } } // save other last... variables for mystical purposes (customizations may be) $this->StoreVar('last_url', $_SERVER['REQUEST_URI']); // needed by ord:StoreContinueShoppingLink $this->StoreVar('last_env', $last_env); $save_last_template = array_key_exists('save_last_template', $params) ? $params['save_last_template'] : true; if ($save_last_template) { // save last template here, because section & module could be added before $this->StoreVar(rtrim('last_template_popup_'.$wid, '_'), $last_template); } } protected function getLastTemplateENV($t, $params = null) { if (!isset($params)) { $params = Array (); } if ($this->Application->GetVar('admin') && !array_key_exists('admin', $params) && !defined('EDITING_MODE')) { $params['editing_mode'] = ''; // used in kApplication::Run } $params = array_merge($this->Application->getPassThroughVariables($params), $params); return $this->Application->BuildEnv($t, $params, 'all', false, false); } /** * Stores variable $val in session under name $var * * Use this method to store variable in session. Later this variable could be recalled. * * @param string $name Variable name * @param mixed $value Variable value * @param bool $optional * @return void * @access public * @see Session::RecallVar() */ public function StoreVar($name, $value, $optional = false) { $this->Data->Set($name, $value); if ( $optional ) { // make variable optional, also remember optional value $this->OptionalData[$name] = $value; } elseif ( !$optional && array_key_exists($name, $this->OptionalData) ) { if ( $this->OptionalData[$name] == $value ) { // same value as optional -> don't remove optional mark return; } // make variable non-optional unset($this->OptionalData[$name]); } } /** * Stores variable to persistent session * * @param string $name * @param mixed $value * @param bool $optional * @return void * @access public */ public function StorePersistentVar($name, $value, $optional = false) { $this->Storage->StorePersistentVar($name, $value, $optional); } function LoadPersistentVars() { $this->Storage->LoadPersistentVars(); } /** * Stores default value for session variable * * @param string $name * @param string $value * @param bool $optional * @return void * @access public * @see Session::RecallVar() * @see Session::StoreVar() */ public function StoreVarDefault($name, $value, $optional = false) { $tmp = $this->RecallVar($name); if ( $tmp === false || $tmp == '' ) { $this->StoreVar($name, $value, $optional); } } /** * Returns session variable value * * Return value of $var variable stored in Session. An optional default value could be passed as second parameter. * * @param string $name Variable name * @param mixed $default Default value to return if no $var variable found in session * @return mixed * @access public */ public function RecallVar($name, $default = false) { $ret = $this->Data->Get($name); return ($ret === false) ? $default : $ret; } /** * Returns variable value from persistent session * * @param string $name * @param mixed $default * @return mixed * @access public */ public function RecallPersistentVar($name, $default = false) { return $this->Storage->RecallPersistentVar($name, $default); } /** * Deletes Session variable * * @param string $var * @return void * @access public */ public function RemoveVar($name) { $this->Storage->RemoveFromData($name); $this->Data->Remove($name); } /** * Removes variable from persistent session * * @param string $name * @return void * @access public */ public function RemovePersistentVar($name) { $this->Storage->RemovePersistentVar($name); } /** * Ignores session variable value set before * * @param string $name * @return void * @access public */ public function RestoreVar($name) { $value = $this->Storage->GetFromData($name, '__missing__'); if ( $value === '__missing__' ) { // there is nothing to restore (maybe session was not saved), look in optional variable values $value = array_key_exists($name, $this->OptionalData) ? $this->OptionalData[$name] : false; } $this->StoreVar($name, $value); } function GetField($var_name, $default = false) { return $this->Storage->GetField($var_name, $default); } function SetField($var_name, $value) { $this->Storage->SetField($var_name, $value); } /** * Deletes expired sessions * * @return Array expired sids if any * @access private */ function DeleteExpired() { return $this->Storage->DeleteExpired(); } /** * Deletes given sessions * * @param $session_ids * @param int $delete_reason * @return void */ function DeleteSessions($session_ids, $delete_reason = SESSION_LOG_EXPIRED) { $this->Storage->DeleteSessions($session_ids, $delete_reason); } /** * Allows to check if user in this session is logged in or not * * @return bool */ function LoggedIn() { $user_id = $this->RecallVar('user_id'); $ret = $user_id > 0; if (($this->RecallVar('admin') == 1 || defined('ADMIN')) && ($user_id == USER_ROOT)) { $ret = true; } return $ret; } }