diff --git a/apps/updatenotification/lib/Settings/Admin.php b/apps/updatenotification/lib/Settings/Admin.php index 1696e97d3a..b859ca79f6 100644 --- a/apps/updatenotification/lib/Settings/Admin.php +++ b/apps/updatenotification/lib/Settings/Admin.php @@ -31,6 +31,8 @@ use OCP\AppFramework\Http\TemplateResponse; use OCP\IConfig; use OCP\IDateTimeFormatter; use OCP\IGroupManager; +use OCP\IUserSession; +use OCP\L10N\IFactory; use OCP\Settings\ISettings; use OCP\Util; @@ -43,21 +45,25 @@ class Admin implements ISettings { private $groupManager; /** @var IDateTimeFormatter */ private $dateTimeFormatter; + /** @var IUserSession */ + private $session; + /** @var IFactory */ + private $l10nFactory; - /** - * @param IConfig $config - * @param UpdateChecker $updateChecker - * @param IGroupManager $groupManager - * @param IDateTimeFormatter $dateTimeFormatter - */ - public function __construct(IConfig $config, - UpdateChecker $updateChecker, - IGroupManager $groupManager, - IDateTimeFormatter $dateTimeFormatter) { + public function __construct( + IConfig $config, + UpdateChecker $updateChecker, + IGroupManager $groupManager, + IDateTimeFormatter $dateTimeFormatter, + IUserSession $session, + IFactory $l10nFactory + ) { $this->config = $config; $this->updateChecker = $updateChecker; $this->groupManager = $groupManager; $this->dateTimeFormatter = $dateTimeFormatter; + $this->session = $session; + $this->l10nFactory = $l10nFactory; } /** @@ -93,8 +99,7 @@ class Admin implements ISettings { 'channels' => $channels, 'newVersionString' => empty($updateState['updateVersion']) ? '' : $updateState['updateVersion'], 'downloadLink' => empty($updateState['downloadLink']) ? '' : $updateState['downloadLink'], - 'changelogURL' => empty($updateState['changelog']) ? false : $updateState['changelog'], - 'whatsNew' => empty($updateState['whatsNew']) ? false : $updateState['whatsNew'], + 'changes' => $this->filterChanges($updateState['changes'] ?? []), 'updaterEnabled' => empty($updateState['updaterEnabled']) ? false : $updateState['updaterEnabled'], 'versionIsEol' => empty($updateState['versionIsEol']) ? false : $updateState['versionIsEol'], 'isDefaultUpdateServerURL' => $updateServerURL === $defaultUpdateServerURL, @@ -109,6 +114,48 @@ class Admin implements ISettings { return new TemplateResponse('updatenotification', 'admin', $params, ''); } + protected function filterChanges(array $changes) { + $filtered = []; + if(isset($changes['changelogURL'])) { + $filtered['changelogURL'] = $changes['changelogURL']; + } + if(!isset($changes['whatsNew'])) { + return $filtered; + } + + $isFirstCall = true; + do { + $lang = $this->l10nFactory->iterateLanguage($isFirstCall); + if($this->findWhatsNewTranslation($lang, $filtered, $changes['whatsNew'])) { + return $filtered; + } + $isFirstCall = false; + } while($lang !== 'en'); + + return $filtered; + } + + protected function getLangTrunk(string $lang):string { + $pos = strpos($lang, '_'); + if($pos !== false) { + $lang = substr($lang, 0, $pos); + } + return $lang; + } + + protected function findWhatsNewTranslation(string $lang, array &$result, array $whatsNew): bool { + if(isset($whatsNew[$lang])) { + $result['whatsNew'] = $whatsNew[$lang]; + return true; + } + $trunkedLang = $this->getLangTrunk($lang); + if($trunkedLang !== $lang && isset($whatsNew[$trunkedLang])) { + $result['whatsNew'] = $whatsNew[$trunkedLang]; + return true; + } + return false; + } + /** * @param array $groupIds * @return array diff --git a/apps/updatenotification/lib/UpdateChecker.php b/apps/updatenotification/lib/UpdateChecker.php index ec8e119049..bd03cb442b 100644 --- a/apps/updatenotification/lib/UpdateChecker.php +++ b/apps/updatenotification/lib/UpdateChecker.php @@ -25,17 +25,21 @@ declare(strict_types=1); namespace OCA\UpdateNotification; +use OC\Updater\ChangesCheck; use OC\Updater\VersionCheck; class UpdateChecker { /** @var VersionCheck */ private $updater; + /** @var ChangesCheck */ + private $changesCheck; /** * @param VersionCheck $updater */ - public function __construct(VersionCheck $updater) { + public function __construct(VersionCheck $updater, ChangesCheck $changesCheck) { $this->updater = $updater; + $this->changesCheck = $changesCheck; } /** @@ -56,11 +60,12 @@ class UpdateChecker { if (strpos($data['url'], 'https://') === 0) { $result['downloadLink'] = $data['url']; } - if (strpos($data['changelog'], 'https://') === 0) { - $result['changelog'] = $data['changelog']; - } - if (is_array($data['whatsNew']) && count($data['whatsNew']) <= 3) { - $result['whatsNew'] = $data['whatsNew']; + if (strpos($data['changes'], 'https://') === 0) { + try { + $result['changes'] = $this->changesCheck->check($data['changes'], $data['version']); + } catch (\Exception $e) { + // no info, not a problem + } } return $result; diff --git a/apps/updatenotification/src/components/root.vue b/apps/updatenotification/src/components/root.vue index 96cce351ca..fcc5a9dd83 100644 --- a/apps/updatenotification/src/components/root.vue +++ b/apps/updatenotification/src/components/root.vue @@ -222,7 +222,9 @@ }, whatsNew: function () { - + if(this.whatsNewData.length === 0) { + return null; + } var whatsNew = []; for (var i in this.whatsNewData) { whatsNew[i] = { icon: 'icon-star-dark', longtext: this.whatsNewData[i] }; @@ -307,7 +309,6 @@ beforeMount: function() { // Parse server data var data = JSON.parse($('#updatenotification').attr('data-json')); - console.warn(data); this.newVersionString = data.newVersionString; this.lastCheckedDate = data.lastChecked; @@ -321,7 +322,15 @@ this.notifyGroups = data.notifyGroups; this.isDefaultUpdateServerURL = data.isDefaultUpdateServerURL; this.versionIsEol = data.versionIsEol; - this.whatsNewData = data.whatsNew; + if(data.changes && data.changes.changelogURL) { + this.changelogURL = data.changes.changelogURL; + } + if(data.changes && data.changes.whatsNew) { + if(data.changes.whatsNew.admin) { + this.whatsNewData = this.whatsNewData.concat(data.changes.whatsNew.admin); + } + this.whatsNewData = this.whatsNewData.concat(data.changes.whatsNew.regular); + } }, mounted: function () { this._$el = $(this.$el); diff --git a/apps/updatenotification/tests/Settings/AdminTest.php b/apps/updatenotification/tests/Settings/AdminTest.php index 3e06990750..ed9614a3b6 100644 --- a/apps/updatenotification/tests/Settings/AdminTest.php +++ b/apps/updatenotification/tests/Settings/AdminTest.php @@ -99,8 +99,7 @@ class AdminTest extends TestCase { 'updateAvailable' => true, 'updateVersion' => '8.1.2', 'downloadLink' => 'https://downloads.nextcloud.org/server', - 'changelog' => 'https://nextcloud.com/changelog/#8.1.2', - 'whatsNew' => ['Autoshare to mother-in-law', 'Faster backend', 'Sparkling frontend'], + 'changes' => 'https://updates.nextcloud.com/changelog_server/?version=8.1.2', 'updaterEnabled' => true, 'versionIsEol' => false, ]); @@ -126,8 +125,7 @@ class AdminTest extends TestCase { 'channels' => $channels, 'newVersionString' => '8.1.2', 'downloadLink' => 'https://downloads.nextcloud.org/server', - 'changelogURL' => 'https://nextcloud.com/changelog/#8.1.2', - 'whatsNew' => ['Autoshare to mother-in-law', 'Faster backend', 'Sparkling frontend'], + 'changesURL' => 'https://updates.nextcloud.com/changelog_server/?version=8.1.2', 'updaterEnabled' => true, 'versionIsEol' => false, 'isDefaultUpdateServerURL' => true, diff --git a/apps/updatenotification/tests/UpdateCheckerTest.php b/apps/updatenotification/tests/UpdateCheckerTest.php index 0a82f950b9..16861eea54 100644 --- a/apps/updatenotification/tests/UpdateCheckerTest.php +++ b/apps/updatenotification/tests/UpdateCheckerTest.php @@ -25,11 +25,14 @@ declare(strict_types=1); namespace OCA\UpdateNotification\Tests; +use OC\Updater\ChangesCheck; use OC\Updater\VersionCheck; use OCA\UpdateNotification\UpdateChecker; use Test\TestCase; class UpdateCheckerTest extends TestCase { + /** @var ChangesCheck|\PHPUnit_Framework_MockObject_MockObject */ + protected $changesChecker; /** @var VersionCheck|\PHPUnit_Framework_MockObject_MockObject */ private $updater; /** @var UpdateChecker */ @@ -39,7 +42,8 @@ class UpdateCheckerTest extends TestCase { parent::setUp(); $this->updater = $this->createMock(VersionCheck::class); - $this->updateChecker = new UpdateChecker($this->updater); + $this->changesChecker = $this->createMock(ChangesCheck::class); + $this->updateChecker = new UpdateChecker($this->updater, $this->changesChecker); } public function testGetUpdateStateWithUpdateAndInvalidLink() { @@ -51,8 +55,7 @@ class UpdateCheckerTest extends TestCase { 'versionstring' => 'Nextcloud 123', 'web'=> 'javascript:alert(1)', 'url'=> 'javascript:alert(2)', - 'changelog' => 'javascript:alert(3)', - 'whatsNew' => 'javascript:alert(4)', + 'changes' => 'javascript:alert(3)', 'autoupdater'=> '0', 'eol'=> '1', ]); @@ -67,20 +70,40 @@ class UpdateCheckerTest extends TestCase { } public function testGetUpdateStateWithUpdateAndValidLink() { + $changes = [ + 'changelog' => 'https://nextcloud.com/changelog/#123-0-0', + 'whatsNew' => [ + 'en' => [ + 'regular' => [ + 'Yardarm heave to brig spyglass smartly pillage', + 'Bounty gangway bilge skysail rope\'s end', + 'Maroon cutlass spirits nipperkin Plate Fleet', + ], + 'admin' => [ + 'Scourge of the seven seas coffer doubloon', + 'Brig me splice the main brace', + ] + ] + ] + ]; + $this->updater ->expects($this->once()) ->method('check') ->willReturn([ - 'version' => 123, + 'version' => '123', 'versionstring' => 'Nextcloud 123', 'web'=> 'https://docs.nextcloud.com/myUrl', 'url'=> 'https://downloads.nextcloud.org/server', - 'changelog' => 'https://nextcloud.com/changelog/#123.0.0', - 'whatsNew' => ['Brews coffee', 'Makes appointments', 'Orchestrates Terminators'], + 'changes' => 'https://updates.nextcloud.com/changelog_server/?version=123.0.0', 'autoupdater'=> '1', 'eol'=> '0', ]); + $this->changesChecker->expects($this->once()) + ->method('check') + ->willReturn($changes); + $expected = [ 'updateAvailable' => true, 'updateVersion' => 'Nextcloud 123', @@ -88,8 +111,7 @@ class UpdateCheckerTest extends TestCase { 'versionIsEol' => false, 'updateLink' => 'https://docs.nextcloud.com/myUrl', 'downloadLink' => 'https://downloads.nextcloud.org/server', - 'changelog' => 'https://nextcloud.com/changelog/#123.0.0', - 'whatsNew' => ['Brews coffee', 'Makes appointments', 'Orchestrates Terminators'], + 'changes' => $changes, ]; $this->assertSame($expected, $this->updateChecker->getUpdateState()); } diff --git a/core/Migrations/Version14000Date20180626223656.php b/core/Migrations/Version14000Date20180626223656.php new file mode 100644 index 0000000000..72348aca3a --- /dev/null +++ b/core/Migrations/Version14000Date20180626223656.php @@ -0,0 +1,62 @@ + + * + * @author Arthur Schiwon + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 + * along with this program. If not, see . + * + */ + +namespace OC\Core\Migrations; + +use OCP\DB\ISchemaWrapper; +use OCP\Migration\SimpleMigrationStep; + +class Version14000Date20180626223656 extends SimpleMigrationStep { + public function changeSchema(\OCP\Migration\IOutput $output, \Closure $schemaClosure, array $options) { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + if(!$schema->hasTable('whats_new')) { + $table = $schema->createTable('whats_new'); + $table->addColumn('version', 'string', [ + 'notnull' => true, + 'length' => 64, + 'default' => '11', + ]); + $table->addColumn('etag', 'string', [ + 'notnull' => true, + 'length' => 64, + 'default' => '', + ]); + $table->addColumn('last_check', 'integer', [ + 'notnull' => true, + 'length' => 4, + 'unsigned' => true, + 'default' => 0, + ]); + $table->addColumn('data', 'text', [ + 'notnull' => true, + 'default' => '', + ]); + $table->setPrimaryKey(['version']); + $table->addIndex(['version', 'etag'], 'version_etag_idx'); + } + + return $schema; + } +} diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index b416d6e77b..a3d4564e3f 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -595,6 +595,7 @@ return array( 'OC\\Core\\Migrations\\Version14000Date20180516101403' => $baseDir . '/core/Migrations/Version14000Date20180516101403.php', 'OC\\Core\\Migrations\\Version14000Date20180518120534' => $baseDir . '/core/Migrations/Version14000Date20180518120534.php', 'OC\\Core\\Migrations\\Version14000Date20180522074438' => $baseDir . '/core/Migrations/Version14000Date20180522074438.php', + 'OC\\Core\\Migrations\\Version14000Date20180626223656' => $baseDir . '/core/Migrations/Version14000Date20180626223656.php', 'OC\\DB\\Adapter' => $baseDir . '/lib/private/DB/Adapter.php', 'OC\\DB\\AdapterMySQL' => $baseDir . '/lib/private/DB/AdapterMySQL.php', 'OC\\DB\\AdapterOCI8' => $baseDir . '/lib/private/DB/AdapterOCI8.php', @@ -1003,6 +1004,9 @@ return array( 'OC\\Template\\TemplateFileLocator' => $baseDir . '/lib/private/Template/TemplateFileLocator.php', 'OC\\URLGenerator' => $baseDir . '/lib/private/URLGenerator.php', 'OC\\Updater' => $baseDir . '/lib/private/Updater.php', + 'OC\\Updater\\ChangesCheck' => $baseDir . '/lib/private/Updater/ChangesCheck.php', + 'OC\\Updater\\ChangesMapper' => $baseDir . '/lib/private/Updater/ChangesMapper.php', + 'OC\\Updater\\ChangesResult' => $baseDir . '/lib/private/Updater/ChangesResult.php', 'OC\\Updater\\VersionCheck' => $baseDir . '/lib/private/Updater/VersionCheck.php', 'OC\\User\\Backend' => $baseDir . '/lib/private/User/Backend.php', 'OC\\User\\Database' => $baseDir . '/lib/private/User/Database.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 2ada951e03..e0b158f90f 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -625,6 +625,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\Core\\Migrations\\Version14000Date20180516101403' => __DIR__ . '/../../..' . '/core/Migrations/Version14000Date20180516101403.php', 'OC\\Core\\Migrations\\Version14000Date20180518120534' => __DIR__ . '/../../..' . '/core/Migrations/Version14000Date20180518120534.php', 'OC\\Core\\Migrations\\Version14000Date20180522074438' => __DIR__ . '/../../..' . '/core/Migrations/Version14000Date20180522074438.php', + 'OC\\Core\\Migrations\\Version14000Date20180626223656' => __DIR__ . '/../../..' . '/core/Migrations/Version14000Date20180626223656.php', 'OC\\DB\\Adapter' => __DIR__ . '/../../..' . '/lib/private/DB/Adapter.php', 'OC\\DB\\AdapterMySQL' => __DIR__ . '/../../..' . '/lib/private/DB/AdapterMySQL.php', 'OC\\DB\\AdapterOCI8' => __DIR__ . '/../../..' . '/lib/private/DB/AdapterOCI8.php', @@ -1033,6 +1034,9 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\Template\\TemplateFileLocator' => __DIR__ . '/../../..' . '/lib/private/Template/TemplateFileLocator.php', 'OC\\URLGenerator' => __DIR__ . '/../../..' . '/lib/private/URLGenerator.php', 'OC\\Updater' => __DIR__ . '/../../..' . '/lib/private/Updater.php', + 'OC\\Updater\\ChangesCheck' => __DIR__ . '/../../..' . '/lib/private/Updater/ChangesCheck.php', + 'OC\\Updater\\ChangesMapper' => __DIR__ . '/../../..' . '/lib/private/Updater/ChangesMapper.php', + 'OC\\Updater\\ChangesResult' => __DIR__ . '/../../..' . '/lib/private/Updater/ChangesResult.php', 'OC\\Updater\\VersionCheck' => __DIR__ . '/../../..' . '/lib/private/Updater/VersionCheck.php', 'OC\\User\\Backend' => __DIR__ . '/../../..' . '/lib/private/User/Backend.php', 'OC\\User\\Database' => __DIR__ . '/../../..' . '/lib/private/User/Database.php', diff --git a/lib/private/L10N/Factory.php b/lib/private/L10N/Factory.php index 8c8735836b..79495f5ff8 100644 --- a/lib/private/L10N/Factory.php +++ b/lib/private/L10N/Factory.php @@ -32,6 +32,7 @@ namespace OC\L10N; use OCP\IConfig; use OCP\IRequest; +use OCP\IUser; use OCP\IUserSession; use OCP\L10N\IFactory; @@ -321,6 +322,37 @@ class Factory implements IFactory { return array_search($lang, $languages) !== false; } + public function iterateLanguage(bool $reset = false): string { + static $i = 0; + if($reset) { + $i = 0; + } + switch($i) { + /** @noinspection PhpMissingBreakStatementInspection */ + case 0: + $i++; + $forcedLang = $this->config->getSystemValue('force_language', false); + if(is_string($forcedLang)) { + return $forcedLang; + } + /** @noinspection PhpMissingBreakStatementInspection */ + case 1: + $i++; + $user = $this->userSession->getUser(); + if($user instanceof IUser) { + $userLang = $this->config->getUserValue($user->getUID(), 'core', 'lang', null); + if(is_string($userLang)) { + return $userLang; + } + } + case 2: + $i++; + return $this->config->getSystemValue('default_language', 'en'); + default: + return 'en'; + } + } + /** * @param string $locale * @return bool diff --git a/lib/private/Updater/ChangesCheck.php b/lib/private/Updater/ChangesCheck.php new file mode 100644 index 0000000000..095f63db87 --- /dev/null +++ b/lib/private/Updater/ChangesCheck.php @@ -0,0 +1,157 @@ + + * + * @author Arthur Schiwon + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 + * along with this program. If not, see . + * + */ + +namespace OC\Updater; + +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\Http\Client\IClientService; +use OCP\Http\Client\IResponse; +use OCP\ILogger; + +class ChangesCheck { + /** @var IClientService */ + protected $clientService; + /** @var ChangesMapper */ + private $mapper; + /** @var ILogger */ + private $logger; + + const RESPONSE_NO_CONTENT = 0; + const RESPONSE_USE_CACHE = 1; + const RESPONSE_HAS_CONTENT = 2; + + public function __construct(IClientService $clientService, ChangesMapper $mapper, ILogger $logger) { + $this->clientService = $clientService; + $this->mapper = $mapper; + $this->logger = $logger; + } + + /** + * @throws \Exception + */ + public function check(string $uri, string $version): array { + try { + $version = $this->normalizeVersion($version); + $changesInfo = $this->mapper->getChanges($version); + if($changesInfo->getLastCheck() + 1800 > time()) { + return json_decode($changesInfo->getData(), true); + } + } catch (DoesNotExistException $e) { + $changesInfo = new ChangesResult(); + } + + $response = $this->queryChangesServer($uri, $changesInfo); + + switch($this->evaluateResponse($response)) { + case self::RESPONSE_NO_CONTENT: + return []; + case self::RESPONSE_USE_CACHE: + return json_decode($changesInfo->getData(), true); + case self::RESPONSE_HAS_CONTENT: + default: + $data = $this->extractData($response->getBody()); + $changesInfo->setData(json_encode($data)); + $changesInfo->setEtag($response->getHeader('Etag')); + $this->cacheResult($changesInfo, $version); + + return $data; + } + } + + protected function evaluateResponse(IResponse $response): int { + if($response->getStatusCode() === 304) { + return self::RESPONSE_USE_CACHE; + } else if($response->getStatusCode() === 404) { + return self::RESPONSE_NO_CONTENT; + } else if($response->getStatusCode() === 200) { + return self::RESPONSE_HAS_CONTENT; + } + $this->logger->debug('Unexpected return code {code} from changelog server', [ + 'app' => 'core', + 'code' => $response->getStatusCode(), + ]); + return self::RESPONSE_NO_CONTENT; + } + + protected function cacheResult(ChangesResult $entry, string $version) { + if($entry->getVersion() === $version) { + $this->mapper->update($entry); + } else { + $entry->setVersion($version); + $this->mapper->insert($entry); + } + } + + /** + * @throws \Exception + */ + protected function queryChangesServer(string $uri, ChangesResult $entry): IResponse { + $headers = []; + if($entry->getEtag() !== '') { + $headers['If-None-Match'] = [$entry->getEtag()]; + } + + $entry->setLastCheck(time()); + $client = $this->clientService->newClient(); + return $client->get($uri, [ + 'headers' => $headers, + ]); + } + + protected function extractData($body):array { + $data = []; + if ($body) { + $loadEntities = libxml_disable_entity_loader(true); + $xml = @simplexml_load_string($body); + libxml_disable_entity_loader($loadEntities); + if ($xml !== false) { + $data['changelogURL'] = (string)$xml->changelog['href']; + $data['whatsNew'] = []; + foreach($xml->whatsNew as $infoSet) { + $data['whatsNew'][(string)$infoSet['lang']] = [ + 'regular' => (array)$infoSet->regular->item, + 'admin' => (array)$infoSet->admin->item, + ]; + } + } else { + libxml_clear_errors(); + } + } + return $data; + } + + /** + * returns a x.y.z form of the provided version. Extra numbers will be + * omitted, missing ones added as zeros. + */ + protected function normalizeVersion(string $version): string { + $versionNumbers = array_slice(explode('.', $version), 0, 3); + $versionNumbers[0] = $versionNumbers[0] ?: '0'; // deal with empty input + while(count($versionNumbers) < 3) { + // changelog server expects x.y.z, pad 0 if it is too short + $versionNumbers[] = 0; + } + return implode('.', $versionNumbers); + } +} diff --git a/lib/private/Updater/ChangesMapper.php b/lib/private/Updater/ChangesMapper.php new file mode 100644 index 0000000000..d1548c415c --- /dev/null +++ b/lib/private/Updater/ChangesMapper.php @@ -0,0 +1,57 @@ + + * + * @author Arthur Schiwon + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 + * along with this program. If not, see . + * + */ + +namespace OC\Updater; + +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Db\QBMapper; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; + +class ChangesMapper extends QBMapper { + const TABLE_NAME = 'whats_new'; + + public function __construct(IDBConnection $db) { + parent::__construct($db, self::TABLE_NAME); + } + + /** + * @throws DoesNotExistException + */ + public function getChanges(string $version): ChangesResult { + /* @var $qb IQueryBuilder */ + $qb = $this->db->getQueryBuilder(); + $result = $qb->select('*') + ->from(self::TABLE_NAME) + ->where($qb->expr()->eq('version', $qb->createNamedParameter($version))) + ->execute(); + + $data = $result->fetch(); + $result->closeCursor(); + if ($data === false) { + throw new DoesNotExistException('Changes info is not present'); + } + return ChangesResult::fromRow($data); + } +} diff --git a/lib/private/Updater/ChangesResult.php b/lib/private/Updater/ChangesResult.php new file mode 100644 index 0000000000..95a1af3910 --- /dev/null +++ b/lib/private/Updater/ChangesResult.php @@ -0,0 +1,61 @@ + + * + * @author Arthur Schiwon + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 + * along with this program. If not, see . + * + */ + +namespace OC\Updater; + +use OCP\AppFramework\Db\Entity; + +/** + * Class ChangesResult + * + * @package OC\Updater + * @method string getVersion()=1 + * @method void setVersion(string $version) + * @method string getEtag() + * @method void setEtag(string $etag) + * @method int getLastCheck() + * @method void setLastCheck(int $lastCheck) + * @method string getData() + * @method void setData(string $data) + */ +class ChangesResult extends Entity { + /** @var string */ + protected $version = ''; + + /** @var string */ + protected $etag = ''; + + /** @var int */ + protected $lastCheck = 0; + + /** @var string */ + protected $data = ''; + + public function __construct() { + $this->addType('version', 'string'); + $this->addType('etag', 'string'); + $this->addType('lastCheck', 'int'); + $this->addType('data', 'string'); + } +} diff --git a/lib/private/Updater/VersionCheck.php b/lib/private/Updater/VersionCheck.php index bc50521163..3cbd7061fe 100644 --- a/lib/private/Updater/VersionCheck.php +++ b/lib/private/Updater/VersionCheck.php @@ -55,7 +55,7 @@ class VersionCheck { */ public function check() { // Look up the cache - it is invalidated all 30 minutes - if (false && ((int)$this->config->getAppValue('core', 'lastupdatedat') + 1800) > time()) { + if (((int)$this->config->getAppValue('core', 'lastupdatedat') + 1800) > time()) { return json_decode($this->config->getAppValue('core', 'lastupdateResult'), true); } @@ -70,7 +70,7 @@ class VersionCheck { $version = Util::getVersion(); $version['installed'] = $this->config->getAppValue('core', 'installedat'); $version['updated'] = $this->config->getAppValue('core', 'lastupdatedat'); - $version['updatechannel'] = 'stable'; //\OC_Util::getChannel(); + $version['updatechannel'] = \OC_Util::getChannel(); $version['edition'] = ''; $version['build'] = \OC_Util::getBuild(); $version['php_major'] = PHP_MAJOR_VERSION; @@ -97,10 +97,7 @@ class VersionCheck { $tmp['versionstring'] = (string)$data->versionstring; $tmp['url'] = (string)$data->url; $tmp['web'] = (string)$data->web; - $tmp['changelog'] = isset($data->changelog) ? (string)$data->changelog : ''; - // TODO: one's it is decided, use the proper field… - $tmp['whatsNew'] = isset($data->whatsNew) ? ((array)$data->whatsNew)['item'] : null; - $tmp['whatsNew'] = isset($data->whatsNew_admin) ? ((array)$data->whatsNew_admin)['item'] : (string)$data->whatsNew; + $tmp['changes'] = isset($data->changes) ? (string)$data->changes : ''; $tmp['autoupdater'] = (string)$data->autoupdater; $tmp['eol'] = isset($data->eol) ? (string)$data->eol : '0'; } else { diff --git a/lib/public/L10N/IFactory.php b/lib/public/L10N/IFactory.php index 263ebe81d0..9c00607320 100644 --- a/lib/public/L10N/IFactory.php +++ b/lib/public/L10N/IFactory.php @@ -89,4 +89,14 @@ interface IFactory { * @since 14.0.0 */ public function createPluralFunction($string); + + /** + * iterate through language settings (if provided) in this order: + * 1. returns the forced language or: + * 2. returns the user language or: + * 3. returns the system default language or: + * 4+∞. returns 'en' + * @since 14.0.0 + */ + public function iterateLanguage(bool $reset = false): string; } diff --git a/tests/lib/Updater/ChangesCheckTest.php b/tests/lib/Updater/ChangesCheckTest.php new file mode 100644 index 0000000000..5f7e8ad4f0 --- /dev/null +++ b/tests/lib/Updater/ChangesCheckTest.php @@ -0,0 +1,344 @@ + + * + * @author Arthur Schiwon + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * 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 + * along with this program. If not, see . + * + */ + +namespace Test\Updater; + +use OC\Updater\ChangesCheck; +use OC\Updater\ChangesMapper; +use OC\Updater\ChangesResult; +use OCP\Http\Client\IClient; +use OCP\Http\Client\IClientService; +use OCP\Http\Client\IResponse; +use OCP\ILogger; +use const Solarium\QueryType\Select\Query\Component\Facet\INCLUDE_LOWER; +use Test\TestCase; + +class ChangesCheckTest extends TestCase { + /** @var IClientService|\PHPUnit_Framework_MockObject_MockObject */ + protected $clientService; + + /** @var ChangesCheck */ + protected $checker; + + /** @var ChangesMapper|\PHPUnit_Framework_MockObject_MockObject */ + protected $mapper; + + /** @var ILogger|\PHPUnit_Framework_MockObject_MockObject */ + protected $logger; + + public function setUp() { + parent::setUp(); + + $this->clientService = $this->createMock(IClientService::class); + $this->mapper = $this->createMock(ChangesMapper::class); + $this->logger = $this->createMock(ILogger::class); + + $this->checker = new ChangesCheck($this->clientService, $this->mapper, $this->logger); + } + + public function statusCodeProvider():array { + return [ + [200, ChangesCheck::RESPONSE_HAS_CONTENT], + [304, ChangesCheck::RESPONSE_USE_CACHE], + [404, ChangesCheck::RESPONSE_NO_CONTENT], + [418, ChangesCheck::RESPONSE_NO_CONTENT], + ]; + } + + /** + * @dataProvider statusCodeProvider + */ + public function testEvaluateResponse(int $statusCode, int $expected) { + $response = $this->createMock(IResponse::class); + $response->expects($this->atLeastOnce()) + ->method('getStatusCode') + ->willReturn($statusCode); + + if(!in_array($statusCode, [200, 304, 404])) { + $this->logger->expects($this->once()) + ->method('debug'); + } + + $evaluation = $this->invokePrivate($this->checker, 'evaluateResponse', [$response]); + $this->assertSame($expected, $evaluation); + } + + public function testCacheResultInsert() { + $version = '13.0.4'; + $entry = $this->createMock(ChangesResult::class); + $entry->expects($this->exactly(2)) + ->method('__call') + ->withConsecutive(['getVersion'], ['setVersion', [$version]]) + ->willReturnOnConsecutiveCalls('', null); + + $this->mapper->expects($this->once()) + ->method('insert'); + $this->mapper->expects($this->never()) + ->method('update'); + + $this->invokePrivate($this->checker, 'cacheResult', [$entry, $version]); + } + + public function testCacheResultUpdate() { + $version = '13.0.4'; + $entry = $this->createMock(ChangesResult::class); + $entry->expects($this->once()) + ->method('__call') + ->willReturn($version); + + $this->mapper->expects($this->never()) + ->method('insert'); + $this->mapper->expects($this->once()) + ->method('update'); + + $this->invokePrivate($this->checker, 'cacheResult', [$entry, $version]); + } + + public function changesXMLProvider(): array { + return [ + [ # 0 - full example + ' + + + + + Refined user interface + End-to-end Encryption + Video and Text Chat + + + Changes to the Nginx configuration + Theming: CSS files were consolidated + + + + + Überarbeitete Benutzerschnittstelle + Ende-zu-Ende Verschlüsselung + Video- und Text-Chat + + + Änderungen an der Nginx Konfiguration + Theming: CSS Dateien wurden konsolidiert + + +', + [ + 'changelogURL' => 'https://nextcloud.com/changelog/#13-0-0', + 'whatsNew' => [ + 'en' => [ + 'regular' => [ + 'Refined user interface', + 'End-to-end Encryption', + 'Video and Text Chat' + ], + 'admin' => [ + 'Changes to the Nginx configuration', + 'Theming: CSS files were consolidated' + ], + ], + 'de' => [ + 'regular' => [ + 'Überarbeitete Benutzerschnittstelle', + 'Ende-zu-Ende Verschlüsselung', + 'Video- und Text-Chat' + ], + 'admin' => [ + 'Änderungen an der Nginx Konfiguration', + 'Theming: CSS Dateien wurden konsolidiert' + ], + ], + ], + ] + ], + [ # 1- admin part not translated + ' + + + + + Refined user interface + End-to-end Encryption + Video and Text Chat + + + Changes to the Nginx configuration + Theming: CSS files were consolidated + + + + + Überarbeitete Benutzerschnittstelle + Ende-zu-Ende Verschlüsselung + Video- und Text-Chat + + +', + [ + 'changelogURL' => 'https://nextcloud.com/changelog/#13-0-0', + 'whatsNew' => [ + 'en' => [ + 'regular' => [ + 'Refined user interface', + 'End-to-end Encryption', + 'Video and Text Chat' + ], + 'admin' => [ + 'Changes to the Nginx configuration', + 'Theming: CSS files were consolidated' + ], + ], + 'de' => [ + 'regular' => [ + 'Überarbeitete Benutzerschnittstelle', + 'Ende-zu-Ende Verschlüsselung', + 'Video- und Text-Chat' + ], + 'admin' => [ + ], + ], + ], + ] + ], + [ # 2 - minimal set + ' + + + + + Refined user interface + End-to-end Encryption + Video and Text Chat + + +', + [ + 'changelogURL' => 'https://nextcloud.com/changelog/#13-0-0', + 'whatsNew' => [ + 'en' => [ + 'regular' => [ + 'Refined user interface', + 'End-to-end Encryption', + 'Video and Text Chat' + ], + 'admin' => [], + ], + ], + ] + ], + [ # 3 - minimal set (procrastinator edition) + ' + + + + + Write this tomorrow + + +', + [ + 'changelogURL' => 'https://nextcloud.com/changelog/#13-0-0', + 'whatsNew' => [ + 'en' => [ + 'regular' => [ + 'Write this tomorrow', + ], + 'admin' => [], + ], + ], + ] + ], + ]; + } + + /** + * @dataProvider changesXMLProvider + */ + public function testExtractData(string $body, array $expected) { + $actual = $this->invokePrivate($this->checker, 'extractData', [$body]); + $this->assertSame($expected, $actual); + } + + public function etagProvider() { + return [ + [''], + ['a27aab83d8205d73978435076e53d143'] + ]; + } + + /** + * @dataProvider etagProvider + */ + public function testQueryChangesServer(string $etag) { + $uri = 'https://changes.nextcloud.server/?13.0.5'; + $entry = $this->createMock(ChangesResult::class); + $entry->expects($this->any()) + ->method('__call') + ->willReturn($etag); + + $expectedHeaders = $etag === '' ? [] : ['If-None-Match: ' . $etag]; + + $client = $this->createMock(IClient::class); + $client->expects($this->once()) + ->method('get') + ->with($uri, ['headers' => $expectedHeaders]) + ->willReturn($this->createMock(IResponse::class)); + + $this->clientService->expects($this->once()) + ->method('newClient') + ->willReturn($client); + + $response = $this->invokePrivate($this->checker, 'queryChangesServer', [$uri, $entry]); + $this->assertInstanceOf(IResponse::class, $response); + } + + public function versionProvider(): array { + return [ + ['13.0.7', '13.0.7'], + ['13.0.7.3', '13.0.7'], + ['13.0.7.3.42', '13.0.7'], + ['13.0', '13.0.0'], + ['13', '13.0.0'], + ['', '0.0.0'], + ]; + } + + /** + * @dataProvider versionProvider + */ + public function testNormalizeVersion(string $input, string $expected) { + $normalized = $this->invokePrivate($this->checker, 'normalizeVersion', [$input]); + $this->assertSame($expected, $normalized); + } +} diff --git a/tests/lib/Updater/VersionCheckTest.php b/tests/lib/Updater/VersionCheckTest.php index c7165b34ef..6da4fd2c3b 100644 --- a/tests/lib/Updater/VersionCheckTest.php +++ b/tests/lib/Updater/VersionCheckTest.php @@ -62,6 +62,7 @@ class VersionCheckTest extends \Test\TestCase { 'versionstring' => 'ownCloud 8.0.4', 'url' => 'https://download.example.org/community/owncloud-8.0.4.zip', 'web' => 'http://doc.example.org/server/8.0/admin_manual/maintenance/upgrade.html', + 'changes' => '', ]; $this->config @@ -84,8 +85,7 @@ class VersionCheckTest extends \Test\TestCase { 'versionstring' => 'ownCloud 8.0.4', 'url' => 'https://download.example.org/community/owncloud-8.0.4.zip', 'web' => 'http://doc.example.org/server/8.0/admin_manual/maintenance/upgrade.html', - 'changelog' => '', - 'whatsNew' => '', + 'changes' => '', 'autoupdater' => '0', 'eol' => '1', ]; @@ -183,8 +183,7 @@ class VersionCheckTest extends \Test\TestCase { 'versionstring' => '', 'url' => '', 'web' => '', - 'changelog' => '', - 'whatsNew' => '', + 'changes' => '', 'autoupdater' => '', 'eol' => '0', ]; @@ -279,8 +278,7 @@ class VersionCheckTest extends \Test\TestCase { 'versionstring' => '', 'url' => '', 'web' => '', - 'changelog' => '', - 'whatsNew' => '', + 'changes' => '', 'autoupdater' => '', 'eol' => '0', ];