1: <?php
2: /*
3: * SimpleID
4: *
5: * Copyright (C) Kelvin Mo 2024-2026
6: *
7: * This program is free software; you can redistribute it and/or
8: * modify it under the terms of the GNU General Public
9: * License as published by the Free Software Foundation; either
10: * version 2 of the License, or (at your option) any later version.
11: *
12: * This program is distributed in the hope that it will be useful,
13: * but WITHOUT ANY WARRANTY; without even the implied warranty of
14: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15: * General Public License for more details.
16: *
17: * You should have received a copy of the GNU General Public
18: * License along with this program; if not, write to the Free
19: * Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
20: *
21: */
22:
23: namespace SimpleID\Auth;
24:
25: use Psr\EventDispatcher\StoppableEventInterface;
26: use SimpleID\Util\Events\StoppableEventTrait;
27: use SimpleID\Util\Events\UIBuildEvent;
28: use SimpleID\Util\Forms\FormBuildEvent;
29: use SimpleID\Util\UI\Template;
30:
31: /**
32: * An event used to build the login form.
33: *
34: * This class is derived from {@link SimpleID\Util\Forms\FormBuildEvent}, which
35: * uses {@link SimpleID\Util\UI\UIBuilder} to build the form. This event uses
36: * a `region` key in the `$additional` array in the {@link addBlock()} method
37: * to associate each block with a *region*. Blocks in each region are sorted
38: * and displayed independently of each other.
39: *
40: * The regions defined by this class, and their function, can be found in the
41: * documentation of the constants below.
42: *
43: */
44: class LoginFormBuildEvent extends FormBuildEvent implements StoppableEventInterface {
45: use StoppableEventTrait;
46:
47: /**
48: * Constant denoting the `identity` region. The identity region contains a single
49: * block to allow the user to specify an identifier (such as the user name), or
50: * show the identifier if is already given.
51: *
52: * Authentication modules that require an identifer to be given should call
53: * the {@link showUIDBlock()} method. Otherwise no blocks should be added
54: * to this region.
55: */
56: const IDENTITY_REGION = 'identity';
57: /**
58: * Constant denoting the `password` region. The password region is added by
59: * the {@link PasswordAuthSchemeModule} module, to be rendered in the DOM
60: * but hidden from the user unless it is required. This allows browser
61: * password managers to automatically fill in the password in the background.
62: *
63: * No other blocks should be added to this region.
64: */
65: const PASSWORD_REGION = 'password';
66: /**
67: * Constant denoting the `default` region. If a region is not specified when
68: * adding the block, the block will be added to this region.
69: *
70: * Note that blocks in the default region are rendered in the DOM, but
71: * not always shown to the user. For example, if the user is able to choose
72: * between multiple authentication methods, only the block associated with
73: * selected method is shown to the user, while other blocks are hidden.
74: * To always show a block, add it to the `options` region.
75: */
76: const DEFAULT_REGION = 'default';
77: /**
78: * Constant denoting the `options` region. Blocks in this region are
79: * rendered below the `default` region but above the submit button.
80: */
81: const OPTIONS_REGION = 'options';
82: /**
83: * Constant denoting the `secondary` region. Blocks in this region
84: * are always rendered below the default submit button.
85: *
86: * If a block is added to this region, then a divider will be shown
87: * in the login form.
88: */
89: const SECONDARY_REGION = 'secondary';
90:
91: /** @var bool */
92: protected $hasUIDBlock = false;
93:
94: /** @var bool */
95: protected $UIDBlockRendered = false;
96:
97: /** @var array<string> */
98: protected $UIDAutocompleteValues = [];
99:
100: /**
101: * {@inheritdoc}
102: */
103: public function addBlock(string $id, string $content, int $weight = 0, array $additional = []): UIBuildEvent {
104: if (!isset($additional['region'])) $additional['region'] = self::DEFAULT_REGION;
105:
106: return parent::addBlock($id, $content, $weight, $additional);
107: }
108:
109: /**
110: * Show the user ID block when presenting the login form, and optionally
111: * add values to the `autocomplete` attribute in the user ID field.
112: *
113: * @param array<string> $uid_autocomplete additional values to the autocomplete
114: * attribute to be inserted into the login form
115: * @return UIBuildEvent
116: */
117: public function showUIDBlock($uid_autocomplete = []): UIBuildEvent {
118: $this->hasUIDBlock = true;
119:
120: if (count($uid_autocomplete) > 0) $this->UIDAutocompleteValues = array_merge($this->UIDAutocompleteValues, $uid_autocomplete);
121: return $this;
122: }
123:
124: /**
125: * {@inheritdoc}
126: *
127: * @return array<array<mixed>>
128: */
129: public function getBlocks(): array {
130: // Check if user name block has already been added
131: if (!$this->UIDBlockRendered) {
132: $this->UIDBlockRendered = true;
133: $tpl = Template::instance();
134: $this->addBlock('auth_uid', $tpl->render('auth_uid.html', false), 0, [ 'region' => self::IDENTITY_REGION ]);
135: }
136:
137: return parent::getBlocks();
138: }
139:
140: /**
141: * Retrieves the blocks grouped by region, ordered by the
142: * weight, from lowest to highest.
143: *
144: * @return array<string, array<mixed>>
145: */
146: public function getBlocksGroupedByRegion(): array {
147: $result = [];
148: $block_data = $this->getBlocks();
149: foreach ($block_data as $block) {
150: /** @var string $region */
151: $region = $block['region'];
152:
153: if (isset($result[$region])) {
154: $result[$region][] = $block;
155: } else {
156: $result[$region] = [ $block ];
157: }
158: }
159: return $result;
160: }
161:
162: /**
163: * @return array<string>
164: */
165: public function getUIDAutocompleteValues(): array {
166: return $this->UIDAutocompleteValues;
167: }
168: }
169:
170: ?>