2007-07-19 12:40:28 +02:00
|
|
|
<?php
|
2016-06-15 06:03:16 +02:00
|
|
|
|
2016-06-23 01:37:22 +02:00
|
|
|
namespace SilverStripe\Security;
|
|
|
|
|
2017-03-02 03:24:38 +01:00
|
|
|
use Page;
|
2017-04-14 05:30:55 +02:00
|
|
|
use LogicException;
|
2017-01-03 08:37:17 +01:00
|
|
|
use SilverStripe\CMS\Controllers\ContentController;
|
2016-08-19 00:51:35 +02:00
|
|
|
use SilverStripe\Control\Controller;
|
|
|
|
use SilverStripe\Control\Director;
|
2016-09-09 08:43:05 +02:00
|
|
|
use SilverStripe\Control\HTTPRequest;
|
|
|
|
use SilverStripe\Control\HTTPResponse;
|
2017-04-22 06:30:10 +02:00
|
|
|
use SilverStripe\Control\HTTPResponse_Exception;
|
2016-08-19 00:51:35 +02:00
|
|
|
use SilverStripe\Control\Session;
|
2017-04-22 06:30:10 +02:00
|
|
|
use SilverStripe\Control\RequestHandler;
|
2017-01-03 08:37:17 +01:00
|
|
|
use SilverStripe\Core\ClassInfo;
|
2017-04-14 05:30:55 +02:00
|
|
|
use SilverStripe\Core\Config\Config;
|
2016-08-19 00:51:35 +02:00
|
|
|
use SilverStripe\Core\Convert;
|
|
|
|
use SilverStripe\Dev\Deprecation;
|
2016-10-06 06:31:38 +02:00
|
|
|
use SilverStripe\Dev\TestOnly;
|
2016-08-19 00:51:35 +02:00
|
|
|
use SilverStripe\Forms\EmailField;
|
|
|
|
use SilverStripe\Forms\FieldList;
|
|
|
|
use SilverStripe\Forms\Form;
|
|
|
|
use SilverStripe\Forms\FormAction;
|
2016-06-15 06:03:16 +02:00
|
|
|
use SilverStripe\ORM\ArrayList;
|
2017-04-22 06:30:10 +02:00
|
|
|
use SilverStripe\ORM\DataModel;
|
2016-06-15 06:03:16 +02:00
|
|
|
use SilverStripe\ORM\DB;
|
2017-01-03 08:37:17 +01:00
|
|
|
use SilverStripe\ORM\DataObject;
|
2016-07-13 09:08:09 +02:00
|
|
|
use SilverStripe\ORM\FieldType\DBField;
|
2016-11-23 06:09:10 +01:00
|
|
|
use SilverStripe\ORM\ValidationResult;
|
2016-08-19 00:51:35 +02:00
|
|
|
use SilverStripe\View\ArrayData;
|
|
|
|
use SilverStripe\View\SSViewer;
|
|
|
|
use SilverStripe\View\TemplateGlobalProvider;
|
2016-06-23 01:37:22 +02:00
|
|
|
use Exception;
|
2017-03-02 03:24:38 +01:00
|
|
|
use SilverStripe\View\ViewableData_Customised;
|
2016-08-19 00:51:35 +02:00
|
|
|
use Subsite;
|
2017-04-22 06:30:10 +02:00
|
|
|
use SilverStripe\Core\Injector\Injector;
|
2016-06-23 01:37:22 +02:00
|
|
|
|
2007-07-19 12:40:28 +02:00
|
|
|
/**
|
|
|
|
* Implements a basic security model
|
|
|
|
*/
|
2016-11-29 00:31:16 +01:00
|
|
|
class Security extends Controller implements TemplateGlobalProvider
|
|
|
|
{
|
|
|
|
|
|
|
|
private static $allowed_actions = array(
|
|
|
|
'index',
|
|
|
|
'login',
|
|
|
|
'logout',
|
|
|
|
'basicauthlogin',
|
|
|
|
'lostpassword',
|
|
|
|
'passwordsent',
|
|
|
|
'changepassword',
|
|
|
|
'ping',
|
|
|
|
'LoginForm',
|
|
|
|
'ChangePasswordForm',
|
|
|
|
'LostPasswordForm',
|
|
|
|
);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Default user name. Only used in dev-mode by {@link setDefaultAdmin()}
|
|
|
|
*
|
|
|
|
* @var string
|
|
|
|
* @see setDefaultAdmin()
|
|
|
|
*/
|
|
|
|
protected static $default_username;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Default password. Only used in dev-mode by {@link setDefaultAdmin()}
|
|
|
|
*
|
|
|
|
* @var string
|
|
|
|
* @see setDefaultAdmin()
|
|
|
|
*/
|
|
|
|
protected static $default_password;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* If set to TRUE to prevent sharing of the session across several sites
|
|
|
|
* in the domain.
|
|
|
|
*
|
|
|
|
* @config
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
protected static $strict_path_checking = false;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The password encryption algorithm to use by default.
|
|
|
|
* This is an arbitrary code registered through {@link PasswordEncryptor}.
|
|
|
|
*
|
|
|
|
* @config
|
|
|
|
* @var string
|
|
|
|
*/
|
|
|
|
private static $password_encryption_algorithm = 'blowfish';
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Showing "Remember me"-checkbox
|
|
|
|
* on loginform, and saving encrypted credentials to a cookie.
|
|
|
|
*
|
|
|
|
* @config
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
private static $autologin_enabled = true;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Determine if login username may be remembered between login sessions
|
|
|
|
* If set to false this will disable autocomplete and prevent username persisting in the session
|
|
|
|
*
|
|
|
|
* @config
|
|
|
|
* @var bool
|
|
|
|
*/
|
|
|
|
private static $remember_username = true;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Location of word list to use for generating passwords
|
|
|
|
*
|
|
|
|
* @config
|
|
|
|
* @var string
|
|
|
|
*/
|
|
|
|
private static $word_list = './wordlist.txt';
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @config
|
|
|
|
* @var string
|
|
|
|
*/
|
|
|
|
private static $template = 'BlankPage';
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Template thats used to render the pages.
|
|
|
|
*
|
|
|
|
* @var string
|
|
|
|
* @config
|
|
|
|
*/
|
|
|
|
private static $template_main = 'Page';
|
|
|
|
|
2017-01-12 21:16:13 +01:00
|
|
|
/**
|
|
|
|
* Class to use for page rendering
|
|
|
|
*
|
|
|
|
* @var string
|
|
|
|
* @config
|
|
|
|
*/
|
2017-03-02 03:24:38 +01:00
|
|
|
private static $page_class = Page::class;
|
2017-01-12 21:16:13 +01:00
|
|
|
|
2016-11-29 00:31:16 +01:00
|
|
|
/**
|
|
|
|
* Default message set used in permission failures.
|
|
|
|
*
|
|
|
|
* @config
|
|
|
|
* @var array|string
|
|
|
|
*/
|
|
|
|
private static $default_message_set;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Random secure token, can be used as a crypto key internally.
|
|
|
|
* Generate one through 'sake dev/generatesecuretoken'.
|
|
|
|
*
|
|
|
|
* @config
|
|
|
|
* @var String
|
|
|
|
*/
|
|
|
|
private static $token;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The default login URL
|
|
|
|
*
|
|
|
|
* @config
|
|
|
|
*
|
|
|
|
* @var string
|
|
|
|
*/
|
|
|
|
private static $login_url = "Security/login";
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The default logout URL
|
|
|
|
*
|
|
|
|
* @config
|
|
|
|
*
|
|
|
|
* @var string
|
|
|
|
*/
|
|
|
|
private static $logout_url = "Security/logout";
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The default lost password URL
|
|
|
|
*
|
|
|
|
* @config
|
|
|
|
*
|
|
|
|
* @var string
|
|
|
|
*/
|
|
|
|
private static $lost_password_url = "Security/lostpassword";
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Value of X-Frame-Options header
|
|
|
|
*
|
|
|
|
* @config
|
|
|
|
* @var string
|
|
|
|
*/
|
|
|
|
private static $frame_options = 'SAMEORIGIN';
|
|
|
|
|
2017-01-17 13:23:41 +01:00
|
|
|
/**
|
|
|
|
* Value of the X-Robots-Tag header (for the Security section)
|
|
|
|
*
|
|
|
|
* @config
|
|
|
|
* @var string
|
|
|
|
*/
|
|
|
|
private static $robots_tag = 'noindex, nofollow';
|
|
|
|
|
2016-11-29 00:31:16 +01:00
|
|
|
/**
|
|
|
|
* Enable or disable recording of login attempts
|
|
|
|
* through the {@link LoginRecord} object.
|
|
|
|
*
|
|
|
|
* @config
|
|
|
|
* @var boolean $login_recording
|
|
|
|
*/
|
|
|
|
private static $login_recording = false;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var boolean If set to TRUE or FALSE, {@link database_is_ready()}
|
|
|
|
* will always return FALSE. Used for unit testing.
|
|
|
|
*/
|
2017-04-22 06:30:10 +02:00
|
|
|
protected static $force_database_is_ready = null;
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* When the database has once been verified as ready, it will not do the
|
|
|
|
* checks again.
|
|
|
|
*
|
|
|
|
* @var bool
|
|
|
|
*/
|
2017-04-22 06:30:10 +02:00
|
|
|
protected static $database_is_ready = false;
|
|
|
|
|
2017-04-23 05:30:33 +02:00
|
|
|
/**
|
|
|
|
* @var array available authenticators
|
|
|
|
*/
|
2017-04-22 06:30:10 +02:00
|
|
|
protected static $authenticators = [];
|
|
|
|
|
2017-04-23 05:30:33 +02:00
|
|
|
/**
|
|
|
|
* @var string Default authenticator
|
|
|
|
*/
|
2017-04-22 06:30:10 +02:00
|
|
|
protected static $default_authenticator = MemberAuthenticator\Authenticator::class;
|
|
|
|
|
|
|
|
/**
|
2017-04-23 05:30:33 +02:00
|
|
|
* @inheritdoc
|
2017-04-22 06:30:10 +02:00
|
|
|
*/
|
2017-04-23 05:30:33 +02:00
|
|
|
protected function init()
|
2017-04-22 06:30:10 +02:00
|
|
|
{
|
2017-04-23 05:30:33 +02:00
|
|
|
parent::init();
|
2017-04-22 06:30:10 +02:00
|
|
|
|
2017-04-23 05:30:33 +02:00
|
|
|
// Prevent clickjacking, see https://developer.mozilla.org/en-US/docs/HTTP/X-Frame-Options
|
2017-04-30 05:17:26 +02:00
|
|
|
$frameOptions = static::config()->get('frame_options');
|
2017-04-23 05:30:33 +02:00
|
|
|
if ($frameOptions) {
|
|
|
|
$this->getResponse()->addHeader('X-Frame-Options', $frameOptions);
|
2017-04-22 06:30:10 +02:00
|
|
|
}
|
|
|
|
|
2017-04-23 05:30:33 +02:00
|
|
|
// Prevent search engines from indexing the login page
|
2017-04-30 05:17:26 +02:00
|
|
|
$robotsTag = static::config()->get('robots_tag');
|
2017-04-23 05:30:33 +02:00
|
|
|
if ($robotsTag) {
|
|
|
|
$this->getResponse()->addHeader('X-Robots-Tag', $robotsTag);
|
2017-04-22 06:30:10 +02:00
|
|
|
}
|
2017-04-23 05:30:33 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @inheritdoc
|
|
|
|
*/
|
|
|
|
public function index()
|
|
|
|
{
|
|
|
|
return $this->httpError(404); // no-op
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the selected authenticator for this request
|
|
|
|
*
|
|
|
|
* @param $name string The identifier of the authenticator in your config
|
|
|
|
* @return string Class name of Authenticator
|
|
|
|
* @throws LogicException
|
|
|
|
*/
|
|
|
|
protected function getAuthenticator($name)
|
|
|
|
{
|
2017-04-30 05:17:26 +02:00
|
|
|
$authenticators = self::config()->get('authenticators');
|
2017-04-23 05:30:33 +02:00
|
|
|
|
2017-04-30 05:17:26 +02:00
|
|
|
$name = $name ?: 'default';
|
2017-04-23 05:30:33 +02:00
|
|
|
|
|
|
|
if (isset($authenticators[$name])) {
|
|
|
|
return Injector::inst()->get($authenticators[$name]);
|
|
|
|
}
|
|
|
|
|
|
|
|
throw new LogicException('No valid authenticator found');
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get all registered authenticators
|
|
|
|
*
|
|
|
|
* @return array Return an array of Authenticator objects
|
|
|
|
*/
|
2017-04-30 05:17:26 +02:00
|
|
|
public static function getAuthenticators($service = Authenticator::LOGIN)
|
2017-04-23 05:30:33 +02:00
|
|
|
{
|
2017-04-30 05:17:26 +02:00
|
|
|
$authenticators = self::config()->get('authenticators');
|
2017-04-22 06:30:10 +02:00
|
|
|
|
2017-04-30 05:17:26 +02:00
|
|
|
foreach($authenticators as $name => &$class) {
|
|
|
|
/** @var Authenticator $authenticator */
|
|
|
|
$authenticator = Injector::inst()->get($class);
|
|
|
|
if($authenticator->supportedServices() & $service) {
|
|
|
|
$class = $authenticator;
|
|
|
|
} else {
|
|
|
|
unset($authenticators[$name]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return $authenticators;
|
2017-04-22 06:30:10 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if a given authenticator is registered
|
|
|
|
*
|
2017-04-23 05:30:33 +02:00
|
|
|
* @param string $authenticator The configured identifier of the authenicator
|
2017-04-22 06:30:10 +02:00
|
|
|
* @return bool Returns TRUE if the authenticator is registered, FALSE
|
|
|
|
* otherwise.
|
|
|
|
*/
|
|
|
|
public static function hasAuthenticator($authenticator)
|
|
|
|
{
|
|
|
|
$authenticators = self::config()->get('authenticators');
|
2017-04-23 05:30:33 +02:00
|
|
|
return !empty($authenticators[$authenticator]);
|
2017-04-22 06:30:10 +02:00
|
|
|
}
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Register that we've had a permission failure trying to view the given page
|
|
|
|
*
|
|
|
|
* This will redirect to a login page.
|
|
|
|
* If you don't provide a messageSet, a default will be used.
|
|
|
|
*
|
|
|
|
* @param Controller $controller The controller that you were on to cause the permission
|
|
|
|
* failure.
|
|
|
|
* @param string|array $messageSet The message to show to the user. This
|
|
|
|
* can be a string, or a map of different
|
|
|
|
* messages for different contexts.
|
|
|
|
* If you pass an array, you can use the
|
|
|
|
* following keys:
|
|
|
|
* - default: The default message
|
|
|
|
* - alreadyLoggedIn: The message to
|
|
|
|
* show if the user
|
|
|
|
* is already logged
|
|
|
|
* in and lacks the
|
|
|
|
* permission to
|
|
|
|
* access the item.
|
|
|
|
*
|
|
|
|
* The alreadyLoggedIn value can contain a '%s' placeholder that will be replaced with a link
|
|
|
|
* to log in.
|
|
|
|
* @return HTTPResponse
|
|
|
|
*/
|
|
|
|
public static function permissionFailure($controller = null, $messageSet = null)
|
|
|
|
{
|
|
|
|
self::set_ignore_disallowed_actions(true);
|
|
|
|
|
|
|
|
if (!$controller) {
|
|
|
|
$controller = Controller::curr();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (Director::is_ajax()) {
|
|
|
|
$response = ($controller) ? $controller->getResponse() : new HTTPResponse();
|
|
|
|
$response->setStatusCode(403);
|
|
|
|
if (!Member::currentUser()) {
|
2017-04-20 03:15:24 +02:00
|
|
|
$response->setBody(_t('SilverStripe\\CMS\\Controllers\\ContentController.NOTLOGGEDIN', 'Not logged in'));
|
|
|
|
$response->setStatusDescription(_t('SilverStripe\\CMS\\Controllers\\ContentController.NOTLOGGEDIN', 'Not logged in'));
|
2016-11-29 00:31:16 +01:00
|
|
|
// Tell the CMS to allow re-aunthentication
|
|
|
|
if (CMSSecurity::enabled()) {
|
|
|
|
$response->addHeader('X-Reauthenticate', '1');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return $response;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Prepare the messageSet provided
|
|
|
|
if (!$messageSet) {
|
|
|
|
if ($configMessageSet = static::config()->get('default_message_set')) {
|
|
|
|
$messageSet = $configMessageSet;
|
|
|
|
} else {
|
|
|
|
$messageSet = array(
|
|
|
|
'default' => _t(
|
2017-04-20 03:15:24 +02:00
|
|
|
'SilverStripe\\Security\\Security.NOTEPAGESECURED',
|
2016-11-29 00:31:16 +01:00
|
|
|
"That page is secured. Enter your credentials below and we will send "
|
|
|
|
. "you right along."
|
|
|
|
),
|
|
|
|
'alreadyLoggedIn' => _t(
|
2017-04-20 03:15:24 +02:00
|
|
|
'SilverStripe\\Security\\Security.ALREADYLOGGEDIN',
|
2016-11-29 00:31:16 +01:00
|
|
|
"You don't have access to this page. If you have another account that "
|
|
|
|
. "can access that page, you can log in again below.",
|
|
|
|
"%s will be replaced with a link to log in."
|
|
|
|
)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!is_array($messageSet)) {
|
|
|
|
$messageSet = array('default' => $messageSet);
|
|
|
|
}
|
|
|
|
|
|
|
|
$member = Member::currentUser();
|
|
|
|
|
|
|
|
// Work out the right message to show
|
|
|
|
if ($member && $member->exists()) {
|
|
|
|
$response = ($controller) ? $controller->getResponse() : new HTTPResponse();
|
|
|
|
$response->setStatusCode(403);
|
|
|
|
|
|
|
|
//If 'alreadyLoggedIn' is not specified in the array, then use the default
|
|
|
|
//which should have been specified in the lines above
|
|
|
|
if (isset($messageSet['alreadyLoggedIn'])) {
|
|
|
|
$message = $messageSet['alreadyLoggedIn'];
|
|
|
|
} else {
|
|
|
|
$message = $messageSet['default'];
|
|
|
|
}
|
|
|
|
|
2017-04-23 05:30:33 +02:00
|
|
|
Security::setLoginMessage($message, ValidationResult::TYPE_WARNING);
|
|
|
|
$loginResponse = (new Security())->login(new HTTPRequest('GET', '/'));
|
2016-11-29 00:31:16 +01:00
|
|
|
if ($loginResponse instanceof HTTPResponse) {
|
|
|
|
return $loginResponse;
|
|
|
|
}
|
|
|
|
|
|
|
|
$response->setBody((string)$loginResponse);
|
|
|
|
|
|
|
|
$controller->extend('permissionDenied', $member);
|
|
|
|
|
|
|
|
return $response;
|
|
|
|
} else {
|
|
|
|
$message = $messageSet['default'];
|
|
|
|
}
|
|
|
|
|
2016-11-23 06:09:10 +01:00
|
|
|
static::setLoginMessage($message, ValidationResult::TYPE_WARNING);
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
Session::set("BackURL", $_SERVER['REQUEST_URI']);
|
|
|
|
|
|
|
|
// TODO AccessLogEntry needs an extension to handle permission denied errors
|
|
|
|
// Audit logging hook
|
|
|
|
$controller->extend('permissionDenied', $member);
|
|
|
|
|
2016-11-23 06:09:10 +01:00
|
|
|
return $controller->redirect(Controller::join_links(
|
2017-02-22 04:14:53 +01:00
|
|
|
Security::config()->uninherited('login_url'),
|
2016-11-23 06:09:10 +01:00
|
|
|
"?BackURL=" . urlencode($_SERVER['REQUEST_URI'])
|
|
|
|
));
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
2017-05-07 21:11:00 +02:00
|
|
|
private static $currentUser;
|
|
|
|
|
|
|
|
public static function setCurrentUser($currentUser)
|
|
|
|
{
|
|
|
|
self::$currentUser = $currentUser;
|
|
|
|
}
|
|
|
|
|
|
|
|
public static function getCurrentUser()
|
|
|
|
{
|
|
|
|
return self::$currentUser;
|
|
|
|
}
|
|
|
|
|
2016-11-29 00:31:16 +01:00
|
|
|
/**
|
|
|
|
* Get the login forms for all available authentication methods
|
|
|
|
*
|
|
|
|
* @return array Returns an array of available login forms (array of Form
|
|
|
|
* objects).
|
|
|
|
*
|
|
|
|
* @todo Check how to activate/deactivate authentication methods
|
|
|
|
*/
|
2017-04-22 06:30:10 +02:00
|
|
|
public function getLoginForms()
|
2016-11-29 00:31:16 +01:00
|
|
|
{
|
2017-04-22 06:30:10 +02:00
|
|
|
return array_map(
|
|
|
|
function ($authenticator) {
|
|
|
|
return $authenticator->getLoginHandler($this->Link())->handleRequest($this->getRequest(), DataModel::inst());
|
|
|
|
},
|
|
|
|
Security::getAuthenticators()
|
|
|
|
);
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get a link to a security action
|
|
|
|
*
|
|
|
|
* @param string $action Name of the action
|
|
|
|
* @return string Returns the link to the given action
|
|
|
|
*/
|
|
|
|
public function Link($action = null)
|
|
|
|
{
|
|
|
|
/** @skipUpgrade */
|
|
|
|
return Controller::join_links(Director::baseURL(), "Security", $action);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* This action is available as a keep alive, so user
|
|
|
|
* sessions don't timeout. A common use is in the admin.
|
|
|
|
*/
|
|
|
|
public function ping()
|
|
|
|
{
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Log the currently logged in user out
|
|
|
|
*
|
2017-04-30 05:17:26 +02:00
|
|
|
* Logging out without ID-parameter in the URL, will log the user out of all applicable Authenticators.
|
|
|
|
*
|
|
|
|
* Adding an ID will only log the user out of that Authentication method.
|
|
|
|
*
|
|
|
|
* Logging out of Default will <i>always</i> completely log out the user.
|
|
|
|
*
|
2016-11-29 00:31:16 +01:00
|
|
|
* @param bool $redirect Redirect the user back to where they came.
|
|
|
|
* - If it's false, the code calling logout() is
|
|
|
|
* responsible for sending the user where-ever
|
|
|
|
* they should go.
|
2017-03-02 03:24:38 +01:00
|
|
|
* @return HTTPResponse|null
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
|
|
|
public function logout($redirect = true)
|
|
|
|
{
|
2017-04-30 05:17:26 +02:00
|
|
|
$this->extend('beforeMemberLoggedOut');
|
|
|
|
$request = $this->getRequest();
|
2016-11-29 00:31:16 +01:00
|
|
|
$member = Member::currentUser();
|
2017-04-30 05:17:26 +02:00
|
|
|
// Reasoning for (now) to not go with a full LoginHandler call, is to not make it circular
|
|
|
|
// re-sending the request forward to the authenticator. In the case of logout, I think it would be
|
|
|
|
// overkill.
|
|
|
|
if (($name = $request->param('ID')) && self::hasAuthenticator($request->param('ID'))){
|
|
|
|
/** @var Authenticator $authenticator */
|
|
|
|
$authenticator = $this->getAuthenticator($request->param('ID'));
|
|
|
|
if($authenticator->doLogOut($member) !== true) {
|
|
|
|
$this->extend('failureMemberLoggedOut', $authenticator);
|
|
|
|
return $this->redirectBack();
|
|
|
|
}
|
|
|
|
$this->extend('successMemberLoggedOut', $authenticator);
|
|
|
|
} else {
|
|
|
|
$authenticators = static::getAuthenticators(Authenticator::LOGOUT);
|
|
|
|
/**
|
|
|
|
* @var string $name
|
|
|
|
* @var Authenticator $authenticator
|
|
|
|
*/
|
|
|
|
foreach ($authenticators as $name => $authenticator) {
|
|
|
|
if ($authenticator->logOut($member) !== true) {
|
|
|
|
$this->extend('failureMemberLoggedOut', $authenticator);
|
|
|
|
// Break on first log out failure(?)
|
|
|
|
return $this->redirectBack();
|
|
|
|
}
|
|
|
|
$this->extend('successMemberLoggedOut', $authenticator);
|
|
|
|
}
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
2017-04-30 05:17:26 +02:00
|
|
|
// Member is successfully logged out. Write possible changes to the database.
|
|
|
|
$member->write();
|
|
|
|
$this->extend('afterMemberLoggedOut');
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
if ($redirect && (!$this->getResponse()->isFinished())) {
|
2017-03-02 03:24:38 +01:00
|
|
|
return $this->redirectBack();
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
2017-04-30 05:17:26 +02:00
|
|
|
|
2017-03-02 03:24:38 +01:00
|
|
|
return null;
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Perform pre-login checking and prepare a response if available prior to login
|
|
|
|
*
|
|
|
|
* @return HTTPResponse Substitute response object if the login process should be curcumvented.
|
|
|
|
* Returns null if should proceed as normal.
|
|
|
|
*/
|
|
|
|
protected function preLogin()
|
|
|
|
{
|
|
|
|
// Event handler for pre-login, with an option to let it break you out of the login form
|
|
|
|
$eventResults = $this->extend('onBeforeSecurityLogin');
|
|
|
|
// If there was a redirection, return
|
|
|
|
if ($this->redirectedTo()) {
|
|
|
|
return $this->getResponse();
|
|
|
|
}
|
|
|
|
// If there was an HTTPResponse object returned, then return that
|
|
|
|
if ($eventResults) {
|
|
|
|
foreach ($eventResults as $result) {
|
|
|
|
if ($result instanceof HTTPResponse) {
|
|
|
|
return $result;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// If arriving on the login page already logged in, with no security error, and a ReturnURL then redirect
|
|
|
|
// back. The login message check is neccesary to prevent infinite loops where BackURL links to
|
|
|
|
// an action that triggers Security::permissionFailure.
|
|
|
|
// This step is necessary in cases such as automatic redirection where a user is authenticated
|
|
|
|
// upon landing on an SSL secured site and is automatically logged in, or some other case
|
|
|
|
// where the user has permissions to continue but is not given the option.
|
|
|
|
if ($this->getRequest()->requestVar('BackURL')
|
|
|
|
&& !$this->getLoginMessage()
|
|
|
|
&& ($member = Member::currentUser())
|
|
|
|
&& $member->exists()
|
|
|
|
) {
|
|
|
|
return $this->redirectBack();
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Prepare the controller for handling the response to this request
|
|
|
|
*
|
|
|
|
* @param string $title Title to use
|
|
|
|
* @return Controller
|
|
|
|
*/
|
|
|
|
protected function getResponseController($title)
|
|
|
|
{
|
2017-03-02 03:24:38 +01:00
|
|
|
// Use the default setting for which Page to use to render the security page
|
|
|
|
$pageClass = $this->stat('page_class');
|
|
|
|
if (!$pageClass || !class_exists($pageClass)) {
|
2016-11-29 00:31:16 +01:00
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
2017-03-02 03:24:38 +01:00
|
|
|
// Create new instance of page holder
|
|
|
|
/** @var Page $holderPage */
|
|
|
|
$holderPage = new $pageClass;
|
|
|
|
$holderPage->Title = $title;
|
2016-11-29 00:31:16 +01:00
|
|
|
/** @skipUpgrade */
|
2017-03-02 03:24:38 +01:00
|
|
|
$holderPage->URLSegment = 'Security';
|
2016-11-29 00:31:16 +01:00
|
|
|
// Disable ID-based caching of the log-in page by making it a random number
|
2017-03-02 03:24:38 +01:00
|
|
|
$holderPage->ID = -1 * rand(1, 10000000);
|
2016-11-29 00:31:16 +01:00
|
|
|
|
2017-03-02 03:24:38 +01:00
|
|
|
$controllerClass = $holderPage->getControllerName();
|
|
|
|
/** @var ContentController $controller */
|
|
|
|
$controller = $controllerClass::create($holderPage);
|
2016-11-29 00:31:16 +01:00
|
|
|
$controller->setDataModel($this->model);
|
|
|
|
$controller->doInit();
|
|
|
|
return $controller;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Combine the given forms into a formset with a tabbed interface
|
|
|
|
*
|
2017-04-22 06:30:10 +02:00
|
|
|
* @param array $authenticators List of Authenticator instances
|
2016-11-29 00:31:16 +01:00
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
protected function generateLoginFormSet($forms)
|
|
|
|
{
|
|
|
|
$viewData = new ArrayData(array(
|
|
|
|
'Forms' => new ArrayList($forms),
|
|
|
|
));
|
|
|
|
return $viewData->renderWith(
|
|
|
|
$this->getTemplatesFor('MultiAuthenticatorLogin')
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the HTML Content for the $Content area during login
|
|
|
|
*
|
|
|
|
* @param string &$messageType Type of message, if available, passed back to caller
|
|
|
|
* @return string Message in HTML format
|
|
|
|
*/
|
|
|
|
protected function getLoginMessage(&$messageType = null)
|
|
|
|
{
|
|
|
|
$message = Session::get('Security.Message.message');
|
|
|
|
$messageType = null;
|
|
|
|
if (empty($message)) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
$messageType = Session::get('Security.Message.type');
|
2016-11-23 06:09:10 +01:00
|
|
|
$messageCast = Session::get('Security.Message.cast');
|
|
|
|
if ($messageCast !== ValidationResult::CAST_HTML) {
|
|
|
|
$message = Convert::raw2xml($message);
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
2016-11-23 06:09:10 +01:00
|
|
|
return sprintf('<p class="message %s">%s</p>', Convert::raw2att($messageType), $message);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set the next message to display for the security login page. Defaults to warning
|
|
|
|
*
|
|
|
|
* @param string $message Message
|
|
|
|
* @param string $messageType Message type. One of ValidationResult::TYPE_*
|
|
|
|
* @param string $messageCast Message cast. One of ValidationResult::CAST_*
|
|
|
|
*/
|
|
|
|
public static function setLoginMessage(
|
|
|
|
$message,
|
|
|
|
$messageType = ValidationResult::TYPE_WARNING,
|
|
|
|
$messageCast = ValidationResult::CAST_TEXT
|
|
|
|
) {
|
|
|
|
Session::set("Security.Message.message", $message);
|
|
|
|
Session::set("Security.Message.type", $messageType);
|
|
|
|
Session::set("Security.Message.cast", $messageCast);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Clear login message
|
|
|
|
*/
|
|
|
|
public static function clearLoginMessage()
|
|
|
|
{
|
|
|
|
Session::clear("Security.Message");
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Show the "login" page
|
|
|
|
*
|
|
|
|
* For multiple authenticators, Security_MultiAuthenticatorLogin is used.
|
|
|
|
* See getTemplatesFor and getIncludeTemplate for how to override template logic
|
|
|
|
*
|
2017-04-23 05:30:33 +02:00
|
|
|
* @param $request
|
|
|
|
* @return HTTPResponse|string Returns the "login" page as HTML code.
|
|
|
|
* @throws HTTPResponse_Exception
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
2017-04-30 05:17:26 +02:00
|
|
|
public function login($request, $service = Authenticator::LOGIN)
|
2016-11-29 00:31:16 +01:00
|
|
|
{
|
|
|
|
// Check pre-login process
|
|
|
|
if ($response = $this->preLogin()) {
|
|
|
|
return $response;
|
|
|
|
}
|
|
|
|
|
2017-04-22 06:30:10 +02:00
|
|
|
$link = $this->link("login");
|
2016-11-29 00:31:16 +01:00
|
|
|
|
2017-04-22 06:30:10 +02:00
|
|
|
// Delegate to a single handler - Security/login/<authname>/...
|
2017-04-30 05:17:26 +02:00
|
|
|
if (($name = $request->param('ID')) && self::hasAuthenticator($request->param('ID'))) {
|
2017-04-22 06:30:10 +02:00
|
|
|
$request->shift();
|
2016-11-29 00:31:16 +01:00
|
|
|
|
2017-04-23 05:30:33 +02:00
|
|
|
$authenticator = $this->getAuthenticator($name);
|
2017-04-30 05:17:26 +02:00
|
|
|
// @todo handle different Authenticator situations
|
|
|
|
if (!$authenticator->supportedServices() & $service) {
|
|
|
|
throw new HTTPResponse_Exception('Invalid Authenticator "' . $name . '" for login action', 418);
|
2017-04-22 06:30:10 +02:00
|
|
|
}
|
2016-11-29 00:31:16 +01:00
|
|
|
|
2017-04-23 05:30:33 +02:00
|
|
|
$authenticators = [ $name => $authenticator ];
|
2016-11-29 00:31:16 +01:00
|
|
|
|
2017-04-22 06:30:10 +02:00
|
|
|
// Delegate to all of them, building a tabbed view - Security/login
|
2016-11-29 00:31:16 +01:00
|
|
|
} else {
|
2017-04-30 05:17:26 +02:00
|
|
|
$authenticators = static::getAuthenticators($service);
|
2017-04-23 05:30:33 +02:00
|
|
|
}
|
2016-11-29 00:31:16 +01:00
|
|
|
|
2017-04-23 05:30:33 +02:00
|
|
|
$handlers = $authenticators;
|
|
|
|
array_walk(
|
|
|
|
$handlers,
|
|
|
|
function (&$auth, $name) use ($link) {
|
|
|
|
$auth = $auth->getLoginHandler(Controller::join_links($link, $name));
|
2017-04-22 06:30:10 +02:00
|
|
|
}
|
2017-04-23 05:30:33 +02:00
|
|
|
);
|
2016-11-29 00:31:16 +01:00
|
|
|
|
2017-04-23 05:30:33 +02:00
|
|
|
return $this->delegateToMultipleHandlers(
|
|
|
|
$handlers,
|
|
|
|
_t('Security.LOGIN', 'Log in'),
|
|
|
|
$this->getTemplatesFor('login')
|
|
|
|
);
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2017-04-22 06:30:10 +02:00
|
|
|
* Delegate to an number of handlers, extracting their forms and rendering a tabbed form-set.
|
|
|
|
* This is used to built the log-in page where there are multiple authenticators active.
|
2016-11-29 00:31:16 +01:00
|
|
|
*
|
2017-04-23 05:30:33 +02:00
|
|
|
* If a single handler is passed, delegateToHandler() will be called instead
|
|
|
|
*
|
2017-04-22 06:30:10 +02:00
|
|
|
* @param string $title The title of the form
|
|
|
|
* @param array $templates
|
|
|
|
* @return array|HTTPResponse|RequestHandler|\SilverStripe\ORM\FieldType\DBHTMLText|string
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
2017-04-23 05:30:33 +02:00
|
|
|
protected function delegateToMultipleHandlers(array $handlers, $title, array $templates)
|
2016-11-29 00:31:16 +01:00
|
|
|
{
|
|
|
|
|
2017-04-23 05:30:33 +02:00
|
|
|
// Simpler case for a single authenticator
|
|
|
|
if (count($handlers) === 1) {
|
|
|
|
return $this->delegateToHandler(array_values($handlers)[0], $title, $templates);
|
|
|
|
}
|
|
|
|
|
2017-04-22 06:30:10 +02:00
|
|
|
// Process each of the handlers
|
|
|
|
$results = array_map(
|
|
|
|
function ($handler) {
|
|
|
|
return $handler->handleRequest($this->getRequest(), \SilverStripe\ORM\DataModel::inst());
|
|
|
|
},
|
|
|
|
$handlers
|
|
|
|
);
|
|
|
|
|
|
|
|
// Aggregate all their forms, assuming they all return
|
|
|
|
$forms = [];
|
|
|
|
foreach ($results as $authName => $singleResult) {
|
|
|
|
// The result *must* be an array with a Form key
|
|
|
|
if (!is_array($singleResult) || !isset($singleResult['Form'])) {
|
|
|
|
user_error('Authenticator "' . $authName . '" doesn\'t support a tabbed login', E_USER_WARNING);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
$forms[] = $singleResult['Form'];
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
2017-04-22 06:30:10 +02:00
|
|
|
if (!$forms) {
|
|
|
|
throw new \LogicException("No authenticators found compatible with a tabbed login");
|
|
|
|
}
|
2016-11-29 00:31:16 +01:00
|
|
|
|
2017-04-22 06:30:10 +02:00
|
|
|
return $this->renderWrappedController(
|
|
|
|
$title,
|
|
|
|
[
|
|
|
|
'Form' => $this->generateLoginFormSet($forms),
|
|
|
|
],
|
|
|
|
$templates
|
|
|
|
);
|
2017-03-02 03:24:38 +01:00
|
|
|
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2017-04-22 06:30:10 +02:00
|
|
|
* Delegate to another RequestHandler, rendering any fragment arrays into an appropriate.
|
|
|
|
* controller.
|
2016-11-29 00:31:16 +01:00
|
|
|
*
|
2017-04-22 06:30:10 +02:00
|
|
|
* @param string $title The title of the form
|
|
|
|
* @param array $templates
|
|
|
|
* @return array|HTTPResponse|RequestHandler|\SilverStripe\ORM\FieldType\DBHTMLText|string
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
2017-04-22 06:30:10 +02:00
|
|
|
protected function delegateToHandler(RequestHandler $handler, $title, array $templates)
|
2016-11-29 00:31:16 +01:00
|
|
|
{
|
2017-04-30 05:17:26 +02:00
|
|
|
$result = $handler->handleRequest($this->getRequest(), DataModel::inst());
|
2017-04-22 06:30:10 +02:00
|
|
|
|
|
|
|
// Return the customised controller - used to render in a Form
|
|
|
|
// Post requests are expected to be login posts, so they'll be handled downstairs
|
|
|
|
if (is_array($result)) {
|
|
|
|
$result = $this->renderWrappedController($title, $result, $templates);
|
|
|
|
}
|
2016-11-29 00:31:16 +01:00
|
|
|
|
2017-04-22 06:30:10 +02:00
|
|
|
return $result;
|
|
|
|
}
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
/**
|
2017-04-22 06:30:10 +02:00
|
|
|
* Render the given fragments into a security page controller with the given title.
|
2017-04-23 05:30:33 +02:00
|
|
|
* @param string $title string The title to give the security page
|
|
|
|
* @param array $fragments A map of objects to render into the page, e.g. "Form"
|
|
|
|
* @param array $templates An array of templates to use for the render
|
|
|
|
* @return HTTPResponse|\SilverStripe\ORM\FieldType\DBHTMLText
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
2017-04-22 06:30:10 +02:00
|
|
|
protected function renderWrappedController($title, array $fragments, array $templates)
|
2016-11-29 00:31:16 +01:00
|
|
|
{
|
2017-04-22 06:30:10 +02:00
|
|
|
$controller = $this->getResponseController($title);
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
// if the controller calls Director::redirect(), this will break early
|
|
|
|
if (($response = $controller->getResponse()) && $response->isFinished()) {
|
|
|
|
return $response;
|
|
|
|
}
|
|
|
|
|
2017-04-22 06:30:10 +02:00
|
|
|
// Handle any form messages from validation, etc.
|
|
|
|
$messageType = '';
|
|
|
|
$message = $this->getLoginMessage($messageType);
|
2016-11-29 00:31:16 +01:00
|
|
|
|
2017-04-22 06:30:10 +02:00
|
|
|
// We've displayed the message in the form output, so reset it for the next run.
|
|
|
|
static::clearLoginMessage();
|
|
|
|
|
|
|
|
if ($message) {
|
|
|
|
$messageResult = [
|
|
|
|
'Content' => DBField::create_field('HTMLFragment', $message),
|
|
|
|
'Message' => DBField::create_field('HTMLFragment', $message),
|
|
|
|
'MessageType' => $messageType
|
|
|
|
];
|
2017-04-23 05:30:33 +02:00
|
|
|
$fragments = array_merge($fragments, $messageResult);
|
2017-04-22 06:30:10 +02:00
|
|
|
}
|
2016-11-29 00:31:16 +01:00
|
|
|
|
2017-04-22 06:30:10 +02:00
|
|
|
return $controller->customise($fragments)->renderWith($templates);
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
2017-04-22 06:30:10 +02:00
|
|
|
public function basicauthlogin()
|
|
|
|
{
|
|
|
|
$member = BasicAuth::requireLogin("SilverStripe login", 'ADMIN');
|
|
|
|
$member->logIn();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Show the "lost password" page
|
|
|
|
*
|
|
|
|
* @return string Returns the "lost password" page as HTML code.
|
|
|
|
*/
|
|
|
|
public function lostpassword()
|
|
|
|
{
|
2017-04-23 05:30:33 +02:00
|
|
|
$handler = $this->getAuthenticator('default')->getLostPasswordHandler(
|
2017-04-22 06:30:10 +02:00
|
|
|
Controller::join_links($this->link(), 'lostpassword')
|
|
|
|
);
|
|
|
|
|
|
|
|
return $this->delegateToHandler(
|
|
|
|
$handler,
|
|
|
|
_t('SilverStripe\\Security\\Security.LOSTPASSWORDHEADER', 'Lost Password'),
|
|
|
|
$this->getTemplatesFor('lostpassword')
|
|
|
|
);
|
|
|
|
}
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Show the "change password" page.
|
|
|
|
* This page can either be called directly by logged-in users
|
|
|
|
* (in which case they need to provide their old password),
|
|
|
|
* or through a link emailed through {@link lostpassword()}.
|
|
|
|
* In this case no old password is required, authentication is ensured
|
|
|
|
* through the Member.AutoLoginHash property.
|
|
|
|
*
|
|
|
|
* @see ChangePasswordForm
|
|
|
|
*
|
|
|
|
* @return string|HTTPRequest Returns the "change password" page as HTML code, or a redirect response
|
|
|
|
*/
|
|
|
|
public function changepassword()
|
|
|
|
{
|
2017-04-20 03:15:24 +02:00
|
|
|
$controller = $this->getResponseController(_t('SilverStripe\\Security\\Security.CHANGEPASSWORDHEADER', 'Change your password'));
|
2016-11-29 00:31:16 +01:00
|
|
|
|
|
|
|
// if the controller calls Director::redirect(), this will break early
|
|
|
|
if (($response = $controller->getResponse()) && $response->isFinished()) {
|
|
|
|
return $response;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Extract the member from the URL.
|
|
|
|
/** @var Member $member */
|
|
|
|
$member = null;
|
|
|
|
if (isset($_REQUEST['m'])) {
|
|
|
|
$member = Member::get()->filter('ID', (int)$_REQUEST['m'])->first();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check whether we are merely changin password, or resetting.
|
|
|
|
if (isset($_REQUEST['t']) && $member && $member->validateAutoLoginToken($_REQUEST['t'])) {
|
|
|
|
// On first valid password reset request redirect to the same URL without hash to avoid referrer leakage.
|
|
|
|
|
|
|
|
// if there is a current member, they should be logged out
|
|
|
|
if ($curMember = Member::currentUser()) {
|
|
|
|
$curMember->logOut();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Store the hash for the change password form. Will be unset after reload within the ChangePasswordForm.
|
|
|
|
Session::set('AutoLoginHash', $member->encryptWithUserSettings($_REQUEST['t']));
|
|
|
|
|
|
|
|
return $this->redirect($this->Link('changepassword'));
|
|
|
|
} elseif (Session::get('AutoLoginHash')) {
|
|
|
|
// Subsequent request after the "first load with hash" (see previous if clause).
|
|
|
|
$customisedController = $controller->customise(array(
|
2017-03-02 03:24:38 +01:00
|
|
|
'Content' => DBField::create_field(
|
|
|
|
'HTMLFragment',
|
2017-04-20 03:15:24 +02:00
|
|
|
'<p>' . _t('SilverStripe\\Security\\Security.ENTERNEWPASSWORD', 'Please enter a new password.') . '</p>'
|
2017-03-02 03:24:38 +01:00
|
|
|
),
|
2016-11-29 00:31:16 +01:00
|
|
|
'Form' => $this->ChangePasswordForm(),
|
|
|
|
));
|
|
|
|
} elseif (Member::currentUser()) {
|
|
|
|
// Logged in user requested a password change form.
|
|
|
|
$customisedController = $controller->customise(array(
|
2017-03-02 03:24:38 +01:00
|
|
|
'Content' => DBField::create_field(
|
|
|
|
'HTMLFragment',
|
2017-04-20 03:15:24 +02:00
|
|
|
'<p>' . _t('SilverStripe\\Security\\Security.CHANGEPASSWORDBELOW', 'You can change your password below.') . '</p>'
|
2017-03-02 03:24:38 +01:00
|
|
|
),
|
2016-11-29 00:31:16 +01:00
|
|
|
'Form' => $this->ChangePasswordForm()));
|
|
|
|
} else {
|
|
|
|
// Show friendly message if it seems like the user arrived here via password reset feature.
|
|
|
|
if (isset($_REQUEST['m']) || isset($_REQUEST['t'])) {
|
|
|
|
$customisedController = $controller->customise(
|
2017-03-02 03:24:38 +01:00
|
|
|
array('Content' => DBField::create_field(
|
|
|
|
'HTMLFragment',
|
2016-11-29 00:31:16 +01:00
|
|
|
_t(
|
2017-04-20 03:15:24 +02:00
|
|
|
'SilverStripe\\Security\\Security.NOTERESETLINKINVALID',
|
2016-11-29 00:31:16 +01:00
|
|
|
'<p>The password reset link is invalid or expired.</p>'
|
|
|
|
. '<p>You can request a new one <a href="{link1}">here</a> or change your password after'
|
|
|
|
. ' you <a href="{link2}">logged in</a>.</p>',
|
2017-03-02 03:24:38 +01:00
|
|
|
[
|
|
|
|
'link1' => $this->Link('lostpassword'),
|
|
|
|
'link2' => $this->Link('login')
|
|
|
|
]
|
2016-11-29 00:31:16 +01:00
|
|
|
)
|
2017-03-02 03:24:38 +01:00
|
|
|
))
|
2016-11-29 00:31:16 +01:00
|
|
|
);
|
|
|
|
} else {
|
|
|
|
return self::permissionFailure(
|
|
|
|
$this,
|
2017-04-20 03:15:24 +02:00
|
|
|
_t('SilverStripe\\Security\\Security.ERRORPASSWORDPERMISSION', 'You must be logged in in order to change your password!')
|
2016-11-29 00:31:16 +01:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $customisedController->renderWith($this->getTemplatesFor('changepassword'));
|
|
|
|
}
|
|
|
|
|
2017-04-23 05:30:33 +02:00
|
|
|
/**
|
|
|
|
* Create a link to the password reset form.
|
|
|
|
*
|
|
|
|
* GET parameters used:
|
|
|
|
* - m: member ID
|
|
|
|
* - t: plaintext token
|
|
|
|
*
|
|
|
|
* @param Member $member Member object associated with this link.
|
|
|
|
* @param string $autologinToken The auto login token.
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
public static function getPasswordResetLink($member, $autologinToken)
|
|
|
|
{
|
|
|
|
$autologinToken = urldecode($autologinToken);
|
|
|
|
$selfControllerClass = __CLASS__;
|
|
|
|
/** @var static $selfController */
|
|
|
|
$selfController = new $selfControllerClass();
|
|
|
|
return $selfController->Link('changepassword') . "?m={$member->ID}&t=$autologinToken";
|
|
|
|
}
|
|
|
|
|
2016-11-29 00:31:16 +01:00
|
|
|
/**
|
|
|
|
* Factory method for the lost password form
|
|
|
|
*
|
2017-03-02 03:24:38 +01:00
|
|
|
* @skipUpgrade
|
2017-04-30 05:17:26 +02:00
|
|
|
* @return MemberAuthenticator\ChangePasswordForm
|
2016-11-29 00:31:16 +01:00
|
|
|
*/
|
|
|
|
public function ChangePasswordForm()
|
|
|
|
{
|
2017-04-22 06:30:10 +02:00
|
|
|
return MemberAuthenticator\ChangePasswordForm::create($this, 'ChangePasswordForm');
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Determine the list of templates to use for rendering the given action.
|
|
|
|
*
|
|
|
|
* @skipUpgrade
|
|
|
|
* @param string $action
|
|
|
|
* @return array Template list
|
|
|
|
*/
|
|
|
|
public function getTemplatesFor($action)
|
|
|
|
{
|
2017-05-17 07:40:13 +02:00
|
|
|
$templates = SSViewer::get_templates_by_class(static::class, "_{$action}", __CLASS__);
|
2016-11-29 00:31:16 +01:00
|
|
|
return array_merge(
|
|
|
|
$templates,
|
|
|
|
[
|
|
|
|
"Security_{$action}",
|
|
|
|
"Security",
|
|
|
|
$this->stat("template_main"),
|
|
|
|
"BlankPage"
|
|
|
|
]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return an existing member with administrator privileges, or create one of necessary.
|
|
|
|
*
|
|
|
|
* Will create a default 'Administrators' group if no group is found
|
|
|
|
* with an ADMIN permission. Will create a new 'Admin' member with administrative permissions
|
|
|
|
* if no existing Member with these permissions is found.
|
|
|
|
*
|
|
|
|
* Important: Any newly created administrator accounts will NOT have valid
|
|
|
|
* login credentials (Email/Password properties), which means they can't be used for login
|
|
|
|
* purposes outside of any default credentials set through {@link Security::setDefaultAdmin()}.
|
|
|
|
*
|
|
|
|
* @return Member
|
|
|
|
*/
|
|
|
|
public static function findAnAdministrator()
|
|
|
|
{
|
|
|
|
// coupling to subsites module
|
|
|
|
$origSubsite = null;
|
|
|
|
if (is_callable('Subsite::changeSubsite')) {
|
|
|
|
$origSubsite = Subsite::currentSubsiteID();
|
|
|
|
Subsite::changeSubsite(0);
|
|
|
|
}
|
|
|
|
|
|
|
|
/** @var Member $member */
|
|
|
|
$member = null;
|
|
|
|
|
|
|
|
// find a group with ADMIN permission
|
|
|
|
$adminGroup = Permission::get_groups_by_permission('ADMIN')->first();
|
|
|
|
|
|
|
|
if (is_callable('Subsite::changeSubsite')) {
|
|
|
|
Subsite::changeSubsite($origSubsite);
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($adminGroup) {
|
|
|
|
$member = $adminGroup->Members()->First();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!$adminGroup) {
|
|
|
|
Group::singleton()->requireDefaultRecords();
|
|
|
|
$adminGroup = Permission::get_groups_by_permission('ADMIN')->first();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!$member) {
|
|
|
|
Member::singleton()->requireDefaultRecords();
|
|
|
|
$member = Permission::get_members_by_permission('ADMIN')->first();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!$member) {
|
|
|
|
$member = Member::default_admin();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!$member) {
|
|
|
|
// Failover to a blank admin
|
|
|
|
$member = Member::create();
|
2017-04-20 03:15:24 +02:00
|
|
|
$member->FirstName = _t('SilverStripe\\Security\\Member.DefaultAdminFirstname', 'Default Admin');
|
2016-11-29 00:31:16 +01:00
|
|
|
$member->write();
|
|
|
|
// Add member to group instead of adding group to member
|
|
|
|
// This bypasses the privilege escallation code in Member_GroupSet
|
|
|
|
$adminGroup
|
|
|
|
->DirectMembers()
|
|
|
|
->add($member);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $member;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Flush the default admin credentials
|
|
|
|
*/
|
|
|
|
public static function clear_default_admin()
|
|
|
|
{
|
|
|
|
self::$default_username = null;
|
|
|
|
self::$default_password = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set a default admin in dev-mode
|
|
|
|
*
|
|
|
|
* This will set a static default-admin which is not existing
|
|
|
|
* as a database-record. By this workaround we can test pages in dev-mode
|
|
|
|
* with a unified login. Submitted login-credentials are first checked
|
|
|
|
* against this static information in {@link Security::authenticate()}.
|
|
|
|
*
|
|
|
|
* @param string $username The user name
|
|
|
|
* @param string $password The password (in cleartext)
|
|
|
|
* @return bool True if successfully set
|
|
|
|
*/
|
|
|
|
public static function setDefaultAdmin($username, $password)
|
|
|
|
{
|
|
|
|
// don't overwrite if already set
|
|
|
|
if (self::$default_username || self::$default_password) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
self::$default_username = $username;
|
|
|
|
self::$default_password = $password;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Checks if the passed credentials are matching the default-admin.
|
|
|
|
* Compares cleartext-password set through Security::setDefaultAdmin().
|
|
|
|
*
|
|
|
|
* @param string $username
|
|
|
|
* @param string $password
|
|
|
|
* @return bool
|
|
|
|
*/
|
|
|
|
public static function check_default_admin($username, $password)
|
|
|
|
{
|
|
|
|
return (
|
|
|
|
self::$default_username === $username
|
|
|
|
&& self::$default_password === $password
|
|
|
|
&& self::has_default_admin()
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check that the default admin account has been set.
|
|
|
|
*/
|
|
|
|
public static function has_default_admin()
|
|
|
|
{
|
2017-04-30 05:17:26 +02:00
|
|
|
return !empty(self::$default_username) && !empty(self::$default_password) && (Director::get_environment_type() === 'dev');
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get default admin username
|
|
|
|
*
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
public static function default_admin_username()
|
|
|
|
{
|
|
|
|
return self::$default_username;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get default admin password
|
|
|
|
*
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
public static function default_admin_password()
|
|
|
|
{
|
|
|
|
return self::$default_password;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Encrypt a password according to the current password encryption settings.
|
|
|
|
* If the settings are so that passwords shouldn't be encrypted, the
|
|
|
|
* result is simple the clear text password with an empty salt except when
|
|
|
|
* a custom algorithm ($algorithm parameter) was passed.
|
|
|
|
*
|
|
|
|
* @param string $password The password to encrypt
|
|
|
|
* @param string $salt Optional: The salt to use. If it is not passed, but
|
|
|
|
* needed, the method will automatically create a
|
|
|
|
* random salt that will then be returned as return value.
|
|
|
|
* @param string $algorithm Optional: Use another algorithm to encrypt the
|
|
|
|
* password (so that the encryption algorithm can be changed over the time).
|
|
|
|
* @param Member $member Optional
|
|
|
|
* @return mixed Returns an associative array containing the encrypted
|
|
|
|
* password and the used salt in the form:
|
|
|
|
* <code>
|
|
|
|
* array(
|
|
|
|
* 'password' => string,
|
|
|
|
* 'salt' => string,
|
|
|
|
* 'algorithm' => string,
|
|
|
|
* 'encryptor' => PasswordEncryptor instance
|
|
|
|
* )
|
|
|
|
* </code>
|
|
|
|
* If the passed algorithm is invalid, FALSE will be returned.
|
|
|
|
*
|
|
|
|
* @see encrypt_passwords()
|
|
|
|
*/
|
|
|
|
public static function encrypt_password($password, $salt = null, $algorithm = null, $member = null)
|
|
|
|
{
|
|
|
|
// Fall back to the default encryption algorithm
|
|
|
|
if (!$algorithm) {
|
2017-03-02 03:24:38 +01:00
|
|
|
$algorithm = self::config()->get('password_encryption_algorithm');
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
$e = PasswordEncryptor::create_for_algorithm($algorithm);
|
|
|
|
|
|
|
|
// New salts will only need to be generated if the password is hashed for the first time
|
|
|
|
$salt = ($salt) ? $salt : $e->salt($password);
|
|
|
|
|
|
|
|
return array(
|
|
|
|
'password' => $e->encrypt($password, $salt, $member),
|
|
|
|
'salt' => $salt,
|
|
|
|
'algorithm' => $algorithm,
|
|
|
|
'encryptor' => $e
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Checks the database is in a state to perform security checks.
|
|
|
|
* See {@link DatabaseAdmin->init()} for more information.
|
|
|
|
*
|
|
|
|
* @return bool
|
|
|
|
*/
|
|
|
|
public static function database_is_ready()
|
|
|
|
{
|
|
|
|
// Used for unit tests
|
|
|
|
if (self::$force_database_is_ready !== null) {
|
|
|
|
return self::$force_database_is_ready;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (self::$database_is_ready) {
|
|
|
|
return self::$database_is_ready;
|
|
|
|
}
|
|
|
|
|
|
|
|
$requiredClasses = ClassInfo::dataClassesFor(Member::class);
|
|
|
|
$requiredClasses[] = Group::class;
|
|
|
|
$requiredClasses[] = Permission::class;
|
|
|
|
$schema = DataObject::getSchema();
|
|
|
|
foreach ($requiredClasses as $class) {
|
|
|
|
// Skip test classes, as not all test classes are scaffolded at once
|
|
|
|
if (is_a($class, TestOnly::class, true)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
// if any of the tables aren't created in the database
|
|
|
|
$table = $schema->tableName($class);
|
|
|
|
if (!ClassInfo::hasTable($table)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// HACK: DataExtensions aren't applied until a class is instantiated for
|
|
|
|
// the first time, so create an instance here.
|
|
|
|
singleton($class);
|
|
|
|
|
|
|
|
// if any of the tables don't have all fields mapped as table columns
|
|
|
|
$dbFields = DB::field_list($table);
|
|
|
|
if (!$dbFields) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
$objFields = $schema->databaseFields($class, false);
|
|
|
|
$missingFields = array_diff_key($objFields, $dbFields);
|
|
|
|
|
|
|
|
if ($missingFields) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
self::$database_is_ready = true;
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2017-04-23 05:30:33 +02:00
|
|
|
/**
|
|
|
|
* Resets the database_is_ready cache
|
|
|
|
*/
|
|
|
|
public static function clear_database_is_ready()
|
|
|
|
{
|
|
|
|
self::$database_is_ready = null;
|
|
|
|
self::$force_database_is_ready = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* For the database_is_ready call to return a certain value - used for testing
|
|
|
|
*/
|
|
|
|
public static function force_database_is_ready($isReady)
|
|
|
|
{
|
|
|
|
self::$force_database_is_ready = $isReady;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Enable or disable recording of login attempts
|
|
|
|
* through the {@link LoginRecord} object.
|
|
|
|
*
|
|
|
|
* @deprecated 4.0 Use the "Security.login_recording" config setting instead
|
|
|
|
* @param boolean $bool
|
|
|
|
*/
|
|
|
|
public static function set_login_recording($bool)
|
|
|
|
{
|
|
|
|
Deprecation::notice('4.0', 'Use the "Security.login_recording" config setting instead');
|
|
|
|
self::$login_recording = (bool)$bool;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @deprecated 4.0 Use the "Security.login_recording" config setting instead
|
|
|
|
* @return boolean
|
|
|
|
*/
|
|
|
|
public static function login_recording()
|
|
|
|
{
|
|
|
|
Deprecation::notice('4.0', 'Use the "Security.login_recording" config setting instead');
|
|
|
|
return self::$login_recording;
|
|
|
|
}
|
|
|
|
|
2016-11-29 00:31:16 +01:00
|
|
|
/**
|
|
|
|
* @config
|
|
|
|
* @var string Set the default login dest
|
|
|
|
* This is the URL that users will be redirected to after they log in,
|
|
|
|
* if they haven't logged in en route to access a secured page.
|
|
|
|
* By default, this is set to the homepage.
|
|
|
|
*/
|
|
|
|
private static $default_login_dest = "";
|
|
|
|
|
|
|
|
protected static $ignore_disallowed_actions = false;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set to true to ignore access to disallowed actions, rather than returning permission failure
|
|
|
|
* Note that this is just a flag that other code needs to check with Security::ignore_disallowed_actions()
|
|
|
|
* @param $flag True or false
|
|
|
|
*/
|
|
|
|
public static function set_ignore_disallowed_actions($flag)
|
|
|
|
{
|
|
|
|
self::$ignore_disallowed_actions = $flag;
|
|
|
|
}
|
|
|
|
|
|
|
|
public static function ignore_disallowed_actions()
|
|
|
|
{
|
|
|
|
return self::$ignore_disallowed_actions;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the URL of the log-in page.
|
|
|
|
*
|
|
|
|
* To update the login url use the "Security.login_url" config setting.
|
|
|
|
*
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
public static function login_url()
|
|
|
|
{
|
2017-03-02 03:24:38 +01:00
|
|
|
return Controller::join_links(Director::baseURL(), self::config()->get('login_url'));
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the URL of the logout page.
|
|
|
|
*
|
|
|
|
* To update the logout url use the "Security.logout_url" config setting.
|
|
|
|
*
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
public static function logout_url()
|
|
|
|
{
|
2017-03-02 03:24:38 +01:00
|
|
|
return Controller::join_links(Director::baseURL(), self::config()->get('logout_url'));
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the URL of the logout page.
|
|
|
|
*
|
|
|
|
* To update the logout url use the "Security.logout_url" config setting.
|
|
|
|
*
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
public static function lost_password_url()
|
|
|
|
{
|
2017-03-02 03:24:38 +01:00
|
|
|
return Controller::join_links(Director::baseURL(), self::config()->get('lost_password_url'));
|
2016-11-29 00:31:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Defines global accessible templates variables.
|
|
|
|
*
|
|
|
|
* @return array
|
|
|
|
*/
|
|
|
|
public static function get_template_global_variables()
|
|
|
|
{
|
|
|
|
return array(
|
|
|
|
"LoginURL" => "login_url",
|
|
|
|
"LogoutURL" => "logout_url",
|
|
|
|
"LostPasswordURL" => "lost_password_url",
|
|
|
|
);
|
|
|
|
}
|
2011-03-25 00:01:50 +01:00
|
|
|
}
|