1: <?php
2: /*
3: * SimpleID
4: *
5: * Copyright (C) Kelvin Mo 2009-2025
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:
24: namespace SimpleID\Protocols\OpenID\Extensions;
25:
26: use SimpleID\Module;
27: use SimpleID\Auth\AuthManager;
28: use SimpleID\Protocols\OpenID\OpenIDResponseBuildEvent;
29: use SimpleID\Util\Events\BaseDataCollectionEvent;
30: use SimpleID\Util\Events\UIBuildEvent;
31: use SimpleID\Util\Forms\FormBuildEvent;
32: use SimpleID\Util\Forms\FormSubmitEvent;
33: use SimpleID\Util\UI\Template;
34:
35: /**
36: * Implements the Attribute Exchange extension.
37: *
38: *
39: * @package simpleid
40: * @subpackage extensions
41: * @filesource
42: */
43: class AXOpenIDExtensionModule extends Module {
44: /** Namespace for the Simple Registration extension */
45: const OPENID_NS_AX = 'http://openid.net/srv/ax/1.0';
46:
47: /** @var AuthManager */
48: private $auth;
49:
50: public function __construct() {
51: parent::__construct();
52: $this->auth = AuthManager::instance();
53: }
54:
55: /**
56: * Returns the support for AX in SimpleID XRDS document
57: *
58: * @param BaseDataCollectionEvent $event
59: * @return void
60: */
61: function onXrdsTypes(BaseDataCollectionEvent $event) {
62: $event->addResult(self::OPENID_NS_AX);
63: }
64:
65: /**
66: * @see SimpleID\Protocols\OpenID\OpenIDResponseBuildEvent
67: * @return void
68: */
69: public function onOpenIDResponseBuildEvent(OpenIDResponseBuildEvent $event) {
70: // We only deal with positive assertions
71: if (!$event->isPositiveAssertion()) return;
72:
73: // We only respond if the extension is requested
74: /** @var \SimpleID\Protocols\OpenID\Request */
75: $request = $event->getRequest();
76: /** @var \SimpleID\Protocols\OpenID\Response */
77: $response = $event->getResponse();
78:
79: if (!$request->hasExtension(self::OPENID_NS_AX)) return;
80:
81: $user = $this->auth->getUser();
82:
83: $ax_request = $request->getParamsForExtension(self::OPENID_NS_AX);
84: if (!isset($ax_request['mode'])) return;
85: $mode = $ax_request['mode'];
86:
87: $alias = $response->getAliasForExtension(self::OPENID_NS_AX, 'ax');
88: $response['ns.' . $alias] = self::OPENID_NS_AX;
89:
90: if ($mode == 'fetch_request') {
91: $response[$alias . '.mode'] = 'fetch_response';
92:
93: $required = (isset($ax_request['required'])) ? explode(',', $ax_request['required']) : [];
94: $optional = (isset($ax_request['if_available'])) ? explode(',', $ax_request['if_available']) : [];
95: $fields = array_merge($required, $optional);
96:
97: foreach ($fields as $field) {
98: if (!isset($ax_request['type.' . $field])) continue;
99: $type = $ax_request['type.' . $field];
100: $response[$alias . '.type.' . $field] = $type;
101: $value = $this->getValue($user, $type);
102:
103: if ($value == NULL) {
104: $response[$alias . '.count.' . $field] = 0;
105: } elseif (is_array($value)) {
106: $response[$alias . '.count.' . $field] = count($value);
107: for ($i = 0; $i < count($value); $i++) {
108: $response[$alias . '.value.' . $field . '.' . ($i + 1)] = $value[$i];
109: }
110: } else {
111: $response[$alias . '.value.' . $field] = $value;
112: }
113: }
114: } elseif ($mode == 'store_request') {
115: // Sadly, we don't support storage at this stage
116: $response[$alias . '.mode'] = 'store_response_failure';
117: $response[$alias . '.error'] = 'OpenID provider does not support storage of attributes';
118: }
119:
120: return;
121: }
122:
123: /**
124: * @see SimpleID\Util\Forms\FormBuildEvent
125: * @return void
126: */
127: function onOpenidConsentFormBuild(FormBuildEvent $event) {
128: $form_state = $event->getFormState();
129: /** @var \SimpleID\Protocols\OpenID\Request */
130: $request = $form_state->getRequest();
131: $prefs = $form_state['prefs'];
132:
133: // We only respond if the extension is requested
134: if (!$request->hasExtension(self::OPENID_NS_AX)) return;
135:
136: $user = $this->auth->getUser();
137:
138: $ax_request = $request->getParamsForExtension(self::OPENID_NS_AX);
139: if (!isset($ax_request['mode'])) return;
140: $mode = $ax_request['mode'];
141:
142: if ($mode == 'fetch_request') {
143: $tpl = new Template();
144: $hive = [
145: 'module' => 'ax',
146: 'userinfo_label' => $this->f3->get('intl.common.consent.send_label'),
147: 'name_label' => $this->f3->get('intl.core.openid.ax.type_url_label'),
148: 'value_label' => $this->f3->get('intl.common.value'),
149: 'fields' => []
150: ];
151:
152: $required = (isset($ax_request['required'])) ? explode(',', $ax_request['required']) : [];
153: $optional = (isset($ax_request['if_available'])) ? explode(',', $ax_request['if_available']) : [];
154: $fields = array_merge($required, $optional);
155: $i = 1;
156:
157: foreach ($fields as $field) {
158: if (!isset($ax_request['type.' . $field])) continue;
159: $type = $ax_request['type.' . $field];
160: $value = $this->getValue($user, $type);
161: if ($value == NULL) continue;
162: if (is_array($value)) $value = implode(',', $value);
163:
164: $form_field = [
165: 'id' => $type,
166: 'html_id' => $i,
167: 'name' => $type,
168: 'value' => $value,
169: ];
170:
171: if (in_array($field, $required)) {
172: $form_field['required'] = true;
173: } else {
174: $form_field['required'] = false;
175: $form_field['checked'] = (!isset($prefs['consents']['ax']) || in_array($field, $prefs['consents']['ax'])) ;
176: }
177:
178: $hive['fields'][] = $form_field;
179: $i++;
180: }
181:
182: $event->addBlock('openid_consent_ax', $tpl->render('openid_userinfo_consent.html', false, $hive), 0);
183: } elseif ($mode == 'store_request') {
184: // Sadly, we don't support storage at this stage
185: $this->f3->set('message', $this->f3->get('intl.core.openid.ax.unsupported_feature'));
186: }
187: }
188:
189: /**
190: * @see SimpleID\Util\Forms\FormSubmitEvent
191: * @return void
192: */
193: function onOpenidConsentFormSubmit(FormSubmitEvent $event) {
194: $form_state = $event->getFormState();
195: /** @var \SimpleID\Protocols\OpenID\Response */
196: $response = $form_state->getResponse();
197: $prefs =& $form_state->ref('prefs');
198:
199: // We only respond if the extension is requested
200: if (!$response->hasExtension(self::OPENID_NS_AX)) return;
201:
202: $fields = array_keys($response->getParamsForExtension(self::OPENID_NS_AX));
203: $alias = $response->getAliasForExtension(self::OPENID_NS_AX, 'ax');
204: $form = $this->f3->get('POST.prefs.consents.ax');
205:
206: foreach ($fields as $field) {
207: if ((strpos($field, 'value.') !== 0) && (strpos($field, 'count.') !== 0)) continue;
208:
209: $type_alias = (strpos($field, '.', 6) === FALSE) ? substr($field, 6) : substr($field, strpos($field, '.', 6) - 6);
210: $type = $response[$alias . '.type.' . $type_alias];
211:
212: if (isset($response[$alias . '.' . $field])) {
213: if (!in_array($type, $form)) {
214: unset($response[$alias . '.' . $field]);
215: }
216: }
217: }
218: foreach ($fields as $field) {
219: if (strpos($field, 'type.') !== 0) continue;
220: $type = $response[$alias . '.' . $field];
221:
222: if (isset($response[$alias . '.' . $field])) {
223: if (!in_array($type, $form)) {
224: unset($response[$alias . '.' . $field]);
225: }
226: }
227: }
228:
229: if (count(array_keys($response->getParamsForExtension(self::OPENID_NS_AX))) == 0) {
230: // We have removed all the responses, so we remove the namespace as well
231: unset($response['ns.' . $alias]);
232: }
233:
234: $prefs['consents']['sreg'] = $form;
235: }
236:
237: /**
238: * @return void
239: */
240: public function onProfileBlocks(UIBuildEvent $event) {
241: $user = $this->auth->getUser();
242:
243: if (!isset($user['ax'])) return;
244:
245: $tpl = new Template();
246: $hive = [
247: 'userinfo_label' => $this->f3->get('intl.core.openid.ax.profile_block'),
248: 'name_label' => $this->f3->get('intl.core.openid.ax.type_url_label'),
249: 'value_label' => $this->f3->get('intl.common.value'),
250: 'info' => $user['ax']
251: ];
252:
253: $event->addBlock('ax', $tpl->render('openid_userinfo_profile.html', false, $hive), 0, [
254: 'title' => $this->f3->get('intl.core.openid.ax.ax_title')
255: ]);
256: }
257:
258: /**
259: * Looks up the value of a specified Attribute Exchange Extension type URI.
260: *
261: * This function looks up the ax section of the user's identity file. If the
262: * specified type cannot be found, it looks up the corresponding field in the
263: * OpenID Connect user information (user_info section) and the Simple Registration
264: * Extension (sreg section).
265: *
266: * @param \SimpleID\Models\User $user the user to look up
267: * @param string $type the type URI to look up
268: * @return string|array<mixed>|null the value or NULL if not found
269: */
270: protected function getValue($user, $type) {
271: $ax = (isset($user['ax'])) ? $user['ax'] : [];
272: $sreg = (isset($user['sreg'])) ? $user['sreg'] : [];
273: $userinfo = (isset($user['userinfo'])) ? $user['userinfo'] : [];
274:
275: if (isset($ax[$type])) {
276: return $ax[$type];
277: } else {
278: // Look up OpenID Connect
279: switch ($type) {
280: case 'http://axschema.org/namePerson/friendly':
281: if (isset($userinfo['nickname'])) return $userinfo['nickname'];
282: break;
283: case 'http://axschema.org/contact/email':
284: if (isset($userinfo['email'])) return $userinfo['email'];
285: break;
286: case 'http://axschema.org/namePerson':
287: if (isset($userinfo['name'])) return $userinfo['name'];
288: break;
289: case 'http://axschema.org/pref/timezone':
290: if (isset($userinfo['zoneinfo'])) return $userinfo['zoneinfo'];
291: break;
292: case 'http://axschema.org/person/gender':
293: if (isset($userinfo['gender'])) return strtoupper(substr($userinfo['gender'], 0, 1));
294: break;
295: case 'http://axschema.org/contact/postalCode/home':
296: if (isset($userinfo['address']['postal_code'])) return $userinfo['address']['postcal_code'];
297: break;
298: }
299: }
300: return null;
301: }
302: }
303:
304: ?>
305: