Line data Source code
1 : // Copyright (C) 2013 The Android Open Source Project
2 : //
3 : // Licensed under the Apache License, Version 2.0 (the "License");
4 : // you may not use this file except in compliance with the License.
5 : // You may obtain a copy of the License at
6 : //
7 : // http://www.apache.org/licenses/LICENSE-2.0
8 : //
9 : // Unless required by applicable law or agreed to in writing, software
10 : // distributed under the License is distributed on an "AS IS" BASIS,
11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 : // See the License for the specific language governing permissions and
13 : // limitations under the License.
14 :
15 : package com.google.gerrit.server.restapi.project;
16 :
17 : import static com.google.gerrit.server.project.ProjectConfig.COMMENTLINK;
18 : import static com.google.gerrit.server.project.ProjectConfig.KEY_ENABLED;
19 : import static com.google.gerrit.server.project.ProjectConfig.KEY_LINK;
20 : import static com.google.gerrit.server.project.ProjectConfig.KEY_MATCH;
21 : import static com.google.gerrit.server.project.ProjectConfig.KEY_PREFIX;
22 : import static com.google.gerrit.server.project.ProjectConfig.KEY_SUFFIX;
23 : import static com.google.gerrit.server.project.ProjectConfig.KEY_TEXT;
24 :
25 : import com.google.common.base.Joiner;
26 : import com.google.common.base.Strings;
27 : import com.google.common.flogger.FluentLogger;
28 : import com.google.gerrit.entities.BooleanProjectConfig;
29 : import com.google.gerrit.entities.Project;
30 : import com.google.gerrit.extensions.api.projects.CommentLinkInput;
31 : import com.google.gerrit.extensions.api.projects.ConfigInfo;
32 : import com.google.gerrit.extensions.api.projects.ConfigInput;
33 : import com.google.gerrit.extensions.api.projects.ConfigValue;
34 : import com.google.gerrit.extensions.api.projects.ProjectConfigEntryType;
35 : import com.google.gerrit.extensions.client.InheritableBoolean;
36 : import com.google.gerrit.extensions.registration.DynamicMap;
37 : import com.google.gerrit.extensions.restapi.BadRequestException;
38 : import com.google.gerrit.extensions.restapi.ResourceConflictException;
39 : import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
40 : import com.google.gerrit.extensions.restapi.Response;
41 : import com.google.gerrit.extensions.restapi.RestApiException;
42 : import com.google.gerrit.extensions.restapi.RestModifyView;
43 : import com.google.gerrit.extensions.restapi.RestView;
44 : import com.google.gerrit.server.CurrentUser;
45 : import com.google.gerrit.server.EnableSignedPush;
46 : import com.google.gerrit.server.config.AllProjectsName;
47 : import com.google.gerrit.server.config.PluginConfigFactory;
48 : import com.google.gerrit.server.config.ProjectConfigEntry;
49 : import com.google.gerrit.server.extensions.webui.UiActions;
50 : import com.google.gerrit.server.git.meta.MetaDataUpdate;
51 : import com.google.gerrit.server.permissions.PermissionBackend;
52 : import com.google.gerrit.server.permissions.PermissionBackendException;
53 : import com.google.gerrit.server.permissions.ProjectPermission;
54 : import com.google.gerrit.server.project.BooleanProjectConfigTransformations;
55 : import com.google.gerrit.server.project.ProjectCache;
56 : import com.google.gerrit.server.project.ProjectConfig;
57 : import com.google.gerrit.server.project.ProjectResource;
58 : import com.google.gerrit.server.project.ProjectState;
59 : import com.google.inject.Inject;
60 : import com.google.inject.Provider;
61 : import com.google.inject.Singleton;
62 : import java.io.IOException;
63 : import java.util.Arrays;
64 : import java.util.List;
65 : import java.util.Map;
66 : import java.util.regex.Pattern;
67 : import org.eclipse.jgit.errors.ConfigInvalidException;
68 : import org.eclipse.jgit.errors.RepositoryNotFoundException;
69 : import org.eclipse.jgit.lib.Config;
70 :
71 : @Singleton
72 : public class PutConfig implements RestModifyView<ProjectResource, ConfigInput> {
73 146 : private static final FluentLogger logger = FluentLogger.forEnclosingClass();
74 :
75 146 : private static final Pattern PARAMETER_NAME_PATTERN =
76 146 : Pattern.compile("^[a-zA-Z0-9]+[a-zA-Z0-9-]*$");
77 :
78 : private final boolean serverEnableSignedPush;
79 : private final Provider<MetaDataUpdate.User> metaDataUpdateFactory;
80 : private final ProjectCache projectCache;
81 : private final ProjectState.Factory projectStateFactory;
82 : private final DynamicMap<ProjectConfigEntry> pluginConfigEntries;
83 : private final PluginConfigFactory cfgFactory;
84 : private final AllProjectsName allProjects;
85 : private final UiActions uiActions;
86 : private final DynamicMap<RestView<ProjectResource>> views;
87 : private final Provider<CurrentUser> user;
88 : private final PermissionBackend permissionBackend;
89 : private final ProjectConfig.Factory projectConfigFactory;
90 :
91 : @Inject
92 : PutConfig(
93 : @EnableSignedPush boolean serverEnableSignedPush,
94 : Provider<MetaDataUpdate.User> metaDataUpdateFactory,
95 : ProjectCache projectCache,
96 : ProjectState.Factory projectStateFactory,
97 : DynamicMap<ProjectConfigEntry> pluginConfigEntries,
98 : PluginConfigFactory cfgFactory,
99 : AllProjectsName allProjects,
100 : UiActions uiActions,
101 : DynamicMap<RestView<ProjectResource>> views,
102 : Provider<CurrentUser> user,
103 : PermissionBackend permissionBackend,
104 146 : ProjectConfig.Factory projectConfigFactory) {
105 146 : this.serverEnableSignedPush = serverEnableSignedPush;
106 146 : this.metaDataUpdateFactory = metaDataUpdateFactory;
107 146 : this.projectCache = projectCache;
108 146 : this.projectStateFactory = projectStateFactory;
109 146 : this.pluginConfigEntries = pluginConfigEntries;
110 146 : this.cfgFactory = cfgFactory;
111 146 : this.allProjects = allProjects;
112 146 : this.uiActions = uiActions;
113 146 : this.views = views;
114 146 : this.user = user;
115 146 : this.permissionBackend = permissionBackend;
116 146 : this.projectConfigFactory = projectConfigFactory;
117 146 : }
118 :
119 : @Override
120 : public Response<ConfigInfo> apply(ProjectResource rsrc, ConfigInput input)
121 : throws RestApiException, PermissionBackendException {
122 22 : permissionBackend
123 22 : .currentUser()
124 22 : .project(rsrc.getNameKey())
125 22 : .check(ProjectPermission.WRITE_CONFIG);
126 22 : return Response.ok(apply(rsrc.getProjectState(), input));
127 : }
128 :
129 : public ConfigInfo apply(ProjectState projectState, ConfigInput input)
130 : throws ResourceNotFoundException, BadRequestException, ResourceConflictException {
131 22 : Project.NameKey projectName = projectState.getNameKey();
132 22 : if (input == null) {
133 0 : throw new BadRequestException("config is required");
134 : }
135 :
136 22 : try (MetaDataUpdate md = metaDataUpdateFactory.get().create(projectName)) {
137 22 : ProjectConfig projectConfig = projectConfigFactory.read(md);
138 22 : projectConfig.updateProject(
139 : p -> {
140 22 : p.setDescription(Strings.emptyToNull(input.description));
141 22 : for (BooleanProjectConfig cfg : BooleanProjectConfig.values()) {
142 22 : InheritableBoolean val = BooleanProjectConfigTransformations.get(cfg, input);
143 22 : if (val != null) {
144 16 : p.setBooleanConfig(cfg, val);
145 : }
146 : }
147 22 : if (input.maxObjectSizeLimit != null) {
148 1 : p.setMaxObjectSizeLimit(input.maxObjectSizeLimit);
149 : }
150 22 : if (input.submitType != null) {
151 2 : p.setSubmitType(input.submitType);
152 : }
153 22 : if (input.state != null) {
154 4 : p.setState(input.state);
155 : }
156 22 : });
157 :
158 22 : if (input.pluginConfigValues != null) {
159 1 : setPluginConfigValues(projectState, projectConfig, input.pluginConfigValues);
160 : }
161 :
162 22 : if (input.commentLinks != null) {
163 1 : updateCommentLinks(projectConfig, input.commentLinks);
164 : }
165 :
166 22 : md.setMessage("Modified project settings\n");
167 : try {
168 22 : projectConfig.commit(md);
169 22 : projectCache.evictAndReindex(projectConfig.getProject());
170 22 : md.getRepository().setGitwebDescription(projectConfig.getProject().getDescription());
171 1 : } catch (IOException e) {
172 1 : if (e.getCause() instanceof ConfigInvalidException) {
173 1 : throw new ResourceConflictException(
174 1 : "Cannot update " + projectName + ": " + e.getCause().getMessage());
175 : }
176 0 : logger.atWarning().withCause(e).log("Failed to update config of project %s.", projectName);
177 0 : throw new ResourceConflictException("Cannot update " + projectName);
178 22 : }
179 :
180 22 : ProjectState state = projectStateFactory.create(projectConfigFactory.read(md).getCacheable());
181 22 : return ConfigInfoCreator.constructInfo(
182 : serverEnableSignedPush,
183 : state,
184 22 : user.get(),
185 : pluginConfigEntries,
186 : cfgFactory,
187 : allProjects,
188 : uiActions,
189 : views);
190 0 : } catch (RepositoryNotFoundException notFound) {
191 0 : throw new ResourceNotFoundException(projectName.get(), notFound);
192 0 : } catch (ConfigInvalidException err) {
193 0 : throw new ResourceConflictException("Cannot read project " + projectName, err);
194 0 : } catch (IOException err) {
195 0 : throw new ResourceConflictException("Cannot update project " + projectName, err);
196 : }
197 : }
198 :
199 : private void setPluginConfigValues(
200 : ProjectState projectState,
201 : ProjectConfig projectConfig,
202 : Map<String, Map<String, ConfigValue>> pluginConfigValues)
203 : throws BadRequestException {
204 1 : for (Map.Entry<String, Map<String, ConfigValue>> e : pluginConfigValues.entrySet()) {
205 1 : String pluginName = e.getKey();
206 1 : for (Map.Entry<String, ConfigValue> v : e.getValue().entrySet()) {
207 1 : ProjectConfigEntry projectConfigEntry = pluginConfigEntries.get(pluginName, v.getKey());
208 1 : if (projectConfigEntry != null) {
209 1 : if (!PARAMETER_NAME_PATTERN.matcher(v.getKey()).matches()) {
210 : // TODO check why we have this restriction
211 0 : logger.atWarning().log(
212 : "Parameter name '%s' must match '%s'",
213 0 : v.getKey(), PARAMETER_NAME_PATTERN.pattern());
214 0 : continue;
215 : }
216 1 : String oldValue = projectConfig.getPluginConfig(pluginName).getString(v.getKey());
217 1 : String value = v.getValue().value;
218 1 : if (projectConfigEntry.getType() == ProjectConfigEntryType.ARRAY) {
219 0 : List<String> l =
220 0 : Arrays.asList(projectConfig.getPluginConfig(pluginName).getStringList(v.getKey()));
221 0 : oldValue = Joiner.on("\n").join(l);
222 0 : value = Joiner.on("\n").join(v.getValue().values);
223 : }
224 1 : if (Strings.emptyToNull(value) != null) {
225 1 : if (!value.equals(oldValue)) {
226 1 : validateProjectConfigEntryIsEditable(
227 1 : projectConfigEntry, projectState, v.getKey(), pluginName);
228 1 : v.setValue(projectConfigEntry.preUpdate(v.getValue()));
229 1 : value = v.getValue().value;
230 : try {
231 1 : switch (projectConfigEntry.getType()) {
232 : case BOOLEAN:
233 1 : boolean newBooleanValue = Boolean.parseBoolean(value);
234 1 : projectConfig.updatePluginConfig(
235 1 : pluginName, cfg -> cfg.setBoolean(v.getKey(), newBooleanValue));
236 1 : break;
237 : case INT:
238 0 : int newIntValue = Integer.parseInt(value);
239 0 : projectConfig.updatePluginConfig(
240 0 : pluginName, cfg -> cfg.setInt(v.getKey(), newIntValue));
241 0 : break;
242 : case LONG:
243 0 : long newLongValue = Long.parseLong(value);
244 0 : projectConfig.updatePluginConfig(
245 0 : pluginName, cfg -> cfg.setLong(v.getKey(), newLongValue));
246 0 : break;
247 : case LIST:
248 0 : if (!projectConfigEntry.getPermittedValues().contains(value)) {
249 0 : throw new BadRequestException(
250 0 : String.format(
251 : "The value '%s' is not permitted for parameter '%s' of plugin '"
252 : + pluginName
253 : + "'",
254 : value,
255 0 : v.getKey()));
256 : }
257 : // $FALL-THROUGH$
258 : case STRING:
259 0 : String valueToSet = value;
260 0 : projectConfig.updatePluginConfig(
261 0 : pluginName, cfg -> cfg.setString(v.getKey(), valueToSet));
262 0 : break;
263 : case ARRAY:
264 0 : projectConfig.updatePluginConfig(
265 0 : pluginName, cfg -> cfg.setStringList(v.getKey(), v.getValue().values));
266 0 : break;
267 : default:
268 0 : logger.atWarning().log(
269 : "The type '%s' of parameter '%s' is not supported.",
270 0 : projectConfigEntry.getType().name(), v.getKey());
271 : }
272 0 : } catch (NumberFormatException ex) {
273 0 : throw new BadRequestException(
274 0 : String.format(
275 : "The value '%s' of config parameter '%s' of plugin '%s' is invalid: %s",
276 0 : v.getValue(), v.getKey(), pluginName, ex.getMessage()));
277 1 : }
278 : }
279 : } else {
280 0 : if (oldValue != null) {
281 0 : validateProjectConfigEntryIsEditable(
282 0 : projectConfigEntry, projectState, v.getKey(), pluginName);
283 0 : projectConfig.updatePluginConfig(pluginName, cfg -> cfg.unset(v.getKey()));
284 : }
285 : }
286 1 : } else {
287 0 : throw new BadRequestException(
288 0 : String.format(
289 : "The config parameter '%s' of plugin '%s' does not exist.",
290 0 : v.getKey(), pluginName));
291 : }
292 1 : }
293 1 : }
294 1 : }
295 :
296 : private void updateCommentLinks(
297 : ProjectConfig projectConfig, Map<String, CommentLinkInput> input) {
298 1 : for (Map.Entry<String, CommentLinkInput> e : input.entrySet()) {
299 1 : String name = e.getKey();
300 1 : CommentLinkInput value = e.getValue();
301 1 : if (value != null) {
302 : // Add or update the commentlink section
303 1 : Config cfg = new Config();
304 1 : cfg.setString(COMMENTLINK, name, KEY_MATCH, value.match);
305 1 : cfg.setString(COMMENTLINK, name, KEY_LINK, value.link);
306 1 : if (!Strings.isNullOrEmpty(value.prefix)) {
307 1 : cfg.setString(COMMENTLINK, name, KEY_PREFIX, value.prefix);
308 : }
309 1 : if (!Strings.isNullOrEmpty(value.suffix)) {
310 1 : cfg.setString(COMMENTLINK, name, KEY_SUFFIX, value.suffix);
311 : }
312 1 : if (!Strings.isNullOrEmpty(value.text)) {
313 1 : cfg.setString(COMMENTLINK, name, KEY_TEXT, value.text);
314 : }
315 1 : cfg.setBoolean(COMMENTLINK, name, KEY_ENABLED, value.enabled == null || value.enabled);
316 1 : projectConfig.addCommentLinkSection(ProjectConfig.buildCommentLink(cfg, name));
317 1 : } else {
318 : // Delete the commentlink section
319 1 : projectConfig.removeCommentLinkSection(name);
320 : }
321 1 : }
322 1 : }
323 :
324 : private static void validateProjectConfigEntryIsEditable(
325 : ProjectConfigEntry projectConfigEntry,
326 : ProjectState projectState,
327 : String parameterName,
328 : String pluginName)
329 : throws BadRequestException {
330 1 : if (!projectConfigEntry.isEditable(projectState)) {
331 0 : throw new BadRequestException(
332 0 : String.format(
333 : "Not allowed to set parameter '%s' of plugin '%s' on project '%s'.",
334 0 : parameterName, pluginName, projectState.getName()));
335 : }
336 1 : }
337 : }
|