Passed
Push — master ( 179526...2874e4 )
by Andreas
28:11
created

midcom_services_auth::get_group()   A

Complexity

Conditions 5
Paths 10

Size

Total Lines 20
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 6.8088

Importance

Changes 0
Metric Value
cc 5
eloc 12
nc 10
nop 1
dl 0
loc 20
ccs 7
cts 12
cp 0.5833
crap 6.8088
rs 9.5555
c 0
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 470
    public function can_do(string $privilege, object $content_object, $user = null) : bool
154
    {
155 470
        if ($this->is_admin($user)) {
156
            // Administrators always have access.
157 2
            return true;
158
        }
159
160 470
        $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 470
        if (midcom::get()->dbclassloader->is_midcom_db_object($content_object)) {
164 470
            $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 470
        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 483
    private function is_admin($user) : bool
175
    {
176 483
        if ($user === null) {
177 483
            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 348
    public function can_user_do(string $privilege, $user = null, $class = null) : bool
197
    {
198 348
        if ($this->is_admin($user)) {
199
            // Administrators always have access.
200 2
            return true;
201
        }
202 348
        if ($this->_component_sudo) {
203 342
            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 693
    public function request_sudo(string $domain = null) : bool
228
    {
229 693
        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 693
        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 693
        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 693
        $this->_component_sudo++;
245
246 693
        debug_add("Entered SUDO mode for domain {$domain}.", MIDCOM_LOG_INFO);
247
248 693
        return true;
249
    }
250
251
    /**
252
     * Drops previously acquired superuser privileges.
253
     *
254
     * @see request_sudo()
255
     */
256 693
    public function drop_sudo()
257
    {
258 693
        if ($this->_component_sudo > 0) {
259 693
            debug_add('Leaving SUDO mode.');
260 693
            $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 670
    public function is_component_sudo() : bool
267
    {
268 670
        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
    public function is_group_member(midcom_core_group|string $group, midcom_core_user $user = null) : bool
278
    {
279
        if ($this->is_admin($user)) {
280
            // Administrators always have access.
281
            return true;
282
        }
283
        // Default parameter
284
        if ($user === null) {
285
            if ($this->user === null) {
286
                // not authenticated
287
                return false;
288
            }
289
            $user = $this->user;
290
        }
291
292
        return $user->is_in_group($group);
293
    }
294
295
    /**
296
     * Returns true if there is an authenticated user, false otherwise.
297
     */
298 157
    public function is_valid_user() : bool
299
    {
300 157
        return $this->user !== null;
301
    }
302
303
    /**
304
     * Validates that the current user has the given privilege granted on the
305
     * content object passed to the function.
306
     *
307
     * If this is not the case, an Access Denied error is generated, the message
308
     * defaulting to the string 'access denied: privilege %s not granted' of the
309
     * MidCOM main L10n table.
310
     *
311
     * The check is always done against the currently authenticated user. If the
312
     * check is successful, the function returns silently.
313
     *
314
     * @param MidgardObject $content_object A Midgard Content Object
315
     */
316 196
    public function require_do(string $privilege, object $content_object, string $message = null)
317
    {
318 196
        if (!$this->can_do($privilege, $content_object)) {
319
            throw $this->access_denied($message, 'privilege %s not granted', $privilege);
320
        }
321
    }
322
323
    /**
324
     * Validates, whether the given user have the privilege assigned to him in general.
325
     * Be aware, that this does not take any permissions overridden by content objects
326
     * into account. Whenever possible, you should user the require_do() variant of this
327
     * call therefore. require_user_do is only of interest in cases where you do not have
328
     * any content object available, for example when creating root topics.
329
     *
330
     * If this is not the case, an Access Denied error is generated, the message
331
     * defaulting to the string 'access denied: privilege %s not granted' of the
332
     * MidCOM main L10n table.
333
     *
334
     * The check is always done against the currently authenticated user. If the
335
     * check is successful, the function returns silently.
336
     *
337
     * @param string $class Optional parameter to set if the check should take type specific permissions into account. The class must be default constructible.
338
     */
339 77
    public function require_user_do(string $privilege, string $message = null, string $class = null)
340
    {
341 77
        if (!$this->can_user_do($privilege, null, $class)) {
342
            throw $this->access_denied($message, 'privilege %s not granted', $privilege);
343
        }
344
    }
345
346
    /**
347
     * Validates that the current user is a member of the given group.
348
     *
349
     * If this is not the case, an Access Denied error is generated, the message
350
     * defaulting to the string 'access denied: user is not member of the group %s' of the
351
     * MidCOM main L10n table.
352
     *
353
     * The check is always done against the currently authenticated user. If the
354
     * check is successful, the function returns silently.
355
     *
356
     * @param mixed $group Group to check against, this can be either a midcom_core_group object or a group string identifier.
357
     * @param string $message The message to show if the user is not member of the given group.
358
     */
359
    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...
360
    {
361
        if (!$this->is_group_member($group)) {
362
            if (is_object($group)) {
363
                $group = $group->name;
364
            }
365
            throw $this->access_denied($message, 'user is not member of the group %s', $group);
366
        }
367
    }
368
369
    /**
370
     * Validates that we currently have admin level privileges, which can either
371
     * come from the current user, or from the sudo service.
372
     *
373
     * If the check is successful, the function returns silently.
374
     */
375 4
    public function require_admin_user(string $message = null)
376
    {
377 4
        if (!$this->admin && !$this->_component_sudo) {
378
            throw $this->access_denied($message, 'admin level privileges required');
379
        }
380
    }
381
382
    private function access_denied(?string $message, string $fallback, string $data = null) : midcom_error_forbidden
383
    {
384
        if ($message === null) {
385
            $message = midcom::get()->i18n->get_string('access denied: ' . $fallback, 'midcom');
386
            if ($data !== null) {
387
                $message = sprintf($message, $data);
388
            }
389
        }
390
        debug_print_function_stack("access_denied was called from here:");
391
        return new midcom_error_forbidden($message);
392
    }
393
394
    /**
395
     * Require either a configured IP address or admin credentials
396
     */
397
    public function require_admin_or_ip(string $domain) : bool
398
    {
399
        $ips = midcom::get()->config->get_array('indexer_reindex_allowed_ips');
400
        if (in_array($_SERVER['REMOTE_ADDR'], $ips)) {
401
            if (!$this->request_sudo($domain)) {
402
                throw new midcom_error('Failed to acquire SUDO rights. Aborting.');
403
            }
404
            return true;
405
        }
406
407
        // Require user to Basic-authenticate for security reasons
408
        $this->require_valid_user('basic');
409
        $this->require_admin_user();
410
        return false;
411
    }
412
413
    /**
414
     * Validates that there is an authenticated user.
415
     *
416
     * If this is not the case, midcom_error_forbidden is thrown, or a
417
     * basic auth challenge is triggered
418
     *
419
     * If the check is successful, the function returns silently.
420
     *
421
     * @param string $method Preferred authentication method: form or basic
422
     */
423 155
    public function require_valid_user(string $method = 'form')
424
    {
425 155
        if ($method === 'basic') {
426 5
            $this->_http_basic_auth();
427
        }
428 155
        if (!$this->is_valid_user()) {
429
            throw new midcom_error_forbidden(null, Response::HTTP_UNAUTHORIZED, $method);
430
        }
431
    }
432
433
    /**
434
     * Handles HTTP Basic authentication
435
     */
436 5
    private function _http_basic_auth()
437
    {
438 5
        if (isset($_SERVER['PHP_AUTH_USER'])) {
439
            if ($user = $this->backend->authenticate($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'])) {
440
                $this->set_user($user);
441
            } else {
442
                // Wrong password
443
                unset($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']);
444
            }
445
        }
446
    }
447
448
    /**
449
     * Resolve any assignee identifier known by the system into an appropriate user/group object.
450
     *
451
     * @param string $id A valid user or group identifier usable as assignee (e.g. the $id member
452
     *     of any midcom_core_user or midcom_core_group object).
453
     * @return object|null corresponding object or false on failure.
454
     */
455 133
    public function get_assignee(string $id) : ?object
456
    {
457 133
        $parts = explode(':', $id);
458
459 133
        if ($parts[0] == 'user') {
460 132
            return $this->get_user($id);
461
        }
462 5
        if ($parts[0] == 'group') {
463 4
            return $this->get_group($id);
464
        }
465 1
        debug_add("The identifier {$id} cannot be resolved into an assignee, it cannot be mapped to a type.", MIDCOM_LOG_WARN);
466
467 1
        return null;
468
    }
469
470
    /**
471
     * This is a wrapper for get_user, which allows user retrieval by its name.
472
     * If the username is unknown, false is returned.
473
     */
474 3
    public function get_user_by_name(string $name) : ?midcom_core_user
475
    {
476 3
        $mc = new midgard_collector('midgard_user', 'login', $name);
477 3
        $mc->set_key_property('person');
478 3
        $mc->add_constraint('authtype', '=', midcom::get()->config->get('auth_type'));
479 3
        $mc->execute();
480 3
        $keys = $mc->list_keys();
481 3
        if (count($keys) != 1) {
482
            return null;
483
        }
484
485 3
        return $this->get_user(key($keys));
486
    }
487
488
    /**
489
     * This is a wrapper for get_group, which allows Midgard Group retrieval by its name.
490
     * If the group name is unknown, false is returned.
491
     *
492
     * In the case that more than one group matches the given name, the first one is returned.
493
     * Note, that this should not happen as midgard group names should be unique according to the specs.
494
     */
495
    public function get_midgard_group_by_name(string $name) : ?midcom_core_group
496
    {
497
        $qb = new midgard_query_builder('midgard_group');
498
        $qb->add_constraint('name', '=', $name);
499
500
        if ($result = $qb->execute()) {
501
            return $this->get_group($result[0]);
502
        }
503
        return null;
504
    }
505
506
    /**
507
     * Load a user from the database and returns an object instance.
508
     *
509
     * @param mixed $id A valid identifier for a MidgardPerson: An existing midgard_person class
510
     *     or subclass thereof, a Person ID or GUID or a midcom_core_user identifier.
511
     */
512 191
    public function get_user($id) : ?midcom_core_user
513
    {
514 191
        return $this->backend->get_user($id);
515
    }
516
517
    /**
518
     * Returns a midcom_core_group instance. Valid arguments are either a valid group identifier
519
     * (group:...), any valid identifier for the midcom_core_group
520
     * constructor or a valid object of that type.
521
     */
522 4
    public function get_group(string|int|midcom_db_group|midgard_group $id) : ?midcom_core_group
523
    {
524 4
        $param = $id;
525
526 4
        if (is_object($param)) {
527
            $id = $param->id;
528
        }
529
530 4
        if (!array_key_exists($id, $this->_group_cache)) {
0 ignored issues
show
Bug introduced by
It seems like $id can also be of type midcom_db_group and midgard_group; however, parameter $key of array_key_exists() does only seem to accept integer|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

530
        if (!array_key_exists(/** @scrutinizer ignore-type */ $id, $this->_group_cache)) {
Loading history...
531
            try {
532 4
                if ($param instanceof midcom_core_dbaobject) {
533
                    $param = $param->__object;
534
                }
535 4
                $this->_group_cache[$id] = new midcom_core_group($param);
536
            } catch (midcom_error $e) {
537
                debug_add("Group with identifier {$id} could not be loaded: " . $e->getMessage(), MIDCOM_LOG_WARN);
538
                $this->_group_cache[$id] = null;
539
            }
540
        }
541 4
        return $this->_group_cache[$id];
542
    }
543
544
    /**
545
     * This call tells the backend to log in.
546
     */
547 65
    public function login(string $username, string $password, string $clientip = null) : bool
548
    {
549 65
        if ($user = $this->backend->login($username, $password, $clientip)) {
550 65
            $this->set_user($user);
551 65
            return true;
552
        }
553
        debug_add('The login information for ' . $username . ' was invalid.', MIDCOM_LOG_WARN);
554
        return false;
555
    }
556
557
    public function trusted_login(string $username) : bool
558
    {
559
        if (midcom::get()->config->get('auth_allow_trusted') !== true) {
560
            debug_add("Trusted logins are prohibited", MIDCOM_LOG_ERROR);
561
            return false;
562
        }
563
564
        if ($user = $this->backend->login($username, '', null, true)) {
565
            $this->set_user($user);
566
            return true;
567
        }
568
        return false;
569
    }
570
571
    /**
572
     * This call clears any authentication state
573
     */
574 1
    public function logout()
575
    {
576 1
        if ($this->user === null) {
577
            debug_add('The backend has no authenticated user set, so we should be fine');
578
        } else {
579 1
            $this->backend->logout($this->user);
580 1
            $this->user = null;
581
        }
582 1
        $this->admin = false;
583
    }
584
585
    /**
586
     * Render the main login form.
587
     * This only includes the form, no heading or whatsoever.
588
     *
589
     * It is recommended to call this function only as long as the headers are not yet sent (which
590
     * is usually given thanks to MidCOMs output buffering).
591
     *
592
     * What gets rendered depends on the authentication frontend, but will usually be some kind
593
     * of form.
594
     */
595
    public function show_login_form()
596
    {
597
        $this->frontend->show_login_form();
598
    }
599
600
    public function has_login_data() : bool
601
    {
602
        return $this->frontend->has_login_data();
603
    }
604
}
605