SessionManager.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634
  1. <?php
  2. /**
  3. * Magento session manager
  4. *
  5. * Copyright © Magento, Inc. All rights reserved.
  6. * See COPYING.txt for license details.
  7. */
  8. namespace Magento\Framework\Session;
  9. use Magento\Framework\Session\Config\ConfigInterface;
  10. /**
  11. * Session Manager
  12. * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
  13. * @SuppressWarnings(PHPMD.CookieAndSessionMisuse)
  14. */
  15. class SessionManager implements SessionManagerInterface
  16. {
  17. /**
  18. * Default options when a call destroy()
  19. *
  20. * Description:
  21. * - send_expire_cookie: whether or not to send a cookie expiring the current session cookie
  22. * - clear_storage: whether or not to empty the storage object of any stored values
  23. *
  24. * @var array
  25. */
  26. protected $defaultDestroyOptions = ['send_expire_cookie' => true, 'clear_storage' => true];
  27. /**
  28. * URL host cache
  29. *
  30. * @var array
  31. */
  32. protected static $urlHostCache = [];
  33. /**
  34. * Validator
  35. *
  36. * @var ValidatorInterface
  37. */
  38. protected $validator;
  39. /**
  40. * Request
  41. *
  42. * @var \Magento\Framework\App\Request\Http
  43. */
  44. protected $request;
  45. /**
  46. * SID resolver
  47. *
  48. * @var SidResolverInterface
  49. */
  50. protected $sidResolver;
  51. /**
  52. * Session config
  53. *
  54. * @var Config\ConfigInterface
  55. */
  56. protected $sessionConfig;
  57. /**
  58. * Save handler
  59. *
  60. * @var SaveHandlerInterface
  61. */
  62. protected $saveHandler;
  63. /**
  64. * Storage
  65. *
  66. * @var StorageInterface
  67. */
  68. protected $storage;
  69. /**
  70. * Cookie Manager
  71. *
  72. * @var \Magento\Framework\Stdlib\CookieManagerInterface
  73. */
  74. protected $cookieManager;
  75. /**
  76. * @var \Magento\Framework\Stdlib\Cookie\CookieMetadataFactory
  77. */
  78. protected $cookieMetadataFactory;
  79. /**
  80. * @var \Magento\Framework\App\State
  81. */
  82. private $appState;
  83. /**
  84. * @var SessionStartChecker
  85. */
  86. private $sessionStartChecker;
  87. /**
  88. * @param \Magento\Framework\App\Request\Http $request
  89. * @param SidResolverInterface $sidResolver
  90. * @param ConfigInterface $sessionConfig
  91. * @param SaveHandlerInterface $saveHandler
  92. * @param ValidatorInterface $validator
  93. * @param StorageInterface $storage
  94. * @param \Magento\Framework\Stdlib\CookieManagerInterface $cookieManager
  95. * @param \Magento\Framework\Stdlib\Cookie\CookieMetadataFactory $cookieMetadataFactory
  96. * @param \Magento\Framework\App\State $appState
  97. * @param SessionStartChecker|null $sessionStartChecker
  98. * @throws \Magento\Framework\Exception\SessionException
  99. *
  100. * @SuppressWarnings(PHPMD.ExcessiveParameterList)
  101. */
  102. public function __construct(
  103. \Magento\Framework\App\Request\Http $request,
  104. SidResolverInterface $sidResolver,
  105. ConfigInterface $sessionConfig,
  106. SaveHandlerInterface $saveHandler,
  107. ValidatorInterface $validator,
  108. StorageInterface $storage,
  109. \Magento\Framework\Stdlib\CookieManagerInterface $cookieManager,
  110. \Magento\Framework\Stdlib\Cookie\CookieMetadataFactory $cookieMetadataFactory,
  111. \Magento\Framework\App\State $appState,
  112. SessionStartChecker $sessionStartChecker = null
  113. ) {
  114. $this->request = $request;
  115. $this->sidResolver = $sidResolver;
  116. $this->sessionConfig = $sessionConfig;
  117. $this->saveHandler = $saveHandler;
  118. $this->validator = $validator;
  119. $this->storage = $storage;
  120. $this->cookieManager = $cookieManager;
  121. $this->cookieMetadataFactory = $cookieMetadataFactory;
  122. $this->appState = $appState;
  123. $this->sessionStartChecker = $sessionStartChecker ?: \Magento\Framework\App\ObjectManager::getInstance()->get(
  124. SessionStartChecker::class
  125. );
  126. $this->start();
  127. }
  128. /**
  129. * This method needs to support sessions with APC enabled.
  130. *
  131. * @return void
  132. */
  133. public function writeClose()
  134. {
  135. session_write_close();
  136. }
  137. /**
  138. * Storage accessor method
  139. *
  140. * @param string $method
  141. * @param array $args
  142. * @return mixed
  143. * @throws \InvalidArgumentException
  144. */
  145. public function __call($method, $args)
  146. {
  147. if (!in_array(substr($method, 0, 3), ['get', 'set', 'uns', 'has'])) {
  148. throw new \InvalidArgumentException(
  149. sprintf('Invalid method %s::%s(%s)', get_class($this), $method, print_r($args, 1))
  150. );
  151. }
  152. $return = call_user_func_array([$this->storage, $method], $args);
  153. return $return === $this->storage ? $this : $return;
  154. }
  155. /**
  156. * Configure session handler and start session
  157. *
  158. * @throws \Magento\Framework\Exception\SessionException
  159. * @return $this
  160. */
  161. public function start()
  162. {
  163. if ($this->sessionStartChecker->check()) {
  164. if (!$this->isSessionExists()) {
  165. \Magento\Framework\Profiler::start('session_start');
  166. try {
  167. $this->appState->getAreaCode();
  168. } catch (\Magento\Framework\Exception\LocalizedException $e) {
  169. throw new \Magento\Framework\Exception\SessionException(
  170. new \Magento\Framework\Phrase(
  171. 'Area code not set: Area code must be set before starting a session.'
  172. ),
  173. $e
  174. );
  175. }
  176. // Need to apply the config options so they can be ready by session_start
  177. $this->initIniOptions();
  178. $this->registerSaveHandler();
  179. if (isset($_SESSION['new_session_id'])) {
  180. // Not fully expired yet. Could be lost cookie by unstable network.
  181. session_commit();
  182. session_id($_SESSION['new_session_id']);
  183. }
  184. $sid = $this->sidResolver->getSid($this);
  185. // potential custom logic for session id (ex. switching between hosts)
  186. $this->setSessionId($sid);
  187. session_start();
  188. if (isset($_SESSION['destroyed'])
  189. && $_SESSION['destroyed'] < time() - $this->sessionConfig->getCookieLifetime()
  190. ) {
  191. $this->destroy(['clear_storage' => true]);
  192. }
  193. $this->validator->validate($this);
  194. $this->renewCookie($sid);
  195. register_shutdown_function([$this, 'writeClose']);
  196. $this->_addHost();
  197. \Magento\Framework\Profiler::stop('session_start');
  198. }
  199. $this->storage->init(isset($_SESSION) ? $_SESSION : []);
  200. }
  201. return $this;
  202. }
  203. /**
  204. * Renew session cookie to prolong session
  205. *
  206. * @param null|string $sid If we have session id we need to use it instead of old cookie value
  207. * @return $this
  208. */
  209. private function renewCookie($sid)
  210. {
  211. if (!$this->getCookieLifetime()) {
  212. return $this;
  213. }
  214. //When we renew cookie, we should aware, that any other session client do not
  215. //change cookie too
  216. $cookieValue = $sid ?: $this->cookieManager->getCookie($this->getName());
  217. if ($cookieValue) {
  218. $metadata = $this->cookieMetadataFactory->createPublicCookieMetadata();
  219. $metadata->setPath($this->sessionConfig->getCookiePath());
  220. $metadata->setDomain($this->sessionConfig->getCookieDomain());
  221. $metadata->setDuration($this->sessionConfig->getCookieLifetime());
  222. $metadata->setSecure($this->sessionConfig->getCookieSecure());
  223. $metadata->setHttpOnly($this->sessionConfig->getCookieHttpOnly());
  224. $this->cookieManager->setPublicCookie(
  225. $this->getName(),
  226. $cookieValue,
  227. $metadata
  228. );
  229. }
  230. return $this;
  231. }
  232. /**
  233. * Register save handler
  234. *
  235. * @return bool
  236. */
  237. protected function registerSaveHandler()
  238. {
  239. return session_set_save_handler(
  240. [$this->saveHandler, 'open'],
  241. [$this->saveHandler, 'close'],
  242. [$this->saveHandler, 'read'],
  243. [$this->saveHandler, 'write'],
  244. [$this->saveHandler, 'destroy'],
  245. [$this->saveHandler, 'gc']
  246. );
  247. }
  248. /**
  249. * Does a session exist
  250. *
  251. * @return bool
  252. */
  253. public function isSessionExists()
  254. {
  255. if (session_status() === PHP_SESSION_NONE && !headers_sent()) {
  256. return false;
  257. }
  258. return true;
  259. }
  260. /**
  261. * Additional get data with clear mode
  262. *
  263. * @param string $key
  264. * @param bool $clear
  265. * @return mixed
  266. */
  267. public function getData($key = '', $clear = false)
  268. {
  269. $data = $this->storage->getData($key);
  270. if ($clear && isset($data)) {
  271. $this->storage->unsetData($key);
  272. }
  273. return $data;
  274. }
  275. /**
  276. * Retrieve session Id
  277. *
  278. * @return string
  279. */
  280. public function getSessionId()
  281. {
  282. return session_id();
  283. }
  284. /**
  285. * Retrieve session name
  286. *
  287. * @return string
  288. */
  289. public function getName()
  290. {
  291. return session_name();
  292. }
  293. /**
  294. * Set session name
  295. *
  296. * @param string $name
  297. * @return $this
  298. */
  299. public function setName($name)
  300. {
  301. session_name($name);
  302. return $this;
  303. }
  304. /**
  305. * Destroy/end a session
  306. *
  307. * @param array $options
  308. * @return void
  309. */
  310. public function destroy(array $options = null)
  311. {
  312. $options = $options ?? [];
  313. $options = array_merge($this->defaultDestroyOptions, $options);
  314. if ($options['clear_storage']) {
  315. $this->clearStorage();
  316. }
  317. if (session_status() !== PHP_SESSION_ACTIVE) {
  318. return;
  319. }
  320. session_regenerate_id(true);
  321. session_destroy();
  322. if ($options['send_expire_cookie']) {
  323. $this->expireSessionCookie();
  324. }
  325. }
  326. /**
  327. * Unset all session data
  328. *
  329. * @return $this
  330. */
  331. public function clearStorage()
  332. {
  333. $this->storage->unsetData();
  334. return $this;
  335. }
  336. /**
  337. * Retrieve Cookie domain
  338. *
  339. * @return string
  340. */
  341. public function getCookieDomain()
  342. {
  343. return $this->sessionConfig->getCookieDomain();
  344. }
  345. /**
  346. * Retrieve cookie path
  347. *
  348. * @return string
  349. */
  350. public function getCookiePath()
  351. {
  352. return $this->sessionConfig->getCookiePath();
  353. }
  354. /**
  355. * Retrieve cookie lifetime
  356. *
  357. * @return int
  358. */
  359. public function getCookieLifetime()
  360. {
  361. return $this->sessionConfig->getCookieLifetime();
  362. }
  363. /**
  364. * Specify session identifier
  365. *
  366. * @param string|null $sessionId
  367. * @return $this
  368. */
  369. public function setSessionId($sessionId)
  370. {
  371. $this->_addHost();
  372. if ($sessionId !== null && preg_match('#^[0-9a-zA-Z,-]+$#', $sessionId)) {
  373. session_id($sessionId);
  374. }
  375. return $this;
  376. }
  377. /**
  378. * If session cookie is not applicable due to host or path mismatch - add session id to query
  379. *
  380. * @param string $urlHost can be host or url
  381. * @return string {session_id_key}={session_id_encrypted}
  382. * @SuppressWarnings(PHPMD.NPathComplexity)
  383. */
  384. public function getSessionIdForHost($urlHost)
  385. {
  386. $httpHost = $this->request->getHttpHost();
  387. if (!$httpHost) {
  388. return '';
  389. }
  390. $urlHostArr = explode('/', $urlHost, 4);
  391. if (!empty($urlHostArr[2])) {
  392. $urlHost = $urlHostArr[2];
  393. }
  394. $urlPath = empty($urlHostArr[3]) ? '' : $urlHostArr[3];
  395. if (!isset(self::$urlHostCache[$urlHost])) {
  396. $urlHostArr = explode(':', $urlHost);
  397. $urlHost = $urlHostArr[0];
  398. $sessionId = $httpHost !== $urlHost && !$this->isValidForHost($urlHost) ? $this->getSessionId() : '';
  399. self::$urlHostCache[$urlHost] = $sessionId;
  400. }
  401. return $this->isValidForPath($urlPath) ? self::$urlHostCache[$urlHost] : $this->getSessionId();
  402. }
  403. /**
  404. * Check if session is valid for given hostname
  405. *
  406. * @param string $host
  407. * @return bool
  408. */
  409. public function isValidForHost($host)
  410. {
  411. $hostArr = explode(':', $host);
  412. $hosts = $this->_getHosts();
  413. return !empty($hosts[$hostArr[0]]);
  414. }
  415. /**
  416. * Check if session is valid for given path
  417. *
  418. * @param string $path
  419. * @return bool
  420. */
  421. public function isValidForPath($path)
  422. {
  423. $cookiePath = trim($this->getCookiePath(), '/') . '/';
  424. if ($cookiePath == '/') {
  425. return true;
  426. }
  427. $urlPath = trim($path, '/') . '/';
  428. return strpos($urlPath, $cookiePath) === 0;
  429. }
  430. /**
  431. * Register request host name as used with session
  432. *
  433. * @return $this
  434. */
  435. protected function _addHost()
  436. {
  437. $host = $this->request->getHttpHost();
  438. if (!$host) {
  439. return $this;
  440. }
  441. $hosts = $this->_getHosts();
  442. $hosts[$host] = true;
  443. $_SESSION[self::HOST_KEY] = $hosts;
  444. return $this;
  445. }
  446. /**
  447. * Get all host names where session was used
  448. *
  449. * @return array
  450. */
  451. protected function _getHosts()
  452. {
  453. return $_SESSION[self::HOST_KEY] ?? [];
  454. }
  455. /**
  456. * Clean all host names that were registered with session
  457. *
  458. * @return $this
  459. */
  460. protected function _cleanHosts()
  461. {
  462. unset($_SESSION[self::HOST_KEY]);
  463. return $this;
  464. }
  465. /**
  466. * Renew session id and update session cookie
  467. *
  468. * @return $this
  469. */
  470. public function regenerateId()
  471. {
  472. if (headers_sent()) {
  473. return $this;
  474. }
  475. if ($this->isSessionExists()) {
  476. // Regenerate the session
  477. session_regenerate_id();
  478. $newSessionId = session_id();
  479. $_SESSION['new_session_id'] = $newSessionId;
  480. // Set destroy timestamp
  481. $_SESSION['destroyed'] = time();
  482. // Write and close current session;
  483. session_commit();
  484. // Called after destroy()
  485. $oldSession = $_SESSION;
  486. // Start session with new session ID
  487. session_id($newSessionId);
  488. session_start();
  489. $_SESSION = $oldSession;
  490. // New session does not need them
  491. unset($_SESSION['destroyed']);
  492. unset($_SESSION['new_session_id']);
  493. } else {
  494. session_start();
  495. }
  496. $this->storage->init(isset($_SESSION) ? $_SESSION : []);
  497. if ($this->sessionConfig->getUseCookies()) {
  498. $this->clearSubDomainSessionCookie();
  499. }
  500. return $this;
  501. }
  502. /**
  503. * Expire the session cookie for sub domains
  504. *
  505. * @return void
  506. */
  507. protected function clearSubDomainSessionCookie()
  508. {
  509. foreach (array_keys($this->_getHosts()) as $host) {
  510. // Delete cookies with the same name for parent domains
  511. if ($this->sessionConfig->getCookieDomain() !== $host) {
  512. $metadata = $this->cookieMetadataFactory->createPublicCookieMetadata();
  513. $metadata->setPath($this->sessionConfig->getCookiePath());
  514. $metadata->setDomain($host);
  515. $metadata->setSecure($this->sessionConfig->getCookieSecure());
  516. $metadata->setHttpOnly($this->sessionConfig->getCookieHttpOnly());
  517. $this->cookieManager->deleteCookie($this->getName(), $metadata);
  518. }
  519. }
  520. }
  521. /**
  522. * Expire the session cookie
  523. *
  524. * Sends a session cookie with no value, and with an expiry in the past.
  525. *
  526. * @return void
  527. */
  528. public function expireSessionCookie()
  529. {
  530. if (!$this->sessionConfig->getUseCookies()) {
  531. return;
  532. }
  533. $metadata = $this->cookieMetadataFactory->createPublicCookieMetadata();
  534. $metadata->setPath($this->sessionConfig->getCookiePath());
  535. $metadata->setDomain($this->sessionConfig->getCookieDomain());
  536. $metadata->setSecure($this->sessionConfig->getCookieSecure());
  537. $metadata->setHttpOnly($this->sessionConfig->getCookieHttpOnly());
  538. $this->cookieManager->deleteCookie($this->getName(), $metadata);
  539. $this->clearSubDomainSessionCookie();
  540. }
  541. /**
  542. * Performs ini_set for all of the config options so they can be read by session_start
  543. *
  544. * @return void
  545. */
  546. private function initIniOptions()
  547. {
  548. $result = ini_set('session.use_only_cookies', '1');
  549. if ($result === false) {
  550. $error = error_get_last();
  551. throw new \InvalidArgumentException(
  552. sprintf('Failed to set ini option session.use_only_cookies to value 1. %s', $error['message'])
  553. );
  554. }
  555. foreach ($this->sessionConfig->getOptions() as $option => $value) {
  556. if ($option=='session.save_handler') {
  557. continue;
  558. } else {
  559. $result = ini_set($option, $value);
  560. if ($result === false) {
  561. $error = error_get_last();
  562. throw new \InvalidArgumentException(
  563. sprintf('Failed to set ini option "%s" to value "%s". %s', $option, $value, $error['message'])
  564. );
  565. }
  566. }
  567. }
  568. }
  569. }