Completed
Push — master ( 20efb0...a2cc06 )
by Hamish
29s
created

Security   F

Complexity

Total Complexity 121

Size/Duplication

Total Lines 1158
Duplicated Lines 0 %

Coupling/Cohesion

Components 4
Dependencies 30

Importance

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

49 Methods

Rating   Name   Duplication   Size   Complexity  
A get_word_list() 0 4 1
A set_word_list() 0 4 1
A set_default_message_set() 0 4 1
D permissionFailure() 0 93 14
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 4 1
A ping() 0 3 1
A logout() 0 8 4
D preLogin() 0 26 9
A getResponseController() 0 16 2
A getTemplatesFor() 0 4 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 8 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 47 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
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
	 * Get location of word list file
160
	 *
161
	 * @deprecated 4.0 Use the "Security.word_list" config setting instead
162
	 */
163
	public static function get_word_list() {
164
		Deprecation::notice('4.0', 'Use the "Security.word_list" config setting instead');
165
		return self::config()->word_list;
166
	}
167
168
	/**
169
	 * Enable or disable recording of login attempts
170
	 * through the {@link LoginRecord} object.
171
	 *
172
	 * @config
173
	 * @var boolean $login_recording
174
	 */
175
	private static $login_recording = false;
176
177
	/**
178
	 * @var boolean If set to TRUE or FALSE, {@link database_is_ready()}
179
	 * will always return FALSE. Used for unit testing.
180
	 */
181
	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...
182
183
	/**
184
	 * When the database has once been verified as ready, it will not do the
185
	 * checks again.
186
	 *
187
	 * @var bool
188
	 */
189
	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...
190
191
	/**
192
	 * Set location of word list file
193
	 *
194
	 * @deprecated 4.0 Use the "Security.word_list" config setting instead
195
	 * @param string $wordListFile Location of word list file
196
	 */
197
	public static function set_word_list($wordListFile) {
198
		Deprecation::notice('4.0', 'Use the "Security.word_list" config setting instead');
199
		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...
200
	}
201
202
	/**
203
	 * Set the default message set used in permissions failures.
204
	 *
205
	 * @deprecated 4.0 Use the "Security.default_message_set" config setting instead
206
	 * @param string|array $messageSet
207
	 */
208
	public static function set_default_message_set($messageSet) {
209
		Deprecation::notice('4.0', 'Use the "Security.default_message_set" config setting instead');
210
		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...
211
	}
212
213
214
	/**
215
	 * Register that we've had a permission failure trying to view the given page
216
	 *
217
	 * This will redirect to a login page.
218
	 * If you don't provide a messageSet, a default will be used.
219
	 *
220
	 * @param Controller $controller The controller that you were on to cause the permission
221
	 *                               failure.
222
	 * @param string|array $messageSet The message to show to the user. This
223
	 *                                 can be a string, or a map of different
224
	 *                                 messages for different contexts.
225
	 *                                 If you pass an array, you can use the
226
	 *                                 following keys:
227
	 *                                   - default: The default message
228
	 *                                   - alreadyLoggedIn: The message to
229
	 *                                                      show if the user
230
	 *                                                      is already logged
231
	 *                                                      in and lacks the
232
	 *                                                      permission to
233
	 *                                                      access the item.
234
	 *
235
	 * The alreadyLoggedIn value can contain a '%s' placeholder that will be replaced with a link
236
	 * to log in.
237
	 * @return SS_HTTPResponse
238
	 */
239
	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...
240
		self::set_ignore_disallowed_actions(true);
241
242
		if(!$controller) $controller = Controller::curr();
243
244
		if(Director::is_ajax()) {
245
			$response = ($controller) ? $controller->getResponse() : new SS_HTTPResponse();
246
			$response->setStatusCode(403);
247
			if(!Member::currentUser()) {
248
				$response->setBody(_t('ContentController.NOTLOGGEDIN','Not logged in'));
249
				$response->setStatusDescription(_t('ContentController.NOTLOGGEDIN','Not logged in'));
250
				// Tell the CMS to allow re-aunthentication
251
				if(CMSSecurity::enabled()) {
252
					$response->addHeader('X-Reauthenticate', '1');
253
				}
254
			}
255
			return $response;
256
		}
257
258
		// Prepare the messageSet provided
259
		if(!$messageSet) {
260
			if($configMessageSet = static::config()->get('default_message_set')) {
261
				$messageSet = $configMessageSet;
262
			} else {
263
				$messageSet = array(
264
					'default' => _t(
265
						'Security.NOTEPAGESECURED',
266
						"That page is secured. Enter your credentials below and we will send "
267
							. "you right along."
268
					),
269
					'alreadyLoggedIn' => _t(
270
						'Security.ALREADYLOGGEDIN',
271
						"You don't have access to this page.  If you have another account that "
272
							. "can access that page, you can log in again below.",
273
274
						"%s will be replaced with a link to log in."
275
					)
276
				);
277
			}
278
		}
279
280
		if(!is_array($messageSet)) {
281
			$messageSet = array('default' => $messageSet);
282
		}
283
284
		$member = Member::currentUser();
285
286
		// Work out the right message to show
287
		if($member && $member->exists()) {
288
			$response = ($controller) ? $controller->getResponse() : new SS_HTTPResponse();
289
			$response->setStatusCode(403);
290
291
			//If 'alreadyLoggedIn' is not specified in the array, then use the default
292
			//which should have been specified in the lines above
293
			if(isset($messageSet['alreadyLoggedIn'])) {
294
				$message = $messageSet['alreadyLoggedIn'];
295
			} else {
296
				$message = $messageSet['default'];
297
			}
298
299
			// Somewhat hackish way to render a login form with an error message.
300
			$me = new Security();
301
			$form = $me->LoginForm();
302
			$form->sessionMessage($message, 'warning');
303
			Session::set('MemberLoginForm.force_message',1);
304
			$loginResponse = $me->login();
305
			if($loginResponse instanceof SS_HTTPResponse) {
306
				return $loginResponse;
307
			}
308
309
			$response->setBody((string)$loginResponse);
310
311
			$controller->extend('permissionDenied', $member);
312
313
			return $response;
314
		} else {
315
			$message = $messageSet['default'];
316
		}
317
318
		Session::set("Security.Message.message", $message);
319
		Session::set("Security.Message.type", 'warning');
320
321
		Session::set("BackURL", $_SERVER['REQUEST_URI']);
322
323
		// TODO AccessLogEntry needs an extension to handle permission denied errors
324
		// Audit logging hook
325
		$controller->extend('permissionDenied', $member);
326
327
		return $controller->redirect(
328
			Config::inst()->get('SilverStripe\\Security\\Security', 'login_url')
329
			. "?BackURL=" . urlencode($_SERVER['REQUEST_URI'])
330
		);
331
	}
332
333
	protected function init() {
334
		parent::init();
335
336
		// Prevent clickjacking, see https://developer.mozilla.org/en-US/docs/HTTP/X-Frame-Options
337
		$this->getResponse()->addHeader('X-Frame-Options', 'SAMEORIGIN');
338
	}
339
340
	public function index() {
341
		return $this->httpError(404); // no-op
342
	}
343
344
	/**
345
	 * Get the selected authenticator for this request
346
	 *
347
	 * @return string Class name of Authenticator
348
	 */
349
	protected function getAuthenticator() {
350
		$authenticator = $this->getRequest()->requestVar('AuthenticationMethod');
351
		if($authenticator) {
352
			$authenticators = Authenticator::get_authenticators();
353
			if(in_array($authenticator, $authenticators)) {
354
				return $authenticator;
355
			}
356
		} else {
357
			return Authenticator::get_default_authenticator();
358
		}
359
360
	}
361
362
	/**
363
	 * Get the login form to process according to the submitted data
364
	 *
365
	 * @return Form
366
	 * @throws Exception
367
	 */
368
	public function LoginForm() {
369
		$authenticator = $this->getAuthenticator();
370
		if($authenticator) return $authenticator::get_login_form($this);
371
		throw new Exception('Passed invalid authentication method');
372
	}
373
374
	/**
375
	 * Get the login forms for all available authentication methods
376
	 *
377
	 * @return array Returns an array of available login forms (array of Form
378
	 *               objects).
379
	 *
380
	 * @todo Check how to activate/deactivate authentication methods
381
	 */
382
	public function GetLoginForms() {
383
		$forms = array();
384
385
		$authenticators = Authenticator::get_authenticators();
386
		foreach($authenticators as $authenticator) {
387
			$forms[] = $authenticator::get_login_form($this);
388
		}
389
390
		return $forms;
391
	}
392
393
394
	/**
395
	 * Get a link to a security action
396
	 *
397
	 * @param string $action Name of the action
398
	 * @return string Returns the link to the given action
399
	 */
400
	public function Link($action = null) {
401
		/** @skipUpgrade */
402
		return Controller::join_links(Director::baseURL(), "Security", $action);
403
	}
404
405
	/**
406
	 * This action is available as a keep alive, so user
407
	 * sessions don't timeout. A common use is in the admin.
408
	 */
409
	public function ping() {
410
		return 1;
411
	}
412
413
	/**
414
	 * Log the currently logged in user out
415
	 *
416
	 * @param bool $redirect Redirect the user back to where they came.
417
	 *                       - If it's false, the code calling logout() is
418
	 *                         responsible for sending the user where-ever
419
	 *                         they should go.
420
	 */
421
	public function logout($redirect = true) {
422
		$member = Member::currentUser();
423
		if($member) $member->logOut();
424
425
		if($redirect && (!$this->getResponse()->isFinished())) {
426
			$this->redirectBack();
427
		}
428
	}
429
430
	/**
431
	 * Perform pre-login checking and prepare a response if available prior to login
432
	 *
433
	 * @return SS_HTTPResponse Substitute response object if the login process should be curcumvented.
434
	 * Returns null if should proceed as normal.
435
	 */
436
	protected function preLogin() {
437
		// Event handler for pre-login, with an option to let it break you out of the login form
438
		$eventResults = $this->extend('onBeforeSecurityLogin');
439
		// If there was a redirection, return
440
		if($this->redirectedTo()) return $this->getResponse();
441
		// If there was an SS_HTTPResponse object returned, then return that
442
		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...
443
			foreach($eventResults as $result) {
444
				if($result instanceof SS_HTTPResponse) return $result;
445
			}
446
		}
447
448
		// If arriving on the login page already logged in, with no security error, and a ReturnURL then redirect
449
		// back. The login message check is neccesary to prevent infinite loops where BackURL links to
450
		// an action that triggers Security::permissionFailure.
451
		// This step is necessary in cases such as automatic redirection where a user is authenticated
452
		// upon landing on an SSL secured site and is automatically logged in, or some other case
453
		// where the user has permissions to continue but is not given the option.
454
		if($this->getRequest()->requestVar('BackURL')
455
			&& !$this->getLoginMessage()
456
			&& ($member = Member::currentUser())
457
			&& $member->exists()
458
		) {
459
			return $this->redirectBack();
460
		}
461
	}
462
463
	/**
464
	 * Prepare the controller for handling the response to this request
465
	 *
466
	 * @param string $title Title to use
467
	 * @return Controller
468
	 */
469
	protected function getResponseController($title) {
470
		if(!class_exists('SiteTree')) return $this;
471
472
		// Use sitetree pages to render the security page
473
		$tmpPage = new Page();
474
		$tmpPage->Title = $title;
475
		/** @skipUpgrade */
476
		$tmpPage->URLSegment = "Security";
477
		// Disable ID-based caching  of the log-in page by making it a random number
478
		$tmpPage->ID = -1 * rand(1,10000000);
479
480
		$controller = Page_Controller::create($tmpPage);
481
		$controller->setDataModel($this->model);
482
		$controller->doInit();
483
		return $controller;
484
	}
485
486
	/**
487
	 * Determine the list of templates to use for rendering the given action
488
	 *
489
	 * @param string $action
490
	 * @return array Template list
491
	 */
492
	public function getTemplatesFor($action) {
493
		/** @skipUpgrade */
494
		return array("Security_{$action}", 'Security', $this->stat('template_main'), 'BlankPage');
495
	}
496
497
	/**
498
	 * Combine the given forms into a formset with a tabbed interface
499
	 *
500
	 * @param array $forms List of LoginForm instances
501
	 * @return string
502
	 */
503
	protected function generateLoginFormSet($forms) {
504
		$viewData = new ArrayData(array(
505
			'Forms' => new ArrayList($forms),
506
		));
507
		return $viewData->renderWith(
508
			$this->getIncludeTemplate('MultiAuthenticatorLogin')
509
		);
510
	}
511
512
	/**
513
	 * Get the HTML Content for the $Content area during login
514
	 *
515
	 * @param string &$messageType Type of message, if available, passed back to caller
516
	 * @return string Message in HTML format
517
	 */
518
	protected function getLoginMessage(&$messageType = null) {
519
		$message = Session::get('Security.Message.message');
520
		$messageType = null;
521
		if(empty($message)) return null;
522
523
		$messageType = Session::get('Security.Message.type');
524
		if($messageType === 'bad') {
525
			return "<p class=\"message $messageType\">$message</p>";
526
		} else {
527
			return "<p>$message</p>";
528
		}
529
	}
530
531
532
	/**
533
	 * Show the "login" page
534
	 *
535
	 * For multiple authenticators, Security_MultiAuthenticatorLogin is used.
536
	 * See getTemplatesFor and getIncludeTemplate for how to override template logic
537
	 *
538
	 * @return string|SS_HTTPResponse Returns the "login" page as HTML code.
539
	 */
540
	public function login() {
541
		// Check pre-login process
542
		if($response = $this->preLogin()) return $response;
543
544
		// Get response handler
545
		$controller = $this->getResponseController(_t('Security.LOGIN', 'Log in'));
546
547
		// if the controller calls Director::redirect(), this will break early
548
		if(($response = $controller->getResponse()) && $response->isFinished()) return $response;
549
550
		$forms = $this->GetLoginForms();
551
		if(!count($forms)) {
552
			user_error('No login-forms found, please use Authenticator::register_authenticator() to add one',
553
				E_USER_ERROR);
554
		}
555
556
		// Handle any form messages from validation, etc.
557
		$messageType = '';
558
		$message = $this->getLoginMessage($messageType);
559
560
		// We've displayed the message in the form output, so reset it for the next run.
561
		Session::clear('Security.Message');
562
563
		// only display tabs when more than one authenticator is provided
564
		// to save bandwidth and reduce the amount of custom styling needed
565
		if(count($forms) > 1) {
566
			$content = $this->generateLoginFormSet($forms);
567
		} else {
568
			$content = $forms[0]->forTemplate();
569
		}
570
571
		// Finally, customise the controller to add any form messages and the form.
572
		$customisedController = $controller->customise(array(
573
			"Content" => $message,
574
			"Message" => $message,
575
			"MessageType" => $messageType,
576
			"Form" => $content,
577
		));
578
579
		// Return the customised controller
580
		return $customisedController->renderWith(
581
			$this->getTemplatesFor('login')
582
		);
583
	}
584
585
	public function basicauthlogin() {
586
		$member = BasicAuth::requireLogin("SilverStripe login", 'ADMIN');
587
		$member->logIn();
588
	}
589
590
	/**
591
	 * Show the "lost password" page
592
	 *
593
	 * @return string Returns the "lost password" page as HTML code.
594
	 */
595
	public function lostpassword() {
596
		$controller = $this->getResponseController(_t('Security.LOSTPASSWORDHEADER', 'Lost Password'));
597
598
		// if the controller calls Director::redirect(), this will break early
599
		if(($response = $controller->getResponse()) && $response->isFinished()) return $response;
600
601
		$customisedController = $controller->customise(array(
602
			'Content' =>
603
				'<p>' .
604
				_t(
605
					'Security.NOTERESETPASSWORD',
606
					'Enter your e-mail address and we will send you a link with which you can reset your password'
607
				) .
608
				'</p>',
609
			'Form' => $this->LostPasswordForm(),
610
		));
611
612
		//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...
613
		return $customisedController->renderWith($this->getTemplatesFor('lostpassword'));
614
	}
615
616
617
	/**
618
	 * Factory method for the lost password form
619
	 *
620
	 * @return Form Returns the lost password form
621
	 */
622
	public function LostPasswordForm() {
623
		return MemberLoginForm::create(
624
			$this,
625
			'LostPasswordForm',
626
			new FieldList(
627
				new EmailField('Email', _t('Member.EMAIL', 'Email'))
628
			),
629
			new FieldList(
630
				new FormAction(
631
					'forgotPassword',
632
					_t('Security.BUTTONSEND', 'Send me the password reset link')
633
				)
634
			),
635
			false
636
		);
637
	}
638
639
640
	/**
641
	 * Show the "password sent" page, after a user has requested
642
	 * to reset their password.
643
	 *
644
	 * @param SS_HTTPRequest $request The SS_HTTPRequest for this action.
645
	 * @return string Returns the "password sent" page as HTML code.
646
	 */
647
	public function passwordsent($request) {
648
		$controller = $this->getResponseController(_t('Security.LOSTPASSWORDHEADER', 'Lost Password'));
649
650
		// if the controller calls Director::redirect(), this will break early
651
		if(($response = $controller->getResponse()) && $response->isFinished()) return $response;
652
653
		$email = Convert::raw2xml(rawurldecode($request->param('ID')) . '.' . $request->getExtension());
654
655
		$customisedController = $controller->customise(array(
656
			'Title' => _t('Security.PASSWORDSENTHEADER', "Password reset link sent to '{email}'",
657
				array('email' => $email)),
658
			'Content' =>
659
				"<p>"
660
				. _t('Security.PASSWORDSENTTEXT',
661
					"Thank you! A reset link has been sent to '{email}', provided an account exists for this email"
662
					. " address.",
663
					array('email' => $email))
664
				. "</p>",
665
			'Email' => $email
666
		));
667
668
		//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...
669
		return $customisedController->renderWith($this->getTemplatesFor('passwordsent'));
670
	}
671
672
673
	/**
674
	 * Create a link to the password reset form.
675
	 *
676
	 * GET parameters used:
677
	 * - m: member ID
678
	 * - t: plaintext token
679
	 *
680
	 * @param Member $member Member object associated with this link.
681
	 * @param string $autologinToken The auto login token.
682
	 * @return string
683
	 */
684
	public static function getPasswordResetLink($member, $autologinToken) {
685
		$autologinToken = urldecode($autologinToken);
686
		$selfControllerClass = __CLASS__;
687
		$selfController = new $selfControllerClass();
688
		return $selfController->Link('changepassword') . "?m={$member->ID}&t=$autologinToken";
689
	}
690
691
	/**
692
	 * Show the "change password" page.
693
	 * This page can either be called directly by logged-in users
694
	 * (in which case they need to provide their old password),
695
	 * or through a link emailed through {@link lostpassword()}.
696
	 * In this case no old password is required, authentication is ensured
697
	 * through the Member.AutoLoginHash property.
698
	 *
699
	 * @see ChangePasswordForm
700
	 *
701
	 * @return string Returns the "change password" page as HTML code.
702
	 */
703
	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...
704
		$controller = $this->getResponseController(_t('Security.CHANGEPASSWORDHEADER', 'Change your password'));
705
706
		// if the controller calls Director::redirect(), this will break early
707
		if(($response = $controller->getResponse()) && $response->isFinished()) return $response;
708
709
		// Extract the member from the URL.
710
		$member = null;
711
		if (isset($_REQUEST['m'])) {
712
			$member = Member::get()->filter('ID', (int)$_REQUEST['m'])->first();
713
		}
714
715
		// Check whether we are merely changin password, or resetting.
716
		if(isset($_REQUEST['t']) && $member && $member->validateAutoLoginToken($_REQUEST['t'])) {
717
			// On first valid password reset request redirect to the same URL without hash to avoid referrer leakage.
718
719
			// if there is a current member, they should be logged out
720
			if ($curMember = Member::currentUser()) {
721
				$curMember->logOut();
722
			}
723
724
			// Store the hash for the change password form. Will be unset after reload within the ChangePasswordForm.
725
			Session::set('AutoLoginHash', $member->encryptWithUserSettings($_REQUEST['t']));
726
727
			return $this->redirect($this->Link('changepassword'));
728
		} elseif(Session::get('AutoLoginHash')) {
729
			// Subsequent request after the "first load with hash" (see previous if clause).
730
			$customisedController = $controller->customise(array(
731
				'Content' =>
732
					'<p>' .
733
					_t('Security.ENTERNEWPASSWORD', 'Please enter a new password.') .
734
					'</p>',
735
				'Form' => $this->ChangePasswordForm(),
736
			));
737
		} elseif(Member::currentUser()) {
738
			// Logged in user requested a password change form.
739
			$customisedController = $controller->customise(array(
740
				'Content' => '<p>'
741
					. _t('Security.CHANGEPASSWORDBELOW', 'You can change your password below.') . '</p>',
742
				'Form' => $this->ChangePasswordForm()));
743
744
		} else {
745
			// Show friendly message if it seems like the user arrived here via password reset feature.
746
			if(isset($_REQUEST['m']) || isset($_REQUEST['t'])) {
747
				$customisedController = $controller->customise(
748
					array('Content' =>
749
						_t(
750
							'Security.NOTERESETLINKINVALID',
751
							'<p>The password reset link is invalid or expired.</p>'
752
							. '<p>You can request a new one <a href="{link1}">here</a> or change your password after'
753
							. ' you <a href="{link2}">logged in</a>.</p>',
754
							array('link1' => $this->Link('lostpassword'), 'link2' => $this->link('login'))
755
						)
756
					)
757
				);
758
			} else {
759
				self::permissionFailure(
760
					$this,
761
					_t('Security.ERRORPASSWORDPERMISSION', 'You must be logged in in order to change your password!')
762
				);
763
				return;
764
			}
765
		}
766
767
		return $customisedController->renderWith($this->getTemplatesFor('changepassword'));
768
	}
769
770
	/**
771
	 * Factory method for the lost password form
772
	 *
773
	 * @return ChangePasswordForm Returns the lost password form
774
	 */
775
	public function ChangePasswordForm() {
776
		/** @skipUpgrade */
777
		$formName = 'ChangePasswordForm';
778
		return \Injector::inst()->createWithArgs(
779
			'SilverStripe\\Security\\ChangePasswordForm',
780
			[ $this,  $formName]
781
		);
782
	}
783
784
	/**
785
	 * Gets the template for an include used for security.
786
	 * For use in any subclass.
787
	 *
788
	 * @param string $name
789
	 * @return array Returns the template(s) for rendering
790
	 */
791
	public function getIncludeTemplate($name) {
792
		return array('Security_' . $name);
793
	}
794
795
	/**
796
	 * Return an existing member with administrator privileges, or create one of necessary.
797
	 *
798
	 * Will create a default 'Administrators' group if no group is found
799
	 * with an ADMIN permission. Will create a new 'Admin' member with administrative permissions
800
	 * if no existing Member with these permissions is found.
801
	 *
802
	 * Important: Any newly created administrator accounts will NOT have valid
803
	 * login credentials (Email/Password properties), which means they can't be used for login
804
	 * purposes outside of any default credentials set through {@link Security::setDefaultAdmin()}.
805
	 *
806
	 * @return Member
807
	 */
808
	public static function findAnAdministrator() {
809
		// coupling to subsites module
810
		$origSubsite = null;
811
		if(is_callable('Subsite::changeSubsite')) {
812
			$origSubsite = \Subsite::currentSubsiteID();
813
			\Subsite::changeSubsite(0);
814
		}
815
816
		$member = null;
817
818
		// find a group with ADMIN permission
819
		$adminGroup = Permission::get_groups_by_permission('ADMIN')->first();
820
821
		if(is_callable('Subsite::changeSubsite')) {
822
			\Subsite::changeSubsite($origSubsite);
823
		}
824
825
		if ($adminGroup) {
826
			$member = $adminGroup->Members()->First();
827
		}
828
829
		if(!$adminGroup) {
830
			Group::singleton()->requireDefaultRecords();
831
			$adminGroup = Permission::get_groups_by_permission('ADMIN')->first();
832
		}
833
834
		if(!$member) {
835
			Member::singleton()->requireDefaultRecords();
836
			$member = Permission::get_members_by_permission('ADMIN')->first();
837
		}
838
839
		if(!$member) {
840
			$member = Member::default_admin();
841
		}
842
843
		if(!$member) {
844
			// Failover to a blank admin
845
			$member = Member::create();
846
			$member->FirstName = _t('Member.DefaultAdminFirstname', 'Default Admin');
847
			$member->write();
848
			// Add member to group instead of adding group to member
849
			// This bypasses the privilege escallation code in Member_GroupSet
850
			$adminGroup
851
				->DirectMembers()
852
				->add($member);
853
		}
854
855
		return $member;
856
	}
857
858
	/**
859
	 * Flush the default admin credentials
860
	 */
861
	public static function clear_default_admin() {
862
		self::$default_username = null;
863
		self::$default_password = null;
864
	}
865
866
867
	/**
868
	 * Set a default admin in dev-mode
869
	 *
870
	 * This will set a static default-admin which is not existing
871
	 * as a database-record. By this workaround we can test pages in dev-mode
872
	 * with a unified login. Submitted login-credentials are first checked
873
	 * against this static information in {@link Security::authenticate()}.
874
	 *
875
	 * @param string $username The user name
876
	 * @param string $password The password (in cleartext)
877
	 * @return bool
878
	 */
879
	public static function setDefaultAdmin($username, $password) {
880
		// don't overwrite if already set
881
		if(self::$default_username || self::$default_password) {
882
			return false;
883
		}
884
885
		self::$default_username = $username;
886
		self::$default_password = $password;
887
	}
888
889
	/**
890
	 * Checks if the passed credentials are matching the default-admin.
891
	 * Compares cleartext-password set through Security::setDefaultAdmin().
892
	 *
893
	 * @param string $username
894
	 * @param string $password
895
	 * @return bool
896
	 */
897
	public static function check_default_admin($username, $password) {
898
		return (
899
			self::$default_username === $username
900
			&& self::$default_password === $password
901
			&& self::has_default_admin()
902
		);
903
	}
904
905
	/**
906
	 * Check that the default admin account has been set.
907
	 */
908
	public static function has_default_admin() {
909
		return !empty(self::$default_username) && !empty(self::$default_password);
910
	}
911
912
	/**
913
	 * Get default admin username
914
	 *
915
	 * @return string
916
	 */
917
	public static function default_admin_username() {
918
		return self::$default_username;
919
	}
920
921
	/**
922
	 * Get default admin password
923
	 *
924
	 * @return string
925
	 */
926
	public static function default_admin_password() {
927
		return self::$default_password;
928
	}
929
930
	/**
931
	 * Set strict path checking
932
	 *
933
	 * This prevents sharing of the session across several sites in the
934
	 * domain.
935
	 *
936
	 * @deprecated 4.0 Use the "Security.strict_path_checking" config setting instead
937
	 * @param boolean $strictPathChecking To enable or disable strict patch
938
	 *                                    checking.
939
	 */
940
	public static function setStrictPathChecking($strictPathChecking) {
941
		Deprecation::notice('4.0', 'Use the "Security.strict_path_checking" config setting instead');
942
		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...
943
	}
944
945
946
	/**
947
	 * Get strict path checking
948
	 *
949
	 * @deprecated 4.0 Use the "Security.strict_path_checking" config setting instead
950
	 * @return boolean Status of strict path checking
951
	 */
952
	public static function getStrictPathChecking() {
953
		Deprecation::notice('4.0', 'Use the "Security.strict_path_checking" config setting instead');
954
		return self::config()->strict_path_checking;
955
	}
956
957
958
	/**
959
	 * Set the password encryption algorithm
960
	 *
961
	 * @deprecated 4.0 Use the "Security.password_encryption_algorithm" config setting instead
962
	 * @param string $algorithm One of the available password encryption
963
	 *  algorithms determined by {@link Security::get_encryption_algorithms()}
964
	 * @return bool Returns TRUE if the passed algorithm was valid, otherwise FALSE.
965
	 */
966
	public static function set_password_encryption_algorithm($algorithm) {
967
		Deprecation::notice('4.0', 'Use the "Security.password_encryption_algorithm" config setting instead');
968
969
		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...
970
	}
971
972
	/**
973
	 * @deprecated 4.0 Use the "Security.password_encryption_algorithm" config setting instead
974
	 * @return String
975
	 */
976
	public static function get_password_encryption_algorithm() {
977
		Deprecation::notice('4.0', 'Use the "Security.password_encryption_algorithm" config setting instead');
978
		return self::config()->password_encryption_algorithm;
979
	}
980
981
	/**
982
	 * Encrypt a password according to the current password encryption settings.
983
	 * If the settings are so that passwords shouldn't be encrypted, the
984
	 * result is simple the clear text password with an empty salt except when
985
	 * a custom algorithm ($algorithm parameter) was passed.
986
	 *
987
	 * @param string $password The password to encrypt
988
	 * @param string $salt Optional: The salt to use. If it is not passed, but
989
	 *  needed, the method will automatically create a
990
	 *  random salt that will then be returned as return value.
991
	 * @param string $algorithm Optional: Use another algorithm to encrypt the
992
	 *  password (so that the encryption algorithm can be changed over the time).
993
	 * @param Member $member Optional
994
	 * @return mixed Returns an associative array containing the encrypted
995
	 *  password and the used salt in the form:
996
	 * <code>
997
	 * 	array(
998
	 * 	'password' => string,
999
	 * 	'salt' => string,
1000
	 * 	'algorithm' => string,
1001
	 * 	'encryptor' => PasswordEncryptor instance
1002
	 * 	)
1003
	 * </code>
1004
	 * If the passed algorithm is invalid, FALSE will be returned.
1005
	 *
1006
	 * @see encrypt_passwords()
1007
	 */
1008
	public static function encrypt_password($password, $salt = null, $algorithm = null, $member = null) {
1009
		// Fall back to the default encryption algorithm
1010
		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...
1011
1012
		$e = PasswordEncryptor::create_for_algorithm($algorithm);
1013
1014
		// New salts will only need to be generated if the password is hashed for the first time
1015
		$salt = ($salt) ? $salt : $e->salt($password);
1016
1017
		return array(
1018
			'password' => $e->encrypt($password, $salt, $member),
1019
			'salt' => $salt,
1020
			'algorithm' => $algorithm,
1021
			'encryptor' => $e
1022
		);
1023
	}
1024
1025
	/**
1026
	 * Checks the database is in a state to perform security checks.
1027
	 * See {@link DatabaseAdmin->init()} for more information.
1028
	 *
1029
	 * @return bool
1030
	 */
1031
	public static function database_is_ready() {
1032
		// Used for unit tests
1033
		if(self::$force_database_is_ready !== null) {
1034
			return self::$force_database_is_ready;
1035
		}
1036
1037
		if(self::$database_is_ready) {
1038
			return self::$database_is_ready;
1039
		}
1040
1041
		$requiredClasses = ClassInfo::dataClassesFor('SilverStripe\\Security\\Member');
1042
		$requiredClasses[] = 'SilverStripe\\Security\\Group';
1043
		$requiredClasses[] = 'SilverStripe\\Security\\Permission';
1044
1045
		foreach($requiredClasses as $class) {
1046
			// Skip test classes, as not all test classes are scaffolded at once
1047
			if(is_subclass_of($class, 'TestOnly')) {
1048
				continue;
1049
			}
1050
1051
			// if any of the tables aren't created in the database
1052
			$table = DataObject::getSchema()->tableName($class);
1053
			if(!ClassInfo::hasTable($table)) {
1054
				return false;
1055
			}
1056
1057
			// HACK: DataExtensions aren't applied until a class is instantiated for
1058
			// the first time, so create an instance here.
1059
			singleton($class);
1060
1061
			// if any of the tables don't have all fields mapped as table columns
1062
			$dbFields = DB::field_list($table);
1063
			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...
1064
				return false;
1065
			}
1066
1067
			$objFields = DataObject::database_fields($class);
1068
			$missingFields = array_diff_key($objFields, $dbFields);
1069
1070
			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...
1071
				return false;
1072
			}
1073
		}
1074
		self::$database_is_ready = true;
1075
1076
		return true;
1077
	}
1078
1079
	/**
1080
	 * Enable or disable recording of login attempts
1081
	 * through the {@link LoginRecord} object.
1082
	 *
1083
	 * @deprecated 4.0 Use the "Security.login_recording" config setting instead
1084
	 * @param boolean $bool
1085
	 */
1086
	public static function set_login_recording($bool) {
1087
		Deprecation::notice('4.0', 'Use the "Security.login_recording" config setting instead');
1088
		self::$login_recording = (bool)$bool;
1089
	}
1090
1091
	/**
1092
	 * @deprecated 4.0 Use the "Security.login_recording" config setting instead
1093
	 * @return boolean
1094
	 */
1095
	public static function login_recording() {
1096
		Deprecation::notice('4.0', 'Use the "Security.login_recording" config setting instead');
1097
		return self::$login_recording;
1098
	}
1099
1100
	/**
1101
	 * @config
1102
	 * @var string Set the default login dest
1103
	 * This is the URL that users will be redirected to after they log in,
1104
	 * if they haven't logged in en route to access a secured page.
1105
	 * By default, this is set to the homepage.
1106
	 */
1107
	private static $default_login_dest = "";
1108
1109
	/**
1110
	 * @deprecated 4.0 Use the "Security.default_login_dest" config setting instead
1111
	 */
1112
	public static function set_default_login_dest($dest) {
1113
		Deprecation::notice('4.0', 'Use the "Security.default_login_dest" config setting instead');
1114
		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...
1115
	}
1116
1117
	/**
1118
	 * Get the default login dest.
1119
	 *
1120
	 * @deprecated 4.0 Use the "Security.default_login_dest" config setting instead
1121
	 */
1122
	public static function default_login_dest() {
1123
		Deprecation::notice('4.0', 'Use the "Security.default_login_dest" config setting instead');
1124
		return self::config()->default_login_dest;
1125
	}
1126
1127
	protected static $ignore_disallowed_actions = false;
1128
1129
	/**
1130
	 * Set to true to ignore access to disallowed actions, rather than returning permission failure
1131
	 * Note that this is just a flag that other code needs to check with Security::ignore_disallowed_actions()
1132
	 * @param $flag True or false
1133
	 */
1134
	public static function set_ignore_disallowed_actions($flag) {
1135
		self::$ignore_disallowed_actions = $flag;
1136
	}
1137
1138
	public static function ignore_disallowed_actions() {
1139
		return self::$ignore_disallowed_actions;
1140
	}
1141
1142
1143
	/**
1144
	 * Set a custom log-in URL if you have built your own log-in page.
1145
	 *
1146
	 * @deprecated 4.0 Use the "Security.login_url" config setting instead.
1147
	 */
1148
	public static function set_login_url($loginUrl) {
1149
		Deprecation::notice('4.0', 'Use the "Security.login_url" config setting instead');
1150
		self::config()->update("login_url", $loginUrl);
1151
	}
1152
1153
1154
	/**
1155
	 * Get the URL of the log-in page.
1156
	 *
1157
	 * To update the login url use the "Security.login_url" config setting.
1158
	 *
1159
	 * @return string
1160
	 */
1161
	public static function login_url() {
1162
		return self::config()->login_url;
1163
	}
1164
1165
1166
	/**
1167
	 * Get the URL of the logout page.
1168
	 *
1169
	 * To update the logout url use the "Security.logout_url" config setting.
1170
	 *
1171
	 * @return string
1172
	 */
1173
	public static function logout_url() {
1174
		return self::config()->logout_url;
1175
	}
1176
1177
1178
	/**
1179
	 * Defines global accessible templates variables.
1180
	 *
1181
	 * @return array
1182
	 */
1183
	public static function get_template_global_variables() {
1184
		return array(
1185
			"LoginURL" => "login_url",
1186
			"LogoutURL" => "logout_url",
1187
		);
1188
	}
1189
1190
}
1191