From 772bbd99bee83d17707c73a630a4d47c4b8bc807 Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Thu, 5 Jul 2018 00:41:59 +0200 Subject: [PATCH 1/2] Backend work to provide NC whats New info to users Signed-off-by: Arthur Schiwon --- apps/files/js/app.js | 2 + .../updatenotification/lib/Settings/Admin.php | 33 +---- core/Controller/WhatsNewController.php | 117 +++++++++++++++ core/js/core.json | 1 + core/js/merged-template-prepend.json | 1 + core/js/public/whatsnew.js | 54 +++++++ core/routes.php | 2 + lib/composer/composer/autoload_classmap.php | 3 + lib/composer/composer/autoload_static.php | 3 + lib/private/L10N/Factory.php | 34 +---- lib/private/L10N/LanguageIterator.php | 137 ++++++++++++++++++ lib/private/Updater/ChangesCheck.php | 11 +- lib/public/L10N/IFactory.php | 16 +- lib/public/L10N/ILanguageIterator.php | 74 ++++++++++ tests/lib/L10N/FactoryTest.php | 30 ++++ tests/lib/L10N/LanguageIteratorTest.php | 98 +++++++++++++ tests/lib/Updater/ChangesCheckTest.php | 39 ++++- 17 files changed, 593 insertions(+), 62 deletions(-) create mode 100644 core/Controller/WhatsNewController.php create mode 100644 core/js/public/whatsnew.js create mode 100644 lib/private/L10N/LanguageIterator.php create mode 100644 lib/public/L10N/ILanguageIterator.php create mode 100644 tests/lib/L10N/LanguageIteratorTest.php diff --git a/apps/files/js/app.js b/apps/files/js/app.js index 6a21bce975..883642b9c1 100644 --- a/apps/files/js/app.js +++ b/apps/files/js/app.js @@ -131,6 +131,8 @@ }); this._debouncedPersistShowHiddenFilesState = _.debounce(this._persistShowHiddenFilesState, 1200); + + OCP.WhatsNew.query(); // for Nextcloud server }, /** diff --git a/apps/updatenotification/lib/Settings/Admin.php b/apps/updatenotification/lib/Settings/Admin.php index b859ca79f6..cae62ee0a9 100644 --- a/apps/updatenotification/lib/Settings/Admin.php +++ b/apps/updatenotification/lib/Settings/Admin.php @@ -123,39 +123,18 @@ class Admin implements ISettings { return $filtered; } - $isFirstCall = true; + $iterator = $this->l10nFactory->getLanguageIterator(); do { - $lang = $this->l10nFactory->iterateLanguage($isFirstCall); - if($this->findWhatsNewTranslation($lang, $filtered, $changes['whatsNew'])) { - return $filtered; + $lang = $iterator->current(); + if(isset($changes['whatsNew'][$lang])) { + return $filtered['whatsNew'][$lang]; } - $isFirstCall = false; - } while($lang !== 'en'); + $iterator->next(); + } while($lang !== 'en' && $iterator->valid()); 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/core/Controller/WhatsNewController.php b/core/Controller/WhatsNewController.php new file mode 100644 index 0000000000..d333165169 --- /dev/null +++ b/core/Controller/WhatsNewController.php @@ -0,0 +1,117 @@ + + * + * @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\Controller; + +use OC\CapabilitiesManager; +use OC\Security\IdentityProof\Manager; +use OC\Updater\ChangesCheck; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\DataResponse; +use OCP\IConfig; +use OCP\IRequest; +use OCP\IUserManager; +use OCP\IUserSession; +use OCP\L10N\IFactory; + +class WhatsNewController extends OCSController { + + /** @var IConfig */ + protected $config; + /** @var IUserSession */ + private $userSession; + /** @var ChangesCheck */ + private $whatsNewService; + /** @var IFactory */ + private $langFactory; + + public function __construct( + string $appName, + IRequest $request, + CapabilitiesManager $capabilitiesManager, + IUserSession $userSession, + IUserManager $userManager, + Manager $keyManager, + IConfig $config, + ChangesCheck $whatsNewService, + IFactory $langFactory + ) { + parent::__construct($appName, $request, $capabilitiesManager, $userSession, $userManager, $keyManager); + $this->config = $config; + $this->userSession = $userSession; + $this->whatsNewService = $whatsNewService; + $this->langFactory = $langFactory; + } + + /** + * @NoAdminRequired + */ + public function get():DataResponse { + $user = $this->userSession->getUser(); + if($user === null) { + throw new \RuntimeException("Acting user cannot be resolved"); + } + $lastRead = $this->config->getUserValue($user->getUID(), 'core', 'whatsNewLastRead', 0); + $currentVersion = $this->whatsNewService->normalizeVersion($this->config->getSystemValue('version')); + + if(version_compare($lastRead, $currentVersion, '>=')) { + return new DataResponse([], Http::STATUS_NO_CONTENT); + } + + try { + $iterator = $this->langFactory->getLanguageIterator(); + $whatsNew = $this->whatsNewService->getChangesForVersion($currentVersion); + $resultData = ['changelogURL' => $whatsNew['changelogURL']]; + do { + $lang = $iterator->current(); + if(isset($whatsNew['whatsNew'][$lang])) { + $resultData['whatsNew'] = $whatsNew['whatsNew'][$lang]; + break; + } + $iterator->next(); + } while ($lang !== 'en' && $iterator->valid()); + return new DataResponse($resultData); + } catch (DoesNotExistException $e) { + return new DataResponse([], Http::STATUS_NO_CONTENT); + } + } + + /** + * @NoAdminRequired + * + * @throws \OCP\PreConditionNotMetException + * @throws DoesNotExistException + */ + public function dismiss(string $version):DataResponse { + $user = $this->userSession->getUser(); + if($user === null) { + throw new \RuntimeException("Acting user cannot be resolved"); + } + $version = $this->whatsNewService->normalizeVersion($version); + // checks whether it's a valid version, throws an Exception otherwise + $this->whatsNewService->getChangesForVersion($version); + $this->config->setUserValue($user->getUID(), 'core', 'whatsNewLastRead', $version); + return new DataResponse(); + } +} diff --git a/core/js/core.json b/core/js/core.json index 41b927147b..502e3a5797 100644 --- a/core/js/core.json +++ b/core/js/core.json @@ -48,6 +48,7 @@ "public/appconfig.js", "public/comments.js", "public/publicpage.js", + "public/whatsnew.js", "multiselect.js", "oc-requesttoken.js", "setupchecks.js", diff --git a/core/js/merged-template-prepend.json b/core/js/merged-template-prepend.json index f4ef511bc7..c274201d97 100644 --- a/core/js/merged-template-prepend.json +++ b/core/js/merged-template-prepend.json @@ -7,6 +7,7 @@ "eventsource.js", "public/appconfig.js", "public/comments.js", + "public/whatsnew.js", "config.js", "oc-requesttoken.js", "apps.js", diff --git a/core/js/public/whatsnew.js b/core/js/public/whatsnew.js new file mode 100644 index 0000000000..dc5862e857 --- /dev/null +++ b/core/js/public/whatsnew.js @@ -0,0 +1,54 @@ +/** + * @copyright (c) 2017 Arthur Schiwon + * + * @author Arthur Schiwon + * + * This file is licensed under the Affero General Public License version 3 or + * later. See the COPYING file. + */ + +(function(OCP) { + "use strict"; + + OCP.WhatsNew = { + + query: function(options) { + options = options || {}; + $.ajax({ + type: 'GET', + url: options.url || OC.linkToOCS('core', 2) + 'whatsnew?format=json', + success: options.success || this._onQuerySuccess, + error: options.error || this._onQueryError + }); + }, + + dismiss: function(version, options) { + options = options || {}; + $.ajax({ + type: 'POST', + url: options.url || OC.linkToOCS('core', 2) + 'whatsnew', + data: {version: encodeURIComponent(version)}, + success: options.success || this._onDismissSuccess, + error: options.error || this._onDismissError + }); + }, + + _onQuerySuccess: function(data, statusText) { + console.debug('querying Whats New data was successful: ' + data || statusText); + console.debug(data); + }, + + _onQueryError: function (o, t, e) { + console.debug(o); + console.debug('querying Whats New Data resulted in an error: ' + t +e); + }, + + _onDismissSuccess: function(data) { + console.debug('dismissing Whats New data was successful: ' + data); + }, + + _onDismissError: function (data) { + console.debug('dismissing Whats New data resulted in an error: ' + data); + } + }; +})(OCP); diff --git a/core/routes.php b/core/routes.php index 90282c5ebf..c5df3a362f 100644 --- a/core/routes.php +++ b/core/routes.php @@ -76,6 +76,8 @@ $application->registerRoutes($this, [ ['root' => '/core', 'name' => 'Navigation#getAppsNavigation', 'url' => '/navigation/apps', 'verb' => 'GET'], ['root' => '/core', 'name' => 'Navigation#getSettingsNavigation', 'url' => '/navigation/settings', 'verb' => 'GET'], ['root' => '/core', 'name' => 'AutoComplete#get', 'url' => '/autocomplete/get', 'verb' => 'GET'], + ['root' => '/core', 'name' => 'WhatsNew#get', 'url' => '/whatsnew', 'verb' => 'GET'], + ['root' => '/core', 'name' => 'WhatsNew#dismiss', 'url' => '/whatsnew', 'verb' => 'POST'], ], ]); diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index d2648c2db6..62e5ed020d 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -259,6 +259,7 @@ return array( 'OCP\\IUserSession' => $baseDir . '/lib/public/IUserSession.php', 'OCP\\Image' => $baseDir . '/lib/public/Image.php', 'OCP\\L10N\\IFactory' => $baseDir . '/lib/public/L10N/IFactory.php', + 'OCP\\L10N\\ILanguageIterator' => $baseDir . '/lib/public/L10N/ILanguageIterator.php', 'OCP\\LDAP\\IDeletionFlagSupport' => $baseDir . '/lib/public/LDAP/IDeletionFlagSupport.php', 'OCP\\LDAP\\ILDAPProvider' => $baseDir . '/lib/public/LDAP/ILDAPProvider.php', 'OCP\\LDAP\\ILDAPProviderFactory' => $baseDir . '/lib/public/LDAP/ILDAPProviderFactory.php', @@ -595,6 +596,7 @@ return array( 'OC\\Core\\Controller\\TwoFactorChallengeController' => $baseDir . '/core/Controller/TwoFactorChallengeController.php', 'OC\\Core\\Controller\\UserController' => $baseDir . '/core/Controller/UserController.php', 'OC\\Core\\Controller\\WalledGardenController' => $baseDir . '/core/Controller/WalledGardenController.php', + 'OC\\Core\\Controller\\WhatsNewController' => $baseDir . '/core/Controller/WhatsNewController.php', 'OC\\Core\\Middleware\\TwoFactorMiddleware' => $baseDir . '/core/Middleware/TwoFactorMiddleware.php', 'OC\\Core\\Migrations\\Version13000Date20170705121758' => $baseDir . '/core/Migrations/Version13000Date20170705121758.php', 'OC\\Core\\Migrations\\Version13000Date20170718121200' => $baseDir . '/core/Migrations/Version13000Date20170718121200.php', @@ -792,6 +794,7 @@ return array( 'OC\\L10N\\Factory' => $baseDir . '/lib/private/L10N/Factory.php', 'OC\\L10N\\L10N' => $baseDir . '/lib/private/L10N/L10N.php', 'OC\\L10N\\L10NString' => $baseDir . '/lib/private/L10N/L10NString.php', + 'OC\\L10N\\LanguageIterator' => $baseDir . '/lib/private/L10N/LanguageIterator.php', 'OC\\L10N\\LanguageNotFoundException' => $baseDir . '/lib/private/L10N/LanguageNotFoundException.php', 'OC\\LargeFileHelper' => $baseDir . '/lib/private/LargeFileHelper.php', 'OC\\Lock\\AbstractLockingProvider' => $baseDir . '/lib/private/Lock/AbstractLockingProvider.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 4a2580fe75..67626435cb 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -289,6 +289,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OCP\\IUserSession' => __DIR__ . '/../../..' . '/lib/public/IUserSession.php', 'OCP\\Image' => __DIR__ . '/../../..' . '/lib/public/Image.php', 'OCP\\L10N\\IFactory' => __DIR__ . '/../../..' . '/lib/public/L10N/IFactory.php', + 'OCP\\L10N\\ILanguageIterator' => __DIR__ . '/../../..' . '/lib/public/L10N/ILanguageIterator.php', 'OCP\\LDAP\\IDeletionFlagSupport' => __DIR__ . '/../../..' . '/lib/public/LDAP/IDeletionFlagSupport.php', 'OCP\\LDAP\\ILDAPProvider' => __DIR__ . '/../../..' . '/lib/public/LDAP/ILDAPProvider.php', 'OCP\\LDAP\\ILDAPProviderFactory' => __DIR__ . '/../../..' . '/lib/public/LDAP/ILDAPProviderFactory.php', @@ -625,6 +626,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\Core\\Controller\\TwoFactorChallengeController' => __DIR__ . '/../../..' . '/core/Controller/TwoFactorChallengeController.php', 'OC\\Core\\Controller\\UserController' => __DIR__ . '/../../..' . '/core/Controller/UserController.php', 'OC\\Core\\Controller\\WalledGardenController' => __DIR__ . '/../../..' . '/core/Controller/WalledGardenController.php', + 'OC\\Core\\Controller\\WhatsNewController' => __DIR__ . '/../../..' . '/core/Controller/WhatsNewController.php', 'OC\\Core\\Middleware\\TwoFactorMiddleware' => __DIR__ . '/../../..' . '/core/Middleware/TwoFactorMiddleware.php', 'OC\\Core\\Migrations\\Version13000Date20170705121758' => __DIR__ . '/../../..' . '/core/Migrations/Version13000Date20170705121758.php', 'OC\\Core\\Migrations\\Version13000Date20170718121200' => __DIR__ . '/../../..' . '/core/Migrations/Version13000Date20170718121200.php', @@ -822,6 +824,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\L10N\\Factory' => __DIR__ . '/../../..' . '/lib/private/L10N/Factory.php', 'OC\\L10N\\L10N' => __DIR__ . '/../../..' . '/lib/private/L10N/L10N.php', 'OC\\L10N\\L10NString' => __DIR__ . '/../../..' . '/lib/private/L10N/L10NString.php', + 'OC\\L10N\\LanguageIterator' => __DIR__ . '/../../..' . '/lib/private/L10N/LanguageIterator.php', 'OC\\L10N\\LanguageNotFoundException' => __DIR__ . '/../../..' . '/lib/private/L10N/LanguageNotFoundException.php', 'OC\\LargeFileHelper' => __DIR__ . '/../../..' . '/lib/private/LargeFileHelper.php', 'OC\\Lock\\AbstractLockingProvider' => __DIR__ . '/../../..' . '/lib/private/Lock/AbstractLockingProvider.php', diff --git a/lib/private/L10N/Factory.php b/lib/private/L10N/Factory.php index 79495f5ff8..cc2de17450 100644 --- a/lib/private/L10N/Factory.php +++ b/lib/private/L10N/Factory.php @@ -35,6 +35,7 @@ use OCP\IRequest; use OCP\IUser; use OCP\IUserSession; use OCP\L10N\IFactory; +use OCP\L10N\ILanguageIterator; /** * A factory that generates language instances @@ -322,35 +323,12 @@ 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'; + public function getLanguageIterator(IUser $user = null): ILanguageIterator { + $user = $user ?? $this->userSession->getUser(); + if($user === null) { + throw new \RuntimeException('Failed to get an IUser instance'); } + return new LanguageIterator($user, $this->config); } /** diff --git a/lib/private/L10N/LanguageIterator.php b/lib/private/L10N/LanguageIterator.php new file mode 100644 index 0000000000..ba8f942c91 --- /dev/null +++ b/lib/private/L10N/LanguageIterator.php @@ -0,0 +1,137 @@ + + * + * @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\L10N; + +use OCP\IConfig; +use OCP\IUser; +use OCP\L10N\ILanguageIterator; + +class LanguageIterator implements ILanguageIterator { + private $i = 0; + /** @var IConfig */ + private $config; + /** @var IUser */ + private $user; + + public function __construct(IUser $user, IConfig $config) { + $this->config = $config; + $this->user = $user; + } + + /** + * Rewind the Iterator to the first element + */ + public function rewind() { + $this->i = 0; + } + + /** + * Return the current element + * + * @since 14.0.0 + */ + public function current(): string { + switch($this->i) { + /** @noinspection PhpMissingBreakStatementInspection */ + case 0: + $forcedLang = $this->config->getSystemValue('force_language', false); + if(is_string($forcedLang)) { + return $forcedLang; + } + $this->next(); + /** @noinspection PhpMissingBreakStatementInspection */ + case 1: + $forcedLang = $this->config->getSystemValue('force_language', false); + if(is_string($forcedLang) + && ($truncated = $this->getTruncatedLanguage($forcedLang)) !== $forcedLang + ) { + return $truncated; + } + $this->next(); + /** @noinspection PhpMissingBreakStatementInspection */ + case 2: + $userLang = $this->config->getUserValue($this->user->getUID(), 'core', 'lang', null); + if(is_string($userLang)) { + return $userLang; + } + $this->next(); + /** @noinspection PhpMissingBreakStatementInspection */ + case 3: + $userLang = $this->config->getUserValue($this->user->getUID(), 'core', 'lang', null); + if(is_string($userLang) + && ($truncated = $this->getTruncatedLanguage($userLang)) !== $userLang + ) { + return $truncated; + } + $this->next(); + case 4: + return $this->config->getSystemValue('default_language', 'en'); + /** @noinspection PhpMissingBreakStatementInspection */ + case 5: + $defaultLang = $this->config->getSystemValue('default_language', 'en'); + if(($truncated = $this->getTruncatedLanguage($defaultLang)) !== $defaultLang) { + return $truncated; + } + $this->next(); + default: + return 'en'; + } + } + + /** + * Move forward to next element + * + * @since 14.0.0 + */ + public function next() { + ++$this->i; + } + + /** + * Return the key of the current element + * + * @since 14.0.0 + */ + public function key(): int { + return $this->i; + } + + /** + * Checks if current position is valid + * + * @since 14.0.0 + */ + public function valid(): bool { + return $this->i <= 6; + } + + protected function getTruncatedLanguage(string $lang):string { + $pos = strpos($lang, '_'); + if($pos !== false) { + $lang = substr($lang, 0, $pos); + } + return $lang; + } +} diff --git a/lib/private/Updater/ChangesCheck.php b/lib/private/Updater/ChangesCheck.php index 095f63db87..095cf396e8 100644 --- a/lib/private/Updater/ChangesCheck.php +++ b/lib/private/Updater/ChangesCheck.php @@ -47,6 +47,15 @@ class ChangesCheck { $this->logger = $logger; } + /** + * @throws DoesNotExistException + */ + public function getChangesForVersion(string $version): array { + $version = $this->normalizeVersion($version); + $changesInfo = $this->mapper->getChanges($version); + return json_decode($changesInfo->getData(), true); + } + /** * @throws \Exception */ @@ -145,7 +154,7 @@ class ChangesCheck { * 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 { + public 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) { diff --git a/lib/public/L10N/IFactory.php b/lib/public/L10N/IFactory.php index 9c00607320..1bc231e4e2 100644 --- a/lib/public/L10N/IFactory.php +++ b/lib/public/L10N/IFactory.php @@ -21,6 +21,8 @@ */ namespace OCP\L10N; +use OCP\IUser; + /** * @since 8.2.0 */ @@ -93,10 +95,16 @@ interface IFactory { /** * 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' + * 2. if applicable, the trunk of 1 (e.g. "fu" instead of "fu_BAR" + * 3. returns the user language or: + * 4. if applicable, the trunk of 3 + * 5. returns the system default language or: + * 6. if applicable, the trunk of 5 + * 7+∞. returns 'en' + * + * Hint: in most cases findLanguage() suits you fine + * * @since 14.0.0 */ - public function iterateLanguage(bool $reset = false): string; + public function getLanguageIterator(IUser $user = null): ILanguageIterator; } diff --git a/lib/public/L10N/ILanguageIterator.php b/lib/public/L10N/ILanguageIterator.php new file mode 100644 index 0000000000..ef923dfd24 --- /dev/null +++ b/lib/public/L10N/ILanguageIterator.php @@ -0,0 +1,74 @@ + + * + * @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 OCP\L10N; + +/** + * Interface ILanguageIterator + * + * iterator across language settings (if provided) in this order: + * 1. returns the forced language or: + * 2. if applicable, the trunk of 1 (e.g. "fu" instead of "fu_BAR" + * 3. returns the user language or: + * 4. if applicable, the trunk of 3 + * 5. returns the system default language or: + * 6. if applicable, the trunk of 5 + * 7+∞. returns 'en' + * + * if settings are not present or truncating is not applicable, the iterator + * skips to the next valid item itself + * + * @package OCP\L10N + * + * @since 14.0.0 + */ +interface ILanguageIterator extends \Iterator { + + /** + * Return the current element + * + * @since 14.0.0 + */ + public function current(): string; + + /** + * Move forward to next element + * + * @since 14.0.0 + */ + public function next(); + + /** + * Return the key of the current element + * + * @since 14.0.0 + */ + public function key():int; + + /** + * Checks if current position is valid + * + * @since 14.0.0 + */ + public function valid():bool; +} diff --git a/tests/lib/L10N/FactoryTest.php b/tests/lib/L10N/FactoryTest.php index 3008e0a239..be842cf12c 100644 --- a/tests/lib/L10N/FactoryTest.php +++ b/tests/lib/L10N/FactoryTest.php @@ -14,6 +14,7 @@ use OCP\IConfig; use OCP\IRequest; use OCP\IUser; use OCP\IUserSession; +use OCP\L10N\ILanguageIterator; use Test\TestCase; /** @@ -596,4 +597,33 @@ class FactoryTest extends TestCase { $this->assertSame($expected, $result); } + public function languageIteratorRequestProvider():array { + return [ + [ true, $this->createMock(IUser::class)], + [ false, $this->createMock(IUser::class)], + [ false, null] + ]; + } + + /** + * @dataProvider languageIteratorRequestProvider + */ + public function testGetLanguageIterator(bool $hasSession, IUser $iUserMock = null) { + $factory = $this->getFactory(); + + if($iUserMock === null) { + $matcher = $this->userSession->expects($this->once()) + ->method('getUser'); + + if($hasSession) { + $matcher->willReturn($this->createMock(IUser::class)); + } else { + $this->expectException(\RuntimeException::class); + } + } + + $iterator = $factory->getLanguageIterator($iUserMock); + $this->assertInstanceOf(ILanguageIterator::class, $iterator); + } + } diff --git a/tests/lib/L10N/LanguageIteratorTest.php b/tests/lib/L10N/LanguageIteratorTest.php new file mode 100644 index 0000000000..c8b1b24685 --- /dev/null +++ b/tests/lib/L10N/LanguageIteratorTest.php @@ -0,0 +1,98 @@ + + * + * @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\L10N; + +use OC\L10N\LanguageIterator; +use OCP\IConfig; +use OCP\IUser; +use Test\TestCase; + +class LanguageIteratorTest extends TestCase { + /** @var IUser|\PHPUnit_Framework_MockObject_MockObject */ + protected $user; + /** @var IConfig|\PHPUnit_Framework_MockObject_MockObject */ + protected $config; + /** @var LanguageIterator */ + protected $iterator; + + public function setUp() { + parent::setUp(); + + $this->user = $this->createMock(IUser::class); + $this->config = $this->createMock(IConfig::class); + + $this->iterator = new LanguageIterator($this->user, $this->config); + } + + public function languageSettingsProvider() { + return [ + // all language settings set + [ 'de_DE', 'es_CU', 'zh_TW', ['de_DE', 'de', 'es_CU', 'es', 'zh_TW', 'zh', 'en']], + [ 'de', 'es', 'zh', ['de', 'es', 'zh', 'en']], + [ 'en', 'en', 'en', ['en', 'en', 'en', 'en']], + // one possible setting is missing each + [ false, 'es_CU', 'zh_TW', ['es_CU', 'es', 'zh_TW', 'zh', 'en']], + [ false, 'es', 'zh_TW', ['es', 'zh_TW', 'zh', 'en']], + [ false, 'es_CU', 'zh', ['es_CU', 'es', 'zh', 'en']], + [ 'de_DE', null, 'zh_TW', ['de_DE', 'de', 'zh_TW', 'zh', 'en']], + [ 'de_DE', null, 'zh', ['de_DE', 'de', 'zh', 'en']], + [ 'de', null, 'zh_TW', ['de', 'zh_TW', 'zh', 'en']], + [ 'de_DE', 'es_CU', 'en', ['de_DE', 'de', 'es_CU', 'es', 'en', 'en']], + [ 'de', 'es_CU', 'en', ['de', 'es_CU', 'es', 'en', 'en']], + [ 'de_DE', 'es', 'en', ['de_DE', 'de', 'es', 'en', 'en']], + // two possible settings are missing each + [ false, null, 'zh_TW', ['zh_TW', 'zh', 'en']], + [ false, null, 'zh', ['zh', 'en']], + [ false, 'es_CU', 'en', ['es_CU', 'es', 'en', 'en']], + [ false, 'es', 'en', ['es', 'en', 'en']], + [ 'de_DE', null, 'en', ['de_DE', 'de', 'en', 'en']], + [ 'de', null, 'en', ['de', 'en', 'en']], + // nothing is set + [ false, null, 'en', ['en', 'en']], + + ]; + } + + /** + * @dataProvider languageSettingsProvider + */ + public function testIterator($forcedLang, $userLang, $sysLang, $expectedValues) { + $this->config->expects($this->any()) + ->method('getSystemValue') + ->willReturnMap([ + ['force_language', false, $forcedLang], + ['default_language', 'en', $sysLang], + ]); + $this->config->expects($this->any()) + ->method('getUserValue') + ->willReturn($userLang); + + foreach ($expectedValues as $expected) { + $this->assertTrue($this->iterator->valid()); + $this->assertSame($expected, $this->iterator->current()); + $this->iterator->next(); + } + $this->assertFalse($this->iterator->valid()); + } +} diff --git a/tests/lib/Updater/ChangesCheckTest.php b/tests/lib/Updater/ChangesCheckTest.php index cbb298647d..fe25e8ceba 100644 --- a/tests/lib/Updater/ChangesCheckTest.php +++ b/tests/lib/Updater/ChangesCheckTest.php @@ -29,11 +29,11 @@ namespace Test\Updater; use OC\Updater\ChangesCheck; use OC\Updater\ChangesMapper; use OC\Updater\ChangesResult; +use OCP\AppFramework\Db\DoesNotExistException; 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 { @@ -338,7 +338,42 @@ class ChangesCheckTest extends TestCase { * @dataProvider versionProvider */ public function testNormalizeVersion(string $input, string $expected) { - $normalized = $this->invokePrivate($this->checker, 'normalizeVersion', [$input]); + $normalized = $this->checker->normalizeVersion($input); $this->assertSame($expected, $normalized); } + + public function changeDataProvider():array { + $testDataFound = $testDataNotFound = $this->versionProvider(); + array_walk($testDataFound, function(&$params) { $params[] = true; }); + array_walk($testDataNotFound, function(&$params) { $params[] = false; }); + return array_merge($testDataFound, $testDataNotFound); + } + + /** + * @dataProvider changeDataProvider + * + */ + public function testGetChangesForVersion(string $inputVersion, string $normalizedVersion, bool $isFound) { + $mocker = $this->mapper->expects($this->once()) + ->method('getChanges') + ->with($normalizedVersion); + + if(!$isFound) { + $this->expectException(DoesNotExistException::class); + $mocker->willThrowException(new DoesNotExistException('Changes info is not present')); + } else { + $entry = $this->createMock(ChangesResult::class); + $entry->expects($this->once()) + ->method('__call') + ->with('getData') + ->willReturn('{"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":["\u00dcberarbeitete Benutzerschnittstelle","Ende-zu-Ende Verschl\u00fcsselung","Video- und Text-Chat"],"admin":["\u00c4nderungen an der Nginx Konfiguration","Theming: CSS Dateien wurden konsolidiert"]}}}'); + + $mocker->willReturn($entry); + } + + /** @noinspection PhpUnhandledExceptionInspection */ + $data = $this->checker->getChangesForVersion($inputVersion); + $this->assertTrue(isset($data['whatsNew']['en']['regular'])); + $this->assertTrue(isset($data['changelogURL'])); + } } From ca6094f3900fd463449d9973589b1d49aed28b2a Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Thu, 5 Jul 2018 20:29:00 +0200 Subject: [PATCH 2/2] wire the frontend Signed-off-by: Arthur Schiwon --- apps/files/css/merged.scss | 1 + core/Controller/WhatsNewController.php | 13 +++- core/css/whatsnew.scss | 31 +++++++++ core/js/public/whatsnew.js | 94 ++++++++++++++++++++++++-- 4 files changed, 130 insertions(+), 9 deletions(-) create mode 100644 core/css/whatsnew.scss diff --git a/apps/files/css/merged.scss b/apps/files/css/merged.scss index d65bac512f..8a11e55c26 100644 --- a/apps/files/css/merged.scss +++ b/apps/files/css/merged.scss @@ -2,3 +2,4 @@ @import 'upload.scss'; @import 'mobile.scss'; @import 'detailsView.scss'; +@import '../../../core/css/whatsnew.scss'; diff --git a/core/Controller/WhatsNewController.php b/core/Controller/WhatsNewController.php index d333165169..c3a6d28cea 100644 --- a/core/Controller/WhatsNewController.php +++ b/core/Controller/WhatsNewController.php @@ -29,6 +29,7 @@ use OC\Updater\ChangesCheck; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Http; use OCP\AppFramework\Http\DataResponse; +use OCP\Defaults; use OCP\IConfig; use OCP\IRequest; use OCP\IUserManager; @@ -45,6 +46,8 @@ class WhatsNewController extends OCSController { private $whatsNewService; /** @var IFactory */ private $langFactory; + /** @var Defaults */ + private $defaults; public function __construct( string $appName, @@ -55,13 +58,15 @@ class WhatsNewController extends OCSController { Manager $keyManager, IConfig $config, ChangesCheck $whatsNewService, - IFactory $langFactory + IFactory $langFactory, + Defaults $defaults ) { parent::__construct($appName, $request, $capabilitiesManager, $userSession, $userManager, $keyManager); $this->config = $config; $this->userSession = $userSession; $this->whatsNewService = $whatsNewService; $this->langFactory = $langFactory; + $this->defaults = $defaults; } /** @@ -82,7 +87,11 @@ class WhatsNewController extends OCSController { try { $iterator = $this->langFactory->getLanguageIterator(); $whatsNew = $this->whatsNewService->getChangesForVersion($currentVersion); - $resultData = ['changelogURL' => $whatsNew['changelogURL']]; + $resultData = [ + 'changelogURL' => $whatsNew['changelogURL'], + 'product' => $this->defaults->getName(), + 'version' => $currentVersion, + ]; do { $lang = $iterator->current(); if(isset($whatsNew['whatsNew'][$lang])) { diff --git a/core/css/whatsnew.scss b/core/css/whatsnew.scss new file mode 100644 index 0000000000..1c2ab08333 --- /dev/null +++ b/core/css/whatsnew.scss @@ -0,0 +1,31 @@ +/** + * @copyright Copyright (c) 2018, Arthur Schiwon + * + * @license GNU AGPL version 3 or any later version + * + */ + +.whatsNewPopover { + bottom: 35px !important; + left: 15px !important; + width: 270px; + background-color: var(--color-background-dark); +} + +.whatsNewPopover p { + width: auto !important; +} + +.whatsNewPopover .caption { + font-weight: bolder; + cursor: auto !important; +} + +.whatsNewPopover .icon-close { + position: absolute; + right: 0; +} + +.whatsNewPopover::after { + content: none; +} diff --git a/core/js/public/whatsnew.js b/core/js/public/whatsnew.js index dc5862e857..20a871ada2 100644 --- a/core/js/public/whatsnew.js +++ b/core/js/public/whatsnew.js @@ -14,10 +14,13 @@ query: function(options) { options = options || {}; + var dismissOptions = options.dismiss || {}; $.ajax({ type: 'GET', url: options.url || OC.linkToOCS('core', 2) + 'whatsnew?format=json', - success: options.success || this._onQuerySuccess, + success: options.success || function(data, statusText, xhr) { + OCP.WhatsNew._onQuerySuccess(data, statusText, xhr, dismissOptions); + }, error: options.error || this._onQueryError }); }, @@ -31,20 +34,97 @@ success: options.success || this._onDismissSuccess, error: options.error || this._onDismissError }); + // remove element immediately + $('.whatsNewPopover').remove(); }, - _onQuerySuccess: function(data, statusText) { - console.debug('querying Whats New data was successful: ' + data || statusText); + _onQuerySuccess: function(data, statusText, xhr, dismissOptions) { + console.debug('querying Whats New data was successful: ' + statusText); console.debug(data); + + if(xhr.status !== 200) { + return; + } + + var item, menuItem, text, icon; + + var div = document.createElement('div'); + div.classList.add('popovermenu', 'open', 'whatsNewPopover', 'menu-left'); + + var list = document.createElement('ul'); + + // header + item = document.createElement('li'); + menuItem = document.createElement('span'); + menuItem.className = "menuitem"; + + text = document.createElement('span'); + text.innerText = t('core', 'New in') + ' ' + data['ocs']['data']['product']; + text.className = 'caption'; + menuItem.appendChild(text); + + icon = document.createElement('span'); + icon.className = 'icon-close'; + icon.onclick = function () { + OCP.WhatsNew.dismiss(data['ocs']['data']['version'], dismissOptions); + }; + menuItem.appendChild(icon); + + item.appendChild(menuItem); + list.appendChild(item); + + // Highlights + for (var i in data['ocs']['data']['whatsNew']['regular']) { + var whatsNewTextItem = data['ocs']['data']['whatsNew']['regular'][i]; + item = document.createElement('li'); + + menuItem = document.createElement('span'); + menuItem.className = "menuitem"; + + icon = document.createElement('span'); + icon.className = 'icon-star-dark'; + menuItem.appendChild(icon); + + text = document.createElement('p'); + text.innerHTML = _.escape(whatsNewTextItem); + menuItem.appendChild(text); + + item.appendChild(menuItem); + list.appendChild(item); + } + + // Changelog URL + if(!_.isUndefined(data['ocs']['data']['changelogURL'])) { + item = document.createElement('li'); + + menuItem = document.createElement('a'); + menuItem.href = data['ocs']['data']['changelogURL']; + menuItem.rel = 'noreferrer noopener'; + menuItem.target = '_blank'; + + icon = document.createElement('span'); + icon.className = 'icon-link'; + menuItem.appendChild(icon); + + text = document.createElement('span'); + text.innerText = t('core', 'View changelog'); + menuItem.appendChild(text); + + item.appendChild(menuItem); + list.appendChild(item); + } + + div.appendChild(list); + document.body.appendChild(div); }, - _onQueryError: function (o, t, e) { - console.debug(o); - console.debug('querying Whats New Data resulted in an error: ' + t +e); + _onQueryError: function (x, t, e) { + console.debug('querying Whats New Data resulted in an error: ' + t + e); + console.debug(x); }, _onDismissSuccess: function(data) { - console.debug('dismissing Whats New data was successful: ' + data); + //noop }, _onDismissError: function (data) {