From 428e76214ed5f414f2cd9a5f67c07db4c14ac828 Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Thu, 15 Jan 2026 23:07:17 +0100 Subject: [PATCH] chore: drop legacy `WhatsNew` This feature was not used in 8 years and from frontend did not even properly work anymore and was implemented using deprecated API. So get rid of it. The last version that was using a changelog from the changelog server was Nextcloud 20. We use the firstrunwizard nowadays for informing about Nextcloud changes in new releases. Signed-off-by: Ferdinand Thiessen --- apps/updatenotification/lib/UpdateChecker.php | 9 - .../tests/UpdateCheckerTest.php | 11 - build/psalm-baseline.xml | 6 - core/Controller/WhatsNewController.php | 104 ----- core/Listener/AddMissingIndicesListener.php | 8 - .../Version14000Date20180626223656.php | 52 --- .../Version34000Date20260122120000.php | 23 ++ core/openapi-full.json | 243 ----------- core/openapi.json | 243 ----------- core/src/OCP/index.js | 2 - core/src/OCP/whatsnew.js | 150 ------- lib/composer/composer/autoload_classmap.php | 6 +- lib/composer/composer/autoload_static.php | 6 +- lib/private/Updater/Changes.php | 46 --- lib/private/Updater/ChangesCheck.php | 158 -------- lib/private/Updater/ChangesMapper.php | 44 -- openapi.json | 243 ----------- tests/lib/Updater/ChangesCheckTest.php | 378 ------------------ 18 files changed, 25 insertions(+), 1707 deletions(-) delete mode 100644 core/Controller/WhatsNewController.php delete mode 100644 core/Migrations/Version14000Date20180626223656.php create mode 100644 core/Migrations/Version34000Date20260122120000.php delete mode 100644 core/src/OCP/whatsnew.js delete mode 100644 lib/private/Updater/Changes.php delete mode 100644 lib/private/Updater/ChangesCheck.php delete mode 100644 lib/private/Updater/ChangesMapper.php delete mode 100644 tests/lib/Updater/ChangesCheckTest.php diff --git a/apps/updatenotification/lib/UpdateChecker.php b/apps/updatenotification/lib/UpdateChecker.php index b206ba4a3e4..198793b8864 100644 --- a/apps/updatenotification/lib/UpdateChecker.php +++ b/apps/updatenotification/lib/UpdateChecker.php @@ -9,7 +9,6 @@ declare(strict_types=1); */ namespace OCA\UpdateNotification; -use OC\Updater\ChangesCheck; use OC\Updater\VersionCheck; use OCP\AppFramework\Services\IInitialState; @@ -17,7 +16,6 @@ class UpdateChecker { public function __construct( private VersionCheck $updater, - private ChangesCheck $changesCheck, private IInitialState $initialState, ) { } @@ -41,13 +39,6 @@ class UpdateChecker { if (strpos($data['url'], 'https://') === 0) { $result['downloadLink'] = $data['url']; } - 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/tests/UpdateCheckerTest.php b/apps/updatenotification/tests/UpdateCheckerTest.php index cffdc25d3e4..b8693acd0ef 100644 --- a/apps/updatenotification/tests/UpdateCheckerTest.php +++ b/apps/updatenotification/tests/UpdateCheckerTest.php @@ -8,7 +8,6 @@ declare(strict_types=1); */ namespace OCA\UpdateNotification\Tests; -use OC\Updater\ChangesCheck; use OC\Updater\VersionCheck; use OCA\UpdateNotification\UpdateChecker; use OCP\AppFramework\Services\IInitialState; @@ -17,7 +16,6 @@ use Test\TestCase; class UpdateCheckerTest extends TestCase { - private ChangesCheck&MockObject $changesChecker; private VersionCheck&MockObject $updater; private IInitialState&MockObject $initialState; private UpdateChecker $updateChecker; @@ -26,11 +24,9 @@ class UpdateCheckerTest extends TestCase { parent::setUp(); $this->updater = $this->createMock(VersionCheck::class); - $this->changesChecker = $this->createMock(ChangesCheck::class); $this->initialState = $this->createMock(IInitialState::class); $this->updateChecker = new UpdateChecker( $this->updater, - $this->changesChecker, $this->initialState, ); } @@ -85,15 +81,10 @@ class UpdateCheckerTest extends TestCase { 'versionstring' => 'Nextcloud 1.2.3', 'web' => 'https://docs.nextcloud.com/myUrl', 'url' => 'https://downloads.nextcloud.org/server', - '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' => '1.2.3', @@ -102,7 +93,6 @@ class UpdateCheckerTest extends TestCase { 'versionIsEol' => false, 'updateLink' => 'https://docs.nextcloud.com/myUrl', 'downloadLink' => 'https://downloads.nextcloud.org/server', - 'changes' => $changes, ]; $this->assertSame($expected, $this->updateChecker->getUpdateState()); } @@ -126,7 +116,6 @@ class UpdateCheckerTest extends TestCase { 'versionstring' => 'Nextcloud 1.2.3', 'web' => 'https://docs.nextcloud.com/myUrl', 'url' => 'https://downloads.nextcloud.org/server', - 'changes' => 'https://updates.nextcloud.com/changelog_server/?version=123.0.0', 'autoupdater' => '1', 'eol' => '0', ]); diff --git a/build/psalm-baseline.xml b/build/psalm-baseline.xml index c3fe00becb5..84f4b71a655 100644 --- a/build/psalm-baseline.xml +++ b/build/psalm-baseline.xml @@ -3215,12 +3215,6 @@ )]]> - - - - - - diff --git a/core/Controller/WhatsNewController.php b/core/Controller/WhatsNewController.php deleted file mode 100644 index e54fb13b892..00000000000 --- a/core/Controller/WhatsNewController.php +++ /dev/null @@ -1,104 +0,0 @@ -, admin: list}}, array{}>|DataResponse, array{}> - * - * 200: Changes returned - * 204: No changes - */ - #[NoAdminRequired] - #[ApiRoute(verb: 'GET', url: '/whatsnew', root: '/core')] - 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'], - 'product' => $this->defaults->getProductName(), - 'version' => $currentVersion, - ]; - 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); - } - } - - /** - * Dismiss the changes - * - * @param string $version Version to dismiss the changes for - * - * @return DataResponse, array{}> - * @throws PreConditionNotMetException - * @throws DoesNotExistException - * - * 200: Changes dismissed - */ - #[NoAdminRequired] - #[ApiRoute(verb: 'POST', url: '/whatsnew', root: '/core')] - 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/Listener/AddMissingIndicesListener.php b/core/Listener/AddMissingIndicesListener.php index 0b8b4f86f6e..27880fabeac 100644 --- a/core/Listener/AddMissingIndicesListener.php +++ b/core/Listener/AddMissingIndicesListener.php @@ -99,14 +99,6 @@ class AddMissingIndicesListener implements IEventListener { true ); - $event->addMissingIndex( - 'whats_new', - 'version', - ['version'], - [], - true - ); - $event->addMissingIndex( 'cards', 'cards_abiduri', diff --git a/core/Migrations/Version14000Date20180626223656.php b/core/Migrations/Version14000Date20180626223656.php deleted file mode 100644 index 7d4dea585f6..00000000000 --- a/core/Migrations/Version14000Date20180626223656.php +++ /dev/null @@ -1,52 +0,0 @@ -hasTable('whats_new')) { - $table = $schema->createTable('whats_new'); - $table->addColumn('id', 'integer', [ - 'autoincrement' => true, - 'notnull' => true, - 'length' => 4, - 'unsigned' => true, - ]); - $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(['id']); - $table->addUniqueIndex(['version'], 'version'); - $table->addIndex(['version', 'etag'], 'version_etag_idx'); - } - - return $schema; - } -} diff --git a/core/Migrations/Version34000Date20260122120000.php b/core/Migrations/Version34000Date20260122120000.php new file mode 100644 index 00000000000..0fcf9b06980 --- /dev/null +++ b/core/Migrations/Version34000Date20260122120000.php @@ -0,0 +1,23 @@ +hasTable('whats_new')) { + $schema->dropTable('whats_new'); + } + return $schema; + } +} diff --git a/core/openapi-full.json b/core/openapi-full.json index ae829249c31..de4c07ec391 100644 --- a/core/openapi-full.json +++ b/core/openapi-full.json @@ -9002,249 +9002,6 @@ } } }, - "/ocs/v2.php/core/whatsnew": { - "get": { - "operationId": "whats_new-get", - "summary": "Get the changes", - "tags": [ - "whats_new" - ], - "security": [ - { - "bearer_auth": [] - }, - { - "basic_auth": [] - } - ], - "parameters": [ - { - "name": "OCS-APIRequest", - "in": "header", - "description": "Required to be true for the API request to pass", - "required": true, - "schema": { - "type": "boolean", - "default": true - } - } - ], - "responses": { - "200": { - "description": "Changes returned", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": { - "type": "object", - "required": [ - "changelogURL", - "product", - "version" - ], - "properties": { - "changelogURL": { - "type": "string" - }, - "product": { - "type": "string" - }, - "version": { - "type": "string" - }, - "whatsNew": { - "type": "object", - "required": [ - "regular", - "admin" - ], - "properties": { - "regular": { - "type": "array", - "items": { - "type": "string" - } - }, - "admin": { - "type": "array", - "items": { - "type": "string" - } - } - } - } - } - } - } - } - } - } - } - } - }, - "204": { - "description": "No changes" - }, - "401": { - "description": "Current user is not logged in", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": {} - } - } - } - } - } - } - } - } - }, - "post": { - "operationId": "whats_new-dismiss", - "summary": "Dismiss the changes", - "tags": [ - "whats_new" - ], - "security": [ - { - "bearer_auth": [] - }, - { - "basic_auth": [] - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "version" - ], - "properties": { - "version": { - "type": "string", - "description": "Version to dismiss the changes for" - } - } - } - } - } - }, - "parameters": [ - { - "name": "OCS-APIRequest", - "in": "header", - "description": "Required to be true for the API request to pass", - "required": true, - "schema": { - "type": "boolean", - "default": true - } - } - ], - "responses": { - "200": { - "description": "Changes dismissed", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": {} - } - } - } - } - } - } - }, - "500": { - "description": "", - "content": { - "text/plain": { - "schema": { - "type": "string" - } - } - } - }, - "401": { - "description": "Current user is not logged in", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": {} - } - } - } - } - } - } - } - } - } - }, "/index.php/avatar/{userId}/{size}/dark": { "get": { "operationId": "avatar-get-avatar-dark", diff --git a/core/openapi.json b/core/openapi.json index 9c03e43bcfd..145894f33a0 100644 --- a/core/openapi.json +++ b/core/openapi.json @@ -9002,249 +9002,6 @@ } } }, - "/ocs/v2.php/core/whatsnew": { - "get": { - "operationId": "whats_new-get", - "summary": "Get the changes", - "tags": [ - "whats_new" - ], - "security": [ - { - "bearer_auth": [] - }, - { - "basic_auth": [] - } - ], - "parameters": [ - { - "name": "OCS-APIRequest", - "in": "header", - "description": "Required to be true for the API request to pass", - "required": true, - "schema": { - "type": "boolean", - "default": true - } - } - ], - "responses": { - "200": { - "description": "Changes returned", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": { - "type": "object", - "required": [ - "changelogURL", - "product", - "version" - ], - "properties": { - "changelogURL": { - "type": "string" - }, - "product": { - "type": "string" - }, - "version": { - "type": "string" - }, - "whatsNew": { - "type": "object", - "required": [ - "regular", - "admin" - ], - "properties": { - "regular": { - "type": "array", - "items": { - "type": "string" - } - }, - "admin": { - "type": "array", - "items": { - "type": "string" - } - } - } - } - } - } - } - } - } - } - } - } - }, - "204": { - "description": "No changes" - }, - "401": { - "description": "Current user is not logged in", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": {} - } - } - } - } - } - } - } - } - }, - "post": { - "operationId": "whats_new-dismiss", - "summary": "Dismiss the changes", - "tags": [ - "whats_new" - ], - "security": [ - { - "bearer_auth": [] - }, - { - "basic_auth": [] - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "version" - ], - "properties": { - "version": { - "type": "string", - "description": "Version to dismiss the changes for" - } - } - } - } - } - }, - "parameters": [ - { - "name": "OCS-APIRequest", - "in": "header", - "description": "Required to be true for the API request to pass", - "required": true, - "schema": { - "type": "boolean", - "default": true - } - } - ], - "responses": { - "200": { - "description": "Changes dismissed", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": {} - } - } - } - } - } - } - }, - "500": { - "description": "", - "content": { - "text/plain": { - "schema": { - "type": "string" - } - } - } - }, - "401": { - "description": "Current user is not logged in", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": {} - } - } - } - } - } - } - } - } - } - }, "/index.php/avatar/{userId}/{size}/dark": { "get": { "operationId": "avatar-get-avatar-dark", diff --git a/core/src/OCP/index.js b/core/src/OCP/index.js index b2377f050a5..20d155109a8 100644 --- a/core/src/OCP/index.js +++ b/core/src/OCP/index.js @@ -10,7 +10,6 @@ import Collaboration from './collaboration.js' import * as Comments from './comments.ts' import Loader from './loader.js' import Toast from './toast.js' -import * as WhatsNew from './whatsnew.js' /** @namespace OCP */ export default { @@ -32,5 +31,4 @@ export default { * @deprecated 19.0.0 use the `@nextcloud/dialogs` package instead */ Toast, - WhatsNew, } diff --git a/core/src/OCP/whatsnew.js b/core/src/OCP/whatsnew.js deleted file mode 100644 index 10966561272..00000000000 --- a/core/src/OCP/whatsnew.js +++ /dev/null @@ -1,150 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -import { generateOcsUrl } from '@nextcloud/router' -import $ from 'jquery' -import _ from 'underscore' -import logger from '../logger.js' - -/** - * @param {any} options - - */ -export function query(options) { - options = options || {} - const dismissOptions = options.dismiss || {} - $.ajax({ - type: 'GET', - url: options.url || generateOcsUrl('core/whatsnew?format=json'), - success: options.success || function(data, statusText, xhr) { - onQuerySuccess(data, statusText, xhr, dismissOptions) - }, - error: options.error || onQueryError, - }) -} - -/** - * @param {any} version - - * @param {any} options - - */ -export function dismiss(version, options) { - options = options || {} - $.ajax({ - type: 'POST', - url: options.url || generateOcsUrl('core/whatsnew'), - data: { version: encodeURIComponent(version) }, - success: options.success || onDismissSuccess, - error: options.error || onDismissError, - }) - // remove element immediately - $('.whatsNewPopover').remove() -} - -/** - * @param {any} data - - * @param {any} statusText - - * @param {any} xhr - - * @param {any} dismissOptions - - */ -function onQuerySuccess(data, statusText, xhr, dismissOptions) { - logger.debug('querying Whats New data was successful: ' + statusText, { data }) - - if (xhr.status !== 200) { - return - } - - let item, menuItem, text, icon - - const div = document.createElement('div') - div.classList.add('popovermenu', 'open', 'whatsNewPopover', 'menu-left') - - const 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() { - dismiss(data.ocs.data.version, dismissOptions) - } - menuItem.appendChild(icon) - - item.appendChild(menuItem) - list.appendChild(item) - - // Highlights - for (const i in data.ocs.data.whatsNew.regular) { - const 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-checkmark' - 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) -} - -/** - * @param {any} x - - * @param {any} t - - * @param {any} e - - */ -function onQueryError(x, t, e) { - logger.debug('querying Whats New Data resulted in an error: ' + t + e) - logger.debug(x) -} - -/** - */ -function onDismissSuccess() { - // noop -} - -/** - * @param {any} data - - */ -function onDismissError(data) { - logger.debug('dismissing Whats New data resulted in an error: ' + data) -} diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 973cb023573..ea981fe1615 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -1476,7 +1476,6 @@ return array( 'OC\\Core\\Controller\\WalledGardenController' => $baseDir . '/core/Controller/WalledGardenController.php', 'OC\\Core\\Controller\\WebAuthnController' => $baseDir . '/core/Controller/WebAuthnController.php', 'OC\\Core\\Controller\\WellKnownController' => $baseDir . '/core/Controller/WellKnownController.php', - 'OC\\Core\\Controller\\WhatsNewController' => $baseDir . '/core/Controller/WhatsNewController.php', 'OC\\Core\\Controller\\WipeController' => $baseDir . '/core/Controller/WipeController.php', 'OC\\Core\\Data\\LoginFlowV2Credentials' => $baseDir . '/core/Data/LoginFlowV2Credentials.php', 'OC\\Core\\Data\\LoginFlowV2Tokens' => $baseDir . '/core/Data/LoginFlowV2Tokens.php', @@ -1506,7 +1505,6 @@ 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\\Core\\Migrations\\Version14000Date20180710092004' => $baseDir . '/core/Migrations/Version14000Date20180710092004.php', 'OC\\Core\\Migrations\\Version14000Date20180712153140' => $baseDir . '/core/Migrations/Version14000Date20180712153140.php', 'OC\\Core\\Migrations\\Version15000Date20180926101451' => $baseDir . '/core/Migrations/Version15000Date20180926101451.php', @@ -1585,6 +1583,7 @@ return array( 'OC\\Core\\Migrations\\Version33000Date20251124110529' => $baseDir . '/core/Migrations/Version33000Date20251124110529.php', 'OC\\Core\\Migrations\\Version33000Date20251126152410' => $baseDir . '/core/Migrations/Version33000Date20251126152410.php', 'OC\\Core\\Migrations\\Version33000Date20251209123503' => $baseDir . '/core/Migrations/Version33000Date20251209123503.php', + 'OC\\Core\\Migrations\\Version34000Date20260122120000' => $baseDir . '/core/Migrations/Version34000Date20260122120000.php', 'OC\\Core\\Notification\\CoreNotifier' => $baseDir . '/core/Notification/CoreNotifier.php', 'OC\\Core\\ResponseDefinitions' => $baseDir . '/core/ResponseDefinitions.php', 'OC\\Core\\Service\\CronService' => $baseDir . '/core/Service/CronService.php', @@ -2230,9 +2229,6 @@ return array( 'OC\\Translation\\TranslationManager' => $baseDir . '/lib/private/Translation/TranslationManager.php', 'OC\\URLGenerator' => $baseDir . '/lib/private/URLGenerator.php', 'OC\\Updater' => $baseDir . '/lib/private/Updater.php', - 'OC\\Updater\\Changes' => $baseDir . '/lib/private/Updater/Changes.php', - 'OC\\Updater\\ChangesCheck' => $baseDir . '/lib/private/Updater/ChangesCheck.php', - 'OC\\Updater\\ChangesMapper' => $baseDir . '/lib/private/Updater/ChangesMapper.php', 'OC\\Updater\\Exceptions\\ReleaseMetadataException' => $baseDir . '/lib/private/Updater/Exceptions/ReleaseMetadataException.php', 'OC\\Updater\\ReleaseMetadata' => $baseDir . '/lib/private/Updater/ReleaseMetadata.php', 'OC\\Updater\\VersionCheck' => $baseDir . '/lib/private/Updater/VersionCheck.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index f589072eb39..cd0ee28a3fb 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -1517,7 +1517,6 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\Core\\Controller\\WalledGardenController' => __DIR__ . '/../../..' . '/core/Controller/WalledGardenController.php', 'OC\\Core\\Controller\\WebAuthnController' => __DIR__ . '/../../..' . '/core/Controller/WebAuthnController.php', 'OC\\Core\\Controller\\WellKnownController' => __DIR__ . '/../../..' . '/core/Controller/WellKnownController.php', - 'OC\\Core\\Controller\\WhatsNewController' => __DIR__ . '/../../..' . '/core/Controller/WhatsNewController.php', 'OC\\Core\\Controller\\WipeController' => __DIR__ . '/../../..' . '/core/Controller/WipeController.php', 'OC\\Core\\Data\\LoginFlowV2Credentials' => __DIR__ . '/../../..' . '/core/Data/LoginFlowV2Credentials.php', 'OC\\Core\\Data\\LoginFlowV2Tokens' => __DIR__ . '/../../..' . '/core/Data/LoginFlowV2Tokens.php', @@ -1547,7 +1546,6 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 '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\\Core\\Migrations\\Version14000Date20180710092004' => __DIR__ . '/../../..' . '/core/Migrations/Version14000Date20180710092004.php', 'OC\\Core\\Migrations\\Version14000Date20180712153140' => __DIR__ . '/../../..' . '/core/Migrations/Version14000Date20180712153140.php', 'OC\\Core\\Migrations\\Version15000Date20180926101451' => __DIR__ . '/../../..' . '/core/Migrations/Version15000Date20180926101451.php', @@ -1626,6 +1624,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\Core\\Migrations\\Version33000Date20251124110529' => __DIR__ . '/../../..' . '/core/Migrations/Version33000Date20251124110529.php', 'OC\\Core\\Migrations\\Version33000Date20251126152410' => __DIR__ . '/../../..' . '/core/Migrations/Version33000Date20251126152410.php', 'OC\\Core\\Migrations\\Version33000Date20251209123503' => __DIR__ . '/../../..' . '/core/Migrations/Version33000Date20251209123503.php', + 'OC\\Core\\Migrations\\Version34000Date20260122120000' => __DIR__ . '/../../..' . '/core/Migrations/Version34000Date20260122120000.php', 'OC\\Core\\Notification\\CoreNotifier' => __DIR__ . '/../../..' . '/core/Notification/CoreNotifier.php', 'OC\\Core\\ResponseDefinitions' => __DIR__ . '/../../..' . '/core/ResponseDefinitions.php', 'OC\\Core\\Service\\CronService' => __DIR__ . '/../../..' . '/core/Service/CronService.php', @@ -2271,9 +2270,6 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\Translation\\TranslationManager' => __DIR__ . '/../../..' . '/lib/private/Translation/TranslationManager.php', 'OC\\URLGenerator' => __DIR__ . '/../../..' . '/lib/private/URLGenerator.php', 'OC\\Updater' => __DIR__ . '/../../..' . '/lib/private/Updater.php', - 'OC\\Updater\\Changes' => __DIR__ . '/../../..' . '/lib/private/Updater/Changes.php', - 'OC\\Updater\\ChangesCheck' => __DIR__ . '/../../..' . '/lib/private/Updater/ChangesCheck.php', - 'OC\\Updater\\ChangesMapper' => __DIR__ . '/../../..' . '/lib/private/Updater/ChangesMapper.php', 'OC\\Updater\\Exceptions\\ReleaseMetadataException' => __DIR__ . '/../../..' . '/lib/private/Updater/Exceptions/ReleaseMetadataException.php', 'OC\\Updater\\ReleaseMetadata' => __DIR__ . '/../../..' . '/lib/private/Updater/ReleaseMetadata.php', 'OC\\Updater\\VersionCheck' => __DIR__ . '/../../..' . '/lib/private/Updater/VersionCheck.php', diff --git a/lib/private/Updater/Changes.php b/lib/private/Updater/Changes.php deleted file mode 100644 index c941dfb3fa5..00000000000 --- a/lib/private/Updater/Changes.php +++ /dev/null @@ -1,46 +0,0 @@ -addType('version', 'string'); - $this->addType('etag', 'string'); - $this->addType('lastCheck', Types::INTEGER); - $this->addType('data', 'string'); - } -} diff --git a/lib/private/Updater/ChangesCheck.php b/lib/private/Updater/ChangesCheck.php deleted file mode 100644 index e88969f62a8..00000000000 --- a/lib/private/Updater/ChangesCheck.php +++ /dev/null @@ -1,158 +0,0 @@ -clientService = $clientService; - $this->mapper = $mapper; - $this->logger = $logger; - } - - /** - * @throws DoesNotExistException - * @return array{changelogURL: string, whatsNew: array, regular: list}>} - */ - public function getChangesForVersion(string $version): array { - $version = $this->normalizeVersion($version); - $changesInfo = $this->mapper->getChanges($version); - $changesData = json_decode($changesInfo->getData(), true); - if (empty($changesData)) { - throw new DoesNotExistException('Unable to decode changes info'); - } - return $changesData; - } - - /** - * @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 Changes(); - } - - $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; - } elseif ($response->getStatusCode() === 404) { - return self::RESPONSE_NO_CONTENT; - } elseif ($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(Changes $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, Changes $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) { - if (\LIBXML_VERSION < 20900) { - $loadEntities = libxml_disable_entity_loader(true); - $xml = @simplexml_load_string($body); - libxml_disable_entity_loader($loadEntities); - } else { - $xml = @simplexml_load_string($body); - } - 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. - */ - 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) { - // 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 deleted file mode 100644 index c399948ff10..00000000000 --- a/lib/private/Updater/ChangesMapper.php +++ /dev/null @@ -1,44 +0,0 @@ - - */ -class ChangesMapper extends QBMapper { - public const TABLE_NAME = 'whats_new'; - - public function __construct(IDBConnection $db) { - parent::__construct($db, self::TABLE_NAME); - } - - /** - * @throws DoesNotExistException - */ - public function getChanges(string $version): Changes { - /* @var $qb IQueryBuilder */ - $qb = $this->db->getQueryBuilder(); - $result = $qb->select('*') - ->from(self::TABLE_NAME) - ->where($qb->expr()->eq('version', $qb->createNamedParameter($version))) - ->executeQuery(); - - $data = $result->fetch(); - $result->closeCursor(); - if ($data === false) { - throw new DoesNotExistException('Changes info is not present'); - } - return Changes::fromRow($data); - } -} diff --git a/openapi.json b/openapi.json index fc220c51fb4..6f30feb7404 100644 --- a/openapi.json +++ b/openapi.json @@ -12549,249 +12549,6 @@ } } }, - "/ocs/v2.php/core/whatsnew": { - "get": { - "operationId": "core-whats_new-get", - "summary": "Get the changes", - "tags": [ - "core/whats_new" - ], - "security": [ - { - "bearer_auth": [] - }, - { - "basic_auth": [] - } - ], - "parameters": [ - { - "name": "OCS-APIRequest", - "in": "header", - "description": "Required to be true for the API request to pass", - "required": true, - "schema": { - "type": "boolean", - "default": true - } - } - ], - "responses": { - "200": { - "description": "Changes returned", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": { - "type": "object", - "required": [ - "changelogURL", - "product", - "version" - ], - "properties": { - "changelogURL": { - "type": "string" - }, - "product": { - "type": "string" - }, - "version": { - "type": "string" - }, - "whatsNew": { - "type": "object", - "required": [ - "regular", - "admin" - ], - "properties": { - "regular": { - "type": "array", - "items": { - "type": "string" - } - }, - "admin": { - "type": "array", - "items": { - "type": "string" - } - } - } - } - } - } - } - } - } - } - } - } - }, - "204": { - "description": "No changes" - }, - "401": { - "description": "Current user is not logged in", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": {} - } - } - } - } - } - } - } - } - }, - "post": { - "operationId": "core-whats_new-dismiss", - "summary": "Dismiss the changes", - "tags": [ - "core/whats_new" - ], - "security": [ - { - "bearer_auth": [] - }, - { - "basic_auth": [] - } - ], - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "version" - ], - "properties": { - "version": { - "type": "string", - "description": "Version to dismiss the changes for" - } - } - } - } - } - }, - "parameters": [ - { - "name": "OCS-APIRequest", - "in": "header", - "description": "Required to be true for the API request to pass", - "required": true, - "schema": { - "type": "boolean", - "default": true - } - } - ], - "responses": { - "200": { - "description": "Changes dismissed", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": {} - } - } - } - } - } - } - }, - "500": { - "description": "", - "content": { - "text/plain": { - "schema": { - "type": "string" - } - } - } - }, - "401": { - "description": "Current user is not logged in", - "content": { - "application/json": { - "schema": { - "type": "object", - "required": [ - "ocs" - ], - "properties": { - "ocs": { - "type": "object", - "required": [ - "meta", - "data" - ], - "properties": { - "meta": { - "$ref": "#/components/schemas/OCSMeta" - }, - "data": {} - } - } - } - } - } - } - } - } - } - }, "/index.php/avatar/{userId}/{size}/dark": { "get": { "operationId": "core-avatar-get-avatar-dark", diff --git a/tests/lib/Updater/ChangesCheckTest.php b/tests/lib/Updater/ChangesCheckTest.php deleted file mode 100644 index dd0d97a9e80..00000000000 --- a/tests/lib/Updater/ChangesCheckTest.php +++ /dev/null @@ -1,378 +0,0 @@ -clientService = $this->createMock(IClientService::class); - $this->mapper = $this->createMock(ChangesMapper::class); - $this->logger = $this->createMock(LoggerInterface::class); - - $this->checker = new ChangesCheck($this->clientService, $this->mapper, $this->logger); - } - - public static function statusCodeProvider(): array { - return [ - [200, ChangesCheck::RESPONSE_HAS_CONTENT], - [304, ChangesCheck::RESPONSE_USE_CACHE], - [404, ChangesCheck::RESPONSE_NO_CONTENT], - [418, ChangesCheck::RESPONSE_NO_CONTENT], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('statusCodeProvider')] - public function testEvaluateResponse(int $statusCode, int $expected): void { - $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(): void { - $version = '13.0.4'; - $entry = $this->createMock(Changes::class); - $entry->expects($this->exactly(2)) - ->method('__call') - ->willReturnMap([ - ['getVersion', [], ''], - ['setVersion', [$version], 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(): void { - $version = '13.0.4'; - $entry = $this->createMock(Changes::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 static 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' => [], - ], - ], - ] - ], - [ # 4 - empty - '', - [] - ], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('changesXMLProvider')] - public function testExtractData(string $body, array $expected): void { - $actual = $this->invokePrivate($this->checker, 'extractData', [$body]); - $this->assertSame($expected, $actual); - } - - public static function etagProvider() { - return [ - [''], - ['a27aab83d8205d73978435076e53d143'] - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('etagProvider')] - public function testQueryChangesServer(string $etag): void { - $uri = 'https://changes.nextcloud.server/?13.0.5'; - $entry = $this->createMock(Changes::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 static 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'], - ]; - } - - #[\PHPUnit\Framework\Attributes\DataProvider('versionProvider')] - public function testNormalizeVersion(string $input, string $expected): void { - $normalized = $this->checker->normalizeVersion($input); - $this->assertSame($expected, $normalized); - } - - public static function changeDataProvider():array { - $testDataFound = $testDataNotFound = self::versionProvider(); - array_walk($testDataFound, static function (&$params): void { - $params[] = true; - }); - array_walk($testDataNotFound, static function (&$params): void { - $params[] = false; - }); - return array_merge($testDataFound, $testDataNotFound); - } - - #[\PHPUnit\Framework\Attributes\DataProvider('changeDataProvider')] - public function testGetChangesForVersion(string $inputVersion, string $normalizedVersion, bool $isFound): void { - $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(Changes::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'])); - } - - public function testGetChangesForVersionEmptyData(): void { - $entry = $this->createMock(Changes::class); - $entry->expects($this->once()) - ->method('__call') - ->with('getData') - ->willReturn(''); - - $this->mapper->expects($this->once()) - ->method('getChanges') - ->with('13.0.7') - ->willReturn($entry); - - $this->expectException(DoesNotExistException::class); - /** @noinspection PhpUnhandledExceptionInspection */ - $this->checker->getChangesForVersion('13.0.7'); - } -}