Completed
Push — hash-nonce ( 07e2e8 )
by Sam
08:52
created

Security   F

Complexity

Total Complexity 121

Size/Duplication

Total Lines 1133
Duplicated Lines 0 %

Coupling/Cohesion

Components 4
Dependencies 28

Importance

Changes 1
Bugs 1 Features 0
Metric Value
c 1
b 1
f 0
dl 0
loc 1133
rs 0.5217
wmc 121
lcom 4
cbo 28

49 Methods

Rating   Name   Duplication   Size   Complexity  
D permissionFailure() 0 93 14
A get_word_list() 0 4 1
A set_word_list() 0 4 1
A set_default_message_set() 0 4 1
A init() 0 6 1
A index() 0 3 1
A getAuthenticator() 0 12 3
A LoginForm() 0 5 2
A GetLoginForms() 0 10 2
A Link() 0 3 1
A ping() 0 3 1
A logout() 0 8 4
D preLogin() 0 26 9
A getResponseController() 0 15 2
A getTemplatesFor() 0 3 1
A generateLoginFormSet() 0 8 1
A getLoginMessage() 0 12 3
B login() 0 44 6
A basicauthlogin() 0 4 1
A lostpassword() 0 20 3
A LostPasswordForm() 0 16 1
B passwordsent() 0 24 3
A getPasswordResetLink() 0 6 1
C changepassword() 0 66 12
A ChangePasswordForm() 0 3 1
A getIncludeTemplate() 0 3 1
C findAnAdministrator() 0 49 8
A clear_default_admin() 0 4 1
A setDefaultAdmin() 0 9 3
A check_default_admin() 0 7 3
A has_default_admin() 0 3 2
A default_admin_username() 0 3 1
A default_admin_password() 0 3 1
A setStrictPathChecking() 0 4 1
A getStrictPathChecking() 0 4 1
A set_password_encryption_algorithm() 0 5 1
A get_password_encryption_algorithm() 0 4 1
A encrypt_password() 0 16 3
C database_is_ready() 0 34 8
A set_login_recording() 0 4 1
A login_recording() 0 4 1
A set_default_login_dest() 0 4 1
A default_login_dest() 0 4 1
A set_ignore_disallowed_actions() 0 3 1
A ignore_disallowed_actions() 0 3 1
A set_login_url() 0 4 1
A login_url() 0 3 1
A logout_url() 0 3 1
A get_template_global_variables() 0 6 1

How to fix   Complexity   

Complex Class

Complex classes like Security often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Security, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Implements a basic security model
4
 * @package framework
5
 * @subpackage security
6
 */
7
class Security extends Controller implements TemplateGlobalProvider {
8
9
	private static $allowed_actions = array(
0 ignored issues
show
Comprehensibility introduced by
Consider using a different property name as you override a private property of the parent class.
Loading history...
10
		'index',
11
		'login',
12
		'logout',
13
		'basicauthlogin',
14
		'lostpassword',
15
		'passwordsent',
16
		'changepassword',
17
		'ping',
18
		'LoginForm',
19
		'ChangePasswordForm',
20
		'LostPasswordForm',
21
	);
22
23
	/**
24
	 * Default user name. Only used in dev-mode by {@link setDefaultAdmin()}
25
	 *
26
	 * @var string
27
	 * @see setDefaultAdmin()
28
	 */
29
	protected static $default_username;
30
31
	/**
32
	 * Default password. Only used in dev-mode by {@link setDefaultAdmin()}
33
	 *
34
	 * @var string
35
	 * @see setDefaultAdmin()
36
	 */
37
	protected static $default_password;
38
39
	/**
40
	 * If set to TRUE to prevent sharing of the session across several sites
41
	 * in the domain.
42
	 *
43
	 * @config
44
	 * @var bool
45
	 */
46
	protected static $strict_path_checking = false;
47
48
	/**
49
	 * The password encryption algorithm to use by default.
50
	 * This is an arbitrary code registered through {@link PasswordEncryptor}.
51
	 *
52
	 * @config
53
	 * @var string
54
	 */
55
	private static $password_encryption_algorithm = 'blowfish';
56
57
	/**
58
	 * Showing "Remember me"-checkbox
59
	 * on loginform, and saving encrypted credentials to a cookie.
60
 	 *
61
 	 * @config
62
	 * @var bool
63
	 */
64
	private static $autologin_enabled = true;
65
66
	/**
67
	 * Determine if login username may be remembered between login sessions
68
	 * If set to false this will disable autocomplete and prevent username persisting in the session
69
	 *
70
	 * @config
71
	 * @var bool
72
	 */
73
	private static $remember_username = true;
74
75
	/**
76
	 * Location of word list to use for generating passwords
77
	 *
78
	 * @config
79
	 * @var string
80
	 */
81
	private static $word_list = './wordlist.txt';
82
83
	/**
84
	 * @config
85
	 * @var string
86
	 */
87
	private static $template = 'BlankPage';
88
89
	/**
90
	 * Template thats used to render the pages.
91
	 *
92
	 * @var string
93
	 * @config
94
	 */
95
	private static $template_main = 'Page';
96
97
	/**
98
	 * Default message set used in permission failures.
99
	 *
100
	 * @config
101
	 * @var array|string
102
	 */
103
	private static $default_message_set;
104
105
	/**
106
	 * Random secure token, can be used as a crypto key internally.
107
	 * Generate one through 'sake dev/generatesecuretoken'.
108
	 *
109
	 * @config
110
	 * @var String
111
	 */
112
	private static $token;
113
114
	/**
115
	 * The default login URL
116
	 *
117
	 * @config
118
	 *
119
	 * @var string
120
	 */
121
	private static $login_url = "Security/login";
122
123
	/**
124
	 * The default logout URL
125
	 *
126
	 * @config
127
	 *
128
	 * @var string
129
	 */
130
	private static $logout_url = "Security/logout";
131
132
	/**
133
	 * Get location of word list file
134
	 *
135
	 * @deprecated 4.0 Use the "Security.word_list" config setting instead
136
	 */
137
	public static function get_word_list() {
138
		Deprecation::notice('4.0', 'Use the "Security.word_list" config setting instead');
139
		return self::config()->word_list;
140
	}
141
142
	/**
143
	 * Enable or disable recording of login attempts
144
	 * through the {@link LoginRecord} object.
145
	 *
146
	 * @config
147
	 * @var boolean $login_recording
148
	 */
149
	private static $login_recording = false;
150
151
	/**
152
	 * @var boolean If set to TRUE or FALSE, {@link database_is_ready()}
153
	 * will always return FALSE. Used for unit testing.
154
	 */
155
	static $force_database_is_ready = null;
156
157
	/**
158
	 * When the database has once been verified as ready, it will not do the
159
	 * checks again.
160
	 *
161
	 * @var bool
162
	 */
163
	static $database_is_ready = false;
164
165
	/**
166
	 * Set location of word list file
167
	 *
168
	 * @deprecated 4.0 Use the "Security.word_list" config setting instead
169
	 * @param string $wordListFile Location of word list file
170
	 */
171
	public static function set_word_list($wordListFile) {
172
		Deprecation::notice('4.0', 'Use the "Security.word_list" config setting instead');
173
		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...
174
	}
175
176
	/**
177
	 * Set the default message set used in permissions failures.
178
	 *
179
	 * @deprecated 4.0 Use the "Security.default_message_set" config setting instead
180
	 * @param string|array $messageSet
181
	 */
182
	public static function set_default_message_set($messageSet) {
183
		Deprecation::notice('4.0', 'Use the "Security.default_message_set" config setting instead');
184
		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...
185
	}
186
187
188
	/**
189
	 * Register that we've had a permission failure trying to view the given page
190
	 *
191
	 * This will redirect to a login page.
192
	 * If you don't provide a messageSet, a default will be used.
193
	 *
194
	 * @param Controller $controller The controller that you were on to cause the permission
195
	 *                               failure.
196
	 * @param string|array $messageSet The message to show to the user. This
197
	 *                                 can be a string, or a map of different
198
	 *                                 messages for different contexts.
199
	 *                                 If you pass an array, you can use the
200
	 *                                 following keys:
201
	 *                                   - default: The default message
202
	 *                                   - alreadyLoggedIn: The message to
203
	 *                                                      show if the user
204
	 *                                                      is already logged
205
	 *                                                      in and lacks the
206
	 *                                                      permission to
207
	 *                                                      access the item.
208
	 *
209
	 * The alreadyLoggedIn value can contain a '%s' placeholder that will be replaced with a link
210
	 * to log in.
211
	 * @return SS_HTTPResponse
212
	 */
213
	public static function permissionFailure($controller = null, $messageSet = null) {
214
		self::set_ignore_disallowed_actions(true);
215
216
		if(!$controller) $controller = Controller::curr();
217
218
		if(Director::is_ajax()) {
219
			$response = ($controller) ? $controller->getResponse() : new SS_HTTPResponse();
220
			$response->setStatusCode(403);
221
			if(!Member::currentUser()) {
222
				$response->setBody(_t('ContentController.NOTLOGGEDIN','Not logged in'));
223
				$response->setStatusDescription(_t('ContentController.NOTLOGGEDIN','Not logged in'));
224
				// Tell the CMS to allow re-aunthentication
225
				if(CMSSecurity::enabled()) {
226
					$response->addHeader('X-Reauthenticate', '1');
227
				}
228
			}
229
			return $response;
230
		}
231
232
		// Prepare the messageSet provided
233
		if(!$messageSet) {
234
			if($configMessageSet = static::config()->get('default_message_set')) {
235
				$messageSet = $configMessageSet;
236
			} else {
237
				$messageSet = array(
238
					'default' => _t(
239
						'Security.NOTEPAGESECURED',
240
						"That page is secured. Enter your credentials below and we will send "
241
							. "you right along."
242
					),
243
					'alreadyLoggedIn' => _t(
244
						'Security.ALREADYLOGGEDIN',
245
						"You don't have access to this page.  If you have another account that "
246
							. "can access that page, you can log in again below.",
247
248
						"%s will be replaced with a link to log in."
249
					)
250
				);
251
			}
252
		}
253
254
		if(!is_array($messageSet)) {
255
			$messageSet = array('default' => $messageSet);
256
		}
257
258
		$member = Member::currentUser();
259
260
		// Work out the right message to show
261
		if($member && $member->exists()) {
262
			$response = ($controller) ? $controller->getResponse() : new SS_HTTPResponse();
263
			$response->setStatusCode(403);
264
265
			//If 'alreadyLoggedIn' is not specified in the array, then use the default
266
			//which should have been specified in the lines above
267
			if(isset($messageSet['alreadyLoggedIn'])) {
268
				$message = $messageSet['alreadyLoggedIn'];
269
			} else {
270
				$message = $messageSet['default'];
271
			}
272
273
			// Somewhat hackish way to render a login form with an error message.
274
			$me = new Security();
275
			$form = $me->LoginForm();
276
			$form->sessionMessage($message, 'warning');
277
			Session::set('MemberLoginForm.force_message',1);
278
			$loginResponse = $me->login();
279
			if($loginResponse instanceof SS_HTTPResponse) {
280
				return $loginResponse;
281
			}
282
283
			$response->setBody((string)$loginResponse);
284
285
			$controller->extend('permissionDenied', $member);
286
287
			return $response;
288
		} else {
289
			$message = $messageSet['default'];
290
		}
291
292
		Session::set("Security.Message.message", $message);
293
		Session::set("Security.Message.type", 'warning');
294
295
		Session::set("BackURL", $_SERVER['REQUEST_URI']);
296
297
		// TODO AccessLogEntry needs an extension to handle permission denied errors
298
		// Audit logging hook
299
		$controller->extend('permissionDenied', $member);
300
301
		return $controller->redirect(
302
			Config::inst()->get('Security', 'login_url')
303
			. "?BackURL=" . urlencode($_SERVER['REQUEST_URI'])
304
		);
305
	}
306
307
	public function init() {
308
		parent::init();
309
310
		// Prevent clickjacking, see https://developer.mozilla.org/en-US/docs/HTTP/X-Frame-Options
311
		$this->getResponse()->addHeader('X-Frame-Options', 'SAMEORIGIN');
312
	}
313
314
	public function index() {
315
		return $this->httpError(404); // no-op
316
	}
317
318
	/**
319
	 * Get the selected authenticator for this request
320
	 *
321
	 * @return string Class name of Authenticator
322
	 */
323
	protected function getAuthenticator() {
324
		$authenticator = $this->getRequest()->requestVar('AuthenticationMethod');
325
		if($authenticator) {
326
			$authenticators = Authenticator::get_authenticators();
327
			if(in_array($authenticator, $authenticators)) {
328
				return $authenticator;
329
			}
330
		} else {
331
			return Authenticator::get_default_authenticator();
332
		}
333
334
	}
335
336
	/**
337
	 * Get the login form to process according to the submitted data
338
	 *
339
	 * @return Form
340
	 */
341
	public function LoginForm() {
342
		$authenticator = $this->getAuthenticator();
343
		if($authenticator) return $authenticator::get_login_form($this);
344
		throw new Exception('Passed invalid authentication method');
345
	}
346
347
	/**
348
	 * Get the login forms for all available authentication methods
349
	 *
350
	 * @return array Returns an array of available login forms (array of Form
351
	 *               objects).
352
	 *
353
	 * @todo Check how to activate/deactivate authentication methods
354
	 */
355
	public function GetLoginForms() {
356
		$forms = array();
357
358
		$authenticators = Authenticator::get_authenticators();
359
		foreach($authenticators as $authenticator) {
360
			$forms[] = $authenticator::get_login_form($this);
361
		}
362
363
		return $forms;
364
	}
365
366
367
	/**
368
	 * Get a link to a security action
369
	 *
370
	 * @param string $action Name of the action
371
	 * @return string Returns the link to the given action
372
	 */
373
	public function Link($action = null) {
374
		return Controller::join_links(Director::baseURL(), "Security", $action);
375
	}
376
377
	/**
378
	 * This action is available as a keep alive, so user
379
	 * sessions don't timeout. A common use is in the admin.
380
	 */
381
	public function ping() {
382
		return 1;
383
	}
384
385
	/**
386
	 * Log the currently logged in user out
387
	 *
388
	 * @param bool $redirect Redirect the user back to where they came.
389
	 *                       - If it's false, the code calling logout() is
390
	 *                         responsible for sending the user where-ever
391
	 *                         they should go.
392
	 */
393
	public function logout($redirect = true) {
394
		$member = Member::currentUser();
395
		if($member) $member->logOut();
396
397
		if($redirect && (!$this->getResponse()->isFinished())) {
398
			$this->redirectBack();
399
		}
400
	}
401
402
	/**
403
	 * Perform pre-login checking and prepare a response if available prior to login
404
	 *
405
	 * @return SS_HTTPResponse Substitute response object if the login process should be curcumvented.
406
	 * Returns null if should proceed as normal.
407
	 */
408
	protected function preLogin() {
409
		// Event handler for pre-login, with an option to let it break you out of the login form
410
		$eventResults = $this->extend('onBeforeSecurityLogin');
411
		// If there was a redirection, return
412
		if($this->redirectedTo()) return $this->getResponse();
413
		// If there was an SS_HTTPResponse object returned, then return that
414
		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...
415
			foreach($eventResults as $result) {
416
				if($result instanceof SS_HTTPResponse) return $result;
417
			}
418
		}
419
420
		// If arriving on the login page already logged in, with no security error, and a ReturnURL then redirect
421
		// back. The login message check is neccesary to prevent infinite loops where BackURL links to
422
		// an action that triggers Security::permissionFailure.
423
		// This step is necessary in cases such as automatic redirection where a user is authenticated
424
		// upon landing on an SSL secured site and is automatically logged in, or some other case
425
		// where the user has permissions to continue but is not given the option.
426
		if($this->getRequest()->requestVar('BackURL')
427
			&& !$this->getLoginMessage()
428
			&& ($member = Member::currentUser())
429
			&& $member->exists()
430
		) {
431
			return $this->redirectBack();
0 ignored issues
show
Comprehensibility Best Practice introduced by
The expression $this->redirectBack(); of type SS_HTTPResponse|null|false adds false to the return on line 431 which is incompatible with the return type documented by Security::preLogin of type SS_HTTPResponse|null. It seems like you forgot to handle an error condition.
Loading history...
432
		}
433
	}
434
435
	/**
436
	 * Prepare the controller for handling the response to this request
437
	 *
438
	 * @param string $title Title to use
439
	 * @return Controller
440
	 */
441
	protected function getResponseController($title) {
442
		if(!class_exists('SiteTree')) return $this;
443
444
		// Use sitetree pages to render the security page
445
		$tmpPage = new Page();
446
		$tmpPage->Title = $title;
447
		$tmpPage->URLSegment = "Security";
448
		// Disable ID-based caching  of the log-in page by making it a random number
449
		$tmpPage->ID = -1 * rand(1,10000000);
450
451
		$controller = Page_Controller::create($tmpPage);
452
		$controller->setDataModel($this->model);
453
		$controller->init();
454
		return $controller;
455
	}
456
457
	/**
458
	 * Determine the list of templates to use for rendering the given action
459
	 *
460
	 * @param string $action
461
	 * @return array Template list
462
	 */
463
	public function getTemplatesFor($action) {
464
		return array("Security_{$action}", 'Security', $this->stat('template_main'), 'BlankPage');
465
	}
466
467
	/**
468
	 * Combine the given forms into a formset with a tabbed interface
469
	 *
470
	 * @param array $forms List of LoginForm instances
471
	 * @return string
472
	 */
473
	protected function generateLoginFormSet($forms) {
474
		$viewData = new ArrayData(array(
475
			'Forms' => new ArrayList($forms),
476
		));
477
		return $viewData->renderWith(
478
			$this->getIncludeTemplate('MultiAuthenticatorLogin')
479
		);
480
	}
481
482
	/**
483
	 * Get the HTML Content for the $Content area during login
484
	 *
485
	 * @param string &$messageType Type of message, if available, passed back to caller
486
	 * @return string Message in HTML format
487
	 */
488
	protected function getLoginMessage(&$messageType = null) {
489
		$message = Session::get('Security.Message.message');
490
		$messageType = null;
491
		if(empty($message)) return null;
492
493
		$messageType = Session::get('Security.Message.type');
494
		if($messageType === 'bad') {
495
			return "<p class=\"message $messageType\">$message</p>";
496
		} else {
497
			return "<p>$message</p>";
498
		}
499
	}
500
501
502
	/**
503
	 * Show the "login" page
504
	 *
505
	 * For multiple authenticators, Security_MultiAuthenticatorLogin is used.
506
	 * See getTemplatesFor and getIncludeTemplate for how to override template logic
507
	 *
508
	 * @return string|SS_HTTPResponse Returns the "login" page as HTML code.
509
	 */
510
	public function login() {
511
		// Check pre-login process
512
		if($response = $this->preLogin()) return $response;
513
514
		// Get response handler
515
		$controller = $this->getResponseController(_t('Security.LOGIN', 'Log in'));
516
517
		// if the controller calls Director::redirect(), this will break early
518
		if(($response = $controller->getResponse()) && $response->isFinished()) return $response;
519
520
		$forms = $this->GetLoginForms();
521
		if(!count($forms)) {
522
			user_error('No login-forms found, please use Authenticator::register_authenticator() to add one',
523
				E_USER_ERROR);
524
		}
525
526
		// Handle any form messages from validation, etc.
527
		$messageType = '';
528
		$message = $this->getLoginMessage($messageType);
529
530
		// We've displayed the message in the form output, so reset it for the next run.
531
		Session::clear('Security.Message');
532
533
		// only display tabs when more than one authenticator is provided
534
		// to save bandwidth and reduce the amount of custom styling needed
535
		if(count($forms) > 1) {
536
			$content = $this->generateLoginFormSet($forms);
537
		} else {
538
			$content = $forms[0]->forTemplate();
539
		}
540
541
		// Finally, customise the controller to add any form messages and the form.
542
		$customisedController = $controller->customise(array(
543
			"Content" => $message,
544
			"Message" => $message,
545
			"MessageType" => $messageType,
546
			"Form" => $content,
547
		));
548
549
		// Return the customised controller
550
		return $customisedController->renderWith(
551
			$this->getTemplatesFor('login')
552
		);
553
	}
554
555
	public function basicauthlogin() {
556
		$member = BasicAuth::requireLogin("SilverStripe login", 'ADMIN');
557
		$member->LogIn();
558
	}
559
560
	/**
561
	 * Show the "lost password" page
562
	 *
563
	 * @return string Returns the "lost password" page as HTML code.
564
	 */
565
	public function lostpassword() {
566
		$controller = $this->getResponseController(_t('Security.LOSTPASSWORDHEADER', 'Lost Password'));
567
568
		// if the controller calls Director::redirect(), this will break early
569
		if(($response = $controller->getResponse()) && $response->isFinished()) return $response;
570
571
		$customisedController = $controller->customise(array(
572
			'Content' =>
573
				'<p>' .
574
				_t(
575
					'Security.NOTERESETPASSWORD',
576
					'Enter your e-mail address and we will send you a link with which you can reset your password'
577
				) .
578
				'</p>',
579
			'Form' => $this->LostPasswordForm(),
580
		));
581
582
		//Controller::$currentController = $controller;
583
		return $customisedController->renderWith($this->getTemplatesFor('lostpassword'));
584
	}
585
586
587
	/**
588
	 * Factory method for the lost password form
589
	 *
590
	 * @return Form Returns the lost password form
591
	 */
592
	public function LostPasswordForm() {
593
		return MemberLoginForm::create(
594
			$this,
595
			'LostPasswordForm',
596
			new FieldList(
597
				new EmailField('Email', _t('Member.EMAIL', 'Email'))
598
			),
599
			new FieldList(
600
				new FormAction(
601
					'forgotPassword',
602
					_t('Security.BUTTONSEND', 'Send me the password reset link')
603
				)
604
			),
605
			false
606
		);
607
	}
608
609
610
	/**
611
	 * Show the "password sent" page, after a user has requested
612
	 * to reset their password.
613
	 *
614
	 * @param SS_HTTPRequest $request The SS_HTTPRequest for this action.
615
	 * @return string Returns the "password sent" page as HTML code.
616
	 */
617
	public function passwordsent($request) {
618
		$controller = $this->getResponseController(_t('Security.LOSTPASSWORDHEADER', 'Lost Password'));
619
620
		// if the controller calls Director::redirect(), this will break early
621
		if(($response = $controller->getResponse()) && $response->isFinished()) return $response;
622
623
		$email = Convert::raw2xml(rawurldecode($request->param('ID')) . '.' . $request->getExtension());
624
625
		$customisedController = $controller->customise(array(
626
			'Title' => _t('Security.PASSWORDSENTHEADER', "Password reset link sent to '{email}'",
627
				array('email' => $email)),
0 ignored issues
show
Documentation introduced by
array('email' => $email) is of type array<string,array|strin...email":"array|string"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
628
			'Content' =>
629
				"<p>"
630
				. _t('Security.PASSWORDSENTTEXT',
631
					"Thank you! A reset link has been sent to '{email}', provided an account exists for this email"
632
					. " address.",
633
					array('email' => $email))
0 ignored issues
show
Documentation introduced by
array('email' => $email) is of type array<string,array|strin...email":"array|string"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
634
				. "</p>",
635
			'Email' => $email
636
		));
637
638
		//Controller::$currentController = $controller;
639
		return $customisedController->renderWith($this->getTemplatesFor('passwordsent'));
640
	}
641
642
643
	/**
644
	 * Create a link to the password reset form.
645
	 *
646
	 * GET parameters used:
647
	 * - m: member ID
648
	 * - t: plaintext token
649
	 *
650
	 * @param Member $member Member object associated with this link.
651
	 * @param string $autoLoginHash The auto login token.
0 ignored issues
show
Bug introduced by
There is no parameter named $autoLoginHash. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
652
	 */
653
	public static function getPasswordResetLink($member, $autologinToken) {
654
		$autologinToken = urldecode($autologinToken);
655
		$selfControllerClass = __CLASS__;
656
		$selfController = new $selfControllerClass();
657
		return $selfController->Link('changepassword') . "?m={$member->ID}&t=$autologinToken";
658
	}
659
660
	/**
661
	 * Show the "change password" page.
662
	 * This page can either be called directly by logged-in users
663
	 * (in which case they need to provide their old password),
664
	 * or through a link emailed through {@link lostpassword()}.
665
	 * In this case no old password is required, authentication is ensured
666
	 * through the Member.AutoLoginHash property.
667
	 *
668
	 * @see ChangePasswordForm
669
	 *
670
	 * @return string Returns the "change password" page as HTML code.
671
	 */
672
	public function changepassword() {
673
		$controller = $this->getResponseController(_t('Security.CHANGEPASSWORDHEADER', 'Change your password'));
674
675
		// if the controller calls Director::redirect(), this will break early
676
		if(($response = $controller->getResponse()) && $response->isFinished()) return $response;
677
678
		// Extract the member from the URL.
679
		$member = null;
680
		if (isset($_REQUEST['m'])) {
681
			$member = Member::get()->filter('ID', (int)$_REQUEST['m'])->First();
682
		}
683
684
		// Check whether we are merely changin password, or resetting.
685
		if(isset($_REQUEST['t']) && $member && $member->validateAutoLoginToken($_REQUEST['t'])) {
686
			// On first valid password reset request redirect to the same URL without hash to avoid referrer leakage.
687
688
			// if there is a current member, they should be logged out
689
			if ($curMember = Member::currentUser()) {
690
				$curMember->logOut();
691
			}
692
693
			// Store the hash for the change password form. Will be unset after reload within the ChangePasswordForm.
694
			Session::set('AutoLoginHash', $member->encryptWithUserSettings($_REQUEST['t']));
695
696
			return $this->redirect($this->Link('changepassword'));
697
		} elseif(Session::get('AutoLoginHash')) {
698
			// Subsequent request after the "first load with hash" (see previous if clause).
699
			$customisedController = $controller->customise(array(
700
				'Content' =>
701
					'<p>' .
702
					_t('Security.ENTERNEWPASSWORD', 'Please enter a new password.') .
703
					'</p>',
704
				'Form' => $this->ChangePasswordForm(),
705
			));
706
		} elseif(Member::currentUser()) {
707
			// Logged in user requested a password change form.
708
			$customisedController = $controller->customise(array(
709
				'Content' => '<p>'
710
					. _t('Security.CHANGEPASSWORDBELOW', 'You can change your password below.') . '</p>',
711
				'Form' => $this->ChangePasswordForm()));
712
713
		} else {
714
			// Show friendly message if it seems like the user arrived here via password reset feature.
715
			if(isset($_REQUEST['m']) || isset($_REQUEST['t'])) {
716
				$customisedController = $controller->customise(
717
					array('Content' =>
718
						_t(
719
							'Security.NOTERESETLINKINVALID',
720
							'<p>The password reset link is invalid or expired.</p>'
721
							. '<p>You can request a new one <a href="{link1}">here</a> or change your password after'
722
							. ' you <a href="{link2}">logged in</a>.</p>',
723
							array('link1' => $this->Link('lostpassword'), 'link2' => $this->link('login'))
0 ignored issues
show
Documentation introduced by
array('link1' => $this->...> $this->link('login')) is of type array<string,string,{"li...ing","link2":"string"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
724
						)
725
					)
726
				);
727
			} else {
728
				self::permissionFailure(
729
					$this,
730
					_t('Security.ERRORPASSWORDPERMISSION', 'You must be logged in in order to change your password!')
731
				);
732
				return;
733
			}
734
		}
735
736
		return $customisedController->renderWith($this->getTemplatesFor('changepassword'));
737
	}
738
739
	/**
740
	 * Factory method for the lost password form
741
	 *
742
	 * @return Form Returns the lost password form
743
	 */
744
	public function ChangePasswordForm() {
745
		return Object::create('ChangePasswordForm', $this, 'ChangePasswordForm');
746
	}
747
748
	/**
749
	 * Gets the template for an include used for security.
750
	 * For use in any subclass.
751
	 *
752
	 * @return string|array Returns the template(s) for rendering
753
	 */
754
	public function getIncludeTemplate($name) {
755
		return array('Security_' . $name);
756
	}
757
758
	/**
759
	 * Return an existing member with administrator privileges, or create one of necessary.
760
	 *
761
	 * Will create a default 'Administrators' group if no group is found
762
	 * with an ADMIN permission. Will create a new 'Admin' member with administrative permissions
763
	 * if no existing Member with these permissions is found.
764
	 *
765
	 * Important: Any newly created administrator accounts will NOT have valid
766
	 * login credentials (Email/Password properties), which means they can't be used for login
767
	 * purposes outside of any default credentials set through {@link Security::setDefaultAdmin()}.
768
	 *
769
	 * @return Member
770
	 */
771
	public static function findAnAdministrator() {
772
		// coupling to subsites module
773
		$origSubsite = null;
774
		if(is_callable('Subsite::changeSubsite')) {
775
			$origSubsite = Subsite::currentSubsiteID();
776
			Subsite::changeSubsite(0);
777
		}
778
779
		$member = null;
780
781
		// find a group with ADMIN permission
782
		$adminGroup = Permission::get_groups_by_permission('ADMIN')->First();
783
784
		if(is_callable('Subsite::changeSubsite')) {
785
			Subsite::changeSubsite($origSubsite);
786
		}
787
788
		if ($adminGroup) {
789
			$member = $adminGroup->Members()->First();
790
		}
791
792
		if(!$adminGroup) {
793
			singleton('Group')->requireDefaultRecords();
794
			$adminGroup = Permission::get_groups_by_permission('ADMIN')->First();
795
		}
796
797
		if(!$member) {
798
			singleton('Member')->requireDefaultRecords();
799
			$member = Permission::get_members_by_permission('ADMIN')->First();
800
		}
801
802
		if(!$member) {
803
			$member = Member::default_admin();
804
		}
805
806
		if(!$member) {
807
			// Failover to a blank admin
808
			$member = Member::create();
809
			$member->FirstName = _t('Member.DefaultAdminFirstname', 'Default Admin');
810
			$member->write();
811
			// Add member to group instead of adding group to member
812
			// This bypasses the privilege escallation code in Member_GroupSet
813
			$adminGroup
814
				->DirectMembers()
815
				->add($member);
816
		}
817
818
		return $member;
819
	}
820
821
	/**
822
	 * Flush the default admin credentials
823
	 */
824
	public static function clear_default_admin() {
825
		self::$default_username = null;
826
		self::$default_password = null;
827
	}
828
829
830
	/**
831
	 * Set a default admin in dev-mode
832
	 *
833
	 * This will set a static default-admin which is not existing
834
	 * as a database-record. By this workaround we can test pages in dev-mode
835
	 * with a unified login. Submitted login-credentials are first checked
836
	 * against this static information in {@link Security::authenticate()}.
837
	 *
838
	 * @param string $username The user name
839
	 * @param string $password The password (in cleartext)
840
	 */
841
	public static function setDefaultAdmin($username, $password) {
842
		// don't overwrite if already set
843
		if(self::$default_username || self::$default_password) {
844
			return false;
845
		}
846
847
		self::$default_username = $username;
848
		self::$default_password = $password;
849
	}
850
851
	/**
852
	 * Checks if the passed credentials are matching the default-admin.
853
	 * Compares cleartext-password set through Security::setDefaultAdmin().
854
	 *
855
	 * @param string $username
856
	 * @param string $password
857
	 * @return bool
858
	 */
859
	public static function check_default_admin($username, $password) {
860
		return (
861
			self::$default_username === $username
862
			&& self::$default_password === $password
863
			&& self::has_default_admin()
864
		);
865
	}
866
867
	/**
868
	 * Check that the default admin account has been set.
869
	 */
870
	public static function has_default_admin() {
871
		return !empty(self::$default_username) && !empty(self::$default_password);
872
	}
873
874
	/**
875
	 * Get default admin username
876
	 *
877
	 * @return string
878
	 */
879
	public static function default_admin_username() {
880
		return self::$default_username;
881
	}
882
883
	/**
884
	 * Get default admin password
885
	 *
886
	 * @return string
887
	 */
888
	public static function default_admin_password() {
889
		return self::$default_password;
890
	}
891
892
	/**
893
	 * Set strict path checking
894
	 *
895
	 * This prevents sharing of the session across several sites in the
896
	 * domain.
897
	 *
898
	 * @deprecated 4.0 Use the "Security.strict_path_checking" config setting instead
899
	 * @param boolean $strictPathChecking To enable or disable strict patch
900
	 *                                    checking.
901
	 */
902
	public static function setStrictPathChecking($strictPathChecking) {
903
		Deprecation::notice('4.0', 'Use the "Security.strict_path_checking" config setting instead');
904
		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...
905
	}
906
907
908
	/**
909
	 * Get strict path checking
910
	 *
911
	 * @deprecated 4.0 Use the "Security.strict_path_checking" config setting instead
912
	 * @return boolean Status of strict path checking
913
	 */
914
	public static function getStrictPathChecking() {
915
		Deprecation::notice('4.0', 'Use the "Security.strict_path_checking" config setting instead');
916
		return self::config()->strict_path_checking;
917
	}
918
919
920
	/**
921
	 * Set the password encryption algorithm
922
	 *
923
	 * @deprecated 4.0 Use the "Security.password_encryption_algorithm" config setting instead
924
	 * @param string $algorithm One of the available password encryption
925
	 *  algorithms determined by {@link Security::get_encryption_algorithms()}
926
	 * @return bool Returns TRUE if the passed algorithm was valid, otherwise FALSE.
927
	 */
928
	public static function set_password_encryption_algorithm($algorithm) {
929
		Deprecation::notice('4.0', 'Use the "Security.password_encryption_algorithm" config setting instead');
930
931
		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...
932
	}
933
934
	/**
935
	 * @deprecated 4.0 Use the "Security.password_encryption_algorithm" config setting instead
936
	 * @return String
937
	 */
938
	public static function get_password_encryption_algorithm() {
939
		Deprecation::notice('4.0', 'Use the "Security.password_encryption_algorithm" config setting instead');
940
		return self::config()->password_encryption_algorithm;
941
	}
942
943
	/**
944
	 * Encrypt a password according to the current password encryption settings.
945
	 * If the settings are so that passwords shouldn't be encrypted, the
946
	 * result is simple the clear text password with an empty salt except when
947
	 * a custom algorithm ($algorithm parameter) was passed.
948
	 *
949
	 * @param string $password The password to encrypt
950
	 * @param string $salt Optional: The salt to use. If it is not passed, but
951
	 *  needed, the method will automatically create a
952
	 *  random salt that will then be returned as return value.
953
	 * @param string $algorithm Optional: Use another algorithm to encrypt the
954
	 *  password (so that the encryption algorithm can be changed over the time).
955
	 * @param Member $member Optional
956
	 * @return mixed Returns an associative array containing the encrypted
957
	 *  password and the used salt in the form:
958
	 * <code>
959
	 * 	array(
960
	 * 	'password' => string,
961
	 * 	'salt' => string,
962
	 * 	'algorithm' => string,
963
	 * 	'encryptor' => PasswordEncryptor instance
964
	 * 	)
965
	 * </code>
966
	 * If the passed algorithm is invalid, FALSE will be returned.
967
	 *
968
	 * @see encrypt_passwords()
969
	 */
970
	public static function encrypt_password($password, $salt = null, $algorithm = null, $member = null) {
971
		// Fall back to the default encryption algorithm
972
		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...
973
974
		$e = PasswordEncryptor::create_for_algorithm($algorithm);
975
976
		// New salts will only need to be generated if the password is hashed for the first time
977
		$salt = ($salt) ? $salt : $e->salt($password);
978
979
		return array(
980
			'password' => $e->encrypt($password, $salt, $member),
981
			'salt' => $salt,
982
			'algorithm' => $algorithm,
983
			'encryptor' => $e
984
		);
985
	}
986
987
	/**
988
	 * Checks the database is in a state to perform security checks.
989
	 * See {@link DatabaseAdmin->init()} for more information.
990
	 *
991
	 * @return bool
992
	 */
993
	public static function database_is_ready() {
994
		// Used for unit tests
995
		if(self::$force_database_is_ready !== NULL) return self::$force_database_is_ready;
996
997
		if(self::$database_is_ready) return self::$database_is_ready;
998
999
		$requiredTables = ClassInfo::dataClassesFor('Member');
1000
		$requiredTables[] = 'Group';
1001
		$requiredTables[] = 'Permission';
1002
1003
		foreach($requiredTables as $table) {
1004
			// Skip test classes, as not all test classes are scaffolded at once
1005
			if(is_subclass_of($table, 'TestOnly')) continue;
1006
1007
			// if any of the tables aren't created in the database
1008
			if(!ClassInfo::hasTable($table)) return false;
1009
1010
			// HACK: DataExtensions aren't applied until a class is instantiated for
1011
			// the first time, so create an instance here.
1012
			singleton($table);
1013
1014
			// if any of the tables don't have all fields mapped as table columns
1015
			$dbFields = DB::field_list($table);
1016
			if(!$dbFields) return false;
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...
1017
1018
			$objFields = DataObject::database_fields($table, false);
1019
			$missingFields = array_diff_key($objFields, $dbFields);
1020
1021
			if($missingFields) return false;
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...
1022
		}
1023
		self::$database_is_ready = true;
1024
1025
		return true;
1026
	}
1027
1028
	/**
1029
	 * Enable or disable recording of login attempts
1030
	 * through the {@link LoginRecord} object.
1031
	 *
1032
	 * @deprecated 4.0 Use the "Security.login_recording" config setting instead
1033
	 * @param boolean $bool
1034
	 */
1035
	public static function set_login_recording($bool) {
1036
		Deprecation::notice('4.0', 'Use the "Security.login_recording" config setting instead');
1037
		self::$login_recording = (bool)$bool;
1038
	}
1039
1040
	/**
1041
	 * @deprecated 4.0 Use the "Security.login_recording" config setting instead
1042
	 * @return boolean
1043
	 */
1044
	public static function login_recording() {
1045
		Deprecation::notice('4.0', 'Use the "Security.login_recording" config setting instead');
1046
		return self::$login_recording;
1047
	}
1048
1049
	/**
1050
	 * @config
1051
	 * @var string Set the default login dest
1052
	 * This is the URL that users will be redirected to after they log in,
1053
	 * if they haven't logged in en route to access a secured page.
1054
	 * By default, this is set to the homepage.
1055
	 */
1056
	private static $default_login_dest = "";
1057
1058
	/**
1059
	 * @deprecated 4.0 Use the "Security.default_login_dest" config setting instead
1060
	 */
1061
	public static function set_default_login_dest($dest) {
1062
		Deprecation::notice('4.0', 'Use the "Security.default_login_dest" config setting instead');
1063
		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...
1064
	}
1065
1066
	/**
1067
	 * Get the default login dest.
1068
	 *
1069
	 * @deprecated 4.0 Use the "Security.default_login_dest" config setting instead
1070
	 */
1071
	public static function default_login_dest() {
1072
		Deprecation::notice('4.0', 'Use the "Security.default_login_dest" config setting instead');
1073
		return self::config()->default_login_dest;
1074
	}
1075
1076
	protected static $ignore_disallowed_actions = false;
1077
1078
	/**
1079
	 * Set to true to ignore access to disallowed actions, rather than returning permission failure
1080
	 * Note that this is just a flag that other code needs to check with Security::ignore_disallowed_actions()
1081
	 * @param $flag True or false
1082
	 */
1083
	public static function set_ignore_disallowed_actions($flag) {
1084
		self::$ignore_disallowed_actions = $flag;
1085
	}
1086
1087
	public static function ignore_disallowed_actions() {
1088
		return self::$ignore_disallowed_actions;
1089
	}
1090
1091
1092
	/**
1093
	 * Set a custom log-in URL if you have built your own log-in page.
1094
	 *
1095
	 * @deprecated 4.0 Use the "Security.login_url" config setting instead.
1096
	 */
1097
	public static function set_login_url($loginUrl) {
1098
		Deprecation::notice('4.0', 'Use the "Security.login_url" config setting instead');
1099
		self::config()->update("login_url", $loginUrl);
1100
	}
1101
1102
1103
	/**
1104
	 * Get the URL of the log-in page.
1105
	 *
1106
	 * To update the login url use the "Security.login_url" config setting.
1107
	 *
1108
	 * @return string
1109
	 */
1110
	public static function login_url() {
1111
		return self::config()->login_url;
1112
	}
1113
1114
1115
	/**
1116
	 * Get the URL of the logout page.
1117
	 *
1118
	 * To update the logout url use the "Security.logout_url" config setting.
1119
	 *
1120
	 * @return string
1121
	 */
1122
	public static function logout_url() {
1123
		return self::config()->logout_url;
1124
	}
1125
1126
1127
	/**
1128
	 * Defines global accessible templates variables.
1129
	 *
1130
	 * @return array
1131
	 */
1132
	public static function get_template_global_variables() {
1133
		return array(
1134
			"LoginURL" => "login_url",
1135
			"LogoutURL" => "logout_url",
1136
		);
1137
	}
1138
1139
}
1140