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\Upgrade;
24:
25: use Symfony\Component\Yaml\Yaml;
26: use Composer\Semver\Comparator;
27: use Composer\Semver\Semver;
28: use SimpleID\Module;
29: use SimpleID\ModuleManager;
30: use SimpleID\Auth\AuthManager;
31: use SimpleID\Crypt\Random;
32: use SimpleID\Store\StoreManager;
33: use SimpleID\Util\SecurityToken;
34: use SimpleID\Util\Events\BaseDataCollectionEvent;
35: use SimpleID\Util\UI\Template;
36:
37: /**
38: * SimpleID upgrade module.
39: *
40: * This script performs various upgrades to SimpleID's storage backend, which
41: * are required for different versions of SimpleID.
42: */
43: class UpgradeModule extends Module {
44: static function init($f3) {
45: $f3->route('GET|POST /', 'SimpleID\Upgrade\UpgradeModule->info');
46: $f3->route('POST /select', 'SimpleID\Upgrade\UpgradeModule->select');
47: $f3->route('POST /apply [sync]', 'SimpleID\Upgrade\UpgradeModule->apply');
48: $f3->route('POST /step [ajax]', 'SimpleID\Upgrade\UpgradeModule->applyStep');
49: $f3->route('GET /complete', 'SimpleID\Upgrade\UpgradeModule->complete');
50: }
51:
52: function beforeroute() {
53: global $upgrade_access_check;
54:
55: parent::beforeroute();
56:
57: $auth = AuthManager::instance();
58: if ($upgrade_access_check) {
59: if (!$auth->isLoggedIn() || !$auth->getUser()->isAdministrator())
60: $this->accessDenied();
61: }
62:
63: $this->f3->set('upgrade_access_check', $upgrade_access_check);
64: }
65:
66: /**
67: * Displays the upgrade info page.
68: */
69: function info() {
70: $tpl = Template::instance();
71:
72: $this->f3->set('upgrade_url', 'http://simpleid.org/documentation/getting-started/upgrading');
73:
74: $token = new SecurityToken();
75: $this->f3->set('tk', $token->generate('upgrade_info', SecurityToken::OPTION_BIND_SESSION));
76:
77: $this->f3->set('title', $this->f3->get('intl.upgrade.upgrade_title'));
78: $this->f3->set('page_class', 'is-dialog-page');
79: $this->f3->set('layout', 'upgrade_info.html');
80:
81: print $tpl->render('page.html');
82: }
83:
84: /**
85: * Detects the current installed version of SimpleID, selects the individual upgrade
86: * functions applicable to this upgrade and displays the upgrade
87: * selection page.
88: */
89: function select() {
90: global $upgrade_access_check;
91:
92: $token = new SecurityToken();
93: if (($this->f3->exists('POST.tk') === false) || !$token->verify($this->f3->get('POST.tk'), 'upgrade_info')) {
94: $this->f3->set('message', $this->f3->get('intl.common.invalid_tk'));
95: $this->info();
96: return;
97: }
98:
99: $tpl = Template::instance();
100: $cache = \Cache::instance();
101:
102: $cache->reset('.upgrade');
103:
104: $list = $this->getUpgradeList();
105:
106: if (count($list) != 0) {
107: $rand = new Random();
108:
109: $upgid = $rand->id();
110: $cache->set($upgid . '.upgrade', [ 'list' => $list, 'results' => '' ]);
111: $this->f3->set('upgid', $upgid);
112:
113: $this->f3->set('tk', $token->generate('upgrade_selection', SecurityToken::OPTION_BIND_SESSION));
114: }
115:
116: $this->f3->set('original_version', $this->getVersion());
117: $this->f3->set('this_version', SIMPLEID_VERSION);
118:
119: $this->f3->set('title', $this->f3->get('intl.upgrade.upgrade_title'));
120: $this->f3->set('page_class', 'is-dialog-page');
121: $this->f3->set('layout', 'upgrade_selection.html');
122:
123: print $tpl->render('page.html');
124: }
125:
126: /**
127: * Applies the upgrade.
128: */
129: function apply() {
130: $token = new SecurityToken();
131: if (($this->f3->exists('POST.tk') === false) || !$token->verify($this->f3->get('POST.tk'), 'upgrade_selection')) {
132: $this->f3->set('message', $this->f3->get('intl.common.invalid_tk'));
133: $this->info();
134: return;
135: }
136:
137: $step = $token->generate([ 'upgid' => $this->f3->get('POST.upgid'), 'step' => 0 ], SecurityToken::OPTION_BIND_SESSION);
138: $this->f3->set('step', $step);
139:
140: $this->f3->set('title', $this->f3->get('intl.upgrade.upgrade_title'));
141: $this->f3->set('page_class', 'is-dialog-page');
142: $this->f3->set('layout', 'upgrade_apply.html');
143:
144: print $tpl->render('page.html');
145: }
146:
147: /**
148: * Applies a single step of the upgrade.
149: */
150: function applyStep() {
151: header('Content-Type: application/json');
152:
153: $cache = \Cache::instance();
154:
155: $token = new SecurityToken();
156: if (!$this->f3->exists('POST.step')) {
157: $this->f3->status(401);
158: print json_encode([
159: 'status' => 'error',
160: 'error' => 'unauthorized',
161: 'error_description' => $this->f3->get('intl.common.unauthorized')
162: ]);
163: return;
164: }
165:
166: $payload = $token->getPayload($this->f3->get('POST.step'));
167: if ($payload == null) {
168: $this->f3->status(401);
169: print json_encode([
170: 'status' => 'error',
171: 'error' => 'unauthorized',
172: 'error_description' => $this->f3->get('intl.common.unauthorized')
173: ]);
174: return;
175: }
176:
177: $upgid = $payload['upgid'];
178: $step = $payload['step'];
179:
180: $upgrade = $cache->get($payload['upgid'] . '.upgrade');
181:
182: if ($upgrade === false) {
183: $this->f3->status(500);
184: print json_encode([
185: 'status' => 'error',
186: 'error' => 'upgrade_error',
187: 'error_description' => $this->f3->get('intl.upgrade.upgrade_not_found')
188: ]);
189: return;
190: }
191:
192: $function = $upgrade['list'][$step];
193: $upgrade['results'] .= $this->f3->call($function);
194:
195: $next = $token->generate([ 'upgid' => $upgid, 'step' => $step + 1 ], SecurityToken::OPTION_BIND_SESSION);
196: if ($step < count($upgrade['list']) - 1) {
197: print json_encode([
198: 'status' => 'next',
199: 'next' => $next,
200: 'progress' => $this->f3->format('{0,number,percent}', ($step + 1) / count($upgrade['list']))
201: ]);
202: } else {
203: print json_encode([
204: 'status' => 'complete',
205: 'redirect' => 'complete?tk=' . rawurlencode($next)
206: ]);
207: }
208: }
209:
210: /**
211: * Applies the upgrade.
212: */
213: function complete() {
214: global $upgrade_access_check;
215:
216: $cache = \Cache::instance();
217:
218: $token = new SecurityToken();
219: if (!$this->f3->exists('GET.tk')) {
220: $this->fatalError($this->f3->get('intl.common.invalid_tk'), 401);
221: return;
222: }
223:
224: $payload = $token->getPayload($this->f3->get('POST.step'));
225: if ($payload == null) {
226: $this->fatalError($this->f3->get('intl.common.invalid_tk'), 401);
227: return;
228: }
229:
230: $upgid = $payload['upgid'];
231:
232: $upgrade = $cache->get($upgid . '.upgrade');
233: $cache->reset('.upgrade');
234:
235: if ($upgrade === false) {
236: $this->fatalError($this->f3->get('intl.upgrade.upgrade_not_found'));
237: }
238:
239: $this->f3->set('results', $upgrade['results']);
240:
241: $this->f3->set('title', $this->f3->get('intl.upgrade.upgrade_title'));
242: $this->f3->set('page_class', 'is-dialog-page');
243: $this->f3->set('layout', 'upgrade_results.html');
244:
245: print $tpl->render('page.html');
246: }
247:
248: /**
249: * Displays a page notifying the user that he or she does not have permission to
250: * run the upgrade script.
251: */
252: protected function accessDenied() {
253: $tpl = Template::instance();
254:
255: $this->f3->set('upgrade_url', 'https://simpleid.org/docs/2/upgrading/#upgrade');
256:
257: $this->f3->set('title', $this->f3->get('intl.common.access_denied'));
258: $this->f3->set('layout', 'upgrade_access_denied.html');
259: print $tpl->render('page.html');
260: exit;
261: }
262:
263: /**
264: * Detects the current installed version of SimpleID
265: *
266: * The current installed version of SimpleID is taken from the `version`
267: * application setting.
268: *
269: * @return string the detected version
270: */
271: protected function getVersion() {
272: $store = StoreManager::instance();
273: return $store->getSetting('version') ? : SIMPLEID_VERSION;
274: }
275:
276: /**
277: * Sets the current version of SimpleID.
278: *
279: * This function sets the version application setting via {@link \SimpleID\Store\StoreManager::setSetting()}.
280: * A specific version can be specified, or it can be taken from {@link SIMPLEID_VERSION}.
281: *
282: * @param string $version the version to set
283: */
284: public function setVersion($version = NULL) {
285: $store = StoreManager::instance();
286: if ($version == NULL) $version = SIMPLEID_VERSION;
287: $store->setSetting('version', $version);
288: }
289:
290: /**
291: * Selects the upgrade functions applicable for this upgrade.
292: *
293: * The upgrade functions are specified by the `upgradeList`
294: * hook. This variable is an associative array containing version numbers
295: * as keys and an array of upgrade function names as values. This function
296: * merges all the upgrade function names of the version between the current
297: * installed version and the upgraded version.
298: *
299: * @param string $version the version of SimpleID to upgrade from, calls
300: * {@link getVersion()} if not specified
301: * @return array an array of strings, containing the list of upgrade functions
302: * to call. The functions should be called in the same order as they appear
303: * in this array
304: * @see SimpleID\API\ModuleHooks::upgradeListHook()
305: */
306: protected function getUpgradeList($version = NULL) {
307: $mgr = ModuleManager::instance();
308:
309: $listeners = \Listeners::instance();
310: foreach ($mgr->getModules() as $name => $module) {
311: $listeners->map($module);
312: }
313:
314: $event = new BaseDataCollectionEvent('upgrade_list', BaseDataCollectionEvent::MERGE_RECURSIVE);
315: \Events::instance()->dispatch($event);
316: $upgrade_data = $event->getResults();
317:
318: if ($version == NULL) $version = $this->getVersion();
319: $list = [];
320:
321: // Sorts versions from newest to oldest
322: $versions = array_keys($upgrade_data);
323: $versions = Semver::rsort($versions);
324:
325: foreach ($versions as $upgrade_version) {
326: if (Comparator::lessThan($version, $upgrade_version)) {
327: $list = array_merge($list, $upgrade_data[$upgrade_version]);
328: }
329: }
330:
331: if (Comparator::lessThan($version, SIMPLEID_VERSION)) $list[] = 'SimpleID\Upgrade->setVersion';
332:
333: return $list;
334: }
335:
336: public function onUpgradeList(BaseDataCollectionEvent $event) {
337: $event->addResults(Yaml::parseFile(__DIR__ . '/upgrade.yml'));
338: }
339: }
340:
341:
342: ?>
343: