package hudson.plugins.jobConfigHistory; import hudson.XmlFile; import hudson.model.Action; import hudson.model.Hudson; import hudson.security.AccessControlled; import hudson.util.MultipartFormDataParser; import java.io.File; import java.io.IOException; import java.io.StringWriter; import javax.servlet.ServletException; import org.kohsuke.stapler.Stapler; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import bmsi.util.Diff; import bmsi.util.DiffPrint; import bmsi.util.Diff.change; /** * Implements some basic methods needed by the {@link JobConfigHistoryRootAction} and {@link JobConfigHistoryProjectAction}. * * @author mfriedenhagen */ public abstract class JobConfigHistoryBaseAction implements Action { /** * The hudson instance. */ private final Hudson hudson; /** * Set the {@link Hudson} instance. */ public JobConfigHistoryBaseAction() { hudson = Hudson.getInstance(); } /** * {@inheritDoc} * * Make method final, as we always want the same display name. */ // @Override public final String getDisplayName() { return Messages.displayName(); } /** * {@inheritDoc} * * Make method final, as we always want the same icon file. Returns {@code null} to hide the icon if the user is not * allowed to configure jobs. */ // @Override public final String getIconFileName() { return hasConfigurePermission() ? JobConfigHistoryConsts.ICONFILENAME : null; } /** * {@inheritDoc} * * Do not make this method final as {@link JobConfigHistoryRootAction} overrides this method. */ // @Override public String getUrlName() { return JobConfigHistoryConsts.URLNAME; } /** * Do we want 'raw' output? * * @return true if request parameter type is 'raw'. */ public final boolean wantRawOutput() { return isTypeParameter("raw"); } /** * Do we want 'xml' output? * * @return true if request parameter type is 'xml'. */ public final boolean wantXmlOutput() { return isTypeParameter("xml"); } /** * Returns {@link JobConfigHistoryBaseAction#getConfigXml(String)} as String. * * @return content of the {@code config.xml} found in directory given by the request parameter {@code file}. * @throws IOException * if the config file could not be read or converted to an xml string. */ public final String getFile() throws IOException { checkConfigurePermission(); final XmlFile xmlFile = getConfigXml(getRequestParameter("file")); return xmlFile.asString(); } /** * Checks whether the type parameter of the current request equals {@code toCompare}. * * @param toCompare * the string we want to compare. * @return true if {@code toCompare} equals request parameter type. */ private boolean isTypeParameter(final String toCompare) { return getRequestParameter("type").equalsIgnoreCase(toCompare); } /** * Returns the configuration file (default is {@code config.xml}) located in {@code diffDir}. * {@code diffDir} must either start with {@code HUDSON_HOME} and contain {@code config-history} * or be located under the configured {@code historyRootDir}. It also must not contain a '..' pattern. * Otherwise an {@link IllegalArgumentException} will be thrown. * <p>This is to ensure that this plugin will not be abused to get arbitrary * xml configuration files located anywhere on the system. * * @param diffDir * timestamped history directory. * @return xmlfile. */ protected XmlFile getConfigXml(final String diffDir) { final JobConfigHistory plugin = hudson.getPlugin(JobConfigHistory.class); final File configuredHistoryRootDir = plugin.getConfiguredHistoryRootDir(); final String allowedHistoryRootDir = configuredHistoryRootDir == null ? getHudson().getRootDir().getAbsolutePath() : configuredHistoryRootDir.getAbsolutePath(); File configFile = null; if (diffDir != null) { if (!diffDir.startsWith(allowedHistoryRootDir) || diffDir.contains("..")) { throw new IllegalArgumentException(diffDir + " does not start with " + allowedHistoryRootDir + " or contains '..'"); } else if (configuredHistoryRootDir == null && !diffDir.contains(JobConfigHistoryConsts.DEFAULT_HISTORY_DIR)) { throw new IllegalArgumentException(diffDir + " does not contain '" + JobConfigHistoryConsts.DEFAULT_HISTORY_DIR + "'"); } configFile = plugin.getConfigFile(new File(diffDir)); } if (configFile == null) { throw new IllegalArgumentException("Unable to get history from: " + diffDir); } else { return new XmlFile(configFile); } } /** * Returns the parameter named {@code parameterName} from current request. * * @param parameterName * name of the parameter. * @return value of the request parameter or null if it does not exist. */ protected String getRequestParameter(final String parameterName) { return Stapler.getCurrentRequest().getParameter(parameterName); } /** * See whether the current user may read configurations in the object returned by * {@link JobConfigHistoryBaseAction#getAccessControlledObject()}. */ protected abstract void checkConfigurePermission(); /** * Returns whether the current user may read configurations in the object returned by * {@link JobConfigHistoryBaseAction#getAccessControlledObject()}. * * @return true if the current user may read configurations. */ protected abstract boolean hasConfigurePermission(); /** * Returns the hudson instance. * * @return the hudson */ protected final Hudson getHudson() { return hudson; } /** * Returns the JobConfigHistory plugin instance. * @return the JobConfigHistory plugin */ protected final JobConfigHistory getPlugin() { return hudson.getPlugin(JobConfigHistory.class); } /** * Returns the object for which we want to provide access control. * * @return the access controlled object. */ protected abstract AccessControlled getAccessControlledObject(); /** * Parses the incoming {@code POST} request and redirects as {@code GET showDiffFiles}. * * @param req * incoming request * @param rsp * outgoing response * @throws ServletException * when parsing the request as {@link MultipartFormDataParser} does not succeed. * @throws IOException * when the redirection does not succeed. */ public final void doDiffFiles(StaplerRequest req, StaplerResponse rsp) throws ServletException, IOException { final MultipartFormDataParser parser = new MultipartFormDataParser(req); rsp.sendRedirect("showDiffFiles?histDir1=" + parser.get("histDir1") + "&histDir2=" + parser.get("histDir2")); } /** * Returns a textual diff between two {@code config.xml} files located in {@code histDir1} and {@code histDir2} * directories given as parameters of {@link Stapler#getCurrentRequest()}. * * @return diff * @throws IOException * if reading one of the config files does not succeed. */ public final String getDiffFile() throws IOException { checkConfigurePermission(); final XmlFile configXml1 = getConfigXml(getRequestParameter("histDir1")); final String[] configXml1Lines = configXml1.asString().split("\\n"); final XmlFile configXml2 = getConfigXml(getRequestParameter("histDir2")); final String[] configXml2Lines = configXml2.asString().split("\\n"); return getDiff(configXml1.getFile(), configXml2.getFile(), configXml1Lines, configXml2Lines); } /** * Returns a unified diff between two string arrays. * * @param file1 * first config file. * @param file2 * second config file. * @param file1Lines * the lines of the first file. * @param file2Lines * the lines of the second file. * @return unified diff */ protected final String getDiff(final File file1, final File file2, final String[] file1Lines, final String[] file2Lines) { final change change = new Diff(file1Lines, file2Lines).diff_2(false); final DiffPrint.UnifiedPrint unifiedPrint = new DiffPrint.UnifiedPrint(file1Lines, file2Lines); final StringWriter output = new StringWriter(); unifiedPrint.setOutput(output); unifiedPrint.print_header(file1.getPath(), file2.getPath()); unifiedPrint.print_script(change); return output.toString(); } }