mirror of
https://github.com/processwire/processwire.git
synced 2025-08-08 15:57:01 +02:00
Add PR #187 - add cookie SameSite support and settings (session and other cookies)
This commit is contained in:
@@ -420,6 +420,25 @@ $config->sessionCookieSecure = 1;
|
||||
*/
|
||||
$config->sessionCookieDomain = null;
|
||||
|
||||
/**
|
||||
* Cookie “SameSite” value for sessions - “Lax” (default) or “Strict”
|
||||
*
|
||||
* - `Lax`: The session cookie will be sent along with the GET requests initiated by third party website.
|
||||
* This ensures an existing session on this site is maintained when clicking to it from another site.
|
||||
*
|
||||
* - `Strict`: The session cookie will not be sent along with requests initiated by third party websites.
|
||||
* If user already has a login session on this site, it won’t be recognized when clicking from another
|
||||
* site to this one.
|
||||
*
|
||||
* The default/recommended value is `Lax`.
|
||||
*
|
||||
* @var string
|
||||
* @since 3.0.178
|
||||
* @see https://www.php.net/manual/en/session.configuration.php#ini.session.cookie-samesite
|
||||
*
|
||||
*/
|
||||
$config->sessionCookieSameSite = 'Lax';
|
||||
|
||||
/**
|
||||
* Number of session history entries to record.
|
||||
*
|
||||
@@ -1020,6 +1039,7 @@ $config->wireInputLazy = false;
|
||||
* #property int age Max age of cookies in seconds or 0 to expire with session (3600=1hr, 86400=1day, 604800=1week, 2592000=30days, etc.)
|
||||
* #property string|null Cookie path or null for PW installation’s root URL (default=null).
|
||||
* #property string|null|bool domain Cookie domain: null for current hostname, true for all subdomains of current domain, domain.com for domain and all subdomains, www.domain.com for www subdomain.
|
||||
* #property string samesite When set to “Lax” cookies are preserved on GET requests to this site originated from external links. May also be 'Strict' or 'None' ('secure' option required for 'None'). 3.0.178+
|
||||
* #property bool|null secure Transmit cookies only over secure HTTPS connection? (true, false, or null to auto-detect, using true option for cookies set when HTTPS is active).
|
||||
* #property bool httponly When true, cookie is http/server-side and not visible to JS code in most browsers.
|
||||
*
|
||||
@@ -1032,6 +1052,7 @@ $config->cookieOptions = array(
|
||||
'path' => null, // Cookie path/URL or null for PW installation’s root URL (default=null).
|
||||
'domain' => null, // Cookie domain: null for current hostname, true for all subdomains of current domain, domain.com for domain and all subdomains, www.domain.com for www subdomain.
|
||||
'secure' => null, // Transmit cookies only over secure HTTPS connection? (true, false, or null to auto-detect, substituting true for cookies set when HTTPS is active).
|
||||
'samesite' => 'Lax', // When set to “Lax” cookies are preserved on GET requests to this site originated from external links. May also be 'Strict' or 'None' ('secure' option required for 'None').
|
||||
'httponly' => false, // When true, cookie is http/server-side only and not visible to client-side JS code.
|
||||
'fallback' => true, // If set cookie fails (perhaps due to output already sent), attempt to set at beginning of next request? (default=true)
|
||||
);
|
||||
|
@@ -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) {
|
||||
|
@@ -179,7 +179,24 @@ class SessionHandlerDB extends WireSessionHandler implements Module, Configurabl
|
||||
$query = $database->prepare("DELETE FROM `$table` WHERE id=:id");
|
||||
$query->execute(array(":id" => $id));
|
||||
$secure = $config->sessionCookieSecure ? (bool) $config->https : false;
|
||||
setcookie(session_name(), '', time()-42000, '/', $config->sessionCookieDomain, $secure, true);
|
||||
$expires = time() - 42000;
|
||||
$samesite = $config->sessionCookieSameSite ? ucfirst(strtolower($config->sessionCookieSameSite)) : 'Lax';
|
||||
|
||||
if($samesite === 'None') $secure = true;
|
||||
|
||||
if(PHP_VERSION_ID < 70300) {
|
||||
setcookie(session_name(), '', $expires, "/; SameSite=$samesite", $config->sessionCookieDomain, $secure, true);
|
||||
} else {
|
||||
setcookie(session_name(), '', array(
|
||||
'expires' => $expires,
|
||||
'path' => '/',
|
||||
'domain' => $config->sessionCookieDomain,
|
||||
'secure' => $secure,
|
||||
'httponly' => true,
|
||||
'samesite' => $samesite
|
||||
));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user