Classes

File phpboost/user/session/SessionData.class.php

File phpboost/user/session/SessionData.class.php

  1:   2:   3:   4:   5:   6:   7:   8:   9:  10:  11:  12:  13:  14:  15:  16:  17:  18:  19:  20:  21:  22:  23:  24:  25:  26:  27:  28:  29:  30:  31:  32:  33:  34:  35:  36:  37:  38:  39:  40:  41:  42:  43:  44:  45:  46:  47:  48:  49:  50:  51:  52:  53:  54:  55:  56:  57:  58:  59:  60:  61:  62:  63:  64:  65:  66:  67:  68:  69:  70:  71:  72:  73:  74:  75:  76:  77:  78:  79:  80:  81:  82:  83:  84:  85:  86:  87:  88:  89:  90:  91:  92:  93:  94:  95:  96:  97:  98:  99: 100: 101: 102: 103: 104: 105: 106: 107: 108: 109: 110: 111: 112: 113: 114: 115: 116: 117: 118: 119: 120: 121: 122: 123: 124: 125: 126: 127: 128: 129: 130: 131: 132: 133: 134: 135: 136: 137: 138: 139: 140: 141: 142: 143: 144: 145: 146: 147: 148: 149: 150: 151: 152: 153: 154: 155: 156: 157: 158: 159: 160: 161: 162: 163: 164: 165: 166: 167: 168: 169: 170: 171: 172: 173: 174: 175: 176: 177: 178: 179: 180: 181: 182: 183: 184: 185: 186: 187: 188: 189: 190: 191: 192: 193: 194: 195: 196: 197: 198: 199: 200: 201: 202: 203: 204: 205: 206: 207: 208: 209: 210: 211: 212: 213: 214: 215: 216: 217: 218: 219: 220: 221: 222: 223: 224: 225: 226: 227: 228: 229: 230: 231: 232: 233: 234: 235: 236: 237: 238: 239: 240: 241: 242: 243: 244: 245: 246: 247: 248: 249: 250: 251: 252: 253: 254: 255: 256: 257: 258: 259: 260: 261: 262: 263: 264: 265: 266: 267: 268: 269: 270: 271: 272: 273: 274: 275: 276: 277: 278: 279: 280: 281: 282: 283: 284: 285: 286: 287: 288: 289: 290: 291: 292: 293: 294: 295: 296: 297: 298: 299: 300: 301: 302: 303: 304: 305: 306: 307: 308: 309: 310: 311: 312: 313: 314: 315: 316: 317: 318: 319: 320: 321: 322: 323: 324: 325: 326: 327: 328: 329: 330: 331: 332: 333: 334: 335: 336: 337: 338: 339: 340: 341: 342: 343: 344: 345: 346: 347: 348: 349: 350: 351: 352: 353: 354: 355: 356: 357: 358: 359: 360: 361: 362: 363: 364: 365: 366: 367: 368: 369: 370: 371: 372: 373: 374: 375: 376: 377: 378: 379: 380: 381: 382: 383: 384: 385: 386: 387: 388: 389: 390: 391: 392: 393: 394: 395: 396: 397: 398: 399: 400: 401: 402: 403: 404: 405: 406: 407: 408: 409: 410: 411: 412: 413: 414: 415: 416: 417: 418: 419: 420: 421: 422: 423: 424: 425: 426: 427: 428: 429: 430: 431: 432: 433: 434: 435: 436: 437: 438: 439: 440: 441: 442: 443: 444: 445: 446: 447: 448: 449: 450: 451: 452: 453: 454: 455: 456: 457: 458: 459: 460: 461: 462: 463: 464: 465: 466: 467: 468: 469: 470: 471: 472: 473: 474: 475: 476: 477: 478: 479: 480: 481: 482: 483: 484: 485: 486: 487: 488: 489: 490: 491: 492: 493: 494: 495: 496: 497: 498: 499: 500: 501: 502: 503: 504: 505: 506: 507: 508: 509: 510: 511: 512: 513: 514: 515: 516: 
<?php
/**
 * This class manages all sessions for the users.
 * @package     PHPBoost
 * @subpackage  User\session
 * @copyright   &copy; 2005-2019 PHPBoost
 * @license     https://www.gnu.org/licenses/gpl-3.0.html GNU/GPL-3.0
 * @author      Loic ROUCHON <horn@phpboost.com>
 * @version     PHPBoost 5.2 - last update: 2019 02 28
 * @since       PHPBoost 3.0 - 2010 11 04
 * @contributor Kevin MASSY <reidlos@phpboost.com>
 * @contributor Julien BRISWALTER <j1.seth@phpboost.com>
 * @contributor Arnaud GENET <elenwii@phpboost.com>
 * @contributor mipel <mipel@phpboost.com>
*/

class SessionData
{
    const DEFAULT_VISITOR_DISPLAY_NAME = 'visitor';

    private static $KEY_USER_ID = 'user_id';
    private static $KEY_SESSION_ID = 'session_id';

    protected $user_id;
    protected $session_id;
    protected $token;
    protected $timestamp;
    protected $ip;
    protected $location_script;
    protected $location_title;
    protected $location_id;
    protected $cached_data = array();
    protected $data = array();

    protected $cached_data_modified = false;
    protected $data_modified = false;

    protected function __construct($user_id, $session_id)
    {
        $this->user_id = $user_id;
        $this->session_id = $session_id;
    }

    public function get_user_id()
    {
        return $this->user_id;
    }

    public function get_session_id()
    {
        return $this->session_id;
    }

    public function get_token()
    {
        return $this->token;
    }

    public function get_expiry_date()
    {
        return $this->timestamp + SessionsConfig::load()->get_session_duration();
    }

    public function get_timestamp()
    {
        return $this->timestamp;
    }

    public function get_ip()
    {
        return $this->ip;
    }

    public function get_location_script()
    {
        return $this->location_script;
    }

    public function get_location_title()
    {
        return $this->location_title;
    }

    public function get_location_id()
    {
        return $this->location_id;
    }

    public function get_all_cached_data()
    {
        return $this->cached_data;
    }

    public function has_cached_data($key)
    {
        return isset($this->cached_data[$key]);
    }

    public function get_cached_data($key, $default = null)
    {
        if (is_array($this->cached_data) && array_key_exists($key, $this->cached_data))
        {
            return $this->cached_data[$key];
        }
        return $default;
    }

    public function add_cached_data($key, $value)
    {
        $this->cached_data_modified = true;
        $this->cached_data[$key] = $value;
    }

    public function remove_cached_data($key)
    {
        $this->cached_data_modified = true;
        unset($this->cached_data[$key]);
    }

    public function get_all_data()
    {
        return $this->data;
    }

    public function has_data($key)
    {
        return isset($this->data[$key]);
    }

    public function get_data($key)
    {
        if (array_key_exists($key, $this->data))
        {
            return $this->data[$key];
        }
        return $default;
    }

    public function add_data($key, $value)
    {
        $this->data_modified = true;
        $this->data[$key] = $value;
    }

    public function remove_data($key)
    {
        $this->data_modified = true;
        unset($this->data[$key]);
    }

    public function recheck_cached_data()
    {
        self::fill_user_cached_data($this);
        $this->cached_data_modified = true;
        $this->save();
    }

    public function save()
    {
        if ($this->cached_data_modified || $this->data_modified)
        {
            $columns = array();
            if ($this->cached_data_modified)
            {
                $columns['cached_data'] = TextHelper::serialize($this->cached_data);
            }
            if ($this->data_modified)
            {
                $columns['data'] = TextHelper::serialize($this->data);
            }
            $condition = 'WHERE user_id=:user_id AND session_id=:session_id';
            $parameters = array('user_id' => $this->user_id, 'session_id' => $this->session_id);
            PersistenceContext::get_querier()->update(DB_TABLE_SESSIONS, $columns, $condition, $parameters);
            $this->cached_data_modified = false;
            $this->data_modified = false;
        }
    }

    private function create()
    {
        $this->create_in_db();
        $this->create_cookie();
    }

    public function delete()
    {
        $this->delete_in_db();
        $this->delete_cookie();
    }

    private function delete_in_db()
    {
        $condition = 'WHERE user_id=:user_id AND session_id=:session_id';
        $parameters = array('user_id' => $this->user_id, 'session_id' => $this->session_id);
        PersistenceContext::get_querier()->delete(DB_TABLE_SESSIONS, $condition, $parameters);
    }

    private function delete_cookie()
    {
        AppContext::get_response()->delete_cookie(Session::$DATA_COOKIE_NAME);
    }

    private function create_in_db()
    {
        $columns = array(
            'user_id' => $this->user_id,
            'session_id' => $this->session_id,
            'token' => $this->token,
            'timestamp' => $this->timestamp,
            'ip' => $this->ip,
            'cached_data' => TextHelper::serialize($this->cached_data),
            'data' => TextHelper::serialize($this->data)
        );
        $row = PersistenceContext::get_querier()->insert(DB_TABLE_SESSIONS, $columns);
    }

    private function create_cookie()
    {
        $cookie = new HTTPCookie(Session::$DATA_COOKIE_NAME, $this->get_serialized_content(), time() + 31536000);
        AppContext::get_response()->set_cookie($cookie);
    }

    private function get_serialized_content()
    {
        return TextHelper::serialize(array(self::$KEY_USER_ID => $this->user_id, self::$KEY_SESSION_ID => $this->session_id));
    }

    public function no_session_location()
    {
        $data = AppContext::get_session();

        $columns = array('timestamp' => $data->timestamp);
        $condition = 'WHERE user_id=:user_id AND session_id=:session_id';
        $parameters = array('user_id' => $data->user_id, 'session_id' => $data->session_id);
        PersistenceContext::get_querier()->update(DB_TABLE_SESSIONS, $columns, $condition, $parameters);
    }

    public function location_id_already_exists($location_id)
    {
        $condition = 'WHERE location_id=:location_id AND user_id!=:user_id';
        $parameters = array('location_id' => $location_id, 'user_id' => $this->user_id);
        $value = PersistenceContext::get_querier()->count(DB_TABLE_SESSIONS, $condition, $parameters);

        return $value > 0;
    }

    public function get_user_on_location_id($location_id)
    {
        try {
            $user_id = PersistenceContext::get_querier()->get_column_value(DB_TABLE_SESSIONS, 'user_id', 'WHERE location_id=:location_id', array('location_id' => $location_id));
        } catch (RowNotFoundException $e) {
            $user_id = User::VISITOR_LEVEL;
        }

        return $user_id;
    }

    public static function admin_session()
    {
        return new SessionData(1, null);
    }

    /**
     *
     */
    public static function gc()
    {
        PersistenceContext::get_querier()->delete(DB_TABLE_SESSIONS, 'WHERE timestamp < :now', array('now' => time() - SessionsConfig::load()->get_session_duration()));
    }

    /**
     *
     * @return SessionData
     */
    public static function create_visitor()
    {
        return self::create_from_user_id(Session::VISITOR_SESSION_ID);
    }

    /**
     *
     * @param int $user_id
     * @return SessionData
     */
    public static function create_from_user_id($user_id)
    {
        $data = null;
        if ($user_id != Session::VISITOR_SESSION_ID && self::session_exists($user_id))
        {
            $data = self::use_existing_session($user_id);
        }
        else
        {
            $data = new self($user_id, KeyGenerator::generate_key(64));
            $data->token = KeyGenerator::generate_key(16);
            $data->timestamp = time();
            $data->ip = AppContext::get_request()->get_ip_address();
            self::fill_user_cached_data($data);
            $data->create();

            self::add_in_visit_counter();
        }
        return $data;
    }

    public static function add_in_visit_counter()
    {
        $ip_address = AppContext::get_request()->get_ip_address();
        $has_already_visited = PersistenceContext::get_querier()->row_exists(DB_TABLE_VISIT_COUNTER, 'WHERE ip=:ip', array('ip' => $ip_address));
        $is_robot = Robots::is_robot();

        if (!$has_already_visited && !$is_robot)
        {
            $now = new Date(Date::DATE_NOW, Timezone::SITE_TIMEZONE);
            $time = $now->format('Y-m-d', Timezone::SITE_TIMEZONE);

            PersistenceContext::get_querier()->inject("UPDATE " . DB_TABLE_VISIT_COUNTER . " SET ip = ip + 1, time=:time, total = total + 1 WHERE id = 1", array('time' => $time));
            PersistenceContext::get_querier()->insert(DB_TABLE_VISIT_COUNTER, array('ip' => $ip_address, 'time' => $time, 'total' => 0));
        }

        $jobs = AppContext::get_extension_provider_service()->get_extension_point(ScheduledJobExtensionPoint::EXTENSION_POINT);
        foreach ($jobs as $job)
        {
            $job->on_new_session(!$has_already_visited, $is_robot);
        }
    }

    public static function update_location($title_page, $location_id = '')
    {
        $data = AppContext::get_session();

        $columns = array('timestamp' => $data->timestamp, 'location_title' => $title_page, 'location_script' => TextHelper::cut_string(REWRITED_SCRIPT, 200), 'location_id' => $location_id);
        $condition = 'WHERE user_id=:user_id AND session_id=:session_id';
        $parameters = array('user_id' => $data->user_id, 'session_id' => $data->session_id);
        PersistenceContext::get_querier()->update(DB_TABLE_SESSIONS, $columns, $condition, $parameters);

        if (is_array($data->cached_data) && array_key_exists('last_connection_date', $data->cached_data))
        {
            $now = new Date();
            $last_connection_date = new Date($data->cached_data['last_connection_date'], Timezone::SERVER_TIMEZONE);
            if (!($last_connection_date->get_day() == $now->get_day() && $last_connection_date->get_month() == $now->get_month() && $last_connection_date->get_year() == $now->get_year()))
            {
                $data->update_user_info($data->user_id);
                $data->recheck_cached_data_from_user_id($data->user_id);
            }
        }

        return $data;
    }

    public static function recheck_cached_data_from_user_id($user_id)
    {
        if ($user_id != Session::VISITOR_SESSION_ID && self::session_exists($user_id))
        {
            $data = self::get_existing_session($user_id);
            $data->recheck_cached_data();
        }
    }

    /**
     *
     * @param int $user_id
     * @return SessionData
     */
    private static function session_exists($user_id)
    {
        $condition = 'WHERE user_id=:user_id';
        $parameters = array('user_id' => $user_id);
        return PersistenceContext::get_querier()->row_exists(DB_TABLE_SESSIONS, $condition, $parameters);
    }

    /**
     *
     * @param int $user_id
     * @return SessionData
     */
    private static function use_existing_session($user_id)
    {
        self::update_existing_session($user_id);
        $data = self::get_existing_session($user_id);
        $data->create_cookie();
        return $data;
    }

    private static function get_existing_session($user_id)
    {
        $parameters = array('user_id' => $user_id);
        $condition = 'WHERE user_id=:user_id';
        $columns = array('session_id', 'token', 'timestamp', 'ip', 'location_script', 'location_title', 'location_id', 'data', 'cached_data');

        try {
            $row = PersistenceContext::get_querier()->select_single_row(DB_TABLE_SESSIONS, $columns, $condition, $parameters);
        } catch (NotASingleRowFoundException $e) {
            $row = PersistenceContext::get_querier()->select_single_row_query('SELECT *
            FROM ' . DB_TABLE_SESSIONS . '
            WHERE user_id=:user_id
            ORDER BY timestamp DESC
            LIMIT 1', $parameters);

            PersistenceContext::get_querier()->delete(DB_TABLE_SESSIONS, 'WHERE user_id=:user_id AND session_id != :session_id', array('user_id' => $user_id, 'session_id' => $row['session_id']));
        }
        return self::init_from_row($user_id, $row['session_id'], $row);
    }

    private static function update_existing_session($user_id)
    {
        $columns = array(
            'timestamp' => time() + SessionsConfig::load()->get_session_duration(),
            'ip' => AppContext::get_request()->get_ip_address()
        );
        $parameters = array('user_id' => $user_id);
        $condition = 'WHERE user_id=:user_id';
        PersistenceContext::get_querier()->update(DB_TABLE_SESSIONS, $columns, $condition, $parameters);
    }

    /**
     *
     * @param string $cookie_content
     * @return SessionData
     */
    public static function from_cookie($cookie_content)
    {
        $values = TextHelper::unserialize($cookie_content);
        if ($values === false || empty($values[self::$KEY_USER_ID]) || empty($values[self::$KEY_SESSION_ID]))
        {
            throw new UnexpectedValueException('invalid session data cookie content: "' . $cookie_content . '"');
        }
        try
        {
            $user_id = $values[self::$KEY_USER_ID];
            $session_id = $values[self::$KEY_SESSION_ID];
            $columns = array('token', 'timestamp', 'ip', 'location_script', 'location_title', 'location_id', 'data', 'cached_data');
            $condition = 'WHERE user_id=:user_id AND session_id=:session_id';
            $parameters = array('user_id' => $user_id, 'session_id' => $session_id);
            $row = PersistenceContext::get_querier()->select_single_row(DB_TABLE_SESSIONS, $columns, $condition, $parameters);
            $data = self::init_from_row($user_id, $session_id, $row);
            $data->timestamp = time();
            return $data;
        }
        catch (RowNotFoundException $ex)
        {
            throw new SessionNotFoundException($user_id, $session_id);
        }
    }

    private static function init_from_row($user_id, $session_id, array $row)
    {
        $fixed_cached_data = preg_replace_callback( '!s:(\d+):"(.*?)";!u', function($match) {
            return ($match[1] == TextHelper::strlen($match[2])) ? $match[0] : 's:' . TextHelper::strlen($match[2]) . ':"' . $match[2] . '";';
        }, $row['cached_data']);

        $data = new SessionData($user_id, $session_id);
        $data->token = $row['token'];
        $data->timestamp = $row['timestamp'];
        $data->ip = $row['ip'];
        $data->location_script = $row['location_script'];
        $data->location_title = $row['location_title'];
        $data->location_id = $row['location_id'];
        $data->cached_data = TextHelper::unserialize($fixed_cached_data);
        $data->data = TextHelper::unserialize($row['data']);
        return $data;
    }

    private static function fill_user_cached_data(SessionData $data)
    {
        try
        {
            $data->cached_data = PersistenceContext::get_querier()->select_single_row_query('SELECT member.user_id AS m_user_id, member.display_name, member.level, member.email, member.show_email, member.locale, member.theme, member.timezone, member.editor, member.unread_pm, member.posted_msg, member.registration_date, member.last_connection_date, member.groups, member.warning_percentage, member.delay_banned, member.delay_readonly, member_extended_fields.*
            FROM ' . DB_TABLE_MEMBER . ' member
            LEFT JOIN ' . DB_TABLE_MEMBER_EXTENDED_FIELDS . ' member_extended_fields ON member_extended_fields.user_id = member.user_id
            WHERE member.user_id = :user_id', array(
                'user_id' => $data->user_id
            ));
        }
        catch (RowNotFoundException $ex)
        {
            $current_robot_name = Robots::get_current_robot_name();
            $data->cached_data = !empty($current_robot_name) ? User::get_visitor_properties($current_robot_name, User::ROBOT_LEVEL) : User::get_visitor_properties(self::DEFAULT_VISITOR_DISPLAY_NAME);
        }
    }

    protected function update_user_info($user_id)
    {
        PersistenceContext::get_querier()->update(DB_TABLE_MEMBER, array('last_connection_date' => time()), 'WHERE user_id=:user_id', array('user_id' => $user_id));
    }

    /**
     * Check the session against CSRF attacks by POST. Checks that POSTs are done with the token of the current session.
     * If the token of the request doesn't match the token of the current session, this method will consider that it's a CSRF attack.
     */
    public function csrf_post_protect()
    {
        if (AppContext::get_request()->is_post_method())
            $this->check_csrf_attack();
    }

    /**
     * Check the session against CSRF attacks by GET. Checks that GETs are done with the token of the current session.
     * If the token of the request doesn't match the token of the current session, this method will consider that it's a CSRF attack.
     */
    public function csrf_get_protect()
    {
        $this->check_csrf_attack();
    }

    private function check_csrf_attack()
    {
        $request = AppContext::get_request();
        if (!$request->has_parameter('token') || $request->get_value('token') !== $this->get_token())
        {
            DispatchManager::redirect(PHPBoostErrors::CSRF());
        }
    }
}
?>