/* * The MIT License * * Copyright 2013 John Borghi. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package hudson.plugins.jobConfigHistory; import java.io.File; import java.io.IOException; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; import javax.servlet.ServletException; import org.apache.commons.lang.StringUtils; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; import hudson.Plugin; import hudson.XmlFile; import hudson.maven.MavenModule; import hudson.model.AbstractProject; import hudson.model.Item; import hudson.model.Job; import hudson.model.Saveable; import hudson.model.TopLevelItem; import hudson.model.Descriptor.FormException; import hudson.util.FormValidation; import jenkins.model.Jenkins; import net.sf.json.JSONObject; /** * Class supporting global configuration settings, along with methods associated * with the plugin itself. * * @author John Borghi * */ public class JobConfigHistory extends Plugin { /** Root directory for storing histories. */ private String historyRootDir; /** Maximum number of configuration history entries to keep. */ private String maxHistoryEntries; /** Maximum number of history entries per site to show. */ private String maxEntriesPerPage; /** Maximum number of days to keep entries. */ private String maxDaysToKeepEntries; private String excludedUsers; /** * Flag to indicate if we should save history when it is a duplication of * the previous saved configuration. */ private boolean skipDuplicateHistory = true; /** * Regular expression pattern for 'system' configuration files to exclude * from saving. */ private String excludePattern; /** Compiled regular expression pattern. */ private transient Pattern excludeRegexpPattern; /** * Flag to indicate if we should save the config history of Maven modules. */ private boolean saveModuleConfiguration = false; /** * Whether build badges should appear when the config of a job has changed * since the last build. Three possible settings: Never, always, only for * users with config permission. */ private String showBuildBadges = "always"; /** our logger. */ private static final Logger LOG = Logger .getLogger(JobConfigHistory.class.getName()); @Override public void start() throws Exception { load(); loadRegexpPatterns(); } @Override public void configure(StaplerRequest req, JSONObject formData) throws IOException, ServletException, FormException { historyRootDir = formData.getString("historyRootDir").trim(); setMaxHistoryEntries(formData.getString("maxHistoryEntries").trim()); setMaxDaysToKeepEntries( formData.getString("maxDaysToKeepEntries").trim()); setMaxEntriesPerPage(formData.getString("maxEntriesPerPage").trim()); skipDuplicateHistory = formData.getBoolean("skipDuplicateHistory"); excludePattern = formData.getString("excludePattern"); saveModuleConfiguration = formData .getBoolean("saveModuleConfiguration"); showBuildBadges = formData.getString("showBuildBadges"); excludedUsers = formData.getString("excludedUsers"); save(); loadRegexpPatterns(); } /** * @return The configured history root directory. */ public String getHistoryRootDir() { return historyRootDir; } /** * @return The default history root directory. */ public String getDefaultRootDir() { return JobConfigHistoryConsts.DEFAULT_HISTORY_DIR; } /** * @return The maximum number of history entries to keep. */ public String getMaxHistoryEntries() { return maxHistoryEntries; } /** * Set the maximum number of history entries per item. * * @param maxEntryInput * The maximum number of history entries to keep */ public void setMaxHistoryEntries(String maxEntryInput) { if (maxEntryInput.isEmpty() || isPositiveInteger(maxEntryInput)) { maxHistoryEntries = maxEntryInput; } } /** * @return The maximum number of history entries to show per site. */ public String getMaxEntriesPerPage() { return maxEntriesPerPage; } /** * Set the maximum number of history entries to show per site. * * @param maxEntryInput * The maximum number of history entries to show per site */ public void setMaxEntriesPerPage(String maxEntryInput) { if (maxEntryInput.isEmpty() || isPositiveInteger(maxEntryInput)) { maxEntriesPerPage = maxEntryInput; } } /** * @return The maximum number of days to keep history entries. */ public String getMaxDaysToKeepEntries() { return maxDaysToKeepEntries; } /** * Set allowed age of history entries. * * @param maxDaysInput * For how long history entries should be kept (in days) */ public void setMaxDaysToKeepEntries(final String maxDaysInput) { if (maxDaysInput.isEmpty() || isPositiveInteger(maxDaysInput)) { maxDaysToKeepEntries = maxDaysInput; } } /** * Checks if a string evaluates to a positive integer number. * * @param numberString * The number in question (as String) * @return Whether the number is a positive integer * */ public boolean isPositiveInteger(String numberString) { try { final int number = Integer.parseInt(numberString); if (number < 0) { throw new NumberFormatException(); } return true; } catch (NumberFormatException e) { LOG.log(Level.WARNING, "No positive integer: {0}", numberString); } return false; } /** * @return True if item group configurations should be saved. * @deprecated since version 2.9 */ @Deprecated public boolean getSaveItemGroupConfiguration() { return true; } /** * @return true if we should skip saving history that duplicates the prior * saved configuration. */ public boolean getSkipDuplicateHistory() { return skipDuplicateHistory; } /** * @return The regular expression for 'system' file names to exclude from * saving. */ public String getExcludePattern() { return excludePattern; } /** * Used by the configuration page. * * @return The default regular expression for 'system' file names to exclude * from saving. */ public String getDefaultExcludePattern() { return JobConfigHistoryConsts.DEFAULT_EXCLUDE; } /** * @return true if we should save 'system' configurations. */ public boolean getSaveModuleConfiguration() { return saveModuleConfiguration; } /** * @return Whether build badges should appear always, never or only for * users with config rights. */ public String getShowBuildBadges() { return showBuildBadges; } /** * Used for testing only. * * @param showBadges * Never, always, userWithConfigPermission or adminUser. */ public void setShowBuildBadges(String showBadges) { showBuildBadges = showBadges; } /** * Whether build badges should appear for the builds of this project for * this user. * * @param project * The project to which the build history belongs. * @return False if the option is set to 'never' or the user doesn't have * the required permissions. */ public boolean showBuildBadges(Job<?, ?> project) { if ("always".equals(showBuildBadges)) { return true; } else if ("userWithConfigPermission".equals(showBuildBadges) && project.hasPermission(Item.CONFIGURE)) { return true; } else if ("adminUser".equals(showBuildBadges) && getJenkins().hasPermission(Jenkins.ADMINISTER)) { return true; } return false; } /** * Used for testing to verify invalid pattern not loaded. * * @return The loaded regexp pattern, or null if pattern was invalid. */ public Pattern getExcludeRegexpPattern() { return excludeRegexpPattern; } /** * Loads regular expression patterns used by this class. */ private void loadRegexpPatterns() { excludeRegexpPattern = loadRegex(excludePattern); } /** * Loads a regular expression pattern for the given string. * * @param patternString * The string representing the regular expression. * @return The {@link Pattern} for the given expression, or null if the * pattern cannot be loaded. */ private Pattern loadRegex(final String patternString) { if (patternString != null) { try { return Pattern.compile(patternString); } catch (PatternSyntaxException e) { return null; } } return null; } /** * Returns the File object representing the configured root history * directory. * * @return The configured root history File object. from the URI. */ public File getConfiguredHistoryRootDir() { File rootDir; final File jenkinsHome = getJenkinsHome(); if (historyRootDir == null || historyRootDir.isEmpty()) { rootDir = new File(jenkinsHome, JobConfigHistoryConsts.DEFAULT_HISTORY_DIR); } else { if (historyRootDir.matches("^(/|\\\\|[a-zA-Z]:).*")) { rootDir = new File(historyRootDir + "/" + JobConfigHistoryConsts.DEFAULT_HISTORY_DIR); } else { rootDir = new File(jenkinsHome, historyRootDir + "/" + JobConfigHistoryConsts.DEFAULT_HISTORY_DIR); } } return rootDir; } /** * @see FileHistoryDao#getConfigFile(java.io.File). * * @param historyDir * The history directory to look under. * @return The configuration file or null if no file is found. */ public File getConfigFile(final File historyDir) { // TODO: refactor away from 'File' return FileHistoryDao.getConfigFile(historyDir); } /** * * @return comma separated list of usernames, whose changes should not get * detected. */ public String getExcludedUsers() { return excludedUsers; } /** * Returns true if configuration for this item should be saved, based on the * plugin settings, the type of item and the configuration file specified. * * <p> * If the item is an instance of {@link AbstractProject} or the * configuration file is stored directly in JENKINS_ROOT, it is considered * for saving. * * If the plugin is configured to skip saving duplicated history, we also * evaluate if this configuration duplicates the previous saved history (if * such history exists). * * @param item * The item whose configuration is under consideration. * @param xmlFile * The configuration file for the above item. * @return true if the item configuration should be saved. */ public boolean isSaveable(final Saveable item, final XmlFile xmlFile) { if (item instanceof TopLevelItem) { return true; } if (xmlFile.getFile().getParentFile().equals(getJenkinsHome())) { return checkRegex(xmlFile); } if (PluginUtils.isMavenPluginAvailable() && item instanceof MavenModule && saveModuleConfiguration) { return true; } return false; } /** * Check whether config file should not be saved because of regex pattern. * * @param xmlFile * The config file * @return True if it should be saved */ private boolean checkRegex(final XmlFile xmlFile) { if (excludeRegexpPattern != null) { final Matcher matcher = excludeRegexpPattern .matcher(xmlFile.getFile().getName()); return !matcher.find(); } else { return true; } } /** * Validates the user entry for the maximum number of history items to keep. * Must be blank or a non-negative integer. * * @param value * The form input entered by the user. * @return ok if the entry is blank or a non-negative integer. */ public FormValidation doCheckMaxHistoryEntries( @QueryParameter final String value) { try { if (StringUtils.isNotBlank(value) && Integer.parseInt(value) < 0) { throw new NumberFormatException(); } return FormValidation.ok(); } catch (NumberFormatException ex) { return FormValidation.error("Enter a valid positive integer"); } } /** * Validates the user entry for the maximum number of history items to show * per site. Must be blank or a non-negative integer. * * @param value * The form input entered by the user. * @return ok if the entry is blank or a non-negative integer. */ public FormValidation doCheckMaxEntriesPerPage( @QueryParameter final String value) { try { if (StringUtils.isNotBlank(value) && Integer.parseInt(value) < 0) { throw new NumberFormatException(); } return FormValidation.ok(); } catch (NumberFormatException ex) { return FormValidation.error("Enter a valid positive integer"); } } /** * Validates the user entry for the maximum number of days to keep history * items. Must be blank or a non-negative integer. * * @param value * The form input entered by the user. * @return ok if the entry is blank or a non-negative integer. */ public FormValidation doCheckMaxDaysToKeepEntries( @QueryParameter final String value) { try { if (StringUtils.isNotBlank(value) && Integer.parseInt(value) < 0) { throw new NumberFormatException(); } return FormValidation.ok(); } catch (NumberFormatException ex) { return FormValidation.error("Enter a valid positive integer"); } } /** * Validates the user entry for the regular expression of system file names * to exclude from saving. * * @param value * The form input entered by the user. * @return ok if the entry is a valid regular expression. */ public FormValidation doCheckExcludePattern( @QueryParameter final String value) { try { Pattern.compile(value); return FormValidation.ok(); } catch (PatternSyntaxException e) { return FormValidation.error("Invalid regexp:\n" + e); } } /** * For tests. * * @return the historyDao */ protected HistoryDao getHistoryDao() { return PluginUtils.getHistoryDao(this); } /** * For tests. * * @return JENKINS_HOME */ protected File getJenkinsHome() { return Jenkins.getInstance().root; } /** * For tests. * * @return Jenkins instance. */ @Deprecated public Jenkins getJenkins() { return Jenkins.getInstance(); } }