1<?php
2
3declare(strict_types=1);
4
5/**
6 * @copyright Copyright (c) 2016, ownCloud, Inc.
7 * @copyright Copyright (c) 2016, Lukas Reschke <lukas@statuscode.ch>
8 *
9 * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
10 * @author Bjoern Schiessle <bjoern@schiessle.org>
11 * @author Christoph Wurst <christoph@winzerhof-wurst.at>
12 * @author Frank Karlitschek <frank@karlitschek.de>
13 * @author Georg Ehrke <oc.list@georgehrke.com>
14 * @author J0WI <J0WI@users.noreply.github.com>
15 * @author Joas Schilling <coding@schilljs.com>
16 * @author Julius Härtl <jus@bitgrid.net>
17 * @author Lukas Reschke <lukas@statuscode.ch>
18 * @author Morris Jobke <hey@morrisjobke.de>
19 * @author Robin Appelman <robin@icewind.nl>
20 * @author Roeland Jago Douma <roeland@famdouma.nl>
21 * @author Steffen Lindner <mail@steffen-lindner.de>
22 * @author Thomas Müller <thomas.mueller@tmit.eu>
23 * @author Victor Dubiniuk <dubiniuk@owncloud.com>
24 * @author Vincent Petry <vincent@nextcloud.com>
25 *
26 * @license AGPL-3.0
27 *
28 * This code is free software: you can redistribute it and/or modify
29 * it under the terms of the GNU Affero General Public License, version 3,
30 * as published by the Free Software Foundation.
31 *
32 * This program is distributed in the hope that it will be useful,
33 * but WITHOUT ANY WARRANTY; without even the implied warranty of
34 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
35 * GNU Affero General Public License for more details.
36 *
37 * You should have received a copy of the GNU Affero General Public License, version 3,
38 * along with this program. If not, see <http://www.gnu.org/licenses/>
39 *
40 */
41namespace OC;
42
43use OC\App\AppManager;
44use OC\DB\Connection;
45use OC\DB\MigrationService;
46use OC\Hooks\BasicEmitter;
47use OC\IntegrityCheck\Checker;
48use OC_App;
49use OCP\App\IAppManager;
50use OCP\HintException;
51use OCP\IConfig;
52use OCP\ILogger;
53use OCP\Util;
54use Psr\Log\LoggerInterface;
55use Symfony\Component\EventDispatcher\GenericEvent;
56
57/**
58 * Class that handles autoupdating of ownCloud
59 *
60 * Hooks provided in scope \OC\Updater
61 *  - maintenanceStart()
62 *  - maintenanceEnd()
63 *  - dbUpgrade()
64 *  - failure(string $message)
65 */
66class Updater extends BasicEmitter {
67
68	/** @var LoggerInterface */
69	private $log;
70
71	/** @var IConfig */
72	private $config;
73
74	/** @var Checker */
75	private $checker;
76
77	/** @var Installer */
78	private $installer;
79
80	private $logLevelNames = [
81		0 => 'Debug',
82		1 => 'Info',
83		2 => 'Warning',
84		3 => 'Error',
85		4 => 'Fatal',
86	];
87
88	public function __construct(IConfig $config,
89								Checker $checker,
90								?LoggerInterface $log,
91								Installer $installer) {
92		$this->log = $log;
93		$this->config = $config;
94		$this->checker = $checker;
95		$this->installer = $installer;
96	}
97
98	/**
99	 * runs the update actions in maintenance mode, does not upgrade the source files
100	 * except the main .htaccess file
101	 *
102	 * @return bool true if the operation succeeded, false otherwise
103	 */
104	public function upgrade(): bool {
105		$this->emitRepairEvents();
106		$this->logAllEvents();
107
108		$logLevel = $this->config->getSystemValue('loglevel', ILogger::WARN);
109		$this->emit('\OC\Updater', 'setDebugLogLevel', [ $logLevel, $this->logLevelNames[$logLevel] ]);
110		$this->config->setSystemValue('loglevel', ILogger::DEBUG);
111
112		$wasMaintenanceModeEnabled = $this->config->getSystemValueBool('maintenance');
113
114		if (!$wasMaintenanceModeEnabled) {
115			$this->config->setSystemValue('maintenance', true);
116			$this->emit('\OC\Updater', 'maintenanceEnabled');
117		}
118
119		// Clear CAN_INSTALL file if not on git
120		if (\OC_Util::getChannel() !== 'git' && is_file(\OC::$configDir.'/CAN_INSTALL')) {
121			if (!unlink(\OC::$configDir . '/CAN_INSTALL')) {
122				$this->log->error('Could not cleanup CAN_INSTALL from your config folder. Please remove this file manually.');
123			}
124		}
125
126		$installedVersion = $this->config->getSystemValue('version', '0.0.0');
127		$currentVersion = implode('.', \OCP\Util::getVersion());
128
129		$this->log->debug('starting upgrade from ' . $installedVersion . ' to ' . $currentVersion, ['app' => 'core']);
130
131		$success = true;
132		try {
133			$this->doUpgrade($currentVersion, $installedVersion);
134		} catch (HintException $exception) {
135			$this->log->error($exception->getMessage(), [
136				'exception' => $exception,
137			]);
138			$this->emit('\OC\Updater', 'failure', [$exception->getMessage() . ': ' .$exception->getHint()]);
139			$success = false;
140		} catch (\Exception $exception) {
141			$this->log->error($exception->getMessage(), [
142				'exception' => $exception,
143			]);
144			$this->emit('\OC\Updater', 'failure', [get_class($exception) . ': ' .$exception->getMessage()]);
145			$success = false;
146		}
147
148		$this->emit('\OC\Updater', 'updateEnd', [$success]);
149
150		if (!$wasMaintenanceModeEnabled && $success) {
151			$this->config->setSystemValue('maintenance', false);
152			$this->emit('\OC\Updater', 'maintenanceDisabled');
153		} else {
154			$this->emit('\OC\Updater', 'maintenanceActive');
155		}
156
157		$this->emit('\OC\Updater', 'resetLogLevel', [ $logLevel, $this->logLevelNames[$logLevel] ]);
158		$this->config->setSystemValue('loglevel', $logLevel);
159		$this->config->setSystemValue('installed', true);
160
161		return $success;
162	}
163
164	/**
165	 * Return version from which this version is allowed to upgrade from
166	 *
167	 * @return array allowed previous versions per vendor
168	 */
169	private function getAllowedPreviousVersions(): array {
170		// this should really be a JSON file
171		require \OC::$SERVERROOT . '/version.php';
172		/** @var array $OC_VersionCanBeUpgradedFrom */
173		return $OC_VersionCanBeUpgradedFrom;
174	}
175
176	/**
177	 * Return vendor from which this version was published
178	 *
179	 * @return string Get the vendor
180	 */
181	private function getVendor(): string {
182		// this should really be a JSON file
183		require \OC::$SERVERROOT . '/version.php';
184		/** @var string $vendor */
185		return (string) $vendor;
186	}
187
188	/**
189	 * Whether an upgrade to a specified version is possible
190	 * @param string $oldVersion
191	 * @param string $newVersion
192	 * @param array $allowedPreviousVersions
193	 * @return bool
194	 */
195	public function isUpgradePossible(string $oldVersion, string $newVersion, array $allowedPreviousVersions): bool {
196		$version = explode('.', $oldVersion);
197		$majorMinor = $version[0] . '.' . $version[1];
198
199		$currentVendor = $this->config->getAppValue('core', 'vendor', '');
200
201		// Vendor was not set correctly on install, so we have to white-list known versions
202		if ($currentVendor === '' && (
203			isset($allowedPreviousVersions['owncloud'][$oldVersion]) ||
204			isset($allowedPreviousVersions['owncloud'][$majorMinor])
205		)) {
206			$currentVendor = 'owncloud';
207			$this->config->setAppValue('core', 'vendor', $currentVendor);
208		}
209
210		if ($currentVendor === 'nextcloud') {
211			return isset($allowedPreviousVersions[$currentVendor][$majorMinor])
212				&& (version_compare($oldVersion, $newVersion, '<=') ||
213					$this->config->getSystemValue('debug', false));
214		}
215
216		// Check if the instance can be migrated
217		return isset($allowedPreviousVersions[$currentVendor][$majorMinor]) ||
218			isset($allowedPreviousVersions[$currentVendor][$oldVersion]);
219	}
220
221	/**
222	 * runs the update actions in maintenance mode, does not upgrade the source files
223	 * except the main .htaccess file
224	 *
225	 * @param string $currentVersion current version to upgrade to
226	 * @param string $installedVersion previous version from which to upgrade from
227	 *
228	 * @throws \Exception
229	 */
230	private function doUpgrade(string $currentVersion, string $installedVersion): void {
231		// Stop update if the update is over several major versions
232		$allowedPreviousVersions = $this->getAllowedPreviousVersions();
233		if (!$this->isUpgradePossible($installedVersion, $currentVersion, $allowedPreviousVersions)) {
234			throw new \Exception('Updates between multiple major versions and downgrades are unsupported.');
235		}
236
237		// Update .htaccess files
238		try {
239			Setup::updateHtaccess();
240			Setup::protectDataDirectory();
241		} catch (\Exception $e) {
242			throw new \Exception($e->getMessage());
243		}
244
245		// create empty file in data dir, so we can later find
246		// out that this is indeed an ownCloud data directory
247		// (in case it didn't exist before)
248		file_put_contents($this->config->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data') . '/.ocdata', '');
249
250		// pre-upgrade repairs
251		$repair = new Repair(Repair::getBeforeUpgradeRepairSteps(), \OC::$server->getEventDispatcher(), \OC::$server->get(LoggerInterface::class));
252		$repair->run();
253
254		$this->doCoreUpgrade();
255
256		try {
257			// TODO: replace with the new repair step mechanism https://github.com/owncloud/core/pull/24378
258			Setup::installBackgroundJobs();
259		} catch (\Exception $e) {
260			throw new \Exception($e->getMessage());
261		}
262
263		// update all shipped apps
264		$this->checkAppsRequirements();
265		$this->doAppUpgrade();
266
267		// Update the appfetchers version so it downloads the correct list from the appstore
268		\OC::$server->getAppFetcher()->setVersion($currentVersion);
269
270		/** @var IAppManager|AppManager $appManager */
271		$appManager = \OC::$server->getAppManager();
272
273		// upgrade appstore apps
274		$this->upgradeAppStoreApps($appManager->getInstalledApps());
275		$autoDisabledApps = $appManager->getAutoDisabledApps();
276		if (!empty($autoDisabledApps)) {
277			$this->upgradeAppStoreApps(array_keys($autoDisabledApps), $autoDisabledApps);
278		}
279
280		// install new shipped apps on upgrade
281		$errors = Installer::installShippedApps(true);
282		foreach ($errors as $appId => $exception) {
283			/** @var \Exception $exception */
284			$this->log->error($exception->getMessage(), [
285				'exception' => $exception,
286				'app' => $appId,
287			]);
288			$this->emit('\OC\Updater', 'failure', [$appId . ': ' . $exception->getMessage()]);
289		}
290
291		// post-upgrade repairs
292		$repair = new Repair(Repair::getRepairSteps(), \OC::$server->getEventDispatcher(), \OC::$server->get(LoggerInterface::class));
293		$repair->run();
294
295		//Invalidate update feed
296		$this->config->setAppValue('core', 'lastupdatedat', 0);
297
298		// Check for code integrity if not disabled
299		if (\OC::$server->getIntegrityCodeChecker()->isCodeCheckEnforced()) {
300			$this->emit('\OC\Updater', 'startCheckCodeIntegrity');
301			$this->checker->runInstanceVerification();
302			$this->emit('\OC\Updater', 'finishedCheckCodeIntegrity');
303		}
304
305		// only set the final version if everything went well
306		$this->config->setSystemValue('version', implode('.', Util::getVersion()));
307		$this->config->setAppValue('core', 'vendor', $this->getVendor());
308	}
309
310	protected function doCoreUpgrade(): void {
311		$this->emit('\OC\Updater', 'dbUpgradeBefore');
312
313		// execute core migrations
314		$ms = new MigrationService('core', \OC::$server->get(Connection::class));
315		$ms->migrate();
316
317		$this->emit('\OC\Updater', 'dbUpgrade');
318	}
319
320	/**
321	 * upgrades all apps within a major ownCloud upgrade. Also loads "priority"
322	 * (types authentication, filesystem, logging, in that order) afterwards.
323	 *
324	 * @throws NeedsUpdateException
325	 */
326	protected function doAppUpgrade(): void {
327		$apps = \OC_App::getEnabledApps();
328		$priorityTypes = ['authentication', 'filesystem', 'logging'];
329		$pseudoOtherType = 'other';
330		$stacks = [$pseudoOtherType => []];
331
332		foreach ($apps as $appId) {
333			$priorityType = false;
334			foreach ($priorityTypes as $type) {
335				if (!isset($stacks[$type])) {
336					$stacks[$type] = [];
337				}
338				if (\OC_App::isType($appId, [$type])) {
339					$stacks[$type][] = $appId;
340					$priorityType = true;
341					break;
342				}
343			}
344			if (!$priorityType) {
345				$stacks[$pseudoOtherType][] = $appId;
346			}
347		}
348		foreach (array_merge($priorityTypes, [$pseudoOtherType]) as $type) {
349			$stack = $stacks[$type];
350			foreach ($stack as $appId) {
351				if (\OC_App::shouldUpgrade($appId)) {
352					$this->emit('\OC\Updater', 'appUpgradeStarted', [$appId, \OC_App::getAppVersion($appId)]);
353					\OC_App::updateApp($appId);
354					$this->emit('\OC\Updater', 'appUpgrade', [$appId, \OC_App::getAppVersion($appId)]);
355				}
356				if ($type !== $pseudoOtherType) {
357					// load authentication, filesystem and logging apps after
358					// upgrading them. Other apps my need to rely on modifying
359					// user and/or filesystem aspects.
360					\OC_App::loadApp($appId);
361				}
362			}
363		}
364	}
365
366	/**
367	 * check if the current enabled apps are compatible with the current
368	 * ownCloud version. disable them if not.
369	 * This is important if you upgrade ownCloud and have non ported 3rd
370	 * party apps installed.
371	 *
372	 * @throws \Exception
373	 */
374	private function checkAppsRequirements(): void {
375		$isCoreUpgrade = $this->isCodeUpgrade();
376		$apps = OC_App::getEnabledApps();
377		$version = implode('.', Util::getVersion());
378		$appManager = \OC::$server->getAppManager();
379		foreach ($apps as $app) {
380			// check if the app is compatible with this version of Nextcloud
381			$info = OC_App::getAppInfo($app);
382			if ($info === null || !OC_App::isAppCompatible($version, $info)) {
383				if ($appManager->isShipped($app)) {
384					throw new \UnexpectedValueException('The files of the app "' . $app . '" were not correctly replaced before running the update');
385				}
386				$appManager->disableApp($app, true);
387				$this->emit('\OC\Updater', 'incompatibleAppDisabled', [$app]);
388			}
389		}
390	}
391
392	/**
393	 * @return bool
394	 */
395	private function isCodeUpgrade(): bool {
396		$installedVersion = $this->config->getSystemValue('version', '0.0.0');
397		$currentVersion = implode('.', Util::getVersion());
398		if (version_compare($currentVersion, $installedVersion, '>')) {
399			return true;
400		}
401		return false;
402	}
403
404	/**
405	 * @param array $apps
406	 * @param array $previousEnableStates
407	 * @throws \Exception
408	 */
409	private function upgradeAppStoreApps(array $apps, array $previousEnableStates = []): void {
410		foreach ($apps as $app) {
411			try {
412				$this->emit('\OC\Updater', 'checkAppStoreAppBefore', [$app]);
413				if ($this->installer->isUpdateAvailable($app)) {
414					$this->emit('\OC\Updater', 'upgradeAppStoreApp', [$app]);
415					$this->installer->updateAppstoreApp($app);
416				}
417				$this->emit('\OC\Updater', 'checkAppStoreApp', [$app]);
418
419				if (!empty($previousEnableStates)) {
420					$ocApp = new \OC_App();
421					if (!empty($previousEnableStates[$app]) && is_array($previousEnableStates[$app])) {
422						$ocApp->enable($app, $previousEnableStates[$app]);
423					} else {
424						$ocApp->enable($app);
425					}
426				}
427			} catch (\Exception $ex) {
428				$this->log->error($ex->getMessage(), [
429					'exception' => $ex,
430				]);
431			}
432		}
433	}
434
435	/**
436	 * Forward messages emitted by the repair routine
437	 */
438	private function emitRepairEvents(): void {
439		$dispatcher = \OC::$server->getEventDispatcher();
440		$dispatcher->addListener('\OC\Repair::warning', function ($event) {
441			if ($event instanceof GenericEvent) {
442				$this->emit('\OC\Updater', 'repairWarning', $event->getArguments());
443			}
444		});
445		$dispatcher->addListener('\OC\Repair::error', function ($event) {
446			if ($event instanceof GenericEvent) {
447				$this->emit('\OC\Updater', 'repairError', $event->getArguments());
448			}
449		});
450		$dispatcher->addListener('\OC\Repair::info', function ($event) {
451			if ($event instanceof GenericEvent) {
452				$this->emit('\OC\Updater', 'repairInfo', $event->getArguments());
453			}
454		});
455		$dispatcher->addListener('\OC\Repair::step', function ($event) {
456			if ($event instanceof GenericEvent) {
457				$this->emit('\OC\Updater', 'repairStep', $event->getArguments());
458			}
459		});
460	}
461
462	private function logAllEvents(): void {
463		$log = $this->log;
464
465		$dispatcher = \OC::$server->getEventDispatcher();
466		$dispatcher->addListener('\OC\DB\Migrator::executeSql', function ($event) use ($log) {
467			if (!$event instanceof GenericEvent) {
468				return;
469			}
470			$log->info('\OC\DB\Migrator::executeSql: ' . $event->getSubject() . ' (' . $event->getArgument(0) . ' of ' . $event->getArgument(1) . ')', ['app' => 'updater']);
471		});
472		$dispatcher->addListener('\OC\DB\Migrator::checkTable', function ($event) use ($log) {
473			if (!$event instanceof GenericEvent) {
474				return;
475			}
476			$log->info('\OC\DB\Migrator::checkTable: ' . $event->getSubject() . ' (' . $event->getArgument(0) . ' of ' . $event->getArgument(1) . ')', ['app' => 'updater']);
477		});
478
479		$repairListener = function ($event) use ($log) {
480			if (!$event instanceof GenericEvent) {
481				return;
482			}
483			switch ($event->getSubject()) {
484				case '\OC\Repair::startProgress':
485					$log->info('\OC\Repair::startProgress: Starting ... ' . $event->getArgument(1) .  ' (' . $event->getArgument(0) . ')', ['app' => 'updater']);
486					break;
487				case '\OC\Repair::advance':
488					$desc = $event->getArgument(1);
489					if (empty($desc)) {
490						$desc = '';
491					}
492					$log->info('\OC\Repair::advance: ' . $desc . ' (' . $event->getArgument(0) . ')', ['app' => 'updater']);
493
494					break;
495				case '\OC\Repair::finishProgress':
496					$log->info('\OC\Repair::finishProgress', ['app' => 'updater']);
497					break;
498				case '\OC\Repair::step':
499					$log->info('\OC\Repair::step: Repair step: ' . $event->getArgument(0), ['app' => 'updater']);
500					break;
501				case '\OC\Repair::info':
502					$log->info('\OC\Repair::info: Repair info: ' . $event->getArgument(0), ['app' => 'updater']);
503					break;
504				case '\OC\Repair::warning':
505					$log->warning('\OC\Repair::warning: Repair warning: ' . $event->getArgument(0), ['app' => 'updater']);
506					break;
507				case '\OC\Repair::error':
508					$log->error('\OC\Repair::error: Repair error: ' . $event->getArgument(0), ['app' => 'updater']);
509					break;
510			}
511		};
512
513		$dispatcher->addListener('\OC\Repair::startProgress', $repairListener);
514		$dispatcher->addListener('\OC\Repair::advance', $repairListener);
515		$dispatcher->addListener('\OC\Repair::finishProgress', $repairListener);
516		$dispatcher->addListener('\OC\Repair::step', $repairListener);
517		$dispatcher->addListener('\OC\Repair::info', $repairListener);
518		$dispatcher->addListener('\OC\Repair::warning', $repairListener);
519		$dispatcher->addListener('\OC\Repair::error', $repairListener);
520
521
522		$this->listen('\OC\Updater', 'maintenanceEnabled', function () use ($log) {
523			$log->info('\OC\Updater::maintenanceEnabled: Turned on maintenance mode', ['app' => 'updater']);
524		});
525		$this->listen('\OC\Updater', 'maintenanceDisabled', function () use ($log) {
526			$log->info('\OC\Updater::maintenanceDisabled: Turned off maintenance mode', ['app' => 'updater']);
527		});
528		$this->listen('\OC\Updater', 'maintenanceActive', function () use ($log) {
529			$log->info('\OC\Updater::maintenanceActive: Maintenance mode is kept active', ['app' => 'updater']);
530		});
531		$this->listen('\OC\Updater', 'updateEnd', function ($success) use ($log) {
532			if ($success) {
533				$log->info('\OC\Updater::updateEnd: Update successful', ['app' => 'updater']);
534			} else {
535				$log->error('\OC\Updater::updateEnd: Update failed', ['app' => 'updater']);
536			}
537		});
538		$this->listen('\OC\Updater', 'dbUpgradeBefore', function () use ($log) {
539			$log->info('\OC\Updater::dbUpgradeBefore: Updating database schema', ['app' => 'updater']);
540		});
541		$this->listen('\OC\Updater', 'dbUpgrade', function () use ($log) {
542			$log->info('\OC\Updater::dbUpgrade: Updated database', ['app' => 'updater']);
543		});
544		$this->listen('\OC\Updater', 'incompatibleAppDisabled', function ($app) use ($log) {
545			$log->info('\OC\Updater::incompatibleAppDisabled: Disabled incompatible app: ' . $app, ['app' => 'updater']);
546		});
547		$this->listen('\OC\Updater', 'checkAppStoreAppBefore', function ($app) use ($log) {
548			$log->debug('\OC\Updater::checkAppStoreAppBefore: Checking for update of app "' . $app . '" in appstore', ['app' => 'updater']);
549		});
550		$this->listen('\OC\Updater', 'upgradeAppStoreApp', function ($app) use ($log) {
551			$log->info('\OC\Updater::upgradeAppStoreApp: Update app "' . $app . '" from appstore', ['app' => 'updater']);
552		});
553		$this->listen('\OC\Updater', 'checkAppStoreApp', function ($app) use ($log) {
554			$log->debug('\OC\Updater::checkAppStoreApp: Checked for update of app "' . $app . '" in appstore', ['app' => 'updater']);
555		});
556		$this->listen('\OC\Updater', 'appSimulateUpdate', function ($app) use ($log) {
557			$log->info('\OC\Updater::appSimulateUpdate: Checking whether the database schema for <' . $app . '> can be updated (this can take a long time depending on the database size)', ['app' => 'updater']);
558		});
559		$this->listen('\OC\Updater', 'appUpgradeStarted', function ($app) use ($log) {
560			$log->info('\OC\Updater::appUpgradeStarted: Updating <' . $app . '> ...', ['app' => 'updater']);
561		});
562		$this->listen('\OC\Updater', 'appUpgrade', function ($app, $version) use ($log) {
563			$log->info('\OC\Updater::appUpgrade: Updated <' . $app . '> to ' . $version, ['app' => 'updater']);
564		});
565		$this->listen('\OC\Updater', 'failure', function ($message) use ($log) {
566			$log->error('\OC\Updater::failure: ' . $message, ['app' => 'updater']);
567		});
568		$this->listen('\OC\Updater', 'setDebugLogLevel', function () use ($log) {
569			$log->info('\OC\Updater::setDebugLogLevel: Set log level to debug', ['app' => 'updater']);
570		});
571		$this->listen('\OC\Updater', 'resetLogLevel', function ($logLevel, $logLevelName) use ($log) {
572			$log->info('\OC\Updater::resetLogLevel: Reset log level to ' . $logLevelName . '(' . $logLevel . ')', ['app' => 'updater']);
573		});
574		$this->listen('\OC\Updater', 'startCheckCodeIntegrity', function () use ($log) {
575			$log->info('\OC\Updater::startCheckCodeIntegrity: Starting code integrity check...', ['app' => 'updater']);
576		});
577		$this->listen('\OC\Updater', 'finishedCheckCodeIntegrity', function () use ($log) {
578			$log->info('\OC\Updater::finishedCheckCodeIntegrity: Finished code integrity check', ['app' => 'updater']);
579		});
580	}
581}
582