Completed
Push — authenticator-refactor ( e3c944...b14200 )
by Sam
12:06
created

Security::delegateToFormSet()   B

Complexity

Conditions 5
Paths 6

Size

Total Lines 36
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 17
nc 6
nop 3
dl 0
loc 36
rs 8.439
c 0
b 0
f 0
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\HTTPResponse_Exception;
13
use SilverStripe\Control\Session;
14
use SilverStripe\Control\RequestHandler;
15
use SilverStripe\Core\ClassInfo;
16
use SilverStripe\Core\Config\Config;
17
use SilverStripe\Core\Convert;
18
use SilverStripe\Dev\Deprecation;
19
use SilverStripe\Dev\TestOnly;
20
use SilverStripe\Forms\EmailField;
21
use SilverStripe\Forms\FieldList;
22
use SilverStripe\Forms\Form;
23
use SilverStripe\Forms\FormAction;
24
use SilverStripe\ORM\ArrayList;
25
use SilverStripe\ORM\DataModel;
26
use SilverStripe\ORM\DB;
27
use SilverStripe\ORM\DataObject;
28
use SilverStripe\ORM\FieldType\DBField;
29
use SilverStripe\ORM\ValidationResult;
30
use SilverStripe\View\ArrayData;
31
use SilverStripe\View\SSViewer;
32
use SilverStripe\View\TemplateGlobalProvider;
33
use Exception;
34
use SilverStripe\View\ViewableData_Customised;
35
use Subsite;
36
use SilverStripe\Core\Injector\Injector;
37
38
/**
39
 * Implements a basic security model
40
 */
41
class Security extends Controller implements TemplateGlobalProvider
42
{
43
44
    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...
45
        'index',
46
        'login',
47
        'logout',
48
        'basicauthlogin',
49
        'lostpassword',
50
        'passwordsent',
51
        'changepassword',
52
        'ping',
53
        'LoginForm',
54
        'ChangePasswordForm',
55
        'LostPasswordForm',
56
    );
57
58
    /**
59
     * Default user name. Only used in dev-mode by {@link setDefaultAdmin()}
60
     *
61
     * @var string
62
     * @see setDefaultAdmin()
63
     */
64
    protected static $default_username;
65
66
    /**
67
     * Default password. Only used in dev-mode by {@link setDefaultAdmin()}
68
     *
69
     * @var string
70
     * @see setDefaultAdmin()
71
     */
72
    protected static $default_password;
73
74
    /**
75
     * If set to TRUE to prevent sharing of the session across several sites
76
     * in the domain.
77
     *
78
     * @config
79
     * @var bool
80
     */
81
    protected static $strict_path_checking = false;
82
83
    /**
84
     * The password encryption algorithm to use by default.
85
     * This is an arbitrary code registered through {@link PasswordEncryptor}.
86
     *
87
     * @config
88
     * @var string
89
     */
90
    private static $password_encryption_algorithm = 'blowfish';
91
92
    /**
93
     * Showing "Remember me"-checkbox
94
     * on loginform, and saving encrypted credentials to a cookie.
95
     *
96
     * @config
97
     * @var bool
98
     */
99
    private static $autologin_enabled = true;
100
101
    /**
102
     * Determine if login username may be remembered between login sessions
103
     * If set to false this will disable autocomplete and prevent username persisting in the session
104
     *
105
     * @config
106
     * @var bool
107
     */
108
    private static $remember_username = true;
109
110
    /**
111
     * Location of word list to use for generating passwords
112
     *
113
     * @config
114
     * @var string
115
     */
116
    private static $word_list = './wordlist.txt';
117
118
    /**
119
     * @config
120
     * @var string
121
     */
122
    private static $template = 'BlankPage';
123
124
    /**
125
     * Template thats used to render the pages.
126
     *
127
     * @var string
128
     * @config
129
     */
130
    private static $template_main = 'Page';
131
132
    /**
133
     * Class to use for page rendering
134
     *
135
     * @var string
136
     * @config
137
     */
138
    private static $page_class = Page::class;
139
140
    /**
141
     * Default message set used in permission failures.
142
     *
143
     * @config
144
     * @var array|string
145
     */
146
    private static $default_message_set;
147
148
    /**
149
     * Random secure token, can be used as a crypto key internally.
150
     * Generate one through 'sake dev/generatesecuretoken'.
151
     *
152
     * @config
153
     * @var String
154
     */
155
    private static $token;
156
157
    /**
158
     * The default login URL
159
     *
160
     * @config
161
     *
162
     * @var string
163
     */
164
    private static $login_url = "Security/login";
165
166
    /**
167
     * The default logout URL
168
     *
169
     * @config
170
     *
171
     * @var string
172
     */
173
    private static $logout_url = "Security/logout";
174
175
    /**
176
     * The default lost password URL
177
     *
178
     * @config
179
     *
180
     * @var string
181
     */
182
    private static $lost_password_url = "Security/lostpassword";
183
184
    /**
185
     * Value of X-Frame-Options header
186
     *
187
     * @config
188
     * @var string
189
     */
190
    private static $frame_options = 'SAMEORIGIN';
191
192
    /**
193
     * Value of the X-Robots-Tag header (for the Security section)
194
     *
195
     * @config
196
     * @var string
197
     */
198
    private static $robots_tag = 'noindex, nofollow';
199
200
    /**
201
     * Enable or disable recording of login attempts
202
     * through the {@link LoginRecord} object.
203
     *
204
     * @config
205
     * @var boolean $login_recording
206
     */
207
    private static $login_recording = false;
208
209
    /**
210
     * @var boolean If set to TRUE or FALSE, {@link database_is_ready()}
211
     * will always return FALSE. Used for unit testing.
212
     */
213
    protected static $force_database_is_ready = null;
214
215
    /**
216
     * When the database has once been verified as ready, it will not do the
217
     * checks again.
218
     *
219
     * @var bool
220
     */
221
    protected static $database_is_ready = false;
222
223
    protected static $authenticators = [];
224
225
    protected static $default_authenticator = MemberAuthenticator\Authenticator::class;
226
227
    /**
228
     * Get all registered authenticators
229
     *
230
     * @return array Return an array of Authenticator objects
231
     */
232
    public static function getAuthenticators()
233
    {
234
        $authenticators = 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...
235
236
        return array_map(function ($class) {
237
            return Injector::inst()->get($class);
238
        }, $authenticators);
239
    }
240
241
    /**
242
     * Check if a given authenticator is registered
243
     *
244
     * @param string $authenticator The configured identifier of the authenicator
245
     * @return bool Returns TRUE if the authenticator is registered, FALSE
246
     *              otherwise.
247
     */
248
    public static function hasAuthenticator($authenticator)
249
    {
250
        $authenticators = self::config()->get('authenticators');
251
        return !empty($authenticators[$authenticator]);
252
    }
253
254
    /**
255
     * Register that we've had a permission failure trying to view the given page
256
     *
257
     * This will redirect to a login page.
258
     * If you don't provide a messageSet, a default will be used.
259
     *
260
     * @param Controller $controller The controller that you were on to cause the permission
261
     *                               failure.
262
     * @param string|array $messageSet The message to show to the user. This
263
     *                                 can be a string, or a map of different
264
     *                                 messages for different contexts.
265
     *                                 If you pass an array, you can use the
266
     *                                 following keys:
267
     *                                   - default: The default message
268
     *                                   - alreadyLoggedIn: The message to
269
     *                                                      show if the user
270
     *                                                      is already logged
271
     *                                                      in and lacks the
272
     *                                                      permission to
273
     *                                                      access the item.
274
     *
275
     * The alreadyLoggedIn value can contain a '%s' placeholder that will be replaced with a link
276
     * to log in.
277
     * @return HTTPResponse
278
     */
279
    public static function permissionFailure($controller = null, $messageSet = null)
280
    {
281
        self::set_ignore_disallowed_actions(true);
282
283
        if (!$controller) {
284
            $controller = Controller::curr();
285
        }
286
287
        if (Director::is_ajax()) {
288
            $response = ($controller) ? $controller->getResponse() : new HTTPResponse();
289
            $response->setStatusCode(403);
290
            if (!Member::currentUser()) {
291
                $response->setBody(_t('ContentController.NOTLOGGEDIN', 'Not logged in'));
292
                $response->setStatusDescription(_t('ContentController.NOTLOGGEDIN', 'Not logged in'));
293
                // Tell the CMS to allow re-aunthentication
294
                if (CMSSecurity::enabled()) {
295
                    $response->addHeader('X-Reauthenticate', '1');
296
                }
297
            }
298
            return $response;
299
        }
300
301
        // Prepare the messageSet provided
302
        if (!$messageSet) {
303
            if ($configMessageSet = static::config()->get('default_message_set')) {
304
                $messageSet = $configMessageSet;
305
            } else {
306
                $messageSet = array(
307
                    'default' => _t(
308
                        'Security.NOTEPAGESECURED',
309
                        "That page is secured. Enter your credentials below and we will send "
310
                            . "you right along."
311
                    ),
312
                    'alreadyLoggedIn' => _t(
313
                        'Security.ALREADYLOGGEDIN',
314
                        "You don't have access to this page.  If you have another account that "
315
                            . "can access that page, you can log in again below.",
316
                        "%s will be replaced with a link to log in."
317
                    )
318
                );
319
            }
320
        }
321
322
        if (!is_array($messageSet)) {
323
            $messageSet = array('default' => $messageSet);
324
        }
325
326
        $member = Member::currentUser();
327
328
        // Work out the right message to show
329
        if ($member && $member->exists()) {
330
            $response = ($controller) ? $controller->getResponse() : new HTTPResponse();
331
            $response->setStatusCode(403);
332
333
            //If 'alreadyLoggedIn' is not specified in the array, then use the default
334
            //which should have been specified in the lines above
335
            if (isset($messageSet['alreadyLoggedIn'])) {
336
                $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...
337
            } else {
338
                $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...
339
            }
340
341
            // Somewhat hackish way to render a login form with an error message.
342
//            $me = new Security();
343
//            $form = $me->LoginForm();
344
//            $form->sessionMessage($message, ValidationResult::TYPE_WARNING);
345
//            Session::set('MemberLoginForm.force_message', 1);
346
            $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...
347
            if ($loginResponse instanceof HTTPResponse) {
348
                return $loginResponse;
349
            }
350
351
            $response->setBody((string)$loginResponse);
352
353
            $controller->extend('permissionDenied', $member);
354
355
            return $response;
356
        } else {
357
            $message = $messageSet['default'];
358
        }
359
360
        static::setLoginMessage($message, ValidationResult::TYPE_WARNING);
361
362
        Session::set("BackURL", $_SERVER['REQUEST_URI']);
363
364
        // TODO AccessLogEntry needs an extension to handle permission denied errors
365
        // Audit logging hook
366
        $controller->extend('permissionDenied', $member);
367
368
        return $controller->redirect(Controller::join_links(
369
            Security::config()->uninherited('login_url'),
370
            "?BackURL=" . urlencode($_SERVER['REQUEST_URI'])
371
        ));
372
    }
373
374
    protected function init()
375
    {
376
        parent::init();
377
378
        // Prevent clickjacking, see https://developer.mozilla.org/en-US/docs/HTTP/X-Frame-Options
379
        $frameOptions = $this->config()->get('frame_options');
380
        if ($frameOptions) {
381
            $this->getResponse()->addHeader('X-Frame-Options', $frameOptions);
382
        }
383
384
        // Prevent search engines from indexing the login page
385
        $robotsTag = $this->config()->get('robots_tag');
386
        if ($robotsTag) {
387
            $this->getResponse()->addHeader('X-Robots-Tag', $robotsTag);
388
        }
389
    }
390
391
    public function index()
392
    {
393
        return $this->httpError(404); // no-op
394
    }
395
396
    /**
397
     * Get the selected authenticator for this request
398
     *
399
     * @param $name string The identifier of the authenticator in your config
400
     * @return string Class name of Authenticator
401
     * @throws LogicException
402
     */
403
    protected function getAuthenticator($name)
404
    {
405
        $authenticators = 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...
406
407
        if (!$name) $name = 'default';
408
409
        if (isset($authenticators[$name])) {
410
            return Injector::inst()->get($authenticators[$name]);
411
        }
412
413
        throw new LogicException('No valid authenticator found');
414
    }
415
416
    /**
417
     * Get the login form to process according to the submitted data
418
     *
419
     * @return Form
420
     * @throws Exception
421
     */
422
    public function LoginForm()
423
    {
424
        $authenticator = $this->getAuthenticator('default');
425
        if ($authenticator) {
426
            $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...
427
            return $handler->handleRequest($this->request, DataModel::inst());
428
        }
429
        throw new Exception('Passed invalid authentication method');
430
    }
431
432
    /**
433
     * Get the login forms for all available authentication methods
434
     *
435
     * @return array Returns an array of available login forms (array of Form
436
     *               objects).
437
     *
438
     * @todo Check how to activate/deactivate authentication methods
439
     */
440
    public function getLoginForms()
441
    {
442
        return array_map(
443
            function ($authenticator) {
444
                return $authenticator->getLoginHandler($this->Link())->handleRequest($this->getRequest(), DataModel::inst());
445
            },
446
            Security::getAuthenticators()
447
        );
448
    }
449
450
451
    /**
452
     * Get a link to a security action
453
     *
454
     * @param string $action Name of the action
455
     * @return string Returns the link to the given action
456
     */
457
    public function Link($action = null)
458
    {
459
        /** @skipUpgrade */
460
        return Controller::join_links(Director::baseURL(), "Security", $action);
461
    }
462
463
    /**
464
     * This action is available as a keep alive, so user
465
     * sessions don't timeout. A common use is in the admin.
466
     */
467
    public function ping()
468
    {
469
        return 1;
470
    }
471
472
    /**
473
     * Log the currently logged in user out
474
     *
475
     * @param bool $redirect Redirect the user back to where they came.
476
     *                       - If it's false, the code calling logout() is
477
     *                         responsible for sending the user where-ever
478
     *                         they should go.
479
     * @return HTTPResponse|null
480
     */
481
    public function logout($redirect = true)
482
    {
483
        $member = Member::currentUser();
484
        if ($member) {
485
            $member->logOut();
486
        }
487
488
        if ($redirect && (!$this->getResponse()->isFinished())) {
489
            return $this->redirectBack();
490
        }
491
        return null;
492
    }
493
494
    /**
495
     * Perform pre-login checking and prepare a response if available prior to login
496
     *
497
     * @return HTTPResponse Substitute response object if the login process should be curcumvented.
498
     * Returns null if should proceed as normal.
499
     */
500
    protected function preLogin()
501
    {
502
        // Event handler for pre-login, with an option to let it break you out of the login form
503
        $eventResults = $this->extend('onBeforeSecurityLogin');
504
        // If there was a redirection, return
505
        if ($this->redirectedTo()) {
506
            return $this->getResponse();
507
        }
508
        // If there was an HTTPResponse object returned, then return that
509
        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...
510
            foreach ($eventResults as $result) {
511
                if ($result instanceof HTTPResponse) {
512
                    return $result;
513
                }
514
            }
515
        }
516
517
        // If arriving on the login page already logged in, with no security error, and a ReturnURL then redirect
518
        // back. The login message check is neccesary to prevent infinite loops where BackURL links to
519
        // an action that triggers Security::permissionFailure.
520
        // This step is necessary in cases such as automatic redirection where a user is authenticated
521
        // upon landing on an SSL secured site and is automatically logged in, or some other case
522
        // where the user has permissions to continue but is not given the option.
523
        if ($this->getRequest()->requestVar('BackURL')
524
            && !$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...
525
            && ($member = Member::currentUser())
526
            && $member->exists()
527
        ) {
528
            return $this->redirectBack();
529
        }
530
531
        return null;
532
    }
533
534
    /**
535
     * Prepare the controller for handling the response to this request
536
     *
537
     * @param string $title Title to use
538
     * @return Controller
539
     */
540
    protected function getResponseController($title)
541
    {
542
        // Use the default setting for which Page to use to render the security page
543
        $pageClass = $this->stat('page_class');
544
        if (!$pageClass || !class_exists($pageClass)) {
545
            return $this;
546
        }
547
548
        // Create new instance of page holder
549
        /** @var Page $holderPage */
550
        $holderPage = new $pageClass;
551
        $holderPage->Title = $title;
552
        /** @skipUpgrade */
553
        $holderPage->URLSegment = 'Security';
554
        // Disable ID-based caching  of the log-in page by making it a random number
555
        $holderPage->ID = -1 * rand(1, 10000000);
556
557
        $controllerClass = $holderPage->getControllerName();
558
        /** @var ContentController $controller */
559
        $controller = $controllerClass::create($holderPage);
560
        $controller->setDataModel($this->model);
561
        $controller->doInit();
562
        return $controller;
563
    }
564
565
    /**
566
     * Combine the given forms into a formset with a tabbed interface
567
     *
568
     * @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...
569
     * @return string
570
     */
571
    protected function generateLoginFormSet($forms)
572
    {
573
        $viewData = new ArrayData(array(
574
            'Forms' => new ArrayList($forms),
575
        ));
576
        return $viewData->renderWith(
577
            $this->getTemplatesFor('MultiAuthenticatorLogin')
578
        );
579
    }
580
581
    /**
582
     * Get the HTML Content for the $Content area during login
583
     *
584
     * @param string &$messageType Type of message, if available, passed back to caller
585
     * @return string Message in HTML format
586
     */
587
    protected function getLoginMessage(&$messageType = null)
588
    {
589
        $message = Session::get('Security.Message.message');
590
        $messageType = null;
591
        if (empty($message)) {
592
            return null;
593
        }
594
595
        $messageType = Session::get('Security.Message.type');
596
        $messageCast = Session::get('Security.Message.cast');
597
        if ($messageCast !== ValidationResult::CAST_HTML) {
598
            $message = Convert::raw2xml($message);
599
        }
600
        return sprintf('<p class="message %s">%s</p>', Convert::raw2att($messageType), $message);
601
    }
602
603
    /**
604
     * Set the next message to display for the security login page. Defaults to warning
605
     *
606
     * @param string $message Message
607
     * @param string $messageType Message type. One of ValidationResult::TYPE_*
608
     * @param string $messageCast Message cast. One of ValidationResult::CAST_*
609
     */
610
    public static function setLoginMessage(
611
        $message,
612
        $messageType = ValidationResult::TYPE_WARNING,
613
        $messageCast = ValidationResult::CAST_TEXT
614
    ) {
615
        Session::set("Security.Message.message", $message);
616
        Session::set("Security.Message.type", $messageType);
617
        Session::set("Security.Message.cast", $messageCast);
618
    }
619
620
    /**
621
     * Clear login message
622
     */
623
    public static function clearLoginMessage()
624
    {
625
        Session::clear("Security.Message");
626
    }
627
628
629
    /**
630
     * Show the "login" page
631
     *
632
     * For multiple authenticators, Security_MultiAuthenticatorLogin is used.
633
     * See getTemplatesFor and getIncludeTemplate for how to override template logic
634
     *
635
     * @return string|HTTPResponse Returns the "login" page as HTML code.
636
     */
637
    public function login($request)
638
    {
639
        // Check pre-login process
640
        if ($response = $this->preLogin()) {
641
            return $response;
642
        }
643
644
        $link = $this->link("login");
645
646
        // Delegate to a single handler - Security/login/<authname>/...
647
        if ($authenticatorName = $request->param('ID')) {
648
            $request->shift();
649
650
            $authenticator = $this->getAuthenticator($authenticatorName);
651
            if (!$authenticator) {
652
                throw new HTTPResponse_Exception(404, 'No authenticator "' . $authenticatorName . '"');
653
            }
654
655
            $handler = $authenticator->getLoginHandler(Controller::join_links($link, $authenticatorName));
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...
656
657
            return $this->delegateToHandler(
658
                $handler,
659
                _t('Security.LOGIN', 'Log in'),
660
                $this->getTemplatesFor('login')
661
            );
662
663
        // Delegate to all of them, building a tabbed view - Security/login
664
        } else {
665
            $handlers = $this->getAuthenticators();
666
            array_walk(
667
                $handlers,
668
                function (&$auth, $name) use ($link) {
669
                    $auth = $auth->getLoginHandler(Controller::join_links($link, $name));
670
                }
671
            );
672
673
            if (count($handlers) === 1) {
674
                return $this->delegateToHandler(
675
                    array_values($handlers)[0],
676
                    _t('Security.LOGIN', 'Log in'),
677
                    $this->getTemplatesFor('login')
678
                );
679
680
            } else {
681
                return $this->delegateToFormSet(
682
                    $handlers,
683
                    _t('Security.LOGIN', 'Log in'),
684
                    $this->getTemplatesFor('login')
685
                );
686
            }
687
        }
688
689
    }
690
691
    /**
692
     * Delegate to an number of handlers, extracting their forms and rendering a tabbed form-set.
693
     * This is used to built the log-in page where there are multiple authenticators active.
694
     *
695
     * @param string $title The title of the form
696
     * @param array $templates
697
     * @return array|HTTPResponse|RequestHandler|\SilverStripe\ORM\FieldType\DBHTMLText|string
698
     */
699
    protected function delegateToFormSet(array $handlers, $title, array $templates)
700
    {
701
702
        // Process each of the handlers
703
        $results = array_map(
704
            function ($handler) {
705
                return $handler->handleRequest($this->getRequest(), \SilverStripe\ORM\DataModel::inst());
706
            },
707
            $handlers
708
        );
709
710
        // Aggregate all their forms, assuming they all return
711
        $forms = [];
712
        foreach ($results as $authName => $singleResult) {
713
            // The result *must* be an array with a Form key
714
            if (!is_array($singleResult) || !isset($singleResult['Form'])) {
715
                user_error('Authenticator "' . $authName . '" doesn\'t support a tabbed login', E_USER_WARNING);
716
                continue;
717
            }
718
719
            $forms[] = $singleResult['Form'];
720
        }
721
722
        if (!$forms) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $forms 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...
723
            throw new \LogicException("No authenticators found compatible with a tabbed login");
724
        }
725
726
        return $this->renderWrappedController(
727
            $title,
728
            [
729
                'Form' => $this->generateLoginFormSet($forms),
730
            ],
731
            $templates
732
        );
733
734
    }
735
736
    /**
737
     * Delegate to another RequestHandler, rendering any fragment arrays into an appropriate.
738
     * controller.
739
     *
740
     * @param string $title The title of the form
741
     * @param array $templates
742
     * @return array|HTTPResponse|RequestHandler|\SilverStripe\ORM\FieldType\DBHTMLText|string
743
     */
744
    protected function delegateToHandler(RequestHandler $handler, $title, array $templates)
745
    {
746
        $result = $handler->handleRequest($this->getRequest(), \SilverStripe\ORM\DataModel::inst());
747
748
        // Return the customised controller - used to render in a Form
749
        // Post requests are expected to be login posts, so they'll be handled downstairs
750
        if (is_array($result)) {
751
            $result = $this->renderWrappedController($title, $result, $templates);
752
        }
753
754
        return $result;
755
    }
756
757
    /**
758
     * Render the given fragments into a security page controller with the given title.
759
     * @param $title string The title to give the security page
760
     * @param $fragments A map of objects to render into the page, e.g. "Form"
761
     * @param $templates An array of templates to use for the render
762
     */
763
    protected function renderWrappedController($title, array $fragments, array $templates)
764
    {
765
        $controller = $this->getResponseController($title);
766
767
        // if the controller calls Director::redirect(), this will break early
768
        if (($response = $controller->getResponse()) && $response->isFinished()) {
769
            return $response;
770
        }
771
772
        // Handle any form messages from validation, etc.
773
        $messageType = '';
774
        $message = $this->getLoginMessage($messageType);
775
776
        // We've displayed the message in the form output, so reset it for the next run.
777
        static::clearLoginMessage();
778
779
        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...
780
            $messageResult = [
781
                'Content'     => DBField::create_field('HTMLFragment', $message),
782
                'Message'     => DBField::create_field('HTMLFragment', $message),
783
                'MessageType' => $messageType
784
            ];
785
            $result = array_merge($fragments, $messageResult);
0 ignored issues
show
Unused Code introduced by
$result 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...
786
        }
787
788
        return $controller->customise($fragments)->renderWith($templates);
789
    }
790
791
    public function basicauthlogin()
792
    {
793
        $member = BasicAuth::requireLogin("SilverStripe login", 'ADMIN');
794
        $member->logIn();
795
    }
796
797
    /**
798
     * Show the "lost password" page
799
     *
800
     * @return string Returns the "lost password" page as HTML code.
801
     */
802
    public function lostpassword()
803
    {
804
        $handler = $this->getAuthenticator('default')->getLostPasswordHandler(
0 ignored issues
show
Bug introduced by
The method getLostPasswordHandler cannot be called on $this->getAuthenticator('default') (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...
805
            Controller::join_links($this->link(), 'lostpassword')
806
        );
807
808
        return $this->delegateToHandler(
809
            $handler,
810
            _t('Security.LOSTPASSWORDHEADER', 'Lost Password'),
811
            $this->getTemplatesFor('lostpassword')
812
        );
813
    }
814
815
    /**
816
     * Create a link to the password reset form.
817
     *
818
     * GET parameters used:
819
     * - m: member ID
820
     * - t: plaintext token
821
     *
822
     * @param Member $member Member object associated with this link.
823
     * @param string $autologinToken The auto login token.
824
     * @return string
825
     */
826
    public static function getPasswordResetLink($member, $autologinToken)
827
    {
828
        $autologinToken = urldecode($autologinToken);
829
        $selfControllerClass = __CLASS__;
830
        /** @var static $selfController */
831
        $selfController = new $selfControllerClass();
832
        return $selfController->Link('changepassword') . "?m={$member->ID}&t=$autologinToken";
833
    }
834
835
    /**
836
     * Show the "change password" page.
837
     * This page can either be called directly by logged-in users
838
     * (in which case they need to provide their old password),
839
     * or through a link emailed through {@link lostpassword()}.
840
     * In this case no old password is required, authentication is ensured
841
     * through the Member.AutoLoginHash property.
842
     *
843
     * @see ChangePasswordForm
844
     *
845
     * @return string|HTTPRequest Returns the "change password" page as HTML code, or a redirect response
846
     */
847
    public function changepassword()
848
    {
849
        $controller = $this->getResponseController(_t('Security.CHANGEPASSWORDHEADER', 'Change your password'));
850
851
        // if the controller calls Director::redirect(), this will break early
852
        if (($response = $controller->getResponse()) && $response->isFinished()) {
853
            return $response;
854
        }
855
856
        // Extract the member from the URL.
857
        /** @var Member $member */
858
        $member = null;
859
        if (isset($_REQUEST['m'])) {
860
            $member = Member::get()->filter('ID', (int)$_REQUEST['m'])->first();
861
        }
862
863
        // Check whether we are merely changin password, or resetting.
864
        if (isset($_REQUEST['t']) && $member && $member->validateAutoLoginToken($_REQUEST['t'])) {
865
            // On first valid password reset request redirect to the same URL without hash to avoid referrer leakage.
866
867
            // if there is a current member, they should be logged out
868
            if ($curMember = Member::currentUser()) {
869
                $curMember->logOut();
870
            }
871
872
            // Store the hash for the change password form. Will be unset after reload within the ChangePasswordForm.
873
            Session::set('AutoLoginHash', $member->encryptWithUserSettings($_REQUEST['t']));
874
875
            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 875 which is incompatible with the return type documented by SilverStripe\Security\Security::changepassword of type string|SilverStripe\Control\HTTPRequest.
Loading history...
876
        } elseif (Session::get('AutoLoginHash')) {
877
            // Subsequent request after the "first load with hash" (see previous if clause).
878
            $customisedController = $controller->customise(array(
879
                'Content' => DBField::create_field(
880
                    'HTMLFragment',
881
                    '<p>' . _t('Security.ENTERNEWPASSWORD', 'Please enter a new password.') . '</p>'
882
                ),
883
                'Form' => $this->ChangePasswordForm(),
884
            ));
885
        } elseif (Member::currentUser()) {
886
            // Logged in user requested a password change form.
887
            $customisedController = $controller->customise(array(
888
                'Content' => DBField::create_field(
889
                    'HTMLFragment',
890
                    '<p>' . _t('Security.CHANGEPASSWORDBELOW', 'You can change your password below.') . '</p>'
891
                ),
892
                'Form' => $this->ChangePasswordForm()));
893
        } else {
894
            // Show friendly message if it seems like the user arrived here via password reset feature.
895
            if (isset($_REQUEST['m']) || isset($_REQUEST['t'])) {
896
                $customisedController = $controller->customise(
897
                    array('Content' => DBField::create_field(
898
                        'HTMLFragment',
899
                        _t(
900
                            'Security.NOTERESETLINKINVALID',
901
                            '<p>The password reset link is invalid or expired.</p>'
902
                            . '<p>You can request a new one <a href="{link1}">here</a> or change your password after'
903
                            . ' you <a href="{link2}">logged in</a>.</p>',
904
                            [
905
                                'link1' => $this->Link('lostpassword'),
906
                                'link2' => $this->Link('login')
907
                            ]
908
                        )
909
                    ))
910
                );
911
            } else {
912
                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 912 which is incompatible with the return type documented by SilverStripe\Security\Security::changepassword of type string|SilverStripe\Control\HTTPRequest.
Loading history...
913
                    $this,
914
                    _t('Security.ERRORPASSWORDPERMISSION', 'You must be logged in in order to change your password!')
915
                );
916
            }
917
        }
918
919
        return $customisedController->renderWith($this->getTemplatesFor('changepassword'));
920
    }
921
922
    /**
923
     * Factory method for the lost password form
924
     *
925
     * @skipUpgrade
926
     * @return ChangePasswordForm Returns the lost password form
927
     */
928
    public function ChangePasswordForm()
929
    {
930
        return MemberAuthenticator\ChangePasswordForm::create($this, 'ChangePasswordForm');
931
    }
932
933
    /**
934
     * Determine the list of templates to use for rendering the given action.
935
     *
936
     * @skipUpgrade
937
     * @param string $action
938
     * @return array Template list
939
     */
940
    public function getTemplatesFor($action)
941
    {
942
        $templates = SSViewer::get_templates_by_class(get_class($this), "_{$action}", __CLASS__);
943
        return array_merge(
944
            $templates,
945
            [
946
                "Security_{$action}",
947
                "Security",
948
                $this->stat("template_main"),
949
                "BlankPage"
950
            ]
951
        );
952
    }
953
954
    /**
955
     * Return an existing member with administrator privileges, or create one of necessary.
956
     *
957
     * Will create a default 'Administrators' group if no group is found
958
     * with an ADMIN permission. Will create a new 'Admin' member with administrative permissions
959
     * if no existing Member with these permissions is found.
960
     *
961
     * Important: Any newly created administrator accounts will NOT have valid
962
     * login credentials (Email/Password properties), which means they can't be used for login
963
     * purposes outside of any default credentials set through {@link Security::setDefaultAdmin()}.
964
     *
965
     * @return Member
966
     */
967
    public static function findAnAdministrator()
968
    {
969
        // coupling to subsites module
970
        $origSubsite = null;
971
        if (is_callable('Subsite::changeSubsite')) {
972
            $origSubsite = Subsite::currentSubsiteID();
973
            Subsite::changeSubsite(0);
974
        }
975
976
        /** @var Member $member */
977
        $member = null;
978
979
        // find a group with ADMIN permission
980
        $adminGroup = Permission::get_groups_by_permission('ADMIN')->first();
981
982
        if (is_callable('Subsite::changeSubsite')) {
983
            Subsite::changeSubsite($origSubsite);
984
        }
985
986
        if ($adminGroup) {
987
            $member = $adminGroup->Members()->First();
988
        }
989
990
        if (!$adminGroup) {
991
            Group::singleton()->requireDefaultRecords();
992
            $adminGroup = Permission::get_groups_by_permission('ADMIN')->first();
993
        }
994
995
        if (!$member) {
996
            Member::singleton()->requireDefaultRecords();
997
            $member = Permission::get_members_by_permission('ADMIN')->first();
998
        }
999
1000
        if (!$member) {
1001
            $member = Member::default_admin();
1002
        }
1003
1004
        if (!$member) {
1005
            // Failover to a blank admin
1006
            $member = Member::create();
1007
            $member->FirstName = _t('Member.DefaultAdminFirstname', 'Default Admin');
1008
            $member->write();
1009
            // Add member to group instead of adding group to member
1010
            // This bypasses the privilege escallation code in Member_GroupSet
1011
            $adminGroup
1012
                ->DirectMembers()
1013
                ->add($member);
1014
        }
1015
1016
        return $member;
1017
    }
1018
1019
    /**
1020
     * Flush the default admin credentials
1021
     */
1022
    public static function clear_default_admin()
1023
    {
1024
        self::$default_username = null;
1025
        self::$default_password = null;
1026
    }
1027
1028
1029
    /**
1030
     * Set a default admin in dev-mode
1031
     *
1032
     * This will set a static default-admin which is not existing
1033
     * as a database-record. By this workaround we can test pages in dev-mode
1034
     * with a unified login. Submitted login-credentials are first checked
1035
     * against this static information in {@link Security::authenticate()}.
1036
     *
1037
     * @param string $username The user name
1038
     * @param string $password The password (in cleartext)
1039
     * @return bool True if successfully set
1040
     */
1041
    public static function setDefaultAdmin($username, $password)
1042
    {
1043
        // don't overwrite if already set
1044
        if (self::$default_username || self::$default_password) {
1045
            return false;
1046
        }
1047
1048
        self::$default_username = $username;
1049
        self::$default_password = $password;
1050
        return true;
1051
    }
1052
1053
    /**
1054
     * Checks if the passed credentials are matching the default-admin.
1055
     * Compares cleartext-password set through Security::setDefaultAdmin().
1056
     *
1057
     * @param string $username
1058
     * @param string $password
1059
     * @return bool
1060
     */
1061
    public static function check_default_admin($username, $password)
1062
    {
1063
        return (
1064
            self::$default_username === $username
1065
            && self::$default_password === $password
1066
            && self::has_default_admin()
1067
        );
1068
    }
1069
1070
    /**
1071
     * Check that the default admin account has been set.
1072
     */
1073
    public static function has_default_admin()
1074
    {
1075
        return !empty(self::$default_username) && !empty(self::$default_password);
1076
    }
1077
1078
    /**
1079
     * Get default admin username
1080
     *
1081
     * @return string
1082
     */
1083
    public static function default_admin_username()
1084
    {
1085
        return self::$default_username;
1086
    }
1087
1088
    /**
1089
     * Get default admin password
1090
     *
1091
     * @return string
1092
     */
1093
    public static function default_admin_password()
1094
    {
1095
        return self::$default_password;
1096
    }
1097
1098
    /**
1099
     * Encrypt a password according to the current password encryption settings.
1100
     * If the settings are so that passwords shouldn't be encrypted, the
1101
     * result is simple the clear text password with an empty salt except when
1102
     * a custom algorithm ($algorithm parameter) was passed.
1103
     *
1104
     * @param string $password The password to encrypt
1105
     * @param string $salt Optional: The salt to use. If it is not passed, but
1106
     *  needed, the method will automatically create a
1107
     *  random salt that will then be returned as return value.
1108
     * @param string $algorithm Optional: Use another algorithm to encrypt the
1109
     *  password (so that the encryption algorithm can be changed over the time).
1110
     * @param Member $member Optional
1111
     * @return mixed Returns an associative array containing the encrypted
1112
     *  password and the used salt in the form:
1113
     * <code>
1114
     *  array(
1115
     *  'password' => string,
1116
     *  'salt' => string,
1117
     *  'algorithm' => string,
1118
     *  'encryptor' => PasswordEncryptor instance
1119
     *  )
1120
     * </code>
1121
     * If the passed algorithm is invalid, FALSE will be returned.
1122
     *
1123
     * @see encrypt_passwords()
1124
     */
1125
    public static function encrypt_password($password, $salt = null, $algorithm = null, $member = null)
1126
    {
1127
        // Fall back to the default encryption algorithm
1128
        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...
1129
            $algorithm = self::config()->get('password_encryption_algorithm');
1130
        }
1131
1132
        $e = PasswordEncryptor::create_for_algorithm($algorithm);
1133
1134
        // New salts will only need to be generated if the password is hashed for the first time
1135
        $salt = ($salt) ? $salt : $e->salt($password);
1136
1137
        return array(
1138
            'password' => $e->encrypt($password, $salt, $member),
1139
            'salt' => $salt,
1140
            'algorithm' => $algorithm,
1141
            'encryptor' => $e
1142
        );
1143
    }
1144
1145
    /**
1146
     * Checks the database is in a state to perform security checks.
1147
     * See {@link DatabaseAdmin->init()} for more information.
1148
     *
1149
     * @return bool
1150
     */
1151
    public static function database_is_ready()
1152
    {
1153
        // Used for unit tests
1154
        if (self::$force_database_is_ready !== null) {
1155
            return self::$force_database_is_ready;
1156
        }
1157
1158
        if (self::$database_is_ready) {
1159
            return self::$database_is_ready;
1160
        }
1161
1162
        $requiredClasses = ClassInfo::dataClassesFor(Member::class);
1163
        $requiredClasses[] = Group::class;
1164
        $requiredClasses[] = Permission::class;
1165
        $schema = DataObject::getSchema();
1166
        foreach ($requiredClasses as $class) {
1167
            // Skip test classes, as not all test classes are scaffolded at once
1168
            if (is_a($class, TestOnly::class, true)) {
1169
                continue;
1170
            }
1171
1172
            // if any of the tables aren't created in the database
1173
            $table = $schema->tableName($class);
1174
            if (!ClassInfo::hasTable($table)) {
1175
                return false;
1176
            }
1177
1178
            // HACK: DataExtensions aren't applied until a class is instantiated for
1179
            // the first time, so create an instance here.
1180
            singleton($class);
1181
1182
            // if any of the tables don't have all fields mapped as table columns
1183
            $dbFields = DB::field_list($table);
1184
            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...
1185
                return false;
1186
            }
1187
1188
            $objFields = $schema->databaseFields($class, false);
1189
            $missingFields = array_diff_key($objFields, $dbFields);
1190
1191
            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...
1192
                return false;
1193
            }
1194
        }
1195
        self::$database_is_ready = true;
1196
1197
        return true;
1198
    }
1199
1200
    /**
1201
     * Enable or disable recording of login attempts
1202
     * through the {@link LoginRecord} object.
1203
     *
1204
     * @deprecated 4.0 Use the "Security.login_recording" config setting instead
1205
     * @param boolean $bool
1206
     */
1207
    public static function set_login_recording($bool)
1208
    {
1209
        Deprecation::notice('4.0', 'Use the "Security.login_recording" config setting instead');
1210
        self::$login_recording = (bool)$bool;
1211
    }
1212
1213
    /**
1214
     * @deprecated 4.0 Use the "Security.login_recording" config setting instead
1215
     * @return boolean
1216
     */
1217
    public static function login_recording()
1218
    {
1219
        Deprecation::notice('4.0', 'Use the "Security.login_recording" config setting instead');
1220
        return self::$login_recording;
1221
    }
1222
1223
    /**
1224
     * @config
1225
     * @var string Set the default login dest
1226
     * This is the URL that users will be redirected to after they log in,
1227
     * if they haven't logged in en route to access a secured page.
1228
     * By default, this is set to the homepage.
1229
     */
1230
    private static $default_login_dest = "";
1231
1232
    protected static $ignore_disallowed_actions = false;
1233
1234
    /**
1235
     * Set to true to ignore access to disallowed actions, rather than returning permission failure
1236
     * Note that this is just a flag that other code needs to check with Security::ignore_disallowed_actions()
1237
     * @param $flag True or false
1238
     */
1239
    public static function set_ignore_disallowed_actions($flag)
1240
    {
1241
        self::$ignore_disallowed_actions = $flag;
1242
    }
1243
1244
    public static function ignore_disallowed_actions()
1245
    {
1246
        return self::$ignore_disallowed_actions;
1247
    }
1248
1249
    /**
1250
     * Get the URL of the log-in page.
1251
     *
1252
     * To update the login url use the "Security.login_url" config setting.
1253
     *
1254
     * @return string
1255
     */
1256
    public static function login_url()
1257
    {
1258
        return Controller::join_links(Director::baseURL(), self::config()->get('login_url'));
1259
    }
1260
1261
1262
    /**
1263
     * Get the URL of the logout page.
1264
     *
1265
     * To update the logout url use the "Security.logout_url" config setting.
1266
     *
1267
     * @return string
1268
     */
1269
    public static function logout_url()
1270
    {
1271
        return Controller::join_links(Director::baseURL(), self::config()->get('logout_url'));
1272
    }
1273
1274
    /**
1275
     * Get the URL of the logout page.
1276
     *
1277
     * To update the logout url use the "Security.logout_url" config setting.
1278
     *
1279
     * @return string
1280
     */
1281
    public static function lost_password_url()
1282
    {
1283
        return Controller::join_links(Director::baseURL(), self::config()->get('lost_password_url'));
1284
    }
1285
1286
    /**
1287
     * Defines global accessible templates variables.
1288
     *
1289
     * @return array
1290
     */
1291
    public static function get_template_global_variables()
1292
    {
1293
        return array(
1294
            "LoginURL" => "login_url",
1295
            "LogoutURL" => "logout_url",
1296
            "LostPasswordURL" => "lost_password_url",
1297
        );
1298
    }
1299
}
1300