/** * Write session data * @private For internal use only * @param string $id Session id * @param string $dataStr Session data. Not that you should ever call this * directly, but note that this has the same issues with code injection * via user-controlled data as does PHP's unserialize function. * @return bool Success */ public function write($id, $dataStr) { if (self::$instance !== $this) { throw new \UnexpectedValueException(__METHOD__ . ': Wrong instance called!'); } if (!$this->enable) { throw new \BadMethodCallException('Attempt to use PHP session management'); } $session = $this->manager->getSessionById($id, true); if (!$session) { // This can happen under normal circumstances, if the session exists but is // invalid. Let's emit a log warning instead of a PHP warning. $this->logger->warning(__METHOD__ . ": Session \"{$id}\" cannot be loaded, skipping write."); return true; } // First, decode the string PHP handed us $data = \Wikimedia\PhpSessionSerializer::decode($dataStr); if ($data === null) { // @codeCoverageIgnoreStart return false; // @codeCoverageIgnoreEnd } // Now merge the data into the Session object. $changed = false; $cache = isset($this->sessionFieldCache[$id]) ? $this->sessionFieldCache[$id] : array(); foreach ($data as $key => $value) { if (!array_key_exists($key, $cache)) { if ($session->exists($key)) { // New in both, so ignore and log $this->logger->warning(__METHOD__ . ": Key \"{$key}\" added in both Session and \$_SESSION!"); } else { // New in $_SESSION, keep it $session->set($key, $value); $changed = true; } } elseif ($cache[$key] === $value) { // Unchanged in $_SESSION, so ignore it } elseif (!$session->exists($key)) { // Deleted in Session, keep but log $this->logger->warning(__METHOD__ . ": Key \"{$key}\" deleted in Session and changed in \$_SESSION!"); $session->set($key, $value); $changed = true; } elseif ($cache[$key] === $session->get($key)) { // Unchanged in Session, so keep it $session->set($key, $value); $changed = true; } else { // Changed in both, so ignore and log $this->logger->warning(__METHOD__ . ": Key \"{$key}\" changed in both Session and \$_SESSION!"); } } // Anything deleted in $_SESSION and unchanged in Session should be deleted too // (but not if $_SESSION can't represent it at all) \Wikimedia\PhpSessionSerializer::setLogger(new \Psr\Log\NullLogger()); foreach ($cache as $key => $value) { if (!array_key_exists($key, $data) && $session->exists($key) && \Wikimedia\PhpSessionSerializer::encode(array($key => true))) { if ($cache[$key] === $session->get($key)) { // Unchanged in Session, delete it $session->remove($key); $changed = true; } else { // Changed in Session, ignore deletion and log $this->logger->warning(__METHOD__ . ": Key \"{$key}\" changed in Session and deleted in \$_SESSION!"); } } } \Wikimedia\PhpSessionSerializer::setLogger($this->logger); // Save and update cache if anything changed if ($changed) { if ($this->warn) { wfDeprecated('$_SESSION', '1.27'); $this->logger->warning('Something wrote to $_SESSION!'); } $session->save(); $this->sessionFieldCache[$id] = iterator_to_array($session); } $session->persist(); return true; }
* user-controlled data here that you would to PHP's unserialize function. * @return array|null Data, or null on failure * @throws \\InvalidArgumentException */ public static function decodePhpSerialize($data) { if (!is_string($data)) { throw new \InvalidArgumentException('$data must be a string'); } $error = null; set_error_handler(function ($errno, $errstr) use(&$error) { $error = $errstr; return true; }); $ret = unserialize($data); restore_error_handler(); if ($error !== null) { self::$logger->error('PHP unserialization failed: ' . $error); return null; } // PHP strangely allows non-arrays to session_decode(), even though // that breaks $_SESSION. Let's not do that. if (!is_array($ret)) { self::$logger->error('PHP unserialization failed (value was not an array)'); return null; } return $ret; } } PhpSessionSerializer::setLogger(new \Psr\Log\NullLogger());