session.php 15.1 KB
Newer Older
1 2 3 4
<?php
/**
 *
 * @package    mahara
Nigel McNie's avatar
Nigel McNie committed
5
 * @subpackage core
6
 * @author     Catalyst IT Ltd
7 8
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL version 3 or later
 * @copyright  For copyright information on Mahara, please see the README file distributed with this software.
9 10 11 12 13
 *
 */

defined('INTERNAL') || die();

14 15 16
//
// Set session settings
//
17
session_name(get_config('cookieprefix') . 'mahara');
18 19 20
// Secure session settings
// See more at http://php.net/manual/en/session.security.php
ini_set('session.use_cookies', true);
21
ini_set('session.use_only_cookies', true);
22 23 24 25 26
ini_set('session.cookie_lifetime', 0);
ini_set('session.cookie_httponly', true);
if (is_https()) {
    ini_set('session.cookie_secure', true);
}
27 28 29
if ($domain = get_config('cookiedomain')) {
    ini_set('session.cookie_domain', $domain);
}
30
ini_set('session.cookie_path', get_mahara_install_subdirectory());
31
ini_set('session.hash_bits_per_character', 4);
32 33 34 35 36 37 38 39 40
ini_set('session.gc_divisor', 1000);
// session timeout must not exceed 30 days
if (get_config('session_timeout')) {
    ini_set('session.gc_maxlifetime', min(get_config('session_timeout'), 60 * 60 * 24 * 30));
}
ini_set('session.use_trans_sid', false);
ini_set('session.hash_function', 'sha256'); // stronger hash functions are sha384 and sha512
if (version_compare(PHP_VERSION, '5.5.2') > 0) {
    ini_set('session.use_strict_mode', true);
41
}
42

43 44 45
$sessionpath = get_config('sessionpath');
ini_set('session.save_path', '3;' . $sessionpath);

46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
// Attempt to create session directories
if (!is_dir("$sessionpath/0")) {
    // Create three levels of directories, named 0-9, a-f
    $characters = array('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f');
    foreach ($characters as $c1) {
        check_dir_exists("$sessionpath/$c1");
        foreach ($characters as $c2) {
            check_dir_exists("$sessionpath/$c1/$c2");
            foreach ($characters as $c3) {
                check_dir_exists("$sessionpath/$c1/$c2/$c3");
            }
        }
    }
}

61
/**
62
 * The session class handles session data and messages.
63
 *
64 65 66 67
 * This class stores information across page loads, using only a cookie to
 * remember the info. User information is stored in the session so it does
 * not have to be requested each time the page is loaded, however any other
 * information can also be stored using this class.
68 69 70 71
 *
 * This class also is smart about giving out sessions - if a visitor
 * has not logged in (e.g. they are a guest, searchbot or a simple
 * 'curl' request), a session will not be created for them.
72
 *
73 74
 * Messages are stored in the session and are displayed the next time
 * a page is displayed to a user, even over multiple requests.
75 76 77 78
 *
 * In addition, to allow the json progress meter to do its work, this
 * class maintains the session state, keeping it read-only except as
 * necessary (in order to not block the progress meter json calls).
79 80 81
 */
class Session {

82
    /**
83
     * Resumes an existing session, only if there is one
84
     */
85
    private function __construct() {
86
        // Resume an existing session if required
87
        if (isset($_COOKIE[session_name()])) {
88
            @session_start();
89 90 91

            // But it's not writable except through functions below.
            $this->ro_session();
92 93 94
        }
    }

95 96 97 98 99 100 101 102 103 104 105 106
    /**
     * Singelton function keeps us from generating multiple instances of this
     * class
     *
     * @return object   The class instance
     * @access public
     */
    public static function singleton() {
        //single instance
        static $instance;

        //if we don't have the single instance, create one
107
        if (!isset($instance)) {
108 109 110 111 112
            $instance = new Session();
        }
        return($instance);
    }

113 114 115 116 117 118 119 120 121 122
    /**
     * Gets the session property keyed by $key.
     *
     * @param string $key The key to get the value of
     * @return mixed
     */
    public function __get($key) {
        return $this->get($key);
    }

123 124 125 126 127 128 129 130 131 132
    /**
     * Gets the session property keyed by $key.
     *
     * @param string $key The key to get the value of
     * @return mixed
     */
    public function get($key) {
        if (isset($_SESSION[$key])) {
            return $_SESSION[$key];
        }
Penny Leach's avatar
Penny Leach committed
133 134 135
        return null;
    }

136 137 138 139 140 141 142 143 144 145
    /**
     * Sets the session property keyed by $key.
     *
     * @param string $key The key to get the value of
     * @return mixed
     */
    public function __set($key, $value) {
        return $this->set($key, $value);
    }

146 147 148 149 150 151 152
    /**
     * Sets the session property keyed by $key.
     *
     * @param string $key   The key to set.
     * @param string $value The value to set for the key
     */
    public function set($key, $value) {
153
        $this->ensure_session();
154 155 156 157 158 159 160 161

        if (is_null($value)) {
            unset($_SESSION[$key]);
        }
        else {
            $_SESSION[$key] = $value;
        }
        $this->ro_session();
162 163
    }

Martyn Smith's avatar
Martyn Smith committed
164
    /**
165
     * Unsets the session property keyed by $key.
Martyn Smith's avatar
Martyn Smith committed
166
     *
167
     * @param string $key   The key to remove.
Martyn Smith's avatar
Martyn Smith committed
168
     */
169
    public function __unset($key) {
Martyn Smith's avatar
Martyn Smith committed
170
        $this->ensure_session();
171 172
        unset($_SESSION[$key]);
        $this->ro_session();
Martyn Smith's avatar
Martyn Smith committed
173 174
    }

175 176 177 178 179 180 181 182 183
    /**
     * Old way of clearing session property - added for backwards compatibility
     *
     * @param string $key   The key to remove.
     */
    public function clear($key) {
        $this->__unset($key);
    }

184 185 186 187 188
    /**
     * Adds a message that indicates something was successful
     *
     * @param string $message The message to add
     * @param boolean $escape Whether to HTML escape the message
189 190
     * @param string $placement Place for messages to appear on page (See render_messages()
     *     for information about placement options)
191
     */
192
    public function add_ok_msg($message, $escape=true, $placement='messages') {
193
        $this->ensure_session();
194
        if ($escape) {
195
            $message = self::escape_message($message);
196
        }
197
        $_SESSION['messages'][] = array('type' => 'ok', 'msg' => $message, 'placement' => $placement);
198
        $this->ro_session();
199 200 201 202 203 204 205
    }

    /**
     * Adds a message that indicates an informational message
     *
     * @param string $message The message to add
     * @param boolean $escape Whether to HTML escape the message
206 207
     * @param string $placement Place for messages to appear on page (See render_messages()
     *     for information about placement options)
208
     */
209
    public function add_info_msg($message, $escape=true, $placement='messages') {
210
        $this->ensure_session();
211
        if ($escape) {
212
            $message = self::escape_message($message);
213
        }
214
        $_SESSION['messages'][] = array('type' => 'info', 'msg' => $message, 'placement' => $placement);
215
        $this->ro_session();
216 217 218 219 220 221 222
    }

    /**
     * Adds a message that indicates a failure to do something
     *
     * @param string $message The message to add
     * @param boolean $escape Whether to HTML escape the message
223 224
     * @param string $placement Place for messages to appear on page (See render_messages()
     *     for information about placement options)
225
     */
226
    public function add_error_msg($message, $escape=true, $placement='messages') {
227
        $this->ensure_session();
228
        if ($escape) {
229
            $message = self::escape_message($message);
230
        }
231
        $_SESSION['messages'][] = array('type' => 'error', 'msg' => $message, 'placement' => $placement);
232
        $this->ro_session();
233 234 235 236 237 238 239 240
    }

    /**
     * Builds HTML that represents all of the messages and returns it.
     *
     * This is designed to let smarty templates hook in any session messages.
     *
     * Calling this function will destroy the session messages that were
241 242
     * assigned to the $placement, so they do not inadvertently get
     * displayed again.
243 244 245 246 247 248 249 250 251 252 253 254 255 256
     *
     * To define where the messages for a particular $placement value should be displayed,
     * add this code to a page template:
     *
     *   {dynamic}{insert_messages placement='your_placement_name_here'}{/dynamic}
     *
     * The default 'messages' placement is shown on every page, and is suitable for most purposes.
     * Alternative placements should only be needed in special situations, such as showing a login-related
     * error in the login box. Note that messages will hang around in the $SESSION until a page template
     * with their "placement" in it is loaded. So, they should only be used in situations where you're
     * certain their placement zone will be present on the next page load, or else the user may be
     * confused by their appearance several page loads later.
     *
     * @param string $placement Render only messages for this placement
257
     *
258 259
     * @return string The HTML representing all of the session messages assigned
     * to $placement.
260
     */
261
    public function render_messages($placement = 'messages') {
262
        global $THEME;
263
        $result = '<div id="' . $placement . '" role="alert" aria-live="assertive">';
264
        if (isset($_SESSION['messages'])) {
265
            $this->ensure_session();  // Make it writable and lock against other threads.
266
            foreach ($_SESSION['messages'] as $key => $data) {
Pat Kira's avatar
Pat Kira committed
267 268 269 270 271 272 273

                $typeClass = $data['type'] === 'ok' ? 'success' : $data['type'];

                if ($typeClass === 'error') {
                    $typeClass = 'danger';
                }

274
                if ($data['placement'] == $placement) {
Pat Kira's avatar
Pat Kira committed
275
                    $result .= '<div class="alert alert-' . $typeClass . '"><div>';
276 277 278
                    $result .= $data['msg'] . '</div></div>';
                    unset($_SESSION['messages'][$key]);
                }
279
            }
280
            $this->ro_session();
281
        }
282
        $result .= '</div>';
283 284 285
        return $result;
    }

286 287 288 289 290 291 292 293 294 295 296 297 298 299 300
    public function set_progress($token, $content) {
        // Make the session writable.
        $this->ensure_session();

        if ($content === FALSE) {
            unset($_SESSION['progress_meters'][$token]);
        }
        else {
            $_SESSION['progress_meters'][$token] = $content;
        }

        // Release our lock.
        $this->ro_session();
    }

301
    /**
302
     * Create a session, by initialising the $_SESSION array.
303
     */
304
    private function ensure_session() {
305 306 307 308
        if (defined('CLI')) {
            return;
        }

309
        if (empty($_SESSION)) {
310
            @session_start();
311 312 313
            $_SESSION = array(
                'messages' => array()
            );
314
        }
315 316 317
        else {
            @session_start();
        }
318 319 320
        // Anytime you call session_start() more than once, PHP will usually
        // send out a duplicate session header.
        clear_duplicate_cookies();
321 322 323 324 325 326 327 328 329
    }

    /*
     * Make a session readonly after modifying it.
     *
     * The session must have been opened already.
     */

    private function ro_session() {
330 331 332 333
        if (defined('CLI')) {
            return;
        }

334
        session_write_close();
335 336
    }

337 338 339 340
    /**
     * Destroy a session
     */
    public function destroy_session() {
341 342 343 344
        if (defined('CLI')) {
            return;
        }

345 346 347 348 349 350 351 352 353 354
        if ($this->is_live()) {
            $_SESSION = array();
            if (isset($_COOKIE[session_name()])) {
                setcookie(session_name(), '', time() - 65536,
                    ini_get('session.cookie_path'),
                    ini_get('session.cookie_domain'),
                    ini_get('session.cookie_secure'),
                    ini_get('session.cookie_httponly')
                );
            }
355
            @session_start();
356
            session_destroy();
357
            clear_duplicate_cookies();
358 359 360
        }
    }

361 362 363 364 365 366 367 368 369 370
    /**
     * Find out if the session has been started yet
     */
    public function is_live() {
        if ("" == session_id()) {
            return false;
        }
        return true;
    }

371 372
    /**
     * Escape a message for HTML output
373
     *
374 375 376 377 378 379 380
     * @param string $message The message to escape
     * @return string         The message, escaped for output as HTML
     */
    private static function escape_message($message) {
        $message = hsc($message);
        $message = str_replace('  ', '&nbsp; ', $message);
        return $message;
381
    }
382

383 384 385 386 387 388 389
}

/**
 * A smarty callback to insert page messages
 *
 * @return string The HTML represening all of the session messages.
 */
390
function insert_messages($placement='messages') {
391
    global $SESSION;
392
    return $SESSION->render_messages($placement);
393 394
}

395

396 397 398 399
/**
 * Delete all sessions belonging to a given user except for the current one
 */
function remove_user_sessions($userid) {
400
    global $sessionpath, $USER, $SESSION;
401

402
    $sessionids = get_column('usr_session', 'session', 'usr', (int) $userid);
403 404

    if (empty($sessionids)) {
405 406 407 408 409
        return;
    }

    $alive = array();
    $dead = array();
410 411 412 413 414 415 416 417 418 419

    // Keep track of the current session id so we can return to it at the end
    if ($SESSION->is_live()) {
        $sid = $USER->get('sessionid');
    }
    else {
        // The user has no session (this function is being called by a CLI script)
        $sid = false;
    }

420
    foreach ($sessionids as $sessionid) {
421 422 423
        if ($sessionid == $sid) {
            continue;
        }
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
        $file = $sessionpath;
        for ($i = 0; $i < 3; $i++) {
            $file .= '/' . substr($sessionid, $i, 1);
        }
        $file .= '/sess_' . $sessionid;
        if (file_exists($file)) {
            $alive[] = $sessionid;
        }
        else {
            $dead[] = $sessionid;
        }
    }

    if (!empty($dead)) {
        delete_records_select('usr_session', 'session IN (' . join(',', array_map('db_quote', $dead)) . ')');
    }

    if (empty($alive)) {
        return;
    }

    session_commit();

    foreach ($alive as $sessionid) {
        session_id($sessionid);
        if (session_start()) {
            session_destroy();
            session_commit();
        }
    }

455 456 457 458
    if ($sid !== false) {
        session_id($sid);
        session_start();
    }
459

460
    clear_duplicate_cookies();
461 462 463
    delete_records_select('usr_session', 'session IN (' . join(',', array_map('db_quote', $alive)) . ')');
}

464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481
/**
 * Delete all session files except for the current one
 */
function remove_all_sessions() {
    global $sessionpath, $USER;

    $sid = $USER->get('sessionid');

    $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($sessionpath));
    foreach ($iterator as $path) {
        if ($path->isFile() && $path->getFilename() !== ('sess_' . $sid)) {
            @unlink($path->getPathname());
        }
    }
    clearstatcache();

    delete_records_select('usr_session', 'session != ?', array($sid));
}
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

/**
 * Every time you call session_start(), PHP adds another
 * identical session cookie to the response header. Do this
 * enough times, and your response header becomes big enough
 * to choke the web server.
 *
 * This method clears out the duplicate session cookies.
 */
function clear_duplicate_cookies() {
    // If headers have already been sent, there's nothing we can do
    if (headers_sent()) {
        return;
    }

    $cookies = array();
    foreach (headers_list() as $header) {
        // Identify cookie headers
        if (strpos($header, 'Set-Cookie:') === 0) {
            $cookies[] = $header;
        }
    }
    // Removes all cookie headers, not just the session one.
    header_remove('Set-Cookie');

    // Restore one copy of each cookie
    foreach(array_unique($cookies) as $cookie) {
        header($cookie, false);
    }
}