// Copyright (C) 2013 The Android Open Source Project // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package com.google.gerrit.server.project; import com.google.common.base.CharMatcher; import com.google.common.base.Joiner; import com.google.common.base.Strings; import com.google.gerrit.extensions.api.projects.ConfigInfo; import com.google.gerrit.extensions.api.projects.ConfigInput; import com.google.gerrit.extensions.api.projects.ConfigValue; import com.google.gerrit.extensions.api.projects.ProjectConfigEntryType; import com.google.gerrit.extensions.registration.DynamicMap; import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.BadRequestException; import com.google.gerrit.extensions.restapi.ResourceConflictException; import com.google.gerrit.extensions.restapi.ResourceNotFoundException; import com.google.gerrit.extensions.restapi.RestApiException; import com.google.gerrit.extensions.restapi.RestModifyView; import com.google.gerrit.extensions.restapi.RestView; import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.server.CurrentUser; import com.google.gerrit.server.EnableSignedPush; import com.google.gerrit.server.config.AllProjectsName; import com.google.gerrit.server.config.PluginConfig; import com.google.gerrit.server.config.PluginConfigFactory; import com.google.gerrit.server.config.ProjectConfigEntry; import com.google.gerrit.server.extensions.webui.UiActions; import com.google.gerrit.server.git.MetaDataUpdate; import com.google.gerrit.server.git.ProjectConfig; import com.google.gerrit.server.git.TransferConfig; import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.Singleton; import java.io.IOException; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Map.Entry; import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.errors.RepositoryNotFoundException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @Singleton public class PutConfig implements RestModifyView<ProjectResource, ConfigInput> { private static final Logger log = LoggerFactory.getLogger(PutConfig.class); private final boolean serverEnableSignedPush; private final Provider<MetaDataUpdate.User> metaDataUpdateFactory; private final ProjectCache projectCache; private final ProjectState.Factory projectStateFactory; private final TransferConfig config; private final DynamicMap<ProjectConfigEntry> pluginConfigEntries; private final PluginConfigFactory cfgFactory; private final AllProjectsName allProjects; private final UiActions uiActions; private final DynamicMap<RestView<ProjectResource>> views; private final Provider<CurrentUser> user; @Inject PutConfig( @EnableSignedPush boolean serverEnableSignedPush, Provider<MetaDataUpdate.User> metaDataUpdateFactory, ProjectCache projectCache, ProjectState.Factory projectStateFactory, TransferConfig config, DynamicMap<ProjectConfigEntry> pluginConfigEntries, PluginConfigFactory cfgFactory, AllProjectsName allProjects, UiActions uiActions, DynamicMap<RestView<ProjectResource>> views, Provider<CurrentUser> user) { this.serverEnableSignedPush = serverEnableSignedPush; this.metaDataUpdateFactory = metaDataUpdateFactory; this.projectCache = projectCache; this.projectStateFactory = projectStateFactory; this.config = config; this.pluginConfigEntries = pluginConfigEntries; this.cfgFactory = cfgFactory; this.allProjects = allProjects; this.uiActions = uiActions; this.views = views; this.user = user; } @Override public ConfigInfo apply(ProjectResource rsrc, ConfigInput input) throws RestApiException { if (!rsrc.getControl().isOwner()) { throw new AuthException("restricted to project owner"); } return apply(rsrc.getControl(), input); } public ConfigInfo apply(ProjectControl ctrl, ConfigInput input) throws ResourceNotFoundException, BadRequestException, ResourceConflictException { Project.NameKey projectName = ctrl.getProject().getNameKey(); if (input == null) { throw new BadRequestException("config is required"); } try (MetaDataUpdate md = metaDataUpdateFactory.get().create(projectName)) { ProjectConfig projectConfig = ProjectConfig.read(md); Project p = projectConfig.getProject(); p.setDescription(Strings.emptyToNull(input.description)); if (input.useContributorAgreements != null) { p.setUseContributorAgreements(input.useContributorAgreements); } if (input.useContentMerge != null) { p.setUseContentMerge(input.useContentMerge); } if (input.useSignedOffBy != null) { p.setUseSignedOffBy(input.useSignedOffBy); } if (input.createNewChangeForAllNotInTarget != null) { p.setCreateNewChangeForAllNotInTarget(input.createNewChangeForAllNotInTarget); } if (input.requireChangeId != null) { p.setRequireChangeID(input.requireChangeId); } if (serverEnableSignedPush) { if (input.enableSignedPush != null) { p.setEnableSignedPush(input.enableSignedPush); } if (input.requireSignedPush != null) { p.setRequireSignedPush(input.requireSignedPush); } } if (input.rejectImplicitMerges != null) { p.setRejectImplicitMerges(input.rejectImplicitMerges); } if (input.maxObjectSizeLimit != null) { p.setMaxObjectSizeLimit(input.maxObjectSizeLimit); } if (input.submitType != null) { p.setSubmitType(input.submitType); } if (input.state != null) { p.setState(input.state); } if (input.enableReviewerByEmail != null) { p.setEnableReviewerByEmail(input.enableReviewerByEmail); } if (input.pluginConfigValues != null) { setPluginConfigValues(ctrl.getProjectState(), projectConfig, input.pluginConfigValues); } md.setMessage("Modified project settings\n"); try { projectConfig.commit(md); projectCache.evict(projectConfig.getProject()); md.getRepository().setGitwebDescription(p.getDescription()); } catch (IOException e) { if (e.getCause() instanceof ConfigInvalidException) { throw new ResourceConflictException( "Cannot update " + projectName + ": " + e.getCause().getMessage()); } log.warn(String.format("Failed to update config of project %s.", projectName), e); throw new ResourceConflictException("Cannot update " + projectName); } ProjectState state = projectStateFactory.create(projectConfig); return new ConfigInfoImpl( serverEnableSignedPush, state.controlFor(user.get()), config, pluginConfigEntries, cfgFactory, allProjects, uiActions, views); } catch (RepositoryNotFoundException notFound) { throw new ResourceNotFoundException(projectName.get()); } catch (ConfigInvalidException err) { throw new ResourceConflictException("Cannot read project " + projectName, err); } catch (IOException err) { throw new ResourceConflictException("Cannot update project " + projectName, err); } } private void setPluginConfigValues( ProjectState projectState, ProjectConfig projectConfig, Map<String, Map<String, ConfigValue>> pluginConfigValues) throws BadRequestException { for (Entry<String, Map<String, ConfigValue>> e : pluginConfigValues.entrySet()) { String pluginName = e.getKey(); PluginConfig cfg = projectConfig.getPluginConfig(pluginName); for (Entry<String, ConfigValue> v : e.getValue().entrySet()) { ProjectConfigEntry projectConfigEntry = pluginConfigEntries.get(pluginName, v.getKey()); if (projectConfigEntry != null) { if (!isValidParameterName(v.getKey())) { log.warn( String.format( "Parameter name '%s' must match '^[a-zA-Z0-9]+[a-zA-Z0-9-]*$'", v.getKey())); continue; } String oldValue = cfg.getString(v.getKey()); String value = v.getValue().value; if (projectConfigEntry.getType() == ProjectConfigEntryType.ARRAY) { List<String> l = Arrays.asList(cfg.getStringList(v.getKey())); oldValue = Joiner.on("\n").join(l); value = Joiner.on("\n").join(v.getValue().values); } if (Strings.emptyToNull(value) != null) { if (!value.equals(oldValue)) { validateProjectConfigEntryIsEditable( projectConfigEntry, projectState, v.getKey(), pluginName); v.setValue(projectConfigEntry.preUpdate(v.getValue())); value = v.getValue().value; try { switch (projectConfigEntry.getType()) { case BOOLEAN: boolean newBooleanValue = Boolean.parseBoolean(value); cfg.setBoolean(v.getKey(), newBooleanValue); break; case INT: int newIntValue = Integer.parseInt(value); cfg.setInt(v.getKey(), newIntValue); break; case LONG: long newLongValue = Long.parseLong(value); cfg.setLong(v.getKey(), newLongValue); break; case LIST: if (!projectConfigEntry.getPermittedValues().contains(value)) { throw new BadRequestException( String.format( "The value '%s' is not permitted for parameter '%s' of plugin '" + pluginName + "'", value, v.getKey())); } //$FALL-THROUGH$ case STRING: cfg.setString(v.getKey(), value); break; case ARRAY: cfg.setStringList(v.getKey(), v.getValue().values); break; default: log.warn( String.format( "The type '%s' of parameter '%s' is not supported.", projectConfigEntry.getType().name(), v.getKey())); } } catch (NumberFormatException ex) { throw new BadRequestException( String.format( "The value '%s' of config parameter '%s' of plugin '%s' is invalid: %s", v.getValue(), v.getKey(), pluginName, ex.getMessage())); } } } else { if (oldValue != null) { validateProjectConfigEntryIsEditable( projectConfigEntry, projectState, v.getKey(), pluginName); cfg.unset(v.getKey()); } } } else { throw new BadRequestException( String.format( "The config parameter '%s' of plugin '%s' does not exist.", v.getKey(), pluginName)); } } } } private static void validateProjectConfigEntryIsEditable( ProjectConfigEntry projectConfigEntry, ProjectState projectState, String parameterName, String pluginName) throws BadRequestException { if (!projectConfigEntry.isEditable(projectState)) { throw new BadRequestException( String.format( "Not allowed to set parameter '%s' of plugin '%s' on project '%s'.", parameterName, pluginName, projectState.getProject().getName())); } } private static boolean isValidParameterName(String name) { return CharMatcher.javaLetterOrDigit().or(CharMatcher.is('-')).matchesAllOf(name) && !name.startsWith("-"); } }