Completed
Push — master ( c17796...052b15 )
by Damian
01:29
created

Security::getAuthenticator()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 1
dl 0
loc 9
rs 9.6666
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Security;
4
5
use LogicException;
6
use Page;
0 ignored issues
show
Bug introduced by
The type Page was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
7
use ReflectionClass;
8
use SilverStripe\CMS\Controllers\ModelAsController;
0 ignored issues
show
Bug introduced by
The type SilverStripe\CMS\Controllers\ModelAsController was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
9
use SilverStripe\Control\Controller;
10
use SilverStripe\Control\Director;
11
use SilverStripe\Control\HTTPRequest;
12
use SilverStripe\Control\HTTPResponse;
13
use SilverStripe\Control\HTTPResponse_Exception;
14
use SilverStripe\Control\RequestHandler;
15
use SilverStripe\Core\ClassInfo;
16
use SilverStripe\Core\Convert;
17
use SilverStripe\Core\Injector\Injector;
18
use SilverStripe\Dev\Deprecation;
19
use SilverStripe\Dev\TestOnly;
20
use SilverStripe\Forms\Form;
21
use SilverStripe\ORM\ArrayList;
22
use SilverStripe\ORM\DataObject;
23
use SilverStripe\ORM\DB;
24
use SilverStripe\ORM\FieldType\DBField;
25
use SilverStripe\ORM\FieldType\DBHTMLText;
26
use SilverStripe\ORM\ValidationResult;
27
use SilverStripe\View\ArrayData;
28
use SilverStripe\View\SSViewer;
29
use SilverStripe\View\TemplateGlobalProvider;
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
introduced by
The private property $allowed_actions is not used, and could be removed.
Loading history...
38
        'index',
39
        'login',
40
        'logout',
41
        'basicauthlogin',
42
        'lostpassword',
43
        'passwordsent',
44
        'changepassword',
45
        'ping',
46
    );
47
48
    /**
49
     * If set to TRUE to prevent sharing of the session across several sites
50
     * in the domain.
51
     *
52
     * @config
53
     * @var bool
54
     */
55
    private static $strict_path_checking = false;
0 ignored issues
show
introduced by
The private property $strict_path_checking is not used, and could be removed.
Loading history...
56
57
    /**
58
     * The password encryption algorithm to use by default.
59
     * This is an arbitrary code registered through {@link PasswordEncryptor}.
60
     *
61
     * @config
62
     * @var string
63
     */
64
    private static $password_encryption_algorithm = 'blowfish';
0 ignored issues
show
introduced by
The private property $password_encryption_algorithm is not used, and could be removed.
Loading history...
65
66
    /**
67
     * Showing "Remember me"-checkbox
68
     * on loginform, and saving encrypted credentials to a cookie.
69
     *
70
     * @config
71
     * @var bool
72
     */
73
    private static $autologin_enabled = true;
0 ignored issues
show
introduced by
The private property $autologin_enabled is not used, and could be removed.
Loading history...
74
75
    /**
76
     * Determine if login username may be remembered between login sessions
77
     * If set to false this will disable auto-complete and prevent username persisting in the session
78
     *
79
     * @config
80
     * @var bool
81
     */
82
    private static $remember_username = true;
0 ignored issues
show
introduced by
The private property $remember_username is not used, and could be removed.
Loading history...
83
84
    /**
85
     * Location of word list to use for generating passwords
86
     *
87
     * @config
88
     * @var string
89
     */
90
    private static $word_list = './wordlist.txt';
0 ignored issues
show
introduced by
The private property $word_list is not used, and could be removed.
Loading history...
91
92
    /**
93
     * @config
94
     * @var string
95
     */
96
    private static $template = 'BlankPage';
0 ignored issues
show
introduced by
The private property $template is not used, and could be removed.
Loading history...
97
98
    /**
99
     * Template that is used to render the pages.
100
     *
101
     * @var string
102
     * @config
103
     */
104
    private static $template_main = 'Page';
0 ignored issues
show
introduced by
The private property $template_main is not used, and could be removed.
Loading history...
105
106
    /**
107
     * Class to use for page rendering
108
     *
109
     * @var string
110
     * @config
111
     */
112
    private static $page_class = Page::class;
0 ignored issues
show
introduced by
The private property $page_class is not used, and could be removed.
Loading history...
113
114
    /**
115
     * Default message set used in permission failures.
116
     *
117
     * @config
118
     * @var array|string
119
     */
120
    private static $default_message_set;
0 ignored issues
show
introduced by
The private property $default_message_set is not used, and could be removed.
Loading history...
121
122
    /**
123
     * The default login URL
124
     *
125
     * @config
126
     *
127
     * @var string
128
     */
129
    private static $login_url = 'Security/login';
0 ignored issues
show
introduced by
The private property $login_url is not used, and could be removed.
Loading history...
130
131
    /**
132
     * The default logout URL
133
     *
134
     * @config
135
     *
136
     * @var string
137
     */
138
    private static $logout_url = 'Security/logout';
0 ignored issues
show
introduced by
The private property $logout_url is not used, and could be removed.
Loading history...
139
140
    /**
141
     * The default lost password URL
142
     *
143
     * @config
144
     *
145
     * @var string
146
     */
147
    private static $lost_password_url = 'Security/lostpassword';
0 ignored issues
show
introduced by
The private property $lost_password_url is not used, and could be removed.
Loading history...
148
149
    /**
150
     * Value of X-Frame-Options header
151
     *
152
     * @config
153
     * @var string
154
     */
155
    private static $frame_options = 'SAMEORIGIN';
0 ignored issues
show
introduced by
The private property $frame_options is not used, and could be removed.
Loading history...
156
157
    /**
158
     * Value of the X-Robots-Tag header (for the Security section)
159
     *
160
     * @config
161
     * @var string
162
     */
163
    private static $robots_tag = 'noindex, nofollow';
0 ignored issues
show
introduced by
The private property $robots_tag is not used, and could be removed.
Loading history...
164
165
    /**
166
     * Enable or disable recording of login attempts
167
     * through the {@link LoginRecord} object.
168
     *
169
     * @config
170
     * @var boolean $login_recording
171
     */
172
    private static $login_recording = false;
0 ignored issues
show
introduced by
The private property $login_recording is not used, and could be removed.
Loading history...
173
174
    /**
175
     * @var boolean If set to TRUE or FALSE, {@link database_is_ready()}
176
     * will always return FALSE. Used for unit testing.
177
     */
178
    protected static $force_database_is_ready;
179
180
    /**
181
     * When the database has once been verified as ready, it will not do the
182
     * checks again.
183
     *
184
     * @var bool
185
     */
186
    protected static $database_is_ready = false;
187
188
    /**
189
     * @var Authenticator[] available authenticators
190
     */
191
    private $authenticators = [];
192
193
    /**
194
     * @var Member Currently logged in user (if available)
195
     */
196
    protected static $currentUser;
197
198
    /**
199
     * @return Authenticator[]
200
     */
201
    public function getAuthenticators()
202
    {
203
        return $this->authenticators;
204
    }
205
206
    /**
207
     * @param Authenticator[] $authenticators
208
     */
209
    public function setAuthenticators(array $authenticators)
210
    {
211
        $this->authenticators = $authenticators;
212
    }
213
214
    protected function init()
215
    {
216
        parent::init();
217
218
        // Prevent clickjacking, see https://developer.mozilla.org/en-US/docs/HTTP/X-Frame-Options
219
        $frameOptions = static::config()->get('frame_options');
220
        if ($frameOptions) {
221
            $this->getResponse()->addHeader('X-Frame-Options', $frameOptions);
222
        }
223
224
        // Prevent search engines from indexing the login page
225
        $robotsTag = static::config()->get('robots_tag');
226
        if ($robotsTag) {
227
            $this->getResponse()->addHeader('X-Robots-Tag', $robotsTag);
228
        }
229
    }
230
231
    public function index()
232
    {
233
        $this->httpError(404); // no-op
234
    }
235
236
    /**
237
     * Get the selected authenticator for this request
238
     *
239
     * @param string $name The identifier of the authenticator in your config
240
     * @return Authenticator Class name of Authenticator
241
     * @throws LogicException
242
     */
243
    protected function getAuthenticator($name = 'default')
244
    {
245
        $authenticators = $this->authenticators;
246
247
        if (isset($authenticators[$name])) {
248
            return $authenticators[$name];
249
        }
250
251
        throw new LogicException('No valid authenticator found');
252
    }
253
254
    /**
255
     * Get all registered authenticators
256
     *
257
     * @param int $service The type of service that is requested
258
     * @return Authenticator[] Return an array of Authenticator objects
259
     */
260
    public function getApplicableAuthenticators($service = Authenticator::LOGIN)
261
    {
262
        $authenticators = $this->getAuthenticators();
263
264
        /** @var Authenticator $authenticator */
265
        foreach ($authenticators as $name => $authenticator) {
266
            if (!($authenticator->supportedServices() & $service)) {
267
                unset($authenticators[$name]);
268
            }
269
        }
270
271
        if (empty($authenticators)) {
272
            throw new LogicException('No applicable authenticators found');
273
        }
274
275
        return $authenticators;
276
    }
277
278
    /**
279
     * Check if a given authenticator is registered
280
     *
281
     * @param string $authenticator The configured identifier of the authenicator
282
     * @return bool Returns TRUE if the authenticator is registered, FALSE
283
     *              otherwise.
284
     */
285
    public function hasAuthenticator($authenticator)
286
    {
287
        $authenticators = $this->authenticators;
288
289
        return !empty($authenticators[$authenticator]);
290
    }
291
292
    /**
293
     * Register that we've had a permission failure trying to view the given page
294
     *
295
     * This will redirect to a login page.
296
     * If you don't provide a messageSet, a default will be used.
297
     *
298
     * @param Controller $controller The controller that you were on to cause the permission
299
     *                               failure.
300
     * @param string|array $messageSet The message to show to the user. This
301
     *                                 can be a string, or a map of different
302
     *                                 messages for different contexts.
303
     *                                 If you pass an array, you can use the
304
     *                                 following keys:
305
     *                                   - default: The default message
306
     *                                   - alreadyLoggedIn: The message to
307
     *                                                      show if the user
308
     *                                                      is already logged
309
     *                                                      in and lacks the
310
     *                                                      permission to
311
     *                                                      access the item.
312
     *
313
     * The alreadyLoggedIn value can contain a '%s' placeholder that will be replaced with a link
314
     * to log in.
315
     * @return HTTPResponse
316
     */
317
    public static function permissionFailure($controller = null, $messageSet = null)
318
    {
319
        self::set_ignore_disallowed_actions(true);
320
321
        // Parse raw message / escape type
322
        $parseMessage = function ($message) {
323
            if ($message instanceof DBField) {
324
                return [
325
                    $message->getValue(),
326
                    $message->config()->get('escape_type') === 'raw'
327
                        ? ValidationResult::CAST_TEXT
328
                        : ValidationResult::CAST_HTML,
329
                ];
330
            }
331
332
            // Default to escaped value
333
            return [
334
                $message,
335
                ValidationResult::CAST_TEXT,
336
            ];
337
        };
338
339
        if (!$controller && Controller::has_curr()) {
340
            $controller = Controller::curr();
341
        }
342
343
        if (Director::is_ajax()) {
344
            $response = ($controller) ? $controller->getResponse() : new HTTPResponse();
345
            $response->setStatusCode(403);
346
            if (!static::getCurrentUser()) {
347
                $response->setBody(
348
                    _t('SilverStripe\\CMS\\Controllers\\ContentController.NOTLOGGEDIN', 'Not logged in')
349
                );
350
                $response->setStatusDescription(
351
                    _t('SilverStripe\\CMS\\Controllers\\ContentController.NOTLOGGEDIN', 'Not logged in')
352
                );
353
                // Tell the CMS to allow re-authentication
354
                if (CMSSecurity::singleton()->enabled()) {
355
                    $response->addHeader('X-Reauthenticate', '1');
356
                }
357
            }
358
359
            return $response;
360
        }
361
362
        // Prepare the messageSet provided
363
        if (!$messageSet) {
364
            if ($configMessageSet = static::config()->get('default_message_set')) {
365
                $messageSet = $configMessageSet;
366
            } else {
367
                $messageSet = array(
368
                    'default' => _t(
369
                        __CLASS__ . '.NOTEPAGESECURED',
370
                        "That page is secured. Enter your credentials below and we will send "
371
                            . "you right along."
372
                    ),
373
                    'alreadyLoggedIn' => _t(
374
                        __CLASS__ . '.ALREADYLOGGEDIN',
375
                        "You don't have access to this page.  If you have another account that "
376
                            . "can access that page, you can log in again below."
377
                    )
378
                );
379
            }
380
        }
381
382
        if (!is_array($messageSet)) {
383
            $messageSet = array('default' => $messageSet);
384
        }
385
386
        $member = static::getCurrentUser();
387
388
        // Work out the right message to show
389
        if ($member && $member->exists()) {
390
            $response = ($controller) ? $controller->getResponse() : new HTTPResponse();
391
            $response->setStatusCode(403);
392
393
            //If 'alreadyLoggedIn' is not specified in the array, then use the default
394
            //which should have been specified in the lines above
395
            if (isset($messageSet['alreadyLoggedIn'])) {
396
                $message = $messageSet['alreadyLoggedIn'];
397
            } else {
398
                $message = $messageSet['default'];
399
            }
400
401
            list($messageText, $messageCast) = $parseMessage($message);
402
            static::singleton()->setSessionMessage($messageText, ValidationResult::TYPE_WARNING, $messageCast);
403
            $request = new HTTPRequest('GET', '/');
404
            if ($controller) {
405
                $request->setSession($controller->getRequest()->getSession());
406
            }
407
            $loginResponse = static::singleton()->login($request);
408
            if ($loginResponse instanceof HTTPResponse) {
409
                return $loginResponse;
410
            }
411
412
            $response->setBody((string)$loginResponse);
413
414
            $controller->extend('permissionDenied', $member);
415
416
            return $response;
417
        } else {
418
            $message = $messageSet['default'];
419
        }
420
421
        list($messageText, $messageCast) = $parseMessage($message);
422
        static::singleton()->setSessionMessage($messageText, ValidationResult::TYPE_WARNING, $messageCast);
423
424
        $controller->getRequest()->getSession()->set("BackURL", $_SERVER['REQUEST_URI']);
425
426
        // TODO AccessLogEntry needs an extension to handle permission denied errors
427
        // Audit logging hook
428
        $controller->extend('permissionDenied', $member);
429
430
        return $controller->redirect(Controller::join_links(
431
            Security::config()->uninherited('login_url'),
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
432
            "?BackURL=" . urlencode($_SERVER['REQUEST_URI'])
433
        ));
434
    }
435
436
    /**
437
     * @param null|Member $currentUser
438
     */
439
    public static function setCurrentUser($currentUser = null)
440
    {
441
        self::$currentUser = $currentUser;
442
    }
443
444
    /**
445
     * @return null|Member
446
     */
447
    public static function getCurrentUser()
448
    {
449
        return self::$currentUser;
450
    }
451
452
    /**
453
     * Get the login forms for all available authentication methods
454
     *
455
     * @deprecated 5.0.0 Now handled by {@link static::delegateToMultipleHandlers}
456
     *
457
     * @return array Returns an array of available login forms (array of Form
458
     *               objects).
459
     *
460
     */
461
    public function getLoginForms()
462
    {
463
        Deprecation::notice('5.0.0', 'Now handled by delegateToMultipleHandlers');
464
465
        return array_map(
466
            function (Authenticator $authenticator) {
467
                return [
468
                    $authenticator->getLoginHandler($this->Link())->loginForm()
469
                ];
470
            },
471
            $this->getApplicableAuthenticators()
472
        );
473
    }
474
475
476
    /**
477
     * Get a link to a security action
478
     *
479
     * @param string $action Name of the action
480
     * @return string Returns the link to the given action
481
     */
482
    public function Link($action = null)
483
    {
484
        /** @skipUpgrade */
485
        return Controller::join_links(Director::baseURL(), "Security", $action);
486
    }
487
488
    /**
489
     * This action is available as a keep alive, so user
490
     * sessions don't timeout. A common use is in the admin.
491
     */
492
    public function ping()
493
    {
494
        return 1;
495
    }
496
497
    /**
498
     * Perform pre-login checking and prepare a response if available prior to login
499
     *
500
     * @return HTTPResponse Substitute response object if the login process should be curcumvented.
501
     * Returns null if should proceed as normal.
502
     */
503
    protected function preLogin()
504
    {
505
        // Event handler for pre-login, with an option to let it break you out of the login form
506
        $eventResults = $this->extend('onBeforeSecurityLogin');
507
        // If there was a redirection, return
508
        if ($this->redirectedTo()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->redirectedTo() 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...
509
            return $this->getResponse();
510
        }
511
        // If there was an HTTPResponse object returned, then return that
512
        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...
513
            foreach ($eventResults as $result) {
514
                if ($result instanceof HTTPResponse) {
515
                    return $result;
516
                }
517
            }
518
        }
519
520
        // If arriving on the login page already logged in, with no security error, and a ReturnURL then redirect
521
        // back. The login message check is neccesary to prevent infinite loops where BackURL links to
522
        // an action that triggers Security::permissionFailure.
523
        // This step is necessary in cases such as automatic redirection where a user is authenticated
524
        // upon landing on an SSL secured site and is automatically logged in, or some other case
525
        // where the user has permissions to continue but is not given the option.
526
        if (!$this->getSessionMessage()
527
            && ($member = static::getCurrentUser())
528
            && $member->exists()
529
            && $this->getRequest()->requestVar('BackURL')
530
        ) {
531
            return $this->redirectBack();
532
        }
533
534
        return null;
535
    }
536
537
    public function getRequest()
538
    {
539
        // Support Security::singleton() where a request isn't always injected
540
        $request = parent::getRequest();
541
        if ($request) {
542
            return $request;
543
        }
544
545
        if (Controller::has_curr() && Controller::curr() !== $this) {
546
            return Controller::curr()->getRequest();
547
        }
548
549
        return null;
550
    }
551
552
    /**
553
     * Prepare the controller for handling the response to this request
554
     *
555
     * @param string $title Title to use
556
     * @return Controller
557
     */
558
    protected function getResponseController($title)
559
    {
560
        // Use the default setting for which Page to use to render the security page
561
        $pageClass = $this->config()->get('page_class');
562
        if (!$pageClass || !class_exists($pageClass)) {
563
            return $this;
564
        }
565
566
        // Create new instance of page holder
567
        /** @var Page $holderPage */
568
        $holderPage = Injector::inst()->create($pageClass);
569
        $holderPage->Title = $title;
570
        /** @skipUpgrade */
571
        $holderPage->URLSegment = 'Security';
572
        // Disable ID-based caching  of the log-in page by making it a random number
573
        $holderPage->ID = -1 * random_int(1, 10000000);
574
575
        $controller = ModelAsController::controller_for($holderPage);
576
        $controller->doInit();
577
        $controller->setRequest($this->getRequest());
578
579
        return $controller;
580
    }
581
582
    /**
583
     * Combine the given forms into a formset with a tabbed interface
584
     *
585
     * @param array|Form[] $forms
586
     * @return string
587
     */
588
    protected function generateTabbedFormSet($forms)
589
    {
590
        if (count($forms) === 1) {
591
            return $forms;
592
        }
593
594
        $viewData = new ArrayData([
595
            'Forms' => new ArrayList($forms),
596
        ]);
597
598
        return $viewData->renderWith(
599
            $this->getTemplatesFor('MultiAuthenticatorTabbedForms')
600
        );
601
    }
602
603
    /**
604
     * Get the HTML Content for the $Content area during login
605
     *
606
     * @param string &$messageType Type of message, if available, passed back to caller
607
     * @return string Message in HTML format
608
     */
609
    protected function getSessionMessage(&$messageType = null)
610
    {
611
        $session = $this->getRequest()->getSession();
612
        $message = $session->get('Security.Message.message');
613
        $messageType = null;
614
        if (empty($message)) {
615
            return null;
616
        }
617
618
        $messageType = $session->get('Security.Message.type');
619
        $messageCast = $session->get('Security.Message.cast');
620
        if ($messageCast !== ValidationResult::CAST_HTML) {
621
            $message = Convert::raw2xml($message);
622
        }
623
624
        return sprintf('<p class="message %s">%s</p>', Convert::raw2att($messageType), $message);
0 ignored issues
show
Bug introduced by
It seems like SilverStripe\Core\Convert::raw2att($messageType) can also be of type array; however, parameter $args of sprintf() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

624
        return sprintf('<p class="message %s">%s</p>', /** @scrutinizer ignore-type */ Convert::raw2att($messageType), $message);
Loading history...
625
    }
626
627
    /**
628
     * Set the next message to display for the security login page. Defaults to warning
629
     *
630
     * @param string $message Message
631
     * @param string $messageType Message type. One of ValidationResult::TYPE_*
632
     * @param string $messageCast Message cast. One of ValidationResult::CAST_*
633
     */
634
    public function setSessionMessage(
635
        $message,
636
        $messageType = ValidationResult::TYPE_WARNING,
637
        $messageCast = ValidationResult::CAST_TEXT
638
    ) {
639
        Controller::curr()
640
            ->getRequest()
641
            ->getSession()
642
            ->set("Security.Message.message", $message)
643
            ->set("Security.Message.type", $messageType)
644
            ->set("Security.Message.cast", $messageCast);
645
    }
646
647
    /**
648
     * Clear login message
649
     */
650
    public static function clearSessionMessage()
651
    {
652
        Controller::curr()
653
            ->getRequest()
654
            ->getSession()
655
            ->clear("Security.Message");
656
    }
657
658
659
    /**
660
     * Show the "login" page
661
     *
662
     * For multiple authenticators, Security_MultiAuthenticatorLogin is used.
663
     * See getTemplatesFor and getIncludeTemplate for how to override template logic
664
     *
665
     * @param null|HTTPRequest $request
666
     * @param int $service
667
     * @return HTTPResponse|string Returns the "login" page as HTML code.
668
     * @throws HTTPResponse_Exception
669
     */
670
    public function login($request = null, $service = Authenticator::LOGIN)
671
    {
672
        if ($request) {
673
            $this->setRequest($request);
674
        } elseif ($request) {
675
            $request = $this->getRequest();
676
        } else {
677
            throw new HTTPResponse_Exception("No request available", 500);
678
        }
679
680
        // Check pre-login process
681
        if ($response = $this->preLogin()) {
682
            return $response;
683
        }
684
        $authName = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $authName is dead and can be removed.
Loading history...
685
686
        $handlers = $this->getServiceAuthenticatorsFromRequest($service, $request);
687
688
        $link = $this->Link('login');
689
        array_walk(
690
            $handlers,
691
            function (Authenticator &$auth, $name) use ($link) {
692
                $auth = $auth->getLoginHandler(Controller::join_links($link, $name));
693
            }
694
        );
695
696
        return $this->delegateToMultipleHandlers(
697
            $handlers,
698
            _t(__CLASS__.'.LOGIN', 'Log in'),
699
            $this->getTemplatesFor('login'),
700
            [$this, 'aggregateTabbedForms']
701
        );
702
    }
703
704
    /**
705
     * Log the currently logged in user out
706
     *
707
     * Logging out without ID-parameter in the URL, will log the user out of all applicable Authenticators.
708
     *
709
     * Adding an ID will only log the user out of that Authentication method.
710
     *
711
     * @param null|HTTPRequest $request
712
     * @param int $service
713
     * @return HTTPResponse|string
714
     */
715
    public function logout($request = null, $service = Authenticator::LOGOUT)
716
    {
717
        $authName = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $authName is dead and can be removed.
Loading history...
718
719
        if (!$request) {
720
            $request = $this->getRequest();
721
        }
722
723
        $handlers = $this->getServiceAuthenticatorsFromRequest($service, $request);
724
725
        $link = $this->Link('logout');
726
        array_walk(
727
            $handlers,
728
            function (Authenticator &$auth, $name) use ($link) {
729
                $auth = $auth->getLogoutHandler(Controller::join_links($link, $name));
730
            }
731
        );
732
733
        return $this->delegateToMultipleHandlers(
734
            $handlers,
735
            _t(__CLASS__.'.LOGOUT', 'Log out'),
736
            $this->getTemplatesFor('logout'),
737
            [$this, 'aggregateAuthenticatorResponses']
738
        );
739
    }
740
741
    /**
742
     * Get authenticators for the given service, optionally filtered by the ID parameter
743
     * of the current request
744
     *
745
     * @param int $service
746
     * @param HTTPRequest $request
747
     * @return array|Authenticator[]
748
     * @throws HTTPResponse_Exception
749
     */
750
    protected function getServiceAuthenticatorsFromRequest($service, HTTPRequest $request)
751
    {
752
        $authName = null;
753
754
        if ($request->param('ID')) {
755
            $authName = $request->param('ID');
756
        }
757
758
        // Delegate to a single named handler - e.g. Security/login/<authname>/
759
        if ($authName && $this->hasAuthenticator($authName)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $authName 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...
760
            if ($request) {
761
                $request->shift();
762
            }
763
764
            $authenticator = $this->getAuthenticator($authName);
765
766
            if (!$authenticator->supportedServices() & $service) {
767
                // Try to be helpful and show the service constant name, e.g. Authenticator::LOGIN
768
                $constants = array_flip((new ReflectionClass(Authenticator::class))->getConstants());
769
770
                $message = 'Invalid Authenticator "' . $authName . '" for ';
771
                if (array_key_exists($service, $constants)) {
772
                    $message .= 'service: Authenticator::' . $constants[$service];
773
                } else {
774
                    $message .= 'unknown authenticator service';
775
                }
776
777
                throw new HTTPResponse_Exception($message, 400);
778
            }
779
780
            $handlers = [$authName => $authenticator];
781
        } else {
782
            // Delegate to all of them, building a tabbed view - e.g. Security/login/
783
            $handlers = $this->getApplicableAuthenticators($service);
784
        }
785
786
        return $handlers;
787
    }
788
789
    /**
790
     * Aggregate tabbed forms from each handler to fragments ready to be rendered.
791
     *
792
     * @skipUpgrade
793
     * @param array $results
794
     * @return array
795
     */
796
    protected function aggregateTabbedForms(array $results)
797
    {
798
        $forms = [];
799
        foreach ($results as $authName => $singleResult) {
800
            // The result *must* be an array with a Form key
801
            if (!is_array($singleResult) || !isset($singleResult['Form'])) {
802
                user_error('Authenticator "' . $authName . '" doesn\'t support tabbed forms', E_USER_WARNING);
803
                continue;
804
            }
805
806
            $forms[] = $singleResult['Form'];
807
        }
808
809
        if (!$forms) {
810
            throw new \LogicException('No authenticators found compatible with tabbed forms');
811
        }
812
813
        return [
814
            'Forms' => ArrayList::create($forms),
815
            'Form' => $this->generateTabbedFormSet($forms)
816
        ];
817
    }
818
819
    /**
820
     * We have three possible scenarios.
821
     * We get back Content (e.g. Password Reset)
822
     * We get back a Form (no token set for logout)
823
     * We get back a HTTPResponse, telling us to redirect.
824
     * Return the first one, which is the default response, as that covers all required scenarios
825
     *
826
     * @param array $results
827
     * @return array|HTTPResponse
828
     */
829
    protected function aggregateAuthenticatorResponses($results)
830
    {
831
        $error = false;
832
        $result = null;
833
        foreach ($results as $authName => $singleResult) {
834
            if (($singleResult instanceof HTTPResponse) ||
835
                (is_array($singleResult) &&
836
                    (isset($singleResult['Content']) || isset($singleResult['Form'])))
837
            ) {
838
                // return the first successful response
839
                return $singleResult;
840
            } else {
841
                // Not a valid response
842
                $error = true;
843
            }
844
        }
845
846
        if ($error) {
847
            throw new \LogicException('No authenticators found compatible with logout operation');
848
        }
849
850
        return $result;
851
    }
852
853
    /**
854
     * Delegate to a number of handlers and aggregate the results. This is used, for example, to
855
     * build the log-in page where there are multiple authenticators active.
856
     *
857
     * If a single handler is passed, delegateToHandler() will be called instead
858
     *
859
     * @param array|RequestHandler[] $handlers
860
     * @param string $title The title of the form
861
     * @param array $templates
862
     * @param callable $aggregator
863
     * @return array|HTTPResponse|RequestHandler|DBHTMLText|string
864
     */
865
    protected function delegateToMultipleHandlers(array $handlers, $title, array $templates, callable $aggregator)
866
    {
867
868
        // Simpler case for a single authenticator
869
        if (count($handlers) === 1) {
870
            return $this->delegateToHandler(array_values($handlers)[0], $title, $templates);
871
        }
872
873
        // Process each of the handlers
874
        $results = array_map(
875
            function (RequestHandler $handler) {
876
                return $handler->handleRequest($this->getRequest());
877
            },
878
            $handlers
879
        );
880
881
        $response = call_user_func_array($aggregator, [$results]);
882
        // The return could be a HTTPResponse, in which we don't want to call the render
883
        if (is_array($response)) {
884
            return $this->renderWrappedController($title, $response, $templates);
885
        }
886
887
        return $response;
888
    }
889
890
    /**
891
     * Delegate to another RequestHandler, rendering any fragment arrays into an appropriate.
892
     * controller.
893
     *
894
     * @param RequestHandler $handler
895
     * @param string $title The title of the form
896
     * @param array $templates
897
     * @return array|HTTPResponse|RequestHandler|DBHTMLText|string
898
     */
899
    protected function delegateToHandler(RequestHandler $handler, $title, array $templates = [])
900
    {
901
        $result = $handler->handleRequest($this->getRequest());
902
903
        // Return the customised controller - may be used to render a Form (e.g. login form)
904
        if (is_array($result)) {
905
            $result = $this->renderWrappedController($title, $result, $templates);
906
        }
907
908
        return $result;
909
    }
910
911
    /**
912
     * Render the given fragments into a security page controller with the given title.
913
     *
914
     * @param string $title string The title to give the security page
915
     * @param array $fragments A map of objects to render into the page, e.g. "Form"
916
     * @param array $templates An array of templates to use for the render
917
     * @return HTTPResponse|DBHTMLText
918
     */
919
    protected function renderWrappedController($title, array $fragments, array $templates)
920
    {
921
        $controller = $this->getResponseController($title);
922
923
        // if the controller calls Director::redirect(), this will break early
924
        if (($response = $controller->getResponse()) && $response->isFinished()) {
925
            return $response;
926
        }
927
928
        // Handle any form messages from validation, etc.
929
        $messageType = '';
930
        $message = $this->getSessionMessage($messageType);
931
932
        // We've displayed the message in the form output, so reset it for the next run.
933
        static::clearSessionMessage();
934
935
        if ($message) {
936
            $messageResult = [
937
                'Content'     => DBField::create_field('HTMLFragment', $message),
938
                'Message'     => DBField::create_field('HTMLFragment', $message),
939
                'MessageType' => $messageType
940
            ];
941
            $fragments = array_merge($fragments, $messageResult);
942
        }
943
944
        return $controller->customise($fragments)->renderWith($templates);
945
    }
946
947
    public function basicauthlogin()
948
    {
949
        $member = BasicAuth::requireLogin($this->getRequest(), 'SilverStripe login', 'ADMIN');
950
        static::setCurrentUser($member);
951
    }
952
953
    /**
954
     * Show the "lost password" page
955
     *
956
     * @return string Returns the "lost password" page as HTML code.
957
     */
958
    public function lostpassword()
959
    {
960
        $handlers = [];
961
        $authenticators = $this->getApplicableAuthenticators(Authenticator::RESET_PASSWORD);
962
        /** @var Authenticator $authenticator */
963
        foreach ($authenticators as $authenticator) {
964
            $handlers[] = $authenticator->getLostPasswordHandler(
965
                Controller::join_links($this->Link(), 'lostpassword')
966
            );
967
        }
968
969
        return $this->delegateToMultipleHandlers(
970
            $handlers,
971
            _t('SilverStripe\\Security\\Security.LOSTPASSWORDHEADER', 'Lost Password'),
972
            $this->getTemplatesFor('lostpassword'),
973
            [$this, 'aggregateAuthenticatorResponses']
974
        );
975
    }
976
977
    /**
978
     * Show the "change password" page.
979
     * This page can either be called directly by logged-in users
980
     * (in which case they need to provide their old password),
981
     * or through a link emailed through {@link lostpassword()}.
982
     * In this case no old password is required, authentication is ensured
983
     * through the Member.AutoLoginHash property.
984
     *
985
     * @see ChangePasswordForm
986
     *
987
     * @return string|HTTPRequest Returns the "change password" page as HTML code, or a redirect response
988
     */
989
    public function changepassword()
990
    {
991
        /** @var array|Authenticator[] $authenticators */
992
        $authenticators = $this->getApplicableAuthenticators(Authenticator::CHANGE_PASSWORD);
993
        $handlers = [];
994
        foreach ($authenticators as $authenticator) {
995
            $handlers[] = $authenticator->getChangePasswordHandler($this->Link('changepassword'));
996
        }
997
998
        return $this->delegateToMultipleHandlers(
999
            $handlers,
1000
            _t('SilverStripe\\Security\\Security.CHANGEPASSWORDHEADER', 'Change your password'),
1001
            $this->getTemplatesFor('changepassword'),
1002
            [$this, 'aggregateAuthenticatorResponses']
1003
        );
1004
    }
1005
1006
    /**
1007
     * Create a link to the password reset form.
1008
     *
1009
     * GET parameters used:
1010
     * - m: member ID
1011
     * - t: plaintext token
1012
     *
1013
     * @param Member $member Member object associated with this link.
1014
     * @param string $autologinToken The auto login token.
1015
     * @return string
1016
     */
1017
    public static function getPasswordResetLink($member, $autologinToken)
1018
    {
1019
        $autologinToken = urldecode($autologinToken);
1020
1021
        return static::singleton()->Link('changepassword') . "?m={$member->ID}&t=$autologinToken";
1022
    }
1023
1024
    /**
1025
     * Determine the list of templates to use for rendering the given action.
1026
     *
1027
     * @skipUpgrade
1028
     * @param string $action
1029
     * @return array Template list
1030
     */
1031
    public function getTemplatesFor($action)
1032
    {
1033
        $templates = SSViewer::get_templates_by_class(static::class, "_{$action}", __CLASS__);
1034
1035
        return array_merge(
1036
            $templates,
1037
            [
1038
                "Security_{$action}",
1039
                "Security",
1040
                $this->config()->get("template_main"),
1041
                "BlankPage"
1042
            ]
1043
        );
1044
    }
1045
1046
    /**
1047
     * Return an existing member with administrator privileges, or create one of necessary.
1048
     *
1049
     * Will create a default 'Administrators' group if no group is found
1050
     * with an ADMIN permission. Will create a new 'Admin' member with administrative permissions
1051
     * if no existing Member with these permissions is found.
1052
     *
1053
     * Important: Any newly created administrator accounts will NOT have valid
1054
     * login credentials (Email/Password properties), which means they can't be used for login
1055
     * purposes outside of any default credentials set through {@link Security::setDefaultAdmin()}.
1056
     *
1057
     * @return Member
1058
     *
1059
     * @deprecated 4.0.0..5.0.0 Please use DefaultAdminService::findOrCreateDefaultAdmin()
1060
     */
1061
    public static function findAnAdministrator()
1062
    {
1063
        Deprecation::notice('5.0.0', 'Please use DefaultAdminService::findOrCreateDefaultAdmin()');
1064
1065
        $service = DefaultAdminService::singleton();
1066
        return $service->findOrCreateDefaultAdmin();
1067
    }
1068
1069
    /**
1070
     * Flush the default admin credentials
1071
     *
1072
     * @deprecated 4.0.0..5.0.0 Please use DefaultAdminService::clearDefaultAdmin()
1073
     */
1074
    public static function clear_default_admin()
1075
    {
1076
        Deprecation::notice('5.0.0', 'Please use DefaultAdminService::clearDefaultAdmin()');
1077
1078
        DefaultAdminService::clearDefaultAdmin();
1079
    }
1080
1081
    /**
1082
     * Set a default admin in dev-mode
1083
     *
1084
     * This will set a static default-admin which is not existing
1085
     * as a database-record. By this workaround we can test pages in dev-mode
1086
     * with a unified login. Submitted login-credentials are first checked
1087
     * against this static information in {@link Security::authenticate()}.
1088
     *
1089
     * @param string $username The user name
1090
     * @param string $password The password (in cleartext)
1091
     * @return bool True if successfully set
1092
     *
1093
     * @deprecated 4.0.0..5.0.0 Please use DefaultAdminService::setDefaultAdmin($username, $password)
1094
     */
1095
    public static function setDefaultAdmin($username, $password)
1096
    {
1097
        Deprecation::notice('5.0.0', 'Please use DefaultAdminService::setDefaultAdmin($username, $password)');
1098
1099
        DefaultAdminService::setDefaultAdmin($username, $password);
1100
        return true;
1101
    }
1102
1103
    /**
1104
     * Checks if the passed credentials are matching the default-admin.
1105
     * Compares cleartext-password set through Security::setDefaultAdmin().
1106
     *
1107
     * @param string $username
1108
     * @param string $password
1109
     * @return bool
1110
     *
1111
     * @deprecated 4.0.0..5.0.0 Use DefaultAdminService::isDefaultAdminCredentials() instead
1112
     */
1113
    public static function check_default_admin($username, $password)
1114
    {
1115
        Deprecation::notice('5.0.0', 'Please use DefaultAdminService::isDefaultAdminCredentials($username, $password)');
1116
1117
        /** @var DefaultAdminService $service */
1118
        return DefaultAdminService::isDefaultAdminCredentials($username, $password);
1119
    }
1120
1121
    /**
1122
     * Check that the default admin account has been set.
1123
     *
1124
     * @deprecated 4.0.0..5.0.0 Use DefaultAdminService::hasDefaultAdmin() instead
1125
     */
1126
    public static function has_default_admin()
1127
    {
1128
        Deprecation::notice('5.0.0', 'Please use DefaultAdminService::hasDefaultAdmin()');
1129
1130
        return DefaultAdminService::hasDefaultAdmin();
1131
    }
1132
1133
    /**
1134
     * Get default admin username
1135
     *
1136
     * @deprecated 4.0.0..5.0.0 Use DefaultAdminService::getDefaultAdminUsername()
1137
     * @return string
1138
     */
1139
    public static function default_admin_username()
1140
    {
1141
        Deprecation::notice('5.0.0', 'Please use DefaultAdminService::getDefaultAdminUsername()');
1142
1143
        return DefaultAdminService::getDefaultAdminUsername();
1144
    }
1145
1146
    /**
1147
     * Get default admin password
1148
     *
1149
     * @deprecated 4.0.0..5.0.0 Use DefaultAdminService::getDefaultAdminPassword()
1150
     * @return string
1151
     */
1152
    public static function default_admin_password()
1153
    {
1154
        Deprecation::notice('5.0.0', 'Please use DefaultAdminService::getDefaultAdminPassword()');
1155
1156
        return DefaultAdminService::getDefaultAdminPassword();
1157
    }
1158
1159
    /**
1160
     * Encrypt a password according to the current password encryption settings.
1161
     * If the settings are so that passwords shouldn't be encrypted, the
1162
     * result is simple the clear text password with an empty salt except when
1163
     * a custom algorithm ($algorithm parameter) was passed.
1164
     *
1165
     * @param string $password The password to encrypt
1166
     * @param string $salt Optional: The salt to use. If it is not passed, but
1167
     *  needed, the method will automatically create a
1168
     *  random salt that will then be returned as return value.
1169
     * @param string $algorithm Optional: Use another algorithm to encrypt the
1170
     *  password (so that the encryption algorithm can be changed over the time).
1171
     * @param Member $member Optional
1172
     * @return mixed Returns an associative array containing the encrypted
1173
     *  password and the used salt in the form:
1174
     * <code>
1175
     *  array(
1176
     *  'password' => string,
1177
     *  'salt' => string,
1178
     *  'algorithm' => string,
1179
     *  'encryptor' => PasswordEncryptor instance
1180
     *  )
1181
     * </code>
1182
     * If the passed algorithm is invalid, FALSE will be returned.
1183
     *
1184
     * @throws PasswordEncryptor_NotFoundException
1185
     * @see encrypt_passwords()
1186
     */
1187
    public static function encrypt_password($password, $salt = null, $algorithm = null, $member = null)
1188
    {
1189
        // Fall back to the default encryption algorithm
1190
        if (!$algorithm) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $algorithm 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...
1191
            $algorithm = self::config()->get('password_encryption_algorithm');
1192
        }
1193
1194
        $encryptor = PasswordEncryptor::create_for_algorithm($algorithm);
1195
1196
        // New salts will only need to be generated if the password is hashed for the first time
1197
        $salt = ($salt) ? $salt : $encryptor->salt($password);
1198
1199
        return [
1200
            'password'  => $encryptor->encrypt($password, $salt, $member),
1201
            'salt' => $salt,
1202
            'algorithm' => $algorithm,
1203
            'encryptor' => $encryptor
1204
        ];
1205
    }
1206
1207
    /**
1208
     * Checks the database is in a state to perform security checks.
1209
     * See {@link DatabaseAdmin->init()} for more information.
1210
     *
1211
     * @return bool
1212
     */
1213
    public static function database_is_ready()
1214
    {
1215
        // Used for unit tests
1216
        if (self::$force_database_is_ready !== null) {
1217
            return self::$force_database_is_ready;
1218
        }
1219
1220
        if (self::$database_is_ready) {
1221
            return self::$database_is_ready;
1222
        }
1223
1224
        $requiredClasses = ClassInfo::dataClassesFor(Member::class);
1225
        $requiredClasses[] = Group::class;
1226
        $requiredClasses[] = Permission::class;
1227
        $schema = DataObject::getSchema();
1228
        foreach ($requiredClasses as $class) {
1229
            // Skip test classes, as not all test classes are scaffolded at once
1230
            if (is_a($class, TestOnly::class, true)) {
1231
                continue;
1232
            }
1233
1234
            // if any of the tables aren't created in the database
1235
            $table = $schema->tableName($class);
1236
            if (!ClassInfo::hasTable($table)) {
1237
                return false;
1238
            }
1239
1240
            // HACK: DataExtensions aren't applied until a class is instantiated for
1241
            // the first time, so create an instance here.
1242
            singleton($class);
1243
1244
            // if any of the tables don't have all fields mapped as table columns
1245
            $dbFields = DB::field_list($table);
1246
            if (!$dbFields) {
1247
                return false;
1248
            }
1249
1250
            $objFields = $schema->databaseFields($class, false);
1251
            $missingFields = array_diff_key($objFields, $dbFields);
1252
1253
            if ($missingFields) {
1254
                return false;
1255
            }
1256
        }
1257
        self::$database_is_ready = true;
1258
1259
        return true;
1260
    }
1261
1262
    /**
1263
     * Resets the database_is_ready cache
1264
     */
1265
    public static function clear_database_is_ready()
1266
    {
1267
        self::$database_is_ready = null;
1268
        self::$force_database_is_ready = null;
1269
    }
1270
1271
    /**
1272
     * For the database_is_ready call to return a certain value - used for testing
1273
     *
1274
     * @param bool $isReady
1275
     */
1276
    public static function force_database_is_ready($isReady)
1277
    {
1278
        self::$force_database_is_ready = $isReady;
1279
    }
1280
1281
    /**
1282
     * @config
1283
     * @var string Set the default login dest
1284
     * This is the URL that users will be redirected to after they log in,
1285
     * if they haven't logged in en route to access a secured page.
1286
     * By default, this is set to the homepage.
1287
     */
1288
    private static $default_login_dest = "";
0 ignored issues
show
introduced by
The private property $default_login_dest is not used, and could be removed.
Loading history...
1289
1290
    /**
1291
     * @config
1292
     * @var string Set the default reset password destination
1293
     * This is the URL that users will be redirected to after they change their password,
1294
     * By default, it's redirecting to {@link $login}.
1295
     */
1296
    private static $default_reset_password_dest;
0 ignored issues
show
introduced by
The private property $default_reset_password_dest is not used, and could be removed.
Loading history...
1297
1298
    protected static $ignore_disallowed_actions = false;
1299
1300
    /**
1301
     * Set to true to ignore access to disallowed actions, rather than returning permission failure
1302
     * Note that this is just a flag that other code needs to check with Security::ignore_disallowed_actions()
1303
     * @param bool $flag True or false
1304
     */
1305
    public static function set_ignore_disallowed_actions($flag)
1306
    {
1307
        self::$ignore_disallowed_actions = $flag;
1308
    }
1309
1310
    public static function ignore_disallowed_actions()
1311
    {
1312
        return self::$ignore_disallowed_actions;
1313
    }
1314
1315
    /**
1316
     * Get the URL of the log-in page.
1317
     *
1318
     * To update the login url use the "Security.login_url" config setting.
1319
     *
1320
     * @return string
1321
     */
1322
    public static function login_url()
1323
    {
1324
        return Controller::join_links(Director::baseURL(), self::config()->get('login_url'));
1325
    }
1326
1327
1328
    /**
1329
     * Get the URL of the logout page.
1330
     *
1331
     * To update the logout url use the "Security.logout_url" config setting.
1332
     *
1333
     * @return string
1334
     */
1335
    public static function logout_url()
1336
    {
1337
        $logoutUrl = Controller::join_links(Director::baseURL(), self::config()->get('logout_url'));
1338
        return SecurityToken::inst()->addToUrl($logoutUrl);
1339
    }
1340
1341
    /**
1342
     * Get the URL of the logout page.
1343
     *
1344
     * To update the logout url use the "Security.logout_url" config setting.
1345
     *
1346
     * @return string
1347
     */
1348
    public static function lost_password_url()
1349
    {
1350
        return Controller::join_links(Director::baseURL(), self::config()->get('lost_password_url'));
1351
    }
1352
1353
    /**
1354
     * Defines global accessible templates variables.
1355
     *
1356
     * @return array
1357
     */
1358
    public static function get_template_global_variables()
1359
    {
1360
        return [
1361
            "LoginURL" => "login_url",
1362
            "LogoutURL" => "logout_url",
1363
            "LostPasswordURL" => "lost_password_url",
1364
            "CurrentMember"   => "getCurrentUser",
1365
            "currentUser"     => "getCurrentUser"
1366
        ];
1367
    }
1368
}
1369