Skip to content

Commit 283e2c7

Browse files
authored
Merge pull request #468 from ConductionNL/fix/438-settings-page
fix(settings): a Settings tab for the settings only occ could change
2 parents 51d01ba + 42068bf commit 283e2c7

22 files changed

Lines changed: 1400 additions & 20 deletions

‎l10n/en.js‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,20 @@ OC.L10N.register(
235235
"Automatic updates are off, so nothing below runs until they are enabled.": "Automatic updates are off, so nothing below runs until they are enabled.",
236236
"Runs in the window {window} ({timeZone}).": "Runs in the window {window} ({timeZone}).",
237237
"No app has an automatic update policy.": "No app has an automatic update policy.",
238-
"policy: {level}": "policy: {level}"
238+
"policy: {level}": "policy: {level}",
239+
"Could not load the settings.": "Could not load the settings.",
240+
"Settings saved.": "Settings saved.",
241+
"Could not save the settings.": "Could not save the settings.",
242+
"History and cache": "History and cache",
243+
"Keep history for (days, {min}-{max})": "Keep history for (days, {min}-{max})",
244+
"Cached versions per app (0 turns the cache off, at most {max})": "Cached versions per app (0 turns the cache off, at most {max})",
245+
"Sources and mirrors": "Sources and mirrors",
246+
"Leave a field empty to use the default shown under it.": "Leave a field empty to use the default shown under it.",
247+
"App Store API address (a store mirror)": "App Store API address (a store mirror)",
248+
"Default: {url}": "Default: {url}",
249+
"GitHub API address (GitHub Enterprise, https only)": "GitHub API address (GitHub Enterprise, https only)",
250+
"GitHub web address (GitHub Enterprise, https only)": "GitHub web address (GitHub Enterprise, https only)",
251+
"Advisory feed address (an internal mirror)": "Advisory feed address (an internal mirror)"
239252
},
240253
"nplurals=2; plural=(n != 1);"
241254
)

‎l10n/en.json‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,19 @@
240240
"Automatic updates are off, so nothing below runs until they are enabled.": "Automatic updates are off, so nothing below runs until they are enabled.",
241241
"Runs in the window {window} ({timeZone}).": "Runs in the window {window} ({timeZone}).",
242242
"No app has an automatic update policy.": "No app has an automatic update policy.",
243-
"policy: {level}": "policy: {level}"
243+
"policy: {level}": "policy: {level}",
244+
"Could not load the settings.": "Could not load the settings.",
245+
"Settings saved.": "Settings saved.",
246+
"Could not save the settings.": "Could not save the settings.",
247+
"History and cache": "History and cache",
248+
"Keep history for (days, {min}-{max})": "Keep history for (days, {min}-{max})",
249+
"Cached versions per app (0 turns the cache off, at most {max})": "Cached versions per app (0 turns the cache off, at most {max})",
250+
"Sources and mirrors": "Sources and mirrors",
251+
"Leave a field empty to use the default shown under it.": "Leave a field empty to use the default shown under it.",
252+
"App Store API address (a store mirror)": "App Store API address (a store mirror)",
253+
"Default: {url}": "Default: {url}",
254+
"GitHub API address (GitHub Enterprise, https only)": "GitHub API address (GitHub Enterprise, https only)",
255+
"GitHub web address (GitHub Enterprise, https only)": "GitHub web address (GitHub Enterprise, https only)",
256+
"Advisory feed address (an internal mirror)": "Advisory feed address (an internal mirror)"
244257
}
245258
}

‎l10n/nl.js‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,20 @@ OC.L10N.register(
262262
"Automatic updates are off, so nothing below runs until they are enabled.": "Automatische updates staan uit, dus niets hieronder draait totdat ze zijn ingeschakeld.",
263263
"Runs in the window {window} ({timeZone}).": "Draait in het venster {window} ({timeZone}).",
264264
"No app has an automatic update policy.": "Geen enkele app heeft een beleid voor automatische updates.",
265-
"policy: {level}": "beleid: {level}"
265+
"policy: {level}": "beleid: {level}",
266+
"Could not load the settings.": "De instellingen konden niet worden geladen.",
267+
"Settings saved.": "Instellingen opgeslagen.",
268+
"Could not save the settings.": "De instellingen konden niet worden opgeslagen.",
269+
"History and cache": "Geschiedenis en cache",
270+
"Keep history for (days, {min}-{max})": "Geschiedenis bewaren (dagen, {min}-{max})",
271+
"Cached versions per app (0 turns the cache off, at most {max})": "Gecachte versies per app (0 zet de cache uit, hoogstens {max})",
272+
"Sources and mirrors": "Bronnen en mirrors",
273+
"Leave a field empty to use the default shown under it.": "Laat een veld leeg om de standaardwaarde eronder te gebruiken.",
274+
"App Store API address (a store mirror)": "App Store API-adres (een mirror van de store)",
275+
"Default: {url}": "Standaard: {url}",
276+
"GitHub API address (GitHub Enterprise, https only)": "GitHub API-adres (GitHub Enterprise, alleen https)",
277+
"GitHub web address (GitHub Enterprise, https only)": "GitHub-webadres (GitHub Enterprise, alleen https)",
278+
"Advisory feed address (an internal mirror)": "Adres van de adviezenfeed (een interne mirror)"
266279
},
267280
"nplurals=2; plural=(n != 1);"
268281
)

‎l10n/nl.json‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,20 @@
267267
"Automatic updates are off, so nothing below runs until they are enabled.": "Automatische updates staan uit, dus niets hieronder draait totdat ze zijn ingeschakeld.",
268268
"Runs in the window {window} ({timeZone}).": "Draait in het venster {window} ({timeZone}).",
269269
"No app has an automatic update policy.": "Geen enkele app heeft een beleid voor automatische updates.",
270-
"policy: {level}": "beleid: {level}"
270+
"policy: {level}": "beleid: {level}",
271+
"Could not load the settings.": "De instellingen konden niet worden geladen.",
272+
"Settings saved.": "Instellingen opgeslagen.",
273+
"Could not save the settings.": "De instellingen konden niet worden opgeslagen.",
274+
"History and cache": "Geschiedenis en cache",
275+
"Keep history for (days, {min}-{max})": "Geschiedenis bewaren (dagen, {min}-{max})",
276+
"Cached versions per app (0 turns the cache off, at most {max})": "Gecachte versies per app (0 zet de cache uit, hoogstens {max})",
277+
"Sources and mirrors": "Bronnen en mirrors",
278+
"Leave a field empty to use the default shown under it.": "Laat een veld leeg om de standaardwaarde eronder te gebruiken.",
279+
"App Store API address (a store mirror)": "App Store API-adres (een mirror van de store)",
280+
"Default: {url}": "Standaard: {url}",
281+
"GitHub API address (GitHub Enterprise, https only)": "GitHub API-adres (GitHub Enterprise, alleen https)",
282+
"GitHub web address (GitHub Enterprise, https only)": "GitHub-webadres (GitHub Enterprise, alleen https)",
283+
"Advisory feed address (an internal mirror)": "Adres van de adviezenfeed (een interne mirror)"
271284
},
272285
"pluralForm": "nplurals=2; plural=(n != 1);"
273286
}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* @license EUPL-1.2
6+
* @copyright Copyright (c) 2026, Conduction B.V. <info@conduction.nl>
7+
*
8+
* SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>
9+
* SPDX-License-Identifier: EUPL-1.2
10+
*/
11+
12+
13+
namespace OCA\Versioniq\Controller;
14+
15+
use InvalidArgumentException;
16+
use OCA\Versioniq\Service\Audit\AuditLogger;
17+
use OCA\Versioniq\Service\Settings\InstanceSettings;
18+
use OCP\AppFramework\Http;
19+
use OCP\AppFramework\Http\Attribute\ApiRoute;
20+
use OCP\AppFramework\Http\Attribute\PasswordConfirmationRequired;
21+
use OCP\AppFramework\Http\DataResponse;
22+
use OCP\AppFramework\OCSController;
23+
use OCP\IGroupManager;
24+
use OCP\IRequest;
25+
use OCP\IUserSession;
26+
27+
/**
28+
* Reads and sets the instance settings that used to be occ-only (#438 item
29+
* 7): audit retention, artifact cache size, App Store and GitHub base URLs
30+
* and the advisory feed URL.
31+
*
32+
* Admin-only twice over, like ForgeController: no NoAdminRequired attribute,
33+
* and the explicit isAdmin() guard the unit tests pin. A change is audited,
34+
* because pointing the App Store or GitHub at another host changes where
35+
* installs come from.
36+
*
37+
* @psalm-suppress UnusedClass
38+
*/
39+
class SettingsController extends OCSController {
40+
public function __construct(
41+
string $appName,
42+
IRequest $request,
43+
private InstanceSettings $settings,
44+
private AuditLogger $auditLogger,
45+
private IGroupManager $groupManager,
46+
private IUserSession $userSession,
47+
) {
48+
parent::__construct($appName, $request);
49+
}
50+
51+
/**
52+
* Returns the instance settings with their defaults
53+
*
54+
* @return DataResponse<Http::STATUS_OK, array<string, int|string>, array{}>|DataResponse<Http::STATUS_FORBIDDEN, array{message: string}, array{}>
55+
*
56+
* 200: The settings, each override ('' when unset) next to its default
57+
* 403: Caller is not an administrator
58+
*
59+
* @spec openspec/specs/audit-trail/spec.md
60+
* @spec openspec/specs/external-sources/spec.md
61+
*/
62+
#[ApiRoute(verb: 'GET', url: '/api/instance-settings')]
63+
public function instanceSettings(): DataResponse {
64+
if (!$this->isAdmin()) {
65+
return new DataResponse(['message' => 'Forbidden'], Http::STATUS_FORBIDDEN);
66+
}
67+
68+
return new DataResponse($this->settings->read());
69+
}
70+
71+
/**
72+
* Updates the instance settings (password-confirmed); an omitted field is left alone
73+
*
74+
* @param string|null $auditRetentionDays Days of history to keep, 30 to 3650
75+
* @param string|null $artifactCacheKeep Archives kept per app, 0 to 20 (0 turns the cache off)
76+
* @param string|null $appStoreApiBase App Store API base URL; blank uses the public store
77+
* @param string|null $githubApiBase GitHub API base URL (https); blank uses api.github.com
78+
* @param string|null $githubWebBase GitHub web base URL (https); blank uses github.com
79+
* @param string|null $advisoryFeedUrl Advisory feed URL; blank uses the published Nextcloud feed
80+
*
81+
* @return DataResponse<Http::STATUS_OK, array<string, int|string>, array{}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_FORBIDDEN, array{message: string}, array{}>
82+
*
83+
* 200: The settings after the change
84+
* 400: A value is out of range or not an acceptable URL; nothing was changed
85+
* 403: Caller is not an administrator
86+
*
87+
* @spec openspec/specs/audit-trail/spec.md
88+
* @spec openspec/specs/external-sources/spec.md
89+
*/
90+
#[PasswordConfirmationRequired(strict: false)]
91+
#[ApiRoute(verb: 'PUT', url: '/api/instance-settings')]
92+
public function updateInstanceSettings(
93+
?string $auditRetentionDays = null,
94+
?string $artifactCacheKeep = null,
95+
?string $appStoreApiBase = null,
96+
?string $githubApiBase = null,
97+
?string $githubWebBase = null,
98+
?string $advisoryFeedUrl = null,
99+
): DataResponse {
100+
if (!$this->isAdmin()) {
101+
return new DataResponse(['message' => 'Forbidden'], Http::STATUS_FORBIDDEN);
102+
}
103+
104+
$fields = array_filter([
105+
'auditRetentionDays' => $auditRetentionDays,
106+
'artifactCacheKeep' => $artifactCacheKeep,
107+
'appStoreApiBase' => $appStoreApiBase,
108+
'githubApiBase' => $githubApiBase,
109+
'githubWebBase' => $githubWebBase,
110+
'advisoryFeedUrl' => $advisoryFeedUrl,
111+
], static fn (?string $value): bool => $value !== null);
112+
113+
try {
114+
$applied = $this->settings->update($fields);
115+
} catch (InvalidArgumentException $error) {
116+
return new DataResponse(['message' => $error->getMessage()], Http::STATUS_BAD_REQUEST);
117+
}
118+
119+
$after = $this->settings->read();
120+
if ($applied !== []) {
121+
$this->auditLogger->record(
122+
$this->userSession->getUser()?->getUID() ?? '',
123+
'versioniq',
124+
AuditLogger::OPERATION_SETTINGS,
125+
null,
126+
null,
127+
null,
128+
AuditLogger::STATUS_SUCCESS,
129+
'Settings changed: ' . implode(', ', array_map(
130+
static fn (string $field): string => $field . '=' . (string)$after[$field],
131+
$applied,
132+
)),
133+
);
134+
}
135+
136+
return new DataResponse($after);
137+
}
138+
139+
private function isAdmin(): bool {
140+
$user = $this->userSession->getUser();
141+
if ($user === null) {
142+
return false;
143+
}
144+
145+
return $this->groupManager->isAdmin($user->getUID());
146+
}
147+
}

‎lib/Service/Advisory/NextcloudAdvisoryFeed.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ class NextcloudAdvisoryFeed {
4141
* The advisory feed. Overridable via `advisory.feed_base` app config so an
4242
* e2e run can point at a fixture, mirroring how `appstore.api_base` works.
4343
*/
44-
private const DEFAULT_FEED_URL = 'https://api.github.com/repos/nextcloud/security-advisories/security-advisories';
44+
public const DEFAULT_FEED_URL = 'https://api.github.com/repos/nextcloud/security-advisories/security-advisories';
4545

4646
/**
4747
* Pages to follow before giving up.

‎lib/Service/Audit/AuditLogger.php‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ class AuditLogger {
3535
public const OPERATION_PIN = 'pin';
3636
public const OPERATION_UNPIN = 'unpin';
3737
public const OPERATION_PIN_DRIFT = 'pin_drift';
38+
/** An instance setting changed from the Settings tab (#438). */
39+
public const OPERATION_SETTINGS = 'settings';
3840

3941
private const OPERATION_PATTERN = '/^[a-z_]{1,32}$/';
4042
private const MESSAGE_MAX_LENGTH = 4000;

‎lib/Service/Cache/ArtifactCache.php‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@
4646
*/
4747
class ArtifactCache {
4848
private const FOLDER_PREFIX = 'artifact-cache-';
49-
private const CONFIG_KEEP = 'artifact_cache_keep';
50-
private const DEFAULT_KEEP = 3;
49+
public const CONFIG_KEEP = 'artifact_cache_keep';
50+
public const DEFAULT_KEEP = 3;
5151
private const ARCHIVE_SUFFIX = '.tar.gz';
5252
private const META_SUFFIX = '.meta.json';
5353

0 commit comments

Comments
 (0)