AC_services_website_design/system/Session/Handlers/MemcachedHandler.php

323 lines
8.2 KiB
PHP

<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Session\Handlers;
use CodeIgniter\I18n\Time;
use CodeIgniter\Session\Exceptions\SessionException;
use Config\Session as SessionConfig;
use Memcached;
use ReturnTypeWillChange;
/**
* Session handler using Memcache for persistence
*/
class MemcachedHandler extends BaseHandler
{
/**
* Memcached instance
*
* @var Memcached|null
*/
protected $memcached;
/**
* Key prefix
*
* @var string
*/
protected $keyPrefix = 'ci_session:';
/**
* Lock key
*
* @var string|null
*/
protected $lockKey;
/**
* Number of seconds until the session ends.
*
* @var int
*/
protected $sessionExpiration = 7200;
/**
* @throws SessionException
*/
public function __construct(SessionConfig $config, string $ipAddress)
{
parent::__construct($config, $ipAddress);
$this->sessionExpiration = $config->expiration;
if (empty($this->savePath)) {
throw SessionException::forEmptySavepath();
}
// Add sessionCookieName for multiple session cookies.
$this->keyPrefix .= $config->cookieName . ':';
if ($this->matchIP === true) {
$this->keyPrefix .= $this->ipAddress . ':';
}
ini_set('memcached.sess_prefix', $this->keyPrefix);
}
/**
* Re-initialize existing session, or creates a new one.
*
* @param string $path The path where to store/retrieve the session
* @param string $name The session name
*/
public function open($path, $name): bool
{
$this->memcached = new Memcached();
$this->memcached->setOption(Memcached::OPT_BINARY_PROTOCOL, true); // required for touch() usage
$serverList = [];
foreach ($this->memcached->getServerList() as $server) {
$serverList[] = $server['host'] . ':' . $server['port'];
}
if (
! preg_match_all(
'#,?([^,:]+)\:(\d{1,5})(?:\:(\d+))?#',
$this->savePath,
$matches,
PREG_SET_ORDER
)
) {
$this->memcached = null;
$this->logger->error('Session: Invalid Memcached save path format: ' . $this->savePath);
return false;
}
foreach ($matches as $match) {
// If Memcached already has this server (or if the port is invalid), skip it
if (in_array($match[1] . ':' . $match[2], $serverList, true)) {
$this->logger->debug(
'Session: Memcached server pool already has ' . $match[1] . ':' . $match[2]
);
continue;
}
if (! $this->memcached->addServer($match[1], (int) $match[2], $match[3] ?? 0)) {
$this->logger->error(
'Could not add ' . $match[1] . ':' . $match[2] . ' to Memcached server pool.'
);
} else {
$serverList[] = $match[1] . ':' . $match[2];
}
}
if ($serverList === []) {
$this->logger->error('Session: Memcached server pool is empty.');
return false;
}
return true;
}
/**
* Reads the session data from the session storage, and returns the results.
*
* @param string $id The session ID
*
* @return false|string Returns an encoded string of the read data.
* If nothing was read, it must return false.
*/
#[ReturnTypeWillChange]
public function read($id)
{
if (isset($this->memcached) && $this->lockSession($id)) {
if (! isset($this->sessionID)) {
$this->sessionID = $id;
}
$data = (string) $this->memcached->get($this->keyPrefix . $id);
$this->fingerprint = md5($data);
return $data;
}
return '';
}
/**
* Writes the session data to the session storage.
*
* @param string $id The session ID
* @param string $data The encoded session data
*/
public function write($id, $data): bool
{
if (! isset($this->memcached)) {
return false;
}
if ($this->sessionID !== $id) {
if (! $this->releaseLock() || ! $this->lockSession($id)) {
return false;
}
$this->fingerprint = md5('');
$this->sessionID = $id;
}
if (isset($this->lockKey)) {
$this->memcached->replace($this->lockKey, Time::now()->getTimestamp(), 300);
if ($this->fingerprint !== ($fingerprint = md5($data))) {
if ($this->memcached->set($this->keyPrefix . $id, $data, $this->sessionExpiration)) {
$this->fingerprint = $fingerprint;
return true;
}
return false;
}
return $this->memcached->touch($this->keyPrefix . $id, $this->sessionExpiration);
}
return false;
}
/**
* Closes the current session.
*/
public function close(): bool
{
if (isset($this->memcached)) {
if (isset($this->lockKey)) {
$this->memcached->delete($this->lockKey);
}
if (! $this->memcached->quit()) {
return false;
}
$this->memcached = null;
return true;
}
return false;
}
/**
* Destroys a session
*
* @param string $id The session ID being destroyed
*/
public function destroy($id): bool
{
if (isset($this->memcached, $this->lockKey)) {
$this->memcached->delete($this->keyPrefix . $id);
return $this->destroyCookie();
}
return false;
}
/**
* Cleans up expired sessions.
*
* @param int $max_lifetime Sessions that have not updated
* for the last max_lifetime seconds will be removed.
*
* @return false|int Returns the number of deleted sessions on success, or false on failure.
*/
#[ReturnTypeWillChange]
public function gc($max_lifetime)
{
return 1;
}
/**
* Acquires an emulated lock.
*
* @param string $sessionID Session ID
*/
protected function lockSession(string $sessionID): bool
{
if (isset($this->lockKey)) {
return $this->memcached->replace($this->lockKey, Time::now()->getTimestamp(), 300);
}
$lockKey = $this->keyPrefix . $sessionID . ':lock';
$attempt = 0;
do {
if ($this->memcached->get($lockKey)) {
sleep(1);
continue;
}
if (! $this->memcached->set($lockKey, Time::now()->getTimestamp(), 300)) {
$this->logger->error(
'Session: Error while trying to obtain lock for ' . $this->keyPrefix . $sessionID
);
return false;
}
$this->lockKey = $lockKey;
break;
} while (++$attempt < 30);
if ($attempt === 30) {
$this->logger->error(
'Session: Unable to obtain lock for ' . $this->keyPrefix . $sessionID . ' after 30 attempts, aborting.'
);
return false;
}
$this->lock = true;
return true;
}
/**
* Releases a previously acquired lock
*/
protected function releaseLock(): bool
{
if (isset($this->memcached, $this->lockKey) && $this->lock) {
if (
! $this->memcached->delete($this->lockKey)
&& $this->memcached->getResultCode() !== Memcached::RES_NOTFOUND
) {
$this->logger->error(
'Session: Error while trying to free lock for ' . $this->lockKey
);
return false;
}
$this->lockKey = null;
$this->lock = false;
}
return true;
}
}