2014-12-01 23:47:22 +03:00
|
|
|
<?php
|
2014-12-12 14:34:53 +03:00
|
|
|
/**
|
2016-07-21 18:07:57 +03:00
|
|
|
* @copyright Copyright (c) 2016, ownCloud, Inc.
|
2016-10-31 13:07:54 +03:00
|
|
|
* @copyright Copyright (c) 2016, Lukas Reschke <lukas@statuscode.ch>
|
2016-07-21 18:07:57 +03:00
|
|
|
*
|
2015-03-26 13:44:34 +03:00
|
|
|
* @author Bernhard Posselt <dev@bernhard-posselt.com>
|
2016-07-21 18:07:57 +03:00
|
|
|
* @author Joas Schilling <coding@schilljs.com>
|
2016-05-26 20:56:05 +03:00
|
|
|
* @author Lukas Reschke <lukas@statuscode.ch>
|
2015-03-26 13:44:34 +03:00
|
|
|
* @author Morris Jobke <hey@morrisjobke.de>
|
2017-11-06 17:56:42 +03:00
|
|
|
* @author Roeland Jago Douma <roeland@famdouma.nl>
|
2016-05-26 20:56:05 +03:00
|
|
|
* @author Stefan Weil <sw@weilnetz.de>
|
2015-03-26 13:44:34 +03:00
|
|
|
* @author Thomas Müller <thomas.mueller@tmit.eu>
|
|
|
|
*
|
|
|
|
* @license AGPL-3.0
|
|
|
|
*
|
|
|
|
* This code is free software: you can redistribute it and/or modify
|
|
|
|
* it under the terms of the GNU Affero General Public License, version 3,
|
|
|
|
* as published by the Free Software Foundation.
|
|
|
|
*
|
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
* GNU Affero General Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU Affero General Public License, version 3,
|
2019-12-03 21:57:53 +03:00
|
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
2014-12-01 23:47:22 +03:00
|
|
|
*
|
|
|
|
*/
|
2015-02-26 13:37:37 +03:00
|
|
|
|
2014-12-01 23:47:22 +03:00
|
|
|
namespace OC\App;
|
|
|
|
|
2019-03-06 21:59:15 +03:00
|
|
|
use OCP\IConfig;
|
2014-12-04 19:04:35 +03:00
|
|
|
use OCP\IL10N;
|
|
|
|
|
2014-12-01 23:47:22 +03:00
|
|
|
class DependencyAnalyzer {
|
|
|
|
|
2014-12-02 13:49:31 +03:00
|
|
|
/** @var Platform */
|
2014-12-04 19:04:35 +03:00
|
|
|
private $platform;
|
2014-12-02 13:49:31 +03:00
|
|
|
/** @var \OCP\IL10N */
|
|
|
|
private $l;
|
2015-07-02 17:34:18 +03:00
|
|
|
/** @var array */
|
|
|
|
private $appInfo;
|
2014-12-02 13:49:31 +03:00
|
|
|
|
2014-12-01 23:47:22 +03:00
|
|
|
/**
|
2014-12-02 13:49:31 +03:00
|
|
|
* @param Platform $platform
|
2014-12-01 23:47:22 +03:00
|
|
|
* @param \OCP\IL10N $l
|
|
|
|
*/
|
2017-07-23 22:03:26 +03:00
|
|
|
public function __construct(Platform $platform, IL10N $l) {
|
2014-12-04 19:04:35 +03:00
|
|
|
$this->platform = $platform;
|
2014-12-01 23:47:22 +03:00
|
|
|
$this->l = $l;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param array $app
|
|
|
|
* @returns array of missing dependencies
|
|
|
|
*/
|
2019-03-06 21:59:15 +03:00
|
|
|
public function analyze(array $app, bool $ignoreMax = false) {
|
2014-12-12 14:34:53 +03:00
|
|
|
$this->appInfo = $app;
|
|
|
|
if (isset($app['dependencies'])) {
|
|
|
|
$dependencies = $app['dependencies'];
|
|
|
|
} else {
|
|
|
|
$dependencies = [];
|
|
|
|
}
|
|
|
|
|
|
|
|
return array_merge(
|
|
|
|
$this->analyzePhpVersion($dependencies),
|
|
|
|
$this->analyzeDatabases($dependencies),
|
|
|
|
$this->analyzeCommands($dependencies),
|
|
|
|
$this->analyzeLibraries($dependencies),
|
|
|
|
$this->analyzeOS($dependencies),
|
2019-03-06 21:59:15 +03:00
|
|
|
$this->analyzeOC($dependencies, $app, $ignoreMax)
|
2015-07-02 17:34:18 +03:00
|
|
|
);
|
2014-12-01 23:47:22 +03:00
|
|
|
}
|
|
|
|
|
2019-03-06 21:59:15 +03:00
|
|
|
public function isMarkedCompatible(array $app): bool {
|
|
|
|
if (isset($app['dependencies'])) {
|
|
|
|
$dependencies = $app['dependencies'];
|
|
|
|
} else {
|
|
|
|
$dependencies = [];
|
|
|
|
}
|
|
|
|
|
|
|
|
$maxVersion = $this->getMaxVersion($dependencies, $app);
|
|
|
|
if ($maxVersion === null) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return !$this->compareBigger($this->platform->getOcVersion(), $maxVersion);
|
|
|
|
}
|
|
|
|
|
2015-02-26 13:23:08 +03:00
|
|
|
/**
|
2015-07-02 17:34:18 +03:00
|
|
|
* Truncates both versions to the lowest common version, e.g.
|
2015-02-26 13:23:08 +03:00
|
|
|
* 5.1.2.3 and 5.1 will be turned into 5.1 and 5.1,
|
|
|
|
* 5.2.6.5 and 5.1 will be turned into 5.2 and 5.1
|
|
|
|
* @param string $first
|
|
|
|
* @param string $second
|
2016-01-15 15:54:26 +03:00
|
|
|
* @return string[] first element is the first version, second element is the
|
2015-02-26 13:23:08 +03:00
|
|
|
* second version
|
|
|
|
*/
|
|
|
|
private function normalizeVersions($first, $second) {
|
|
|
|
$first = explode('.', $first);
|
|
|
|
$second = explode('.', $second);
|
|
|
|
|
|
|
|
// get both arrays to the same minimum size
|
|
|
|
$length = min(count($second), count($first));
|
|
|
|
$first = array_slice($first, 0, $length);
|
|
|
|
$second = array_slice($second, 0, $length);
|
|
|
|
|
|
|
|
return [implode('.', $first), implode('.', $second)];
|
|
|
|
}
|
|
|
|
|
2015-02-26 13:49:51 +03:00
|
|
|
/**
|
|
|
|
* Parameters will be normalized and then passed into version_compare
|
|
|
|
* in the same order they are specified in the method header
|
|
|
|
* @param string $first
|
|
|
|
* @param string $second
|
|
|
|
* @param string $operator
|
|
|
|
* @return bool result similar to version_compare
|
|
|
|
*/
|
2015-02-26 13:23:08 +03:00
|
|
|
private function compare($first, $second, $operator) {
|
2016-04-07 20:51:27 +03:00
|
|
|
// we can't normalize versions if one of the given parameters is not a
|
2015-02-26 13:23:08 +03:00
|
|
|
// version string but null. In case one parameter is null normalization
|
|
|
|
// will therefore be skipped
|
|
|
|
if ($first !== null && $second !== null) {
|
|
|
|
list($first, $second) = $this->normalizeVersions($first, $second);
|
|
|
|
}
|
|
|
|
|
|
|
|
return version_compare($first, $second, $operator);
|
|
|
|
}
|
|
|
|
|
2015-02-26 13:49:51 +03:00
|
|
|
/**
|
|
|
|
* Checks if a version is bigger than another version
|
|
|
|
* @param string $first
|
|
|
|
* @param string $second
|
|
|
|
* @return bool true if the first version is bigger than the second
|
|
|
|
*/
|
2015-02-26 13:23:08 +03:00
|
|
|
private function compareBigger($first, $second) {
|
|
|
|
return $this->compare($first, $second, '>');
|
|
|
|
}
|
|
|
|
|
2015-02-26 13:49:51 +03:00
|
|
|
/**
|
|
|
|
* Checks if a version is smaller than another version
|
|
|
|
* @param string $first
|
|
|
|
* @param string $second
|
|
|
|
* @return bool true if the first version is smaller than the second
|
|
|
|
*/
|
2015-02-26 13:23:08 +03:00
|
|
|
private function compareSmaller($first, $second) {
|
|
|
|
return $this->compare($first, $second, '<');
|
|
|
|
}
|
|
|
|
|
2015-07-02 17:34:18 +03:00
|
|
|
/**
|
|
|
|
* @param array $dependencies
|
|
|
|
* @return array
|
|
|
|
*/
|
|
|
|
private function analyzePhpVersion(array $dependencies) {
|
2014-12-12 14:34:53 +03:00
|
|
|
$missing = [];
|
|
|
|
if (isset($dependencies['php']['@attributes']['min-version'])) {
|
|
|
|
$minVersion = $dependencies['php']['@attributes']['min-version'];
|
2015-02-26 13:23:08 +03:00
|
|
|
if ($this->compareSmaller($this->platform->getPhpVersion(), $minVersion)) {
|
2018-02-21 21:53:44 +03:00
|
|
|
$missing[] = (string)$this->l->t('PHP %s or higher is required.', [$minVersion]);
|
2014-12-01 23:47:22 +03:00
|
|
|
}
|
|
|
|
}
|
2014-12-12 14:34:53 +03:00
|
|
|
if (isset($dependencies['php']['@attributes']['max-version'])) {
|
|
|
|
$maxVersion = $dependencies['php']['@attributes']['max-version'];
|
2015-02-26 13:23:08 +03:00
|
|
|
if ($this->compareBigger($this->platform->getPhpVersion(), $maxVersion)) {
|
2018-02-21 21:53:44 +03:00
|
|
|
$missing[] = (string)$this->l->t('PHP with a version lower than %s is required.', [$maxVersion]);
|
2014-12-01 23:47:22 +03:00
|
|
|
}
|
|
|
|
}
|
2016-04-27 22:24:33 +03:00
|
|
|
if (isset($dependencies['php']['@attributes']['min-int-size'])) {
|
|
|
|
$intSize = $dependencies['php']['@attributes']['min-int-size'];
|
|
|
|
if ($intSize > $this->platform->getIntSize()*8) {
|
2018-02-21 21:53:44 +03:00
|
|
|
$missing[] = (string)$this->l->t('%sbit or higher PHP required.', [$intSize]);
|
2016-04-27 22:24:33 +03:00
|
|
|
}
|
|
|
|
}
|
2014-12-12 14:34:53 +03:00
|
|
|
return $missing;
|
2014-12-01 23:47:22 +03:00
|
|
|
}
|
|
|
|
|
2015-07-02 17:34:18 +03:00
|
|
|
/**
|
|
|
|
* @param array $dependencies
|
|
|
|
* @return array
|
|
|
|
*/
|
|
|
|
private function analyzeDatabases(array $dependencies) {
|
2014-12-12 14:34:53 +03:00
|
|
|
$missing = [];
|
|
|
|
if (!isset($dependencies['database'])) {
|
|
|
|
return $missing;
|
2014-12-02 01:43:27 +03:00
|
|
|
}
|
|
|
|
|
2014-12-12 14:34:53 +03:00
|
|
|
$supportedDatabases = $dependencies['database'];
|
2014-12-02 01:43:27 +03:00
|
|
|
if (empty($supportedDatabases)) {
|
2014-12-12 14:34:53 +03:00
|
|
|
return $missing;
|
2014-12-02 01:43:27 +03:00
|
|
|
}
|
2014-12-11 17:37:45 +03:00
|
|
|
if (!is_array($supportedDatabases)) {
|
|
|
|
$supportedDatabases = array($supportedDatabases);
|
|
|
|
}
|
2014-12-12 14:34:53 +03:00
|
|
|
$supportedDatabases = array_map(function ($db) {
|
2014-12-04 19:04:35 +03:00
|
|
|
return $this->getValue($db);
|
2014-12-02 13:49:31 +03:00
|
|
|
}, $supportedDatabases);
|
2014-12-04 19:04:35 +03:00
|
|
|
$currentDatabase = $this->platform->getDatabase();
|
2014-12-02 01:43:27 +03:00
|
|
|
if (!in_array($currentDatabase, $supportedDatabases)) {
|
2017-07-24 12:36:20 +03:00
|
|
|
$missing[] = (string)$this->l->t('Following databases are supported: %s', [implode(', ', $supportedDatabases)]);
|
2014-12-04 19:04:35 +03:00
|
|
|
}
|
2014-12-12 14:34:53 +03:00
|
|
|
return $missing;
|
2014-12-04 19:04:35 +03:00
|
|
|
}
|
|
|
|
|
2015-07-02 17:34:18 +03:00
|
|
|
/**
|
|
|
|
* @param array $dependencies
|
|
|
|
* @return array
|
|
|
|
*/
|
|
|
|
private function analyzeCommands(array $dependencies) {
|
2014-12-12 14:34:53 +03:00
|
|
|
$missing = [];
|
|
|
|
if (!isset($dependencies['command'])) {
|
|
|
|
return $missing;
|
2014-12-04 19:04:35 +03:00
|
|
|
}
|
|
|
|
|
2014-12-12 14:34:53 +03:00
|
|
|
$commands = $dependencies['command'];
|
2014-12-11 17:37:45 +03:00
|
|
|
if (!is_array($commands)) {
|
|
|
|
$commands = array($commands);
|
|
|
|
}
|
2016-10-20 00:17:39 +03:00
|
|
|
if (isset($commands['@value'])) {
|
|
|
|
$commands = [$commands];
|
|
|
|
}
|
2014-12-04 19:04:35 +03:00
|
|
|
$os = $this->platform->getOS();
|
2014-12-12 14:34:53 +03:00
|
|
|
foreach ($commands as $command) {
|
2014-12-04 19:04:35 +03:00
|
|
|
if (isset($command['@attributes']['os']) && $command['@attributes']['os'] !== $os) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
$commandName = $this->getValue($command);
|
|
|
|
if (!$this->platform->isCommandKnown($commandName)) {
|
2018-02-21 21:53:44 +03:00
|
|
|
$missing[] = (string)$this->l->t('The command line tool %s could not be found', [$commandName]);
|
2014-12-04 19:04:35 +03:00
|
|
|
}
|
2014-12-02 01:43:27 +03:00
|
|
|
}
|
2014-12-12 14:34:53 +03:00
|
|
|
return $missing;
|
2014-12-02 01:43:27 +03:00
|
|
|
}
|
2014-12-04 19:04:35 +03:00
|
|
|
|
2015-07-02 17:34:18 +03:00
|
|
|
/**
|
|
|
|
* @param array $dependencies
|
|
|
|
* @return array
|
|
|
|
*/
|
|
|
|
private function analyzeLibraries(array $dependencies) {
|
2014-12-12 14:34:53 +03:00
|
|
|
$missing = [];
|
|
|
|
if (!isset($dependencies['lib'])) {
|
|
|
|
return $missing;
|
2014-12-05 15:52:51 +03:00
|
|
|
}
|
|
|
|
|
2014-12-12 14:34:53 +03:00
|
|
|
$libs = $dependencies['lib'];
|
2014-12-11 17:37:45 +03:00
|
|
|
if (!is_array($libs)) {
|
|
|
|
$libs = array($libs);
|
|
|
|
}
|
2016-10-07 12:27:33 +03:00
|
|
|
if (isset($libs['@value'])) {
|
|
|
|
$libs = [$libs];
|
|
|
|
}
|
2014-12-12 14:34:53 +03:00
|
|
|
foreach ($libs as $lib) {
|
2014-12-05 15:52:51 +03:00
|
|
|
$libName = $this->getValue($lib);
|
|
|
|
$libVersion = $this->platform->getLibraryVersion($libName);
|
|
|
|
if (is_null($libVersion)) {
|
2018-10-09 15:32:14 +03:00
|
|
|
$missing[] = $this->l->t('The library %s is not available.', [$libName]);
|
2014-12-05 15:52:51 +03:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (is_array($lib)) {
|
|
|
|
if (isset($lib['@attributes']['min-version'])) {
|
|
|
|
$minVersion = $lib['@attributes']['min-version'];
|
2015-02-26 13:23:08 +03:00
|
|
|
if ($this->compareSmaller($libVersion, $minVersion)) {
|
2018-10-09 15:32:14 +03:00
|
|
|
$missing[] = $this->l->t('Library %1$s with a version higher than %2$s is required - available version %3$s.',
|
|
|
|
[$libName, $minVersion, $libVersion]);
|
2014-12-05 15:52:51 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
if (isset($lib['@attributes']['max-version'])) {
|
|
|
|
$maxVersion = $lib['@attributes']['max-version'];
|
2015-02-26 13:23:08 +03:00
|
|
|
if ($this->compareBigger($libVersion, $maxVersion)) {
|
2018-10-09 15:32:14 +03:00
|
|
|
$missing[] = $this->l->t('Library %1$s with a version lower than %2$s is required - available version %3$s.',
|
|
|
|
[$libName, $maxVersion, $libVersion]);
|
2014-12-05 15:52:51 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2014-12-12 14:34:53 +03:00
|
|
|
return $missing;
|
2014-12-05 15:52:51 +03:00
|
|
|
}
|
|
|
|
|
2015-07-02 17:34:18 +03:00
|
|
|
/**
|
|
|
|
* @param array $dependencies
|
|
|
|
* @return array
|
|
|
|
*/
|
|
|
|
private function analyzeOS(array $dependencies) {
|
2014-12-12 14:34:53 +03:00
|
|
|
$missing = [];
|
|
|
|
if (!isset($dependencies['os'])) {
|
|
|
|
return $missing;
|
2014-12-05 16:51:41 +03:00
|
|
|
}
|
|
|
|
|
2014-12-12 14:34:53 +03:00
|
|
|
$oss = $dependencies['os'];
|
2014-12-05 16:51:41 +03:00
|
|
|
if (empty($oss)) {
|
2014-12-12 14:34:53 +03:00
|
|
|
return $missing;
|
2014-12-05 16:51:41 +03:00
|
|
|
}
|
2014-12-11 17:37:45 +03:00
|
|
|
if (is_array($oss)) {
|
|
|
|
$oss = array_map(function ($os) {
|
|
|
|
return $this->getValue($os);
|
|
|
|
}, $oss);
|
|
|
|
} else {
|
|
|
|
$oss = array($oss);
|
|
|
|
}
|
2014-12-05 16:51:41 +03:00
|
|
|
$currentOS = $this->platform->getOS();
|
|
|
|
if (!in_array($currentOS, $oss)) {
|
2017-07-24 12:36:20 +03:00
|
|
|
$missing[] = (string)$this->l->t('Following platforms are supported: %s', [implode(', ', $oss)]);
|
2014-12-05 16:51:41 +03:00
|
|
|
}
|
2014-12-12 14:34:53 +03:00
|
|
|
return $missing;
|
2014-12-05 16:51:41 +03:00
|
|
|
}
|
|
|
|
|
2015-07-02 17:34:18 +03:00
|
|
|
/**
|
|
|
|
* @param array $dependencies
|
|
|
|
* @param array $appInfo
|
|
|
|
* @return array
|
|
|
|
*/
|
2019-03-06 21:59:15 +03:00
|
|
|
private function analyzeOC(array $dependencies, array $appInfo, bool $ignoreMax) {
|
2014-12-12 14:34:53 +03:00
|
|
|
$missing = [];
|
2014-12-05 17:28:33 +03:00
|
|
|
$minVersion = null;
|
2016-10-31 13:07:54 +03:00
|
|
|
if (isset($dependencies['nextcloud']['@attributes']['min-version'])) {
|
|
|
|
$minVersion = $dependencies['nextcloud']['@attributes']['min-version'];
|
|
|
|
} elseif (isset($dependencies['owncloud']['@attributes']['min-version'])) {
|
2014-12-12 14:34:53 +03:00
|
|
|
$minVersion = $dependencies['owncloud']['@attributes']['min-version'];
|
|
|
|
} elseif (isset($appInfo['requiremin'])) {
|
|
|
|
$minVersion = $appInfo['requiremin'];
|
2015-01-14 14:48:59 +03:00
|
|
|
} elseif (isset($appInfo['require'])) {
|
|
|
|
$minVersion = $appInfo['require'];
|
2014-12-05 17:28:33 +03:00
|
|
|
}
|
2019-03-06 21:59:15 +03:00
|
|
|
$maxVersion = $this->getMaxVersion($dependencies, $appInfo);
|
2014-12-05 16:51:41 +03:00
|
|
|
|
2014-12-05 17:28:33 +03:00
|
|
|
if (!is_null($minVersion)) {
|
2015-02-26 13:23:08 +03:00
|
|
|
if ($this->compareSmaller($this->platform->getOcVersion(), $minVersion)) {
|
2017-07-24 12:36:20 +03:00
|
|
|
$missing[] = (string)$this->l->t('Server version %s or higher is required.', [$this->toVisibleVersion($minVersion)]);
|
2014-12-05 17:28:33 +03:00
|
|
|
}
|
|
|
|
}
|
2019-03-06 21:59:15 +03:00
|
|
|
if (!$ignoreMax && !is_null($maxVersion)) {
|
2015-02-26 13:23:08 +03:00
|
|
|
if ($this->compareBigger($this->platform->getOcVersion(), $maxVersion)) {
|
2017-07-24 12:36:20 +03:00
|
|
|
$missing[] = (string)$this->l->t('Server version %s or lower is required.', [$this->toVisibleVersion($maxVersion)]);
|
2014-12-05 17:28:33 +03:00
|
|
|
}
|
|
|
|
}
|
2014-12-12 14:34:53 +03:00
|
|
|
return $missing;
|
2014-12-05 17:28:33 +03:00
|
|
|
}
|
2014-12-05 16:51:41 +03:00
|
|
|
|
2019-03-06 21:59:15 +03:00
|
|
|
private function getMaxVersion(array $dependencies, array $appInfo): ?string {
|
|
|
|
if (isset($dependencies['nextcloud']['@attributes']['max-version'])) {
|
|
|
|
return $dependencies['nextcloud']['@attributes']['max-version'];
|
|
|
|
}
|
|
|
|
if (isset($dependencies['owncloud']['@attributes']['max-version'])) {
|
|
|
|
return $dependencies['owncloud']['@attributes']['max-version'];
|
|
|
|
}
|
|
|
|
if (isset($appInfo['requiremax'])) {
|
|
|
|
return $appInfo['requiremax'];
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2016-07-25 17:13:34 +03:00
|
|
|
/**
|
|
|
|
* Map the internal version number to the Nextcloud version
|
|
|
|
*
|
|
|
|
* @param string $version
|
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
protected function toVisibleVersion($version) {
|
|
|
|
switch ($version) {
|
|
|
|
case '9.1':
|
|
|
|
return '10';
|
|
|
|
default:
|
|
|
|
if (strpos($version, '9.1.') === 0) {
|
|
|
|
$version = '10.0.' . substr($version, 4);
|
|
|
|
}
|
|
|
|
return $version;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-12-04 19:04:35 +03:00
|
|
|
/**
|
|
|
|
* @param $element
|
|
|
|
* @return mixed
|
|
|
|
*/
|
|
|
|
private function getValue($element) {
|
2017-07-23 22:03:26 +03:00
|
|
|
if (isset($element['@value'])) {
|
2014-12-04 19:04:35 +03:00
|
|
|
return $element['@value'];
|
2017-07-23 22:03:26 +03:00
|
|
|
}
|
2014-12-11 17:37:45 +03:00
|
|
|
return (string)$element;
|
2014-12-04 19:04:35 +03:00
|
|
|
}
|
2014-12-01 23:47:22 +03:00
|
|
|
}
|