Completed
Push — authenticator-refactor ( 7dc887...371abb )
by Simon
06:49
created

Security::login_recording()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 0
dl 0
loc 6
rs 9.4285
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\Convert;
17
use SilverStripe\Dev\Deprecation;
18
use SilverStripe\Dev\TestOnly;
19
use SilverStripe\ORM\ArrayList;
20
use SilverStripe\ORM\DataModel;
21
use SilverStripe\ORM\DB;
22
use SilverStripe\ORM\DataObject;
23
use SilverStripe\ORM\FieldType\DBField;
24
use SilverStripe\ORM\ValidationResult;
25
use SilverStripe\Security\MemberAuthenticator\LogoutHandler;
26
use SilverStripe\View\ArrayData;
27
use SilverStripe\View\SSViewer;
28
use SilverStripe\View\TemplateGlobalProvider;
29
use Subsite;
30
31
/**
32
 * Implements a basic security model
33
 */
34
class Security extends Controller implements TemplateGlobalProvider
35
{
36
37
    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...
38
        'index',
39
        'login',
40
        'logout',
41
        'basicauthlogin',
42
        'lostpassword',
43
        'passwordsent',
44
        'changepassword',
45
        'ping',
46
        'LoginForm',
47
        'ChangePasswordForm',
48
        'LostPasswordForm',
49
    );
50
51
    /**
52
     * Default user name. Only used in dev-mode by {@link setDefaultAdmin()}
53
     *
54
     * @var string
55
     * @see setDefaultAdmin()
56
     */
57
    protected static $default_username;
58
59
    /**
60
     * Default password. Only used in dev-mode by {@link setDefaultAdmin()}
61
     *
62
     * @var string
63
     * @see setDefaultAdmin()
64
     */
65
    protected static $default_password;
66
67
    /**
68
     * If set to TRUE to prevent sharing of the session across several sites
69
     * in the domain.
70
     *
71
     * @config
72
     * @var bool
73
     */
74
    protected static $strict_path_checking = false;
75
76
    /**
77
     * The password encryption algorithm to use by default.
78
     * This is an arbitrary code registered through {@link PasswordEncryptor}.
79
     *
80
     * @config
81
     * @var string
82
     */
83
    private static $password_encryption_algorithm = 'blowfish';
84
85
    /**
86
     * Showing "Remember me"-checkbox
87
     * on loginform, and saving encrypted credentials to a cookie.
88
     *
89
     * @config
90
     * @var bool
91
     */
92
    private static $autologin_enabled = true;
93
94
    /**
95
     * Determine if login username may be remembered between login sessions
96
     * If set to false this will disable autocomplete and prevent username persisting in the session
97
     *
98
     * @config
99
     * @var bool
100
     */
101
    private static $remember_username = true;
102
103
    /**
104
     * Location of word list to use for generating passwords
105
     *
106
     * @config
107
     * @var string
108
     */
109
    private static $word_list = './wordlist.txt';
110
111
    /**
112
     * @config
113
     * @var string
114
     */
115
    private static $template = 'BlankPage';
116
117
    /**
118
     * Template thats used to render the pages.
119
     *
120
     * @var string
121
     * @config
122
     */
123
    private static $template_main = 'Page';
124
125
    /**
126
     * Class to use for page rendering
127
     *
128
     * @var string
129
     * @config
130
     */
131
    private static $page_class = Page::class;
132
133
    /**
134
     * Default message set used in permission failures.
135
     *
136
     * @config
137
     * @var array|string
138
     */
139
    private static $default_message_set;
140
141
    /**
142
     * Random secure token, can be used as a crypto key internally.
143
     * Generate one through 'sake dev/generatesecuretoken'.
144
     *
145
     * @config
146
     * @var String
147
     */
148
    private static $token;
149
150
    /**
151
     * The default login URL
152
     *
153
     * @config
154
     *
155
     * @var string
156
     */
157
    private static $login_url = "Security/login";
158
159
    /**
160
     * The default logout URL
161
     *
162
     * @config
163
     *
164
     * @var string
165
     */
166
    private static $logout_url = "Security/logout";
167
168
    /**
169
     * The default lost password URL
170
     *
171
     * @config
172
     *
173
     * @var string
174
     */
175
    private static $lost_password_url = "Security/lostpassword";
176
177
    /**
178
     * Value of X-Frame-Options header
179
     *
180
     * @config
181
     * @var string
182
     */
183
    private static $frame_options = 'SAMEORIGIN';
184
185
    /**
186
     * Value of the X-Robots-Tag header (for the Security section)
187
     *
188
     * @config
189
     * @var string
190
     */
191
    private static $robots_tag = 'noindex, nofollow';
192
193
    /**
194
     * Enable or disable recording of login attempts
195
     * through the {@link LoginRecord} object.
196
     *
197
     * @config
198
     * @var boolean $login_recording
199
     */
200
    private static $login_recording = false;
201
202
    /**
203
     * @var boolean If set to TRUE or FALSE, {@link database_is_ready()}
204
     * will always return FALSE. Used for unit testing.
205
     */
206
    protected static $force_database_is_ready = null;
207
208
    /**
209
     * When the database has once been verified as ready, it will not do the
210
     * checks again.
211
     *
212
     * @var bool
213
     */
214
    protected static $database_is_ready = false;
215
216
    /**
217
     * @var string Default authenticator to use when none is given
218
     */
219
    protected static $default_authenticator = 'default';
220
221
    /**
222
     * @var array available authenticators
223
     */
224
    protected static $authenticators = [];
225
226
    /**
227
     * @var Member Currently logged in user (if available)
228
     */
229
    protected static $currentUser;
230
231
    /**
232
     * @return array
233
     */
234
    public static function getAuthenticators()
235
    {
236
        return self::$authenticators;
237
    }
238
239
    /**
240
     * @param array|\SilverStripe\Security\Authenticator $authenticators
241
     */
242
    public static function setAuthenticators(array $authenticators)
243
    {
244
        self::$authenticators = $authenticators;
245
    }
246
247
    /**
248
     * @inheritdoc
249
     */
250
    protected function init()
251
    {
252
        parent::init();
253
254
        // Prevent clickjacking, see https://developer.mozilla.org/en-US/docs/HTTP/X-Frame-Options
255
        $frameOptions = static::config()->get('frame_options');
256
        if ($frameOptions) {
257
            $this->getResponse()->addHeader('X-Frame-Options', $frameOptions);
258
        }
259
260
        // Prevent search engines from indexing the login page
261
        $robotsTag = static::config()->get('robots_tag');
262
        if ($robotsTag) {
263
            $this->getResponse()->addHeader('X-Robots-Tag', $robotsTag);
264
        }
265
    }
266
267
    /**
268
     * @inheritdoc
269
     */
270
    public function index()
271
    {
272
        return $this->httpError(404); // no-op
273
    }
274
275
    /**
276
     * Get the selected authenticator for this request
277
     *
278
     * @param $name string The identifier of the authenticator in your config
279
     * @return Authenticator Class name of Authenticator
280
     * @throws LogicException
281
     */
282
    protected function getAuthenticator($name = 'default')
283
    {
284
        $authenticators = static::$authenticators;
285
286
        if (isset($authenticators[$name])) {
287
            return $authenticators[$name];
288
        }
289
290
        throw new LogicException('No valid authenticator found');
291
    }
292
293
    /**
294
     * Get all registered authenticators
295
     *
296
     * @param int $service The type of service that is requested
297
     * @return array Return an array of Authenticator objects
298
     */
299
    public function getApplicableAuthenticators($service = Authenticator::LOGIN)
300
    {
301
        $authenticators = static::$authenticators;
302
303
        /** @var Authenticator $class */
304
        foreach ($authenticators as $name => $class) {
305
            if (!($class->supportedServices() & $service)) {
306
                unset($authenticators[$name]);
307
            }
308
        }
309
310
        return $authenticators;
311
    }
312
313
    /**
314
     * Check if a given authenticator is registered
315
     *
316
     * @param string $authenticator The configured identifier of the authenicator
317
     * @return bool Returns TRUE if the authenticator is registered, FALSE
318
     *              otherwise.
319
     */
320
    public static function hasAuthenticator($authenticator)
321
    {
322
        $authenticators = static::$authenticators;
323
324
        return !empty($authenticators[$authenticator]);
325
    }
326
327
    /**
328
     * Register that we've had a permission failure trying to view the given page
329
     *
330
     * This will redirect to a login page.
331
     * If you don't provide a messageSet, a default will be used.
332
     *
333
     * @param Controller $controller The controller that you were on to cause the permission
334
     *                               failure.
335
     * @param string|array $messageSet The message to show to the user. This
336
     *                                 can be a string, or a map of different
337
     *                                 messages for different contexts.
338
     *                                 If you pass an array, you can use the
339
     *                                 following keys:
340
     *                                   - default: The default message
341
     *                                   - alreadyLoggedIn: The message to
342
     *                                                      show if the user
343
     *                                                      is already logged
344
     *                                                      in and lacks the
345
     *                                                      permission to
346
     *                                                      access the item.
347
     *
348
     * The alreadyLoggedIn value can contain a '%s' placeholder that will be replaced with a link
349
     * to log in.
350
     * @return HTTPResponse
351
     */
352
    public static function permissionFailure($controller = null, $messageSet = null)
353
    {
354
        self::set_ignore_disallowed_actions(true);
355
356
        if (!$controller) {
357
            $controller = Controller::curr();
358
        }
359
360
        if (Director::is_ajax()) {
361
            $response = ($controller) ? $controller->getResponse() : new HTTPResponse();
362
            $response->setStatusCode(403);
363
            if (!static::getCurrentUser()) {
364
                $response->setBody(
365
                    _t('SilverStripe\\CMS\\Controllers\\ContentController.NOTLOGGEDIN',
366
                    'Not logged in')
367
                );
368
                $response->setStatusDescription(
369
                    _t('SilverStripe\\CMS\\Controllers\\ContentController.NOTLOGGEDIN',
370
                    'Not logged in')
371
                );
372
                // Tell the CMS to allow re-aunthentication
373
                if (CMSSecurity::enabled()) {
374
                    $response->addHeader('X-Reauthenticate', '1');
375
                }
376
            }
377
378
            return $response;
379
        }
380
381
        // Prepare the messageSet provided
382
        if (!$messageSet) {
383
            if ($configMessageSet = static::config()->get('default_message_set')) {
384
                $messageSet = $configMessageSet;
385
            } else {
386
                $messageSet = array(
387
                    'default'         => _t(
388
                        'SilverStripe\\Security\\Security.NOTEPAGESECURED',
389
                        "That page is secured. Enter your credentials below and we will send "
390
                        . "you right along."
391
                    ),
392
                    'alreadyLoggedIn' => _t(
393
                        'SilverStripe\\Security\\Security.ALREADYLOGGEDIN',
394
                        "You don't have access to this page.  If you have another account that "
395
                        . "can access that page, you can log in again below.",
396
                        "%s will be replaced with a link to log in."
397
                    )
398
                );
399
            }
400
        }
401
402
        if (!is_array($messageSet)) {
403
            $messageSet = array('default' => $messageSet);
404
        }
405
406
        $member = static::getCurrentUser();
407
408
        // Work out the right message to show
409
        if ($member && $member->exists()) {
410
            $response = ($controller) ? $controller->getResponse() : new HTTPResponse();
411
            $response->setStatusCode(403);
412
413
            //If 'alreadyLoggedIn' is not specified in the array, then use the default
414
            //which should have been specified in the lines above
415
            if (isset($messageSet['alreadyLoggedIn'])) {
416
                $message = $messageSet['alreadyLoggedIn'];
417
            } else {
418
                $message = $messageSet['default'];
419
            }
420
421
            Security::singleton()->setLoginMessage($message, ValidationResult::TYPE_WARNING);
422
            $loginResponse = Security::singleton()->login();
423
            if ($loginResponse instanceof HTTPResponse) {
424
                return $loginResponse;
425
            }
426
427
            $response->setBody((string)$loginResponse);
428
429
            $controller->extend('permissionDenied', $member);
430
431
            return $response;
432
        } else {
433
            $message = $messageSet['default'];
434
        }
435
436
        Security::singleton()->setLoginMessage($message, ValidationResult::TYPE_WARNING);
437
438
        Session::set("BackURL", $_SERVER['REQUEST_URI']);
439
440
        // TODO AccessLogEntry needs an extension to handle permission denied errors
441
        // Audit logging hook
442
        $controller->extend('permissionDenied', $member);
443
444
        return $controller->redirect(Controller::join_links(
445
            Security::config()->uninherited('login_url'),
446
            "?BackURL=" . urlencode($_SERVER['REQUEST_URI'])
447
        ));
448
    }
449
450
    public static function setCurrentUser($currentUser = null)
451
    {
452
        self::$currentUser = $currentUser;
453
    }
454
455
    public static function getCurrentUser()
456
    {
457
        return self::$currentUser;
458
    }
459
460
    /**
461
     * Get the login forms for all available authentication methods
462
     *
463
     * @deprecated 5.0.0 Now handled by {@link static::delegateToMultipleHandlers}
464
     *
465
     * @return array Returns an array of available login forms (array of Form
466
     *               objects).
467
     *
468
     */
469
    public function getLoginForms()
470
    {
471
        Deprecation::notice('5.0.0', 'Now handled by delegateToMultipleHandlers');
472
473
        return array_map(
474
            function ($authenticator) {
475
                return [$authenticator->getLoginHandler($this->Link())->loginForm()];
476
            },
477
            $this->getApplicableAuthenticators()
478
        );
479
    }
480
481
482
    /**
483
     * Get a link to a security action
484
     *
485
     * @param string $action Name of the action
486
     * @return string Returns the link to the given action
487
     */
488
    public function Link($action = null)
489
    {
490
        /** @skipUpgrade */
491
        return Controller::join_links(Director::baseURL(), "Security", $action);
492
    }
493
494
    /**
495
     * This action is available as a keep alive, so user
496
     * sessions don't timeout. A common use is in the admin.
497
     */
498
    public function ping()
499
    {
500
        return 1;
501
    }
502
503
    /**
504
     * Log the currently logged in user out
505
     *
506
     * Logging out without ID-parameter in the URL, will log the user out of all applicable Authenticators.
507
     *
508
     * Adding an ID will only log the user out of that Authentication method.
509
     *
510
     * Logging out of Default will <i>always</i> completely log out the user.
511
     *
512
     * @param bool $redirect Redirect the user back to where they came.
513
     *                       - If it's false, the code calling logout() is
514
     *                         responsible for sending the user where-ever
515
     *                         they should go.
516
     * @return HTTPResponse|null
517
     */
518
    public function logout($redirect = true)
519
    {
520
        $this->extend('beforeMemberLoggedOut');
521
        $member = static::getCurrentUser();
522
523
        if ($member) { // If we don't have a member, there's not much to log out.
524
            /** @var Authenticator[] $authenticator */
525
            $authenticators = $this->getApplicableAuthenticators(Authenticator::LOGOUT);
526
            foreach ($authenticators as $name => $authenticator) {
527
                $handler = $authenticator->getLogOutHandler(Controller::join_links($this->Link(), 'logout'));
528
                $result = $this->delegateToHandler($handler, $name, []);
529
            }
530
            if ($result !== true) {
0 ignored issues
show
Bug introduced by
The variable $result does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
531
                $this->extend('failureMemberLoggedOut', $authenticator);
532
533
                return $this->redirectBack();
534
            }
535
            $this->extend('successMemberLoggedOut', $authenticator);
536
            // Member is successfully logged out. Write possible changes to the database.
537
            $member->write();
538
        }
539
        $this->extend('afterMemberLoggedOut');
540
541
        if ($redirect && (!$this->getResponse()->isFinished())) {
542
            return $this->redirectBack();
543
        }
544
545
        return null;
546
    }
547
548
    /**
549
     * Perform pre-login checking and prepare a response if available prior to login
550
     *
551
     * @return HTTPResponse Substitute response object if the login process should be curcumvented.
552
     * Returns null if should proceed as normal.
553
     */
554
    protected function preLogin()
555
    {
556
        // Event handler for pre-login, with an option to let it break you out of the login form
557
        $eventResults = $this->extend('onBeforeSecurityLogin');
558
        // If there was a redirection, return
559
        if ($this->redirectedTo()) {
560
            return $this->getResponse();
561
        }
562
        // If there was an HTTPResponse object returned, then return that
563
        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...
564
            foreach ($eventResults as $result) {
565
                if ($result instanceof HTTPResponse) {
566
                    return $result;
567
                }
568
            }
569
        }
570
571
        // If arriving on the login page already logged in, with no security error, and a ReturnURL then redirect
572
        // back. The login message check is neccesary to prevent infinite loops where BackURL links to
573
        // an action that triggers Security::permissionFailure.
574
        // This step is necessary in cases such as automatic redirection where a user is authenticated
575
        // upon landing on an SSL secured site and is automatically logged in, or some other case
576
        // where the user has permissions to continue but is not given the option.
577
        if ($this->getRequest()->requestVar('BackURL')
578
            && !$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...
579
            && ($member = static::getCurrentUser())
580
            && $member->exists()
581
        ) {
582
            return $this->redirectBack();
583
        }
584
585
        return null;
586
    }
587
588
    /**
589
     * Prepare the controller for handling the response to this request
590
     *
591
     * @param string $title Title to use
592
     * @return Controller
593
     */
594
    protected function getResponseController($title)
595
    {
596
        // Use the default setting for which Page to use to render the security page
597
        $pageClass = $this->stat('page_class');
598
        if (!$pageClass || !class_exists($pageClass)) {
599
            return $this;
600
        }
601
602
        // Create new instance of page holder
603
        /** @var Page $holderPage */
604
        $holderPage = new $pageClass;
605
        $holderPage->Title = $title;
606
        /** @skipUpgrade */
607
        $holderPage->URLSegment = 'Security';
608
        // Disable ID-based caching  of the log-in page by making it a random number
609
        $holderPage->ID = -1 * random_int(1, 10000000);
610
611
        $controllerClass = $holderPage->getControllerName();
612
        /** @var ContentController $controller */
613
        $controller = $controllerClass::create($holderPage);
614
        $controller->setDataModel($this->model);
615
        $controller->doInit();
616
617
        return $controller;
618
    }
619
620
    /**
621
     * Combine the given forms into a formset with a tabbed interface
622
     *
623
     * @param $forms
624
     * @return string
625
     */
626
    protected function generateLoginFormSet($forms)
627
    {
628
        $viewData = new ArrayData(array(
629
            'Forms' => new ArrayList($forms),
630
        ));
631
632
        return $viewData->renderWith(
633
            $this->getTemplatesFor('MultiAuthenticatorLogin')
634
        );
635
    }
636
637
    /**
638
     * Get the HTML Content for the $Content area during login
639
     *
640
     * @param string &$messageType Type of message, if available, passed back to caller
641
     * @return string Message in HTML format
642
     */
643
    protected function getLoginMessage(&$messageType = null)
644
    {
645
        $message = Session::get('Security.Message.message');
646
        $messageType = null;
647
        if (empty($message)) {
648
            return null;
649
        }
650
651
        $messageType = Session::get('Security.Message.type');
652
        $messageCast = Session::get('Security.Message.cast');
653
        if ($messageCast !== ValidationResult::CAST_HTML) {
654
            $message = Convert::raw2xml($message);
655
        }
656
657
        return sprintf('<p class="message %s">%s</p>', Convert::raw2att($messageType), $message);
658
    }
659
660
    /**
661
     * Set the next message to display for the security login page. Defaults to warning
662
     *
663
     * @param string $message Message
664
     * @param string $messageType Message type. One of ValidationResult::TYPE_*
665
     * @param string $messageCast Message cast. One of ValidationResult::CAST_*
666
     */
667
    public function setLoginMessage(
668
        $message,
669
        $messageType = ValidationResult::TYPE_WARNING,
670
        $messageCast = ValidationResult::CAST_TEXT
671
    ) {
672
        Session::set("Security.Message.message", $message);
673
        Session::set("Security.Message.type", $messageType);
674
        Session::set("Security.Message.cast", $messageCast);
675
    }
676
677
    /**
678
     * Clear login message
679
     */
680
    public static function clearLoginMessage()
681
    {
682
        Session::clear("Security.Message");
683
    }
684
685
686
    /**
687
     * Show the "login" page
688
     *
689
     * For multiple authenticators, Security_MultiAuthenticatorLogin is used.
690
     * See getTemplatesFor and getIncludeTemplate for how to override template logic
691
     *
692
     * @param $request
693
     * @return HTTPResponse|string Returns the "login" page as HTML code.
694
     * @throws HTTPResponse_Exception
695
     */
696
    public function login($request = null, $service = Authenticator::LOGIN)
697
    {
698
        // Check pre-login process
699
        if ($response = $this->preLogin()) {
700
            return $response;
701
        }
702
        if (!$request) {
703
            $request = $this->getRequest();
704
        }
705
        $authName = ($request && $request->param('ID')) ? $request->param('ID') : static::$default_authenticator;
706
707
        $link = $this->link("login");
708
709
        // Delegate to a single handler - Security/login/<authname>/...
710
        if ($authName && self::hasAuthenticator($authName)) {
711
            if ($request) {
712
                $request->shift();
713
            }
714
715
            $authenticator = $this->getAuthenticator($authName);
716
            // @todo handle different Authenticator situations
717
            if (!$authenticator->supportedServices() & $service) {
718
                throw new HTTPResponse_Exception('Invalid Authenticator "' . $name . '" for login action', 418);
719
            }
720
721
            $handlers = [$authName => $authenticator];
722
723
            // Delegate to all of them, building a tabbed view - Security/login
724
        } else {
725
            $handlers = $this->getApplicableAuthenticators($service);
726
        }
727
728
        array_walk(
729
            $handlers,
730
            function (&$auth, $name) use ($link) {
731
                $auth = $auth->getLoginHandler(Controller::join_links($link, $name));
732
            }
733
        );
734
735
        return $this->delegateToMultipleHandlers(
736
            $handlers,
737
            _t('Security.LOGIN', 'Log in'),
738
            $this->getTemplatesFor('login')
739
        );
740
    }
741
742
    /**
743
     * Delegate to an number of handlers, extracting their forms and rendering a tabbed form-set.
744
     * This is used to built the log-in page where there are multiple authenticators active.
745
     *
746
     * If a single handler is passed, delegateToHandler() will be called instead
747
     *
748
     * @param string $title The title of the form
749
     * @param array $templates
750
     * @return array|HTTPResponse|RequestHandler|\SilverStripe\ORM\FieldType\DBHTMLText|string
751
     */
752
    protected function delegateToMultipleHandlers(array $handlers, $title, array $templates)
753
    {
754
755
        // Simpler case for a single authenticator
756
        if (count($handlers) === 1) {
757
            return $this->delegateToHandler(array_values($handlers)[0], $title, $templates);
758
        }
759
760
        // Process each of the handlers
761
        $results = array_map(
762
            function ($handler) {
763
                return $handler->handleRequest($this->getRequest(), DataModel::inst());
764
            },
765
            $handlers
766
        );
767
768
        // Aggregate all their forms, assuming they all return
769
        $forms = [];
770
        foreach ($results as $authName => $singleResult) {
771
            // The result *must* be an array with a Form key
772
            if (!is_array($singleResult) || !isset($singleResult['Form'])) {
773
                user_error('Authenticator "' . $authName . '" doesn\'t support a tabbed login', E_USER_WARNING);
774
                continue;
775
            }
776
777
            $forms[] = $singleResult['Form'];
778
        }
779
780
        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...
781
            throw new \LogicException("No authenticators found compatible with a tabbed login");
782
        }
783
784
        return $this->renderWrappedController(
785
            $title,
786
            [
787
                'Form' => $this->generateLoginFormSet($forms),
788
            ],
789
            $templates
790
        );
791
    }
792
793
    /**
794
     * Delegate to another RequestHandler, rendering any fragment arrays into an appropriate.
795
     * controller.
796
     *
797
     * @param string $title The title of the form
798
     * @param array $templates
799
     * @return array|HTTPResponse|RequestHandler|\SilverStripe\ORM\FieldType\DBHTMLText|string
800
     */
801
    protected function delegateToHandler(RequestHandler $handler, $title, array $templates)
802
    {
803
        $result = $handler->handleRequest($this->getRequest(), DataModel::inst());
804
805
        // Return the customised controller - used to render in a Form
806
        // Post requests are expected to be login posts, so they'll be handled downstairs
807
        if (is_array($result)) {
808
            $result = $this->renderWrappedController($title, $result, $templates);
809
        }
810
811
        return $result;
812
    }
813
814
    /**
815
     * Render the given fragments into a security page controller with the given title.
816
     * @param string $title string The title to give the security page
817
     * @param array $fragments A map of objects to render into the page, e.g. "Form"
818
     * @param array $templates An array of templates to use for the render
819
     * @return HTTPResponse|\SilverStripe\ORM\FieldType\DBHTMLText
820
     */
821
    protected function renderWrappedController($title, array $fragments, array $templates)
822
    {
823
        $controller = $this->getResponseController($title);
824
825
        // if the controller calls Director::redirect(), this will break early
826
        if (($response = $controller->getResponse()) && $response->isFinished()) {
827
            return $response;
828
        }
829
830
        // Handle any form messages from validation, etc.
831
        $messageType = '';
832
        $message = $this->getLoginMessage($messageType);
833
834
        // We've displayed the message in the form output, so reset it for the next run.
835
        static::clearLoginMessage();
836
837
        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...
838
            $messageResult = [
839
                'Content'     => DBField::create_field('HTMLFragment', $message),
840
                'Message'     => DBField::create_field('HTMLFragment', $message),
841
                'MessageType' => $messageType
842
            ];
843
            $fragments = array_merge($fragments, $messageResult);
844
        }
845
846
        return $controller->customise($fragments)->renderWith($templates);
847
    }
848
849
    public function basicauthlogin()
850
    {
851
        $member = BasicAuth::requireLogin($this->getRequest(), "SilverStripe login", 'ADMIN');
852
        static::setCurrentUser($member);
853
    }
854
855
    /**
856
     * Show the "lost password" page
857
     *
858
     * @return string Returns the "lost password" page as HTML code.
859
     */
860
    public function lostpassword()
861
    {
862
        $handler = $this->getAuthenticator('default')->getLostPasswordHandler(
863
            Controller::join_links($this->link(), 'lostpassword')
864
        );
865
866
        return $this->delegateToHandler(
867
            $handler,
868
            _t('SilverStripe\\Security\\Security.LOSTPASSWORDHEADER', 'Lost Password'),
869
            $this->getTemplatesFor('lostpassword')
870
        );
871
    }
872
873
    /**
874
     * Show the "change password" page.
875
     * This page can either be called directly by logged-in users
876
     * (in which case they need to provide their old password),
877
     * or through a link emailed through {@link lostpassword()}.
878
     * In this case no old password is required, authentication is ensured
879
     * through the Member.AutoLoginHash property.
880
     *
881
     * @see ChangePasswordForm
882
     *
883
     * @return string|HTTPRequest Returns the "change password" page as HTML code, or a redirect response
884
     */
885
    public function changepassword()
886
    {
887
        $controller = $this->getResponseController(
888
            _t('SilverStripe\\Security\\Security.CHANGEPASSWORDHEADER',
889
            'Change your password')
890
        );
891
892
        // if the controller calls Director::redirect(), this will break early
893
        if (($response = $controller->getResponse()) && $response->isFinished()) {
894
            return $response;
895
        }
896
897
        // Extract the member from the URL.
898
        /** @var Member $member */
899
        $member = null;
900
        if (isset($_REQUEST['m'])) {
901
            $member = Member::get()->filter('ID', (int)$_REQUEST['m'])->first();
902
        }
903
904
        // Check whether we are merely changin password, or resetting.
905
        if (isset($_REQUEST['t']) && $member && $member->validateAutoLoginToken($_REQUEST['t'])) {
906
            // On first valid password reset request redirect to the same URL without hash to avoid referrer leakage.
907
908
            // if there is a current member, they should be logged out
909
            if ($curMember = static::getCurrentUser()) {
910
                /** @var LogoutHandler $handler */
911
                $handler = $this->getAuthenticator('default')->getLogoutHandler($this->Link('logout'));
912
                $handler->doLogOut($curMember);
913
            }
914
915
            // Store the hash for the change password form. Will be unset after reload within the ChangePasswordForm.
916
            Session::set('AutoLoginHash', $member->encryptWithUserSettings($_REQUEST['t']));
917
918
            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 918 which is incompatible with the return type documented by SilverStripe\Security\Security::changepassword of type string|SilverStripe\Control\HTTPRequest.
Loading history...
919
        } elseif (Session::get('AutoLoginHash')) {
920
            // Subsequent request after the "first load with hash" (see previous if clause).
921
            $customisedController = $controller->customise(
922
                array(
923
                    'Content' => DBField::create_field(
924
                        'HTMLFragment',
925
                        '<p>' . _t('SilverStripe\\Security\\Security.ENTERNEWPASSWORD',
926
                            'Please enter a new password.') . '</p>'
927
                    ),
928
                    'Form'    => $this->ChangePasswordForm(),
929
                ));
930
        } elseif (static::getCurrentUser()) {
931
            // Logged in user requested a password change form.
932
            $customisedController = $controller->customise(
933
                array(
934
                    'Content' => DBField::create_field(
935
                        'HTMLFragment',
936
                        '<p>' . _t('SilverStripe\\Security\\Security.CHANGEPASSWORDBELOW',
937
                            'You can change your password below.') . '</p>'
938
                    ),
939
                    'Form'    => $this->ChangePasswordForm()
940
                ));
941
        } else {
942
            // Show friendly message if it seems like the user arrived here via password reset feature.
943
            if (isset($_REQUEST['m']) || isset($_REQUEST['t'])) {
944
                $customisedController = $controller->customise(
945
                    array(
946
                        'Content' => DBField::create_field(
947
                            'HTMLFragment',
948
                            _t(
949
                                'SilverStripe\\Security\\Security.NOTERESETLINKINVALID',
950
                                '<p>The password reset link is invalid or expired.</p>'
951
                                . '<p>You can request a new one <a href="{link1}">here</a> or change your password after'
952
                                . ' you <a href="{link2}">logged in</a>.</p>',
953
                                [
954
                                    'link1' => $this->Link('lostpassword'),
955
                                    'link2' => $this->Link('login')
956
                                ]
957
                            )
958
                        )
959
                    )
960
                );
961
            } else {
962
                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 962 which is incompatible with the return type documented by SilverStripe\Security\Security::changepassword of type string|SilverStripe\Control\HTTPRequest.
Loading history...
963
                    $this,
964
                    _t('SilverStripe\\Security\\Security.ERRORPASSWORDPERMISSION',
965
                        'You must be logged in in order to change your password!')
966
                );
967
            }
968
        }
969
970
        return $customisedController->renderWith($this->getTemplatesFor('changepassword'));
971
    }
972
973
    /**
974
     * Create a link to the password reset form.
975
     *
976
     * GET parameters used:
977
     * - m: member ID
978
     * - t: plaintext token
979
     *
980
     * @param Member $member Member object associated with this link.
981
     * @param string $autologinToken The auto login token.
982
     * @return string
983
     */
984
    public static function getPasswordResetLink($member, $autologinToken)
985
    {
986
        $autologinToken = urldecode($autologinToken);
987
988
        return static::singleton()->Link('changepassword') . "?m={$member->ID}&t=$autologinToken";
989
    }
990
991
    /**
992
     * Factory method for the lost password form
993
     *
994
     * @skipUpgrade
995
     * @return MemberAuthenticator\ChangePasswordForm
996
     */
997
    public function ChangePasswordForm()
998
    {
999
        return MemberAuthenticator\ChangePasswordForm::create($this, 'ChangePasswordForm');
1000
    }
1001
1002
    /**
1003
     * Determine the list of templates to use for rendering the given action.
1004
     *
1005
     * @skipUpgrade
1006
     * @param string $action
1007
     * @return array Template list
1008
     */
1009
    public function getTemplatesFor($action)
1010
    {
1011
        $templates = SSViewer::get_templates_by_class(static::class, "_{$action}", __CLASS__);
1012
1013
        return array_merge(
1014
            $templates,
1015
            [
1016
                "Security_{$action}",
1017
                "Security",
1018
                $this->stat("template_main"),
1019
                "BlankPage"
1020
            ]
1021
        );
1022
    }
1023
1024
    /**
1025
     * Return an existing member with administrator privileges, or create one of necessary.
1026
     *
1027
     * Will create a default 'Administrators' group if no group is found
1028
     * with an ADMIN permission. Will create a new 'Admin' member with administrative permissions
1029
     * if no existing Member with these permissions is found.
1030
     *
1031
     * Important: Any newly created administrator accounts will NOT have valid
1032
     * login credentials (Email/Password properties), which means they can't be used for login
1033
     * purposes outside of any default credentials set through {@link Security::setDefaultAdmin()}.
1034
     *
1035
     * @return Member
1036
     */
1037
    public static function findAnAdministrator()
1038
    {
1039
        // coupling to subsites module
1040
        $origSubsite = null;
1041
        if (is_callable('Subsite::changeSubsite')) {
1042
            $origSubsite = Subsite::currentSubsiteID();
1043
            Subsite::changeSubsite(0);
1044
        }
1045
1046
        /** @var Member $member */
1047
        $member = null;
1048
1049
        // find a group with ADMIN permission
1050
        $adminGroup = Permission::get_groups_by_permission('ADMIN')->first();
1051
1052
        if (is_callable('Subsite::changeSubsite')) {
1053
            Subsite::changeSubsite($origSubsite);
1054
        }
1055
1056
        if ($adminGroup) {
1057
            $member = $adminGroup->Members()->First();
1058
        }
1059
1060
        if (!$adminGroup) {
1061
            Group::singleton()->requireDefaultRecords();
1062
            $adminGroup = Permission::get_groups_by_permission('ADMIN')->first();
1063
        }
1064
1065
        if (!$member) {
1066
            Member::singleton()->requireDefaultRecords();
1067
            $member = Permission::get_members_by_permission('ADMIN')->first();
1068
        }
1069
1070
        if (!$member) {
1071
            $member = Member::default_admin();
1072
        }
1073
1074
        if (!$member) {
1075
            // Failover to a blank admin
1076
            $member = Member::create();
1077
            $member->FirstName = _t('SilverStripe\\Security\\Member.DefaultAdminFirstname', 'Default Admin');
1078
            $member->write();
1079
            // Add member to group instead of adding group to member
1080
            // This bypasses the privilege escallation code in Member_GroupSet
1081
            $adminGroup
1082
                ->DirectMembers()
1083
                ->add($member);
1084
        }
1085
1086
        return $member;
1087
    }
1088
1089
    /**
1090
     * Flush the default admin credentials
1091
     */
1092
    public static function clear_default_admin()
1093
    {
1094
        self::$default_username = null;
1095
        self::$default_password = null;
1096
    }
1097
1098
1099
    /**
1100
     * Set a default admin in dev-mode
1101
     *
1102
     * This will set a static default-admin which is not existing
1103
     * as a database-record. By this workaround we can test pages in dev-mode
1104
     * with a unified login. Submitted login-credentials are first checked
1105
     * against this static information in {@link Security::authenticate()}.
1106
     *
1107
     * @param string $username The user name
1108
     * @param string $password The password (in cleartext)
1109
     * @return bool True if successfully set
1110
     */
1111
    public static function setDefaultAdmin($username, $password)
1112
    {
1113
        // don't overwrite if already set
1114
        if (self::$default_username || self::$default_password) {
1115
            return false;
1116
        }
1117
1118
        self::$default_username = $username;
1119
        self::$default_password = $password;
1120
1121
        return true;
1122
    }
1123
1124
    /**
1125
     * Checks if the passed credentials are matching the default-admin.
1126
     * Compares cleartext-password set through Security::setDefaultAdmin().
1127
     *
1128
     * @param string $username
1129
     * @param string $password
1130
     * @return bool
1131
     */
1132
    public static function check_default_admin($username, $password)
1133
    {
1134
        return (
1135
            self::$default_username === $username
1136
            && self::$default_password === $password
1137
            && self::has_default_admin()
1138
        );
1139
    }
1140
1141
    /**
1142
     * Check that the default admin account has been set.
1143
     */
1144
    public static function has_default_admin()
1145
    {
1146
        return !empty(self::$default_username) && !empty(self::$default_password);
1147
    }
1148
1149
    /**
1150
     * Get default admin username
1151
     *
1152
     * @return string
1153
     */
1154
    public static function default_admin_username()
1155
    {
1156
        return self::$default_username;
1157
    }
1158
1159
    /**
1160
     * Get default admin password
1161
     *
1162
     * @return string
1163
     */
1164
    public static function default_admin_password()
1165
    {
1166
        return self::$default_password;
1167
    }
1168
1169
    /**
1170
     * Encrypt a password according to the current password encryption settings.
1171
     * If the settings are so that passwords shouldn't be encrypted, the
1172
     * result is simple the clear text password with an empty salt except when
1173
     * a custom algorithm ($algorithm parameter) was passed.
1174
     *
1175
     * @param string $password The password to encrypt
1176
     * @param string $salt Optional: The salt to use. If it is not passed, but
1177
     *  needed, the method will automatically create a
1178
     *  random salt that will then be returned as return value.
1179
     * @param string $algorithm Optional: Use another algorithm to encrypt the
1180
     *  password (so that the encryption algorithm can be changed over the time).
1181
     * @param Member $member Optional
1182
     * @return mixed Returns an associative array containing the encrypted
1183
     *  password and the used salt in the form:
1184
     * <code>
1185
     *  array(
1186
     *  'password' => string,
1187
     *  'salt' => string,
1188
     *  'algorithm' => string,
1189
     *  'encryptor' => PasswordEncryptor instance
1190
     *  )
1191
     * </code>
1192
     * If the passed algorithm is invalid, FALSE will be returned.
1193
     *
1194
     * @see encrypt_passwords()
1195
     */
1196
    public static function encrypt_password($password, $salt = null, $algorithm = null, $member = null)
1197
    {
1198
        // Fall back to the default encryption algorithm
1199
        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...
1200
            $algorithm = self::config()->get('password_encryption_algorithm');
1201
        }
1202
1203
        $e = PasswordEncryptor::create_for_algorithm($algorithm);
1204
1205
        // New salts will only need to be generated if the password is hashed for the first time
1206
        $salt = ($salt) ? $salt : $e->salt($password);
1207
1208
        return array(
1209
            'password'  => $e->encrypt($password, $salt, $member),
1210
            'salt'      => $salt,
1211
            'algorithm' => $algorithm,
1212
            'encryptor' => $e
1213
        );
1214
    }
1215
1216
    /**
1217
     * Checks the database is in a state to perform security checks.
1218
     * See {@link DatabaseAdmin->init()} for more information.
1219
     *
1220
     * @return bool
1221
     */
1222
    public static function database_is_ready()
1223
    {
1224
        // Used for unit tests
1225
        if (self::$force_database_is_ready !== null) {
1226
            return self::$force_database_is_ready;
1227
        }
1228
1229
        if (self::$database_is_ready) {
1230
            return self::$database_is_ready;
1231
        }
1232
1233
        $requiredClasses = ClassInfo::dataClassesFor(Member::class);
1234
        $requiredClasses[] = Group::class;
1235
        $requiredClasses[] = Permission::class;
1236
        $schema = DataObject::getSchema();
1237
        foreach ($requiredClasses as $class) {
1238
            // Skip test classes, as not all test classes are scaffolded at once
1239
            if (is_a($class, TestOnly::class, true)) {
1240
                continue;
1241
            }
1242
1243
            // if any of the tables aren't created in the database
1244
            $table = $schema->tableName($class);
1245
            if (!ClassInfo::hasTable($table)) {
1246
                return false;
1247
            }
1248
1249
            // HACK: DataExtensions aren't applied until a class is instantiated for
1250
            // the first time, so create an instance here.
1251
            singleton($class);
1252
1253
            // if any of the tables don't have all fields mapped as table columns
1254
            $dbFields = DB::field_list($table);
1255
            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...
1256
                return false;
1257
            }
1258
1259
            $objFields = $schema->databaseFields($class, false);
1260
            $missingFields = array_diff_key($objFields, $dbFields);
1261
1262
            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...
1263
                return false;
1264
            }
1265
        }
1266
        self::$database_is_ready = true;
1267
1268
        return true;
1269
    }
1270
1271
    /**
1272
     * Resets the database_is_ready cache
1273
     */
1274
    public static function clear_database_is_ready()
1275
    {
1276
        self::$database_is_ready = null;
1277
        self::$force_database_is_ready = null;
1278
    }
1279
1280
    /**
1281
     * For the database_is_ready call to return a certain value - used for testing
1282
     */
1283
    public static function force_database_is_ready($isReady)
1284
    {
1285
        self::$force_database_is_ready = $isReady;
1286
    }
1287
1288
    /**
1289
     * Enable or disable recording of login attempts
1290
     * through the {@link LoginRecord} object.
1291
     *
1292
     * @deprecated 4.0 Use the "Security.login_recording" config setting instead
1293
     * @param boolean $bool
1294
     */
1295
    public static function set_login_recording($bool)
1296
    {
1297
        Deprecation::notice('4.0', 'Use the "Security.login_recording" config setting instead');
1298
        self::$login_recording = (bool)$bool;
1299
    }
1300
1301
    /**
1302
     * @deprecated 4.0 Use the "Security.login_recording" config setting instead
1303
     * @return boolean
1304
     */
1305
    public static function login_recording()
1306
    {
1307
        Deprecation::notice('4.0', 'Use the "Security.login_recording" config setting instead');
1308
1309
        return self::$login_recording;
1310
    }
1311
1312
    /**
1313
     * @config
1314
     * @var string Set the default login dest
1315
     * This is the URL that users will be redirected to after they log in,
1316
     * if they haven't logged in en route to access a secured page.
1317
     * By default, this is set to the homepage.
1318
     */
1319
    private static $default_login_dest = "";
1320
1321
    protected static $ignore_disallowed_actions = false;
1322
1323
    /**
1324
     * Set to true to ignore access to disallowed actions, rather than returning permission failure
1325
     * Note that this is just a flag that other code needs to check with Security::ignore_disallowed_actions()
1326
     * @param $flag True or false
1327
     */
1328
    public static function set_ignore_disallowed_actions($flag)
1329
    {
1330
        self::$ignore_disallowed_actions = $flag;
1331
    }
1332
1333
    public static function ignore_disallowed_actions()
1334
    {
1335
        return self::$ignore_disallowed_actions;
1336
    }
1337
1338
    /**
1339
     * Get the URL of the log-in page.
1340
     *
1341
     * To update the login url use the "Security.login_url" config setting.
1342
     *
1343
     * @return string
1344
     */
1345
    public static function login_url()
1346
    {
1347
        return Controller::join_links(Director::baseURL(), self::config()->get('login_url'));
1348
    }
1349
1350
1351
    /**
1352
     * Get the URL of the logout page.
1353
     *
1354
     * To update the logout url use the "Security.logout_url" config setting.
1355
     *
1356
     * @return string
1357
     */
1358
    public static function logout_url()
1359
    {
1360
        return Controller::join_links(Director::baseURL(), self::config()->get('logout_url'));
1361
    }
1362
1363
    /**
1364
     * Get the URL of the logout page.
1365
     *
1366
     * To update the logout url use the "Security.logout_url" config setting.
1367
     *
1368
     * @return string
1369
     */
1370
    public static function lost_password_url()
1371
    {
1372
        return Controller::join_links(Director::baseURL(), self::config()->get('lost_password_url'));
1373
    }
1374
1375
    /**
1376
     * Defines global accessible templates variables.
1377
     *
1378
     * @return array
1379
     */
1380
    public static function get_template_global_variables()
1381
    {
1382
        return array(
1383
            "LoginURL"        => "login_url",
1384
            "LogoutURL"       => "logout_url",
1385
            "LostPasswordURL" => "lost_password_url",
1386
            "CurrentMember"   => "getCurrentUser",
1387
            "currentUser"     => "getCurrentUser"
1388
        );
1389
    }
1390
}
1391