1: <?php
2: /*
3: * SimpleID
4: *
5: * Copyright (C) Kelvin Mo 2014-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: namespace SimpleID\Models;
24:
25: use \Base;
26: use \Serializable;
27: use SimpleID\ModuleManager;
28: use SimpleID\Crypt\OpaqueIdentifier;
29: use SimpleID\Store\Storable;
30: use SimpleID\Util\ArrayWrapper;
31: use SimpleID\Util\Events\BaseDataCollectionEvent;
32: use SimpleID\Base\UserModule;
33:
34: /**
35: * Represents a SimpleID user
36: */
37: class User extends ArrayWrapper implements Serializable, Storable {
38:
39: const ACTIVITY_LOG_SIZE = 10;
40:
41: /** @var string the user ID */
42: protected $uid;
43:
44: /** @var array<string, array<string, mixed>> the activity log */
45: protected $activities = [];
46:
47: /** @var array<string, array<string, mixed>> client preferences */
48: public $clients = [];
49:
50: /**
51: * @param array<string, mixed> $data
52: */
53: public function __construct($data = [ 'openid' => [] ]) {
54: parent::__construct($data);
55: }
56:
57: /**
58: * Determines whether the user is an administrator
59: *
60: * @return bool true if the user is an administrator
61: */
62: public function isAdministrator() {
63: return ($this->container['administrator']);
64: }
65:
66: /**
67: * Determines whether the user has a local OpenID identity
68: *
69: * @return bool true if the user has a local OpenID identity
70: */
71: public function hasLocalOpenIDIdentity() {
72: return isset($this->container['openid']['identity']);
73: }
74:
75: /**
76: * Returns the user's local OpenID identity
77: *
78: * @return string the user's local OpenID identity
79: */
80: public function getLocalOpenIDIdentity() {
81: return ($this->hasLocalOpenIDIdentity()) ? $this->container['openid']['identity'] : null;
82: }
83:
84: /**
85: * Generates a pairwise identity for this user, based on a specified
86: * sector identifier.
87: *
88: * A *pairwise identity* is an opaque string that is unique to one or more clients
89: * (identified by a common `$sector_identifier` parameter) which identifies the
90: * user to those clients. Issuing a pairwise identity means that the user's
91: * SimpleID user name is not exposed to the clients.
92: *
93: * @param string $sector_identifier the client's sector identifier.
94: * @return string the pairwise identity
95: */
96: public function getPairwiseIdentity($sector_identifier) {
97: $opaque = new OpaqueIdentifier();
98: return 'pwid:' . $opaque->generate($sector_identifier . ':' . $this->uid);
99: }
100:
101: /**
102: * Returns a display name for the user. The display name is worked
103: * out based on the data available.
104: *
105: * @return string the display name
106: */
107: public function getDisplayName() {
108: if (isset($this->container['userinfo']['name'])) return $this->container['userinfo']['name'];
109: if (isset($this->container['userinfo']['given_name']) && isset($this->container['userinfo']['family_name']))
110: return $this->container['userinfo']['given_name'] . ' ' . $this->container['userinfo']['family_name'];
111: return $this->uid;
112: }
113:
114: /**
115: * Add an entry to the user's recent activity log.
116: *
117: * The recent activity log contains the most recent authentication
118: * activity performed by or on behalf of the user. This includes instances
119: * where the user manually logged into SimpleID, or where a client
120: * requested authentication or authorisation from the user
121: *
122: * @param string $id the ID of the agent creating the activity - this could
123: * be the user agent ID assigned by SimpleID (in case of user logins) or
124: * the client ID
125: * @param array<string, mixed> $data additional data
126: * @return void
127: */
128: public function addActivity($id, $data) {
129: $this->activities[$id] = $data;
130: uasort($this->activities, function($a, $b) {
131: if ($a['time'] == $b['time']) { return 0; } return ($a['time'] < $b['time']) ? 1 : -1;
132: });
133: if (count($this->activities) > self::ACTIVITY_LOG_SIZE) {
134: $this->activities = array_slice($this->activities, 0, self::ACTIVITY_LOG_SIZE);
135: }
136: }
137:
138: /**
139: * Returns the user's recent activity log.
140: *
141: * @return array<string, array<string, mixed>> the activity log
142: */
143: public function getActivities() {
144: return $this->activities;
145: }
146:
147: /**
148: * Overrides {@link ArrayWrapper::loadData()} to copy over the activity
149: * log and client preferences
150: *
151: * @param array<mixed>|ArrayWrapper $data the data
152: * @return void
153: */
154: public function loadData($data) {
155: parent::loadData($data);
156: if ($data instanceof User) {
157: $this->activities = $data->activities;
158: $this->clients = $data->clients;
159: }
160: }
161:
162: public function offsetSet($offset, $value): void {
163: switch ($offset) {
164: case 'uid':
165: $this->uid = $value;
166: break;
167: case 'display_name':
168: return;
169: case 'identity':
170: $this->container['openid']['identity'] = $value;
171: break;
172: default:
173: parent::offsetSet($offset, $value);
174: }
175: }
176:
177: public function offsetExists($offset): bool {
178: switch ($offset) {
179: case 'uid':
180: case 'display_name':
181: return true;
182: case 'identity':
183: return $this->hasLocalOpenIDIdentity();
184: default:
185: return parent::offsetExists($offset);
186: }
187: }
188:
189: public function offsetGet($offset): mixed {
190: switch ($offset) {
191: case 'uid':
192: return $this->uid;
193: case 'display_name':
194: return $this->getDisplayName();
195: case 'identity':
196: // Retained for compatibility purposes
197: $mod = UserModule::instance();
198: return ($this->hasLocalOpenIDIdentity()) ? $this->getLocalOpenIDIdentity() : $mod->getCanonicalURL('user/' . rawurlencode($this['uid']));
199: default:
200: return parent::offsetGet($offset);
201: }
202: }
203:
204: /**
205: * @param string|null $hidden_value
206: * @return array<string, mixed>
207: */
208: private function toSecureArray($hidden_value = null) {
209: $event = new BaseDataCollectionEvent('user_secret_data_paths');
210: $copy = new ArrayWrapper($this->container);
211: \Events::instance()->dispatch($event);
212: $secret_paths = $event->getResults();
213: if ($secret_paths == null) $secret_paths = [];
214: $secret_paths[] = 'uid';
215: foreach ($secret_paths as $path) {
216: if ($hidden_value) {
217: $copy->set($path, $hidden_value);
218: } else {
219: $copy->unset($path);
220: }
221: }
222: return $copy->toArray();
223: }
224:
225: /**
226: * {@inheritdoc}
227: */
228: public function serialize() {
229: $f3 = Base::instance();
230: return $f3->serialize($this->__serialize());
231: }
232:
233: /**
234: * PHP `__serialize` magic method.
235: *
236: * @see https://www.php.net/manual/en/language.oop5.magic.php#object.serialize
237: * @return array<string, mixed>
238: */
239: public function __serialize() {
240: $result = [];
241: foreach (get_object_vars($this) as $var => $value) {
242: if ($var == 'container') {
243: $result['container'] = $this->toSecureArray();
244: } else {
245: $result[$var] = $value;
246: }
247: }
248: return $result;
249: }
250:
251: /**
252: * {@inheritdoc}
253: */
254: public function unserialize($data) {
255: $f3 = Base::instance();
256:
257: /** @var array<string, mixed> $array */
258: $array = $f3->unserialize($data);
259: $this->__unserialize($array);
260: }
261:
262: /**
263: * PHP `__unserialize` magic method.
264: *
265: * @see https://www.php.net/manual/en/language.oop5.magic.php#object.unserialize
266: * @param array<string, mixed> $array
267: * @return void
268: */
269: public function __unserialize($array) {
270: foreach ($array as $var => $value) {
271: $this->$var = $value;
272: }
273: }
274:
275: public function getStoreType() {
276: return 'user';
277: }
278:
279:
280: public function getStoreID() {
281: return $this->uid;
282: }
283:
284:
285: public function setStoreID($id) {
286: $this->uid = $id;
287: }
288:
289: /**
290: * Returns a string representation of the user, with sensitive
291: * information removed.
292: *
293: * @return string
294: */
295: public function toString() {
296: $result = [];
297: foreach (get_object_vars($this) as $var => $value) {
298: if ($var == 'container') {
299: $result['container'] = $this->toSecureArray('[hidden]');
300: } else {
301: $result[$var] = $value;
302: }
303: }
304:
305: return print_r($result, true);
306: }
307: }
308:
309: ?>
310: