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: namespace SimpleID\Base;
24:
25: use SimpleID\Auth\AuthManager;
26: use SimpleID\Module;
27: use SimpleID\ModuleManager;
28: use SimpleID\Base\ConsentEvent;
29: use SimpleID\Crypt\SecurityToken;
30: use SimpleID\Protocols\ProtocolResultEvent;
31: use SimpleID\Store\StoreManager;
32: use SimpleID\Util\Events\OrderedDataCollectionEvent;
33: use SimpleID\Util\Events\UIBuildEvent;
34: use SimpleID\Util\UI\Template;
35:
36: /**
37: * Functions for displaying various pages in SimpleID.
38: *
39: * @since 0.7
40: */
41: class MyModule extends Module {
42: static function init($f3) {
43: $f3->route('GET /my/dashboard', 'SimpleID\Base\MyModule->dashboard');
44: $f3->route('GET /my/apps [sync]', 'SimpleID\Base\MyModule->apps_sync');
45: $f3->route('GET /my/profile', 'SimpleID\Base\MyModule->profile');
46: /* AJAX handlers */
47: $f3->route('GET /my/apps [ajax]', 'SimpleID\Base\MyModule->apps_ajax');
48: $f3->map('/my/apps/@cid', 'SimpleID\Base\MyModule');
49: }
50:
51: public function beforeroute() {
52: parent::beforeroute();
53:
54: $auth = AuthManager::instance();
55: if (!$auth->isLoggedIn()) {
56: if ($this->f3->get('AJAX')) {
57: $this->f3->status(401);
58: header('Content-Type: application/json');
59: print json_encode([
60: 'error' => 'unauthorized',
61: 'error_description' => $this->f3->get('intl.common.unauthorized')
62: ]);
63: exit;
64: } else {
65: $route = ltrim($this->f3->get('PARAMS.0'), '/');
66: $this->f3->reroute('@auth_login(1=' . $route . ')');
67: }
68: }
69:
70: if (!$this->f3->get('AJAX')) {
71: $this->f3->set('user_header', true);
72: $this->f3->set('logout_link', true);
73: $this->insertNav();
74: }
75: }
76:
77: /**
78: * Displays the dashboard page.
79: *
80: * @return void
81: */
82: public function dashboard() {
83: $this->blocksPage($this->f3->get('intl.core.my.dashboard_title'), 'dashboard_blocks');
84: }
85:
86: /**
87: * Displays the profile page.
88: *
89: * @return void
90: */
91: public function profile() {
92: $this->blocksPage($this->f3->get('intl.core.my.profile_title'), 'profile_blocks');
93: }
94:
95: /**
96: * Returns the sites page.
97: *
98: * @return void
99: */
100: public function apps_sync() {
101: // Require HTTPS, redirect if necessary
102: $this->checkHttps('redirect', true);
103:
104: $token = new SecurityToken();
105:
106: $tpl = Template::instance();
107: $this->f3->set('tk', $token->generate('apps', SecurityToken::OPTION_BIND_SESSION));
108: $this->f3->set('title', $this->f3->get('intl.core.my.apps_title'));
109: $this->f3->set('js_data.intl', [
110: 'first_time_label' => $this->f3->get('intl.core.my.first_time_label'),
111: 'last_time_label' => $this->f3->get('intl.core.my.last_time_label'),
112: 'consents_label' => $this->f3->get('intl.core.my.consents_label'),
113: 'app_confirm_delete' => $this->f3->get('intl.core.my.app_confirm_delete')
114: ]);
115: $this->f3->set('layout', 'my_apps.html');
116: print $tpl->render('page.html');
117: }
118:
119: /**
120: * @return void
121: */
122: public function apps_ajax() {
123: $this->checkHttps('error', true);
124:
125: header('Content-Type: application/json');
126:
127: $token = new SecurityToken();
128: if (!$this->f3->exists('HEADERS.X-Request-Token') || !$token->verify($this->f3->get('HEADERS.X-Request-Token'), 'apps')) {
129: $this->f3->status(401);
130: print json_encode([
131: 'error' => 'unauthorized',
132: 'error_description' => $this->f3->get('intl.common.unauthorized')
133: ]);
134: return;
135: }
136:
137: $auth = AuthManager::instance();
138: $user = $auth->getUser();
139: $prefs = $user->clients;
140:
141: uasort($prefs, function ($a, $b) {
142: return strcasecmp($a['display_name'], $b['display_name']);
143: });
144:
145: $event = new ScopeInfoCollectionEvent();
146: \Events::instance()->dispatch($event);
147: $scope_info = $event->getAllScopeInfo();
148:
149: $results = [];
150: foreach ($prefs as $cid => $client_prefs) {
151: $consent_info = [];
152: foreach ($client_prefs['consents'] as $type => $consents) {
153: if (is_array($consents)) {
154: foreach ($consents as $consent) {
155: $consent_info[] = [
156: 'description' => isset($scope_info[$type][$consent]['description']) ? $scope_info[$type][$consent]['description'] : $type . ':' . $consent,
157: 'weight' => isset($scope_info[$type][$consent]['weight']) ? $scope_info[$type][$consent]['weight'] : 0
158: ];
159: }
160: } elseif ($consents) {
161: $consent_info[] = [
162: 'description' => isset($scope_info[$type]['description']) ? $scope_info[$type]['description'] : $type,
163: 'weight' => isset($scope_info[$type]['weight']) ? $scope_info[$type]['weight'] : 0
164: ];
165: }
166: }
167: usort($consent_info, function ($a, $b) {
168: return $a['weight'] - $b['weight'];
169: });
170:
171: $results[] = [
172: 'cid' => $cid,
173: 'display_name' => $client_prefs['display_name'],
174: 'display_html' => $client_prefs['display_html'],
175: 'first_time' => $this->f3->format('{0,date} {0,time}', $client_prefs['first_time']),
176: 'last_time' => $this->f3->format('{0,date} {0,time}', $client_prefs['last_time']),
177: 'consents' => $consent_info
178: ];
179: }
180:
181: print json_encode($results);
182: }
183:
184: /**
185: * @param \Base $f3
186: * @param array<string, mixed> $params
187: * @return void
188: */
189: public function delete($f3, $params) {
190: $this->checkHttps('error', true);
191: parse_str($this->f3->get('BODY'), $delete);
192:
193: header('Content-Type: application/json');
194:
195: $token = new SecurityToken();
196: if (!$this->f3->exists('HEADERS.X-Request-Token') || !$token->verify($this->f3->get('HEADERS.X-Request-Token'), 'apps')) {
197: $this->f3->status(401);
198: print json_encode([
199: 'error' => 'unauthorized',
200: 'error_description' => $this->f3->get('intl.common.unauthorized'),
201: ]);
202: return;
203: }
204:
205: $auth = AuthManager::instance();
206: $user = $auth->getUser();
207: $prefs = &$user->clients;
208: if (!isset($prefs[$params['cid']])) {
209: $this->f3->status(404);
210: print json_encode([
211: 'error' => 'not_found',
212: 'error_description' => $this->f3->get('intl.common.not_found')
213: ]);
214: return;
215: }
216:
217: $event = new ConsentEvent('consent_revoke', $user, $params['cid'], $prefs[$params['cid']]);
218: \Events::instance()->dispatch($event);
219:
220: unset($prefs[$params['cid']]);
221:
222: $store = StoreManager::instance();
223: $store->saveUser($user);
224:
225: print json_encode([
226: 'result' => 'success',
227: 'result_description' => $this->f3->get('intl.core.my.app_delete_success')
228: ]);
229: }
230:
231: /**
232: * @return void
233: */
234: public function onNav(OrderedDataCollectionEvent $event) {
235: $event->addResult([ 'name' => $this->f3->get('intl.core.my.dashboard_title'), 'path' =>'my/dashboard' ], -10);
236: $event->addResult([ 'name' => $this->f3->get('intl.core.my.profile_title'), 'path' =>'my/profile' ], -9);
237: $event->addResult([ 'name' => $this->f3->get('intl.core.my.apps_title'), 'path' =>'my/apps' ], -8);
238: }
239:
240: /**
241: * Returns the welcome block.
242: *
243: * @param UIBuildEvent $event the event to pick up the welcome block
244: * @return void
245: */
246: public function onDashboardBlocks(UIBuildEvent $event) {
247: $auth = AuthManager::instance();
248: $user = $auth->getUser();
249: $tpl = Template::instance();
250:
251: $event->addBlock('welcome', $this->f3->get('intl.core.my.logged_in_as', [ $user->getDisplayName(), $user['uid'] ]), -10, [
252: 'title' => $this->f3->get('intl.core.my.welcome_title')
253: ]);
254:
255: $event->addBlock('activity', $tpl->render('my_activity.html', false), 0, [
256: 'title' => $this->f3->get('intl.core.my.activity_title')
257: ]);
258:
259: if ($this->f3->get('config.debug')) {
260: $event->addBlock('auth', '<pre class="code">' . $this->f3->encode($auth->toString()) . '</pre>', 10, [
261: 'title' => $this->f3->get('intl.core.my.debug_auth_title')
262: ]);
263:
264: $event->addBlock('user', '<pre class="code">' . $this->f3->encode($user->toString()) . '</pre>', 10, [
265: 'title' => $this->f3->get('intl.core.my.debug_user_title')
266: ]);
267: }
268: }
269:
270: /**
271: * Saves a positive assertion result to the user's activity log.
272: *
273: * @param ProtocolResultEvent $event the assertion result event
274: * @return void
275: */
276: public function onProtocolResultEvent(ProtocolResultEvent $event) {
277: if ($event->isPositiveAssertion()) {
278: /** @var \SimpleID\Models\User $user */
279: $user = $event->getSubject();
280: $client_id = $event->getClient()->getStoreID();
281:
282: $activity = [
283: 'type' => 'app',
284: 'id' => $client_id,
285: 'time' => $event->getTime()->getTimestamp()
286: ];
287: if ($event->getIP()) $activity['remote'] = $event->getIP();
288: $user->addActivity($client_id, $activity);
289: }
290: }
291:
292: /**
293: * @return void
294: */
295: public function insertNav() {
296: $event = new OrderedDataCollectionEvent('nav');
297: \Events::instance()->dispatch($event);
298: $this->f3->set('nav', $event->getResults());
299: }
300:
301: /**
302: * Generic function to display a page comprising blocks returned
303: * from a hook.
304: *
305: * @param string $title the page title
306: * @param string $event_name the hook to call
307: * @return void
308: */
309: protected function blocksPage($title, $event_name) {
310: // Require HTTPS, redirect if necessary
311: $this->checkHttps('redirect', true);
312:
313: $event = new UIBuildEvent($event_name);
314: \Events::instance()->dispatch($event);
315:
316: $tpl = Template::instance();
317: $tpl->mergeAttachments($event);
318: $this->f3->set('blocks', $event->getBlocks());
319: $this->f3->set('title', $title);
320: $this->f3->set('layout', 'my_blocks.html');
321: print $tpl->render('page.html');
322: }
323: }
324:
325: ?>
326: