Passed
Push — master ( 3420f6...8740f6 )
by Andreas
10:59
created

midcom_services_auth::check_for_login_session()   B

Complexity

Conditions 9
Paths 16

Size

Total Lines 40
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 15.6581

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 9
eloc 22
nc 16
nop 1
dl 0
loc 40
ccs 13
cts 23
cp 0.5652
crap 15.6581
rs 8.0555
c 1
b 0
f 0
1
<?php
2
/**
3
 * @package midcom.services
4
 * @author The Midgard Project, http://www.midgard-project.org
5
 * @copyright The Midgard Project, http://www.midgard-project.org
6
 * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License
7
 */
8
9
use Symfony\Component\HttpFoundation\Request;
10
use Symfony\Component\HttpFoundation\Response;
11
12
/**
13
 * Main Authentication/Authorization service class, it provides means to authenticate
14
 * users and to check for permissions.
15
 *
16
 * <b>Authentication</b>
17
 *
18
 * Whenever the system successfully creates a new login session (during auth service startup),
19
 * it checks whether the key <i>midcom_services_auth_login_success_url</i> is present in the HTTP
20
 * Request data. If this is the case, it relocates to the URL given in it. This member isn't set
21
 * by default in the MidCOM core, it is intended for custom authentication forms. The MidCOM
22
 * relocate function is used to for relocation, thus you can take full advantage of the
23
 * convenience functions in there. See midcom_application::relocate() for details.
24
 *
25
 * <b>Checking Privileges</b>
26
 *
27
 * This class offers various methods to verify the privilege state of a user, all of them prefixed
28
 * with can_* for privileges and is_* for membership checks.
29
 *
30
 * Each function is available in a simple check version, which returns true or false, and a
31
 * require_* prefixed variant, which has no return value. The require variants of these calls
32
 * instead check if the given condition is met, if yes, they return silently, otherwise they
33
 * throw an access denied error.
34
 *
35
 * @todo Fully document authentication.
36
 * @package midcom.services
37
 */
38
class midcom_services_auth
39
{
40
    /**
41
     * The currently authenticated user or null in case of anonymous access.
42
     * It is to be considered read-only.
43
     */
44
    public ?midcom_core_user $user = null;
45
46
    /**
47
     * Admin user level state. This is true if the currently authenticated user is an
48
     * Administrator, false otherwise.
49
     */
50
    public bool $admin = false;
51
52
    public midcom_services_auth_acl $acl;
53
54
    /**
55
     * Internal cache of all loaded groups, indexed by their identifiers.
56
     */
57
    private array $_group_cache = [];
58
59
    /**
60
     * This flag indicates if sudo mode is active during execution. This will only be the
61
     * case if the sudo system actually grants this privileges, and only until components
62
     * release the rights again. This does override the full access control system at this time
63
     * and essentially give you full admin privileges (though this might change in the future).
64
     *
65
     * Note, that this is no boolean but an int, otherwise it would be impossible to trace nested
66
     * sudo invocations, which are quite possible with multiple components calling each others
67
     * callback. A value of 0 indicates that sudo is inactive. A value greater than zero indicates
68
     * sudo mode is active, with the count being equal to the depth of the sudo callers.
69
     *
70
     * It is thus still safely possible to evaluate this member in a boolean context to check
71
     * for an enabled sudo mode.
72
     *
73
     * @see request_sudo()
74
     * @see drop_sudo()
75
     */
76
    private int $_component_sudo = 0;
77
78
    private midcom_services_auth_backend $backend;
79
80
    private midcom_services_auth_frontend $frontend;
81
82
    /**
83
     * Loads all configured authentication drivers.
84
     */
85 1
    public function __construct(midcom_services_auth_acl $acl, midcom_services_auth_backend $backend, midcom_services_auth_frontend $frontend)
86
    {
87 1
        $this->acl = $acl;
88 1
        $this->backend = $backend;
89 1
        $this->frontend = $frontend;
90
    }
91
92
    /**
93
     * Checks if the current authentication fronted has new credentials
94
     * ready. If yes, it processes the login accordingly. Otherwise look for existing session
95
     */
96 1
    public function check_for_login_session(Request $request) : ?midcom_response_relocate
97
    {
98
        // Try to start up a new session, this will authenticate as well.
99 1
        if ($credentials = $this->frontend->read_login_data($request)) {
100 1
            if (!$this->login($credentials['username'], $credentials['password'], $request->getClientIp())) {
101
                if (is_callable(midcom::get()->config->get('auth_failure_callback'))) {
102
                    debug_print_r('Calling auth failure callback: ', midcom::get()->config->get('auth_failure_callback'));
103
                    // Calling the failure function with the username as a parameter. No password sent to the user function for security reasons
104
                    call_user_func(midcom::get()->config->get('auth_failure_callback'), $credentials['username']);
0 ignored issues
show
Bug introduced by
It seems like midcom::get()->config->g...auth_failure_callback') can also be of type null; however, parameter $callback of call_user_func() does only seem to accept callable, 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

104
                    call_user_func(/** @scrutinizer ignore-type */ midcom::get()->config->get('auth_failure_callback'), $credentials['username']);
Loading history...
105
                }
106
                return null;
107
            }
108 1
            debug_add('Authentication was successful, we have a new login session now. Updating timestamps');
109
110 1
            $person_class = midcom::get()->config->get('person_class');
111 1
            $person = new $person_class($this->user->guid);
112
113 1
            if (!$person->get_parameter('midcom', 'first_login')) {
114 1
                $person->set_parameter('midcom', 'first_login', time());
115
            } elseif (midcom::get()->config->get('auth_save_prev_login')) {
116
                $person->set_parameter('midcom', 'prev_login', $person->get_parameter('midcom', 'last_login'));
117
            }
118 1
            $person->set_parameter('midcom', 'last_login', time());
119
120 1
            if (is_callable(midcom::get()->config->get('auth_success_callback'))) {
121
                debug_print_r('Calling auth success callback:', midcom::get()->config->get('auth_success_callback'));
122
                // Calling the success function. No parameters, because authenticated user is stored in midcom_connection
123
                call_user_func(midcom::get()->config->get('auth_success_callback'));
124
            }
125
126
            // Now we check whether there is a success-relocate URL given somewhere.
127 1
            if ($request->get('midcom_services_auth_login_success_url')) {
128 1
                return new midcom_response_relocate($request->get('midcom_services_auth_login_success_url'));
0 ignored issues
show
Bug introduced by
It seems like $request->get('midcom_se...uth_login_success_url') can also be of type null; however, parameter $url of midcom_response_relocate::__construct() 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

128
                return new midcom_response_relocate(/** @scrutinizer ignore-type */ $request->get('midcom_services_auth_login_success_url'));
Loading history...
129
            }
130
        }
131
        // No new login detected, so we check if there is a running session.
132
        elseif ($user = $this->backend->check_for_active_login_session($request)) {
133
            $this->set_user($user);
134
        }
135 1
        return null;
136
    }
137
138 65
    private function set_user(midcom_core_user $user)
139
    {
140 65
        $this->user = $user;
141 65
        $this->admin = $user->is_admin();
142
    }
143
144
    /**
145
     * Checks whether a user has a certain privilege on the given content object.
146
     * Works on the currently authenticated user by default, but can take another
147
     * user as an optional argument.
148
     *
149
     * @param MidgardObject $content_object A Midgard Content Object
0 ignored issues
show
Bug introduced by
The type MidgardObject 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...
150
     * @param midcom_core_user $user The user against which to check the privilege, defaults to the currently authenticated user.
151
     *     You may specify "EVERYONE" instead of an object to check what an anonymous user can do.
152
     */
153 469
    public function can_do(string $privilege, object $content_object, $user = null) : bool
154
    {
155 469
        if ($this->is_admin($user)) {
156
            // Administrators always have access.
157 2
            return true;
158
        }
159
160 469
        $user_id = $this->acl->get_user_id($user);
161
162
        //if we're handed the correct object type, we use its class right away
163 469
        if (midcom::get()->dbclassloader->is_midcom_db_object($content_object)) {
164 469
            $content_object_class = get_class($content_object);
165
        }
166
        //otherwise, we assume (hope) that it's a midgard object
167
        else {
168
            $content_object_class = midcom::get()->dbclassloader->get_midcom_class_name_for_mgdschema_object($content_object);
169
        }
170
171 469
        return $this->acl->can_do_byguid($privilege, $content_object->guid, $content_object_class, $user_id);
0 ignored issues
show
Bug introduced by
It seems like $content_object_class can also be of type null; however, parameter $object_class of midcom_services_auth_acl::can_do_byguid() 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

171
        return $this->acl->can_do_byguid($privilege, $content_object->guid, /** @scrutinizer ignore-type */ $content_object_class, $user_id);
Loading history...
172
    }
173
174 482
    private function is_admin($user) : bool
175
    {
176 482
        if ($user === null) {
177 482
            return $this->user && $this->admin;
178
        }
179 5
        if ($user instanceof midcom_core_user) {
180 2
            return $user->is_admin();
181
        }
182 3
        return false;
183
    }
184
185
    /**
186
     * Checks, whether the given user have the privilege assigned to him in general.
187
     * Be aware, that this does not take any permissions overridden by content objects
188
     * into account. Whenever possible, you should user the can_do() variant of this
189
     * call therefore. can_user_do is only of interest in cases where you do not have
190
     * any content object available, for example when creating root topics.
191
     *
192
     * @param midcom_core_user $user The user against which to check the privilege, defaults to the currently authenticated user,
193
     *     you may specify 'EVERYONE' here to check what an anonymous user can do.
194
     * @param string $class Optional parameter to set if the check should take type specific permissions into account. The class must be default constructible.
195
     */
196 347
    public function can_user_do(string $privilege, $user = null, $class = null) : bool
197
    {
198 347
        if ($this->is_admin($user)) {
199
            // Administrators always have access.
200 2
            return true;
201
        }
202 347
        if ($this->_component_sudo) {
203 341
            return true;
204
        }
205 9
        if ($user === null) {
206 9
            $user =& $this->user;
207
        }
208
209 9
        if ($user == 'EVERYONE') {
210
            $user = null;
211
        }
212
213 9
        return $this->acl->can_do_byclass($privilege, $user, $class);
214
    }
215
216
    /**
217
     * Request superuser privileges for the domain passed.
218
     *
219
     * STUB IMPLEMENTATION ONLY, WILL ALWAYS GRANT SUDO.
220
     *
221
     * You have to call midcom_services_auth::drop_sudo() as soon as you no longer
222
     * need the elevated privileges, which will reset the authentication data to the
223
     * initial credentials.
224
     *
225
     * @param string $domain The domain to request sudo for. This is a component name.
226
     */
227 692
    public function request_sudo(string $domain = null) : bool
228
    {
229 692
        if (!midcom::get()->config->get('auth_allow_sudo')) {
230 1
            debug_add("SUDO is not allowed on this website.", MIDCOM_LOG_ERROR);
231 1
            return false;
232
        }
233
234 692
        if ($domain === null) {
235 1
            $domain = midcom_core_context::get()->get_key(MIDCOM_CONTEXT_COMPONENT);
236 1
            debug_add("Domain was not supplied, falling back to '{$domain}' which we got from the current component context.");
237
        }
238
239 692
        if ($domain == '') {
240 1
            debug_add("SUDO request for an empty domain, this should not happen. Denying sudo.", MIDCOM_LOG_INFO);
241 1
            return false;
242
        }
243
244 692
        $this->_component_sudo++;
245
246 692
        debug_add("Entered SUDO mode for domain {$domain}.", MIDCOM_LOG_INFO);
247
248 692
        return true;
249
    }
250
251
    /**
252
     * Drops previously acquired superuser privileges.
253
     *
254
     * @see request_sudo()
255
     */
256 692
    public function drop_sudo()
257
    {
258 692
        if ($this->_component_sudo > 0) {
259 692
            debug_add('Leaving SUDO mode.');
260 692
            $this->_component_sudo--;
261
        } else {
262 1
            debug_add('Requested to leave SUDO mode, but sudo was already disabled. Ignoring request.', MIDCOM_LOG_INFO);
263
        }
264
    }
265
266 669
    public function is_component_sudo() : bool
267
    {
268 669
        return $this->_component_sudo > 0;
269
    }
270
271
    /**
272
     * Check, whether a user is member of a given group. By default, the query is run
273
     * against the currently authenticated user.
274
     *
275
     * It always returns true for administrative users.
276
     *
277
     * @param mixed $group Group to check against, this can be either a midcom_core_group object or a group string identifier.
278
     * @param midcom_core_user $user The user which should be checked, defaults to the current user.
279
     */
280
    public function is_group_member($group, $user = null) : bool
281
    {
282
        if ($this->is_admin($user)) {
283
            // Administrators always have access.
284
            return true;
285
        }
286
        // Default parameter
287
        if ($user === null) {
288
            if ($this->user === null) {
289
                // not authenticated
290
                return false;
291
            }
292
            $user = $this->user;
293
        }
294
295
        return $user->is_in_group($group);
296
    }
297
298
    /**
299
     * Returns true if there is an authenticated user, false otherwise.
300
     */
301 156
    public function is_valid_user() : bool
302
    {
303 156
        return $this->user !== null;
304
    }
305
306
    /**
307
     * Validates that the current user has the given privilege granted on the
308
     * content object passed to the function.
309
     *
310
     * If this is not the case, an Access Denied error is generated, the message
311
     * defaulting to the string 'access denied: privilege %s not granted' of the
312
     * MidCOM main L10n table.
313
     *
314
     * The check is always done against the currently authenticated user. If the
315
     * check is successful, the function returns silently.
316
     *
317
     * @param MidgardObject $content_object A Midgard Content Object
318
     */
319 195
    public function require_do(string $privilege, object $content_object, string $message = null)
320
    {
321 195
        if (!$this->can_do($privilege, $content_object)) {
322
            throw $this->access_denied($message, 'privilege %s not granted', $privilege);
323
        }
324
    }
325
326
    /**
327
     * Validates, whether the given user have the privilege assigned to him in general.
328
     * Be aware, that this does not take any permissions overridden by content objects
329
     * into account. Whenever possible, you should user the require_do() variant of this
330
     * call therefore. require_user_do is only of interest in cases where you do not have
331
     * any content object available, for example when creating root topics.
332
     *
333
     * If this is not the case, an Access Denied error is generated, the message
334
     * defaulting to the string 'access denied: privilege %s not granted' of the
335
     * MidCOM main L10n table.
336
     *
337
     * The check is always done against the currently authenticated user. If the
338
     * check is successful, the function returns silently.
339
     *
340
     * @param string $class Optional parameter to set if the check should take type specific permissions into account. The class must be default constructible.
341
     */
342 77
    public function require_user_do(string $privilege, string $message = null, string $class = null)
343
    {
344 77
        if (!$this->can_user_do($privilege, null, $class)) {
345
            throw $this->access_denied($message, 'privilege %s not granted', $privilege);
346
        }
347
    }
348
349
    /**
350
     * Validates that the current user is a member of the given group.
351
     *
352
     * If this is not the case, an Access Denied error is generated, the message
353
     * defaulting to the string 'access denied: user is not member of the group %s' of the
354
     * MidCOM main L10n table.
355
     *
356
     * The check is always done against the currently authenticated user. If the
357
     * check is successful, the function returns silently.
358
     *
359
     * @param mixed $group Group to check against, this can be either a midcom_core_group object or a group string identifier.
360
     * @param string $message The message to show if the user is not member of the given group.
361
     */
362
    function require_group_member($group, $message = null)
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
363
    {
364
        if (!$this->is_group_member($group)) {
365
            if (is_object($group)) {
366
                $group = $group->name;
367
            }
368
            throw $this->access_denied($message, 'user is not member of the group %s', $group);
369
        }
370
    }
371
372
    /**
373
     * Validates that we currently have admin level privileges, which can either
374
     * come from the current user, or from the sudo service.
375
     *
376
     * If the check is successful, the function returns silently.
377
     */
378 4
    public function require_admin_user(string $message = null)
379
    {
380 4
        if (!$this->admin && !$this->_component_sudo) {
381
            throw $this->access_denied($message, 'admin level privileges required');
382
        }
383
    }
384
385
    private function access_denied(?string $message, string $fallback, string $data = null) : midcom_error_forbidden
386
    {
387
        if ($message === null) {
388
            $message = midcom::get()->i18n->get_string('access denied: ' . $fallback, 'midcom');
389
            if ($data !== null) {
390
                $message = sprintf($message, $data);
391
            }
392
        }
393
        debug_print_function_stack("access_denied was called from here:");
394
        return new midcom_error_forbidden($message);
395
    }
396
397
    /**
398
     * Require either a configured IP address or admin credentials
399
     */
400
    public function require_admin_or_ip(string $domain) : bool
401
    {
402
        $ips = midcom::get()->config->get_array('indexer_reindex_allowed_ips');
403
        if (in_array($_SERVER['REMOTE_ADDR'], $ips)) {
404
            if (!$this->request_sudo($domain)) {
405
                throw new midcom_error('Failed to acquire SUDO rights. Aborting.');
406
            }
407
            return true;
408
        }
409
410
        // Require user to Basic-authenticate for security reasons
411
        $this->require_valid_user('basic');
412
        $this->require_admin_user();
413
        return false;
414
    }
415
416
    /**
417
     * Validates that there is an authenticated user.
418
     *
419
     * If this is not the case, midcom_error_forbidden is thrown, or a
420
     * basic auth challenge is triggered
421
     *
422
     * If the check is successful, the function returns silently.
423
     *
424
     * @param string $method Preferred authentication method: form or basic
425
     */
426 154
    public function require_valid_user(string $method = 'form')
427
    {
428 154
        if ($method === 'basic') {
429 5
            $this->_http_basic_auth();
430
        }
431 154
        if (!$this->is_valid_user()) {
432
            throw new midcom_error_forbidden(null, Response::HTTP_UNAUTHORIZED, $method);
433
        }
434
    }
435
436
    /**
437
     * Handles HTTP Basic authentication
438
     */
439 5
    private function _http_basic_auth()
440
    {
441 5
        if (isset($_SERVER['PHP_AUTH_USER'])) {
442
            if ($user = $this->backend->authenticate($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'])) {
443
                $this->set_user($user);
444
            } else {
445
                // Wrong password
446
                unset($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']);
447
            }
448
        }
449
    }
450
451
    /**
452
     * Resolve any assignee identifier known by the system into an appropriate user/group object.
453
     *
454
     * @param string $id A valid user or group identifier usable as assignee (e.g. the $id member
455
     *     of any midcom_core_user or midcom_core_group object).
456
     * @return object|null corresponding object or false on failure.
457
     */
458 133
    public function get_assignee(string $id) : ?object
459
    {
460 133
        $parts = explode(':', $id);
461
462 133
        if ($parts[0] == 'user') {
463 132
            return $this->get_user($id);
464
        }
465 5
        if ($parts[0] == 'group') {
466 4
            return $this->get_group($id);
467
        }
468 1
        debug_add("The identifier {$id} cannot be resolved into an assignee, it cannot be mapped to a type.", MIDCOM_LOG_WARN);
469
470 1
        return null;
471
    }
472
473
    /**
474
     * This is a wrapper for get_user, which allows user retrieval by its name.
475
     * If the username is unknown, false is returned.
476
     */
477 3
    public function get_user_by_name(string $name) : ?midcom_core_user
478
    {
479 3
        $mc = new midgard_collector('midgard_user', 'login', $name);
480 3
        $mc->set_key_property('person');
481 3
        $mc->add_constraint('authtype', '=', midcom::get()->config->get('auth_type'));
482 3
        $mc->execute();
483 3
        $keys = $mc->list_keys();
484 3
        if (count($keys) != 1) {
485
            return null;
486
        }
487
488 3
        return $this->get_user(key($keys));
489
    }
490
491
    /**
492
     * This is a wrapper for get_group, which allows Midgard Group retrieval by its name.
493
     * If the group name is unknown, false is returned.
494
     *
495
     * In the case that more than one group matches the given name, the first one is returned.
496
     * Note, that this should not happen as midgard group names should be unique according to the specs.
497
     */
498
    public function get_midgard_group_by_name(string $name) : ?midcom_core_group
499
    {
500
        $qb = new midgard_query_builder('midgard_group');
501
        $qb->add_constraint('name', '=', $name);
502
503
        if ($result = $qb->execute()) {
504
            return $this->get_group($result[0]);
505
        }
506
        return null;
507
    }
508
509
    /**
510
     * Load a user from the database and returns an object instance.
511
     *
512
     * @param mixed $id A valid identifier for a MidgardPerson: An existing midgard_person class
513
     *     or subclass thereof, a Person ID or GUID or a midcom_core_user identifier.
514
     */
515 191
    public function get_user($id) : ?midcom_core_user
516
    {
517 191
        return $this->backend->get_user($id);
518
    }
519
520
    /**
521
     * Returns a midcom_core_group instance. Valid arguments are either a valid group identifier
522
     * (group:...), any valid identifier for the midcom_core_group
523
     * constructor or a valid object of that type.
524
     *
525
     * @param mixed $id The identifier of the group as outlined above.
526
     */
527 4
    public function get_group($id) : ?midcom_core_group
528
    {
529 4
        $param = $id;
530
531 4
        if (isset($param->id)) {
532
            $id = $param->id;
533 4
        } elseif (!is_string($id) && !is_int($id)) {
534
            debug_add('The group identifier is of an unsupported type: ' . gettype($param), MIDCOM_LOG_WARN);
535
            debug_print_r('Complete dump:', $param);
536
            return null;
537
        }
538
539 4
        if (!array_key_exists($id, $this->_group_cache)) {
540
            try {
541 4
                if ($param instanceof midcom_core_dbaobject) {
542
                    $param = $param->__object;
543
                }
544 4
                $this->_group_cache[$id] = new midcom_core_group($param);
545
            } catch (midcom_error $e) {
546
                debug_add("Group with identifier {$id} could not be loaded: " . $e->getMessage(), MIDCOM_LOG_WARN);
547
                $this->_group_cache[$id] = null;
548
            }
549
        }
550 4
        return $this->_group_cache[$id];
551
    }
552
553
    /**
554
     * This call tells the backend to log in.
555
     */
556 65
    public function login(string $username, string $password, string $clientip = null) : bool
557
    {
558 65
        if ($user = $this->backend->login($username, $password, $clientip)) {
559 65
            $this->set_user($user);
560 65
            return true;
561
        }
562
        debug_add('The login information for ' . $username . ' was invalid.', MIDCOM_LOG_WARN);
563
        return false;
564
    }
565
566
    public function trusted_login(string $username) : bool
567
    {
568
        if (midcom::get()->config->get('auth_allow_trusted') !== true) {
569
            debug_add("Trusted logins are prohibited", MIDCOM_LOG_ERROR);
570
            return false;
571
        }
572
573
        if ($user = $this->backend->login($username, '', null, true)) {
574
            $this->set_user($user);
575
            return true;
576
        }
577
        return false;
578
    }
579
580
    /**
581
     * This call clears any authentication state
582
     */
583 1
    public function logout()
584
    {
585 1
        if ($this->user === null) {
586
            debug_add('The backend has no authenticated user set, so we should be fine');
587
        } else {
588 1
            $this->backend->logout($this->user);
589 1
            $this->user = null;
590
        }
591 1
        $this->admin = false;
592
    }
593
594
    /**
595
     * Render the main login form.
596
     * This only includes the form, no heading or whatsoever.
597
     *
598
     * It is recommended to call this function only as long as the headers are not yet sent (which
599
     * is usually given thanks to MidCOMs output buffering).
600
     *
601
     * What gets rendered depends on the authentication frontend, but will usually be some kind
602
     * of form.
603
     */
604
    public function show_login_form()
605
    {
606
        $this->frontend->show_login_form();
607
    }
608
609
    public function has_login_data() : bool
610
    {
611
        return $this->frontend->has_login_data();
612
    }
613
}
614