session.php 25.6 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();

Aaron Wells's avatar
Aaron Wells committed
14
15
Session::setup_server_settings();
Session::setup_response_settings();
16

17
/**
18
 * The session class handles session data and messages.
19
 *
20
21
22
23
 * 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.
24
25
26
27
 *
 * 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.
28
 *
29
30
 * Messages are stored in the session and are displayed the next time
 * a page is displayed to a user, even over multiple requests.
31
32
33
34
 *
 * 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).
35
36
37
 */
class Session {

Aaron Wells's avatar
Aaron Wells committed
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
    /**
     * Configures Session settings that affect server-side behavior. These
     * should be set up as soon as possible on page load, so that they'll
     * be ready in case some bad third-party code calls session_start()
     * out of the normal sequence.
     *
     * So, these should try to avoid relying on $CFG values that are
     * determined dynamically (such as $CFG->wwwroot)
     */
    public static function setup_server_settings() {

        session_name(get_config('cookieprefix') . 'mahara');

        // Secure session settings
        // See more at http://php.net/manual/en/session.security.php
        ini_set('session.use_cookies', true);
        ini_set('session.use_only_cookies', true);
55
56
57
58
59
60
        if (version_compare(PHP_VERSION, '7.1.0') >= 0) {
            ini_set('session.sid_bits_per_character', 5);
        }
        else {
            ini_set('session.hash_bits_per_character', 4);
        }
Aaron Wells's avatar
Aaron Wells committed
61
        ini_set('session.gc_divisor', 1000);
62

Aaron Wells's avatar
Aaron Wells committed
63
        if (get_config('session_timeout')) {
64
65
66
67
68
69
70
            // Limit session timeout to 30 days.
            $session_timeout = min(get_config('session_timeout'), 60 * 60 * 24 * 30);
        }
        else {
            // If session was started up by an error message before the database was initiated,
            // then fall back to a default session timeout of 1 hour.
            $session_timeout = 60 * 60;
Aaron Wells's avatar
Aaron Wells committed
71
        }
72
73
74
75
76
77
        // Note: session.gc_maxlifetime is not the main way login session expiry is enforced.
        // We do that by looking at usr.last_access, in htdocs/auth/user.php.
        // And if you're using the default PHP file session handler with depthdir 3, cleanup
        // of old session files is actually handled by the Mahara cron task auth_remove_old_session_files.
        ini_set('session.gc_maxlifetime', $session_timeout);

Aaron Wells's avatar
Aaron Wells committed
78
        ini_set('session.use_trans_sid', false);
79
80
81
82
83
84
        if (version_compare(PHP_VERSION, '7.1.0') >= 0) {
            ini_set('session.sid_length', 32);
        }
        else {
            ini_set('session.hash_function', 'sha256'); // stronger hash functions are sha384 and sha512
        }
Aaron Wells's avatar
Aaron Wells committed
85
86
87
88
        if (version_compare(PHP_VERSION, '5.5.2') > 0) {
            ini_set('session.use_strict_mode', true);
        }

89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
        // Now lets use the correct session handler
        // Currently only deals with file and memcached
        switch (get_config('sessionhandler')) {
            case 'memcache':
                throw new ConfigSanityException(get_string('memcacheusememcached', 'error'));
                break;
            case 'memcached':
                $memcacheservers = get_config('memcacheservers');
                if (!$memcacheservers) {
                    throw new ConfigSanityException(get_string('nomemcacheserversdefined', 'error', get_config('sessionhandler')));
                }
                if (!extension_loaded(get_config('sessionhandler'))) {
                    throw new ConfigSanityException(get_string('nophpextension', 'error', get_config('sessionhandler')));
                }
                // Because we only want memcached servers we need to strip off any 'tcp://' if accidentally added
                $servers = preg_replace('#tcp://#', '', $memcacheservers);
                foreach (explode(',', $servers) as $server) {
                    list($destination, $port) = explode(':', $server);
107
108
109
110
111
112
113
114
                    $memcached = new Memcached;
                    if (!empty($destination) && !empty($port)) {
                        $memcached->addServer($destination, $port);
                        // addServer doesn't make a connection to the server
                        // but if the server is added, but not running pid will be -1
                        $server_stats = $memcached->getStats();
                        if ($server_stats[$destination . ":" . $port]['pid'] <= 0) {
                            // can't reach the destination:port
115
116
117
118
                            $server_version = $memcached->getVersion();
                            if (empty($server_version[$destination . ":" . $port])) {
                                throw new ConfigSanityException(get_string('nomemcachedserver', 'error', $server));
                            }
119
120
121
                        }
                    }
                    else {
122
                        throw new ConfigSanityException(get_string('nomemcachedserver', 'error', $server));
Aaron Wells's avatar
Aaron Wells committed
123
124
                    }
                }
125
126
127
128
129
130
131
132
133
134
135
136
                ini_set('session.save_handler', 'memcached');
                ini_set('session.save_path', $servers);

                $sess = new MemcachedSession();
                session_set_save_handler($sess, true);
                break;
            case 'file':
                $sessionpath = get_config('sessionpath');
                ini_set('session.save_path', '3;' . $sessionpath);
                // Attempt to create session directories
                if (!is_dir("$sessionpath/0")) {
                    // Create three levels of directories, named 0-9, a-f
137
138
139
                    Session::create_directory_levels($sessionpath);
                }
                break;
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
            case 'redis':
                if (!extension_loaded(get_config('sessionhandler'))) {
                    throw new ConfigSanityException(get_string('nophpextension', 'error', get_config('sessionhandler')));
                }
                else if (
                         ($redissentinelservers = get_config('redissentinelservers')) &&
                         ($redismastergroup = get_config('redismastergroup')) &&
                         ($redisprefix = get_config('redisprefix'))
                        ) {
                    require_once(get_config('libroot') . 'redis/sentinel.php');

                    $sentinel = new sentinel($redissentinelservers);
                    $master = $sentinel->get_master_addr($redismastergroup);

                    if (!empty($master)) {
                        ini_set('session.save_handler', 'redis');
                        ini_set('session.save_path', 'tcp://' . $master->ip . ':' . $master->port . '?prefix=' . $redisprefix);
                    }
                    else {
                        throw new ConfigSanityException(get_string('badsessionhandle', 'error', get_config('sessionhandler')));
                    }
                }
                else {
                    throw new ConfigSanityException(get_string('badsessionhandle', 'error', get_config('sessionhandler')));
                }
                break;
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
            default:
                throw new ConfigSanityException(get_string('wrongsessionhandle', 'error', get_config('sessionhandler')));
        }
    }

    public static function create_directory_levels($sessionpath) {
        $status = true;

        if (check_dir_exists($sessionpath)) {
            // 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');
            if (version_compare(PHP_VERSION, '7.1.0') >= 0) {
                $characters = array_merge($characters, array('g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
                                                             'o', 'p', 'q', 'r', 's', 't', 'u', 'v'));
            }
            foreach ($characters as $c1) {
                if (check_dir_exists("$sessionpath/$c1")) {
                    foreach ($characters as $c2) {
                        if (check_dir_exists("$sessionpath/$c1/$c2")) {
185
                            foreach ($characters as $c3) {
186
187
188
189
                                if (!check_dir_exists("$sessionpath/$c1/$c2/$c3")) {
                                    $status = false;
                                    break(3);
                                }
190
191
                            }
                        }
192
193
194
195
                        else {
                            $status = false;
                            break(2);
                        }
196
197
                    }
                }
198
199
200
201
202
203
204
205
                else {
                    $status = false;
                    break;
                }
            }
        }
        else {
            $status = false;
Aaron Wells's avatar
Aaron Wells committed
206
        }
207
        return $status;
Aaron Wells's avatar
Aaron Wells committed
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
    }

    /**
     * Sets up Session settings that affect the cookie that we write out to
     * the browser. These need to be set up correctly before we send out
     * response headers. So we can be more flexible here, and include
     * ones that are set up dynamically.
     */
    public static function setup_response_settings() {
        ini_set('session.cookie_lifetime', 0);
        ini_set('session.cookie_httponly', true);
        if (is_https()) {
            ini_set('session.cookie_secure', true);
        }
        if ($domain = get_config('cookiedomain')) {
            ini_set('session.cookie_domain', $domain);
        }
        ini_set('session.cookie_path', get_mahara_install_subdirectory());
    }

    private $sessionid = null;

    /**
     * Returns the current (or last known) session id
     */
    public function session_id() {
        if (session_id()) {
            $this->sessionid = session_id();
        }
        return $this->sessionid;
    }

240
    /**
241
     * Resumes an existing session, only if there is one
242
     */
243
    private function __construct() {
244
        // Resume an existing session if required
245
        if (isset($_COOKIE[session_name()])) {
246
            @session_start();
Aaron Wells's avatar
Aaron Wells committed
247
            $this->sessionid = session_id();
248
249
250

            // But it's not writable except through functions below.
            $this->ro_session();
251
252
253
        }
    }

254
255
256
257
258
259
260
261
262
263
264
265
    /**
     * 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
266
        if (!isset($instance)) {
267
268
269
270
271
            $instance = new Session();
        }
        return($instance);
    }

272
273
274
275
276
277
278
279
280
281
    /**
     * 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);
    }

282
283
284
285
286
287
288
289
290
291
    /**
     * 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
292
293
294
        return null;
    }

295
296
297
298
299
300
301
302
303
304
    /**
     * 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);
    }

305
306
307
308
309
310
311
    /**
     * 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) {
312
        $this->ensure_session();
313
314
315
316
317
318
319
320

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

Martyn Smith's avatar
Martyn Smith committed
323
    /**
324
     * Unsets the session property keyed by $key.
Martyn Smith's avatar
Martyn Smith committed
325
     *
326
     * @param string $key   The key to remove.
Martyn Smith's avatar
Martyn Smith committed
327
     */
328
    public function __unset($key) {
Martyn Smith's avatar
Martyn Smith committed
329
        $this->ensure_session();
330
331
        unset($_SESSION[$key]);
        $this->ro_session();
Martyn Smith's avatar
Martyn Smith committed
332
333
    }

334
335
336
337
338
339
340
341
342
    /**
     * Old way of clearing session property - added for backwards compatibility
     *
     * @param string $key   The key to remove.
     */
    public function clear($key) {
        $this->__unset($key);
    }

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
    /**
     * Checks that a successful message is only added once
     *
     * @param string $message The message to add
     * @param boolean $escape Whether to HTML escape the message
     * @param string $placement Place for messages to appear on page (See render_messages()
     *     for information about placement options)
     */
    public function add_msg_once($message, $type, $escape=true, $placement='messages') {
        $this->ensure_session();
        if ($escape) {
            $message = self::escape_message($message);
        }
        $msgs = $this->get('messages');
        foreach ($msgs as $msg) {
            if ($msg ['msg'] == $message && $msg['type'] == $type && $msg['placement'] == $placement) {
                // msg exists
                $this->ro_session();
                return;
            }
        }
        $typestr = 'add_' . $type . '_msg';
        $this->$typestr($message, $escape=true, $placement='messages');
        $this->ro_session();
    }
368
369
370
371
372
    /**
     * Adds a message that indicates something was successful
     *
     * @param string $message The message to add
     * @param boolean $escape Whether to HTML escape the message
373
374
     * @param string $placement Place for messages to appear on page (See render_messages()
     *     for information about placement options)
375
     */
376
    public function add_ok_msg($message, $escape=true, $placement='messages') {
377
        $this->ensure_session();
378
        if ($escape) {
379
            $message = self::escape_message($message);
380
        }
381
        $_SESSION['messages'][] = array('type' => 'ok', 'msg' => $message, 'placement' => $placement);
382
        $this->ro_session();
383
384
385
386
387
388
389
    }

    /**
     * Adds a message that indicates an informational message
     *
     * @param string $message The message to add
     * @param boolean $escape Whether to HTML escape the message
390
391
     * @param string $placement Place for messages to appear on page (See render_messages()
     *     for information about placement options)
392
     */
393
    public function add_info_msg($message, $escape=true, $placement='messages') {
394
        $this->ensure_session();
395
        if ($escape) {
396
            $message = self::escape_message($message);
397
        }
398
        $_SESSION['messages'][] = array('type' => 'info', 'msg' => $message, 'placement' => $placement);
399
        $this->ro_session();
400
401
402
403
404
405
406
    }

    /**
     * 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
407
408
     * @param string $placement Place for messages to appear on page (See render_messages()
     *     for information about placement options)
409
     */
410
    public function add_error_msg($message, $escape=true, $placement='messages') {
411
        $this->ensure_session();
412
        if ($escape) {
413
            $message = self::escape_message($message);
414
        }
415
        $_SESSION['messages'][] = array('type' => 'error', 'msg' => $message, 'placement' => $placement);
416
        $this->ro_session();
417
418
419
420
421
422
423
424
    }

    /**
     * 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
425
426
     * assigned to the $placement, so they do not inadvertently get
     * displayed again.
427
428
429
430
431
432
433
434
435
436
437
438
439
440
     *
     * 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
441
     *
442
443
     * @return string The HTML representing all of the session messages assigned
     * to $placement.
444
     */
445
    public function render_messages($placement = 'messages') {
446
        global $THEME;
447
        $result = '<div id="' . $placement . '" role="alert" aria-live="assertive">';
448
        if (isset($_SESSION['messages'])) {
449
            $this->ensure_session();  // Make it writable and lock against other threads.
450
            foreach ($_SESSION['messages'] as $key => $data) {
Pat Kira's avatar
Pat Kira committed
451
452
453
454
455
456
457

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

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

458
                if ($data['placement'] == $placement) {
Pat Kira's avatar
Pat Kira committed
459
                    $result .= '<div class="alert alert-' . $typeClass . '"><div>';
460
461
462
                    $result .= $data['msg'] . '</div></div>';
                    unset($_SESSION['messages'][$key]);
                }
463
            }
464
            $this->ro_session();
465
        }
466
        $result .= '</div>';
467
468
469
        return $result;
    }

470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
    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();
    }

485
    /**
486
     * Create a session, by initialising the $_SESSION array.
487
     */
488
    private function ensure_session() {
489
490
491
492
        if (defined('CLI')) {
            return;
        }

493
        if (empty($_SESSION)) {
494
            @session_start();
Aaron Wells's avatar
Aaron Wells committed
495
            $this->sessionid = session_id();
496
497
498
            $_SESSION = array(
                'messages' => array()
            );
499
        }
500
501
        else {
            @session_start();
Aaron Wells's avatar
Aaron Wells committed
502
            $this->sessionid = session_id();
503
        }
504
505
506
        // Anytime you call session_start() more than once, PHP will usually
        // send out a duplicate session header.
        clear_duplicate_cookies();
507
508
509
510
511
512
513
514
515
    }

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

    private function ro_session() {
516
517
518
519
        if (defined('CLI')) {
            return;
        }

520
        session_write_close();
521
522
    }

523
    /**
Aaron Wells's avatar
Aaron Wells committed
524
525
526
     * Destroy a session. This removes all data from the $_SESSION object,
     * deletes it from the server, and rotates the user to a new session
     * id.
527
528
     */
    public function destroy_session() {
529
530
531
532
        if (defined('CLI')) {
            return;
        }

533
534
        if ($this->is_live()) {
            $_SESSION = array();
Aaron Wells's avatar
Aaron Wells committed
535
536
537
538
539
540
541
542
543
            @session_start();
            session_destroy();
            $this->sessionid = null;
            clear_duplicate_cookies();

            // Tell the browser to immediately expire the session cookie.
            // (If we take any actions that require a new session, this
            // will be ignored, and instead the old session cookie will
            // be replaced by the new one.)
544
            if (isset($_COOKIE[session_name()])) {
545
546
547
548
                setcookie(
                    session_name(),
                    '',
                    1,
549
550
551
552
553
554
555
556
557
                    ini_get('session.cookie_path'),
                    ini_get('session.cookie_domain'),
                    ini_get('session.cookie_secure'),
                    ini_get('session.cookie_httponly')
                );
            }
        }
    }

558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
    /**
     * Regenerate session id. This does *not* clear the $_SESSION object
     * or the session data on the server. It just changes the user's
     * session ID. You should do this whenever a user logs in or otherwise
     * changes their permission level, to avoid session fixation attacks.
     *
     * If you want to clear the session, call $SESSION->destroy_session()
     *
     * @return boolean
     */
    public function regenerate_id() {
        $this->ensure_session();
        $result = session_regenerate_id(true);
        $this->sessionid = session_id();
        if (!$result) {
            log_warn("session_regenerate_id() failed");
        }
        $this->ro_session();
        return $result;
    }

579
580
581
582
583
584
585
586
587
588
    /**
     * Find out if the session has been started yet
     */
    public function is_live() {
        if ("" == session_id()) {
            return false;
        }
        return true;
    }

589
590
    /**
     * Escape a message for HTML output
591
     *
592
593
594
595
596
597
598
     * @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;
599
    }
600

601
602
603
604
605
606
607
}

/**
 * A smarty callback to insert page messages
 *
 * @return string The HTML represening all of the session messages.
 */
608
function insert_messages($placement='messages') {
609
    global $SESSION;
610
611
    $messages = $SESSION->render_messages($placement);
    return (array($placement => $messages));
612
613
}

614

615
616
617
618
/**
 * Delete all sessions belonging to a given user except for the current one
 */
function remove_user_sessions($userid) {
619
620
    global $USER, $SESSION;
    $sessionpath = get_config('sessionpath');
621
    $sessionids = get_column('usr_session', 'session', 'usr', (int) $userid);
622
623

    if (empty($sessionids)) {
624
625
626
627
628
        return;
    }

    $alive = array();
    $dead = array();
629
630
631
632
633
634
635
636
637
638

    // 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;
    }

639
    foreach ($sessionids as $sessionid) {
640
641
642
        if ($sessionid == $sid) {
            continue;
        }
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
        $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);
Aaron Wells's avatar
Aaron Wells committed
668
        if (session_start() && session_id() === $sessionid) {
669
670
671
672
            session_destroy();
        }
    }

673
674
675
    if ($sid !== false) {
        session_id($sid);
        session_start();
Aaron Wells's avatar
Aaron Wells committed
676
        session_write_close();
677
    }
678

679
    clear_duplicate_cookies();
680
681
682
    delete_records_select('usr_session', 'session IN (' . join(',', array_map('db_quote', $alive)) . ')');
}

683
684
685
686
/**
 * Delete all session files except for the current one
 */
function remove_all_sessions() {
687
688
    global $USER;
    $sessionpath = get_config('sessionpath');
689
690
691
692
693
694
695
696
697
698
699
700
    $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));
}
701
702
703
704
705
706
707
708
709
710

/**
 * 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() {
Aaron Wells's avatar
Aaron Wells committed
711
    global $SESSION;
712
713
714
715
716
    // If headers have already been sent, there's nothing we can do
    if (headers_sent()) {
        return;
    }

Aaron Wells's avatar
Aaron Wells committed
717
718
    $cookiename = session_name();
    $headers = array();
719
    foreach (headers_list() as $header) {
Aaron Wells's avatar
Aaron Wells committed
720
721
        if (strpos($header, "Set-Cookie: {$cookiename}=") !== 0) {
            $headers[] = $header;
722
723
        }
    }
Aaron Wells's avatar
Aaron Wells committed
724
725
726
727
728
729
730
    // Remove all the headers
    header_remove();

    // Now bring back the ones we want.
    foreach($headers as $header) {
        header($header, false);
    }
731

Aaron Wells's avatar
Aaron Wells committed
732
733
    // Now manually regenerate just ONE session cookie header.
    if ($SESSION->session_id()) {
734
        setcookie(
Aaron Wells's avatar
Aaron Wells committed
735
736
737
            $cookiename,
            $SESSION->session_id(),
            0,
738
            ini_get('session.cookie_path'),
Aaron Wells's avatar
Aaron Wells committed
739
740
741
742
            ini_get('session.cookie_domain'),
            ini_get('session.cookie_secure'),
            ini_get('session.cookie_httponly')
        );
743
744
    }
}
745
746
747
748
749
750
751
752

class MemcachedSession extends SessionHandler {
    // we need to extend and override the read method to force it to return string value
    // in order to comply with PHP 7's more strict type checking
    public function read($session_id) {
        return (string)parent::read($session_id);
    }
}