-
Notifications
You must be signed in to change notification settings - Fork 26
Expand file tree
/
Copy pathExAppPreferenceService.php
More file actions
145 lines (133 loc) · 5.26 KB
/
Copy pathExAppPreferenceService.php
File metadata and controls
145 lines (133 loc) · 5.26 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\AppAPI\Service;
use OCA\AppAPI\Db\ExAppPreference;
use OCP\Config\IUserConfig;
use OCP\IDBConnection;
use Psr\Log\LoggerInterface;
/**
* App per-user preferences backed by the server's standard `IUserConfig` storage (`oc_preferences`).
*
* Replaces AppAPI's former `preferences_ex` table. Values are stored as lazy strings, scoped
* per user; sensitive values are encrypted by the server via the `FLAG_SENSITIVE` flag.
*/
readonly class ExAppPreferenceService {
public function __construct(
private IUserConfig $userConfig,
private IDBConnection $connection,
private LoggerInterface $logger,
) {
}
/**
* Create or update a per-user preference value.
*
* When `$sensitive` is null the current sensitivity is preserved (or defaults to
* non-sensitive for a new key). The returned object always carries the plaintext value.
*/
public function setUserConfigValue(string $userId, string $appId, string $configKey, mixed $configValue, ?int $sensitive = null): ?ExAppPreference {
try {
$value = (string)($configValue ?? '');
$currentSensitive = $this->userConfig->hasKey($userId, $appId, $configKey, null)
&& $this->userConfig->isSensitive($userId, $appId, $configKey, null);
$targetSensitive = $sensitive !== null ? (bool)$sensitive : $currentSensitive;
if ($currentSensitive && !$targetSensitive) {
$this->downgradeToPlain($userId, $appId, $configKey, $value);
} else {
$this->userConfig->setValueString(
$userId, $appId, $configKey, $value,
lazy: true,
flags: $targetSensitive ? IUserConfig::FLAG_SENSITIVE : 0,
);
}
return new ExAppPreference([
'userid' => $userId,
'appid' => $appId,
'configkey' => $configKey,
'configvalue' => $value,
'sensitive' => $targetSensitive ? 1 : 0,
]);
} catch (\Throwable $e) {
$this->logger->error(sprintf('Failed to set user config value for user %s, app %s, config key %s. Error: %s', $userId, $appId, $configKey, $e->getMessage()), ['exception' => $e]);
return null;
}
}
/**
* Return the values of the requested keys that actually exist, in the shape
* `[['configkey' => ..., 'configvalue' => ...], ...]`. Sensitive values are returned decrypted.
*/
public function getUserConfigValues(string $userId, string $appId, array $configKeys): ?array {
try {
$values = [];
// array_unique: a single SQL `IN (...)` used to dedupe; preserve that so duplicate
// request keys don't yield duplicate result rows.
foreach (array_unique(array_map('strval', $configKeys)) as $configKey) {
// `null` lazy matches keys regardless of their lazy flag (e.g. eager values written
// through the server-native provisioning_api user-config endpoint).
if (!$this->userConfig->hasKey($userId, $appId, $configKey, null)) {
continue;
}
try {
$lazy = $this->userConfig->isLazy($userId, $appId, $configKey);
$configValue = $this->userConfig->getValueString($userId, $appId, $configKey, '', lazy: $lazy);
} catch (\Throwable $e) {
$this->logger->warning(sprintf('Failed to read value for user %s, app %s, config key %s', $userId, $appId, $configKey), ['exception' => $e]);
$configValue = '';
}
$values[] = [
'configkey' => $configKey,
'configvalue' => $configValue,
];
}
return $values;
} catch (\Throwable $e) {
$this->logger->warning(sprintf('Failed to get user config values for user %s, app %s', $userId, $appId), ['exception' => $e]);
return [];
}
}
/**
* Delete the requested keys that exist; returns the number deleted, or -1 on error.
*/
public function deleteUserConfigValues(array $configKeys, string $userId, string $appId): int {
try {
$deleted = 0;
foreach ($configKeys as $configKey) {
$configKey = (string)$configKey;
if ($this->userConfig->hasKey($userId, $appId, $configKey, null)) {
$this->userConfig->deleteUserConfig($userId, $appId, $configKey);
$deleted++;
}
}
return $deleted;
} catch (\Throwable $e) {
$this->logger->error(sprintf('Failed to delete user config values for user %s, app %s. Error: %s', $userId, $appId, $e->getMessage()), ['exception' => $e]);
return -1;
}
}
/**
* Turn a currently-sensitive key into a plain value with the new content.
*
* IUserConfig won't unset sensitivity via setValueString() (it is sticky), and updateSensitive()
* refreshes the type cache but not the value cache. So we drop the key and re-create it as a
* plain value, which leaves both storage and the in-request cache correct. The two writes run
* in a transaction so a failure can never leave the key deleted (the legacy path was atomic).
*/
private function downgradeToPlain(string $userId, string $appId, string $configKey, string $value): void {
$this->connection->beginTransaction();
try {
$this->userConfig->deleteUserConfig($userId, $appId, $configKey);
$this->userConfig->setValueString($userId, $appId, $configKey, $value, lazy: true, flags: 0);
$this->connection->commit();
} catch (\Throwable $e) {
try {
$this->connection->rollBack();
} catch (\Throwable) {
// rollBack on an already-aborted transaction is not actionable here.
}
throw $e;
}
}
}