PHPSessionHandler.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374
  1. <?php
  2. /**
  3. * Session storage in object cache.
  4. *
  5. * This program is free software; you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation; either version 2 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License along
  16. * with this program; if not, write to the Free Software Foundation, Inc.,
  17. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  18. * http://www.gnu.org/copyleft/gpl.html
  19. *
  20. * @file
  21. * @ingroup Session
  22. */
  23. namespace MediaWiki\Session;
  24. use Psr\Log\LoggerInterface;
  25. use BagOStuff;
  26. use Psr\Log\NullLogger;
  27. /**
  28. * Adapter for PHP's session handling
  29. * @ingroup Session
  30. * @since 1.27
  31. */
  32. class PHPSessionHandler implements \SessionHandlerInterface {
  33. /** @var PHPSessionHandler */
  34. protected static $instance = null;
  35. /** @var bool Whether PHP session handling is enabled */
  36. protected $enable = false;
  37. /** @var bool */
  38. protected $warn = true;
  39. /** @var SessionManagerInterface|null */
  40. protected $manager;
  41. /** @var BagOStuff|null */
  42. protected $store;
  43. /** @var LoggerInterface */
  44. protected $logger;
  45. /** @var array Track original session fields for later modification check */
  46. protected $sessionFieldCache = [];
  47. protected function __construct( SessionManager $manager ) {
  48. $this->setEnableFlags(
  49. \RequestContext::getMain()->getConfig()->get( 'PHPSessionHandling' )
  50. );
  51. $manager->setupPHPSessionHandler( $this );
  52. }
  53. /**
  54. * Set $this->enable and $this->warn
  55. *
  56. * Separate just because there doesn't seem to be a good way to test it
  57. * otherwise.
  58. *
  59. * @param string $PHPSessionHandling See $wgPHPSessionHandling
  60. */
  61. private function setEnableFlags( $PHPSessionHandling ) {
  62. switch ( $PHPSessionHandling ) {
  63. case 'enable':
  64. $this->enable = true;
  65. $this->warn = false;
  66. break;
  67. case 'warn':
  68. $this->enable = true;
  69. $this->warn = true;
  70. break;
  71. case 'disable':
  72. $this->enable = false;
  73. $this->warn = false;
  74. break;
  75. }
  76. }
  77. /**
  78. * Test whether the handler is installed
  79. * @return bool
  80. */
  81. public static function isInstalled() {
  82. return (bool)self::$instance;
  83. }
  84. /**
  85. * Test whether the handler is installed and enabled
  86. * @return bool
  87. */
  88. public static function isEnabled() {
  89. return self::$instance && self::$instance->enable;
  90. }
  91. /**
  92. * Install a session handler for the current web request
  93. * @param SessionManager $manager
  94. */
  95. public static function install( SessionManager $manager ) {
  96. if ( self::$instance ) {
  97. $manager->setupPHPSessionHandler( self::$instance );
  98. return;
  99. }
  100. // @codeCoverageIgnoreStart
  101. if ( defined( 'MW_NO_SESSION_HANDLER' ) ) {
  102. throw new \BadMethodCallException( 'MW_NO_SESSION_HANDLER is defined' );
  103. }
  104. // @codeCoverageIgnoreEnd
  105. self::$instance = new self( $manager );
  106. // Close any auto-started session, before we replace it
  107. session_write_close();
  108. try {
  109. \Wikimedia\suppressWarnings();
  110. // Tell PHP not to mess with cookies itself
  111. ini_set( 'session.use_cookies', 0 );
  112. ini_set( 'session.use_trans_sid', 0 );
  113. // T124510: Disable automatic PHP session related cache headers.
  114. // MediaWiki adds it's own headers and the default PHP behavior may
  115. // set headers such as 'Pragma: no-cache' that cause problems with
  116. // some user agents.
  117. session_cache_limiter( '' );
  118. // Also set a sane serialization handler
  119. \Wikimedia\PhpSessionSerializer::setSerializeHandler();
  120. // Register this as the save handler, and register an appropriate
  121. // shutdown function.
  122. session_set_save_handler( self::$instance, true );
  123. } finally {
  124. \Wikimedia\restoreWarnings();
  125. }
  126. }
  127. /**
  128. * Set the manager, store, and logger
  129. * @private Use self::install().
  130. * @param SessionManagerInterface $manager
  131. * @param BagOStuff $store
  132. * @param LoggerInterface $logger
  133. */
  134. public function setManager(
  135. SessionManagerInterface $manager, BagOStuff $store, LoggerInterface $logger
  136. ) {
  137. if ( $this->manager !== $manager ) {
  138. // Close any existing session before we change stores
  139. if ( $this->manager ) {
  140. session_write_close();
  141. }
  142. $this->manager = $manager;
  143. $this->store = $store;
  144. $this->logger = $logger;
  145. \Wikimedia\PhpSessionSerializer::setLogger( $this->logger );
  146. }
  147. }
  148. /**
  149. * Initialize the session (handler)
  150. * @private For internal use only
  151. * @param string $save_path Path used to store session files (ignored)
  152. * @param string $session_name Session name (ignored)
  153. * @return true
  154. */
  155. public function open( $save_path, $session_name ) {
  156. if ( self::$instance !== $this ) {
  157. throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
  158. }
  159. if ( !$this->enable ) {
  160. throw new \BadMethodCallException( 'Attempt to use PHP session management' );
  161. }
  162. return true;
  163. }
  164. /**
  165. * Close the session (handler)
  166. * @private For internal use only
  167. * @return true
  168. */
  169. public function close() {
  170. if ( self::$instance !== $this ) {
  171. throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
  172. }
  173. $this->sessionFieldCache = [];
  174. return true;
  175. }
  176. /**
  177. * Read session data
  178. * @private For internal use only
  179. * @param string $id Session id
  180. * @return string Session data
  181. */
  182. public function read( $id ) {
  183. if ( self::$instance !== $this ) {
  184. throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
  185. }
  186. if ( !$this->enable ) {
  187. throw new \BadMethodCallException( 'Attempt to use PHP session management' );
  188. }
  189. $session = $this->manager->getSessionById( $id, false );
  190. if ( !$session ) {
  191. return '';
  192. }
  193. $session->persist();
  194. $data = iterator_to_array( $session );
  195. $this->sessionFieldCache[$id] = $data;
  196. return (string)\Wikimedia\PhpSessionSerializer::encode( $data );
  197. }
  198. /**
  199. * Write session data
  200. * @private For internal use only
  201. * @param string $id Session id
  202. * @param string $dataStr Session data. Not that you should ever call this
  203. * directly, but note that this has the same issues with code injection
  204. * via user-controlled data as does PHP's unserialize function.
  205. * @return bool
  206. */
  207. public function write( $id, $dataStr ) {
  208. if ( self::$instance !== $this ) {
  209. throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
  210. }
  211. if ( !$this->enable ) {
  212. throw new \BadMethodCallException( 'Attempt to use PHP session management' );
  213. }
  214. $session = $this->manager->getSessionById( $id, true );
  215. if ( !$session ) {
  216. // This can happen under normal circumstances, if the session exists but is
  217. // invalid. Let's emit a log warning instead of a PHP warning.
  218. $this->logger->warning(
  219. __METHOD__ . ': Session "{session}" cannot be loaded, skipping write.',
  220. [
  221. 'session' => $id,
  222. ] );
  223. return true;
  224. }
  225. // First, decode the string PHP handed us
  226. $data = \Wikimedia\PhpSessionSerializer::decode( $dataStr );
  227. if ( $data === null ) {
  228. // @codeCoverageIgnoreStart
  229. return false;
  230. // @codeCoverageIgnoreEnd
  231. }
  232. // Now merge the data into the Session object.
  233. $changed = false;
  234. $cache = $this->sessionFieldCache[$id] ?? [];
  235. foreach ( $data as $key => $value ) {
  236. if ( !array_key_exists( $key, $cache ) ) {
  237. if ( $session->exists( $key ) ) {
  238. // New in both, so ignore and log
  239. $this->logger->warning(
  240. __METHOD__ . ": Key \"$key\" added in both Session and \$_SESSION!"
  241. );
  242. } else {
  243. // New in $_SESSION, keep it
  244. $session->set( $key, $value );
  245. $changed = true;
  246. }
  247. } elseif ( $cache[$key] === $value ) {
  248. // Unchanged in $_SESSION, so ignore it
  249. } elseif ( !$session->exists( $key ) ) {
  250. // Deleted in Session, keep but log
  251. $this->logger->warning(
  252. __METHOD__ . ": Key \"$key\" deleted in Session and changed in \$_SESSION!"
  253. );
  254. $session->set( $key, $value );
  255. $changed = true;
  256. } elseif ( $cache[$key] === $session->get( $key ) ) {
  257. // Unchanged in Session, so keep it
  258. $session->set( $key, $value );
  259. $changed = true;
  260. } else {
  261. // Changed in both, so ignore and log
  262. $this->logger->warning(
  263. __METHOD__ . ": Key \"$key\" changed in both Session and \$_SESSION!"
  264. );
  265. }
  266. }
  267. // Anything deleted in $_SESSION and unchanged in Session should be deleted too
  268. // (but not if $_SESSION can't represent it at all)
  269. \Wikimedia\PhpSessionSerializer::setLogger( new NullLogger() );
  270. foreach ( $cache as $key => $value ) {
  271. if ( !array_key_exists( $key, $data ) && $session->exists( $key ) &&
  272. \Wikimedia\PhpSessionSerializer::encode( [ $key => true ] )
  273. ) {
  274. if ( $cache[$key] === $session->get( $key ) ) {
  275. // Unchanged in Session, delete it
  276. $session->remove( $key );
  277. $changed = true;
  278. } else {
  279. // Changed in Session, ignore deletion and log
  280. $this->logger->warning(
  281. __METHOD__ . ": Key \"$key\" changed in Session and deleted in \$_SESSION!"
  282. );
  283. }
  284. }
  285. }
  286. \Wikimedia\PhpSessionSerializer::setLogger( $this->logger );
  287. // Save and update cache if anything changed
  288. if ( $changed ) {
  289. if ( $this->warn ) {
  290. wfDeprecated( '$_SESSION', '1.27' );
  291. $this->logger->warning( 'Something wrote to $_SESSION!' );
  292. }
  293. $session->save();
  294. $this->sessionFieldCache[$id] = iterator_to_array( $session );
  295. }
  296. $session->persist();
  297. return true;
  298. }
  299. /**
  300. * Destroy a session
  301. * @private For internal use only
  302. * @param string $id Session id
  303. * @return true
  304. */
  305. public function destroy( $id ) {
  306. if ( self::$instance !== $this ) {
  307. throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
  308. }
  309. if ( !$this->enable ) {
  310. throw new \BadMethodCallException( 'Attempt to use PHP session management' );
  311. }
  312. $session = $this->manager->getSessionById( $id, false );
  313. if ( $session ) {
  314. $session->clear();
  315. }
  316. return true;
  317. }
  318. /**
  319. * Execute garbage collection.
  320. * @private For internal use only
  321. * @param int $maxlifetime Maximum session life time (ignored)
  322. * @return true
  323. * @codeCoverageIgnore See T135576
  324. */
  325. public function gc( $maxlifetime ) {
  326. if ( self::$instance !== $this ) {
  327. throw new \UnexpectedValueException( __METHOD__ . ': Wrong instance called!' );
  328. }
  329. $before = date( 'YmdHis', time() );
  330. $this->store->deleteObjectsExpiringBefore( $before );
  331. return true;
  332. }
  333. }