Completed
Push — authenticator-refactor ( 54d3b4...521de8 )
by Simon
06:39
created

Security::delegateToHandler()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 57
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 16
nc 4
nop 3
dl 0
loc 57
rs 8.7433
c 0
b 0
f 0

1 Method

Rating   Name   Duplication   Size   Complexity  
A Security::basicauthlogin() 0 5 1

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace SilverStripe\Security;
4
5
use Page;
6
use LogicException;
7
use SilverStripe\CMS\Controllers\ContentController;
8
use SilverStripe\Control\Controller;
9
use SilverStripe\Control\Director;
10
use SilverStripe\Control\HTTPRequest;
11
use SilverStripe\Control\HTTPResponse;
12
use SilverStripe\Control\Session;
13
use SilverStripe\Control\RequestHandler;
14
use SilverStripe\Core\ClassInfo;
15
use SilverStripe\Core\Config\Config;
16
use SilverStripe\Core\Convert;
17
use SilverStripe\Dev\Deprecation;
18
use SilverStripe\Dev\TestOnly;
19
use SilverStripe\Forms\EmailField;
20
use SilverStripe\Forms\FieldList;
21
use SilverStripe\Forms\Form;
22
use SilverStripe\Forms\FormAction;
23
use SilverStripe\ORM\ArrayList;
24
use SilverStripe\ORM\DataModel;
25
use SilverStripe\ORM\DB;
26
use SilverStripe\ORM\DataObject;
27
use SilverStripe\ORM\FieldType\DBField;
28
use SilverStripe\ORM\ValidationResult;
29
use SilverStripe\View\ArrayData;
30
use SilverStripe\View\SSViewer;
31
use SilverStripe\View\TemplateGlobalProvider;
32
use Exception;
33
use SilverStripe\View\ViewableData_Customised;
34
use Subsite;
35
use SilverStripe\Core\Injector\Injector;
36
37
/**
38
 * Implements a basic security model
39
 */
40
class Security extends Controller implements TemplateGlobalProvider
41
{
42
43
    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...
44
        'index',
45
        'login',
46
        'logout',
47
        'basicauthlogin',
48
        'lostpassword',
49
        'passwordsent',
50
        'changepassword',
51
        'ping',
52
        'LoginForm',
53
        'ChangePasswordForm',
54
        'LostPasswordForm',
55
    );
56
57
    /**
58
     * Default user name. Only used in dev-mode by {@link setDefaultAdmin()}
59
     *
60
     * @var string
61
     * @see setDefaultAdmin()
62
     */
63
    protected static $default_username;
64
65
    /**
66
     * Default password. Only used in dev-mode by {@link setDefaultAdmin()}
67
     *
68
     * @var string
69
     * @see setDefaultAdmin()
70
     */
71
    protected static $default_password;
72
73
    /**
74
     * If set to TRUE to prevent sharing of the session across several sites
75
     * in the domain.
76
     *
77
     * @config
78
     * @var bool
79
     */
80
    protected static $strict_path_checking = false;
81
82
    /**
83
     * The password encryption algorithm to use by default.
84
     * This is an arbitrary code registered through {@link PasswordEncryptor}.
85
     *
86
     * @config
87
     * @var string
88
     */
89
    private static $password_encryption_algorithm = 'blowfish';
90
91
    /**
92
     * Showing "Remember me"-checkbox
93
     * on loginform, and saving encrypted credentials to a cookie.
94
     *
95
     * @config
96
     * @var bool
97
     */
98
    private static $autologin_enabled = true;
99
100
    /**
101
     * Determine if login username may be remembered between login sessions
102
     * If set to false this will disable autocomplete and prevent username persisting in the session
103
     *
104
     * @config
105
     * @var bool
106
     */
107
    private static $remember_username = true;
108
109
    /**
110
     * Location of word list to use for generating passwords
111
     *
112
     * @config
113
     * @var string
114
     */
115
    private static $word_list = './wordlist.txt';
116
117
    /**
118
     * @config
119
     * @var string
120
     */
121
    private static $template = 'BlankPage';
122
123
    /**
124
     * Template thats used to render the pages.
125
     *
126
     * @var string
127
     * @config
128
     */
129
    private static $template_main = 'Page';
130
131
    /**
132
     * Class to use for page rendering
133
     *
134
     * @var string
135
     * @config
136
     */
137
    private static $page_class = Page::class;
138
139
    /**
140
     * Default message set used in permission failures.
141
     *
142
     * @config
143
     * @var array|string
144
     */
145
    private static $default_message_set;
146
147
    /**
148
     * Random secure token, can be used as a crypto key internally.
149
     * Generate one through 'sake dev/generatesecuretoken'.
150
     *
151
     * @config
152
     * @var String
153
     */
154
    private static $token;
155
156
    /**
157
     * The default login URL
158
     *
159
     * @config
160
     *
161
     * @var string
162
     */
163
    private static $login_url = "Security/login";
164
165
    /**
166
     * The default logout URL
167
     *
168
     * @config
169
     *
170
     * @var string
171
     */
172
    private static $logout_url = "Security/logout";
173
174
    /**
175
     * The default lost password URL
176
     *
177
     * @config
178
     *
179
     * @var string
180
     */
181
    private static $lost_password_url = "Security/lostpassword";
182
183
    /**
184
     * Value of X-Frame-Options header
185
     *
186
     * @config
187
     * @var string
188
     */
189
    private static $frame_options = 'SAMEORIGIN';
190
191
    /**
192
     * Value of the X-Robots-Tag header (for the Security section)
193
     *
194
     * @config
195
     * @var string
196
     */
197
    private static $robots_tag = 'noindex, nofollow';
198
199
    /**
200
     * Enable or disable recording of login attempts
201
     * through the {@link LoginRecord} object.
202
     *
203
     * @config
204
     * @var boolean $login_recording
205
     */
206
    private static $login_recording = false;
207
208
    /**
209
     * @var boolean If set to TRUE or FALSE, {@link database_is_ready()}
210
     * will always return FALSE. Used for unit testing.
211
     */
212
    protected static $force_database_is_ready = null;
213
214
    /**
215
     * When the database has once been verified as ready, it will not do the
216
     * checks again.
217
     *
218
     * @var bool
219
     */
220
    protected static $database_is_ready = false;
221
222
    protected static $authenticators = [];
223
224
    protected static $default_authenticator = MemberAuthenticator\Authenticator::class;
225
226
    /**
227
     * Get all registered authenticators
228
     *
229
     * @return array Return an array of Authenticator objects
230
     */
231
    public static function getAuthenticators()
232
    {
233
        $authenticatorClasses = self::config()->authenticators;
0 ignored issues
show
Documentation introduced by
The property authenticators does not exist on object<SilverStripe\Core\Config\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...
234
        $default = self::config()->default_authenticator;
0 ignored issues
show
Documentation introduced by
The property default_authenticator does not exist on object<SilverStripe\Core\Config\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...
235
236
        if (!$authenticatorClasses) {
237
            if ($default) {
238
                $authenticatorClasses = [$default];
239
            } else {
240
                return [];
241
            }
242
        }
243
244
        // put default authenticator first (mainly for tab-order on loginform)
245
        // But only if there's no other authenticator
246
        if (($key = array_search($default, $authenticatorClasses, true)) && count($$authenticatorClasses) > 1) {
247
            unset($authenticatorClasses[$key]);
248
            array_unshift($authenticatorClasses, $default);
249
        }
250
251
        return array_map(function ($class) {
252
            return Injector::inst()->get($class);
253
        }, $authenticatorClasses);
254
    }
255
256
    /**
257
     * Check if a given authenticator is registered
258
     *
259
     * @param string $authenticator Name of the authenticator class to check
260
     * @return bool Returns TRUE if the authenticator is registered, FALSE
261
     *              otherwise.
262
     */
263
    public static function hasAuthenticator($authenticator)
264
    {
265
        $authenticators = self::config()->get('authenticators');
266
        if (count($authenticators) === 0) {
267
            $authenticators = [self::config()->get('default_authenticator')];
268
        }
269
270
        return in_array($authenticator, $authenticators, true);
271
    }
272
273
    /**
274
     * Register that we've had a permission failure trying to view the given page
275
     *
276
     * This will redirect to a login page.
277
     * If you don't provide a messageSet, a default will be used.
278
     *
279
     * @param Controller $controller The controller that you were on to cause the permission
280
     *                               failure.
281
     * @param string|array $messageSet The message to show to the user. This
282
     *                                 can be a string, or a map of different
283
     *                                 messages for different contexts.
284
     *                                 If you pass an array, you can use the
285
     *                                 following keys:
286
     *                                   - default: The default message
287
     *                                   - alreadyLoggedIn: The message to
288
     *                                                      show if the user
289
     *                                                      is already logged
290
     *                                                      in and lacks the
291
     *                                                      permission to
292
     *                                                      access the item.
293
     *
294
     * The alreadyLoggedIn value can contain a '%s' placeholder that will be replaced with a link
295
     * to log in.
296
     * @return HTTPResponse
297
     */
298
    public static function permissionFailure($controller = null, $messageSet = null)
299
    {
300
        self::set_ignore_disallowed_actions(true);
301
302
        if (!$controller) {
303
            $controller = Controller::curr();
304
        }
305
306
        if (Director::is_ajax()) {
307
            $response = ($controller) ? $controller->getResponse() : new HTTPResponse();
308
            $response->setStatusCode(403);
309
            if (!Member::currentUser()) {
310
                $response->setBody(_t('ContentController.NOTLOGGEDIN', 'Not logged in'));
311
                $response->setStatusDescription(_t('ContentController.NOTLOGGEDIN', 'Not logged in'));
312
                // Tell the CMS to allow re-aunthentication
313
                if (CMSSecurity::enabled()) {
314
                    $response->addHeader('X-Reauthenticate', '1');
315
                }
316
            }
317
            return $response;
318
        }
319
320
        // Prepare the messageSet provided
321
        if (!$messageSet) {
322
            if ($configMessageSet = static::config()->get('default_message_set')) {
323
                $messageSet = $configMessageSet;
324
            } else {
325
                $messageSet = array(
326
                    'default' => _t(
327
                        'Security.NOTEPAGESECURED',
328
                        "That page is secured. Enter your credentials below and we will send "
329
                            . "you right along."
330
                    ),
331
                    'alreadyLoggedIn' => _t(
332
                        'Security.ALREADYLOGGEDIN',
333
                        "You don't have access to this page.  If you have another account that "
334
                            . "can access that page, you can log in again below.",
335
                        "%s will be replaced with a link to log in."
336
                    )
337
                );
338
            }
339
        }
340
341
        if (!is_array($messageSet)) {
342
            $messageSet = array('default' => $messageSet);
343
        }
344
345
        $member = Member::currentUser();
346
347
        // Work out the right message to show
348
        if ($member && $member->exists()) {
349
            $response = ($controller) ? $controller->getResponse() : new HTTPResponse();
350
            $response->setStatusCode(403);
351
352
            //If 'alreadyLoggedIn' is not specified in the array, then use the default
353
            //which should have been specified in the lines above
354
            if (isset($messageSet['alreadyLoggedIn'])) {
355
                $message = $messageSet['alreadyLoggedIn'];
0 ignored issues
show
Unused Code introduced by
$message is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
356
            } else {
357
                $message = $messageSet['default'];
0 ignored issues
show
Unused Code introduced by
$message is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
358
            }
359
360
            // Somewhat hackish way to render a login form with an error message.
361
//            $me = new Security();
362
//            $form = $me->LoginForm();
363
//            $form->sessionMessage($message, ValidationResult::TYPE_WARNING);
364
//            Session::set('MemberLoginForm.force_message', 1);
365
            $loginResponse = $me->login();
0 ignored issues
show
Bug introduced by
The variable $me does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
366
            if ($loginResponse instanceof HTTPResponse) {
367
                return $loginResponse;
368
            }
369
370
            $response->setBody((string)$loginResponse);
371
372
            $controller->extend('permissionDenied', $member);
373
374
            return $response;
375
        } else {
376
            $message = $messageSet['default'];
377
        }
378
379
        static::setLoginMessage($message, ValidationResult::TYPE_WARNING);
380
381
        Session::set("BackURL", $_SERVER['REQUEST_URI']);
382
383
        // TODO AccessLogEntry needs an extension to handle permission denied errors
384
        // Audit logging hook
385
        $controller->extend('permissionDenied', $member);
386
387
        return $controller->redirect(Controller::join_links(
388
            Security::config()->uninherited('login_url'),
389
            "?BackURL=" . urlencode($_SERVER['REQUEST_URI'])
390
        ));
391
    }
392
393
    protected function init()
394
    {
395
        parent::init();
396
397
        // Prevent clickjacking, see https://developer.mozilla.org/en-US/docs/HTTP/X-Frame-Options
398
        $frameOptions = $this->config()->get('frame_options');
399
        if ($frameOptions) {
400
            $this->getResponse()->addHeader('X-Frame-Options', $frameOptions);
401
        }
402
403
        // Prevent search engines from indexing the login page
404
        $robotsTag = $this->config()->get('robots_tag');
405
        if ($robotsTag) {
406
            $this->getResponse()->addHeader('X-Robots-Tag', $robotsTag);
407
        }
408
    }
409
410
    public function index()
411
    {
412
        return $this->httpError(404); // no-op
413
    }
414
415
    /**
416
     * Get the selected authenticator for this request
417
     *
418
     * @return string Class name of Authenticator
419
     * @throws LogicException
420
     */
421
    protected function getAuthenticator()
422
    {
423
        $authenticator = $this->getRequest()->requestVar('AuthenticationMethod');
424
        if ($authenticator && self::hasAuthenticator($authenticator)) {
425
            return Injector::inst()->get($authenticator);
426
427
        } elseif ($authenticator !== '') {
428
            $authenticators = self::getAuthenticators();
429
            if (count($authenticators) > 0) {
430
                return $authenticators[0];
431
            }
432
        }
433
434
        throw new LogicException('No valid authenticator found');
435
    }
436
437
    /**
438
     * Get the login form to process according to the submitted data
439
     *
440
     * @return Form
441
     * @throws Exception
442
     */
443
    public function LoginForm()
444
    {
445
        $authenticator = $this->getAuthenticator();
446
        if ($authenticator) {
447
            $handler = $authenticator->getLoginHandler($this->Link());
0 ignored issues
show
Bug introduced by
The method getLoginHandler cannot be called on $authenticator (of type string).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
448
            return $handler->handleRequest($this->request, DataModel::inst());
449
        }
450
        throw new Exception('Passed invalid authentication method');
451
    }
452
453
    /**
454
     * Get the login forms for all available authentication methods
455
     *
456
     * @return array Returns an array of available login forms (array of Form
457
     *               objects).
458
     *
459
     * @todo Check how to activate/deactivate authentication methods
460
     */
461
    public function getLoginForms()
462
    {
463
        return array_map(
464
            function ($authenticator) {
465
                return $authenticator->getLoginHandler($this->Link())->handleRequest($this->getRequest(), DataModel::inst());
466
            },
467
            Security::getAuthenticators()
468
        );
469
    }
470
471
472
    /**
473
     * Get a link to a security action
474
     *
475
     * @param string $action Name of the action
476
     * @return string Returns the link to the given action
477
     */
478
    public function Link($action = null)
479
    {
480
        /** @skipUpgrade */
481
        return Controller::join_links(Director::baseURL(), "Security", $action);
482
    }
483
484
    /**
485
     * This action is available as a keep alive, so user
486
     * sessions don't timeout. A common use is in the admin.
487
     */
488
    public function ping()
489
    {
490
        return 1;
491
    }
492
493
    /**
494
     * Log the currently logged in user out
495
     *
496
     * @param bool $redirect Redirect the user back to where they came.
497
     *                       - If it's false, the code calling logout() is
498
     *                         responsible for sending the user where-ever
499
     *                         they should go.
500
     * @return HTTPResponse|null
501
     */
502
    public function logout($redirect = true)
503
    {
504
        $member = Member::currentUser();
505
        if ($member) {
506
            $member->logOut();
507
        }
508
509
        if ($redirect && (!$this->getResponse()->isFinished())) {
510
            return $this->redirectBack();
511
        }
512
        return null;
513
    }
514
515
    /**
516
     * Perform pre-login checking and prepare a response if available prior to login
517
     *
518
     * @return HTTPResponse Substitute response object if the login process should be curcumvented.
519
     * Returns null if should proceed as normal.
520
     */
521
    protected function preLogin()
522
    {
523
        // Event handler for pre-login, with an option to let it break you out of the login form
524
        $eventResults = $this->extend('onBeforeSecurityLogin');
525
        // If there was a redirection, return
526
        if ($this->redirectedTo()) {
527
            return $this->getResponse();
528
        }
529
        // If there was an HTTPResponse object returned, then return that
530
        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...
531
            foreach ($eventResults as $result) {
532
                if ($result instanceof HTTPResponse) {
533
                    return $result;
534
                }
535
            }
536
        }
537
538
        // If arriving on the login page already logged in, with no security error, and a ReturnURL then redirect
539
        // back. The login message check is neccesary to prevent infinite loops where BackURL links to
540
        // an action that triggers Security::permissionFailure.
541
        // This step is necessary in cases such as automatic redirection where a user is authenticated
542
        // upon landing on an SSL secured site and is automatically logged in, or some other case
543
        // where the user has permissions to continue but is not given the option.
544
        if ($this->getRequest()->requestVar('BackURL')
545
            && !$this->getLoginMessage()
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->getLoginMessage() of type null|string 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...
546
            && ($member = Member::currentUser())
547
            && $member->exists()
548
        ) {
549
            return $this->redirectBack();
550
        }
551
552
        return null;
553
    }
554
555
    /**
556
     * Prepare the controller for handling the response to this request
557
     *
558
     * @param string $title Title to use
559
     * @return Controller
560
     */
561
    protected function getResponseController($title)
562
    {
563
        // Use the default setting for which Page to use to render the security page
564
        $pageClass = $this->stat('page_class');
565
        if (!$pageClass || !class_exists($pageClass)) {
566
            return $this;
567
        }
568
569
        // Create new instance of page holder
570
        /** @var Page $holderPage */
571
        $holderPage = new $pageClass;
572
        $holderPage->Title = $title;
573
        /** @skipUpgrade */
574
        $holderPage->URLSegment = 'Security';
575
        // Disable ID-based caching  of the log-in page by making it a random number
576
        $holderPage->ID = -1 * rand(1, 10000000);
577
578
        $controllerClass = $holderPage->getControllerName();
579
        /** @var ContentController $controller */
580
        $controller = $controllerClass::create($holderPage);
581
        $controller->setDataModel($this->model);
582
        $controller->doInit();
583
        return $controller;
584
    }
585
586
    /**
587
     * Combine the given forms into a formset with a tabbed interface
588
     *
589
     * @param array $authenticators List of Authenticator instances
0 ignored issues
show
Bug introduced by
There is no parameter named $authenticators. 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...
590
     * @return string
591
     */
592
    protected function generateLoginFormSet($forms)
593
    {
594
        $viewData = new ArrayData(array(
595
            'Forms' => new ArrayList($forms),
596
        ));
597
        return $viewData->renderWith(
598
            $this->getTemplatesFor('MultiAuthenticatorLogin')
599
        );
600
    }
601
602
    /**
603
     * Get the HTML Content for the $Content area during login
604
     *
605
     * @param string &$messageType Type of message, if available, passed back to caller
606
     * @return string Message in HTML format
607
     */
608
    protected function getLoginMessage(&$messageType = null)
609
    {
610
        $message = Session::get('Security.Message.message');
611
        $messageType = null;
612
        if (empty($message)) {
613
            return null;
614
        }
615
616
        $messageType = Session::get('Security.Message.type');
617
        $messageCast = Session::get('Security.Message.cast');
618
        if ($messageCast !== ValidationResult::CAST_HTML) {
619
            $message = Convert::raw2xml($message);
620
        }
621
        return sprintf('<p class="message %s">%s</p>', Convert::raw2att($messageType), $message);
622
    }
623
624
    /**
625
     * Set the next message to display for the security login page. Defaults to warning
626
     *
627
     * @param string $message Message
628
     * @param string $messageType Message type. One of ValidationResult::TYPE_*
629
     * @param string $messageCast Message cast. One of ValidationResult::CAST_*
630
     */
631
    public static function setLoginMessage(
632
        $message,
633
        $messageType = ValidationResult::TYPE_WARNING,
634
        $messageCast = ValidationResult::CAST_TEXT
635
    ) {
636
        Session::set("Security.Message.message", $message);
637
        Session::set("Security.Message.type", $messageType);
638
        Session::set("Security.Message.cast", $messageCast);
639
    }
640
641
    /**
642
     * Clear login message
643
     */
644
    public static function clearLoginMessage()
645
    {
646
        Session::clear("Security.Message");
647
    }
648
649
650
    /**
651
     * Show the "login" page
652
     *
653
     * For multiple authenticators, Security_MultiAuthenticatorLogin is used.
654
     * See getTemplatesFor and getIncludeTemplate for how to override template logic
655
     *
656
     * @return string|HTTPResponse Returns the "login" page as HTML code.
657
     */
658
    public function login($request)
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
659
    {
660
        // Check pre-login process
661
        if ($response = $this->preLogin()) {
662
            return $response;
663
        }
664
665
        return $this->delegateToHandlers(
666
            _t('Security.LOGIN', 'Log in'),
667
            $this->getTemplatesFor('login')
668
        );
669
    }
670
671
    /**
672
     * Delegate to another RequestHandler, rendering any fragment arrays into an appropriate.
673
     * controller.
674
     * @param string $title The title of the form
675
     * @param array $templates
676
     * @return array|HTTPResponse|RequestHandler|\SilverStripe\ORM\FieldType\DBHTMLText|string
677
     */
678
    protected function delegateToHandlers($title, array $templates)
679
    {
680
        // Return the customised controller - used to render in a Form
681
        // Post requests are expected to be login posts, so they'll be handled downstairs
682
        if (!$this->getRequest()->isPOST()) {
683
684
            $controller = $this->getResponseController($title);
685
            $authenticators = self::getAuthenticators();
0 ignored issues
show
Unused Code introduced by
$authenticators is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
686
            $result = [];
687
688
            // if the controller calls Director::redirect(), this will break early
689
            if (($response = $controller->getResponse()) && $response->isFinished()) {
690
                return $response;
691
            }
692
693
            // Generate the login forms
694
            $forms = $this->getLoginForms();
695
            if(count($forms) > 1) {
696
                $result['Form'] = $this->generateLoginFormSet($forms);
697
            } else {
698
                $result['Form'] = $forms[0];
699
            }
700
701
            // Handle any form messages from validation, etc.
702
            $messageType = '';
703
            $message = $this->getLoginMessage($messageType);
704
705
            // We've displayed the message in the form output, so reset it for the next run.
706
            static::clearLoginMessage();
707
708
            if ($message) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $message of type null|string is loosely compared to true; 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...
709
                $messageResult = [
710
                    'Content'     => DBField::create_field('HTMLFragment', $message),
711
                    'Message'     => DBField::create_field('HTMLFragment', $message),
712
                    'MessageType' => $messageType
713
                ];
714
                $result = array_merge($result, $messageResult);
715
            }
716
717
            return $controller->customise($result)->renderWith($templates);
718
719
        // Return a complete result
720
        } else {
721
            $authenticator = self::getAuthenticator();
722
            return $authenticator->getLoginHandler($this->Link)->handleRequest($this->getRequest(), DataModel::inst());
0 ignored issues
show
Bug introduced by
The method getLoginHandler cannot be called on $authenticator (of type string).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
723
        }
724
    }
725
726
    public function basicauthlogin()
727
    {
728
        $member = BasicAuth::requireLogin("SilverStripe login", 'ADMIN');
729
        $member->logIn();
730
    }
731
732
    /**
733
     * Show the "lost password" page
734
     *
735
     * @return string Returns the "lost password" page as HTML code.
736
     */
737
    public function lostpassword()
738
    {
739
        $handler = $this->getAuthenticator()->getLostPasswordHandler(
0 ignored issues
show
Bug introduced by
The method getLostPasswordHandler cannot be called on $this->getAuthenticator() (of type string).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
740
            Controller::join_links($this->link(), 'lostpassword')
741
        );
742
743
        return $this->delegateToHandlers(
744
            $handler,
745
            _t('Security.LOSTPASSWORDHEADER', 'Lost Password'),
0 ignored issues
show
Documentation introduced by
_t('Security.LOSTPASSWOR...ADER', 'Lost Password') is of type string, but the function expects a array.

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...
746
            $this->getTemplatesFor('lostpassword')
0 ignored issues
show
Unused Code introduced by
The call to Security::delegateToHandlers() has too many arguments starting with $this->getTemplatesFor('lostpassword').

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
747
        );
748
    }
749
750
    /**
751
     * Create a link to the password reset form.
752
     *
753
     * GET parameters used:
754
     * - m: member ID
755
     * - t: plaintext token
756
     *
757
     * @param Member $member Member object associated with this link.
758
     * @param string $autologinToken The auto login token.
759
     * @return string
760
     */
761
    public static function getPasswordResetLink($member, $autologinToken)
762
    {
763
        $autologinToken = urldecode($autologinToken);
764
        $selfControllerClass = __CLASS__;
765
        /** @var static $selfController */
766
        $selfController = new $selfControllerClass();
767
        return $selfController->Link('changepassword') . "?m={$member->ID}&t=$autologinToken";
768
    }
769
770
    /**
771
     * Show the "change password" page.
772
     * This page can either be called directly by logged-in users
773
     * (in which case they need to provide their old password),
774
     * or through a link emailed through {@link lostpassword()}.
775
     * In this case no old password is required, authentication is ensured
776
     * through the Member.AutoLoginHash property.
777
     *
778
     * @see ChangePasswordForm
779
     *
780
     * @return string|HTTPRequest Returns the "change password" page as HTML code, or a redirect response
781
     */
782
    public function changepassword()
783
    {
784
        $controller = $this->getResponseController(_t('Security.CHANGEPASSWORDHEADER', 'Change your password'));
785
786
        // if the controller calls Director::redirect(), this will break early
787
        if (($response = $controller->getResponse()) && $response->isFinished()) {
788
            return $response;
789
        }
790
791
        // Extract the member from the URL.
792
        /** @var Member $member */
793
        $member = null;
794
        if (isset($_REQUEST['m'])) {
795
            $member = Member::get()->filter('ID', (int)$_REQUEST['m'])->first();
796
        }
797
798
        // Check whether we are merely changin password, or resetting.
799
        if (isset($_REQUEST['t']) && $member && $member->validateAutoLoginToken($_REQUEST['t'])) {
800
            // On first valid password reset request redirect to the same URL without hash to avoid referrer leakage.
801
802
            // if there is a current member, they should be logged out
803
            if ($curMember = Member::currentUser()) {
804
                $curMember->logOut();
805
            }
806
807
            // Store the hash for the change password form. Will be unset after reload within the ChangePasswordForm.
808
            Session::set('AutoLoginHash', $member->encryptWithUserSettings($_REQUEST['t']));
809
810
            return $this->redirect($this->Link('changepassword'));
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->redirect($this->Link('changepassword')); of type SilverStripe\Control\HTTPResponse|null adds the type SilverStripe\Control\HTTPResponse to the return on line 810 which is incompatible with the return type documented by SilverStripe\Security\Security::changepassword of type string|SilverStripe\Control\HTTPRequest.
Loading history...
811
        } elseif (Session::get('AutoLoginHash')) {
812
            // Subsequent request after the "first load with hash" (see previous if clause).
813
            $customisedController = $controller->customise(array(
814
                'Content' => DBField::create_field(
815
                    'HTMLFragment',
816
                    '<p>' . _t('Security.ENTERNEWPASSWORD', 'Please enter a new password.') . '</p>'
817
                ),
818
                'Form' => $this->ChangePasswordForm(),
819
            ));
820
        } elseif (Member::currentUser()) {
821
            // Logged in user requested a password change form.
822
            $customisedController = $controller->customise(array(
823
                'Content' => DBField::create_field(
824
                    'HTMLFragment',
825
                    '<p>' . _t('Security.CHANGEPASSWORDBELOW', 'You can change your password below.') . '</p>'
826
                ),
827
                'Form' => $this->ChangePasswordForm()));
828
        } else {
829
            // Show friendly message if it seems like the user arrived here via password reset feature.
830
            if (isset($_REQUEST['m']) || isset($_REQUEST['t'])) {
831
                $customisedController = $controller->customise(
832
                    array('Content' => DBField::create_field(
833
                        'HTMLFragment',
834
                        _t(
835
                            'Security.NOTERESETLINKINVALID',
836
                            '<p>The password reset link is invalid or expired.</p>'
837
                            . '<p>You can request a new one <a href="{link1}">here</a> or change your password after'
838
                            . ' you <a href="{link2}">logged in</a>.</p>',
839
                            [
840
                                'link1' => $this->Link('lostpassword'),
841
                                'link2' => $this->Link('login')
842
                            ]
843
                        )
844
                    ))
845
                );
846
            } else {
847
                return self::permissionFailure(
0 ignored issues
show
Bug Compatibility introduced by
The expression self::permissionFailure(...ange your password!')); of type SilverStripe\Control\HTTPResponse|null adds the type SilverStripe\Control\HTTPResponse to the return on line 847 which is incompatible with the return type documented by SilverStripe\Security\Security::changepassword of type string|SilverStripe\Control\HTTPRequest.
Loading history...
848
                    $this,
849
                    _t('Security.ERRORPASSWORDPERMISSION', 'You must be logged in in order to change your password!')
850
                );
851
            }
852
        }
853
854
        return $customisedController->renderWith($this->getTemplatesFor('changepassword'));
855
    }
856
857
    /**
858
     * Factory method for the lost password form
859
     *
860
     * @skipUpgrade
861
     * @return ChangePasswordForm Returns the lost password form
862
     */
863
    public function ChangePasswordForm()
864
    {
865
        return MemberAuthenticator\ChangePasswordForm::create($this, 'ChangePasswordForm');
866
    }
867
868
    /**
869
     * Determine the list of templates to use for rendering the given action.
870
     *
871
     * @skipUpgrade
872
     * @param string $action
873
     * @return array Template list
874
     */
875
    public function getTemplatesFor($action)
876
    {
877
        $templates = SSViewer::get_templates_by_class(get_class($this), "_{$action}", __CLASS__);
878
        return array_merge(
879
            $templates,
880
            [
881
                "Security_{$action}",
882
                "Security",
883
                $this->stat("template_main"),
884
                "BlankPage"
885
            ]
886
        );
887
    }
888
889
    /**
890
     * Return an existing member with administrator privileges, or create one of necessary.
891
     *
892
     * Will create a default 'Administrators' group if no group is found
893
     * with an ADMIN permission. Will create a new 'Admin' member with administrative permissions
894
     * if no existing Member with these permissions is found.
895
     *
896
     * Important: Any newly created administrator accounts will NOT have valid
897
     * login credentials (Email/Password properties), which means they can't be used for login
898
     * purposes outside of any default credentials set through {@link Security::setDefaultAdmin()}.
899
     *
900
     * @return Member
901
     */
902
    public static function findAnAdministrator()
903
    {
904
        // coupling to subsites module
905
        $origSubsite = null;
906
        if (is_callable('Subsite::changeSubsite')) {
907
            $origSubsite = Subsite::currentSubsiteID();
908
            Subsite::changeSubsite(0);
909
        }
910
911
        /** @var Member $member */
912
        $member = null;
913
914
        // find a group with ADMIN permission
915
        $adminGroup = Permission::get_groups_by_permission('ADMIN')->first();
916
917
        if (is_callable('Subsite::changeSubsite')) {
918
            Subsite::changeSubsite($origSubsite);
919
        }
920
921
        if ($adminGroup) {
922
            $member = $adminGroup->Members()->First();
923
        }
924
925
        if (!$adminGroup) {
926
            Group::singleton()->requireDefaultRecords();
927
            $adminGroup = Permission::get_groups_by_permission('ADMIN')->first();
928
        }
929
930
        if (!$member) {
931
            Member::singleton()->requireDefaultRecords();
932
            $member = Permission::get_members_by_permission('ADMIN')->first();
933
        }
934
935
        if (!$member) {
936
            $member = Member::default_admin();
937
        }
938
939
        if (!$member) {
940
            // Failover to a blank admin
941
            $member = Member::create();
942
            $member->FirstName = _t('Member.DefaultAdminFirstname', 'Default Admin');
943
            $member->write();
944
            // Add member to group instead of adding group to member
945
            // This bypasses the privilege escallation code in Member_GroupSet
946
            $adminGroup
947
                ->DirectMembers()
948
                ->add($member);
949
        }
950
951
        return $member;
952
    }
953
954
    /**
955
     * Flush the default admin credentials
956
     */
957
    public static function clear_default_admin()
958
    {
959
        self::$default_username = null;
960
        self::$default_password = null;
961
    }
962
963
964
    /**
965
     * Set a default admin in dev-mode
966
     *
967
     * This will set a static default-admin which is not existing
968
     * as a database-record. By this workaround we can test pages in dev-mode
969
     * with a unified login. Submitted login-credentials are first checked
970
     * against this static information in {@link Security::authenticate()}.
971
     *
972
     * @param string $username The user name
973
     * @param string $password The password (in cleartext)
974
     * @return bool True if successfully set
975
     */
976
    public static function setDefaultAdmin($username, $password)
977
    {
978
        // don't overwrite if already set
979
        if (self::$default_username || self::$default_password) {
980
            return false;
981
        }
982
983
        self::$default_username = $username;
984
        self::$default_password = $password;
985
        return true;
986
    }
987
988
    /**
989
     * Checks if the passed credentials are matching the default-admin.
990
     * Compares cleartext-password set through Security::setDefaultAdmin().
991
     *
992
     * @param string $username
993
     * @param string $password
994
     * @return bool
995
     */
996
    public static function check_default_admin($username, $password)
997
    {
998
        return (
999
            self::$default_username === $username
1000
            && self::$default_password === $password
1001
            && self::has_default_admin()
1002
        );
1003
    }
1004
1005
    /**
1006
     * Check that the default admin account has been set.
1007
     */
1008
    public static function has_default_admin()
1009
    {
1010
        return !empty(self::$default_username) && !empty(self::$default_password);
1011
    }
1012
1013
    /**
1014
     * Get default admin username
1015
     *
1016
     * @return string
1017
     */
1018
    public static function default_admin_username()
1019
    {
1020
        return self::$default_username;
1021
    }
1022
1023
    /**
1024
     * Get default admin password
1025
     *
1026
     * @return string
1027
     */
1028
    public static function default_admin_password()
1029
    {
1030
        return self::$default_password;
1031
    }
1032
1033
    /**
1034
     * Encrypt a password according to the current password encryption settings.
1035
     * If the settings are so that passwords shouldn't be encrypted, the
1036
     * result is simple the clear text password with an empty salt except when
1037
     * a custom algorithm ($algorithm parameter) was passed.
1038
     *
1039
     * @param string $password The password to encrypt
1040
     * @param string $salt Optional: The salt to use. If it is not passed, but
1041
     *  needed, the method will automatically create a
1042
     *  random salt that will then be returned as return value.
1043
     * @param string $algorithm Optional: Use another algorithm to encrypt the
1044
     *  password (so that the encryption algorithm can be changed over the time).
1045
     * @param Member $member Optional
1046
     * @return mixed Returns an associative array containing the encrypted
1047
     *  password and the used salt in the form:
1048
     * <code>
1049
     *  array(
1050
     *  'password' => string,
1051
     *  'salt' => string,
1052
     *  'algorithm' => string,
1053
     *  'encryptor' => PasswordEncryptor instance
1054
     *  )
1055
     * </code>
1056
     * If the passed algorithm is invalid, FALSE will be returned.
1057
     *
1058
     * @see encrypt_passwords()
1059
     */
1060
    public static function encrypt_password($password, $salt = null, $algorithm = null, $member = null)
1061
    {
1062
        // Fall back to the default encryption algorithm
1063
        if (!$algorithm) {
0 ignored issues
show
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...
1064
            $algorithm = self::config()->get('password_encryption_algorithm');
1065
        }
1066
1067
        $e = PasswordEncryptor::create_for_algorithm($algorithm);
1068
1069
        // New salts will only need to be generated if the password is hashed for the first time
1070
        $salt = ($salt) ? $salt : $e->salt($password);
1071
1072
        return array(
1073
            'password' => $e->encrypt($password, $salt, $member),
1074
            'salt' => $salt,
1075
            'algorithm' => $algorithm,
1076
            'encryptor' => $e
1077
        );
1078
    }
1079
1080
    /**
1081
     * Checks the database is in a state to perform security checks.
1082
     * See {@link DatabaseAdmin->init()} for more information.
1083
     *
1084
     * @return bool
1085
     */
1086
    public static function database_is_ready()
1087
    {
1088
        // Used for unit tests
1089
        if (self::$force_database_is_ready !== null) {
1090
            return self::$force_database_is_ready;
1091
        }
1092
1093
        if (self::$database_is_ready) {
1094
            return self::$database_is_ready;
1095
        }
1096
1097
        $requiredClasses = ClassInfo::dataClassesFor(Member::class);
1098
        $requiredClasses[] = Group::class;
1099
        $requiredClasses[] = Permission::class;
1100
        $schema = DataObject::getSchema();
1101
        foreach ($requiredClasses as $class) {
1102
            // Skip test classes, as not all test classes are scaffolded at once
1103
            if (is_a($class, TestOnly::class, true)) {
1104
                continue;
1105
            }
1106
1107
            // if any of the tables aren't created in the database
1108
            $table = $schema->tableName($class);
1109
            if (!ClassInfo::hasTable($table)) {
1110
                return false;
1111
            }
1112
1113
            // HACK: DataExtensions aren't applied until a class is instantiated for
1114
            // the first time, so create an instance here.
1115
            singleton($class);
1116
1117
            // if any of the tables don't have all fields mapped as table columns
1118
            $dbFields = DB::field_list($table);
1119
            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...
1120
                return false;
1121
            }
1122
1123
            $objFields = $schema->databaseFields($class, false);
1124
            $missingFields = array_diff_key($objFields, $dbFields);
1125
1126
            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...
1127
                return false;
1128
            }
1129
        }
1130
        self::$database_is_ready = true;
1131
1132
        return true;
1133
    }
1134
1135
    /**
1136
     * Enable or disable recording of login attempts
1137
     * through the {@link LoginRecord} object.
1138
     *
1139
     * @deprecated 4.0 Use the "Security.login_recording" config setting instead
1140
     * @param boolean $bool
1141
     */
1142
    public static function set_login_recording($bool)
1143
    {
1144
        Deprecation::notice('4.0', 'Use the "Security.login_recording" config setting instead');
1145
        self::$login_recording = (bool)$bool;
1146
    }
1147
1148
    /**
1149
     * @deprecated 4.0 Use the "Security.login_recording" config setting instead
1150
     * @return boolean
1151
     */
1152
    public static function login_recording()
1153
    {
1154
        Deprecation::notice('4.0', 'Use the "Security.login_recording" config setting instead');
1155
        return self::$login_recording;
1156
    }
1157
1158
    /**
1159
     * @config
1160
     * @var string Set the default login dest
1161
     * This is the URL that users will be redirected to after they log in,
1162
     * if they haven't logged in en route to access a secured page.
1163
     * By default, this is set to the homepage.
1164
     */
1165
    private static $default_login_dest = "";
1166
1167
    protected static $ignore_disallowed_actions = false;
1168
1169
    /**
1170
     * Set to true to ignore access to disallowed actions, rather than returning permission failure
1171
     * Note that this is just a flag that other code needs to check with Security::ignore_disallowed_actions()
1172
     * @param $flag True or false
1173
     */
1174
    public static function set_ignore_disallowed_actions($flag)
1175
    {
1176
        self::$ignore_disallowed_actions = $flag;
1177
    }
1178
1179
    public static function ignore_disallowed_actions()
1180
    {
1181
        return self::$ignore_disallowed_actions;
1182
    }
1183
1184
    /**
1185
     * Get the URL of the log-in page.
1186
     *
1187
     * To update the login url use the "Security.login_url" config setting.
1188
     *
1189
     * @return string
1190
     */
1191
    public static function login_url()
1192
    {
1193
        return Controller::join_links(Director::baseURL(), self::config()->get('login_url'));
1194
    }
1195
1196
1197
    /**
1198
     * Get the URL of the logout page.
1199
     *
1200
     * To update the logout url use the "Security.logout_url" config setting.
1201
     *
1202
     * @return string
1203
     */
1204
    public static function logout_url()
1205
    {
1206
        return Controller::join_links(Director::baseURL(), self::config()->get('logout_url'));
1207
    }
1208
1209
    /**
1210
     * Get the URL of the logout page.
1211
     *
1212
     * To update the logout url use the "Security.logout_url" config setting.
1213
     *
1214
     * @return string
1215
     */
1216
    public static function lost_password_url()
1217
    {
1218
        return Controller::join_links(Director::baseURL(), self::config()->get('lost_password_url'));
1219
    }
1220
1221
    /**
1222
     * Defines global accessible templates variables.
1223
     *
1224
     * @return array
1225
     */
1226
    public static function get_template_global_variables()
1227
    {
1228
        return array(
1229
            "LoginURL" => "login_url",
1230
            "LogoutURL" => "logout_url",
1231
            "LostPasswordURL" => "lost_password_url",
1232
        );
1233
    }
1234
}
1235