mirror of
https://github.com/processwire/processwire.git
synced 2025-08-13 02:04:35 +02:00
Add PR #187 - add cookie SameSite support and settings (session and other cookies)
This commit is contained in:
@@ -62,6 +62,7 @@
|
||||
* @property string $sessionNameSecure Session name when on HTTPS. Used when the sessionCookieSecure option is enabled (default). When blank (default), it will assume sessionName + 's'. #pw-group-session
|
||||
* @property bool|int $sessionCookieSecure Use secure cookies when on HTTPS? When enabled, separate sessions will be maintained for HTTP vs. HTTPS. Good for security but tradeoff is login session may be lost when switching (default=1 or true). #pw-group-session
|
||||
* @property null|string $sessionCookieDomain Domain to use for sessions, which enables a session to work across subdomains, or NULL to disable (default/recommended). #pw-group-session
|
||||
* @property string $sessionCookieSameSite Cookie “SameSite” value for sessions - “Lax” (default) or “Strict”. (3.0.178+) #pw-group-session
|
||||
* @property bool|callable $sessionAllow Are sessions allowed? Typically boolean true, unless provided a callable function that returns boolean. See /wire/config.php for an example. #pw-group-session
|
||||
* @property int $sessionExpireSeconds How many seconds of inactivity before session expires? #pw-group-session
|
||||
* @property bool $sessionChallenge Should login sessions have a challenge key? (for extra security, recommended) #pw-group-session
|
||||
|
@@ -305,7 +305,18 @@ class Session extends Wire implements \IteratorAggregate {
|
||||
}
|
||||
}
|
||||
|
||||
@session_start();
|
||||
$options = array();
|
||||
$cookieSameSite = $this->sessionCookieSameSite();
|
||||
|
||||
if(PHP_VERSION_ID < 70300) {
|
||||
$cookiePath = ini_get('session.cookie_path');
|
||||
if(empty($cookiePath)) $cookiePath = '/';
|
||||
$options['cookie_path'] = "$cookiePath; SameSite=$cookieSameSite";
|
||||
} else {
|
||||
$options['cookie_samesite'] = $cookieSameSite;
|
||||
}
|
||||
|
||||
@session_start($options);
|
||||
|
||||
if(!empty($this->data)) {
|
||||
foreach($this->data as $key => $value) $this->set($key, $value);
|
||||
@@ -937,8 +948,16 @@ class Session extends Wire implements \IteratorAggregate {
|
||||
$this->set('_user', 'challenge', $challenge);
|
||||
$secure = $this->config->sessionCookieSecure ? (bool) $this->config->https : false;
|
||||
// set challenge cookie to last 30 days (should be longer than any session would feasibly last)
|
||||
setcookie(session_name() . self::challengeSuffix, $challenge, time()+60*60*24*30, '/',
|
||||
$this->config->sessionCookieDomain, $secure, true);
|
||||
$this->setCookie(
|
||||
session_name() . self::challengeSuffix,
|
||||
$challenge,
|
||||
time() + 60*60*24*30,
|
||||
'/',
|
||||
$this->config->sessionCookieDomain,
|
||||
$secure,
|
||||
true,
|
||||
$this->config->sessionCookieSameSite
|
||||
);
|
||||
}
|
||||
|
||||
if($this->config->sessionFingerprint) {
|
||||
@@ -947,7 +966,7 @@ class Session extends Wire implements \IteratorAggregate {
|
||||
}
|
||||
|
||||
$this->wire('user', $user);
|
||||
$this->get('CSRF')->resetAll();
|
||||
$this->CSRF()->resetAll();
|
||||
$this->loginSuccess($user);
|
||||
$fail = false;
|
||||
|
||||
@@ -1119,22 +1138,81 @@ class Session extends Wire implements \IteratorAggregate {
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a SetCookie response header
|
||||
*
|
||||
* @param string $name
|
||||
* @param string|null|false $value
|
||||
* @param int $expires
|
||||
* @param string $path
|
||||
* @param string|null $domain
|
||||
* @param bool $secure
|
||||
* @param bool $httponly
|
||||
* @param string $samesite One of 'Strict', 'Lax', 'None'
|
||||
* @return bool
|
||||
* @since 3.0.178
|
||||
*
|
||||
*/
|
||||
protected function setCookie($name, $value, $expires = 0, $path = '/', $domain = null, $secure = false, $httponly = false, $samesite = 'Lax') {
|
||||
|
||||
if(empty($path)) $path = '/';
|
||||
|
||||
$samesite = $this->sessionCookieSameSite($samesite);
|
||||
|
||||
if($samesite === 'None') $secure = true;
|
||||
|
||||
if(PHP_VERSION_ID < 70300) {
|
||||
return setcookie($name, $value, $expires, "$path; SameSite=$samesite", $domain, $secure, $httponly);
|
||||
}
|
||||
|
||||
// PHP 7.3+ supports $options array
|
||||
return setcookie($name, $value, array(
|
||||
'expires' => $expires,
|
||||
'path' => $path,
|
||||
'domain' => $domain,
|
||||
'secure' => $secure,
|
||||
'httponly' => $httponly,
|
||||
'samesite' => $samesite,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Remove all cookies used by the session
|
||||
*
|
||||
*/
|
||||
protected function removeCookies() {
|
||||
$sessionName = session_name();
|
||||
$challengeName = $sessionName . self::challengeSuffix;
|
||||
$time = time() - 42000;
|
||||
$domain = $this->config->sessionCookieDomain;
|
||||
$secure = $this->config->sessionCookieSecure ? (bool) $this->config->https : false;
|
||||
$samesite = $this->sessionCookieSameSite();
|
||||
|
||||
if(isset($_COOKIE[$sessionName])) {
|
||||
setcookie($sessionName, '', $time, '/', $this->config->sessionCookieDomain, $secure, true);
|
||||
$this->setCookie($sessionName, '', $time, '/', $domain, $secure, true, $samesite);
|
||||
}
|
||||
if(isset($_COOKIE[$sessionName . self::challengeSuffix])) {
|
||||
setcookie($sessionName . self::challengeSuffix, '', $time, '/', $this->config->sessionCookieDomain, $secure, true);
|
||||
|
||||
if(isset($_COOKIE[$challengeName])) {
|
||||
$this->setCookie($challengeName, '', $time, '/', $domain, $secure, true, $samesite);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get 'SameSite' value for session cookie
|
||||
*
|
||||
* @param string|null $value
|
||||
* @return string
|
||||
* @since 3.0.178
|
||||
*
|
||||
*/
|
||||
protected function sessionCookieSameSite($value = null) {
|
||||
$samesite = $value === null ? $this->config->sessionCookieSameSite : $value;
|
||||
$samesite = empty($samesite) ? 'Lax' : ucfirst(strtolower($samesite));
|
||||
if(!in_array($samesite, array('Strict', 'Lax', 'None'), true)) $samesite = 'Lax';
|
||||
return $samesite;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the names of all cookies managed by Session
|
||||
*
|
||||
|
@@ -21,7 +21,7 @@
|
||||
* @property array|string[] $urlSegments Retrieve all URL segments (array). This requires url segments are enabled on the template of the requested page. You can turn it on or off under the url tab when editing a template. #pw-group-URL-segments
|
||||
* @property WireInputData $post POST variables
|
||||
* @property WireInputData $get GET variables
|
||||
* @property WireInputData $cookie COOKIE variables
|
||||
* @property WireInputDataCookie $cookie COOKIE variables
|
||||
* @property WireInputData $whitelist Whitelisted variables
|
||||
* @property int $pageNum Current page number (where 1 is first) #pw-group-URLs
|
||||
* @property string $urlSegmentsStr String of current URL segments, separated by slashes, i.e. a/b/c #pw-internal
|
||||
|
@@ -56,6 +56,11 @@
|
||||
* // Specify true, false, or null to auto-detect (uses true for cookies set when HTTPS).
|
||||
* 'secure' => null,
|
||||
*
|
||||
* // Cookie SameSite value: When set to “Lax” cookies are preserved on GET requests to this site
|
||||
* // originated from external links. May also be “Strict” or “None”. The 'secure' option is
|
||||
* // required for “None”. Default value is “Lax”. Available in PW 3.0.178+.
|
||||
* 'samesite' => 'Lax',
|
||||
*
|
||||
* // Make cookies accessible by HTTP (ProcessWire/PHP) only?
|
||||
* // When true, cookie is http/server-side only and not visible to client-side JS code.
|
||||
* 'httponly' => false,
|
||||
@@ -95,6 +100,7 @@ class WireInputDataCookie extends WireInputData {
|
||||
'domain' => null,
|
||||
'secure' => null,
|
||||
'httponly' => false,
|
||||
'samesite' => 'Lax',
|
||||
'fallback' => true,
|
||||
);
|
||||
|
||||
@@ -340,6 +346,7 @@ class WireInputDataCookie extends WireInputData {
|
||||
* - `path` (string|null): Cookie path/URL or null for PW installation’s root URL. (default=null)
|
||||
* - `secure` (bool|null): Transmit cookies only over secure HTTPS connection? Specify true or false, or use null to auto-detect,
|
||||
* which uses true for cookies set when HTTPS is detected. (default=null)
|
||||
* - `samesite` (string): SameSite value, one of 'Lax' (default), 'Strict' or 'None'. (default='Lax') 3.0.178+
|
||||
* - `httponly` (bool): When true, cookie is visible to PHP/ProcessWire only and not visible to client-side JS code. (default=false)
|
||||
* - `fallback` (bool): If set cookie fails (perhaps due to output already sent), attempt to set at beginning of next request? (default=true)
|
||||
* - `domain` (string|bool|null): Cookie domain, specify one of the following: `null` or blank string for current hostname [default],
|
||||
@@ -351,8 +358,7 @@ class WireInputDataCookie extends WireInputData {
|
||||
*/
|
||||
public function setCookie($key, $value, array $options) {
|
||||
|
||||
/** @var Config $config */
|
||||
$config = $this->wire('config');
|
||||
$config = $this->wire()->config;
|
||||
$options = array_merge($this->defaultOptions, $config->cookieOptions, $this->options, $options);
|
||||
|
||||
$path = $options['path'] === null || $options['path'] === true ? $config->urls->root : $options['path'];
|
||||
@@ -361,6 +367,13 @@ class WireInputDataCookie extends WireInputData {
|
||||
$domain = $options['domain'];
|
||||
$remove = $value === null;
|
||||
$expires = null;
|
||||
$samesite = $options['samesite'] ? ucfirst(strtolower($options['samesite'])) : 'Lax';
|
||||
|
||||
if($samesite === 'None') {
|
||||
$secure = true;
|
||||
} else if(!in_array($samesite, array('Lax', 'Strict', 'None'), true)) {
|
||||
$samesite = 'Lax';
|
||||
}
|
||||
|
||||
if(!empty($options['expire'])) {
|
||||
if(is_string($options['expire']) && !ctype_digit($options['expire'])) {
|
||||
@@ -395,11 +408,22 @@ class WireInputDataCookie extends WireInputData {
|
||||
if($remove) list($value, $expires) = array('', 1);
|
||||
|
||||
// set the cookie
|
||||
$result = setcookie($key, $value, $expires, $path, $domain, $secure, $httponly);
|
||||
if(PHP_VERSION_ID < 70300) {
|
||||
$result = setcookie($key, $value, $expires, "$path; SameSite=$samesite", $domain, $secure, $httponly);
|
||||
} else {
|
||||
$result = setcookie($key, $value, array(
|
||||
'expires' => $expires,
|
||||
'path' => $path,
|
||||
'domain' => $domain,
|
||||
'secure' => $secure,
|
||||
'httponly' => $httponly,
|
||||
'samesite' => $samesite,
|
||||
));
|
||||
}
|
||||
|
||||
if($result === false && $options['fallback']) {
|
||||
// output must have already started, set at construct on next request
|
||||
$this->wire('session')->setFor($this, $key, $value);
|
||||
$this->wire()->session->setFor($this, $key, $value);
|
||||
}
|
||||
|
||||
if($remove) {
|
||||
|
Reference in New Issue
Block a user