From 7816062044e1a2efedd6e568cbae32d64fbbe417 Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Tue, 5 Jul 2016 23:27:25 +0900 Subject: [PATCH 01/37] Initial structure of session class --- common/framework/session.php | 285 +++++++++++++++++++++++++++++++++++ 1 file changed, 285 insertions(+) create mode 100644 common/framework/session.php diff --git a/common/framework/session.php b/common/framework/session.php new file mode 100644 index 000000000..07a031925 --- /dev/null +++ b/common/framework/session.php @@ -0,0 +1,285 @@ + Date: Sat, 9 Jul 2016 23:13:44 +0900 Subject: [PATCH 02/37] Add generic getter and setter methods --- common/framework/session.php | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/common/framework/session.php b/common/framework/session.php index 07a031925..4f4d6b7be 100644 --- a/common/framework/session.php +++ b/common/framework/session.php @@ -14,6 +14,29 @@ class Session protected static $_must_create = false; protected static $_must_refresh = false; + /** + * Get a session variable. + * + * @param string $key + * @return mixed + */ + public static function get($key) + { + + } + + /** + * Set a session variable. + * + * @param string $key + * @param mixed $value + * @return void + */ + public static function set($key, $value) + { + + } + /** * Start the session. * From e2b2861e812f6234040549ade2bb238709413cf7 Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Mon, 15 Aug 2016 17:58:13 +0900 Subject: [PATCH 03/37] Implement most of the functionality of the Session class --- common/framework/session.php | 341 ++++++++++++++++++++++++++++++++--- 1 file changed, 320 insertions(+), 21 deletions(-) diff --git a/common/framework/session.php b/common/framework/session.php index 4f4d6b7be..84a3bc36d 100644 --- a/common/framework/session.php +++ b/common/framework/session.php @@ -11,8 +11,6 @@ class Session * Properties for internal use only. */ protected static $_started = false; - protected static $_must_create = false; - protected static $_must_refresh = false; /** * Get a session variable. @@ -22,7 +20,17 @@ class Session */ public static function get($key) { - + $data = $_SESSION; + $key = explode('.', $key); + foreach ($key as $step) + { + if ($key === '' || !isset($data[$step])) + { + return null; + } + $data = $data[$step]; + } + return $data; } /** @@ -34,7 +42,13 @@ class Session */ public static function set($key, $value) { - + $data = &$_SESSION; + $key = explode('.', $key); + foreach ($key as $step) + { + $data = &$data[$step]; + } + $data = $value; } /** @@ -42,12 +56,141 @@ class Session * * This method is called automatically at Rhymix startup. * There is usually no need to call it manually. - * + * + * @param bool $relax_key_checks (optional) * @return bool */ - public static function start() + public static function start($relax_key_checks = false) { + // Do not start the session if it is already started. + if (self::$_started) + { + return false; + } + // Set session parameters. + list($lifetime, $refresh_interval, $domain, $path) = self::_getParams(); + ini_set('session.gc_maxlifetime', $lifetime + 28800); + ini_set('session.use_cookies', 1); + ini_set('session.use_only_cookies', 1); + ini_set('session.use_strict_mode', 1); + session_set_cookie_params($lifetime, $path, $domain, false, false); + session_name(Config::get('session.name') ?: session_name()); + + // Start the PHP native session. + if (!session_start()) + { + return false; + } + + // Fetch session keys. + list($key1, $key2) = self::_getKeys(); + $must_create = $must_refresh = $must_resend_keys = false; + + // Validate the HTTP key. + if (isset($_SESSION['RHYMIX']) && $_SESSION['RHYMIX']) + { + if ($_SESSION['RHYMIX']['keys'][$domain]['key1'] === $key1) + { + // OK + } + elseif ($_SESSION['RHYMIX']['keys'][$domain]['key1_prev'] === $key1) + { + $must_resend_keys = true; + } + elseif (!$relax_key_checks) + { + unset($_SESSION['RHYMIX']); + $must_create = true; + } + } + else + { + $must_create = true; + } + + // Validate the SSL key. + if (!$must_create && \RX_SSL) + { + if (!isset($_SESSION['RHYMIX']['keys'][$domain]['key2'])) + { + $must_refresh = true; + } + elseif ($_SESSION['RHYMIX']['keys'][$domain]['key2'] === $key2) + { + // OK + } + elseif ($_SESSION['RHYMIX']['keys'][$domain]['key2_prev'] === $key2) + { + $must_resend_keys = true; + } + elseif (!$relax_key_checks) + { + unset($_SESSION['RHYMIX']); + $must_create = true; + } + } + + // Check the IP address. + if (!$must_create && !Filters\IpFilter::inRange(\RX_CLIENT_IP, $_SESSION['RHYMIX']['ipaddress'])) + { + $must_create = true; + } + + // Check the refresh interval. + if (!$must_create && $_SESSION['RHYMIX']['keys'][$domain]['key1_time'] < time() - $refresh_interval && !$relax_key_checks) + { + $must_refresh = true; + } + elseif (!$must_create && \RX_SSL && $_SESSION['RHYMIX']['keys'][$domain]['key2_time'] < time() - $refresh_interval && !$relax_key_checks) + { + $must_refresh = true; + } + + // Create or refresh the session if needed. + if ($must_create) + { + return self::create(); + } + elseif ($must_refresh) + { + return self::refresh(); + } + elseif ($must_resend_keys) + { + return self::_setKeys(); + } + else + { + return true; + } + } + + /** + * Create the data structure for a new Rhymix session. + * + * This method is called automatically by start() when needed. + * + * @return bool + */ + public static function create() + { + // Ensure backward compatibility with XE session. + $member_srl = $_SESSION['member_srl'] ?: false; + $_SESSION['is_logged'] = (bool)$member_srl; + + // Create the data structure for a new Rhymix session. + $_SESSION['RHYMIX'] = array(); + $_SESSION['RHYMIX']['login'] = $_SESSION['member_srl'] = $member_srl; + $_SESSION['RHYMIX']['ipaddress'] = $_SESSION['ipaddress'] = \RX_CLIENT_IP; + $_SESSION['RHYMIX']['useragent'] = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : ''; + $_SESSION['RHYMIX']['language'] = \Context::getLangType(); + $_SESSION['RHYMIX']['timezone'] = DateTime::getTimezoneForCurrentUser(); + $_SESSION['RHYMIX']['secret'] = Security::getRandom(32, 'alnum'); + $_SESSION['RHYMIX']['tokens'] = array(); + + // Pass control to refresh() to generate security keys. + return self::refresh(); } /** @@ -60,7 +203,36 @@ class Session */ public static function refresh() { + // Get session parameters. + list($lifetime, $refresh_interval, $domain, $path) = self::_getParams(); + // Reset the trusted information. + if (!isset($_SESSION['RHYMIX']['keys'][$domain]['trusted'])) + { + $_SESSION['RHYMIX']['keys'][$domain]['trusted'] = 0; + } + + // Create or refresh the HTTP-only key. + if (isset($_SESSION['RHYMIX']['keys'][$domain]['key1'])) + { + $_SESSION['RHYMIX']['keys'][$domain]['key1_prev'] = $_SESSION['RHYMIX']['keys'][$domain]['key1']; + } + $_SESSION['RHYMIX']['keys'][$domain]['key1'] = Security::getRandom(24, 'alnum'); + $_SESSION['RHYMIX']['keys'][$domain]['key1_time'] = time(); + + // Create or refresh the HTTPS-only key. + if (\RX_SSL) + { + if (isset($_SESSION['RHYMIX']['keys'][$domain]['key2'])) + { + $_SESSION['RHYMIX']['keys'][$domain]['key2_prev'] = $_SESSION['RHYMIX']['keys'][$domain]['key2']; + } + $_SESSION['RHYMIX']['keys'][$domain]['key2'] = Security::getRandom(24, 'alnum'); + $_SESSION['RHYMIX']['keys'][$domain]['key2_time'] = time(); + } + + // Pass control to _setKeys() to send the keys to the client. + return self::_setKeys(); } /** @@ -73,7 +245,8 @@ class Session */ public static function close() { - + self::$_started = false; + session_write_close(); } /** @@ -85,7 +258,10 @@ class Session */ public static function destroy() { - + unset($_SESSION['RHYMIX']); + self::$_started = false; + self::_setKeys(); + session_destroy(); } /** @@ -95,12 +271,22 @@ class Session * It returns true on success and false on failure. * * @param int $member_srl - * @param bool $is_admin * @return bool */ - public static function login($member_srl, $is_admin = false) + public static function login($member_srl) { + if (is_object($member_srl) && isset($member_srl->member_srl)) + { + $member_srl = $member_srl->member_srl; + } + if ($member_srl < 1) + { + return false; + } + $_SESSION['RHYMIX']['login'] = $_SESSION['member_srl'] = $member_srl; + $_SESSION['is_logged'] = (bool)$member_srl; + self::refresh(); } /** @@ -112,7 +298,9 @@ class Session */ public static function logout() { - + $_SESSION['RHYMIX']['login'] = $_SESSION['member_srl'] = false; + $_SESSION['is_logged'] = false; + self::refresh(); } /** @@ -124,7 +312,7 @@ class Session */ public static function isMember() { - + return ($_SESSION['member_srl'] > 0 && $_SESSION['RHYMIX']['login'] > 0); } /** @@ -150,7 +338,18 @@ class Session */ public static function isTrusted() { + // Get session parameters. + list($lifetime, $refresh_interval, $domain, $path) = self::_getParams(); + // Check the 'trusted' parameter. + if ($_SESSION['RHYMIX']['keys'][$domain]['trusted'] > time()) + { + return true; + } + else + { + return false; + } } /** @@ -162,7 +361,7 @@ class Session */ public static function getMemberSrl() { - + return $_SESSION['member_srl'] ?: ($_SESSION['RHYMIX']['login'] ?: false); } /** @@ -187,7 +386,7 @@ class Session */ public static function getLanguage() { - + return isset($_SESSION['RHYMIX']['language']) ? $_SESSION['RHYMIX']['language'] : \Context::getLangType(); } /** @@ -198,7 +397,7 @@ class Session */ public static function setLanguage($language) { - + $_SESSION['RHYMIX']['language'] = $language; } /** @@ -211,7 +410,7 @@ class Session */ public static function getTimezone() { - + return DateTime::getTimezoneForCurrentUser(); } /** @@ -222,7 +421,7 @@ class Session */ public static function setTimezone($timezone) { - + $_SESSION['RHYMIX']['timezone'] = $timezone; } /** @@ -235,7 +434,19 @@ class Session */ public static function setTrusted($duration = 300) { + // Get session parameters. + list($lifetime, $refresh_interval, $domain, $path) = self::_getParams(); + // Update the 'trusted' parameter if the current user is logged in. + if (isset($_SESSION['RHYMIX']['keys'][$domain]) && $_SESSION['RHYMIX']['login']) + { + $_SESSION['RHYMIX']['keys'][$domain]['trusted'] = time() + $duration; + return true; + } + else + { + return false; + } } /** @@ -249,7 +460,9 @@ class Session */ public static function createToken($key = null) { - + $token_id = Security::getRandom(16, 'alnum'); + $_SESSION['RHYMIX']['tokens'][$token_id] = strval($key); + return $token_id; } /** @@ -263,7 +476,14 @@ class Session */ public static function verifyToken($token, $key = null) { - + if (isset($_SESSION['RHYMIX']['tokens'][$token_id]) && $_SESSION['RHYMIX']['tokens'][$token_id] === strval($key)) + { + return true; + } + else + { + return false; + } } /** @@ -275,7 +495,15 @@ class Session */ public static function invalidateToken($token) { - + if (isset($_SESSION['RHYMIX']['tokens'][$token_id])) + { + unset($_SESSION['RHYMIX']['tokens'][$token_id]); + return true; + } + else + { + return false; + } } /** @@ -289,7 +517,8 @@ class Session */ public static function encrypt($plaintext) { - + $key = $_SESSION['RHYMIX']['secret'] . Config::get('crypto.encryption_key'); + return Security::encrypt($plaintext, $key); } /** @@ -303,6 +532,76 @@ class Session */ public static function decrypt($ciphertext) { + $key = $_SESSION['RHYMIX']['secret'] . Config::get('crypto.encryption_key'); + return Security::encrypt($ciphertext, $key); + } + + /** + * Get session parameters. + * + * @return array + */ + protected static function _getParams() + { + $lifetime = Config::get('session.lifetime'); + $refresh = Config::get('session.refresh') ?: 300; + $domain = Config::get('session.domain') ?: (ini_get('session.cookie_domain') ?: preg_replace('/:\\d+$/', '', $_SERVER['HTTP_HOST'])); + $path = Config::get('session.path') ?: ini_get('session.cookie_path'); + return array($lifetime, $refresh, $domain, $path); + } + + /** + * Get session keys. + * + * @return array + */ + protected static function _getKeys() + { + // Initialize keys. + $key1 = $key2 = null; + // Fetch and validate the HTTP-only key. + if (isset($_COOKIE['rx_sesskey1']) && ctype_alnum($_COOKIE['rx_sesskey1']) && strlen($_COOKIE['rx_sesskey1']) === 24) + { + $key1 = $_COOKIE['rx_sesskey1']; + } + + // Fetch and validate the HTTPS-only key. + if (isset($_COOKIE['rx_sesskey2']) && ctype_alnum($_COOKIE['rx_sesskey2']) && strlen($_COOKIE['rx_sesskey2']) === 24) + { + $key2 = $_COOKIE['rx_sesskey2']; + } + return array($key1, $key1 === null ? null : $key2); + } + + /** + * Set session keys. + * + * @return bool + */ + protected static function _setKeys() + { + // Get session parameters. + list($lifetime, $refresh_interval, $domain, $path) = self::_getParams(); + $lifetime = $lifetime ? ($lifetime + time()) : 0; + + // Set or destroy the HTTP-only key. + if (isset($_SESSION['RHYMIX']['keys'][$domain]['key1'])) + { + setcookie('rx_sesskey1', $_SESSION['RHYMIX']['keys'][$domain]['key1'], $lifetime, $path, $domain, false, true); + $_COOKIE['rx_sesskey1'] = $_SESSION['RHYMIX']['keys'][$domain]['key1']; + } + else + { + setcookie('rx_sesskey1', 'deleted', time() - 86400, $path, $domain, false, true); + unset($_COOKIE['rx_sesskey1']); + } + + // Set or delete the HTTPS-only key. + if (\RX_SSL && isset($_SESSION['RHYMIX']['keys'][$domain]['key2'])) + { + setcookie('rx_sesskey2', $_SESSION['RHYMIX']['keys'][$domain]['key2'], $lifetime, $path, $domain, true, true); + $_COOKIE['rx_sesskey2'] = $_SESSION['RHYMIX']['keys'][$domain]['key2']; + } } } From 12ee15637456babb40db7599fb9cc835a8fb577c Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Mon, 15 Aug 2016 17:58:48 +0900 Subject: [PATCH 04/37] Update DateTime class to use timezone information from Session --- common/framework/datetime.php | 4 ++-- tests/unit/framework/DateTimeTest.php | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/common/framework/datetime.php b/common/framework/datetime.php index e5deac5c2..66511d2ba 100644 --- a/common/framework/datetime.php +++ b/common/framework/datetime.php @@ -62,9 +62,9 @@ class DateTime */ public static function getTimezoneForCurrentUser() { - if (isset($_SESSION['timezone']) && $_SESSION['timezone']) + if (isset($_SESSION['RHYMIX']['timezone']) && $_SESSION['RHYMIX']['timezone']) { - return $_SESSION['timezone']; + return $_SESSION['RHYMIX']['timezone']; } elseif ($default = Config::get('locale.default_timezone')) { diff --git a/tests/unit/framework/DateTimeTest.php b/tests/unit/framework/DateTimeTest.php index ac0bb6dbc..694c766c1 100644 --- a/tests/unit/framework/DateTimeTest.php +++ b/tests/unit/framework/DateTimeTest.php @@ -22,11 +22,11 @@ class DateTimeTest extends \Codeception\TestCase\Test public function testZgap() { // Test zgap() when the current user's time zone is different from the system default. - $_SESSION['timezone'] = 'Etc/UTC'; + $_SESSION['RHYMIX']['timezone'] = 'Etc/UTC'; $this->assertEquals(-10800, zgap()); // Test zgap() when the current user's time zone is the same as the system default. - unset($_SESSION['timezone']); + unset($_SESSION['RHYMIX']['timezone']); $this->assertEquals(21600, zgap()); } @@ -80,11 +80,11 @@ class DateTimeTest extends \Codeception\TestCase\Test $timestamp = 1454000000; // Test when the display time zone is different from the internal time zone. - $_SESSION['timezone'] = 'America/Los_Angeles'; + $_SESSION['RHYMIX']['timezone'] = 'America/Los_Angeles'; $this->assertEquals('20160128085320', getDisplayDateTime($timestamp)); // Test when the display time zone is the same as the internal time zone. - $_SESSION['timezone'] = 'Etc/GMT-3'; + $_SESSION['RHYMIX']['timezone'] = 'Etc/GMT-3'; $this->assertEquals('20160128195320', getDisplayDateTime($timestamp)); } @@ -109,11 +109,11 @@ class DateTimeTest extends \Codeception\TestCase\Test public function testGetTimezoneForCurrentUser() { // Test when the current user's time zone is different from the system default. - $_SESSION['timezone'] = 'Pacific/Auckland'; + $_SESSION['RHYMIX']['timezone'] = 'Pacific/Auckland'; $this->assertEquals('Pacific/Auckland', Rhymix\Framework\DateTime::getTimezoneForCurrentUser()); // Test when the current user's time zone is the same as the system default. - unset($_SESSION['timezone']); + unset($_SESSION['RHYMIX']['timezone']); $this->assertEquals('Asia/Seoul', Rhymix\Framework\DateTime::getTimezoneForCurrentUser()); } @@ -123,19 +123,19 @@ class DateTimeTest extends \Codeception\TestCase\Test $timestamp_summer = $timestamp_winter - (86400 * 184); // Test when the current user's time zone is in the Northern hemisphere with DST. - $_SESSION['timezone'] = 'America/Chicago'; + $_SESSION['RHYMIX']['timezone'] = 'America/Chicago'; $this->assertEquals('20160128 105320', Rhymix\Framework\DateTime::formatTimestampForCurrentUser('Ymd His', $timestamp_winter)); $this->assertEquals('20150728 115320', Rhymix\Framework\DateTime::formatTimestampForCurrentUser('Ymd His', $timestamp_summer)); $this->assertEquals('20150728 115320', getDisplayDateTime($timestamp_summer, 'Ymd His')); // Test when the current user's time zone is in the Southern hemisphere with DST. - $_SESSION['timezone'] = 'Pacific/Auckland'; + $_SESSION['RHYMIX']['timezone'] = 'Pacific/Auckland'; $this->assertEquals('20160129 055320', Rhymix\Framework\DateTime::formatTimestampForCurrentUser('Ymd His', $timestamp_winter)); $this->assertEquals('20150729 045320', Rhymix\Framework\DateTime::formatTimestampForCurrentUser('Ymd His', $timestamp_summer)); $this->assertEquals('20150729 045320', getDisplayDateTime($timestamp_summer, 'Ymd His')); // Test when the current user's time zone is the same as the system default without DST. - unset($_SESSION['timezone']); + unset($_SESSION['RHYMIX']['timezone']); $this->assertEquals('20160129 015320', Rhymix\Framework\DateTime::formatTimestampForCurrentUser('Ymd His', $timestamp_winter)); $this->assertEquals('20150729 015320', Rhymix\Framework\DateTime::formatTimestampForCurrentUser('Ymd His', $timestamp_summer)); $this->assertEquals('20150729 015320', getDisplayDateTime($timestamp_summer, 'Ymd His')); From d07bd15b8054ad0c3af2b7ab92cf494a04427d9d Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Mon, 15 Aug 2016 21:49:17 +0900 Subject: [PATCH 05/37] Integrate session class with Context class and Member module --- classes/context/Context.class.php | 28 ++++++++++--------- common/framework/session.php | 40 ++++++++++++++++++++++------ modules/member/member.controller.php | 24 +++++++++-------- modules/member/member.model.php | 38 ++++---------------------- 4 files changed, 66 insertions(+), 64 deletions(-) diff --git a/classes/context/Context.class.php b/classes/context/Context.class.php index 8ea34a771..cc46efcfa 100644 --- a/classes/context/Context.class.php +++ b/classes/context/Context.class.php @@ -338,7 +338,7 @@ class Context array(&$oSessionController, 'open'), array(&$oSessionController, 'close'), array(&$oSessionModel, 'read'), array(&$oSessionController, 'write'), array(&$oSessionController, 'destroy'), array(&$oSessionController, 'gc') ); } - + // start session if it was previously started $session_name = session_name(); $session_id = NULL; @@ -354,7 +354,8 @@ class Context if($session_id !== NULL || !config('session.delay')) { $this->setCacheControl(0, false); - session_start(); + $relax_key_checks = ($this->act === 'procFileUpload' && preg_match('/shockwave\s?flash/i', $_SERVER['HTTP_USER_AGENT'])); + Rhymix\Framework\Session::start($relax_key_checks); } else { @@ -376,7 +377,7 @@ class Context if($oMemberController && $oMemberModel) { // if signed in, validate it. - if($oMemberModel->isLogged()) + if(Rhymix\Framework\Session::getMemberSrl()) { $oMemberController->setSessionInfo(); } @@ -385,12 +386,9 @@ class Context { $oMemberController->doAutologin(); } - + self::set('is_logged', $oMemberModel->isLogged()); - if($oMemberModel->isLogged()) - { - self::set('logged_info', $oMemberModel->getLoggedInfo()); - } + self::set('logged_info', $oMemberModel->getLoggedInfo()); } } @@ -428,7 +426,7 @@ class Context */ public static function getSessionStatus() { - return (session_id() !== ''); + return Rhymix\Framework\Session::isStarted(); } /** @@ -446,8 +444,14 @@ class Context { $tempSession = $_SESSION; unset($_SESSION); - session_start(); - $_SESSION = $tempSession; + Rhymix\Framework\Session::start(); + foreach ($tempSession as $key => $val) + { + if ($key !== 'RHYMIX') + { + $_SESSION[$key] = $val; + } + } return true; } return false; @@ -469,7 +473,7 @@ class Context // Check session status and close it if open. if (self::checkSessionStatus()) { - session_write_close(); + Rhymix\Framework\Session::close(); } } diff --git a/common/framework/session.php b/common/framework/session.php index 84a3bc36d..bffba0b63 100644 --- a/common/framework/session.php +++ b/common/framework/session.php @@ -11,6 +11,7 @@ class Session * Properties for internal use only. */ protected static $_started = false; + protected static $_member_info = false; /** * Get a session variable. @@ -83,6 +84,9 @@ class Session return false; } + // Mark the session as started. + self::$_started = true; + // Fetch session keys. list($key1, $key2) = self::_getKeys(); $must_create = $must_refresh = $must_resend_keys = false; @@ -131,12 +135,6 @@ class Session } } - // Check the IP address. - if (!$must_create && !Filters\IpFilter::inRange(\RX_CLIENT_IP, $_SESSION['RHYMIX']['ipaddress'])) - { - $must_create = true; - } - // Check the refresh interval. if (!$must_create && $_SESSION['RHYMIX']['keys'][$domain]['key1_time'] < time() - $refresh_interval && !$relax_key_checks) { @@ -178,6 +176,7 @@ class Session // Ensure backward compatibility with XE session. $member_srl = $_SESSION['member_srl'] ?: false; $_SESSION['is_logged'] = (bool)$member_srl; + $_SESSION['is_admin'] = ''; // Create the data structure for a new Rhymix session. $_SESSION['RHYMIX'] = array(); @@ -300,7 +299,17 @@ class Session { $_SESSION['RHYMIX']['login'] = $_SESSION['member_srl'] = false; $_SESSION['is_logged'] = false; - self::refresh(); + self::destroy(); + } + + /** + * Check if the session has been started. + * + * @return bool + */ + public static function isStarted() + { + return self::$_started; } /** @@ -324,7 +333,8 @@ class Session */ public static function isAdmin() { - + $member_info = self::getMemberInfo(); + return ($member_info && $member_info->is_admin === 'Y'); } /** @@ -373,7 +383,21 @@ class Session */ public static function getMemberInfo() { + // Return false if the current user is not logged in. + $member_srl = self::getMemberSrl(); + if (!$member_srl) + { + return false; + } + // Create a member info object. + if (!self::$_member_info) + { + !self::$_member_info = getModel('member')->getMemberInfoByMemberSrl($member_srl); + } + + // Return the member info object. + return self::$_member_info; } /** diff --git a/modules/member/member.controller.php b/modules/member/member.controller.php index 915a25a04..8a9a626c6 100644 --- a/modules/member/member.controller.php +++ b/modules/member/member.controller.php @@ -101,7 +101,8 @@ class memberController extends member if(!$trigger_output->toBool()) return $trigger_output; // Destroy session information - $this->destroySessionInfo(); + Rhymix\Framework\Session::logout(); + $this->_clearMemberCache($logged_info->member_srl); // Call a trigger after log-out (after) ModuleHandler::triggerCall('member.doLogout', 'after', $logged_info); @@ -111,9 +112,9 @@ class memberController extends member $oModuleModel = getModel('module'); $config = $oModuleModel->getModuleConfig('member'); if($config->after_logout_url) + { $output->redirect_url = $config->after_logout_url; - - $this->_clearMemberCache($logged_info->member_srl); + } return $output; } @@ -700,7 +701,7 @@ class memberController extends member $output = $this->deleteMember($member_srl); if(!$output->toBool()) return $output; // Destroy all session information - $this->destroySessionInfo(); + Rhymix\Framework\Session::logout(); // Return success message $this->setMessage('success_leaved'); @@ -1844,6 +1845,7 @@ class memberController extends member } $this->setSessionInfo(); + Rhymix\Framework\Session::login($this->memberInfo->member_srl); return $output; } @@ -1855,11 +1857,11 @@ class memberController extends member $oMemberModel = getModel('member'); $config = $oMemberModel->getMemberConfig(); // If your information came through the current session information to extract information from the users - if(!$this->memberInfo && $_SESSION['member_srl'] && $oMemberModel->isLogged() ) + if(!$this->memberInfo && $member_srl = Rhymix\Framework\Session::getMemberSrl()) { - $this->memberInfo = $oMemberModel->getMemberInfoByMemberSrl($_SESSION['member_srl']); + $this->memberInfo = $oMemberModel->getMemberInfoByMemberSrl($member_srl); // If you do not destroy the session Profile - if($this->memberInfo->member_srl != $_SESSION['member_srl']) + if($this->memberInfo->member_srl != $member_srl) { $this->destroySessionInfo(); return; @@ -1872,11 +1874,11 @@ class memberController extends member return; } // Log in for treatment sessions set + /* $_SESSION['is_logged'] = true; - $_SESSION['ipaddress'] = $_SERVER['REMOTE_ADDR']; - $_SESSION['member_srl'] = $this->memberInfo->member_srl; + $_SESSION['member_srl'] = $_SESSION['RHYMIX']['login'] = $this->memberInfo->member_srl; $_SESSION['is_admin'] = ''; - setcookie('xe_logged', 'true', 0, '/'); + */ // Do not save your password in the session jiwojum;; //unset($this->memberInfo->password); // User Group Settings @@ -2582,7 +2584,7 @@ class memberController extends member $_SESSION[$key] = ''; } - session_destroy(); + Rhymix\Framework\Session::destroy(); setcookie(session_name(), '', $_SERVER['REQUEST_TIME']-42000, '/'); setcookie('sso','',$_SERVER['REQUEST_TIME']-42000, '/'); setcookie('xeak','',$_SERVER['REQUEST_TIME']-42000, '/'); diff --git a/modules/member/member.model.php b/modules/member/member.model.php index 93191b070..471437ce8 100644 --- a/modules/member/member.model.php +++ b/modules/member/member.model.php @@ -200,36 +200,9 @@ class memberModel extends member /** * @brief Check if logged-in */ - function isLogged() { - if($_SESSION['is_logged']) - { - if(Mobile::isFromMobilePhone()) - { - return true; - } - elseif(filter_var($_SESSION['ipaddress'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) - { - // IPv6: require same /48 - if(strncmp(inet_pton($_SESSION['ipaddress']), inet_pton($_SERVER['REMOTE_ADDR']), 6) == 0) - { - return true; - } - } - else - { - // IPv4: require same /24 - if(ip2long($_SESSION['ipaddress']) >> 8 == ip2long($_SERVER['REMOTE_ADDR']) >> 8) - { - return true; - } - } - } - - if(Context::getSessionStatus()) - { - $_SESSION['is_logged'] = false; - } - return false; + function isLogged() + { + return Rhymix\Framework\Session::getMemberSrl() ? true : false; } /** @@ -238,7 +211,7 @@ class memberModel extends member function getLoggedInfo() { // Return session info if session info is requested and the user is logged-in - if($this->isLogged()) + if(Rhymix\Framework\Session::getMemberSrl()) { $logged_info = Context::get('logged_info'); // Admin/Group list defined depending on site_module_info @@ -462,8 +435,7 @@ class memberModel extends member */ function getLoggedMemberSrl() { - if(!$this->isLogged()) return; - return $_SESSION['member_srl']; + return Rhymix\Framework\Session::getMemberSrl(); } /** From d21990d32219d3004014710e0f5a92d5b56a627f Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Mon, 15 Aug 2016 23:36:42 +0900 Subject: [PATCH 06/37] Add session shield addon to blacklist --- common/defaults/blacklist.php | 1 + modules/admin/lang/en.php | 1 + modules/admin/lang/ko.php | 1 + 3 files changed, 3 insertions(+) diff --git a/common/defaults/blacklist.php b/common/defaults/blacklist.php index 6706f0ea6..774fca774 100644 --- a/common/defaults/blacklist.php +++ b/common/defaults/blacklist.php @@ -11,6 +11,7 @@ return array( 'fix_mysql_utf8' => true, 'member_communication' => true, 'seo' => true, + 'session_shield' => true, 'smartphone' => true, 'zipperupper' => true, ); diff --git a/modules/admin/lang/en.php b/modules/admin/lang/en.php index 4e26b9682..e750b6a30 100644 --- a/modules/admin/lang/en.php +++ b/modules/admin/lang/en.php @@ -70,6 +70,7 @@ $lang->msg_blacklisted_reason['errorlogger'] = 'Similar functionality can be con $lang->msg_blacklisted_reason['fix_mysql_utf8'] = 'The functionality that this addon used to provide is included by default in Rhymix.'; $lang->msg_blacklisted_reason['member_communication'] = 'The functionality that this addon used to provide has been moved to the member and ncenterlite modules.'; $lang->msg_blacklisted_reason['seo'] = 'Similar functionality can be configured in the SEO Settings page.'; +$lang->msg_blacklisted_reason['session_shield'] = 'The functionality that this addon used to provide is included by default in Rhymix.'; $lang->msg_blacklisted_reason['smartphone'] = 'This module was disabled in XE long before Rhymix even existed.'; $lang->msg_blacklisted_reason['zipperupper'] = 'Similar functionality can be configured in the Advanced Settings page.'; $lang->msg_warning = 'Warning'; diff --git a/modules/admin/lang/ko.php b/modules/admin/lang/ko.php index 221dcce35..6ba02b1ee 100644 --- a/modules/admin/lang/ko.php +++ b/modules/admin/lang/ko.php @@ -70,6 +70,7 @@ $lang->msg_blacklisted_reason['errorlogger'] = '이 모듈에서 제공하던 $lang->msg_blacklisted_reason['fix_mysql_utf8'] = '이 애드온에서 제공하던 기능은 Rhymix에 포함되어 있습니다.'; $lang->msg_blacklisted_reason['member_communication'] = '이 애드온에서 제공하던 기능은 알림센터 모듈에서 관리할 수 있습니다.'; $lang->msg_blacklisted_reason['seo'] = '이 모듈에서 제공하던 기능은 SEO 설정 페이지에서 관리할 수 있습니다.'; +$lang->msg_blacklisted_reason['session_shield'] = '이 애드온에서 제공하던 기능은 Rhymix에 포함되어 있습니다.'; $lang->msg_blacklisted_reason['smartphone'] = '이 모듈은 XE에서도 사용되지 않고 있었습니다.'; $lang->msg_blacklisted_reason['zipperupper'] = '이 애드온에서 제공하던 기능은 고급 설정 페이지에서 관리할 수 있습니다.'; $lang->msg_warning = '경고'; From a74daeb0716e269f3df3c7d6a705662c89f29c60 Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Tue, 16 Aug 2016 11:39:40 +0900 Subject: [PATCH 07/37] Fix typo in variable name --- common/framework/session.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/common/framework/session.php b/common/framework/session.php index bffba0b63..fd6404f7b 100644 --- a/common/framework/session.php +++ b/common/framework/session.php @@ -484,9 +484,9 @@ class Session */ public static function createToken($key = null) { - $token_id = Security::getRandom(16, 'alnum'); - $_SESSION['RHYMIX']['tokens'][$token_id] = strval($key); - return $token_id; + $token = Security::getRandom(16, 'alnum'); + $_SESSION['RHYMIX']['tokens'][$token] = strval($key); + return $token; } /** @@ -500,7 +500,7 @@ class Session */ public static function verifyToken($token, $key = null) { - if (isset($_SESSION['RHYMIX']['tokens'][$token_id]) && $_SESSION['RHYMIX']['tokens'][$token_id] === strval($key)) + if (isset($_SESSION['RHYMIX']['tokens'][$token]) && $_SESSION['RHYMIX']['tokens'][$token] === strval($key)) { return true; } @@ -519,9 +519,9 @@ class Session */ public static function invalidateToken($token) { - if (isset($_SESSION['RHYMIX']['tokens'][$token_id])) + if (isset($_SESSION['RHYMIX']['tokens'][$token])) { - unset($_SESSION['RHYMIX']['tokens'][$token_id]); + unset($_SESSION['RHYMIX']['tokens'][$token]); return true; } else From 02a45ece9a600120c6e32cd24485ebda83af07fd Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Tue, 16 Aug 2016 21:55:32 +0900 Subject: [PATCH 08/37] Fix missing static keyword --- common/framework/security.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/framework/security.php b/common/framework/security.php index 5a2803c2d..da5ada968 100644 --- a/common/framework/security.php +++ b/common/framework/security.php @@ -153,7 +153,7 @@ class Security * @param string $format * @return string */ - public function getRandom($length = 32, $format = 'alnum') + public static function getRandom($length = 32, $format = 'alnum') { // Find out how many bytes of entropy we really need. switch($format) From ab3d1b5fd6770e2ad5fbae6417e150277f759c4c Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Fri, 19 Aug 2016 23:07:11 +0900 Subject: [PATCH 09/37] Fix miscellaneous bugs and improve security of Session class --- common/framework/session.php | 52 +++++++++++++++++++++++++++--------- 1 file changed, 40 insertions(+), 12 deletions(-) diff --git a/common/framework/session.php b/common/framework/session.php index fd6404f7b..e8dd1b8b2 100644 --- a/common/framework/session.php +++ b/common/framework/session.php @@ -94,17 +94,18 @@ class Session // Validate the HTTP key. if (isset($_SESSION['RHYMIX']) && $_SESSION['RHYMIX']) { - if ($_SESSION['RHYMIX']['keys'][$domain]['key1'] === $key1) + if ($_SESSION['RHYMIX']['keys'][$domain]['key1'] === $key1 && $key1 !== null) { // OK } - elseif ($_SESSION['RHYMIX']['keys'][$domain]['key1_prev'] === $key1) + elseif ($_SESSION['RHYMIX']['keys'][$domain]['key1_prev'] === $key1 && $key1 !== null) { + return 2; $must_resend_keys = true; } elseif (!$relax_key_checks) { - unset($_SESSION['RHYMIX']); + $_SESSION = array(); $must_create = true; } } @@ -120,17 +121,17 @@ class Session { $must_refresh = true; } - elseif ($_SESSION['RHYMIX']['keys'][$domain]['key2'] === $key2) + elseif ($_SESSION['RHYMIX']['keys'][$domain]['key2'] === $key2 && $key2 !== null) { // OK } - elseif ($_SESSION['RHYMIX']['keys'][$domain]['key2_prev'] === $key2) + elseif ($_SESSION['RHYMIX']['keys'][$domain]['key2_prev'] === $key2 && $key2 !== null) { $must_resend_keys = true; } elseif (!$relax_key_checks) { - unset($_SESSION['RHYMIX']); + $_SESSION = array(); $must_create = true; } } @@ -259,8 +260,10 @@ class Session { unset($_SESSION['RHYMIX']); self::$_started = false; + self::$_member_info = false; self::_setKeys(); session_destroy(); + return true; } /** @@ -285,7 +288,8 @@ class Session $_SESSION['RHYMIX']['login'] = $_SESSION['member_srl'] = $member_srl; $_SESSION['is_logged'] = (bool)$member_srl; - self::refresh(); + self::$_member_info = false; + return self::refresh(); } /** @@ -299,7 +303,8 @@ class Session { $_SESSION['RHYMIX']['login'] = $_SESSION['member_srl'] = false; $_SESSION['is_logged'] = false; - self::destroy(); + self::$_member_info = false; + return self::destroy(); } /** @@ -391,13 +396,33 @@ class Session } // Create a member info object. - if (!self::$_member_info) + if (!self::$_member_info || self::$_member_info->member_srl != $member_srl) { - !self::$_member_info = getModel('member')->getMemberInfoByMemberSrl($member_srl); + self::$_member_info = getModel('member')->getMemberInfoByMemberSrl($member_srl); } // Return the member info object. - return self::$_member_info; + if (self::$_member_info == new \stdClass) + { + return false; + } + else + { + return self::$_member_info; + } + } + + /** + * Set the member info. + * + * This method is for debugging and testing purposes only. + * + * @param object $member_info + * @return void + */ + public static function setMemberInfo($member_info) + { + self::$_member_info = $member_info; } /** @@ -557,7 +582,7 @@ class Session public static function decrypt($ciphertext) { $key = $_SESSION['RHYMIX']['secret'] . Config::get('crypto.encryption_key'); - return Security::encrypt($ciphertext, $key); + return Security::decrypt($ciphertext, $key); } /** @@ -595,6 +620,7 @@ class Session { $key2 = $_COOKIE['rx_sesskey2']; } + return array($key1, $key1 === null ? null : $key2); } @@ -627,5 +653,7 @@ class Session setcookie('rx_sesskey2', $_SESSION['RHYMIX']['keys'][$domain]['key2'], $lifetime, $path, $domain, true, true); $_COOKIE['rx_sesskey2'] = $_SESSION['RHYMIX']['keys'][$domain]['key2']; } + + return true; } } From c43f102aeef6faa5599832fba8f7a077b0b7eb7c Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Fri, 19 Aug 2016 23:07:26 +0900 Subject: [PATCH 10/37] Add unit tests for Session class --- tests/unit/framework/SessionTest.php | 291 +++++++++++++++++++++++++++ 1 file changed, 291 insertions(+) create mode 100644 tests/unit/framework/SessionTest.php diff --git a/tests/unit/framework/SessionTest.php b/tests/unit/framework/SessionTest.php new file mode 100644 index 000000000..35d550d65 --- /dev/null +++ b/tests/unit/framework/SessionTest.php @@ -0,0 +1,291 @@ +assertFalse(isset($_SESSION['foo']['bar'])); + $this->assertNull(Rhymix\Framework\Session::get('foo.bar')); + Rhymix\Framework\Session::set('foo.bar', 'bazz'); + $this->assertTrue(isset($_SESSION['foo']['bar'])); + $this->assertEquals('bazz', Rhymix\Framework\Session::get('foo.bar')); + Rhymix\Framework\Session::set('foo.baz', 'bazzzz'); + $this->assertEquals(array('bar' => 'bazz', 'baz' => 'bazzzz'), Rhymix\Framework\Session::get('foo')); + $this->assertEquals(array('bar' => 'bazz', 'baz' => 'bazzzz'), $_SESSION['foo']); + } + + public function testStart() + { + // Test normal start. + $this->assertTrue(Rhymix\Framework\Session::start()); + $this->assertTrue(isset($_COOKIE['rx_sesskey1'])); + $this->assertTrue(isset($_COOKIE['rx_sesskey2'])); + $this->assertNotEmpty($_SESSION['RHYMIX']['secret']); + $this->assertEquals($_SESSION['RHYMIX']['keys']['www.rhymix.org']['key1'], $_COOKIE['rx_sesskey1']); + $this->assertEquals($_SESSION['RHYMIX']['keys']['www.rhymix.org']['key2'], $_COOKIE['rx_sesskey2']); + $this->assertEquals(0, $_SESSION['RHYMIX']['keys']['www.rhymix.org']['trusted']); + $session_secret = $_SESSION['RHYMIX']['secret']; + Rhymix\Framework\Session::close(); + + // Test normal restart. + $this->assertTrue(Rhymix\Framework\Session::start()); + $this->assertEquals($session_secret, $_SESSION['RHYMIX']['secret']); + $session_secret = $_SESSION['RHYMIX']['secret']; + Rhymix\Framework\Session::close(); + + // Test missing HTTP key. + unset($_COOKIE['rx_sesskey1']); + $this->assertTrue(Rhymix\Framework\Session::start()); + $this->assertNotEquals($session_secret, $_SESSION['RHYMIX']['secret']); + $session_secret = $_SESSION['RHYMIX']['secret']; + Rhymix\Framework\Session::close(); + + // Test missing HTTPS key. + unset($_COOKIE['rx_sesskey2']); + $this->assertTrue(Rhymix\Framework\Session::start()); + $this->assertNotEquals($session_secret, $_SESSION['RHYMIX']['secret']); + $session_secret = $_SESSION['RHYMIX']['secret']; + Rhymix\Framework\Session::close(); + + // Test invalid HTTP key. + $_COOKIE['rx_sesskey1'] = substr(md5(mt_rand()), 0, 24); + $this->assertTrue(Rhymix\Framework\Session::start()); + $this->assertNotEquals($session_secret, $_SESSION['RHYMIX']['secret']); + $session_secret = $_SESSION['RHYMIX']['secret']; + Rhymix\Framework\Session::close(); + + // Test invalid HTTPS key. + $_COOKIE['rx_sesskey2'] = substr(md5(mt_rand()), 0, 24); + $this->assertTrue(Rhymix\Framework\Session::start()); + $this->assertNotEquals($session_secret, $_SESSION['RHYMIX']['secret']); + $session_secret = $_SESSION['RHYMIX']['secret']; + Rhymix\Framework\Session::close(); + + // Test initial transition from HTTP to HTTPS. + session_start(); + unset($_SESSION['RHYMIX']['keys']['www.rhymix.org']['key2']); + unset($_COOKIE['rx_sesskey2']); + session_write_close(); + $this->assertTrue(Rhymix\Framework\Session::start()); + $this->assertEquals($session_secret, $_SESSION['RHYMIX']['secret']); + $session_secret = $_SESSION['RHYMIX']['secret']; + Rhymix\Framework\Session::close(); + } + + public function testRefresh() + { + Rhymix\Framework\Session::start(); + $session_secret = $_SESSION['RHYMIX']['secret']; + $key1 = $_SESSION['RHYMIX']['keys']['www.rhymix.org']['key1']; + $key2 = $_SESSION['RHYMIX']['keys']['www.rhymix.org']['key2']; + $_SESSION['RHYMIX']['keys']['www.rhymix.org']['key1_time'] = time() - 3600; + $_SESSION['RHYMIX']['keys']['www.rhymix.org']['key2_time'] = time() - 3600; + Rhymix\Framework\Session::close(); + + Rhymix\Framework\Session::start(); + $this->assertNotEquals($key1, $_SESSION['RHYMIX']['keys']['www.rhymix.org']['key1']); + $this->assertNotEquals($key2, $_SESSION['RHYMIX']['keys']['www.rhymix.org']['key2']); + $this->assertEquals($key1, $_SESSION['RHYMIX']['keys']['www.rhymix.org']['key1_prev']); + $this->assertEquals($key2, $_SESSION['RHYMIX']['keys']['www.rhymix.org']['key2_prev']); + $this->assertEquals($session_secret, $_SESSION['RHYMIX']['secret']); + Rhymix\Framework\Session::close(); + } + + public function testClose() + { + Rhymix\Framework\Session::start(); + $this->assertEquals(\PHP_SESSION_ACTIVE, session_status()); + Rhymix\Framework\Session::close(); + $this->assertEquals(\PHP_SESSION_NONE, session_status()); + } + + public function testDestroy() + { + Rhymix\Framework\Session::start(); + $this->assertTrue(isset($_SESSION['RHYMIX'])); + Rhymix\Framework\Session::destroy(); + $this->assertFalse(isset($_SESSION['RHYMIX'])); + } + + public function testLoginLogout() + { + Rhymix\Framework\Session::start(); + $this->assertFalse($_SESSION['RHYMIX']['login']); + $this->assertFalse($_SESSION['member_srl']); + $this->assertFalse($_SESSION['is_logged']); + + Rhymix\Framework\Session::login(42); + $this->assertEquals(42, $_SESSION['RHYMIX']['login']); + $this->assertEquals(42, $_SESSION['member_srl']); + $this->assertTrue($_SESSION['is_logged']); + + Rhymix\Framework\Session::logout(); + $this->assertFalse(isset($_SESSION['RHYMIX']['login'])); + $this->assertFalse($_SESSION['member_srl']); + $this->assertFalse($_SESSION['is_logged']); + + Rhymix\Framework\Session::close(); + } + + public function testIsStarted() + { + $this->assertFalse(Rhymix\Framework\Session::isStarted()); + Rhymix\Framework\Session::start(); + $this->assertTrue(Rhymix\Framework\Session::isStarted()); + Rhymix\Framework\Session::close(); + $this->assertFalse(Rhymix\Framework\Session::isStarted()); + } + + public function testIsMember() + { + Rhymix\Framework\Session::start(); + $this->assertFalse(Rhymix\Framework\Session::isMember()); + + Rhymix\Framework\Session::login(42); + $this->assertTrue(Rhymix\Framework\Session::isMember()); + + Rhymix\Framework\Session::close(); + } + + public function testIsAdmin() + { + Rhymix\Framework\Session::start(); + $this->assertFalse(Rhymix\Framework\Session::isAdmin()); + + Rhymix\Framework\Session::login(42); + $this->assertFalse(Rhymix\Framework\Session::isAdmin()); + + Rhymix\Framework\Session::setMemberInfo((object)array('member_srl' => 42, 'is_admin' => 'Y')); + $this->assertTrue(Rhymix\Framework\Session::isAdmin()); + + Rhymix\Framework\Session::setMemberInfo((object)array('member_srl' => 99, 'is_admin' => 'Y')); + $this->assertFalse(Rhymix\Framework\Session::isAdmin()); + + Rhymix\Framework\Session::close(); + } + + public function testIsTrusted() + { + Rhymix\Framework\Session::start(); + + $_SESSION['RHYMIX']['keys']['www.rhymix.org']['trusted'] = 0; + $this->assertFalse(Rhymix\Framework\Session::isTrusted()); + $_SESSION['RHYMIX']['keys']['www.rhymix.org']['trusted'] = time() + 300; + $this->assertTrue(Rhymix\Framework\Session::isTrusted()); + + Rhymix\Framework\Session::close(); + } + + public function testGetMemberSrl() + { + Rhymix\Framework\Session::start(); + $this->assertEquals(false, Rhymix\Framework\Session::getMemberSrl()); + + Rhymix\Framework\Session::login(42); + $this->assertEquals(42, Rhymix\Framework\Session::getMemberSrl()); + + Rhymix\Framework\Session::close(); + } + + public function testGetMemberInfo() + { + Rhymix\Framework\Session::start(); + $this->assertEquals(false, Rhymix\Framework\Session::getMemberInfo()); + + Rhymix\Framework\Session::login(42); + $this->assertEquals(false, Rhymix\Framework\Session::getMemberInfo()); + + Rhymix\Framework\Session::setMemberInfo((object)array('member_srl' => 42)); + $this->assertEquals((object)array('member_srl' => 42), Rhymix\Framework\Session::getMemberInfo()); + + Rhymix\Framework\Session::setMemberInfo((object)array('member_srl' => 99, 'is_admin' => 'Y')); + $this->assertEquals(false, Rhymix\Framework\Session::getMemberInfo()); + + Rhymix\Framework\Session::close(); + } + + public function testGetSetLanguage() + { + Rhymix\Framework\Session::start(); + $this->assertEquals(config('locale.default_language'), Rhymix\Framework\Session::getLanguage()); + + Rhymix\Framework\Session::setLanguage('ja'); + $this->assertEquals('ja', Rhymix\Framework\Session::getLanguage()); + + Rhymix\Framework\Session::close(); + } + + public function testGetSetTimezone() + { + Rhymix\Framework\Session::start(); + $this->assertEquals(config('locale.default_timezone'), Rhymix\Framework\Session::getTimezone()); + + Rhymix\Framework\Session::setTimezone('Asia/Beijing'); + $this->assertEquals('Asia/Beijing', Rhymix\Framework\Session::getTimezone()); + + Rhymix\Framework\Session::close(); + } + + public function testTokens() + { + Rhymix\Framework\Session::start(); + + $token1 = Rhymix\Framework\Session::createToken(); + $this->assertTrue(ctype_alnum($token1)); + $this->assertEquals(16, strlen($token1)); + $this->assertTrue(Rhymix\Framework\Session::verifyToken($token1)); + $this->assertFalse(Rhymix\Framework\Session::verifyToken(strrev($token1))); + + $token2 = Rhymix\Framework\Session::createToken('/my/key'); + $this->assertTrue(Rhymix\Framework\Session::verifyToken($token2, '/my/key')); + $this->assertFalse(Rhymix\Framework\Session::verifyToken($token2)); + $this->assertFalse(Rhymix\Framework\Session::verifyToken($token2, '/wrong/key')); + $this->assertFalse(Rhymix\Framework\Session::verifyToken(strrev($token2))); + + Rhymix\Framework\Session::destroy(); + $this->assertFalse(Rhymix\Framework\Session::verifyToken($token1)); + $this->assertFalse(Rhymix\Framework\Session::verifyToken($token, '/my/key')); + } + + public function testEncryption() + { + Rhymix\Framework\Session::start(); + + $plaintext = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'; + $ciphertext = Rhymix\Framework\Session::encrypt($plaintext); + $this->assertNotEquals(false, $ciphertext); + $this->assertEquals($plaintext, Rhymix\Framework\Session::decrypt($ciphertext)); + + Rhymix\Framework\Session::destroy(); + $this->assertFalse(Rhymix\Framework\Session::decrypt($ciphertext)); + + Rhymix\Framework\Session::start(); + $this->assertFalse(Rhymix\Framework\Session::decrypt($ciphertext)); + + Rhymix\Framework\Session::close(); + } +} From f3b30d8afc7df8e5eeb85efec2dce0064b5d14fe Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Fri, 19 Aug 2016 23:14:33 +0900 Subject: [PATCH 11/37] Fix unit tests --- tests/unit/framework/SessionTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/unit/framework/SessionTest.php b/tests/unit/framework/SessionTest.php index 35d550d65..3a901faf5 100644 --- a/tests/unit/framework/SessionTest.php +++ b/tests/unit/framework/SessionTest.php @@ -5,7 +5,7 @@ class SessionTest extends \Codeception\TestCase\Test public function _before() { Rhymix\Framework\Session::close(); - session_id('rhymix_test_session'); + session_id('rhymix-test-session'); $_SESSION = array(); $_COOKIE = array(); } @@ -13,7 +13,7 @@ class SessionTest extends \Codeception\TestCase\Test public function _after() { Rhymix\Framework\Session::close(); - session_id('rhymix_test_session'); + session_id('rhymix-test-session'); $_SESSION = array(); $_COOKIE = array(); } @@ -21,7 +21,7 @@ class SessionTest extends \Codeception\TestCase\Test public function _failed() { Rhymix\Framework\Session::close(); - session_id('rhymix_test_session'); + session_id('rhymix-test-session'); $_SESSION = array(); $_COOKIE = array(); } @@ -231,7 +231,7 @@ class SessionTest extends \Codeception\TestCase\Test public function testGetSetLanguage() { Rhymix\Framework\Session::start(); - $this->assertEquals(config('locale.default_language'), Rhymix\Framework\Session::getLanguage()); + $this->assertEquals(\Context::getLangType(), Rhymix\Framework\Session::getLanguage()); Rhymix\Framework\Session::setLanguage('ja'); $this->assertEquals('ja', Rhymix\Framework\Session::getLanguage()); From c61316b3daf8e3ec70a35f1c9f75c66d1490d3b7 Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Wed, 14 Sep 2016 21:18:41 +0900 Subject: [PATCH 12/37] Fix compatibility with xe_logged cookie state --- modules/member/member.controller.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modules/member/member.controller.php b/modules/member/member.controller.php index 8a9a626c6..73f4101a7 100644 --- a/modules/member/member.controller.php +++ b/modules/member/member.controller.php @@ -102,6 +102,7 @@ class memberController extends member // Destroy session information Rhymix\Framework\Session::logout(); + $this->destroySessionInfo(); $this->_clearMemberCache($logged_info->member_srl); // Call a trigger after log-out (after) @@ -1896,6 +1897,7 @@ class memberController extends member // Information stored in the session login user Context::set('is_logged', true); Context::set('logged_info', $this->memberInfo); + setcookie('xe_logged', 'true', 0, '/'); // Only the menu configuration of the user (such as an add-on to the menu can be changed) $this->addMemberMenu( 'dispMemberInfo', 'cmd_view_member_info'); From b7c558a96faadacc5a9893565bc28273640ad5ad Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Wed, 5 Oct 2016 17:26:05 +0900 Subject: [PATCH 13/37] Move session delay feature into Session class --- classes/context/Context.class.php | 51 ++++---------------------- common/framework/session.php | 60 +++++++++++++++++++++++++++++-- 2 files changed, 65 insertions(+), 46 deletions(-) diff --git a/classes/context/Context.class.php b/classes/context/Context.class.php index cc46efcfa..b101b6a69 100644 --- a/classes/context/Context.class.php +++ b/classes/context/Context.class.php @@ -339,30 +339,11 @@ class Context ); } - // start session if it was previously started - $session_name = session_name(); - $session_id = NULL; - if($session_id = $_POST[$session_name]) - { - session_id($session_id); - } - else - { - $session_id = $_COOKIE[$session_name]; - } - - if($session_id !== NULL || !config('session.delay')) - { - $this->setCacheControl(0, false); - $relax_key_checks = ($this->act === 'procFileUpload' && preg_match('/shockwave\s?flash/i', $_SERVER['HTTP_USER_AGENT'])); - Rhymix\Framework\Session::start($relax_key_checks); - } - else - { - $this->setCacheControl(-1, true); - $_SESSION = array(); - } + // start session + $relax_key_checks = ($this->act === 'procFileUpload' && preg_match('/shockwave\s?flash/i', $_SERVER['HTTP_USER_AGENT'])); + Rhymix\Framework\Session::start(false, $relax_key_checks); + // start output buffer ob_start(); // set authentication information in Context and session @@ -434,27 +415,9 @@ class Context * * @return void */ - public static function checkSessionStatus($force_start = false) + public static function checkSessionStatus($force = false) { - if(self::getSessionStatus()) - { - return true; - } - if($force_start || (count($_SESSION) && !headers_sent())) - { - $tempSession = $_SESSION; - unset($_SESSION); - Rhymix\Framework\Session::start(); - foreach ($tempSession as $key => $val) - { - if ($key !== 'RHYMIX') - { - $_SESSION[$key] = $val; - } - } - return true; - } - return false; + return Rhymix\Framework\Session::checkStart($force); } /** @@ -471,7 +434,7 @@ class Context } // Check session status and close it if open. - if (self::checkSessionStatus()) + if (Rhymix\Framework\Session::checkStart()) { Rhymix\Framework\Session::close(); } diff --git a/common/framework/session.php b/common/framework/session.php index e8dd1b8b2..6a6eff1cd 100644 --- a/common/framework/session.php +++ b/common/framework/session.php @@ -57,11 +57,12 @@ class Session * * This method is called automatically at Rhymix startup. * There is usually no need to call it manually. - * + * + * @param bool $force (optional) * @param bool $relax_key_checks (optional) * @return bool */ - public static function start($relax_key_checks = false) + public static function start($force = false, $relax_key_checks = false) { // Do not start the session if it is already started. if (self::$_started) @@ -78,6 +79,19 @@ class Session session_set_cookie_params($lifetime, $path, $domain, false, false); session_name(Config::get('session.name') ?: session_name()); + // Get session ID from POST parameter if using relaxed key checks. + if ($relax_key_checks && isset($_POST[session_name()])) + { + session_id($_POST[session_name()]); + } + + // Abort if using delayed session. + if(Config::get('session.delay') && !$force && !isset($_COOKIE[session_name()])) + { + $_SESSION = array(); + return false; + } + // Start the PHP native session. if (!session_start()) { @@ -165,6 +179,48 @@ class Session } } + /** + * Check if the session needs to be started. + * + * This method is called automatically at Rhymix shutdown. + * It is only necessary if the session is delayed. + * + * @param bool $force (optional) + * @return bool + */ + public static function checkStart($force = false) + { + // Return if the session is already started. + if (self::$_started) + { + return true; + } + + // Start the session if it contains data. + if ($force || (count($_SESSION) && !headers_sent())) + { + // Copy session data to a temporary array. + $temp = $_SESSION; + unset($_SESSION); + + // Start the session. + self::start(true); + + // Copy session data back to $_SESSION. + foreach ($temp as $key => $val) + { + if ($key !== 'RHYMIX') + { + $_SESSION[$key] = $val; + } + } + return true; + } + + // Return false if nothing needed to be done. + return false; + } + /** * Create the data structure for a new Rhymix session. * From 7d85a8dd14ebe369d2e8fe216f5d23ee6c3963ee Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Wed, 5 Oct 2016 17:40:09 +0900 Subject: [PATCH 14/37] Use common variable to refer to session name --- common/framework/session.php | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/common/framework/session.php b/common/framework/session.php index 6a6eff1cd..c3901186f 100644 --- a/common/framework/session.php +++ b/common/framework/session.php @@ -77,16 +77,16 @@ class Session ini_set('session.use_only_cookies', 1); ini_set('session.use_strict_mode', 1); session_set_cookie_params($lifetime, $path, $domain, false, false); - session_name(Config::get('session.name') ?: session_name()); + session_name($session_name = Config::get('session.name') ?: session_name()); // Get session ID from POST parameter if using relaxed key checks. - if ($relax_key_checks && isset($_POST[session_name()])) + if ($relax_key_checks && isset($_POST[$session_name])) { - session_id($_POST[session_name()]); + session_id($_POST[$session_name]); } // Abort if using delayed session. - if(Config::get('session.delay') && !$force && !isset($_COOKIE[session_name()])) + if(Config::get('session.delay') && !$force && !isset($_COOKIE[$session_name])) { $_SESSION = array(); return false; @@ -114,7 +114,6 @@ class Session } elseif ($_SESSION['RHYMIX']['keys'][$domain]['key1_prev'] === $key1 && $key1 !== null) { - return 2; $must_resend_keys = true; } elseif (!$relax_key_checks) From db7b613d03efbb7a39642096b71fec8f505ec370 Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Wed, 5 Oct 2016 17:55:25 +0900 Subject: [PATCH 15/37] Prevent Mobile class from adding data to session --- classes/mobile/Mobile.class.php | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/classes/mobile/Mobile.class.php b/classes/mobile/Mobile.class.php index 5e3972ca8..e6bad61ce 100644 --- a/classes/mobile/Mobile.class.php +++ b/classes/mobile/Mobile.class.php @@ -45,12 +45,22 @@ class Mobile // Try to detect from URL arguments and cookies, and finally fall back to user-agent detection. $m = Context::get('m'); - $cookie = (isset($_COOKIE['mobile']) && $_SESSION['user_agent'] === md5($_SERVER['HTTP_USER_AGENT'])) ? $_COOKIE['mobile'] : null; - if ($m === '1' || ($m === null && $cookie === 'true')) + $cookie = isset($_COOKIE['rx_uatype']) ? $_COOKIE['rx_uatype'] : null; + $uahash = base64_encode_urlsafe(md5($_SERVER['HTTP_USER_AGENT'], true)); + if (strncmp($cookie, $uahash . ':', strlen($uahash) + 1) !== 0) + { + $cookie = null; + } + elseif ($m === null) + { + $m = substr($cookie, -1); + } + + if ($m === '1') { self::$_ismobile = TRUE; } - elseif ($m === '0' || ($m === null && $cookie === 'false')) + elseif ($m === '0') { self::$_ismobile = FALSE; } @@ -60,11 +70,11 @@ class Mobile } // Set cookie to prevent recalculation. - if ($cookie !== (self::$_ismobile ? 'true' : 'false')) + $uatype = $uahash . ':' . (self::$_ismobile ? '1' : '0'); + if ($cookie !== $uatype) { - $_SESSION['user_agent'] = md5($_SERVER['HTTP_USER_AGENT']); - $_COOKIE['mobile'] = self::$_ismobile ? 'true' : 'false'; - setcookie('mobile', $_COOKIE['mobile'], 0, RX_BASEURL); + setcookie('rx_uatype', $uatype, 0, RX_BASEURL); + $_COOKIE['rx_uatype'] = $uatype; } return self::$_ismobile; From c1b932d36010121dc08b726f5b0547150ece4eae Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Wed, 5 Oct 2016 20:26:56 +0900 Subject: [PATCH 16/37] Add unit tests for Session::checkStart() --- common/framework/session.php | 2 +- tests/unit/framework/SessionTest.php | 33 ++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/common/framework/session.php b/common/framework/session.php index c3901186f..74426de7a 100644 --- a/common/framework/session.php +++ b/common/framework/session.php @@ -317,7 +317,7 @@ class Session self::$_started = false; self::$_member_info = false; self::_setKeys(); - session_destroy(); + @session_destroy(); return true; } diff --git a/tests/unit/framework/SessionTest.php b/tests/unit/framework/SessionTest.php index 3a901faf5..9e248b9c0 100644 --- a/tests/unit/framework/SessionTest.php +++ b/tests/unit/framework/SessionTest.php @@ -4,6 +4,7 @@ class SessionTest extends \Codeception\TestCase\Test { public function _before() { + Rhymix\Framework\Config::set('session.delay', false); Rhymix\Framework\Session::close(); session_id('rhymix-test-session'); $_SESSION = array(); @@ -12,6 +13,7 @@ class SessionTest extends \Codeception\TestCase\Test public function _after() { + Rhymix\Framework\Config::set('session.delay', false); Rhymix\Framework\Session::close(); session_id('rhymix-test-session'); $_SESSION = array(); @@ -20,6 +22,7 @@ class SessionTest extends \Codeception\TestCase\Test public function _failed() { + Rhymix\Framework\Config::set('session.delay', false); Rhymix\Framework\Session::close(); session_id('rhymix-test-session'); $_SESSION = array(); @@ -96,6 +99,36 @@ class SessionTest extends \Codeception\TestCase\Test Rhymix\Framework\Session::close(); } + public function testCheckStart() + { + Rhymix\Framework\Config::set('session.delay', true); + + $_SESSION = array(); + unset($_COOKIE['PHPSESSID']); + $this->assertFalse(Rhymix\Framework\Session::start()); + $this->assertFalse(Rhymix\Framework\Session::isStarted()); + $this->assertFalse(Rhymix\Framework\Session::checkStart()); + $this->assertFalse(Rhymix\Framework\Session::isStarted()); + + $_SESSION['foo'] = 'bar'; + $this->assertTrue(Rhymix\Framework\Session::checkStart()); + $this->assertTrue(Rhymix\Framework\Session::isStarted()); + $this->assertEquals('bar', $_SESSION['foo']); + $this->assertEquals('bar', Rhymix\Framework\Session::get('foo')); + Rhymix\Framework\Session::close(); + + $_SESSION = array(); + unset($_COOKIE['PHPSESSID']); + $this->assertTrue(Rhymix\Framework\Session::checkStart(true)); + $this->assertTrue(Rhymix\Framework\Session::isStarted()); + Rhymix\Framework\Session::close(); + + $_SESSION = array(); + unset($_COOKIE['PHPSESSID']); + $this->assertTrue(Rhymix\Framework\Session::start(true)); + $this->assertTrue(Rhymix\Framework\Session::isStarted()); + } + public function testRefresh() { Rhymix\Framework\Session::start(); From af41f36bf7eb8538e917a24367e09a078c02ee00 Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Tue, 7 Feb 2017 23:26:43 +0900 Subject: [PATCH 17/37] Move checkSSO() from Context class to Session class --- classes/context/Context.class.php | 92 +----------------------- common/framework/session.php | 101 ++++++++++++++++++++++++++- tests/unit/framework/SessionTest.php | 5 ++ 3 files changed, 106 insertions(+), 92 deletions(-) diff --git a/classes/context/Context.class.php b/classes/context/Context.class.php index 6099080f9..e92d25259 100644 --- a/classes/context/Context.class.php +++ b/classes/context/Context.class.php @@ -670,97 +670,7 @@ class Context */ public function checkSSO() { - // pass if it's not GET request or XE is not yet installed - if(!config('use_sso') || Rhymix\Framework\UA::isRobot()) - { - return TRUE; - } - $checkActList = array('rss' => 1, 'atom' => 1); - if(self::getRequestMethod() != 'GET' || !self::isInstalled() || isset($checkActList[self::get('act')])) - { - return TRUE; - } - - // pass if default URL is not set - $default_url = trim($this->db_info->default_url); - if(!$default_url) - { - return TRUE; - } - - if(substr_compare($default_url, '/', -1) !== 0) - { - $default_url .= '/'; - } - - // Get current site information (only the base URL, not the full URL) - $current_site = self::getRequestUri(); - - // Step 1: if the current site is not the default site, send SSO validation request to the default site - if($default_url !== $current_site && !self::get('sso_response') && $_COOKIE['sso'] !== md5($current_site)) - { - // Set sso cookie to prevent multiple simultaneous SSO validation requests - setcookie('sso', md5($current_site), 0, '/'); - - // Redirect to the default site - $sso_request = Rhymix\Framework\Security::encrypt(Rhymix\Framework\URL::getCurrentURL()); - $redirect_url = $default_url . '?sso_request=' . urlencode($sso_request); - header('Location:' . $redirect_url); - return false; - } - - // Step 2: receive and process SSO validation request at the default site - if($default_url === $current_site && self::get('sso_request')) - { - // Get the URL of the origin site - $sso_request = Rhymix\Framework\Security::decrypt(self::get('sso_request')); - if (!$sso_request || !preg_match('!^https?://!', $sso_request)) - { - self::displayErrorPage('SSO Error', 'Invalid SSO Request', 400); - return false; - } - - // Check that the origin site is a valid site in this XE installation (to prevent open redirect vuln) - if(!getModel('module')->getSiteInfoByDomain(rtrim($url, '/'))->site_srl) - { - self::displayErrorPage('SSO Error', 'Invalid SSO Request', 400); - return false; - } - - // Redirect back to the origin site - $sso_response = Rhymix\Framework\Security::encrypt(session_id()); - header('Location: ' . Rhymix\Framework\URL::modifyURL($sso_request, array('sso_response' => $sso_response))); - return false; - } - - // Step 3: back at the origin site, set session ID to be the same as the default site - if($default_url !== $current_site && self::get('sso_response')) - { - // Check SSO response - $sso_response = Rhymix\Framework\Security::decrypt(self::get('sso_response')); - if ($sso_response === false) - { - self::displayErrorPage('SSO Error', 'Invalid SSO Response', 400); - return false; - } - - // Check that the response was given by the default site (to prevent session fixation CSRF) - if(isset($_SERVER['HTTP_REFERER']) && strpos($_SERVER['HTTP_REFERER'], $default_url) !== 0) - { - self::displayErrorPage('SSO Error', 'Invalid SSO Response', 400); - return false; - } - - // Set session ID - setcookie(session_name(), $sso_response); - - // Finally, redirect to the originally requested URL - header('Location: ' . Rhymix\Framework\URL::getCurrentURL(array('sso_response' => null))); - return false; - } - - // If none of the conditions above apply, proceed normally - return TRUE; + return !Rhymix\Framework\Session::checkSSO(); } /** diff --git a/common/framework/session.php b/common/framework/session.php index 74426de7a..ba8d2c076 100644 --- a/common/framework/session.php +++ b/common/framework/session.php @@ -108,7 +108,11 @@ class Session // Validate the HTTP key. if (isset($_SESSION['RHYMIX']) && $_SESSION['RHYMIX']) { - if ($_SESSION['RHYMIX']['keys'][$domain]['key1'] === $key1 && $key1 !== null) + if (!isset($_SESSION['RHYMIX']['keys'][$domain]) && config('use_sso')) + { + $must_refresh = true; + } + elseif ($_SESSION['RHYMIX']['keys'][$domain]['key1'] === $key1 && $key1 !== null) { // OK } @@ -220,6 +224,95 @@ class Session return false; } + /** + * Check if this session needs to be shared with another site with SSO. + * + * This method uses more or less the same logic as XE's SSO mechanism. + * It may need to be changed to a more secure mechanism later. + * + * @return bool + */ + public static function checkSSO() + { + // Abort if SSO is disabled, the visitor is a robot, or this is not a typical GET request. + if ($_SERVER['REQUEST_METHOD'] !== 'GET' || !config('use_sso') || UA::isRobot() || in_array(\Context::get('act'), array('rss', 'atom'))) + { + return false; + } + + // Abort of the default URL is not set. + $default_url = \Context::getDefaultUrl(); + if (!$default_url) + { + return false; + } + + // Get the current site information. + $current_url = URL::getCurrentURL(); + $current_host = parse_url($current_url, \PHP_URL_HOST); + $default_host = parse_url($default_url, \PHP_URL_HOST); + + // Step 1: if the current site is not the default site, send SSO validation request to the default site. + if($default_host !== $current_host && !\Context::get('sso_response') && $_COOKIE['sso'] !== md5($current_host)) + { + // Set sso cookie to prevent multiple simultaneous SSO validation requests. + setcookie('sso', md5($current_host), 0, '/'); + + // Redirect to the default site. + $sso_request = Security::encrypt($current_url); + header('Location:' . URL::modifyURL($default_url, array('sso_request' => $sso_request))); + return true; + } + + // Step 2: receive and process SSO validation request at the default site. + if($default_host === $current_host && \Context::get('sso_request')) + { + // Get the URL of the origin site + $sso_request = Security::decrypt(\Context::get('sso_request')); + if (!$sso_request || !preg_match('!^https?://!', $sso_request)) + { + \Context::displayErrorPage('SSO Error', 'Invalid SSO Request', 400); + return true; + } + + // Redirect back to the origin site. + $sso_response = Security::encrypt(session_id()); + header('Location: ' . URL::modifyURL($sso_request, array('sso_response' => $sso_response))); + return true; + } + + // Step 3: back at the origin site, set session ID to be the same as the default site. + if($default_host !== $current_host && \Context::get('sso_response')) + { + // Check SSO response + $sso_response = Security::decrypt(\Context::get('sso_response')); + if ($sso_response === false) + { + \Context::displayErrorPage('SSO Error', 'Invalid SSO Response', 400); + return true; + } + + // Check that the response was given by the default site (to prevent session fixation CSRF). + if(isset($_SERVER['HTTP_REFERER']) && strpos($_SERVER['HTTP_REFERER'], $default_url) !== 0) + { + \Context::displayErrorPage('SSO Error', 'Invalid SSO Response', 400); + return true; + } + + // Set session ID. + self::close(); + session_id($sso_response); + self::start(); + + // Finally, redirect to the originally requested URL. + header('Location: ' . URL::getCurrentURL(array('sso_response' => null))); + return true; + } + + // If none of the conditions above apply, proceed normally. + return false; + } + /** * Create the data structure for a new Rhymix session. * @@ -261,6 +354,12 @@ class Session // Get session parameters. list($lifetime, $refresh_interval, $domain, $path) = self::_getParams(); + // Set the domain initialization timestamp. + if (!isset($_SESSION['RHYMIX']['keys'][$domain]['started'])) + { + $_SESSION['RHYMIX']['keys'][$domain]['started'] = time(); + } + // Reset the trusted information. if (!isset($_SESSION['RHYMIX']['keys'][$domain]['trusted'])) { diff --git a/tests/unit/framework/SessionTest.php b/tests/unit/framework/SessionTest.php index 9e248b9c0..5ca61074c 100644 --- a/tests/unit/framework/SessionTest.php +++ b/tests/unit/framework/SessionTest.php @@ -129,6 +129,11 @@ class SessionTest extends \Codeception\TestCase\Test $this->assertTrue(Rhymix\Framework\Session::isStarted()); } + public function testCheckSSO() + { + $this->assertFalse(Rhymix\Framework\Session::checkSSO()); + } + public function testRefresh() { Rhymix\Framework\Session::start(); From ca9a0aef25043c753314b41a4282302975d14cc7 Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Wed, 8 Feb 2017 16:16:31 +0900 Subject: [PATCH 18/37] Update autologin table with more columns --- classes/db/DBCubrid.class.php | 23 +++++++++++++++++++++ classes/db/DBMssql.class.php | 23 +++++++++++++++++++++ classes/db/DBMysql.class.php | 23 +++++++++++++++++++++ modules/member/member.class.php | 10 +++++++++ modules/member/schemas/member_autologin.xml | 10 +++++++-- 5 files changed, 87 insertions(+), 2 deletions(-) diff --git a/classes/db/DBCubrid.class.php b/classes/db/DBCubrid.class.php index 1491eb73b..7963ae620 100644 --- a/classes/db/DBCubrid.class.php +++ b/classes/db/DBCubrid.class.php @@ -956,6 +956,29 @@ class DBCubrid extends DB } } + /** + * Drop table + * + * @param string $name + * @return bool + */ + function dropTable($table_name) + { + // Generate the drop query + $query = sprintf('drop class "%s"', $this->addQuotes($this->prefix . $table_name)); + + // Execute the drop query + $output = $this->_query($query); + if($output) + { + return true; + } + else + { + return false; + } + } + /** * Handles insertAct * @param Object $queryObject diff --git a/classes/db/DBMssql.class.php b/classes/db/DBMssql.class.php index a1473ac73..b0ffa4bd2 100644 --- a/classes/db/DBMssql.class.php +++ b/classes/db/DBMssql.class.php @@ -748,6 +748,29 @@ class DBMssql extends DB } } + /** + * Drop table + * + * @param string $name + * @return bool + */ + function dropTable($table_name) + { + // Generate the drop query + $query = sprintf('DROP TABLE %s', $this->addQuotes($this->prefix . $table_name)); + + // Execute the drop query + $output = $this->_query($query); + if($output) + { + return true; + } + else + { + return false; + } + } + /** * Handles insertAct * @todo Lookup _filterNumber against sql injection - see if it is still needed and how to integrate diff --git a/classes/db/DBMysql.class.php b/classes/db/DBMysql.class.php index 27e16b315..5ec68cc04 100644 --- a/classes/db/DBMysql.class.php +++ b/classes/db/DBMysql.class.php @@ -682,6 +682,29 @@ class DBMysql extends DB } } + /** + * Drop table + * + * @param string $name + * @return bool + */ + function dropTable($table_name) + { + // Generate the drop query + $query = sprintf('DROP TABLE `%s`', $this->addQuotes($this->prefix . $table_name)); + + // Execute the drop query + $output = $this->_query($query); + if($output) + { + return true; + } + else + { + return false; + } + } + /** * Handles insertAct * @param Object $queryObject diff --git a/modules/member/member.class.php b/modules/member/member.class.php index c2a59818e..31ae1df4e 100644 --- a/modules/member/member.class.php +++ b/modules/member/member.class.php @@ -204,6 +204,9 @@ class member extends ModuleObject { if(!$oDB->isColumnExists("member", "list_order")) return true; if(!$oDB->isIndexExists("member","idx_list_order")) return true; + + // Check autologin table + if(!$oDB->isColumnExists("member_autologin", "security_key")) return true; $oModuleModel = getModel('module'); $config = $oModuleModel->getModuleConfig('member'); @@ -314,6 +317,13 @@ class member extends ModuleObject { { $oDB->addIndex("member","idx_list_order", array("list_order")); } + + // Check autologin table + if(!$oDB->isColumnExists("member_autologin", "security_key")) + { + $oDB->dropTable('member_autologin'); + $oDB->createTableByXmlFile($this->module_path . '/schemas/member_autologin.xml'); + } $oModuleModel = getModel('module'); $config = $oModuleModel->getModuleConfig('member'); diff --git a/modules/member/schemas/member_autologin.xml b/modules/member/schemas/member_autologin.xml index c4bc326fe..2ec17e1d6 100644 --- a/modules/member/schemas/member_autologin.xml +++ b/modules/member/schemas/member_autologin.xml @@ -1,4 +1,10 @@ - - + + + + + + + +
From d36263f539d5deb9a063ab5c6174313bf605646c Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Wed, 8 Feb 2017 16:39:29 +0900 Subject: [PATCH 19/37] Update autologin-related queries --- modules/member/queries/deleteAutologin.xml | 2 +- modules/member/queries/getAutologin.xml | 13 ++++++++++--- modules/member/queries/insertAutologin.xml | 20 +++++++++++++------- modules/member/queries/updateAutologin.xml | 12 ++++++++++++ 4 files changed, 36 insertions(+), 11 deletions(-) create mode 100644 modules/member/queries/updateAutologin.xml diff --git a/modules/member/queries/deleteAutologin.xml b/modules/member/queries/deleteAutologin.xml index 789171804..a26672edc 100644 --- a/modules/member/queries/deleteAutologin.xml +++ b/modules/member/queries/deleteAutologin.xml @@ -3,7 +3,7 @@ - + diff --git a/modules/member/queries/getAutologin.xml b/modules/member/queries/getAutologin.xml index 87ce14402..ba915f0fc 100644 --- a/modules/member/queries/getAutologin.xml +++ b/modules/member/queries/getAutologin.xml @@ -4,13 +4,20 @@
+ - - - + + + + + + + + + diff --git a/modules/member/queries/insertAutologin.xml b/modules/member/queries/insertAutologin.xml index ed3c5aa3d..9bbdf43d7 100644 --- a/modules/member/queries/insertAutologin.xml +++ b/modules/member/queries/insertAutologin.xml @@ -1,9 +1,15 @@ - -
- - - - - + +
+ + + + + + + + + + + diff --git a/modules/member/queries/updateAutologin.xml b/modules/member/queries/updateAutologin.xml new file mode 100644 index 000000000..cd215a0e6 --- /dev/null +++ b/modules/member/queries/updateAutologin.xml @@ -0,0 +1,12 @@ + + +
+ + + + + + + + + From 2af90c8e1d0c56f6ad9d7f676aaaca5acee96b63 Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Wed, 8 Feb 2017 17:08:31 +0900 Subject: [PATCH 20/37] Implement autologin in the Session class --- classes/context/Context.class.php | 5 -- common/framework/session.php | 74 ++++++++++++++-- modules/member/member.controller.php | 126 +++++++++++---------------- 3 files changed, 117 insertions(+), 88 deletions(-) diff --git a/classes/context/Context.class.php b/classes/context/Context.class.php index e92d25259..449bf7edc 100644 --- a/classes/context/Context.class.php +++ b/classes/context/Context.class.php @@ -362,11 +362,6 @@ class Context { $oMemberController->setSessionInfo(); } - // check auto sign-in - elseif($_COOKIE['xeak']) - { - $oMemberController->doAutologin(); - } self::set('is_logged', $oMemberModel->isLogged()); self::set('logged_info', $oMemberModel->getLoggedInfo()); diff --git a/common/framework/session.php b/common/framework/session.php index ba8d2c076..fe56acc01 100644 --- a/common/framework/session.php +++ b/common/framework/session.php @@ -102,7 +102,7 @@ class Session self::$_started = true; // Fetch session keys. - list($key1, $key2) = self::_getKeys(); + list($key1, $key2, $autologin_key) = self::_getKeys(); $must_create = $must_refresh = $must_resend_keys = false; // Validate the HTTP key. @@ -122,8 +122,11 @@ class Session } elseif (!$relax_key_checks) { + // Hacked session! Destroy everything. $_SESSION = array(); $must_create = true; + self::setAutologinKeys(null, null); + $autologin_key = null; } } else @@ -148,8 +151,11 @@ class Session } elseif (!$relax_key_checks) { + // Hacked session! Destroy everything. $_SESSION = array(); $must_create = true; + self::setAutologinKeys(null, null); + $autologin_key = null; } } @@ -166,7 +172,7 @@ class Session // Create or refresh the session if needed. if ($must_create) { - return self::create(); + return self::create($autologin_key); } elseif ($must_refresh) { @@ -318,9 +324,10 @@ class Session * * This method is called automatically by start() when needed. * + * @param string $autologin_key (optional) * @return bool */ - public static function create() + public static function create($autologin_key = null) { // Ensure backward compatibility with XE session. $member_srl = $_SESSION['member_srl'] ?: false; @@ -329,7 +336,7 @@ class Session // Create the data structure for a new Rhymix session. $_SESSION['RHYMIX'] = array(); - $_SESSION['RHYMIX']['login'] = $_SESSION['member_srl'] = $member_srl; + $_SESSION['RHYMIX']['login'] = $_SESSION['member_srl'] = intval($member_srl); $_SESSION['RHYMIX']['ipaddress'] = $_SESSION['ipaddress'] = \RX_CLIENT_IP; $_SESSION['RHYMIX']['useragent'] = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : ''; $_SESSION['RHYMIX']['language'] = \Context::getLangType(); @@ -338,7 +345,25 @@ class Session $_SESSION['RHYMIX']['tokens'] = array(); // Pass control to refresh() to generate security keys. - return self::refresh(); + $result = self::refresh(); + + // Try autologin. + if (!$member_srl && $autologin_key) + { + $member_srl = getController('member')->doAutologin($autologin_key); + if ($member_srl) + { + $_SESSION['RHYMIX']['login'] = $_SESSION['member_srl'] = intval($member_srl); + $_SESSION['is_logged'] = true; + } + else + { + self::setAutologinKeys(null, null); + } + } + + // Return the result obtained above. + return $result; } /** @@ -416,6 +441,7 @@ class Session self::$_started = false; self::$_member_info = false; self::_setKeys(); + self::setAutologinKeys(null, null); @session_destroy(); return true; } @@ -761,7 +787,7 @@ class Session protected static function _getKeys() { // Initialize keys. - $key1 = $key2 = null; + $key1 = $key2 = $key3 = null; // Fetch and validate the HTTP-only key. if (isset($_COOKIE['rx_sesskey1']) && ctype_alnum($_COOKIE['rx_sesskey1']) && strlen($_COOKIE['rx_sesskey1']) === 24) @@ -775,7 +801,13 @@ class Session $key2 = $_COOKIE['rx_sesskey2']; } - return array($key1, $key1 === null ? null : $key2); + // Fetch and validate the autologin key. + if (isset($_COOKIE['rx_autologin']) && ctype_alnum($_COOKIE['rx_autologin']) && strlen($_COOKIE['rx_autologin']) === 48) + { + $key3 = $_COOKIE['rx_autologin']; + } + + return array($key1, $key1 === null ? null : $key2, $key3); } /** @@ -810,4 +842,32 @@ class Session return true; } + + /** + * Set autologin key. + * + * @param string $autologin_key + * @param string $security_key + * @return bool + */ + public static function setAutologinKeys($autologin_key = null, $security_key = null) + { + // Get session parameters. + list($lifetime, $refresh_interval, $domain, $path) = self::_getParams(); + $lifetime = time() + (86400 * 365); + + // Set or destroy the HTTP-only key. + if ($autologin_key && $security_key) + { + setcookie('rx_autologin', $autologin_key . $security_key, $lifetime, $path, $domain, false, true); + $_COOKIE['rx_autologin'] = $autologin_key . $security_key; + } + else + { + setcookie('rx_autologin', 'deleted', time() - 86400, $path, $domain, false, true); + unset($_COOKIE['rx_autologin']); + } + + return true; + } } diff --git a/modules/member/member.controller.php b/modules/member/member.controller.php index d200e96e4..a655b3c90 100644 --- a/modules/member/member.controller.php +++ b/modules/member/member.controller.php @@ -1623,82 +1623,56 @@ class memberController extends member /** * Auto-login * - * @return void + * @param string $autologin_key + * @return int|false */ - function doAutologin() + function doAutologin($autologin_key = null) { - // Get a key value of auto log-in - $args = new stdClass; - $args->autologin_key = $_COOKIE['xeak']; - // Get information of the key - $output = executeQuery('member.getAutologin', $args); - // If no information exists, delete a cookie - if(!$output->toBool() || !$output->data) + // Validate the key. + if (strlen($autologin_key) == 48) { - setCookie('xeak',null,$_SERVER['REQUEST_TIME']+60*60*24*365, '/'); - return; - } - - $oMemberModel = getModel('member'); - $config = $oMemberModel->getMemberConfig(); - - $user_id = ($config->identifier == 'user_id') ? $output->data->user_id : $output->data->email_address; - $password = $output->data->password; - - if(!$user_id || !$password) - { - setCookie('xeak',null,$_SERVER['REQUEST_TIME']+60*60*24*365, '/'); - return; - } - - $do_auto_login = false; - - // Compare key values based on the information - $check_key = strtolower($user_id).$password.$_SERVER['HTTP_USER_AGENT']; - $check_key = substr(hash_hmac('sha256', $check_key, substr($args->autologin_key, 0, 32)), 0, 32); - - if($check_key === substr($args->autologin_key, 32)) - { - // Check change_password_date - $oModuleModel = getModel('module'); - $member_config = $oModuleModel->getModuleConfig('member'); - $limit_date = $member_config->change_password_date; - - // Check if change_password_date is set - if($limit_date > 0) - { - $oMemberModel = getModel('member'); - $columnList = array('member_srl', 'change_password_date'); - - if($config->identifier == 'user_id') - { - $member_info = $oMemberModel->getMemberInfoByUserID($user_id, $columnList); - } - else - { - $member_info = $oMemberModel->getMemberInfoByEmailAddress($user_id, $columnList); - } - - if($member_info->change_password_date >= date('YmdHis', strtotime('-'.$limit_date.' day')) ){ - $do_auto_login = true; - } - - } - else - { - $do_auto_login = true; - } - } - - if($do_auto_login) - { - $output = $this->doLogin($user_id); + $security_key = substr($autologin_key, 24, 24); + $autologin_key = substr($autologin_key, 0, 24); } else { - executeQuery('member.deleteAutologin', $args); - setCookie('xeak',null,$_SERVER['REQUEST_TIME']+60*60*24*365, '/'); + return false; } + + // Fetch autologin information from DB. + $args = new stdClass; + $args->autologin_key = $autologin_key; + $output = executeQuery('member.getAutologin', $args); + if (!$output->toBool() || !$output->data) + { + return false; + } + if (is_array($output->data)) + { + $output->data = array_first($output->data); + } + + // Check the security key. + if ($output->data->security_key !== $security_key || !$output->data->member_srl) + { + $args = new stdClass; + $args->autologin_key = $autologin_key; + executeQuery('member.deleteAutologin', $args); + return false; + } + + // Update the security key. + $new_security_key = Rhymix\Framework\Security::getRandom(24, 'alnum'); + $args = new stdClass; + $args->security_key = $new_security_key; + $update_output = executeQuery('member.updateAutologin', $args); + if ($update_output->toBool()) + { + Rhymix\Framework\Session::setAutologinKeys($autologin_key, $new_security_key); + } + + // Return the member_srl. + return intval($output->data->member_srl); } /** @@ -1860,16 +1834,16 @@ class memberController extends member // When user checked to use auto-login if($keep_signed) { - // Key generate for auto login - $random_key = Rhymix\Framework\Security::getRandom(32, 'hex'); - $extra_key = strtolower($user_id).$this->memberInfo->password.$_SERVER['HTTP_USER_AGENT']; - $extra_key = substr(hash_hmac('sha256', $extra_key, $random_key), 0, 32); + $random_key = Rhymix\Framework\Security::getRandom(48, 'alnum'); $autologin_args = new stdClass; - $autologin_args->autologin_key = $random_key.$extra_key; + $autologin_args->autologin_key = substr($random_key, 0, 24); + $autologin_args->security_key = substr($random_key, 24, 24); $autologin_args->member_srl = $this->memberInfo->member_srl; - executeQuery('member.deleteAutologin', $autologin_args); $autologin_output = executeQuery('member.insertAutologin', $autologin_args); - if($autologin_output->toBool()) setCookie('xeak',$autologin_args->autologin_key, $_SERVER['REQUEST_TIME']+31536000, '/'); + if ($autologin_output->toBool()) + { + Rhymix\Framework\Session::setAutologinKeys(substr($random_key, 0, 24), substr($random_key, 24, 24)); + } } $this->setSessionInfo(); From b0b416b8230d64db21f9deae1c172c048901cfb3 Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Wed, 8 Feb 2017 17:12:46 +0900 Subject: [PATCH 21/37] Fix autologin table structure and add user agent data --- modules/member/member.controller.php | 1 + modules/member/queries/getAutologin.xml | 2 +- modules/member/schemas/member_autologin.xml | 5 +++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/modules/member/member.controller.php b/modules/member/member.controller.php index a655b3c90..fca3fdea2 100644 --- a/modules/member/member.controller.php +++ b/modules/member/member.controller.php @@ -1839,6 +1839,7 @@ class memberController extends member $autologin_args->autologin_key = substr($random_key, 0, 24); $autologin_args->security_key = substr($random_key, 24, 24); $autologin_args->member_srl = $this->memberInfo->member_srl; + $autologin_args->user_agent = json_encode(Rhymix\Framework\UA::getBrowserInfo()); $autologin_output = executeQuery('member.insertAutologin', $autologin_args); if ($autologin_output->toBool()) { diff --git a/modules/member/queries/getAutologin.xml b/modules/member/queries/getAutologin.xml index ba915f0fc..d790ac576 100644 --- a/modules/member/queries/getAutologin.xml +++ b/modules/member/queries/getAutologin.xml @@ -15,7 +15,7 @@ - + diff --git a/modules/member/schemas/member_autologin.xml b/modules/member/schemas/member_autologin.xml index 2ec17e1d6..183b7ef47 100644 --- a/modules/member/schemas/member_autologin.xml +++ b/modules/member/schemas/member_autologin.xml @@ -1,10 +1,11 @@
- + + - +
From 59c3fa1381eb5897e500edf351ae9d6b7ae79d1a Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Wed, 8 Feb 2017 18:06:26 +0900 Subject: [PATCH 22/37] Fix unexpected cast to int --- common/framework/session.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/framework/session.php b/common/framework/session.php index fe56acc01..fa82eb56c 100644 --- a/common/framework/session.php +++ b/common/framework/session.php @@ -336,7 +336,7 @@ class Session // Create the data structure for a new Rhymix session. $_SESSION['RHYMIX'] = array(); - $_SESSION['RHYMIX']['login'] = $_SESSION['member_srl'] = intval($member_srl); + $_SESSION['RHYMIX']['login'] = $_SESSION['member_srl'] = $member_srl; $_SESSION['RHYMIX']['ipaddress'] = $_SESSION['ipaddress'] = \RX_CLIENT_IP; $_SESSION['RHYMIX']['useragent'] = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : ''; $_SESSION['RHYMIX']['language'] = \Context::getLangType(); From 8afddb8b5e8defd3bae9721294031ba5f899139b Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Wed, 8 Feb 2017 18:07:19 +0900 Subject: [PATCH 23/37] Implement login management screen in default member skin --- modules/member/conf/module.xml | 2 + modules/member/lang/en.php | 4 ++ modules/member/lang/ko.php | 4 ++ modules/member/member.controller.php | 27 +++++++++++ modules/member/member.view.php | 24 ++++++++++ modules/member/queries/getAutologin.xml | 1 + modules/member/schemas/member_autologin.xml | 2 +- .../member/skins/default/active_logins.html | 45 +++++++++++++++++++ 8 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 modules/member/skins/default/active_logins.html diff --git a/modules/member/conf/module.xml b/modules/member/conf/module.xml index 0154c5404..27baf50b7 100644 --- a/modules/member/conf/module.xml +++ b/modules/member/conf/module.xml @@ -16,6 +16,7 @@ + @@ -62,6 +63,7 @@ + diff --git a/modules/member/lang/en.php b/modules/member/lang/en.php index e840906e0..800144cf2 100644 --- a/modules/member/lang/en.php +++ b/modules/member/lang/en.php @@ -107,6 +107,7 @@ $lang->cmd_manage_email_host = 'E-mail provider check'; $lang->cmd_manage_nick_name = 'Prohibited NickNames'; $lang->cmd_manage_form = 'Signup Form'; $lang->cmd_view_own_document = 'Written Articles'; +$lang->cmd_view_active_logins = 'Active Logins'; $lang->cmd_manage_member_info = 'Manage Member Info'; $lang->cmd_trace_document = 'Trace Written Articles'; $lang->cmd_trace_comment = 'Trace Written Comments'; @@ -297,3 +298,6 @@ $lang->spammer_description = '

Spam user management. This function denied user $lang->btn_spammer_delete_all = 'Delete all'; $lang->spammer_move_to_trash = 'Move to trash'; $lang->msg_spammer_complete = 'Completed.'; +$lang->cmd_login_browser_info = 'Browser Information'; +$lang->cmd_initial_login = 'First Login'; +$lang->cmd_recent_visit = 'Recent Visit'; diff --git a/modules/member/lang/ko.php b/modules/member/lang/ko.php index d7f774352..f20d1752f 100644 --- a/modules/member/lang/ko.php +++ b/modules/member/lang/ko.php @@ -109,6 +109,7 @@ $lang->cmd_manage_email_host = '이메일 제공자 관리'; $lang->cmd_manage_nick_name = '금지 닉네임 관리'; $lang->cmd_manage_form = '가입 폼 관리'; $lang->cmd_view_own_document = '작성 글 보기'; +$lang->cmd_view_active_logins = '로그인 관리'; $lang->cmd_manage_member_info = '회원 정보 관리'; $lang->cmd_trace_document = '작성글 추적'; $lang->cmd_trace_comment = '작성 댓글 추적'; @@ -317,3 +318,6 @@ $lang->spammer_move_to_trash = '휴지통으로 이동'; $lang->msg_spammer_complete = '완료되었습니다.'; $lang->nick_name_before_changing = '닉네임 변경 전'; $lang->nick_name_after_changing = '닉네임 변경 후'; +$lang->cmd_login_browser_info = '브라우저 정보'; +$lang->cmd_initial_login = '최초 로그인'; +$lang->cmd_recent_visit = '최근 방문'; diff --git a/modules/member/member.controller.php b/modules/member/member.controller.php index fca3fdea2..78624235a 100644 --- a/modules/member/member.controller.php +++ b/modules/member/member.controller.php @@ -204,6 +204,32 @@ class memberController extends member $oDocumentController = getController('document'); $oDocumentController->deleteDocument($document_srl, true); } + + /** + * Delete an autologin + */ + function procMemberDeleteAutologin() + { + // Check login information + if(!Context::get('is_logged')) return new Object(-1, 'msg_not_logged'); + $logged_info = Context::get('logged_info'); + + $autologin_id = intval(Context::get('autologin_id')); + $autologin_key = Context::get('autologin_key'); + + $args = new stdClass; + $args->autologin_id = $autologin_id; + $args->autologin_key = $autologin_key; + $output = executeQueryArray('member.getAutologin', $args); + if ($output->toBool() && $output->data) + { + $autologin_info = array_first($output->data); + if ($autologin_info->member_srl == $logged_info->member_srl) + { + executeQuery('member.deleteAutologin', $args); + } + } + } /** * Check values when member joining @@ -1906,6 +1932,7 @@ class memberController extends member $this->addMemberMenu( 'dispMemberScrappedDocument', 'cmd_view_scrapped_document'); $this->addMemberMenu( 'dispMemberSavedDocument', 'cmd_view_saved_document'); $this->addMemberMenu( 'dispMemberOwnDocument', 'cmd_view_own_document'); + $this->addMemberMenu( 'dispMemberActiveLogins', 'cmd_view_active_logins'); if($config->update_nickname_log == 'Y') { $this->addMemberMenu( 'dispMemberModifyNicknameLog', 'cmd_modify_nickname_log'); diff --git a/modules/member/member.view.php b/modules/member/member.view.php index 147680318..e7a1a5427 100644 --- a/modules/member/member.view.php +++ b/modules/member/member.view.php @@ -430,6 +430,30 @@ class memberView extends member $this->setTemplateFile('saved_list'); } + /** + * @brief Display the login management page + */ + function dispMemberActiveLogins() + { + $logged_info = Context::get('logged_info'); + if (!$logged_info->member_srl) + { + return $this->stop('msg_not_logged'); + } + + $args = new stdClass(); + $args->member_srl = $logged_info->member_srl; + $args->page = (int)Context::get('page'); + $output = executeQueryArray('member.getAutologin', $args); + Context::set('total_count', $output->total_count); + Context::set('total_page', $output->total_page); + Context::set('page', $output->page); + Context::set('active_logins', $output->data); + Context::set('page_navigation', $output->page_navigation); + + $this->setTemplateFile('active_logins'); + } + /** * @brief Display the login form */ diff --git a/modules/member/queries/getAutologin.xml b/modules/member/queries/getAutologin.xml index d790ac576..e96595f8b 100644 --- a/modules/member/queries/getAutologin.xml +++ b/modules/member/queries/getAutologin.xml @@ -11,6 +11,7 @@ + diff --git a/modules/member/schemas/member_autologin.xml b/modules/member/schemas/member_autologin.xml index 183b7ef47..e051ed8cf 100644 --- a/modules/member/schemas/member_autologin.xml +++ b/modules/member/schemas/member_autologin.xml @@ -1,6 +1,6 @@ - + diff --git a/modules/member/skins/default/active_logins.html b/modules/member/skins/default/active_logins.html new file mode 100644 index 000000000..a0db8cb26 --- /dev/null +++ b/modules/member/skins/default/active_logins.html @@ -0,0 +1,45 @@ + +

{$member_title = $lang->cmd_view_active_logins }

+
+ + + + + + + + + + + + {@ $autologin_info->user_agent = @json_decode($autologin_info->user_agent) ?: new stdClass()} + + + + + + +
{$lang->no}{$lang->cmd_login_browser_info}{$lang->cmd_initial_login}{$lang->cmd_recent_visit}{$lang->cmd_delete}
{$no} + {$autologin_info->user_agent->browser} {$autologin_info->user_agent->version}
+ {$autologin_info->user_agent->os} {$autologin_info->user_agent->is_tablet ? 'Tablet' : ($autologin_info->user_agent->is_mobile ? 'Mobile' : 'PC')} +
{zdate($autologin_info->regdate, 'Y-m-d H:i')}
{$autologin_info->ipaddress}
{zdate($autologin_info->last_visit, 'Y-m-d H:i')}
{$autologin_info->last_ipaddress}
+
+

+ + From 07708195fb352100136e83f90851d84c6ee402ef Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Wed, 8 Feb 2017 20:01:16 +0900 Subject: [PATCH 24/37] Add login management screen to simple_world skin, too --- modules/member/member.controller.php | 18 ++++++- .../member/skins/default/active_logins.html | 9 ++-- .../skins/simple_world/active_logins.html | 47 +++++++++++++++++++ 3 files changed, 69 insertions(+), 5 deletions(-) create mode 100644 modules/member/skins/simple_world/active_logins.html diff --git a/modules/member/member.controller.php b/modules/member/member.controller.php index 78624235a..991cc420b 100644 --- a/modules/member/member.controller.php +++ b/modules/member/member.controller.php @@ -226,8 +226,24 @@ class memberController extends member $autologin_info = array_first($output->data); if ($autologin_info->member_srl == $logged_info->member_srl) { - executeQuery('member.deleteAutologin', $args); + $output = executeQuery('member.deleteAutologin', $args); + if ($output->toBool()) + { + $this->add('deleted', 'Y'); + } + else + { + $this->add('deleted', 'N'); + } } + else + { + $this->add('deleted', 'N'); + } + } + else + { + $this->add('deleted', 'N'); } } diff --git a/modules/member/skins/default/active_logins.html b/modules/member/skins/default/active_logins.html index a0db8cb26..b24baed76 100644 --- a/modules/member/skins/default/active_logins.html +++ b/modules/member/skins/default/active_logins.html @@ -15,12 +15,12 @@ {@ $autologin_info->user_agent = @json_decode($autologin_info->user_agent) ?: new stdClass()} {$no} - {$autologin_info->user_agent->browser} {$autologin_info->user_agent->version}
- {$autologin_info->user_agent->os} {$autologin_info->user_agent->is_tablet ? 'Tablet' : ($autologin_info->user_agent->is_mobile ? 'Mobile' : 'PC')} + {escape($autologin_info->user_agent->browser)} {escape($autologin_info->user_agent->version)}
+ {escape($autologin_info->user_agent->os)} {$autologin_info->user_agent->is_tablet ? 'Tablet' : ($autologin_info->user_agent->is_mobile ? 'Mobile' : 'PC')} {zdate($autologin_info->regdate, 'Y-m-d H:i')}
{$autologin_info->ipaddress} {zdate($autologin_info->last_visit, 'Y-m-d H:i')}
{$autologin_info->last_ipaddress} - + @@ -35,7 +35,8 @@ + From bdb10d57c5eae31bd39c87189dcaf2460562e5bf Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Wed, 8 Feb 2017 21:35:00 +0900 Subject: [PATCH 25/37] Miscellaneous fixes to session handling --- common/framework/session.php | 22 ++++++++++++++-------- modules/member/member.controller.php | 12 ++++-------- modules/member/queries/deleteAutologin.xml | 2 +- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/common/framework/session.php b/common/framework/session.php index fa82eb56c..44a36d1e7 100644 --- a/common/framework/session.php +++ b/common/framework/session.php @@ -11,6 +11,7 @@ class Session * Properties for internal use only. */ protected static $_started = false; + protected static $_autologin_key = false; protected static $_member_info = false; /** @@ -102,7 +103,7 @@ class Session self::$_started = true; // Fetch session keys. - list($key1, $key2, $autologin_key) = self::_getKeys(); + list($key1, $key2, self::$_autologin_key) = self::_getKeys(); $must_create = $must_refresh = $must_resend_keys = false; // Validate the HTTP key. @@ -126,7 +127,6 @@ class Session $_SESSION = array(); $must_create = true; self::setAutologinKeys(null, null); - $autologin_key = null; } } else @@ -155,7 +155,6 @@ class Session $_SESSION = array(); $must_create = true; self::setAutologinKeys(null, null); - $autologin_key = null; } } @@ -172,7 +171,7 @@ class Session // Create or refresh the session if needed. if ($must_create) { - return self::create($autologin_key); + return self::create(); } elseif ($must_refresh) { @@ -324,10 +323,9 @@ class Session * * This method is called automatically by start() when needed. * - * @param string $autologin_key (optional) * @return bool */ - public static function create($autologin_key = null) + public static function create() { // Ensure backward compatibility with XE session. $member_srl = $_SESSION['member_srl'] ?: false; @@ -337,6 +335,7 @@ class Session // Create the data structure for a new Rhymix session. $_SESSION['RHYMIX'] = array(); $_SESSION['RHYMIX']['login'] = $_SESSION['member_srl'] = $member_srl; + $_SESSION['RHYMIX']['last_login'] = $member_srl ? time() : false; $_SESSION['RHYMIX']['ipaddress'] = $_SESSION['ipaddress'] = \RX_CLIENT_IP; $_SESSION['RHYMIX']['useragent'] = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : ''; $_SESSION['RHYMIX']['language'] = \Context::getLangType(); @@ -348,9 +347,9 @@ class Session $result = self::refresh(); // Try autologin. - if (!$member_srl && $autologin_key) + if (!$member_srl && self::$_autologin_key) { - $member_srl = getController('member')->doAutologin($autologin_key); + $member_srl = getController('member')->doAutologin(self::$_autologin_key); if ($member_srl) { $_SESSION['RHYMIX']['login'] = $_SESSION['member_srl'] = intval($member_srl); @@ -467,6 +466,7 @@ class Session } $_SESSION['RHYMIX']['login'] = $_SESSION['member_srl'] = $member_srl; + $_SESSION['RHYMIX']['last_login'] = time(); $_SESSION['is_logged'] = (bool)$member_srl; self::$_member_info = false; return self::refresh(); @@ -482,6 +482,7 @@ class Session public static function logout() { $_SESSION['RHYMIX']['login'] = $_SESSION['member_srl'] = false; + $_SESSION['RHYMIX']['last_login'] = false; $_SESSION['is_logged'] = false; self::$_member_info = false; return self::destroy(); @@ -864,6 +865,11 @@ class Session } else { + if (self::$_autologin_key) + { + executeQuery('member.deleteAutologin', (object)array('autologin_key' => substr(self::$_autologin_key, 0, 24))); + self::$_autologin_key = false; + } setcookie('rx_autologin', 'deleted', time() - 86400, $path, $domain, false, true); unset($_COOKIE['rx_autologin']); } diff --git a/modules/member/member.controller.php b/modules/member/member.controller.php index 991cc420b..1ec17d081 100644 --- a/modules/member/member.controller.php +++ b/modules/member/member.controller.php @@ -216,6 +216,10 @@ class memberController extends member $autologin_id = intval(Context::get('autologin_id')); $autologin_key = Context::get('autologin_key'); + if (!$autologin_id || !$autologin_key) + { + return new Object(-1, 'msg_invalid_request'); + } $args = new stdClass; $args->autologin_id = $autologin_id; @@ -2645,14 +2649,6 @@ class memberController extends member setcookie('sso','',$_SERVER['REQUEST_TIME']-42000, '/'); setcookie('xeak','',$_SERVER['REQUEST_TIME']-42000, '/'); setcookie('xe_logged', 'false', $_SERVER['REQUEST_TIME'] - 42000, '/'); - - if($memberSrl || $_COOKIE['xeak']) - { - $args = new stdClass(); - $args->member_srl = $memberSrl; - $args->autologin_key = $_COOKIE['xeak']; - $output = executeQuery('member.deleteAutologin', $args); - } } function _updatePointByGroup($memberSrl, $groupSrlList) diff --git a/modules/member/queries/deleteAutologin.xml b/modules/member/queries/deleteAutologin.xml index a26672edc..ac8ae821b 100644 --- a/modules/member/queries/deleteAutologin.xml +++ b/modules/member/queries/deleteAutologin.xml @@ -4,6 +4,6 @@ - + From c7d8d84500652355303072b2ba70206dc3468456 Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Thu, 9 Feb 2017 00:06:32 +0900 Subject: [PATCH 26/37] Add option to invalidate other sessions on password change Feature request in https://www.xetown.com/lakepark/345786 --- common/framework/session.php | 66 ++++++++++++++++++---- modules/member/lang/en.php | 2 + modules/member/lang/ko.php | 2 + modules/member/member.admin.controller.php | 1 + modules/member/member.controller.php | 31 ++++++++++ modules/member/queries/deleteAutologin.xml | 1 + modules/member/tpl/default_config.html | 8 +++ 7 files changed, 99 insertions(+), 12 deletions(-) diff --git a/common/framework/session.php b/common/framework/session.php index 44a36d1e7..0b93e299f 100644 --- a/common/framework/session.php +++ b/common/framework/session.php @@ -126,7 +126,7 @@ class Session // Hacked session! Destroy everything. $_SESSION = array(); $must_create = true; - self::setAutologinKeys(null, null); + self::destroyAutologinKeys(); } } else @@ -154,7 +154,7 @@ class Session // Hacked session! Destroy everything. $_SESSION = array(); $must_create = true; - self::setAutologinKeys(null, null); + self::destroyAutologinKeys(); } } @@ -353,11 +353,12 @@ class Session if ($member_srl) { $_SESSION['RHYMIX']['login'] = $_SESSION['member_srl'] = intval($member_srl); + $_SESSION['RHYMIX']['last_login'] = time(); $_SESSION['is_logged'] = true; } else { - self::setAutologinKeys(null, null); + self::destroyAutologinKeys(); } } @@ -440,7 +441,7 @@ class Session self::$_started = false; self::$_member_info = false; self::_setKeys(); - self::setAutologinKeys(null, null); + self::destroyAutologinKeys(); @session_destroy(); return true; } @@ -851,7 +852,7 @@ class Session * @param string $security_key * @return bool */ - public static function setAutologinKeys($autologin_key = null, $security_key = null) + public static function setAutologinKeys($autologin_key, $security_key) { // Get session parameters. list($lifetime, $refresh_interval, $domain, $path) = self::_getParams(); @@ -862,18 +863,59 @@ class Session { setcookie('rx_autologin', $autologin_key . $security_key, $lifetime, $path, $domain, false, true); $_COOKIE['rx_autologin'] = $autologin_key . $security_key; + return true; } else { - if (self::$_autologin_key) - { - executeQuery('member.deleteAutologin', (object)array('autologin_key' => substr(self::$_autologin_key, 0, 24))); - self::$_autologin_key = false; - } - setcookie('rx_autologin', 'deleted', time() - 86400, $path, $domain, false, true); - unset($_COOKIE['rx_autologin']); + return false; + } + } + + /** + * Destroy autologin keys. + * + * @return bool + */ + public static function destroyAutologinKeys() + { + if (self::$_autologin_key) + { + executeQuery('member.deleteAutologin', (object)array('autologin_key' => substr(self::$_autologin_key, 0, 24))); + self::$_autologin_key = false; + $result = true; + } + else + { + $result = false; } + setcookie('rx_autologin', 'deleted', time() - 86400, $path, $domain, false, true); + unset($_COOKIE['rx_autologin']); + return $result; + } + + /** + * Destroy all other autologin keys (except the current session). + * + * @param int $member_srl + * @return bool + */ + public static function destroyOtherAutologinKeys($member_srl) + { + $member_srl = intval($member_srl); + if (!$member_srl) + { + return false; + } + + if (self::$_autologin_key) + { + executeQuery('member.deleteAutologin', (object)array('member_srl' => $member_srl, 'not_autologin_key' => substr(self::$_autologin_key, 0, 24))); + } + else + { + executeQuery('member.deleteAutologin', (object)array('member_srl' => $member_srl)); + } return true; } } diff --git a/modules/member/lang/en.php b/modules/member/lang/en.php index 800144cf2..a13cd0041 100644 --- a/modules/member/lang/en.php +++ b/modules/member/lang/en.php @@ -161,6 +161,7 @@ $lang->cmd_config_password_strength = 'password strength'; $lang->cmd_password_hashing_algorithm = 'Password Hashing Algorithm'; $lang->cmd_password_hashing_work_factor = 'Password Hashing Work Factor'; $lang->cmd_password_hashing_auto_upgrade = 'Auto-upgrade Hashing Algorithm'; +$lang->cmd_password_change_invalidate_other_sessions = 'Log out other devices on password change'; $lang->password_strength_low = 'low'; $lang->password_strength_normal = 'normal'; $lang->password_strength_high = 'high'; @@ -168,6 +169,7 @@ $lang->about_password_strength_config = 'When members register or change the pas $lang->about_password_hashing_algorithm = 'You can choose how to encrypt (hash) members\' passwords stored in the database.'; $lang->about_password_hashing_work_factor = 'Higher work factors are more secure, but logins may take a long time. This only applies to bcrypt and pbkdf2.'; $lang->about_password_hashing_auto_upgrade = 'Passwords encrypted using different algorithms will be automatically converted to the configured algorithm at next login.'; +$lang->about_password_change_invalidate_other_sessions = 'Log out all other devices (browsers) when a member changes the password.'; $lang->about_password_strength['low'] = 'the password must be at least 4'; $lang->about_password_strength['normal'] = 'the password must be at least 6, and must have at least one alpha character and numeric characters'; $lang->about_password_strength['high'] = 'the password must be at least 8, and must have at least one alpha character, numeric character and special character '; diff --git a/modules/member/lang/ko.php b/modules/member/lang/ko.php index f20d1752f..a7b604339 100644 --- a/modules/member/lang/ko.php +++ b/modules/member/lang/ko.php @@ -170,6 +170,7 @@ $lang->cmd_config_password_strength = '비밀번호 보안수준'; $lang->cmd_password_hashing_algorithm = '비밀번호 암호화 알고리듬'; $lang->cmd_password_hashing_work_factor = '비밀번호 암호화 소요시간'; $lang->cmd_password_hashing_auto_upgrade = '알고리듬 자동 업그레이드'; +$lang->cmd_password_change_invalidate_other_sessions = '비번 변경시 다른 기기 로그아웃'; $lang->password_strength_low = '낮음'; $lang->password_strength_normal = '보통'; $lang->password_strength_high = '높음'; @@ -177,6 +178,7 @@ $lang->about_password_strength_config = '회원들이 비밀번호를 등록/변 $lang->about_password_hashing_algorithm = '회원들의 비밀번호를 DB에 저장할 때 암호화(해싱)하는 방식을 지정할 수 있습니다.'; $lang->about_password_hashing_work_factor = '시간이 오래 걸리는 알고리듬일수록 보안이 강하지만, 로그인이 오래 걸릴 수 있습니다. bcrypt 및 pbkdf2 알고리듬에만 적용됩니다.'; $lang->about_password_hashing_auto_upgrade = '설정된 알고리듬과 다른 방법으로 암호화된 비밀번호가 있으면 다음 로그인시 설정된 알고리듬으로 자동 변환합니다.'; +$lang->about_password_change_invalidate_other_sessions = '비밀번호를 변경하면 현재 기기(브라우저)를 제외한 모든 로그인이 풀리도록 합니다.'; $lang->about_password_strength['low'] = '비밀번호는 4자 이상이어야 합니다.'; $lang->about_password_strength['normal'] = '비밀번호는 6자리 이상이어야 하며 영문과 숫자를 반드시 포함해야 합니다.'; $lang->about_password_strength['high'] = '비밀번호는 8자리 이상이어야 하며 영문과 숫자, 특수문자를 반드시 포함해야 합니다.'; diff --git a/modules/member/member.admin.controller.php b/modules/member/member.admin.controller.php index 70ff7e575..e34edaa74 100644 --- a/modules/member/member.admin.controller.php +++ b/modules/member/member.admin.controller.php @@ -171,6 +171,7 @@ class memberAdminController extends member 'password_hashing_algorithm', 'password_hashing_work_factor', 'password_hashing_auto_upgrade', + 'password_change_invalidate_other_sessions', 'update_nickname_log', 'member_allow_fileupload' ); diff --git a/modules/member/member.controller.php b/modules/member/member.controller.php index 1ec17d081..d375844b1 100644 --- a/modules/member/member.controller.php +++ b/modules/member/member.controller.php @@ -735,6 +735,21 @@ class memberController extends member $args->password = $password; $output = $this->updateMemberPassword($args); if(!$output->toBool()) return $output; + + // Log out all other sessions. + $oModuleModel = getModel('module'); + $member_config = $oModuleModel->getModuleConfig('member'); + if ($member_config->password_change_invalidate_other_sessions === 'Y') + { + $invalid_before = time(); + $filename = RX_BASEDIR . sprintf('files/member_extra_info/invalid_before/%s%d.txt', getNumberingPath($member_srl), $member_srl); + Rhymix\Framework\Storage::write($filename, $invalid_before); + Rhymix\Framework\Session::destroyOtherAutologinKeys($member_srl); + if ($_SESSION['RHYMIX'] && $_SESSION['RHYMIX']['last_login']) + { + $_SESSION['RHYMIX']['last_login'] = $invalid_before; + } + } $this->add('member_srl', $args->member_srl); $this->setMessage('success_updated'); @@ -771,6 +786,7 @@ class memberController extends member $output = $this->deleteMember($member_srl); if(!$output->toBool()) return $output; // Destroy all session information + executeQuery('member.deleteAutologin', (object)array('member_srl' => $member_srl)); Rhymix\Framework\Session::logout(); // Return success message $this->setMessage('success_leaved'); @@ -1922,6 +1938,21 @@ class memberController extends member $this->destroySessionInfo(); return; } + + // Invalidate the session if the member's password has changed + $oModuleModel = getModel('module'); + $member_config = $oModuleModel->getModuleConfig('member'); + if ($member_config->password_change_invalidate_other_sessions === 'Y') + { + $filename = RX_BASEDIR . sprintf('files/member_extra_info/invalid_before/%s%d.txt', getNumberingPath($member_srl), $member_srl); + $invalid_before = Rhymix\Framework\Storage::read($filename); + if ($invalid_before && $_SESSION['RHYMIX'] && $_SESSION['RHYMIX']['last_login'] && $_SESSION['RHYMIX']['last_login'] < $invalid_before) + { + $this->destroySessionInfo(); + return; + } + } + // Log in for treatment sessions set /* $_SESSION['is_logged'] = true; diff --git a/modules/member/queries/deleteAutologin.xml b/modules/member/queries/deleteAutologin.xml index ac8ae821b..41aabfeec 100644 --- a/modules/member/queries/deleteAutologin.xml +++ b/modules/member/queries/deleteAutologin.xml @@ -4,6 +4,7 @@ + diff --git a/modules/member/tpl/default_config.html b/modules/member/tpl/default_config.html index f1f95be0a..a796c748e 100644 --- a/modules/member/tpl/default_config.html +++ b/modules/member/tpl/default_config.html @@ -71,6 +71,14 @@

{$lang->about_password_hashing_auto_upgrade}

+
+ +
+ + +

{$lang->about_password_change_invalidate_other_sessions}

+
+
From 3be0e79abb24ee84097ed65f70d8c7514f3be067 Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Thu, 9 Feb 2017 00:34:01 +0900 Subject: [PATCH 27/37] Improve session invalidation routines --- common/framework/session.php | 109 ++++++++++++++++++++++----- modules/member/member.controller.php | 23 +----- 2 files changed, 91 insertions(+), 41 deletions(-) diff --git a/common/framework/session.php b/common/framework/session.php index 0b93e299f..1740856db 100644 --- a/common/framework/session.php +++ b/common/framework/session.php @@ -168,6 +168,13 @@ class Session $must_refresh = true; } + // If a member is logged in, check if the current session is valid for the member_srl. + if (isset($_SESSION['RHYMIX']['login']) && $_SESSION['RHYMIX']['login'] && !self::isValid($_SESSION['RHYMIX']['login'])) + { + $_SESSION['RHYMIX']['login'] = $_SESSION['member_srl'] = false; + $must_create = true; + } + // Create or refresh the session if needed. if ($must_create) { @@ -327,34 +334,33 @@ class Session */ public static function create() { - // Ensure backward compatibility with XE session. - $member_srl = $_SESSION['member_srl'] ?: false; - $_SESSION['is_logged'] = (bool)$member_srl; - $_SESSION['is_admin'] = ''; - // Create the data structure for a new Rhymix session. $_SESSION['RHYMIX'] = array(); - $_SESSION['RHYMIX']['login'] = $_SESSION['member_srl'] = $member_srl; - $_SESSION['RHYMIX']['last_login'] = $member_srl ? time() : false; + $_SESSION['RHYMIX']['login'] = false; + $_SESSION['RHYMIX']['last_login'] = false; $_SESSION['RHYMIX']['ipaddress'] = $_SESSION['ipaddress'] = \RX_CLIENT_IP; $_SESSION['RHYMIX']['useragent'] = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : ''; $_SESSION['RHYMIX']['language'] = \Context::getLangType(); $_SESSION['RHYMIX']['timezone'] = DateTime::getTimezoneForCurrentUser(); $_SESSION['RHYMIX']['secret'] = Security::getRandom(32, 'alnum'); $_SESSION['RHYMIX']['tokens'] = array(); + $_SESSION['is_logged'] = false; + $_SESSION['is_admin'] = ''; - // Pass control to refresh() to generate security keys. - $result = self::refresh(); + // Ensure backward compatibility with XE session. + $member_srl = isset($_SESSION['member_srl']) ? ($_SESSION['member_srl'] ?: false) : false; + if ($member_srl && self::isValid($member_srl)) + { + self::login($member_srl, false); + } // Try autologin. - if (!$member_srl && self::$_autologin_key) + elseif (self::$_autologin_key) { $member_srl = getController('member')->doAutologin(self::$_autologin_key); - if ($member_srl) + if ($member_srl && self::isValid($member_srl)) { - $_SESSION['RHYMIX']['login'] = $_SESSION['member_srl'] = intval($member_srl); - $_SESSION['RHYMIX']['last_login'] = time(); - $_SESSION['is_logged'] = true; + self::login($member_srl, false); } else { @@ -362,8 +368,8 @@ class Session } } - // Return the result obtained above. - return $result; + // Pass control to refresh() to generate security keys. + return self::refresh(); } /** @@ -453,10 +459,12 @@ class Session * It returns true on success and false on failure. * * @param int $member_srl + * @param bool $refresh (optional) * @return bool */ - public static function login($member_srl) + public static function login($member_srl, $refresh = true) { + // Check the validity of member_srl. if (is_object($member_srl) && isset($member_srl->member_srl)) { $member_srl = $member_srl->member_srl; @@ -466,11 +474,21 @@ class Session return false; } + // Set member_srl to session. $_SESSION['RHYMIX']['login'] = $_SESSION['member_srl'] = $member_srl; $_SESSION['RHYMIX']['last_login'] = time(); $_SESSION['is_logged'] = (bool)$member_srl; self::$_member_info = false; - return self::refresh(); + + // Refresh the session keys. + if ($refresh) + { + return self::refresh(); + } + else + { + return true; + } } /** @@ -549,6 +567,42 @@ class Session } } + /** + * Check if the current session is valid for a given member_srl. + * + * The session can be invalidated by password changes and other user action. + * + * @param int $member_srl (optional) + * @return bool + */ + public static function isValid($member_srl = null) + { + // If no member_srl is given, the session is always valid. + $member_srl = intval($member_srl) ?: (isset($_SESSION['RHYMIX']['login']) ? $_SESSION['RHYMIX']['login'] : 0); + if (!$member_srl) + { + return true; + } + + // Get the invalidation timestamp. + $invalid_before = Cache::get(sprintf('session:invalid_before:%d', $member_srl)); + if (!$invalid_before) + { + $filename = \RX_BASEDIR . sprintf('files/member_extra_info/invalid_before/%s%d.txt', getNumberingPath($member_srl), $member_srl); + $invalid_before = intval(Storage::read($filename, $invalid_before)); + } + + // Check the invalidation timestamp against the current session. + if ($invalid_before && self::isStarted() && $_SESSION['RHYMIX']['last_login'] && $_SESSION['RHYMIX']['last_login'] < $invalid_before) + { + return false; + } + else + { + return true; + } + } + /** * Get the member_srl of the currently logged in member. * @@ -900,14 +954,30 @@ class Session * @param int $member_srl * @return bool */ - public static function destroyOtherAutologinKeys($member_srl) + public static function destroyOtherSessions($member_srl) { + // Check the validity of member_srl. $member_srl = intval($member_srl); if (!$member_srl) { return false; } + // Invalidate all sessions that were logged in before the current timestamp. + if (self::isStarted()) + { + $invalid_before = time(); + $filename = \RX_BASEDIR . sprintf('files/member_extra_info/invalid_before/%s%d.txt', getNumberingPath($member_srl), $member_srl); + Storage::write($filename, $invalid_before); + Cache::set(sprintf('session:invalid_before:%d', $member_srl), $invalid_before); + $_SESSION['RHYMIX']['last_login'] = $invalid_before; + } + else + { + return false; + } + + // Destroy all other autologin keys. if (self::$_autologin_key) { executeQuery('member.deleteAutologin', (object)array('member_srl' => $member_srl, 'not_autologin_key' => substr(self::$_autologin_key, 0, 24))); @@ -916,6 +986,7 @@ class Session { executeQuery('member.deleteAutologin', (object)array('member_srl' => $member_srl)); } + return true; } } diff --git a/modules/member/member.controller.php b/modules/member/member.controller.php index d375844b1..3f3881d1a 100644 --- a/modules/member/member.controller.php +++ b/modules/member/member.controller.php @@ -741,14 +741,7 @@ class memberController extends member $member_config = $oModuleModel->getModuleConfig('member'); if ($member_config->password_change_invalidate_other_sessions === 'Y') { - $invalid_before = time(); - $filename = RX_BASEDIR . sprintf('files/member_extra_info/invalid_before/%s%d.txt', getNumberingPath($member_srl), $member_srl); - Rhymix\Framework\Storage::write($filename, $invalid_before); - Rhymix\Framework\Session::destroyOtherAutologinKeys($member_srl); - if ($_SESSION['RHYMIX'] && $_SESSION['RHYMIX']['last_login']) - { - $_SESSION['RHYMIX']['last_login'] = $invalid_before; - } + Rhymix\Framework\Session::destroyOtherSessions($member_srl); } $this->add('member_srl', $args->member_srl); @@ -1939,20 +1932,6 @@ class memberController extends member return; } - // Invalidate the session if the member's password has changed - $oModuleModel = getModel('module'); - $member_config = $oModuleModel->getModuleConfig('member'); - if ($member_config->password_change_invalidate_other_sessions === 'Y') - { - $filename = RX_BASEDIR . sprintf('files/member_extra_info/invalid_before/%s%d.txt', getNumberingPath($member_srl), $member_srl); - $invalid_before = Rhymix\Framework\Storage::read($filename); - if ($invalid_before && $_SESSION['RHYMIX'] && $_SESSION['RHYMIX']['last_login'] && $_SESSION['RHYMIX']['last_login'] < $invalid_before) - { - $this->destroySessionInfo(); - return; - } - } - // Log in for treatment sessions set /* $_SESSION['is_logged'] = true; From 4b26db99328594f7314091c84f7dc5572eac46d7 Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Thu, 9 Feb 2017 00:42:34 +0900 Subject: [PATCH 28/37] Fix unit tests, and add tests for Session::isValid() --- common/framework/session.php | 6 +++++- tests/unit/framework/SessionTest.php | 28 ++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/common/framework/session.php b/common/framework/session.php index 1740856db..de67c1999 100644 --- a/common/framework/session.php +++ b/common/framework/session.php @@ -353,9 +353,13 @@ class Session { self::login($member_srl, false); } + else + { + $_SESSION['member_srl'] = false; + } // Try autologin. - elseif (self::$_autologin_key) + if (!$member_srl && self::$_autologin_key) { $member_srl = getController('member')->doAutologin(self::$_autologin_key); if ($member_srl && self::isValid($member_srl)) diff --git a/tests/unit/framework/SessionTest.php b/tests/unit/framework/SessionTest.php index 5ca61074c..806da9ee3 100644 --- a/tests/unit/framework/SessionTest.php +++ b/tests/unit/framework/SessionTest.php @@ -238,6 +238,34 @@ class SessionTest extends \Codeception\TestCase\Test Rhymix\Framework\Session::close(); } + public function testIsValid() + { + Rhymix\Framework\Session::start(); + + $member_srl = 4; + Rhymix\Framework\Session::login($member_srl); + + $invalid_before = time() - 300; + $filename = \RX_BASEDIR . sprintf('files/member_extra_info/invalid_before/%s%d.txt', getNumberingPath($member_srl), $member_srl); + Rhymix\Framework\Storage::write($filename, $invalid_before); + Rhymix\Framework\Cache::set(sprintf('session:invalid_before:%d', $member_srl), $invalid_before); + $this->assertTrue(Rhymix\Framework\Session::isValid()); + + $invalid_before = time() + 300; + $filename = \RX_BASEDIR . sprintf('files/member_extra_info/invalid_before/%s%d.txt', getNumberingPath($member_srl), $member_srl); + Rhymix\Framework\Storage::write($filename, $invalid_before); + Rhymix\Framework\Cache::set(sprintf('session:invalid_before:%d', $member_srl), $invalid_before); + $this->assertFalse(Rhymix\Framework\Session::isValid()); + + $invalid_before = time(); + $filename = \RX_BASEDIR . sprintf('files/member_extra_info/invalid_before/%s%d.txt', getNumberingPath($member_srl), $member_srl); + Rhymix\Framework\Storage::write($filename, $invalid_before); + Rhymix\Framework\Cache::set(sprintf('session:invalid_before:%d', $member_srl), $invalid_before); + $this->assertTrue(Rhymix\Framework\Session::isValid()); + + Rhymix\Framework\Session::close(); + } + public function testGetMemberSrl() { Rhymix\Framework\Session::start(); From eea7518e98458c57fda180fcae0851ae98cd674b Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Fri, 10 Feb 2017 19:28:26 +0900 Subject: [PATCH 29/37] Add auto_login module to blacklist --- common/defaults/blacklist.php | 1 + modules/admin/lang/en.php | 1 + modules/admin/lang/ko.php | 1 + 3 files changed, 3 insertions(+) diff --git a/common/defaults/blacklist.php b/common/defaults/blacklist.php index 774fca774..b3add3eaf 100644 --- a/common/defaults/blacklist.php +++ b/common/defaults/blacklist.php @@ -7,6 +7,7 @@ */ return array( 'autolang' => true, + 'auto_login' => true, 'errorlogger' => true, 'fix_mysql_utf8' => true, 'member_communication' => true, diff --git a/modules/admin/lang/en.php b/modules/admin/lang/en.php index 4881a3ea0..3583880b2 100644 --- a/modules/admin/lang/en.php +++ b/modules/admin/lang/en.php @@ -67,6 +67,7 @@ $lang->msg_blacklisted_addon = 'This addon has been disabled because it conflict $lang->msg_blacklisted_widget = 'This widget has been disabled because it conflicts with a feature that Rhymix supports by default, or is known to have other compatibility problems.'; $lang->msg_blacklisted_layout = 'This layout has been disabled because it conflicts with a feature that Rhymix supports by default, or is known to have other compatibility problems.'; $lang->msg_blacklisted_reason['autolang'] = 'Similar functionality can be configured in the System Settings page.'; +$lang->msg_blacklisted_reason['auto_login'] = 'The functionality that this addon used to provide is included by default in Rhymix.'; $lang->msg_blacklisted_reason['errorlogger'] = 'Similar functionality can be configured in the Debug Settings page.'; $lang->msg_blacklisted_reason['fix_mysql_utf8'] = 'The functionality that this addon used to provide is included by default in Rhymix.'; $lang->msg_blacklisted_reason['member_communication'] = 'The functionality that this addon used to provide has been moved to the member and ncenterlite modules.'; diff --git a/modules/admin/lang/ko.php b/modules/admin/lang/ko.php index bd32b9edf..517a30cb3 100644 --- a/modules/admin/lang/ko.php +++ b/modules/admin/lang/ko.php @@ -67,6 +67,7 @@ $lang->msg_blacklisted_addon = '이 애드온은 Rhymix에서 기본 제공하 $lang->msg_blacklisted_widget = '이 위젯은 Rhymix에서 기본 제공하는 기능과 충돌하거나 그 밖의 호환성 문제가 있으므로 사용이 중단되었습니다.'; $lang->msg_blacklisted_layout = '이 레이아웃은 Rhymix에서 기본 제공하는 기능과 충돌하거나 그 밖의 호환성 문제가 있으므로 사용이 중단되었습니다.'; $lang->msg_blacklisted_reason['autolang'] = '이 애드온에서 제공하던 기능은 시스템 설정 페이지에서 관리할 수 있습니다.'; +$lang->msg_blacklisted_reason['auto_login'] = '이 애드온에서 제공하던 기능은 Rhymix에 포함되어 있습니다.'; $lang->msg_blacklisted_reason['errorlogger'] = '이 모듈에서 제공하던 기능은 디버그 설정 페이지에서 관리할 수 있습니다.'; $lang->msg_blacklisted_reason['fix_mysql_utf8'] = '이 애드온에서 제공하던 기능은 Rhymix에 포함되어 있습니다.'; $lang->msg_blacklisted_reason['member_communication'] = '이 애드온에서 제공하던 기능은 알림센터 모듈에서 관리할 수 있습니다.'; From 14d464da225869135f5a6802f0365c5bc075ab85 Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Fri, 10 Feb 2017 20:02:21 +0900 Subject: [PATCH 30/37] Remove unnecessary xe_logged cookie --- modules/member/member.controller.php | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/member/member.controller.php b/modules/member/member.controller.php index 3f3881d1a..b1d375380 100644 --- a/modules/member/member.controller.php +++ b/modules/member/member.controller.php @@ -1955,7 +1955,6 @@ class memberController extends member // Information stored in the session login user Context::set('is_logged', true); Context::set('logged_info', $this->memberInfo); - setcookie('xe_logged', 'true', 0, '/'); // Only the menu configuration of the user (such as an add-on to the menu can be changed) $this->addMemberMenu( 'dispMemberInfo', 'cmd_view_member_info'); From dfdbc1db85946c7e87576d92ea961f6576661e5d Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Fri, 10 Feb 2017 20:50:38 +0900 Subject: [PATCH 31/37] Add session helper class and move remainder of session validation logic to Session class --- classes/context/Context.class.php | 16 +--- common/framework/helpers/sessionhelper.php | 101 +++++++++++++++++++++ common/framework/session.php | 27 +++--- modules/member/member.controller.php | 17 +--- modules/member/member.model.php | 41 ++------- 5 files changed, 130 insertions(+), 72 deletions(-) create mode 100644 common/framework/helpers/sessionhelper.php diff --git a/classes/context/Context.class.php b/classes/context/Context.class.php index 449bf7edc..5a5b13820 100644 --- a/classes/context/Context.class.php +++ b/classes/context/Context.class.php @@ -347,24 +347,14 @@ class Context ob_start(); // set authentication information in Context and session - if(self::isInstalled()) + if (self::isInstalled()) { $oModuleModel = getModel('module'); $oModuleModel->loadModuleExtends(); - $oMemberModel = getModel('member'); - $oMemberController = getController('member'); - - if($oMemberController && $oMemberModel) + if (Rhymix\Framework\Session::getMemberSrl()) { - // if signed in, validate it. - if(Rhymix\Framework\Session::getMemberSrl()) - { - $oMemberController->setSessionInfo(); - } - - self::set('is_logged', $oMemberModel->isLogged()); - self::set('logged_info', $oMemberModel->getLoggedInfo()); + getController('member')->setSessionInfo(); } } diff --git a/common/framework/helpers/sessionhelper.php b/common/framework/helpers/sessionhelper.php new file mode 100644 index 000000000..901c2f59d --- /dev/null +++ b/common/framework/helpers/sessionhelper.php @@ -0,0 +1,101 @@ +getMemberInfoByMemberSrl($member_srl); + if (intval($member_info->member_srl) === $member_srl) + { + foreach (get_object_vars($member_info) as $key => $value) + { + $this->{$key} = $value; + } + $this->member_srl = $member_srl; + $this->group_list = $oMemberModel->getMemberGroups($member_srl); + } + } + } + + /** + * Check if this user is a member. + * + * @return bool + */ + public static function isMember() + { + return $this->member_srl > 0; + } + + /** + * Check if this user is an administrator. + * + * @return bool + */ + public static function isAdmin() + { + return $this->is_admin === 'Y'; + } + + /** + * Check if this user is an administrator of a module. + * + * @param int $module_srl (optional) + * @return bool + */ + public static function isModuleAdmin($module_srl = null) + { + return $this->is_admin === 'Y' || getModel('module')->isModuleAdmin($this, $module_srl); + } + + /** + * Check if this user is valid (not denied or limited). + * + * @return bool + */ + public static function isValid() + { + if ($this->denied === 'N' && (!$this->limit_date || substr($this->limit_date, 0, 8) < date('Ymd'))) + { + return true; + } + else + { + return false; + } + } + + /** + * Get the list of groups that this user belongs to. + * + * @return array + */ + public static function getGroups() + { + return $this->group_list; + } +} diff --git a/common/framework/session.php b/common/framework/session.php index de67c1999..e52df455b 100644 --- a/common/framework/session.php +++ b/common/framework/session.php @@ -585,7 +585,7 @@ class Session $member_srl = intval($member_srl) ?: (isset($_SESSION['RHYMIX']['login']) ? $_SESSION['RHYMIX']['login'] : 0); if (!$member_srl) { - return true; + return false; } // Get the invalidation timestamp. @@ -601,10 +601,20 @@ class Session { return false; } - else + + // Check member information to see if denied or limited. + $member_info = getModel('member')->getMemberInfoByMemberSrl($member_srl); + if ($member_info->denied === 'Y') { - return true; + return false; } + if ($member_info->limit_date && substr($member_info->limit_date, 0, 8) >= date('Ymd')) + { + return false; + } + + // Return true if all checks have passed. + return true; } /** @@ -638,18 +648,11 @@ class Session // Create a member info object. if (!self::$_member_info || self::$_member_info->member_srl != $member_srl) { - self::$_member_info = getModel('member')->getMemberInfoByMemberSrl($member_srl); + self::$_member_info = new Helpers\SessionHelper($member_srl); } // Return the member info object. - if (self::$_member_info == new \stdClass) - { - return false; - } - else - { - return self::$_member_info; - } + return self::$_member_info->member_srl ? self::$_member_info : false; } /** diff --git a/modules/member/member.controller.php b/modules/member/member.controller.php index b1d375380..85eba2d78 100644 --- a/modules/member/member.controller.php +++ b/modules/member/member.controller.php @@ -1914,22 +1914,11 @@ class memberController extends member { $oMemberModel = getModel('member'); $config = $oMemberModel->getMemberConfig(); + // If your information came through the current session information to extract information from the users - if(!$this->memberInfo && $member_srl = Rhymix\Framework\Session::getMemberSrl()) + if(!$this->memberInfo && Rhymix\Framework\Session::getMemberSrl()) { - $this->memberInfo = $oMemberModel->getMemberInfoByMemberSrl($member_srl); - // If you do not destroy the session Profile - if($this->memberInfo->member_srl != $member_srl) - { - $this->destroySessionInfo(); - return; - } - } - // Stop using the session id is destroyed - if($this->memberInfo->denied=='Y') - { - $this->destroySessionInfo(); - return; + $this->memberInfo = Rhymix\Framework\Session::getMemberInfo(); } // Log in for treatment sessions set diff --git a/modules/member/member.model.php b/modules/member/member.model.php index 7e470a087..f29729411 100644 --- a/modules/member/member.model.php +++ b/modules/member/member.model.php @@ -241,39 +241,7 @@ class memberModel extends member */ function getLoggedInfo() { - // Return session info if session info is requested and the user is logged-in - if(Rhymix\Framework\Session::getMemberSrl()) - { - $logged_info = Context::get('logged_info'); - // Admin/Group list defined depending on site_module_info - $site_module_info = Context::get('site_module_info'); - if($site_module_info->site_srl) - { - $logged_info->group_list = $this->getMemberGroups($logged_info->member_srl, $site_module_info->site_srl); - // Add is_site_admin bool variable into logged_info if site_administrator is - $oModuleModel = getModel('module'); - if($oModuleModel->isSiteAdmin($logged_info)) $logged_info->is_site_admin = true; - else $logged_info->is_site_admin = false; - } - else - { - // Register a default group if the site doesn't have a member group - if(count($logged_info->group_list) === 0) - { - $default_group = $this->getDefaultGroup(0); - $oMemberController = getController('member'); - $oMemberController->addMemberToGroup($logged_info->member_srl, $default_group->group_srl, 0); - $groups[$default_group->group_srl] = $default_group->title; - $logged_info->group_list = $groups; - } - - $logged_info->is_site_admin = false; - } - Context::set('logged_info', $logged_info); - - return $logged_info; - } - return new stdClass; + return Context::get('logged_info'); } /** @@ -371,6 +339,7 @@ class memberModel extends member } $info->signature = $this->getSignature($info->member_srl); $info->group_list = $this->getMemberGroups($info->member_srl, $site_srl); + $info->is_site_admin = $oModuleModel->isSiteAdmin($info) ? true : false; $extra_vars = unserialize($info->extra_vars); unset($info->extra_vars); @@ -499,6 +468,12 @@ class memberModel extends member $args->site_srl = $site_srl; $output = executeQueryArray('member.getMemberGroups', $args); $group_list = $output->data; + if (!count($group_list)) + { + $default_group = $this->getDefaultGroup($site_srl); + getController('member')->addMemberToGroup($member_srl, $default_group->group_srl, $site_srl); + $group_list[$default_group->group_srl] = $default_group->title; + } //insert in cache Rhymix\Framework\Cache::set($cache_key, $group_list, 0, true); } From 980f40072b7ec9318fcef24053a08ddef0edaae9 Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Fri, 10 Feb 2017 20:51:15 +0900 Subject: [PATCH 32/37] Fix documentation on auto_login module blacklist: --- modules/admin/lang/en.php | 2 +- modules/admin/lang/ko.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/admin/lang/en.php b/modules/admin/lang/en.php index 3583880b2..e57508c64 100644 --- a/modules/admin/lang/en.php +++ b/modules/admin/lang/en.php @@ -67,7 +67,7 @@ $lang->msg_blacklisted_addon = 'This addon has been disabled because it conflict $lang->msg_blacklisted_widget = 'This widget has been disabled because it conflicts with a feature that Rhymix supports by default, or is known to have other compatibility problems.'; $lang->msg_blacklisted_layout = 'This layout has been disabled because it conflicts with a feature that Rhymix supports by default, or is known to have other compatibility problems.'; $lang->msg_blacklisted_reason['autolang'] = 'Similar functionality can be configured in the System Settings page.'; -$lang->msg_blacklisted_reason['auto_login'] = 'The functionality that this addon used to provide is included by default in Rhymix.'; +$lang->msg_blacklisted_reason['auto_login'] = 'The functionality that this module used to provide is included by default in Rhymix.'; $lang->msg_blacklisted_reason['errorlogger'] = 'Similar functionality can be configured in the Debug Settings page.'; $lang->msg_blacklisted_reason['fix_mysql_utf8'] = 'The functionality that this addon used to provide is included by default in Rhymix.'; $lang->msg_blacklisted_reason['member_communication'] = 'The functionality that this addon used to provide has been moved to the member and ncenterlite modules.'; diff --git a/modules/admin/lang/ko.php b/modules/admin/lang/ko.php index 517a30cb3..c90915f14 100644 --- a/modules/admin/lang/ko.php +++ b/modules/admin/lang/ko.php @@ -67,7 +67,7 @@ $lang->msg_blacklisted_addon = '이 애드온은 Rhymix에서 기본 제공하 $lang->msg_blacklisted_widget = '이 위젯은 Rhymix에서 기본 제공하는 기능과 충돌하거나 그 밖의 호환성 문제가 있으므로 사용이 중단되었습니다.'; $lang->msg_blacklisted_layout = '이 레이아웃은 Rhymix에서 기본 제공하는 기능과 충돌하거나 그 밖의 호환성 문제가 있으므로 사용이 중단되었습니다.'; $lang->msg_blacklisted_reason['autolang'] = '이 애드온에서 제공하던 기능은 시스템 설정 페이지에서 관리할 수 있습니다.'; -$lang->msg_blacklisted_reason['auto_login'] = '이 애드온에서 제공하던 기능은 Rhymix에 포함되어 있습니다.'; +$lang->msg_blacklisted_reason['auto_login'] = '이 모듈에서 제공하던 기능은 Rhymix에 포함되어 있습니다.'; $lang->msg_blacklisted_reason['errorlogger'] = '이 모듈에서 제공하던 기능은 디버그 설정 페이지에서 관리할 수 있습니다.'; $lang->msg_blacklisted_reason['fix_mysql_utf8'] = '이 애드온에서 제공하던 기능은 Rhymix에 포함되어 있습니다.'; $lang->msg_blacklisted_reason['member_communication'] = '이 애드온에서 제공하던 기능은 알림센터 모듈에서 관리할 수 있습니다.'; From 57ab94187e19bfa09fd3dfc0b8c6e0abce773b9a Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Fri, 10 Feb 2017 20:53:38 +0900 Subject: [PATCH 33/37] Remove static keyword --- common/framework/helpers/sessionhelper.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/common/framework/helpers/sessionhelper.php b/common/framework/helpers/sessionhelper.php index 901c2f59d..7e38b1cfe 100644 --- a/common/framework/helpers/sessionhelper.php +++ b/common/framework/helpers/sessionhelper.php @@ -46,7 +46,7 @@ class SessionHelper * * @return bool */ - public static function isMember() + public function isMember() { return $this->member_srl > 0; } @@ -56,7 +56,7 @@ class SessionHelper * * @return bool */ - public static function isAdmin() + public function isAdmin() { return $this->is_admin === 'Y'; } @@ -67,7 +67,7 @@ class SessionHelper * @param int $module_srl (optional) * @return bool */ - public static function isModuleAdmin($module_srl = null) + public function isModuleAdmin($module_srl = null) { return $this->is_admin === 'Y' || getModel('module')->isModuleAdmin($this, $module_srl); } @@ -77,7 +77,7 @@ class SessionHelper * * @return bool */ - public static function isValid() + public function isValid() { if ($this->denied === 'N' && (!$this->limit_date || substr($this->limit_date, 0, 8) < date('Ymd'))) { @@ -94,7 +94,7 @@ class SessionHelper * * @return array */ - public static function getGroups() + public function getGroups() { return $this->group_list; } From 5f7b4837dc57fe6c5ba4e1ddbb4144b564a4ff50 Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Fri, 10 Feb 2017 20:59:17 +0900 Subject: [PATCH 34/37] Add more safeguards for the possibility of an empty member_srl --- common/framework/helpers/sessionhelper.php | 4 ++-- modules/member/member.controller.php | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/common/framework/helpers/sessionhelper.php b/common/framework/helpers/sessionhelper.php index 7e38b1cfe..0216189a9 100644 --- a/common/framework/helpers/sessionhelper.php +++ b/common/framework/helpers/sessionhelper.php @@ -18,10 +18,10 @@ class SessionHelper /** * Constructor. * - * @param int $member_srl + * @param int $member_srl (optional) * @return void */ - public function __construct($member_srl) + public function __construct($member_srl = null) { // Load member information. $member_srl = intval($member_srl); diff --git a/modules/member/member.controller.php b/modules/member/member.controller.php index 85eba2d78..b20d1f6a4 100644 --- a/modules/member/member.controller.php +++ b/modules/member/member.controller.php @@ -1920,6 +1920,10 @@ class memberController extends member { $this->memberInfo = Rhymix\Framework\Session::getMemberInfo(); } + if(!$this->memberInfo->member_srl) + { + return; + } // Log in for treatment sessions set /* From 45e930f04ca8b28b01258a27c02ac28dcdf9b1f9 Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Fri, 10 Feb 2017 21:08:05 +0900 Subject: [PATCH 35/37] Populate 'user' property of every module instance with current user info --- classes/module/ModuleHandler.class.php | 6 ++++++ classes/module/ModuleObject.class.php | 14 ++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/classes/module/ModuleHandler.class.php b/classes/module/ModuleHandler.class.php index 23671c859..5b51c3509 100644 --- a/classes/module/ModuleHandler.class.php +++ b/classes/module/ModuleHandler.class.php @@ -1178,6 +1178,12 @@ class ModuleHandler extends Handler { return NULL; } + + // Populate default properties + if($oModule->user === false) + { + $oModule->user = Context::get('logged_info'); + } // Load language files for the class if($module !== 'module') diff --git a/classes/module/ModuleObject.class.php b/classes/module/ModuleObject.class.php index 08cefd9f9..32029c566 100644 --- a/classes/module/ModuleObject.class.php +++ b/classes/module/ModuleObject.class.php @@ -26,6 +26,20 @@ class ModuleObject extends Object var $module_config = NULL; var $ajaxRequestMethod = array('XMLRPC', 'JSON'); var $gzhandler_enable = TRUE; + var $user = FALSE; + + /** + * Constructor + * + * @param int $error Error code + * @param string $message Error message + * @return void + */ + function __construct($error = 0, $message = 'success') + { + $this->user = Context::get('logged_info'); + parent::__construct($error, $message); + } /** * setter to set the name of module From 11883fb965229dedeb6ceb7f7b1d6cab468d4291 Mon Sep 17 00:00:00 2001 From: Kijin Sung Date: Fri, 10 Feb 2017 21:18:53 +0900 Subject: [PATCH 36/37] Populate 'user' property of TemplateHandler instances --- classes/template/TemplateHandler.class.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/classes/template/TemplateHandler.class.php b/classes/template/TemplateHandler.class.php index cf0f2121f..981136add 100644 --- a/classes/template/TemplateHandler.class.php +++ b/classes/template/TemplateHandler.class.php @@ -20,6 +20,11 @@ class TemplateHandler private $skipTags = NULL; private $handler_mtime = 0; private static $rootTpl = NULL; + + /** + * Context variables accessible as $this in template files + */ + public $user = FALSE; /** * constructor @@ -29,6 +34,7 @@ class TemplateHandler { $this->config = new stdClass; $this->handler_mtime = filemtime(__FILE__); + $this->user = Context::get('logged_info'); } /** @@ -818,7 +824,7 @@ class TemplateHandler } return preg_replace_callback('@(? Date: Fri, 10 Feb 2017 21:26:38 +0900 Subject: [PATCH 37/37] Populate an empty SessionHelper object if not logged in --- classes/module/ModuleHandler.class.php | 2 +- classes/module/ModuleObject.class.php | 2 +- classes/template/TemplateHandler.class.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/classes/module/ModuleHandler.class.php b/classes/module/ModuleHandler.class.php index 5b51c3509..767c0823a 100644 --- a/classes/module/ModuleHandler.class.php +++ b/classes/module/ModuleHandler.class.php @@ -1182,7 +1182,7 @@ class ModuleHandler extends Handler // Populate default properties if($oModule->user === false) { - $oModule->user = Context::get('logged_info'); + $oModule->user = Context::get('logged_info') ?: new Rhymix\Framework\Helpers\SessionHelper; } // Load language files for the class diff --git a/classes/module/ModuleObject.class.php b/classes/module/ModuleObject.class.php index 32029c566..074b06624 100644 --- a/classes/module/ModuleObject.class.php +++ b/classes/module/ModuleObject.class.php @@ -37,7 +37,7 @@ class ModuleObject extends Object */ function __construct($error = 0, $message = 'success') { - $this->user = Context::get('logged_info'); + $this->user = Context::get('logged_info') ?: new Rhymix\Framework\Helpers\SessionHelper; parent::__construct($error, $message); } diff --git a/classes/template/TemplateHandler.class.php b/classes/template/TemplateHandler.class.php index 981136add..1dd8364cf 100644 --- a/classes/template/TemplateHandler.class.php +++ b/classes/template/TemplateHandler.class.php @@ -34,7 +34,7 @@ class TemplateHandler { $this->config = new stdClass; $this->handler_mtime = filemtime(__FILE__); - $this->user = Context::get('logged_info'); + $this->user = Context::get('logged_info') ?: new Rhymix\Framework\Helpers\SessionHelper; } /**