1701 lines
47 KiB
PHP
1701 lines
47 KiB
PHP
<?php namespace ProcessWire;
|
||
|
||
/**
|
||
* ProcessWire Session
|
||
*
|
||
* Start a session with login/logout capability
|
||
*
|
||
* #pw-summary Maintains sessions in ProcessWire, authentication, persistent variables, notices and redirects.
|
||
* #pw-order-groups redirects,get,set,remove,info,notices,authentication,advanced,hooker
|
||
*
|
||
* This should be used instead of the $_SESSION superglobal, though the $_SESSION superglobal can still be
|
||
* used, but it's in a different namespace than this. A value set in $_SESSION won't appear in $session
|
||
* and likewise a value set in $session won't appear in $_SESSION. It's also good to use this class
|
||
* over the $_SESSION superglobal just in case we ever need to replace PHP's session handling in the future.
|
||
*
|
||
* ProcessWire 3.x, Copyright 2022 by Ryan Cramer
|
||
* https://processwire.com
|
||
*
|
||
* @see https://processwire.com/api/ref/session/ Session documentation
|
||
*
|
||
* @method User login() login($name, $pass, $force = false) Login the user identified by $name and authenticated by $pass. Returns the user object on successful login or null on failure.
|
||
* @method Session logout() logout() Logout the current user, and clear all session variables.
|
||
* @method void redirect() redirect($url, $http301 = true) Redirect this session to the specified URL.
|
||
* @method void init() Initialize session (called automatically by constructor) #pw-hooker
|
||
* @method bool authenticate(User $user, $pass) #pw-hooker
|
||
* @method bool isValidSession($userID) #pw-hooker
|
||
* @method bool allowLoginAttempt($name) #pw-hooker
|
||
* @method bool allowLogin($name, User $user = null) #pw-hooker
|
||
* @method void loginSuccess(User $user) #pw-hooker
|
||
* @method void loginFailure($name, $reason) #pw-hooker
|
||
* @method void logoutSuccess(User $user) #pw-hooker
|
||
*
|
||
* @property SessionCSRF $CSRF
|
||
*
|
||
* Expected $config variables include:
|
||
* ===================================
|
||
* string $config->sessionName Name of session on http
|
||
* string $config->sessionNameSecure Name of session on https
|
||
* int $config->sessionExpireSeconds Number of seconds of inactivity before session expires
|
||
* bool $config->sessionChallenge True if a separate challenge cookie should be used for validating sessions
|
||
* bool $config->sessionFingerprint True if a fingerprint should be kept of the user's IP & user agent to validate sessions
|
||
* bool $config->sessionCookieSecure Use secure cookies or session? (default=true)
|
||
*
|
||
* @todo enable login/forceLogin to recognize non-HTTP use of login, when no session needs to be maintained
|
||
* @todo add a default $config->apiUser to be used when non-HTTP/bootstrap usage
|
||
*
|
||
*/
|
||
|
||
class Session extends Wire implements \IteratorAggregate {
|
||
|
||
/**
|
||
* Fingerprint bitmask: Use remote addr (recommended)
|
||
*
|
||
*/
|
||
const fingerprintRemoteAddr = 2;
|
||
|
||
/**
|
||
* Fingerprint bitmask: Use client provided addr
|
||
*
|
||
*/
|
||
const fingerprintClientAddr = 4;
|
||
|
||
/**
|
||
* Fingerprint bitmask: Use user agent (recommended)
|
||
*
|
||
*/
|
||
const fingerprintUseragent = 8;
|
||
|
||
/**
|
||
* Fingerprint bitmask: Use “accept” content-types header
|
||
*
|
||
* @since 3.0.159
|
||
*
|
||
*/
|
||
const fingerprintAccept = 16;
|
||
|
||
/**
|
||
* Suffix applied to challenge cookies
|
||
*
|
||
* @since 3.0.141
|
||
*
|
||
*/
|
||
const challengeSuffix = '_challenge';
|
||
|
||
/**
|
||
* Reference to ProcessWire $config object
|
||
*
|
||
* For convenience, since our __get() does not reference the Fuel, unlike other Wire derived classes.
|
||
*
|
||
*/
|
||
protected $config;
|
||
|
||
/**
|
||
* Instance of the SessionCSRF protection class, instantiated when requested from $session->CSRF.
|
||
*
|
||
*/
|
||
protected $CSRF = null;
|
||
|
||
/**
|
||
* Set to true when maintenance should be skipped
|
||
*
|
||
* @var bool
|
||
*
|
||
*/
|
||
protected $skipMaintenance = false;
|
||
|
||
/**
|
||
* Has the Session::init() method been called?
|
||
*
|
||
* @var bool
|
||
*
|
||
*/
|
||
protected $sessionInit = false;
|
||
|
||
/**
|
||
* Are sessions allowed?
|
||
*
|
||
* @var bool|null
|
||
*
|
||
*/
|
||
protected $sessionAllow = null;
|
||
|
||
/**
|
||
* Instance of custom session handler module, when in use (null when not)
|
||
*
|
||
* @var WireSessionHandler|null
|
||
*
|
||
*/
|
||
protected $sessionHandler = null;
|
||
|
||
/**
|
||
* Name of key/index within $_SESSION where PW keeps its session data
|
||
*
|
||
* @var string
|
||
*
|
||
*/
|
||
protected $sessionKey = '';
|
||
|
||
/**
|
||
* Data storage when no session initialized
|
||
*
|
||
* @var array
|
||
*
|
||
*/
|
||
protected $data = array();
|
||
|
||
/**
|
||
* True if there is an external session provider
|
||
*
|
||
* @var bool
|
||
*
|
||
*/
|
||
protected $isExternal = false;
|
||
|
||
/**
|
||
* True if this is a secondary instance of ProcessWire
|
||
*
|
||
* @var bool
|
||
*
|
||
*/
|
||
protected $isSecondary = false;
|
||
|
||
/**
|
||
* Start the session and set the current User if a session is active
|
||
*
|
||
* Assumes that you have already performed all session-specific ini_set() and session_name() calls
|
||
*
|
||
* @param ProcessWire $wire
|
||
* @throws WireException
|
||
*
|
||
*/
|
||
public function __construct(ProcessWire $wire) {
|
||
|
||
$wire->wire($this);
|
||
$this->config = $wire->wire()->config;
|
||
$this->sessionKey = $this->className();
|
||
|
||
$instanceID = $wire->getProcessWireInstanceID();
|
||
if($instanceID) {
|
||
$this->isSecondary = true;
|
||
$this->sessionKey .= $instanceID;
|
||
}
|
||
|
||
$user = null;
|
||
$sessionAllow = $this->config->sessionAllow;
|
||
|
||
if(is_null($sessionAllow)) {
|
||
$sessionAllow = true;
|
||
} else if(is_bool($sessionAllow)) {
|
||
// okay, keep as-is
|
||
} else if(is_callable($sessionAllow)) {
|
||
// call function that returns boolean
|
||
$sessionAllow = call_user_func_array($sessionAllow, array($this));
|
||
if(!is_bool($sessionAllow)) throw new WireException("\$config->sessionAllow callable must return boolean");
|
||
} else {
|
||
$sessionAllow = true;
|
||
}
|
||
|
||
$this->sessionAllow = $sessionAllow;
|
||
|
||
if($sessionAllow) {
|
||
$this->init();
|
||
if(empty($_SESSION[$this->sessionKey])) $_SESSION[$this->sessionKey] = array();
|
||
$userID = $this->get('_user', 'id');
|
||
if($userID) {
|
||
if($this->isValidSession($userID)) {
|
||
$user = $this->wire('users')->get($userID);
|
||
} else {
|
||
$this->logout();
|
||
}
|
||
}
|
||
}
|
||
|
||
if(!$user || !$user->id) $user = $this->wire('users')->getGuestUser();
|
||
$this->wire('users')->setCurrentUser($user);
|
||
|
||
if($sessionAllow) $this->wakeupNotices();
|
||
$this->setTrackChanges(true);
|
||
}
|
||
|
||
/**
|
||
* Are session cookie(s) present?
|
||
*
|
||
* #pw-group-info
|
||
*
|
||
* @param bool $checkLogin Specify true to check instead for challenge cookie (which indicates login may be active).
|
||
* @return bool Returns true if session cookie present, false if not.
|
||
*
|
||
*/
|
||
public function hasCookie($checkLogin = false) {
|
||
if($this->config->https && $this->config->sessionCookieSecure) {
|
||
$name = $this->config->sessionNameSecure;
|
||
if(!$name) $name = $this->config->sessionName . 's';
|
||
} else {
|
||
$name = $this->config->sessionName;
|
||
}
|
||
if($checkLogin) $name .= self::challengeSuffix;
|
||
return !empty($_COOKIE[$name]);
|
||
}
|
||
|
||
/**
|
||
* Is a session login cookie present?
|
||
*
|
||
* This only indicates the user was likely logged in at some point, and may not indicate an active login.
|
||
* This method is more self describing version of `$session->hasCookie(true);`
|
||
*
|
||
* #pw-group-info
|
||
*
|
||
* @return bool
|
||
* @since 3.0.175
|
||
*
|
||
*/
|
||
public function hasLoginCookie() {
|
||
return $this->hasCookie(true);
|
||
}
|
||
|
||
/**
|
||
* Start the session
|
||
*
|
||
* Provided here in any case anything wants to hook in before session_start()
|
||
* is called to provide an alternate save handler.
|
||
*
|
||
* #pw-hooker
|
||
*
|
||
*/
|
||
public function ___init() {
|
||
|
||
if($this->sessionInit || !$this->sessionAllow) return;
|
||
if(!$this->config->sessionName) return;
|
||
$this->sessionInit = true;
|
||
|
||
if(function_exists("\\session_status")) {
|
||
// abort session init if there is already a session active
|
||
// note: there is no session_status() function prior to PHP 5.4
|
||
if(session_status() === PHP_SESSION_ACTIVE) {
|
||
// use a more unique sessionKey when there is an external session provider
|
||
$this->isExternal = true;
|
||
$this->sessionKey = str_replace($this->className(), 'ProcessWire', $this->sessionKey);
|
||
return;
|
||
}
|
||
}
|
||
|
||
if($this->config->https && $this->config->sessionCookieSecure) {
|
||
ini_set('session.cookie_secure', 1); // #1264
|
||
if($this->config->sessionNameSecure) {
|
||
session_name($this->config->sessionNameSecure);
|
||
} else {
|
||
session_name($this->config->sessionName . 's');
|
||
}
|
||
} else {
|
||
session_name($this->config->sessionName);
|
||
}
|
||
|
||
ini_set('session.use_cookies', true);
|
||
ini_set('session.use_only_cookies', 1);
|
||
ini_set('session.cookie_httponly', 1);
|
||
ini_set('session.gc_maxlifetime', $this->config->sessionExpireSeconds);
|
||
|
||
if($this->config->sessionCookieDomain) {
|
||
ini_set('session.cookie_domain', $this->config->sessionCookieDomain);
|
||
}
|
||
|
||
if(ini_get('session.save_handler') == 'files') {
|
||
if(ini_get('session.gc_probability') == 0) {
|
||
// Some debian distros replace PHP's gc without fully implementing it,
|
||
// which results in broken garbage collection if the save_path is set.
|
||
// As a result, we avoid setting the save_path when this is detected.
|
||
} else {
|
||
ini_set("session.save_path", rtrim($this->config->paths->sessions, '/'));
|
||
}
|
||
}
|
||
|
||
$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);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Checks if the session is valid based on a challenge cookie and fingerprint
|
||
*
|
||
* These items may be disabled at the config level, in which case this method always returns true
|
||
*
|
||
* #pw-hooker
|
||
*
|
||
* @param int $userID
|
||
* @return bool
|
||
*
|
||
*/
|
||
protected function ___isValidSession($userID) {
|
||
|
||
$valid = true;
|
||
$reason = '';
|
||
$sessionName = session_name();
|
||
|
||
// check challenge cookie
|
||
if($this->config->sessionChallenge) {
|
||
$cookieName = $sessionName . self::challengeSuffix;
|
||
if(empty($_COOKIE[$cookieName]) || ($this->get('_user', 'challenge') != $_COOKIE[$cookieName])) {
|
||
$valid = false;
|
||
$reason = "Error: Invalid challenge value";
|
||
}
|
||
}
|
||
|
||
// check fingerprint
|
||
if(!$this->isValidFingerprint()) {
|
||
$reason = "Error: Session fingerprint changed (IP address or useragent)";
|
||
$valid = false;
|
||
}
|
||
|
||
// check session expiration
|
||
if($this->config->sessionExpireSeconds) {
|
||
$ts = (int) $this->get('_user', 'ts');
|
||
if($ts < (time() - $this->config->sessionExpireSeconds)) {
|
||
// session time expired
|
||
$valid = false;
|
||
$this->error($this->_('Session timed out'));
|
||
$reason = "Session timed out (session older than {$this->config->sessionExpireSeconds} seconds)";
|
||
}
|
||
}
|
||
|
||
if($valid) {
|
||
// if valid, update last request time
|
||
$this->set('_user', 'ts', time());
|
||
|
||
} else if($reason && $userID && $userID != $this->wire('config')->guestUserPageID) {
|
||
// otherwise log the invalid session
|
||
$user = $this->wire('users')->get((int) $userID);
|
||
if($user && $user->id) $reason = "User '$user->name' - $reason";
|
||
$reason .= " (IP: " . $this->getIP() . ")";
|
||
$this->log($reason);
|
||
}
|
||
|
||
return $valid;
|
||
}
|
||
|
||
/**
|
||
* Returns whether or not the current session fingerprint is valid
|
||
*
|
||
* @return bool
|
||
*
|
||
*/
|
||
protected function isValidFingerprint() {
|
||
$fingerprint = $this->getFingerprint();
|
||
if($fingerprint === false) return true; // fingerprints off
|
||
if($fingerprint !== $this->get('_user', 'fingerprint')) return false;
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Generate a session fingerprint
|
||
*
|
||
* If the `$mode` argument is omitted, the mode is pulled from `$config->sessionFingerprint`.
|
||
* If using the mode argument, specify one of the following:
|
||
*
|
||
* - 2: Remote IP
|
||
* - 4: Forwarded/client IP (can be spoofed)
|
||
* - 8: Useragent
|
||
* - 16: Accept header
|
||
*
|
||
* To use the custom `$mode` settings above, select one or more of those you want
|
||
* to fingerprint, note the numbers, and determine the `$mode` like this:
|
||
* ~~~~~~
|
||
* // to fingerprint just remote IP
|
||
* $mode = 2;
|
||
*
|
||
* // to fingerprint remote IP and useragent:
|
||
* $mode = 2 | 8;
|
||
*
|
||
* // to fingerprint remote IP, useragent and accept header:
|
||
* $mode = 2 | 8 | 16;
|
||
* ~~~~~~
|
||
* If using fingerprint in an environment where the user’s IP address may
|
||
* change during the session, you should fingerprint only the useragent
|
||
* and/or accept header, or disable fingerprinting.
|
||
*
|
||
* If using fingerprint with an AWS load balancer, you should use one of
|
||
* the options that uses the “client IP” rather than the “remote IP”,
|
||
* fingerprint only useragent and/or accept header, or disable fingerprinting.
|
||
*
|
||
* #pw-internal
|
||
*
|
||
* @param int|bool|null $mode Optionally specify fingerprint mode (default=$config->sessionFingerprint)
|
||
* @param bool $debug Return non-hashed fingerprint for debugging purposes? (default=false)
|
||
* @return bool|string Returns false if fingerprints not enabled. Returns string if enabled.
|
||
*
|
||
*/
|
||
public function getFingerprint($mode = null, $debug = false) {
|
||
|
||
$debugInfo = array();
|
||
$useFingerprint = $mode === null ? $this->config->sessionFingerprint : $mode;
|
||
|
||
if(!$useFingerprint) return false;
|
||
|
||
if($useFingerprint === true || $useFingerprint === 1 || $useFingerprint === "1") {
|
||
// default (boolean true or int 1)
|
||
$useFingerprint = self::fingerprintRemoteAddr | self::fingerprintUseragent;
|
||
if($debug) $debugInfo[] = 'default';
|
||
}
|
||
|
||
$fingerprint = '';
|
||
|
||
if($useFingerprint & self::fingerprintRemoteAddr) {
|
||
$fingerprint .= $this->getIP(true);
|
||
if($debug) $debugInfo[] = 'remote-addr';
|
||
}
|
||
|
||
if($useFingerprint & self::fingerprintClientAddr) {
|
||
$fingerprint .= $this->getIP(false, 2);
|
||
if($debug) $debugInfo[] = 'client-addr';
|
||
}
|
||
|
||
if($useFingerprint & self::fingerprintUseragent) {
|
||
$fingerprint .= isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '';
|
||
if($debug) $debugInfo[] = 'useragent';
|
||
}
|
||
|
||
if($useFingerprint & self::fingerprintAccept) {
|
||
$fingerprint .= isset($_SERVER['HTTP_ACCEPT']) ? $_SERVER['HTTP_ACCEPT'] : '';
|
||
if($debug) $debugInfo[] = 'accept';
|
||
}
|
||
|
||
if($debug) {
|
||
$fingerprint = implode(',', $debugInfo) . ': ' . $fingerprint;
|
||
} else {
|
||
$fingerprint = md5($fingerprint);
|
||
}
|
||
|
||
return $fingerprint;
|
||
}
|
||
|
||
/**
|
||
* Get a session variable
|
||
*
|
||
* - This method returns the value of the requested session variable, or NULL if it's not present.
|
||
* - You can optionally use a namespace with this method, to avoid collisions with other session variables.
|
||
* But if using namespaces we recommended using the dedicated getFor() and setFor() methods instead.
|
||
* - You can also get or set non-namespaced session values directly (see examples).
|
||
*
|
||
* ~~~~~
|
||
* // Set value "Bob" to session variable named "firstName"
|
||
* $session->set('firstName', 'Bob');
|
||
*
|
||
* // You can retrieve the firstName now, or any later request
|
||
* $firstName = $session->get('firstName');
|
||
*
|
||
* // outputs: Hello Bob
|
||
* echo "Hello $firstName";
|
||
* ~~~~~
|
||
* ~~~~~
|
||
* // Setting and getting a session value directly
|
||
* $session->firstName = 'Bob';
|
||
* $firstName = $session->firstName;
|
||
* ~~~~~
|
||
*
|
||
* #pw-group-get
|
||
*
|
||
* @param string|object $key Name of session variable to retrieve (or object if using namespaces)
|
||
* @param string $_key Name of session variable to get if first argument is namespace, omit otherwise.
|
||
* @return mixed Returns value of seession variable, or NULL if not found.
|
||
*
|
||
*/
|
||
public function get($key, $_key = null) {
|
||
if($key === 'CSRF') {
|
||
return $this->CSRF();
|
||
} else if(!is_null($_key)) {
|
||
// namespace
|
||
return $this->getFor($key, $_key);
|
||
}
|
||
if($this->sessionInit) {
|
||
$value = isset($_SESSION[$this->sessionKey][$key]) ? $_SESSION[$this->sessionKey][$key] : null;
|
||
} else {
|
||
if($key == 'config') return $this->config;
|
||
$value = isset($this->data[$key]) ? $this->data[$key] : null;
|
||
}
|
||
|
||
if(is_null($value) && is_null($_key) && strpos($key, '_user_') === 0) {
|
||
// for backwards compatiblity with non-core modules or templates that may be checking _user_[property]
|
||
// not currently aware of any instances, but this is just a precaution
|
||
return $this->get('_user', str_replace('_user_', '', $key));
|
||
}
|
||
|
||
return $value;
|
||
}
|
||
|
||
/**
|
||
* Get a session variable or return $val argument if session value not present
|
||
*
|
||
* This is the same as get() except that it lets you specify a fallback return value in the method call.
|
||
* For a namespace version use `Session::getValFor()` instead.
|
||
*
|
||
* #pw-group-get
|
||
*
|
||
* @param string $key Name of session variable to retrieve.
|
||
* @param mixed $val Fallback value to return if session does not have it.
|
||
* @return mixed Returns value of seession variable, or NULL if not found.
|
||
* @since 3.0.133
|
||
*
|
||
*/
|
||
public function getVal($key, $val = null) {
|
||
$value = $this->get($key);
|
||
if($value === null) $value = $val;
|
||
return $value;
|
||
}
|
||
|
||
/**
|
||
* Get all session variables in an associative array
|
||
*
|
||
* #pw-group-get
|
||
*
|
||
* @param object|string $ns Optional namespace
|
||
* @return array
|
||
*
|
||
*/
|
||
public function getAll($ns = null) {
|
||
if(!is_null($ns)) return $this->getFor($ns, '');
|
||
if($this->sessionInit) {
|
||
return $_SESSION[$this->sessionKey];
|
||
} else {
|
||
return $this->data;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Get all session variables for given namespace and return associative array
|
||
*
|
||
* #pw-group-get
|
||
*
|
||
* @param string|Wire $ns
|
||
* @return array
|
||
* @since 3.0.141 Method added for consistency, but any version can do this with $session->getFor($ns, '');
|
||
*
|
||
*/
|
||
public function getAllFor($ns) {
|
||
return $this->getFor($ns, '');
|
||
}
|
||
|
||
/**
|
||
* Set a session variable
|
||
*
|
||
* - You can optionally use a namespace with this method, to avoid collisions with other session variables.
|
||
* But if using namespaces we recommended using the dedicated getFor() and setFor() methods instead.
|
||
* - You can also get or set non-namespaced session values directly (see examples).
|
||
*
|
||
* ~~~~~
|
||
* // Set value "Bob" to session variable named "firstName"
|
||
* $session->set('firstName', 'Bob');
|
||
*
|
||
* // You can retrieve the firstName now, or any later request
|
||
* $firstName = $session->get('firstName');
|
||
*
|
||
* // outputs: Hello Bob
|
||
* echo "Hello $firstName";
|
||
* ~~~~~
|
||
* ~~~~~
|
||
* // Setting and getting a session value directly
|
||
* $session->firstName = 'Bob';
|
||
* $firstName = $session->firstName;
|
||
* ~~~~~
|
||
*
|
||
* #pw-group-set
|
||
*
|
||
* @param string|object $key Name of session variable to set (or object for namespace)
|
||
* @param string|mixed $value Value to set (or name of variable, if first argument is namespace)
|
||
* @param mixed $_value Value to set if first argument is namespace. Omit otherwise.
|
||
* @return $this
|
||
*
|
||
*/
|
||
public function set($key, $value, $_value = null) {
|
||
if(!is_null($_value)) return $this->setFor($key, $value, $_value);
|
||
$oldValue = $this->get($key);
|
||
if($value !== $oldValue) $this->trackChange($key, $oldValue, $value);
|
||
if($this->sessionInit) {
|
||
$_SESSION[$this->sessionKey][$key] = $value;
|
||
} else {
|
||
$this->data[$key] = $value;
|
||
}
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* Get a session variable within a given namespace
|
||
*
|
||
* ~~~~~
|
||
* // Retrieve namespaced session value
|
||
* $firstName = $session->getFor($this, 'firstName');
|
||
* ~~~~~
|
||
*
|
||
* #pw-group-get
|
||
*
|
||
* @param string|object $ns Namespace string or object
|
||
* @param string $key Specify variable name to retrieve, or blank string to return all variables in the namespace.
|
||
* @return mixed
|
||
*
|
||
*/
|
||
public function getFor($ns, $key) {
|
||
$ns = $this->getNamespace($ns);
|
||
$data = $this->get($ns);
|
||
if(!is_array($data)) $data = array();
|
||
if($key === '') return $data;
|
||
return isset($data[$key]) ? $data[$key] : null;
|
||
}
|
||
|
||
/**
|
||
* Get a session variable or return $val argument if session value not present
|
||
*
|
||
* This is the same as get() except that it lets you specify a fallback return value in the method call.
|
||
* For a namespace version use `Session::getValFor()` instead.
|
||
*
|
||
* #pw-group-get
|
||
*
|
||
* @param string|object $ns Namespace string or object
|
||
* @param string $key Specify variable name to retrieve
|
||
* @param mixed $val Fallback value if session does not have one
|
||
* @return mixed
|
||
* @since 3.0.133
|
||
*
|
||
*/
|
||
public function getValFor($ns, $key, $val = null) {
|
||
$value = $this->getFor($ns, $key);
|
||
if($value === null) $value = $val;
|
||
return $value;
|
||
}
|
||
|
||
/**
|
||
* Set a session variable within a given namespace
|
||
*
|
||
* To remove a namespace, use `$session->remove($namespace)`.
|
||
*
|
||
* ~~~~~
|
||
* // Set a session value for a namespace
|
||
* $session->setFor($this, 'firstName', 'Bob');
|
||
* ~~~~~
|
||
*
|
||
* #pw-group-set
|
||
*
|
||
* @param string|object $ns Namespace string or object.
|
||
* @param string $key Name of session variable you want to set.
|
||
* @param mixed $value Value you want to set, or specify null to unset.
|
||
* @return $this
|
||
*
|
||
*/
|
||
public function setFor($ns, $key, $value) {
|
||
$ns = $this->getNamespace($ns);
|
||
$data = $this->get($ns);
|
||
if(!is_array($data)) $data = array();
|
||
if(is_null($value)) unset($data[$key]);
|
||
else $data[$key] = $value;
|
||
return $this->set($ns, $data);
|
||
}
|
||
|
||
/**
|
||
* Unset a session variable
|
||
*
|
||
* ~~~~~
|
||
* // Unset a session var
|
||
* $session->remove('firstName');
|
||
*
|
||
* // Unset a session var in a namespace
|
||
* $session->remove($this, 'firstName');
|
||
*
|
||
* // Unset all session vars in a namespace
|
||
* $session->remove($this, true);
|
||
* ~~~~~
|
||
*
|
||
* #pw-group-remove
|
||
*
|
||
* @param string|object $key Name of session variable you want to remove (or namespace string/object)
|
||
* @param string|bool|null $_key Omit this argument unless first argument is a namespace. Otherwise specify one of:
|
||
* - If first argument is namespace and you want to remove a property from the namespace, provide key here.
|
||
* - If first argument is namespace and you want to remove all properties from the namespace, provide boolean TRUE.
|
||
* @return $this
|
||
*
|
||
*/
|
||
public function remove($key, $_key = null) {
|
||
if($this->sessionInit) {
|
||
if(is_null($_key)) {
|
||
unset($_SESSION[$this->sessionKey][$key]);
|
||
} else if(is_bool($_key)) {
|
||
unset($_SESSION[$this->sessionKey][$this->getNamespace($key)]);
|
||
} else {
|
||
unset($_SESSION[$this->sessionKey][$this->getNamespace($key)][$_key]);
|
||
}
|
||
} else {
|
||
if(is_null($_key)) {
|
||
unset($this->data[$key]);
|
||
} else if(is_bool($_key)) {
|
||
unset($this->data[$this->getNamespace($key)]);
|
||
} else {
|
||
unset($this->data[$this->getNamespace($key)][$_key]);
|
||
}
|
||
}
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* Unset a session variable within a namespace
|
||
*
|
||
* #pw-group-remove
|
||
*
|
||
* @param string|object $ns Namespace
|
||
* @param string $key Provide name of variable to remove, or boolean true to remove all in namespace.
|
||
* @return $this
|
||
*
|
||
*/
|
||
public function removeFor($ns, $key) {
|
||
return $this->remove($ns, $key);
|
||
}
|
||
|
||
/**
|
||
* Remove all session variables in given namespace
|
||
*
|
||
* #pw-group-remove
|
||
*
|
||
* @param string|object $ns
|
||
* @return $this
|
||
*
|
||
*/
|
||
public function removeAllFor($ns) {
|
||
$this->remove($ns, true);
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* Given a namespace object or string, return the namespace string
|
||
*
|
||
* #pw-group-retrieval
|
||
*
|
||
* @param object|string $ns
|
||
* @return string
|
||
* @throws WireException if given invalid namespace type
|
||
*
|
||
*/
|
||
protected function getNamespace($ns) {
|
||
if(is_object($ns)) {
|
||
if($ns instanceof Wire) $ns = $ns->className();
|
||
else $ns = wireClassName($ns, false);
|
||
} else if(is_string($ns)) {
|
||
// good
|
||
} else {
|
||
throw new WireException("Session namespace must be string or object");
|
||
}
|
||
return $ns;
|
||
}
|
||
|
||
/**
|
||
* Provide non-namespaced $session->variable get access
|
||
*
|
||
* @param string $key
|
||
* @return SessionCSRF|mixed|null
|
||
*
|
||
*/
|
||
public function __get($key) {
|
||
return $this->get($key);
|
||
}
|
||
|
||
/**
|
||
* Provide non-namespaced $session->variable = variable set access
|
||
*
|
||
* @param string $key
|
||
* @param mixed $value
|
||
* @return $this
|
||
*
|
||
*/
|
||
public function __set($key, $value) {
|
||
return $this->set($key, $value);
|
||
}
|
||
|
||
/**
|
||
* Allow iteration of session variables
|
||
*
|
||
* ~~~~~
|
||
* foreach($session as $key => $value) {
|
||
* echo "<li>$key: $value</li>";
|
||
* }
|
||
* ~~~~~
|
||
*
|
||
* #pw-internal
|
||
*
|
||
* @return \ArrayObject
|
||
*
|
||
*/
|
||
#[\ReturnTypeWillChange]
|
||
public function getIterator() {
|
||
$data = $this->sessionInit ? $_SESSION[$this->sessionKey] : $this->data;
|
||
return new \ArrayObject($data);
|
||
}
|
||
|
||
/**
|
||
* Get the IP address of the current user
|
||
*
|
||
* ~~~~~
|
||
* $ip = $session->getIP();
|
||
* echo $ip; // outputs 111.222.333.444
|
||
* ~~~~~
|
||
*
|
||
* #pw-group-info
|
||
*
|
||
* @param bool $int Return as a long integer? (default=false)
|
||
* - IPv6 addresses cannot be represented as an integer, so please note that using this int option makes it return a CRC32
|
||
* integer when using IPv6 addresses (3.0.184+).
|
||
* @param bool|int $useClient Give preference to client headers for IP? HTTP_CLIENT_IP and HTTP_X_FORWARDED_FOR (default=false)
|
||
* - Specify integer 2 to include potential multiple CSV separated IPs (when provided by client).
|
||
* @return string|int Returns string by default, or integer if $int argument indicates to.
|
||
*
|
||
*/
|
||
public function getIP($int = false, $useClient = false) {
|
||
|
||
$ip = $this->config->sessionForceIP;
|
||
$ipv6 = false;
|
||
|
||
if(!empty($ip)) {
|
||
// use IP address specified in $config->sessionForceIP and disregard other options
|
||
$useClient = false;
|
||
|
||
} else if(empty($_SERVER['REMOTE_ADDR'])) {
|
||
// when accessing via CLI Interface, $_SERVER['REMOTE_ADDR'] isn't set and trying to get it, throws a php-notice
|
||
$ip = '127.0.0.1';
|
||
|
||
} else if($useClient) {
|
||
if(!empty($_SERVER['HTTP_CLIENT_IP'])) $ip = $_SERVER['HTTP_CLIENT_IP'];
|
||
else if(!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) $ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
|
||
else if(!empty($_SERVER['REMOTE_ADDR'])) $ip = $_SERVER['REMOTE_ADDR'];
|
||
else $ip = '0.0.0.0';
|
||
// It's possible for X_FORWARDED_FOR to have more than one CSV separated IP address, per @tuomassalo
|
||
if(strpos($ip, ',') !== false && $useClient !== 2) {
|
||
list($ip) = explode(',', $ip);
|
||
}
|
||
// sanitize: if IP contains something other than digits, periods, commas, spaces,
|
||
// then don't use it and instead fallback to the REMOTE_ADDR.
|
||
$test = str_replace(array('.', ',', ' '), '', $ip);
|
||
if(!ctype_digit("$test")) {
|
||
if(strpos($test, ':') !== false) {
|
||
// ipv6 allowed
|
||
$test = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6);
|
||
$ip = $test === false ? $_SERVER['REMOTE_ADDR'] : $test;
|
||
} else {
|
||
$ip = $_SERVER['REMOTE_ADDR'];
|
||
}
|
||
}
|
||
|
||
} else {
|
||
$ip = $_SERVER['REMOTE_ADDR'];
|
||
}
|
||
|
||
if(strpos($ip, ':') !== false) {
|
||
// attempt to identify an IPv4 version when an integer required for return value
|
||
if($int && $ip === '::1') {
|
||
$ip = '127.0.0.1';
|
||
} else if($int && strpos($ip, '.') && preg_match('!(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})!', $ip, $m)) {
|
||
$ip = $m[1]; // i.e. 0:0:0:0:0:ffff:192.1.56.10 => 192.1.56.10
|
||
} else {
|
||
$ipv6 = true;
|
||
}
|
||
}
|
||
|
||
if($useClient === 2 && strpos($ip, ',') !== false) {
|
||
// return multiple IPs
|
||
$ips = array();
|
||
foreach(explode(',', $ip) as $ip) {
|
||
if($ipv6) {
|
||
$ip = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6);
|
||
if($ip !== false && $int) $ip = crc32($ip);
|
||
} else {
|
||
$ip = ip2long(trim($ip));
|
||
if(!$int) $ip = long2ip($ip);
|
||
}
|
||
if($ip !== false) $ips[] = $ip;
|
||
}
|
||
$ip = implode(',', $ips);
|
||
|
||
} else if($ipv6) {
|
||
$ip = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6);
|
||
if($ip === false) {
|
||
$ip = $int ? 0 : '0.0.0.0';
|
||
} else if($int) {
|
||
$ip = crc32($ip);
|
||
}
|
||
|
||
} else {
|
||
// sanitize by converting to and from integer
|
||
$ip = ip2long(trim($ip));
|
||
if(!$int) $ip = long2ip($ip);
|
||
}
|
||
|
||
return $ip;
|
||
}
|
||
|
||
/**
|
||
* Login a user with the given name and password
|
||
*
|
||
* Also sets them to the current user.
|
||
*
|
||
* ~~~~~
|
||
* $u = $session->login('bob', 'laj3939$a');
|
||
* if($u) {
|
||
* echo "Welcome Bob";
|
||
* } else {
|
||
* echo "Sorry Bob";
|
||
* }
|
||
* ~~~~~
|
||
*
|
||
* #pw-group-authentication
|
||
*
|
||
* @param string|User $name May be user name or User object.
|
||
* @param string $pass Raw, non-hashed password.
|
||
* @param bool $force Specify boolean true to login user without requiring a password ($pass argument can be blank, or anything).
|
||
* You can also use the `$session->forceLogin($user)` method to force a login without a password.
|
||
* @return User|null Return the $user if the login was successful or null if not.
|
||
* @throws WireException
|
||
*
|
||
*/
|
||
public function ___login($name, $pass, $force = false) {
|
||
|
||
/** @var User|null $user */
|
||
$user = null;
|
||
/** @var Sanitizer $sanitizer */
|
||
$sanitizer = $this->wire('sanitizer');
|
||
/** @var Users $users */
|
||
$users = $this->wire('users');
|
||
/** @var int $guestUserID */
|
||
$guestUserID = $this->wire('config')->guestUserPageID;
|
||
|
||
$fail = true;
|
||
$failReason = '';
|
||
|
||
if(is_object($name) && $name instanceof User) {
|
||
$user = $name;
|
||
$name = $user->name;
|
||
} else {
|
||
$name = $sanitizer->pageNameUTF8($name);
|
||
}
|
||
|
||
if(!strlen($name)) return null;
|
||
|
||
$allowAttempt = $this->allowLoginAttempt($name);
|
||
|
||
if($allowAttempt && is_null($user)) {
|
||
$user = $users->get('name=' . $sanitizer->selectorValue($name));
|
||
}
|
||
|
||
if(!$allowAttempt) {
|
||
$failReason = 'Blocked login attempt';
|
||
|
||
} else if(!$user || !$user->id) {
|
||
$failReason = 'Unknown user';
|
||
|
||
} else if($user->id == $guestUserID) {
|
||
$failReason = 'Guest user may not login';
|
||
|
||
} else if(!$this->allowLogin($name, $user)) {
|
||
$failReason = 'Login not allowed';
|
||
|
||
} else if($force === true || $this->authenticate($user, $pass)) {
|
||
|
||
$this->trackChange('login', $this->wire('user'), $user);
|
||
session_regenerate_id(true);
|
||
$this->set('_user', 'id', $user->id);
|
||
$this->set('_user', 'ts', time());
|
||
|
||
if($this->config->sessionChallenge) {
|
||
// create new challenge
|
||
$rand = new WireRandom();
|
||
$challenge = $rand->base64(32);
|
||
$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)
|
||
$this->setCookie(
|
||
session_name() . self::challengeSuffix,
|
||
$challenge,
|
||
time() + 60*60*24*30,
|
||
'/',
|
||
$this->config->sessionCookieDomain,
|
||
$secure,
|
||
true,
|
||
$this->config->sessionCookieSameSite
|
||
);
|
||
}
|
||
|
||
if($this->config->sessionFingerprint) {
|
||
// remember a fingerprint that tracks the user's IP and user agent
|
||
$this->set('_user', 'fingerprint', $this->getFingerprint());
|
||
}
|
||
|
||
$this->wire('user', $user);
|
||
$this->CSRF()->resetAll();
|
||
$this->loginSuccess($user);
|
||
$fail = false;
|
||
|
||
} else {
|
||
// authentication failed
|
||
$failReason = 'Invalid password';
|
||
}
|
||
|
||
if($fail) {
|
||
$this->loginFailure($name, $failReason);
|
||
$user = null;
|
||
}
|
||
|
||
return $user;
|
||
}
|
||
|
||
/**
|
||
* Login a user without requiring a password
|
||
*
|
||
* ~~~~~
|
||
* // login bob without knowing his password
|
||
* $u = $session->forceLogin('bob');
|
||
* ~~~~~
|
||
*
|
||
* #pw-group-authentication
|
||
*
|
||
* @param string|User $user Username or User object
|
||
* @return User|null Returns User object on success, or null on failure
|
||
*
|
||
*/
|
||
public function forceLogin($user) {
|
||
return $this->login($user, '', true);
|
||
}
|
||
|
||
/**
|
||
* Login success method for hooks
|
||
*
|
||
* #pw-hooker
|
||
*
|
||
* @param User $user
|
||
*
|
||
*/
|
||
protected function ___loginSuccess(User $user) {
|
||
$this->log("Successful login for '$user->name'");
|
||
}
|
||
|
||
/**
|
||
* Login failure method for hooks
|
||
*
|
||
* #pw-hooker
|
||
*
|
||
* @param string $name Attempted login name
|
||
* @param string $reason Reason for login failure
|
||
*
|
||
*/
|
||
protected function ___loginFailure($name, $reason) {
|
||
$this->log("Error: Failed login for '$name' - $reason");
|
||
}
|
||
|
||
/**
|
||
* Allow the user $name to login? Provided for use by hooks.
|
||
*
|
||
* #pw-hooker
|
||
*
|
||
* @param string $name User login name
|
||
* @param User|null $user User object
|
||
* @return bool True if allowed to login, false if not (hooks may modify this)
|
||
*
|
||
*/
|
||
public function ___allowLogin($name, $user = null) {
|
||
$allow = true;
|
||
if(!strlen($name)) return false;
|
||
if(!$user || !$user instanceof User) {
|
||
$name = $this->wire('sanitizer')->pageNameUTF8($name);
|
||
$user = $this->wire('users')->get("name=" . $this->wire('sanitizer')->selectorValue($name));
|
||
}
|
||
if(!$user || !$user->id || !$user instanceof User) return false;
|
||
if($user->isGuest()) return false;
|
||
$xroles = $this->wire('config')->loginDisabledRoles;
|
||
if(!is_array($xroles) && !empty($xroles)) $xroles = array($xroles);
|
||
if($name) {}
|
||
if($user) {
|
||
if($user->isUnpublished()) {
|
||
$allow = false;
|
||
} else if(is_array($xroles)) {
|
||
foreach($xroles as $xrole) {
|
||
if($user->hasRole($xrole)) {
|
||
$allow = false;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return $allow;
|
||
}
|
||
|
||
/**
|
||
* Allow login attempt for given name at all?
|
||
*
|
||
* This method does nothing and is purely for hooks to modify return value.
|
||
*
|
||
* #pw-hooker
|
||
*
|
||
* @param string $name
|
||
* @return bool
|
||
*
|
||
*/
|
||
public function ___allowLoginAttempt($name) {
|
||
return strlen($name) > 0;
|
||
}
|
||
|
||
/**
|
||
* Return true or false whether the user authenticated with the supplied password
|
||
*
|
||
* #pw-hooker
|
||
*
|
||
* @param User $user User attempting to login
|
||
* @param string $pass Password they are attempting to login with
|
||
* @return bool
|
||
*
|
||
*/
|
||
public function ___authenticate(User $user, $pass) {
|
||
return $user->pass->matches($pass);
|
||
}
|
||
|
||
/**
|
||
* Logout the current user, and clear all session variables
|
||
*
|
||
* ~~~~~
|
||
* // logout user when "?logout=1" in URL query string
|
||
* if($input->get('logout')) {
|
||
* $session->logout();
|
||
* // good to redirect somewhere else after a login or logout
|
||
* $session->redirect('/');
|
||
* }
|
||
* ~~~~~
|
||
*
|
||
* #pw-group-authentication
|
||
*
|
||
* @param bool $startNew Start a new session after logout? (default=true)
|
||
* @return $this
|
||
* @throws WireException if session is disabled
|
||
*
|
||
*/
|
||
public function ___logout($startNew = true) {
|
||
$sessionName = session_name();
|
||
if($this->sessionInit) {
|
||
if(!$this->isExternal && !$this->isSecondary) {
|
||
$_SESSION = array();
|
||
}
|
||
} else {
|
||
$this->data = array();
|
||
}
|
||
$this->removeCookies();
|
||
$this->sessionInit = false;
|
||
if($startNew) {
|
||
session_destroy();
|
||
session_name($sessionName);
|
||
$this->init();
|
||
session_regenerate_id(true);
|
||
$_SESSION[$this->sessionKey] = array();
|
||
}
|
||
$user = $this->wire('user');
|
||
if($user) $this->logoutSuccess($user);
|
||
$guest = $this->wire('users')->getGuestUser();
|
||
if($this->wire('languages') && "$user->language" != "$guest->language") $guest->language = $user->language;
|
||
$this->wire('users')->setCurrentUser($guest);
|
||
$this->trackChange('logout', $user, $guest);
|
||
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])) {
|
||
$this->setCookie($sessionName, '', $time, '/', $domain, $secure, true, $samesite);
|
||
}
|
||
|
||
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
|
||
*
|
||
* #pw-internal
|
||
*
|
||
* @return array
|
||
* @since 3.0.141
|
||
*
|
||
*/
|
||
public function getCookieNames() {
|
||
$name = $this->config->sessionName;
|
||
$nameSecure = $this->config->sessionNameSecure;
|
||
if(empty($nameSecure)) $nameSecure = $this->config->sessionName . 's';
|
||
$a = array($name, $nameSecure);
|
||
if($this->config->sessionChallenge) {
|
||
$a[] = $name . self::challengeSuffix;
|
||
$a[] = $nameSecure . self::challengeSuffix;
|
||
}
|
||
return $a;
|
||
}
|
||
|
||
/**
|
||
* Logout success method for hooks
|
||
*
|
||
* #pw-hooker
|
||
*
|
||
* @param User $user User that logged in
|
||
*
|
||
*/
|
||
protected function ___logoutSuccess(User $user) {
|
||
$this->log("Logout for '$user->name'");
|
||
}
|
||
|
||
/**
|
||
* Redirect this session to another URL.
|
||
*
|
||
* Execution halts within this function after redirect has been issued.
|
||
*
|
||
* ~~~~~
|
||
* // redirect to homepage
|
||
* $session->redirect('/');
|
||
* ~~~~~
|
||
*
|
||
* #pw-group-redirects
|
||
*
|
||
* @param string $url URL to redirect to
|
||
* @param bool|int $status One of the following (or omit for 301):
|
||
* - `true` (bool): Permanent redirect (same as 301).
|
||
* - `false` (bool): Temporary redirect (same as 302).
|
||
* - `301` (int): Permanent redirect using GET. (3.0.166+)
|
||
* - `302` (int): “Found”, Temporary redirect using GET. (3.0.166+)
|
||
* - `303` (int): “See other”, Temporary redirect using GET. (3.0.166+)
|
||
* - `307` (int): Temporary redirect using current request method such as POST (repeat that request). (3.0.166+)
|
||
* @see Session::location()
|
||
*
|
||
*/
|
||
public function ___redirect($url, $status = 301) {
|
||
|
||
$page = $this->wire()->page;
|
||
|
||
if($status === true || "$status" === "301" || "$status" === "1") {
|
||
$status = 301;
|
||
} else if($status === false || "$status" === "302" || "$status" === "0") {
|
||
$status = 302;
|
||
} else {
|
||
$status = (int) $status;
|
||
// if invalid redirect http status code, fallback to 302
|
||
if($status < 300 || $status > 399) $status = 302;
|
||
}
|
||
|
||
// if there are notices, then queue them so that they aren't lost
|
||
if($this->sessionInit) {
|
||
$notices = $this->wire()->notices;
|
||
if($notices && count($notices)) {
|
||
foreach($notices as $notice) {
|
||
$this->queueNotice($notice);
|
||
}
|
||
}
|
||
}
|
||
|
||
// perform the redirect
|
||
if($page) {
|
||
$process = $this->wire()->process;
|
||
if("$process" !== "ProcessPageView") {
|
||
$process = $this->wire()->modules->get('ProcessPageView');
|
||
}
|
||
/** @var ProcessPageView $process */
|
||
if($process) {
|
||
// ensure ProcessPageView is properly closed down
|
||
$process->setResponseType(ProcessPageView::responseTypeRedirect);
|
||
$process->finished();
|
||
// retain modal=1 get variables through redirects (this can be moved to a hook later)
|
||
$input = $this->wire()->input;
|
||
if($page->template == 'admin' && $input && $input->get('modal') && strpos($url, '//') === false) {
|
||
if(!strpos($url, 'modal=')) $url .= (strpos($url, '?') !== false ? '&' : '?') . 'modal=1';
|
||
}
|
||
}
|
||
}
|
||
|
||
$this->wire()->setStatus(ProcessWire::statusFinished, array(
|
||
'redirectUrl' => $url,
|
||
'redirectType' => $status,
|
||
));
|
||
|
||
// note for 302 redirects we send no header other than 'Location: url'
|
||
$http = new WireHttp();
|
||
$this->wire($http);
|
||
$http->sendStatusHeader($status);
|
||
$http->sendHeader("Location: $url");
|
||
|
||
exit(0);
|
||
}
|
||
|
||
/**
|
||
* Perform a temporary redirect
|
||
*
|
||
* This is an alias of `$session->redirect($url, false);` that sends only the
|
||
* location header, which translates to a 302 redirect.
|
||
*
|
||
* #pw-group-redirects
|
||
*
|
||
* @param string $url
|
||
* @param int $status One of the following HTTP status codes, or omit for 302 (added 3.0.192):
|
||
* - `302` (int): “Found”, Temporary redirect using GET. (default)
|
||
* - `303` (int): “See other”, Temporary redirect using GET.
|
||
* - `307` (int): Temporary redirect using current request method such as POST (repeat that request).
|
||
* @since 3.0.166
|
||
* @see Session::redirect()
|
||
*
|
||
*/
|
||
public function location($url, $status = 302) {
|
||
$this->redirect($url, $status);
|
||
}
|
||
|
||
/**
|
||
* Manually close the session, before program execution is done
|
||
*
|
||
* #pw-internal
|
||
*
|
||
*/
|
||
public function close() {
|
||
if($this->sessionInit) session_write_close();
|
||
}
|
||
|
||
/**
|
||
* Queue notice text to be shown the next time this session class is instantiated
|
||
*
|
||
* #pw-internal
|
||
*
|
||
* @param string $text
|
||
* @param string $type One of "messages", "errors" or "warnings"
|
||
* @param int $flags
|
||
*
|
||
*/
|
||
protected function queueNoticeText($text, $type, $flags) {
|
||
if(!$this->sessionInit) return;
|
||
$items = $this->getFor('_notices', $type);
|
||
if(is_null($items)) $items = array();
|
||
$item = array('text' => $text, 'flags' => $flags);
|
||
$items[] = $item;
|
||
$this->setFor('_notices', $type, $items);
|
||
}
|
||
|
||
/**
|
||
* Queue a Notice object to be shown the next time this session class is instantiated
|
||
*
|
||
* #pw-internal
|
||
*
|
||
* @param Notice $notice
|
||
*
|
||
*/
|
||
protected function queueNotice(Notice $notice) {
|
||
if(!$this->sessionInit) return;
|
||
$type = $notice->getName();
|
||
$items = $this->getFor('_notices', $type);
|
||
if(is_null($items)) $items = array();
|
||
$items[] = $notice->getArray();
|
||
$this->setFor('_notices', $type, $items);
|
||
}
|
||
|
||
/**
|
||
* Pull queued notices and convert them to notices for this request
|
||
*
|
||
* #pw-internal
|
||
*
|
||
*/
|
||
protected function wakeupNotices() {
|
||
|
||
/** @var Notices $notices */
|
||
$notices = $this->wire('notices');
|
||
if(!$notices) return;
|
||
|
||
$types = array(
|
||
'messages' => 'NoticeMessage',
|
||
'errors' => 'NoticeError',
|
||
'warnings' => 'NoticeWarning',
|
||
);
|
||
|
||
foreach($types as $type => $className) {
|
||
$items = $this->getFor('_notices', $type);
|
||
if(!is_array($items)) continue;
|
||
|
||
foreach($items as $item) {
|
||
if(!isset($item['text'])) continue;
|
||
$class = wireClassName($className, true);
|
||
$notice = $this->wire(new $class(''));
|
||
$notice->setArray($item);
|
||
$notices->add($notice);
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
/**
|
||
* Queue a message to appear on the next pageview
|
||
*
|
||
* #pw-group-notices
|
||
*
|
||
* @param string $text Message to queue
|
||
* @param int $flags Optional flags, See Notice::flags
|
||
* @return $this
|
||
*
|
||
*/
|
||
public function message($text, $flags = 0) {
|
||
$this->queueNoticeText($text, 'messages', $flags);
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* Queue an error to appear on the next pageview
|
||
*
|
||
* #pw-group-notices
|
||
*
|
||
* @param string $text Error to queue
|
||
* @param int $flags See Notice::flags
|
||
* @return $this
|
||
*
|
||
*/
|
||
public function error($text, $flags = 0) {
|
||
$this->queueNoticeText($text, 'errors', $flags);
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* Queue a warning to appear the next pageview
|
||
*
|
||
* #pw-group-notices
|
||
*
|
||
* @param string $text Warning to queue
|
||
* @param int $flags See Notice::flags
|
||
* @return $this
|
||
*
|
||
*/
|
||
public function warning($text, $flags = 0) {
|
||
$this->queueNoticeText($text, 'warnings', $flags);
|
||
return $this;
|
||
}
|
||
|
||
/**
|
||
* Session maintenance
|
||
*
|
||
* This is automatically called by ProcessWire at the end of the request,
|
||
* no need to call it on your own.
|
||
*
|
||
* Keep track of session history, if $config->sessionHistory is used.
|
||
* It can be retrieved with the $session->getHistory() method.
|
||
*
|
||
* #pw-internal
|
||
*
|
||
* @todo add extra gc checks
|
||
*
|
||
*/
|
||
public function maintenance() {
|
||
|
||
if($this->skipMaintenance || !$this->sessionInit) return;
|
||
|
||
// prevent multiple calls, just in case
|
||
$this->skipMaintenance = true;
|
||
|
||
$config = $this->wire()->config;
|
||
$historyCnt = (int) ($config ? $config->sessionHistory : 0);
|
||
|
||
if($historyCnt) {
|
||
|
||
$sanitizer = $this->wire()->sanitizer;
|
||
$input = $this->wire()->input;
|
||
$page = $this->wire()->page;
|
||
|
||
if(!$sanitizer || !$input || !$page) return;
|
||
|
||
$history = $this->get('_user', 'history');
|
||
if(!is_array($history)) $history = array();
|
||
|
||
$item = array(
|
||
'time' => time(),
|
||
'url' => $sanitizer->entities($input->httpUrl()),
|
||
'page' => $page->id,
|
||
);
|
||
|
||
$cnt = count($history);
|
||
if($cnt) {
|
||
end($history);
|
||
$lastKey = key($history);
|
||
$nextKey = $lastKey+1;
|
||
if($cnt >= $historyCnt) {
|
||
if($historyCnt > 1) {
|
||
$history = array_slice($history, -1 * ($historyCnt - 1), null, true);
|
||
} else {
|
||
$history = array();
|
||
}
|
||
}
|
||
} else {
|
||
$nextKey = 0;
|
||
}
|
||
|
||
$history[$nextKey] = $item;
|
||
$this->set('_user', 'history', $history);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Get the session history (if enabled)
|
||
*
|
||
* Applicable only if `$config->sessionHistory > 0`.
|
||
*
|
||
* ~~~~~
|
||
* $history = $session->getHistory();
|
||
* print_r($history);
|
||
* // outputs the following:
|
||
* array(
|
||
* 0 => array(
|
||
* 'url' => 'http://domain.com/path/to/page/', // URL
|
||
* 'page' => 1234, // page ID
|
||
* 'time' => 234993498, // unix timestamp
|
||
* ),
|
||
* 1 => array(
|
||
* // ...
|
||
* ),
|
||
* 2 => array(
|
||
* // ...
|
||
* ),
|
||
* ...
|
||
* );
|
||
* ~~~~~
|
||
*
|
||
* #pw-group-advanced
|
||
*
|
||
* @return array Array of arrays containing history entries.
|
||
*
|
||
*/
|
||
public function getHistory() {
|
||
$value = $this->get('_user', 'history');
|
||
if(!is_array($value)) $value = array();
|
||
return $value;
|
||
}
|
||
|
||
/**
|
||
* Remove queued notices
|
||
*
|
||
* Call this after displaying queued message, error or warning notices.
|
||
* This prevents them from re-appearing on the next request.
|
||
*
|
||
* #pw-group-notices
|
||
*
|
||
*/
|
||
public function removeNotices() {
|
||
$this->removeAllFor('_notices');
|
||
}
|
||
|
||
/**
|
||
* Return an instance of ProcessWire’s CSRF object, which provides an API for cross site request forgery protection.
|
||
*
|
||
* ~~~~
|
||
* // output somewhere in <form> markup when rendering a form
|
||
* echo $session->CSRF->renderInput();
|
||
* ~~~~
|
||
* ~~~~
|
||
* // when processing form (POST request), check to see if token is present
|
||
* if($session->CSRF->hasValidToken()) {
|
||
* // form submission is valid
|
||
* // okay to process
|
||
* } else {
|
||
* // form submission is NOT valid
|
||
* throw new WireException('CSRF check failed!');
|
||
* }
|
||
* ~~~~
|
||
*
|
||
* #pw-group-advanced
|
||
*
|
||
* @return SessionCSRF
|
||
* @see SessionCSRF::renderInput(), SessionCSRF::validate(), SessionCSRF::hasValidToken()
|
||
*
|
||
*/
|
||
public function CSRF() {
|
||
if(!$this->sessionInit) $this->init(); // init required for CSRF
|
||
if(is_null($this->CSRF)) $this->CSRF = $this->wire(new SessionCSRF());
|
||
return $this->CSRF;
|
||
}
|
||
|
||
/**
|
||
* Get or set current session handler instance (WireSessionHandler)
|
||
*
|
||
* #pw-internal
|
||
*
|
||
* @param WireSessionHandler|null $sessionHandler Specify only when setting, omit to get session handler.
|
||
* @return null|WireSessionHandler Returns WireSessionHandler instance, or…
|
||
* returns null when session handler is not yet known or is PHP (file system)
|
||
* @since 3.0.166
|
||
*
|
||
*/
|
||
public function sessionHandler(WireSessionHandler $sessionHandler = null) {
|
||
if($sessionHandler) $this->sessionHandler = $sessionHandler;
|
||
return $this->sessionHandler;
|
||
}
|
||
|
||
|
||
}
|