Completed
Push — master ( 327ddb...5776a0 )
by Daniel
17:43
created

Security::default_admin_password()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 3
rs 10
1
<?php
2
3
namespace SilverStripe\Security;
4
5
use Form;
6
use SilverStripe\ORM\ArrayList;
7
use SilverStripe\ORM\DataObject;
8
use SilverStripe\ORM\DB;
9
use Controller;
10
use SS_HTTPRequest;
11
use TemplateGlobalProvider;
12
use Deprecation;
13
use Director;
14
use SS_HTTPResponse;
15
use Session;
16
use Config;
17
use Exception;
18
use Page;
19
use Page_Controller;
20
use ArrayData;
21
use FieldList;
22
use EmailField;
23
use FormAction;
24
use Convert;
25
use Object;
26
use ClassInfo;
27
28
/**
29
 * Implements a basic security model
30
 * @package framework
31
 * @subpackage security
32
 */
33
class Security extends Controller implements TemplateGlobalProvider {
34
35
	private static $allowed_actions = array(
36
		'index',
37
		'login',
38
		'logout',
39
		'basicauthlogin',
40
		'lostpassword',
41
		'passwordsent',
42
		'changepassword',
43
		'ping',
44
		'LoginForm',
45
		'ChangePasswordForm',
46
		'LostPasswordForm',
47
	);
48
49
	/**
50
	 * Default user name. Only used in dev-mode by {@link setDefaultAdmin()}
51
	 *
52
	 * @var string
53
	 * @see setDefaultAdmin()
54
	 */
55
	protected static $default_username;
56
57
	/**
58
	 * Default password. Only used in dev-mode by {@link setDefaultAdmin()}
59
	 *
60
	 * @var string
61
	 * @see setDefaultAdmin()
62
	 */
63
	protected static $default_password;
64
65
	/**
66
	 * If set to TRUE to prevent sharing of the session across several sites
67
	 * in the domain.
68
	 *
69
	 * @config
70
	 * @var bool
71
	 */
72
	protected static $strict_path_checking = false;
73
74
	/**
75
	 * The password encryption algorithm to use by default.
76
	 * This is an arbitrary code registered through {@link PasswordEncryptor}.
77
	 *
78
	 * @config
79
	 * @var string
80
	 */
81
	private static $password_encryption_algorithm = 'blowfish';
82
83
	/**
84
	 * Showing "Remember me"-checkbox
85
	 * on loginform, and saving encrypted credentials to a cookie.
86
 	 *
87
 	 * @config
88
	 * @var bool
89
	 */
90
	private static $autologin_enabled = true;
91
92
	/**
93
	 * Determine if login username may be remembered between login sessions
94
	 * If set to false this will disable autocomplete and prevent username persisting in the session
95
	 *
96
	 * @config
97
	 * @var bool
98
	 */
99
	private static $remember_username = true;
100
101
	/**
102
	 * Location of word list to use for generating passwords
103
	 *
104
	 * @config
105
	 * @var string
106
	 */
107
	private static $word_list = './wordlist.txt';
108
109
	/**
110
	 * @config
111
	 * @var string
112
	 */
113
	private static $template = 'BlankPage';
114
115
	/**
116
	 * Template thats used to render the pages.
117
	 *
118
	 * @var string
119
	 * @config
120
	 */
121
	private static $template_main = 'Page';
122
123
	/**
124
	 * Default message set used in permission failures.
125
	 *
126
	 * @config
127
	 * @var array|string
128
	 */
129
	private static $default_message_set;
130
131
	/**
132
	 * Random secure token, can be used as a crypto key internally.
133
	 * Generate one through 'sake dev/generatesecuretoken'.
134
	 *
135
	 * @config
136
	 * @var String
137
	 */
138
	private static $token;
139
140
	/**
141
	 * The default login URL
142
	 *
143
	 * @config
144
	 *
145
	 * @var string
146
	 */
147
	private static $login_url = "Security/login";
148
149
	/**
150
	 * The default logout URL
151
	 *
152
	 * @config
153
	 *
154
	 * @var string
155
	 */
156
	private static $logout_url = "Security/logout";
157
158
	/**
159
	 * The default lost password URL
160
	 *
161
	 * @config
162
	 *
163
	 * @var string
164
	 */
165
	private static $lost_password_url = "Security/lostpassword";
166
167
	/**
168
	 * Get location of word list file
169
	 *
170
	 * @deprecated 4.0 Use the "Security.word_list" config setting instead
171
	 */
172
	public static function get_word_list() {
173
		Deprecation::notice('4.0', 'Use the "Security.word_list" config setting instead');
174
		return self::config()->word_list;
175
	}
176
177
	/**
178
	 * Enable or disable recording of login attempts
179
	 * through the {@link LoginRecord} object.
180
	 *
181
	 * @config
182
	 * @var boolean $login_recording
183
	 */
184
	private static $login_recording = false;
185
186
	/**
187
	 * @var boolean If set to TRUE or FALSE, {@link database_is_ready()}
188
	 * will always return FALSE. Used for unit testing.
189
	 */
190
	static $force_database_is_ready = null;
0 ignored issues
show
Coding Style introduced by
The visibility should be declared for property $force_database_is_ready.

The PSR-2 coding standard requires that all properties in a class have their visibility explicitly declared. If you declare a property using

class A {
    var $property;
}

the property is implicitly global.

To learn more about the PSR-2, please see the PHP-FIG site on the PSR-2.

Loading history...
191
192
	/**
193
	 * When the database has once been verified as ready, it will not do the
194
	 * checks again.
195
	 *
196
	 * @var bool
197
	 */
198
	static $database_is_ready = false;
0 ignored issues
show
Coding Style introduced by
The visibility should be declared for property $database_is_ready.

The PSR-2 coding standard requires that all properties in a class have their visibility explicitly declared. If you declare a property using

class A {
    var $property;
}

the property is implicitly global.

To learn more about the PSR-2, please see the PHP-FIG site on the PSR-2.

Loading history...
199
200
	/**
201
	 * Set location of word list file
202
	 *
203
	 * @deprecated 4.0 Use the "Security.word_list" config setting instead
204
	 * @param string $wordListFile Location of word list file
205
	 */
206
	public static function set_word_list($wordListFile) {
207
		Deprecation::notice('4.0', 'Use the "Security.word_list" config setting instead');
208
		self::config()->word_list = $wordListFile;
0 ignored issues
show
Documentation introduced by
The property word_list does not exist on object<Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
209
	}
210
211
	/**
212
	 * Set the default message set used in permissions failures.
213
	 *
214
	 * @deprecated 4.0 Use the "Security.default_message_set" config setting instead
215
	 * @param string|array $messageSet
216
	 */
217
	public static function set_default_message_set($messageSet) {
218
		Deprecation::notice('4.0', 'Use the "Security.default_message_set" config setting instead');
219
		self::config()->default_message_set = $messageSet;
0 ignored issues
show
Documentation introduced by
The property default_message_set does not exist on object<Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
220
	}
221
222
223
	/**
224
	 * Register that we've had a permission failure trying to view the given page
225
	 *
226
	 * This will redirect to a login page.
227
	 * If you don't provide a messageSet, a default will be used.
228
	 *
229
	 * @param Controller $controller The controller that you were on to cause the permission
230
	 *                               failure.
231
	 * @param string|array $messageSet The message to show to the user. This
232
	 *                                 can be a string, or a map of different
233
	 *                                 messages for different contexts.
234
	 *                                 If you pass an array, you can use the
235
	 *                                 following keys:
236
	 *                                   - default: The default message
237
	 *                                   - alreadyLoggedIn: The message to
238
	 *                                                      show if the user
239
	 *                                                      is already logged
240
	 *                                                      in and lacks the
241
	 *                                                      permission to
242
	 *                                                      access the item.
243
	 *
244
	 * The alreadyLoggedIn value can contain a '%s' placeholder that will be replaced with a link
245
	 * to log in.
246
	 * @return SS_HTTPResponse
247
	 */
248
	public static function permissionFailure($controller = null, $messageSet = null) {
0 ignored issues
show
Coding Style introduced by
permissionFailure uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
249
		self::set_ignore_disallowed_actions(true);
250
251
		if(!$controller) $controller = Controller::curr();
252
253
		if(Director::is_ajax()) {
254
			$response = ($controller) ? $controller->getResponse() : new SS_HTTPResponse();
255
			$response->setStatusCode(403);
256
			if(!Member::currentUser()) {
257
				$response->setBody(_t('ContentController.NOTLOGGEDIN','Not logged in'));
258
				$response->setStatusDescription(_t('ContentController.NOTLOGGEDIN','Not logged in'));
259
				// Tell the CMS to allow re-aunthentication
260
				if(CMSSecurity::enabled()) {
261
					$response->addHeader('X-Reauthenticate', '1');
262
				}
263
			}
264
			return $response;
265
		}
266
267
		// Prepare the messageSet provided
268
		if(!$messageSet) {
269
			if($configMessageSet = static::config()->get('default_message_set')) {
270
				$messageSet = $configMessageSet;
271
			} else {
272
				$messageSet = array(
273
					'default' => _t(
274
						'Security.NOTEPAGESECURED',
275
						"That page is secured. Enter your credentials below and we will send "
276
							. "you right along."
277
					),
278
					'alreadyLoggedIn' => _t(
279
						'Security.ALREADYLOGGEDIN',
280
						"You don't have access to this page.  If you have another account that "
281
							. "can access that page, you can log in again below.",
282
283
						"%s will be replaced with a link to log in."
284
					)
285
				);
286
			}
287
		}
288
289
		if(!is_array($messageSet)) {
290
			$messageSet = array('default' => $messageSet);
291
		}
292
293
		$member = Member::currentUser();
294
295
		// Work out the right message to show
296
		if($member && $member->exists()) {
297
			$response = ($controller) ? $controller->getResponse() : new SS_HTTPResponse();
298
			$response->setStatusCode(403);
299
300
			//If 'alreadyLoggedIn' is not specified in the array, then use the default
301
			//which should have been specified in the lines above
302
			if(isset($messageSet['alreadyLoggedIn'])) {
303
				$message = $messageSet['alreadyLoggedIn'];
304
			} else {
305
				$message = $messageSet['default'];
306
			}
307
308
			// Somewhat hackish way to render a login form with an error message.
309
			$me = new Security();
310
			$form = $me->LoginForm();
311
			$form->sessionMessage($message, 'warning');
312
			Session::set('MemberLoginForm.force_message',1);
313
			$loginResponse = $me->login();
314
			if($loginResponse instanceof SS_HTTPResponse) {
315
				return $loginResponse;
316
			}
317
318
			$response->setBody((string)$loginResponse);
319
320
			$controller->extend('permissionDenied', $member);
321
322
			return $response;
323
		} else {
324
			$message = $messageSet['default'];
325
		}
326
327
		Session::set("Security.Message.message", $message);
328
		Session::set("Security.Message.type", 'warning');
329
330
		Session::set("BackURL", $_SERVER['REQUEST_URI']);
331
332
		// TODO AccessLogEntry needs an extension to handle permission denied errors
333
		// Audit logging hook
334
		$controller->extend('permissionDenied', $member);
335
336
		return $controller->redirect(
337
			Config::inst()->get('SilverStripe\\Security\\Security', 'login_url')
338
			. "?BackURL=" . urlencode($_SERVER['REQUEST_URI'])
339
		);
340
	}
341
342
	protected function init() {
343
		parent::init();
344
345
		// Prevent clickjacking, see https://developer.mozilla.org/en-US/docs/HTTP/X-Frame-Options
346
		$this->getResponse()->addHeader('X-Frame-Options', 'SAMEORIGIN');
347
	}
348
349
	public function index() {
350
		return $this->httpError(404); // no-op
351
	}
352
353
	/**
354
	 * Get the selected authenticator for this request
355
	 *
356
	 * @return string Class name of Authenticator
357
	 */
358
	protected function getAuthenticator() {
359
		$authenticator = $this->getRequest()->requestVar('AuthenticationMethod');
360
		if($authenticator) {
361
			$authenticators = Authenticator::get_authenticators();
362
			if(in_array($authenticator, $authenticators)) {
363
				return $authenticator;
364
			}
365
		} else {
366
			return Authenticator::get_default_authenticator();
367
		}
368
369
	}
370
371
	/**
372
	 * Get the login form to process according to the submitted data
373
	 *
374
	 * @return Form
375
	 * @throws Exception
376
	 */
377
	public function LoginForm() {
378
		$authenticator = $this->getAuthenticator();
379
		if($authenticator) return $authenticator::get_login_form($this);
380
		throw new Exception('Passed invalid authentication method');
381
	}
382
383
	/**
384
	 * Get the login forms for all available authentication methods
385
	 *
386
	 * @return array Returns an array of available login forms (array of Form
387
	 *               objects).
388
	 *
389
	 * @todo Check how to activate/deactivate authentication methods
390
	 */
391
	public function GetLoginForms() {
392
		$forms = array();
393
394
		$authenticators = Authenticator::get_authenticators();
395
		foreach($authenticators as $authenticator) {
396
			$forms[] = $authenticator::get_login_form($this);
397
		}
398
399
		return $forms;
400
	}
401
402
403
	/**
404
	 * Get a link to a security action
405
	 *
406
	 * @param string $action Name of the action
407
	 * @return string Returns the link to the given action
408
	 */
409
	public function Link($action = null) {
410
		/** @skipUpgrade */
411
		return Controller::join_links(Director::baseURL(), "Security", $action);
412
	}
413
414
	/**
415
	 * This action is available as a keep alive, so user
416
	 * sessions don't timeout. A common use is in the admin.
417
	 */
418
	public function ping() {
419
		return 1;
420
	}
421
422
	/**
423
	 * Log the currently logged in user out
424
	 *
425
	 * @param bool $redirect Redirect the user back to where they came.
426
	 *                       - If it's false, the code calling logout() is
427
	 *                         responsible for sending the user where-ever
428
	 *                         they should go.
429
	 */
430
	public function logout($redirect = true) {
431
		$member = Member::currentUser();
432
		if($member) $member->logOut();
433
434
		if($redirect && (!$this->getResponse()->isFinished())) {
435
			$this->redirectBack();
436
		}
437
	}
438
439
	/**
440
	 * Perform pre-login checking and prepare a response if available prior to login
441
	 *
442
	 * @return SS_HTTPResponse Substitute response object if the login process should be curcumvented.
443
	 * Returns null if should proceed as normal.
444
	 */
445
	protected function preLogin() {
446
		// Event handler for pre-login, with an option to let it break you out of the login form
447
		$eventResults = $this->extend('onBeforeSecurityLogin');
448
		// If there was a redirection, return
449
		if($this->redirectedTo()) return $this->getResponse();
450
		// If there was an SS_HTTPResponse object returned, then return that
451
		if($eventResults) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $eventResults of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
452
			foreach($eventResults as $result) {
453
				if($result instanceof SS_HTTPResponse) return $result;
454
			}
455
		}
456
457
		// If arriving on the login page already logged in, with no security error, and a ReturnURL then redirect
458
		// back. The login message check is neccesary to prevent infinite loops where BackURL links to
459
		// an action that triggers Security::permissionFailure.
460
		// This step is necessary in cases such as automatic redirection where a user is authenticated
461
		// upon landing on an SSL secured site and is automatically logged in, or some other case
462
		// where the user has permissions to continue but is not given the option.
463
		if($this->getRequest()->requestVar('BackURL')
464
			&& !$this->getLoginMessage()
465
			&& ($member = Member::currentUser())
466
			&& $member->exists()
467
		) {
468
			return $this->redirectBack();
469
		}
470
	}
471
472
	/**
473
	 * Prepare the controller for handling the response to this request
474
	 *
475
	 * @param string $title Title to use
476
	 * @return Controller
477
	 */
478
	protected function getResponseController($title) {
479
		if(!class_exists('SiteTree')) return $this;
480
481
		// Use sitetree pages to render the security page
482
		$tmpPage = new Page();
483
		$tmpPage->Title = $title;
484
		/** @skipUpgrade */
485
		$tmpPage->URLSegment = "Security";
486
		// Disable ID-based caching  of the log-in page by making it a random number
487
		$tmpPage->ID = -1 * rand(1,10000000);
488
489
		$controller = Page_Controller::create($tmpPage);
490
		$controller->setDataModel($this->model);
491
		$controller->doInit();
492
		return $controller;
493
	}
494
495
	/**
496
	 * Determine the list of templates to use for rendering the given action
497
	 *
498
	 * @param string $action
499
	 * @return array Template list
500
	 */
501
	public function getTemplatesFor($action) {
502
		/** @skipUpgrade */
503
		return array("Security_{$action}", 'Security', $this->stat('template_main'), 'BlankPage');
504
	}
505
506
	/**
507
	 * Combine the given forms into a formset with a tabbed interface
508
	 *
509
	 * @param array $forms List of LoginForm instances
510
	 * @return string
511
	 */
512
	protected function generateLoginFormSet($forms) {
513
		$viewData = new ArrayData(array(
514
			'Forms' => new ArrayList($forms),
515
		));
516
		return $viewData->renderWith(
517
			$this->getIncludeTemplate('MultiAuthenticatorLogin')
518
		);
519
	}
520
521
	/**
522
	 * Get the HTML Content for the $Content area during login
523
	 *
524
	 * @param string &$messageType Type of message, if available, passed back to caller
525
	 * @return string Message in HTML format
526
	 */
527
	protected function getLoginMessage(&$messageType = null) {
528
		$message = Session::get('Security.Message.message');
529
		$messageType = null;
530
		if(empty($message)) return null;
531
532
		$messageType = Session::get('Security.Message.type');
533
		if($messageType === 'bad') {
534
			return "<p class=\"message $messageType\">$message</p>";
535
		} else {
536
			return "<p>$message</p>";
537
		}
538
	}
539
540
541
	/**
542
	 * Show the "login" page
543
	 *
544
	 * For multiple authenticators, Security_MultiAuthenticatorLogin is used.
545
	 * See getTemplatesFor and getIncludeTemplate for how to override template logic
546
	 *
547
	 * @return string|SS_HTTPResponse Returns the "login" page as HTML code.
548
	 */
549
	public function login() {
550
		// Check pre-login process
551
		if($response = $this->preLogin()) return $response;
552
553
		// Get response handler
554
		$controller = $this->getResponseController(_t('Security.LOGIN', 'Log in'));
555
556
		// if the controller calls Director::redirect(), this will break early
557
		if(($response = $controller->getResponse()) && $response->isFinished()) return $response;
558
559
		$forms = $this->GetLoginForms();
560
		if(!count($forms)) {
561
			user_error('No login-forms found, please use Authenticator::register_authenticator() to add one',
562
				E_USER_ERROR);
563
		}
564
565
		// Handle any form messages from validation, etc.
566
		$messageType = '';
567
		$message = $this->getLoginMessage($messageType);
568
569
		// We've displayed the message in the form output, so reset it for the next run.
570
		Session::clear('Security.Message');
571
572
		// only display tabs when more than one authenticator is provided
573
		// to save bandwidth and reduce the amount of custom styling needed
574
		if(count($forms) > 1) {
575
			$content = $this->generateLoginFormSet($forms);
576
		} else {
577
			$content = $forms[0]->forTemplate();
578
		}
579
580
		// Finally, customise the controller to add any form messages and the form.
581
		$customisedController = $controller->customise(array(
582
			"Content" => $message,
583
			"Message" => $message,
584
			"MessageType" => $messageType,
585
			"Form" => $content,
586
		));
587
588
		// Return the customised controller
589
		return $customisedController->renderWith(
590
			$this->getTemplatesFor('login')
591
		);
592
	}
593
594
	public function basicauthlogin() {
595
		$member = BasicAuth::requireLogin("SilverStripe login", 'ADMIN');
596
		$member->logIn();
597
	}
598
599
	/**
600
	 * Show the "lost password" page
601
	 *
602
	 * @return string Returns the "lost password" page as HTML code.
603
	 */
604
	public function lostpassword() {
605
		$controller = $this->getResponseController(_t('Security.LOSTPASSWORDHEADER', 'Lost Password'));
606
607
		// if the controller calls Director::redirect(), this will break early
608
		if(($response = $controller->getResponse()) && $response->isFinished()) return $response;
609
610
		$customisedController = $controller->customise(array(
611
			'Content' =>
612
				'<p>' .
613
				_t(
614
					'Security.NOTERESETPASSWORD',
615
					'Enter your e-mail address and we will send you a link with which you can reset your password'
616
				) .
617
				'</p>',
618
			'Form' => $this->LostPasswordForm(),
619
		));
620
621
		//Controller::$currentController = $controller;
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
622
		return $customisedController->renderWith($this->getTemplatesFor('lostpassword'));
623
	}
624
625
626
	/**
627
	 * Factory method for the lost password form
628
	 *
629
	 * @return Form Returns the lost password form
630
	 */
631
	public function LostPasswordForm() {
632
		return MemberLoginForm::create(
633
			$this,
634
			'LostPasswordForm',
635
			new FieldList(
636
				new EmailField('Email', _t('Member.EMAIL', 'Email'))
637
			),
638
			new FieldList(
639
				new FormAction(
640
					'forgotPassword',
641
					_t('Security.BUTTONSEND', 'Send me the password reset link')
642
				)
643
			),
644
			false
645
		);
646
	}
647
648
649
	/**
650
	 * Show the "password sent" page, after a user has requested
651
	 * to reset their password.
652
	 *
653
	 * @param SS_HTTPRequest $request The SS_HTTPRequest for this action.
654
	 * @return string Returns the "password sent" page as HTML code.
655
	 */
656
	public function passwordsent($request) {
657
		$controller = $this->getResponseController(_t('Security.LOSTPASSWORDHEADER', 'Lost Password'));
658
659
		// if the controller calls Director::redirect(), this will break early
660
		if(($response = $controller->getResponse()) && $response->isFinished()) return $response;
661
662
		$email = Convert::raw2xml(rawurldecode($request->param('ID')) . '.' . $request->getExtension());
663
664
		$customisedController = $controller->customise(array(
665
			'Title' => _t('Security.PASSWORDSENTHEADER', "Password reset link sent to '{email}'",
666
				array('email' => $email)),
667
			'Content' =>
668
				"<p>"
669
				. _t('Security.PASSWORDSENTTEXT',
670
					"Thank you! A reset link has been sent to '{email}', provided an account exists for this email"
671
					. " address.",
672
					array('email' => $email))
673
				. "</p>",
674
			'Email' => $email
675
		));
676
677
		//Controller::$currentController = $controller;
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
678
		return $customisedController->renderWith($this->getTemplatesFor('passwordsent'));
679
	}
680
681
682
	/**
683
	 * Create a link to the password reset form.
684
	 *
685
	 * GET parameters used:
686
	 * - m: member ID
687
	 * - t: plaintext token
688
	 *
689
	 * @param Member $member Member object associated with this link.
690
	 * @param string $autologinToken The auto login token.
691
	 * @return string
692
	 */
693
	public static function getPasswordResetLink($member, $autologinToken) {
694
		$autologinToken = urldecode($autologinToken);
695
		$selfControllerClass = __CLASS__;
696
		$selfController = new $selfControllerClass();
697
		return $selfController->Link('changepassword') . "?m={$member->ID}&t=$autologinToken";
698
	}
699
700
	/**
701
	 * Show the "change password" page.
702
	 * This page can either be called directly by logged-in users
703
	 * (in which case they need to provide their old password),
704
	 * or through a link emailed through {@link lostpassword()}.
705
	 * In this case no old password is required, authentication is ensured
706
	 * through the Member.AutoLoginHash property.
707
	 *
708
	 * @see ChangePasswordForm
709
	 *
710
	 * @return string Returns the "change password" page as HTML code.
711
	 */
712
	public function changepassword() {
0 ignored issues
show
Coding Style introduced by
changepassword uses the super-global variable $_REQUEST which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
713
		$controller = $this->getResponseController(_t('Security.CHANGEPASSWORDHEADER', 'Change your password'));
714
715
		// if the controller calls Director::redirect(), this will break early
716
		if(($response = $controller->getResponse()) && $response->isFinished()) return $response;
717
718
		// Extract the member from the URL.
719
		$member = null;
720
		if (isset($_REQUEST['m'])) {
721
			$member = Member::get()->filter('ID', (int)$_REQUEST['m'])->first();
722
		}
723
724
		// Check whether we are merely changin password, or resetting.
725
		if(isset($_REQUEST['t']) && $member && $member->validateAutoLoginToken($_REQUEST['t'])) {
726
			// On first valid password reset request redirect to the same URL without hash to avoid referrer leakage.
727
728
			// if there is a current member, they should be logged out
729
			if ($curMember = Member::currentUser()) {
730
				$curMember->logOut();
731
			}
732
733
			// Store the hash for the change password form. Will be unset after reload within the ChangePasswordForm.
734
			Session::set('AutoLoginHash', $member->encryptWithUserSettings($_REQUEST['t']));
735
736
			return $this->redirect($this->Link('changepassword'));
737
		} elseif(Session::get('AutoLoginHash')) {
738
			// Subsequent request after the "first load with hash" (see previous if clause).
739
			$customisedController = $controller->customise(array(
740
				'Content' =>
741
					'<p>' .
742
					_t('Security.ENTERNEWPASSWORD', 'Please enter a new password.') .
743
					'</p>',
744
				'Form' => $this->ChangePasswordForm(),
745
			));
746
		} elseif(Member::currentUser()) {
747
			// Logged in user requested a password change form.
748
			$customisedController = $controller->customise(array(
749
				'Content' => '<p>'
750
					. _t('Security.CHANGEPASSWORDBELOW', 'You can change your password below.') . '</p>',
751
				'Form' => $this->ChangePasswordForm()));
752
753
		} else {
754
			// Show friendly message if it seems like the user arrived here via password reset feature.
755
			if(isset($_REQUEST['m']) || isset($_REQUEST['t'])) {
756
				$customisedController = $controller->customise(
757
					array('Content' =>
758
						_t(
759
							'Security.NOTERESETLINKINVALID',
760
							'<p>The password reset link is invalid or expired.</p>'
761
							. '<p>You can request a new one <a href="{link1}">here</a> or change your password after'
762
							. ' you <a href="{link2}">logged in</a>.</p>',
763
							array('link1' => $this->Link('lostpassword'), 'link2' => $this->link('login'))
764
						)
765
					)
766
				);
767
			} else {
768
				self::permissionFailure(
769
					$this,
770
					_t('Security.ERRORPASSWORDPERMISSION', 'You must be logged in in order to change your password!')
771
				);
772
				return;
773
			}
774
		}
775
776
		return $customisedController->renderWith($this->getTemplatesFor('changepassword'));
777
	}
778
779
	/**
780
	 * Factory method for the lost password form
781
	 *
782
	 * @return ChangePasswordForm Returns the lost password form
783
	 */
784
	public function ChangePasswordForm() {
785
		/** @skipUpgrade */
786
		$formName = 'ChangePasswordForm';
787
		return \Injector::inst()->createWithArgs(
788
			'SilverStripe\\Security\\ChangePasswordForm',
789
			[ $this,  $formName]
790
		);
791
	}
792
793
	/**
794
	 * Gets the template for an include used for security.
795
	 * For use in any subclass.
796
	 *
797
	 * @param string $name
798
	 * @return array Returns the template(s) for rendering
799
	 */
800
	public function getIncludeTemplate($name) {
801
		return array('Security_' . $name);
802
	}
803
804
	/**
805
	 * Return an existing member with administrator privileges, or create one of necessary.
806
	 *
807
	 * Will create a default 'Administrators' group if no group is found
808
	 * with an ADMIN permission. Will create a new 'Admin' member with administrative permissions
809
	 * if no existing Member with these permissions is found.
810
	 *
811
	 * Important: Any newly created administrator accounts will NOT have valid
812
	 * login credentials (Email/Password properties), which means they can't be used for login
813
	 * purposes outside of any default credentials set through {@link Security::setDefaultAdmin()}.
814
	 *
815
	 * @return Member
816
	 */
817
	public static function findAnAdministrator() {
818
		// coupling to subsites module
819
		$origSubsite = null;
820
		if(is_callable('Subsite::changeSubsite')) {
821
			$origSubsite = \Subsite::currentSubsiteID();
822
			\Subsite::changeSubsite(0);
823
		}
824
825
		$member = null;
826
827
		// find a group with ADMIN permission
828
		$adminGroup = Permission::get_groups_by_permission('ADMIN')->first();
829
830
		if(is_callable('Subsite::changeSubsite')) {
831
			\Subsite::changeSubsite($origSubsite);
832
		}
833
834
		if ($adminGroup) {
835
			$member = $adminGroup->Members()->First();
836
		}
837
838
		if(!$adminGroup) {
839
			Group::singleton()->requireDefaultRecords();
840
			$adminGroup = Permission::get_groups_by_permission('ADMIN')->first();
841
		}
842
843
		if(!$member) {
844
			Member::singleton()->requireDefaultRecords();
845
			$member = Permission::get_members_by_permission('ADMIN')->first();
846
		}
847
848
		if(!$member) {
849
			$member = Member::default_admin();
850
		}
851
852
		if(!$member) {
853
			// Failover to a blank admin
854
			$member = Member::create();
855
			$member->FirstName = _t('Member.DefaultAdminFirstname', 'Default Admin');
856
			$member->write();
857
			// Add member to group instead of adding group to member
858
			// This bypasses the privilege escallation code in Member_GroupSet
859
			$adminGroup
860
				->DirectMembers()
861
				->add($member);
862
		}
863
864
		return $member;
865
	}
866
867
	/**
868
	 * Flush the default admin credentials
869
	 */
870
	public static function clear_default_admin() {
871
		self::$default_username = null;
872
		self::$default_password = null;
873
	}
874
875
876
	/**
877
	 * Set a default admin in dev-mode
878
	 *
879
	 * This will set a static default-admin which is not existing
880
	 * as a database-record. By this workaround we can test pages in dev-mode
881
	 * with a unified login. Submitted login-credentials are first checked
882
	 * against this static information in {@link Security::authenticate()}.
883
	 *
884
	 * @param string $username The user name
885
	 * @param string $password The password (in cleartext)
886
	 * @return bool
887
	 */
888
	public static function setDefaultAdmin($username, $password) {
889
		// don't overwrite if already set
890
		if(self::$default_username || self::$default_password) {
891
			return false;
892
		}
893
894
		self::$default_username = $username;
895
		self::$default_password = $password;
896
	}
897
898
	/**
899
	 * Checks if the passed credentials are matching the default-admin.
900
	 * Compares cleartext-password set through Security::setDefaultAdmin().
901
	 *
902
	 * @param string $username
903
	 * @param string $password
904
	 * @return bool
905
	 */
906
	public static function check_default_admin($username, $password) {
907
		return (
908
			self::$default_username === $username
909
			&& self::$default_password === $password
910
			&& self::has_default_admin()
911
		);
912
	}
913
914
	/**
915
	 * Check that the default admin account has been set.
916
	 */
917
	public static function has_default_admin() {
918
		return !empty(self::$default_username) && !empty(self::$default_password);
919
	}
920
921
	/**
922
	 * Get default admin username
923
	 *
924
	 * @return string
925
	 */
926
	public static function default_admin_username() {
927
		return self::$default_username;
928
	}
929
930
	/**
931
	 * Get default admin password
932
	 *
933
	 * @return string
934
	 */
935
	public static function default_admin_password() {
936
		return self::$default_password;
937
	}
938
939
	/**
940
	 * Set strict path checking
941
	 *
942
	 * This prevents sharing of the session across several sites in the
943
	 * domain.
944
	 *
945
	 * @deprecated 4.0 Use the "Security.strict_path_checking" config setting instead
946
	 * @param boolean $strictPathChecking To enable or disable strict patch
947
	 *                                    checking.
948
	 */
949
	public static function setStrictPathChecking($strictPathChecking) {
950
		Deprecation::notice('4.0', 'Use the "Security.strict_path_checking" config setting instead');
951
		self::config()->strict_path_checking = $strictPathChecking;
0 ignored issues
show
Documentation introduced by
The property strict_path_checking does not exist on object<Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
952
	}
953
954
955
	/**
956
	 * Get strict path checking
957
	 *
958
	 * @deprecated 4.0 Use the "Security.strict_path_checking" config setting instead
959
	 * @return boolean Status of strict path checking
960
	 */
961
	public static function getStrictPathChecking() {
962
		Deprecation::notice('4.0', 'Use the "Security.strict_path_checking" config setting instead');
963
		return self::config()->strict_path_checking;
964
	}
965
966
967
	/**
968
	 * Set the password encryption algorithm
969
	 *
970
	 * @deprecated 4.0 Use the "Security.password_encryption_algorithm" config setting instead
971
	 * @param string $algorithm One of the available password encryption
972
	 *  algorithms determined by {@link Security::get_encryption_algorithms()}
973
	 * @return bool Returns TRUE if the passed algorithm was valid, otherwise FALSE.
974
	 */
975
	public static function set_password_encryption_algorithm($algorithm) {
976
		Deprecation::notice('4.0', 'Use the "Security.password_encryption_algorithm" config setting instead');
977
978
		self::config()->password_encryption_algorithm = $algorithm;
0 ignored issues
show
Documentation introduced by
The property password_encryption_algorithm does not exist on object<Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
979
	}
980
981
	/**
982
	 * @deprecated 4.0 Use the "Security.password_encryption_algorithm" config setting instead
983
	 * @return String
984
	 */
985
	public static function get_password_encryption_algorithm() {
986
		Deprecation::notice('4.0', 'Use the "Security.password_encryption_algorithm" config setting instead');
987
		return self::config()->password_encryption_algorithm;
988
	}
989
990
	/**
991
	 * Encrypt a password according to the current password encryption settings.
992
	 * If the settings are so that passwords shouldn't be encrypted, the
993
	 * result is simple the clear text password with an empty salt except when
994
	 * a custom algorithm ($algorithm parameter) was passed.
995
	 *
996
	 * @param string $password The password to encrypt
997
	 * @param string $salt Optional: The salt to use. If it is not passed, but
998
	 *  needed, the method will automatically create a
999
	 *  random salt that will then be returned as return value.
1000
	 * @param string $algorithm Optional: Use another algorithm to encrypt the
1001
	 *  password (so that the encryption algorithm can be changed over the time).
1002
	 * @param Member $member Optional
1003
	 * @return mixed Returns an associative array containing the encrypted
1004
	 *  password and the used salt in the form:
1005
	 * <code>
1006
	 * 	array(
1007
	 * 	'password' => string,
1008
	 * 	'salt' => string,
1009
	 * 	'algorithm' => string,
1010
	 * 	'encryptor' => PasswordEncryptor instance
1011
	 * 	)
1012
	 * </code>
1013
	 * If the passed algorithm is invalid, FALSE will be returned.
1014
	 *
1015
	 * @see encrypt_passwords()
1016
	 */
1017
	public static function encrypt_password($password, $salt = null, $algorithm = null, $member = null) {
1018
		// Fall back to the default encryption algorithm
1019
		if(!$algorithm) $algorithm = self::config()->password_encryption_algorithm;
0 ignored issues
show
Documentation introduced by
The property password_encryption_algorithm does not exist on object<Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
Bug Best Practice introduced by
The expression $algorithm of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1020
1021
		$e = PasswordEncryptor::create_for_algorithm($algorithm);
1022
1023
		// New salts will only need to be generated if the password is hashed for the first time
1024
		$salt = ($salt) ? $salt : $e->salt($password);
1025
1026
		return array(
1027
			'password' => $e->encrypt($password, $salt, $member),
1028
			'salt' => $salt,
1029
			'algorithm' => $algorithm,
1030
			'encryptor' => $e
1031
		);
1032
	}
1033
1034
	/**
1035
	 * Checks the database is in a state to perform security checks.
1036
	 * See {@link DatabaseAdmin->init()} for more information.
1037
	 *
1038
	 * @return bool
1039
	 */
1040
	public static function database_is_ready() {
1041
		// Used for unit tests
1042
		if(self::$force_database_is_ready !== null) {
1043
			return self::$force_database_is_ready;
1044
		}
1045
1046
		if(self::$database_is_ready) {
1047
			return self::$database_is_ready;
1048
		}
1049
1050
		$requiredClasses = ClassInfo::dataClassesFor('SilverStripe\\Security\\Member');
1051
		$requiredClasses[] = 'SilverStripe\\Security\\Group';
1052
		$requiredClasses[] = 'SilverStripe\\Security\\Permission';
1053
1054
		foreach($requiredClasses as $class) {
1055
			// Skip test classes, as not all test classes are scaffolded at once
1056
			if(is_subclass_of($class, 'TestOnly')) {
1057
				continue;
1058
			}
1059
1060
			// if any of the tables aren't created in the database
1061
			$table = DataObject::getSchema()->tableName($class);
1062
			if(!ClassInfo::hasTable($table)) {
1063
				return false;
1064
			}
1065
1066
			// HACK: DataExtensions aren't applied until a class is instantiated for
1067
			// the first time, so create an instance here.
1068
			singleton($class);
1069
1070
			// if any of the tables don't have all fields mapped as table columns
1071
			$dbFields = DB::field_list($table);
1072
			if(!$dbFields) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $dbFields of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1073
				return false;
1074
			}
1075
1076
			$objFields = DataObject::database_fields($class);
1077
			$missingFields = array_diff_key($objFields, $dbFields);
1078
1079
			if($missingFields) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $missingFields of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1080
				return false;
1081
			}
1082
		}
1083
		self::$database_is_ready = true;
1084
1085
		return true;
1086
	}
1087
1088
	/**
1089
	 * Enable or disable recording of login attempts
1090
	 * through the {@link LoginRecord} object.
1091
	 *
1092
	 * @deprecated 4.0 Use the "Security.login_recording" config setting instead
1093
	 * @param boolean $bool
1094
	 */
1095
	public static function set_login_recording($bool) {
1096
		Deprecation::notice('4.0', 'Use the "Security.login_recording" config setting instead');
1097
		self::$login_recording = (bool)$bool;
1098
	}
1099
1100
	/**
1101
	 * @deprecated 4.0 Use the "Security.login_recording" config setting instead
1102
	 * @return boolean
1103
	 */
1104
	public static function login_recording() {
1105
		Deprecation::notice('4.0', 'Use the "Security.login_recording" config setting instead');
1106
		return self::$login_recording;
1107
	}
1108
1109
	/**
1110
	 * @config
1111
	 * @var string Set the default login dest
1112
	 * This is the URL that users will be redirected to after they log in,
1113
	 * if they haven't logged in en route to access a secured page.
1114
	 * By default, this is set to the homepage.
1115
	 */
1116
	private static $default_login_dest = "";
1117
1118
	/**
1119
	 * @deprecated 4.0 Use the "Security.default_login_dest" config setting instead
1120
	 */
1121
	public static function set_default_login_dest($dest) {
1122
		Deprecation::notice('4.0', 'Use the "Security.default_login_dest" config setting instead');
1123
		self::config()->default_login_dest = $dest;
0 ignored issues
show
Documentation introduced by
The property default_login_dest does not exist on object<Config_ForClass>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
1124
	}
1125
1126
	/**
1127
	 * Get the default login dest.
1128
	 *
1129
	 * @deprecated 4.0 Use the "Security.default_login_dest" config setting instead
1130
	 */
1131
	public static function default_login_dest() {
1132
		Deprecation::notice('4.0', 'Use the "Security.default_login_dest" config setting instead');
1133
		return self::config()->default_login_dest;
1134
	}
1135
1136
	protected static $ignore_disallowed_actions = false;
1137
1138
	/**
1139
	 * Set to true to ignore access to disallowed actions, rather than returning permission failure
1140
	 * Note that this is just a flag that other code needs to check with Security::ignore_disallowed_actions()
1141
	 * @param $flag True or false
1142
	 */
1143
	public static function set_ignore_disallowed_actions($flag) {
1144
		self::$ignore_disallowed_actions = $flag;
1145
	}
1146
1147
	public static function ignore_disallowed_actions() {
1148
		return self::$ignore_disallowed_actions;
1149
	}
1150
1151
1152
	/**
1153
	 * Set a custom log-in URL if you have built your own log-in page.
1154
	 *
1155
	 * @deprecated 4.0 Use the "Security.login_url" config setting instead.
1156
	 */
1157
	public static function set_login_url($loginUrl) {
1158
		Deprecation::notice('4.0', 'Use the "Security.login_url" config setting instead');
1159
		self::config()->update("login_url", $loginUrl);
1160
	}
1161
1162
1163
	/**
1164
	 * Get the URL of the log-in page.
1165
	 *
1166
	 * To update the login url use the "Security.login_url" config setting.
1167
	 *
1168
	 * @return string
1169
	 */
1170
	public static function login_url() {
1171
		return Controller::join_links(Director::baseURL(), self::config()->login_url);
1172
	}
1173
1174
1175
	/**
1176
	 * Get the URL of the logout page.
1177
	 *
1178
	 * To update the logout url use the "Security.logout_url" config setting.
1179
	 *
1180
	 * @return string
1181
	 */
1182
	public static function logout_url() {
1183
		return Controller::join_links(Director::baseURL(), self::config()->logout_url);
1184
	}
1185
1186
	/**
1187
	 * Get the URL of the logout page.
1188
	 *
1189
	 * To update the logout url use the "Security.logout_url" config setting.
1190
	 *
1191
	 * @return string
1192
	 */
1193
	public static function lost_password_url() {
1194
		return Controller::join_links(Director::baseURL(), self::config()->lost_password_url);
1195
	}
1196
1197
	/**
1198
	 * Defines global accessible templates variables.
1199
	 *
1200
	 * @return array
1201
	 */
1202
	public static function get_template_global_variables() {
1203
		return array(
1204
			"LoginURL" => "login_url",
1205
			"LogoutURL" => "logout_url",
1206
			"LostPasswordURL" => "lost_password_url",
1207
		);
1208
	}
1209
1210
}
1211