Session.php 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  1. <?php
  2. /**
  3. * @package Grav\Framework\Session
  4. *
  5. * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
  6. * @license MIT License; see LICENSE file for details.
  7. */
  8. namespace Grav\Framework\Session;
  9. use Grav\Common\User\Interfaces\UserInterface;
  10. use Grav\Framework\Session\Exceptions\SessionException;
  11. /**
  12. * Class Session
  13. * @package Grav\Framework\Session
  14. */
  15. class Session implements SessionInterface
  16. {
  17. /** @var array */
  18. protected $options = [];
  19. /** @var bool */
  20. protected $started = false;
  21. /** @var Session */
  22. protected static $instance;
  23. /**
  24. * @inheritdoc
  25. */
  26. public static function getInstance()
  27. {
  28. if (null === self::$instance) {
  29. throw new \RuntimeException("Session hasn't been initialized.", 500);
  30. }
  31. return self::$instance;
  32. }
  33. public function __construct(array $options = [])
  34. {
  35. // Session is a singleton.
  36. if (\PHP_SAPI === 'cli') {
  37. self::$instance = $this;
  38. return;
  39. }
  40. if (null !== self::$instance) {
  41. throw new \RuntimeException('Session has already been initialized.', 500);
  42. }
  43. // Destroy any existing sessions started with session.auto_start
  44. if ($this->isSessionStarted()) {
  45. session_unset();
  46. session_destroy();
  47. }
  48. // Set default options.
  49. $options += [
  50. 'cache_limiter' => 'nocache',
  51. 'use_trans_sid' => 0,
  52. 'use_cookies' => 1,
  53. 'lazy_write' => 1,
  54. 'use_strict_mode' => 1
  55. ];
  56. $this->setOptions($options);
  57. session_register_shutdown();
  58. self::$instance = $this;
  59. }
  60. /**
  61. * @inheritdoc
  62. */
  63. public function getId()
  64. {
  65. return session_id();
  66. }
  67. /**
  68. * @inheritdoc
  69. */
  70. public function setId($id)
  71. {
  72. session_id($id);
  73. return $this;
  74. }
  75. /**
  76. * @inheritdoc
  77. */
  78. public function getName()
  79. {
  80. return session_name();
  81. }
  82. /**
  83. * @inheritdoc
  84. */
  85. public function setName($name)
  86. {
  87. session_name($name);
  88. return $this;
  89. }
  90. /**
  91. * @inheritdoc
  92. */
  93. public function setOptions(array $options)
  94. {
  95. if (headers_sent() || \PHP_SESSION_ACTIVE === session_status()) {
  96. return;
  97. }
  98. $allowedOptions = [
  99. 'save_path' => true,
  100. 'name' => true,
  101. 'save_handler' => true,
  102. 'gc_probability' => true,
  103. 'gc_divisor' => true,
  104. 'gc_maxlifetime' => true,
  105. 'serialize_handler' => true,
  106. 'cookie_lifetime' => true,
  107. 'cookie_path' => true,
  108. 'cookie_domain' => true,
  109. 'cookie_secure' => true,
  110. 'cookie_httponly' => true,
  111. 'use_strict_mode' => true,
  112. 'use_cookies' => true,
  113. 'use_only_cookies' => true,
  114. 'referer_check' => true,
  115. 'cache_limiter' => true,
  116. 'cache_expire' => true,
  117. 'use_trans_sid' => true,
  118. 'trans_sid_tags' => true,
  119. 'trans_sid_hosts' => true,
  120. 'sid_length' => true,
  121. 'sid_bits_per_character' => true,
  122. 'upload_progress.enabled' => true,
  123. 'upload_progress.cleanup' => true,
  124. 'upload_progress.prefix' => true,
  125. 'upload_progress.name' => true,
  126. 'upload_progress.freq' => true,
  127. 'upload_progress.min-freq' => true,
  128. 'lazy_write' => true
  129. ];
  130. foreach ($options as $key => $value) {
  131. if (\is_array($value)) {
  132. // Allow nested options.
  133. foreach ($value as $key2 => $value2) {
  134. $ckey = "{$key}.{$key2}";
  135. if (isset($value2, $allowedOptions[$ckey])) {
  136. $this->setOption($ckey, $value2);
  137. }
  138. }
  139. } elseif (isset($value, $allowedOptions[$key])) {
  140. $this->setOption($key, $value);
  141. }
  142. }
  143. }
  144. /**
  145. * @inheritdoc
  146. */
  147. public function start($readonly = false)
  148. {
  149. if (\PHP_SAPI === 'cli') {
  150. return $this;
  151. }
  152. $sessionName = session_name();
  153. $sessionExists = isset($_COOKIE[$sessionName]);
  154. // Protection against invalid session cookie names throwing exception: http://php.net/manual/en/function.session-id.php#116836
  155. if ($sessionExists && !preg_match('/^[-,a-zA-Z0-9]{1,128}$/', $_COOKIE[$sessionName])) {
  156. unset($_COOKIE[$sessionName]);
  157. $sessionExists = false;
  158. }
  159. $options = $this->options;
  160. if ($readonly) {
  161. $options['read_and_close'] = '1';
  162. }
  163. $success = @session_start($options);
  164. $user = $success ? $this->__get('user') : null;
  165. if (!$success) {
  166. $last = error_get_last();
  167. $error = $last ? $last['message'] : 'Unknown error';
  168. throw new SessionException('Failed to start session: ' . $error, 500);
  169. }
  170. $this->started = true;
  171. if ($user && (!$user instanceof UserInterface || !$user->isValid())) {
  172. $this->invalidate();
  173. throw new SessionException('Invalid User object, session destroyed.', 500);
  174. }
  175. // Extend the lifetime of the session.
  176. if ($sessionExists) {
  177. $params = session_get_cookie_params();
  178. setcookie(
  179. $sessionName,
  180. session_id(),
  181. time() + $params['lifetime'],
  182. $params['path'],
  183. $params['domain'],
  184. $params['secure'],
  185. $params['httponly']
  186. );
  187. }
  188. return $this;
  189. }
  190. /**
  191. * @inheritdoc
  192. */
  193. public function invalidate()
  194. {
  195. $params = session_get_cookie_params();
  196. setcookie(
  197. session_name(),
  198. '',
  199. time() - 42000,
  200. $params['path'],
  201. $params['domain'],
  202. $params['secure'],
  203. $params['httponly']
  204. );
  205. if ($this->isSessionStarted()) {
  206. session_unset();
  207. session_destroy();
  208. }
  209. $this->started = false;
  210. return $this;
  211. }
  212. /**
  213. * @inheritdoc
  214. */
  215. public function close()
  216. {
  217. if ($this->started) {
  218. session_write_close();
  219. }
  220. $this->started = false;
  221. return $this;
  222. }
  223. /**
  224. * @inheritdoc
  225. */
  226. public function clear()
  227. {
  228. session_unset();
  229. return $this;
  230. }
  231. /**
  232. * @inheritdoc
  233. */
  234. public function getAll()
  235. {
  236. return $_SESSION;
  237. }
  238. /**
  239. * @inheritdoc
  240. */
  241. public function getIterator()
  242. {
  243. return new \ArrayIterator($_SESSION);
  244. }
  245. /**
  246. * @inheritdoc
  247. */
  248. public function isStarted()
  249. {
  250. return $this->started;
  251. }
  252. /**
  253. * @inheritdoc
  254. */
  255. public function __isset($name)
  256. {
  257. return isset($_SESSION[$name]);
  258. }
  259. /**
  260. * @inheritdoc
  261. */
  262. public function __get($name)
  263. {
  264. return $_SESSION[$name] ?? null;
  265. }
  266. /**
  267. * @inheritdoc
  268. */
  269. public function __set($name, $value)
  270. {
  271. $_SESSION[$name] = $value;
  272. }
  273. /**
  274. * @inheritdoc
  275. */
  276. public function __unset($name)
  277. {
  278. unset($_SESSION[$name]);
  279. }
  280. /**
  281. * http://php.net/manual/en/function.session-status.php#113468
  282. * Check if session is started nicely.
  283. * @return bool
  284. */
  285. protected function isSessionStarted()
  286. {
  287. return \PHP_SAPI !== 'cli' ? \PHP_SESSION_ACTIVE === session_status() : false;
  288. }
  289. /**
  290. * @param string $key
  291. * @param mixed $value
  292. */
  293. protected function setOption($key, $value)
  294. {
  295. if (!\is_string($value)) {
  296. if (\is_bool($value)) {
  297. $value = $value ? '1' : '0';
  298. } else {
  299. $value = (string)$value;
  300. }
  301. }
  302. $this->options[$key] = $value;
  303. ini_set("session.{$key}", $value);
  304. }
  305. }