1: <?php
2: /*
3: * SimpleID
4: *
5: * Copyright (C) Kelvin Mo 2014-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\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|false|null $secret_paths_mask if a string, the string
206: * to mask secret paths, or if false, remove secret paths completely
207: * @return array<string, mixed>
208: */
209: private function toSecureArray($secret_paths_mask = false) {
210: $event = new BaseDataCollectionEvent('user_secret_data_paths');
211: $copy = new ArrayWrapper($this->container);
212: \Events::instance()->dispatch($event);
213: $secret_paths = $event->getResults();
214: if ($secret_paths == null) $secret_paths = [];
215: $secret_paths[] = 'uid';
216: foreach ($secret_paths as $path) {
217: if (is_string($secret_paths_mask)) {
218: $copy->set($path, $secret_paths_mask);
219: } elseif ($secret_paths_mask === false) {
220: $copy->unset($path);
221: }
222: }
223: return $copy->toArray();
224: }
225:
226: /**
227: * {@inheritdoc}
228: */
229: public function serialize() {
230: $f3 = Base::instance();
231: return $f3->serialize($this->__serialize());
232: }
233:
234: /**
235: * PHP `__serialize` magic method.
236: *
237: * @see https://www.php.net/manual/en/language.oop5.magic.php#object.serialize
238: * @return array<string, mixed>
239: */
240: public function __serialize() {
241: $result = [];
242: foreach (get_object_vars($this) as $var => $value) {
243: if ($var == 'container') {
244: $result['container'] = $this->toSecureArray(null);
245: } else {
246: $result[$var] = $value;
247: }
248: }
249: return $result;
250: }
251:
252: /**
253: * {@inheritdoc}
254: */
255: public function unserialize($data) {
256: $f3 = Base::instance();
257:
258: /** @var array<string, mixed> $array */
259: $array = $f3->unserialize($data);
260: $this->__unserialize($array);
261: }
262:
263: /**
264: * PHP `__unserialize` magic method.
265: *
266: * @see https://www.php.net/manual/en/language.oop5.magic.php#object.unserialize
267: * @param array<string, mixed> $array
268: * @return void
269: */
270: public function __unserialize($array) {
271: foreach ($array as $var => $value) {
272: $this->$var = $value;
273: }
274: }
275:
276: public function getStoreType() {
277: return 'user';
278: }
279:
280:
281: public function getStoreID() {
282: return $this->uid;
283: }
284:
285:
286: public function setStoreID($id) {
287: $this->uid = $id;
288: }
289:
290: /**
291: * Returns a string representation of the user, with sensitive
292: * information removed.
293: *
294: * @return string
295: */
296: public function toString() {
297: $result = [];
298: foreach (get_object_vars($this) as $var => $value) {
299: if ($var == 'container') {
300: $result['container'] = $this->toSecureArray('[hidden]');
301: } else {
302: $result[$var] = $value;
303: }
304: }
305:
306: return print_r($result, true);
307: }
308: }
309:
310: ?>
311: