/* * The MIT License * * Copyright 2015 acearl. * * 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.emailext.plugins.content; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.ExtensionList; import hudson.FilePath; import hudson.Plugin; import hudson.model.AbstractBuild; import hudson.model.Run; import hudson.model.TaskListener; import hudson.plugins.emailext.ExtendedEmailPublisher; import hudson.remoting.VirtualChannel; import hudson.security.ACL; import hudson.util.FormValidation; import jenkins.MasterToSlaveFileCallable; import jenkins.model.Jenkins; import jenkins.security.NotReallyRoleSensitiveCallable; import org.apache.commons.io.FilenameUtils; import org.jenkinsci.lib.configprovider.ConfigProvider; import org.jenkinsci.lib.configprovider.model.Config; import org.jenkinsci.plugins.scriptsecurity.scripts.Language; import org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval; import org.jenkinsci.plugins.tokenmacro.DataBoundTokenMacro; import org.jenkinsci.plugins.tokenmacro.MacroEvaluationException; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.util.logging.Level; import java.util.logging.Logger; /** * * @author acearl */ public abstract class AbstractEvalContent extends DataBoundTokenMacro { protected static final String EMAIL_TEMPLATES_DIRECTORY = "email-templates"; protected final String macroName; public AbstractEvalContent(String macroName) { this.macroName = macroName; } @Override public String evaluate(AbstractBuild<?, ?> build, TaskListener listener, String macroName) throws MacroEvaluationException, IOException, InterruptedException { return evaluate(build, build.getWorkspace(), listener, macroName); } @Override public abstract String evaluate(Run<?, ?> run, FilePath workspace, TaskListener listener, String macroName) throws MacroEvaluationException, IOException, InterruptedException; @Override public boolean acceptsMacroName(String macroName) { return macroName.equals(this.macroName); } public static File scriptsFolder() { return new File(Jenkins.getActiveInstance().getRootDir(), EMAIL_TEMPLATES_DIRECTORY); } protected abstract Class<? extends ConfigProvider> getProviderClass(); @Override public boolean hasNestedContent() { return false; } protected InputStream getFileInputStream(FilePath workspace, String fileName, String extension) throws FileNotFoundException, IOException, InterruptedException { InputStream inputStream = null; if(fileName.startsWith("managed:")) { String managedFileName = fileName.substring(8); try { inputStream = getManagedFile(managedFileName); } catch(NoClassDefFoundError e) { inputStream = null; } if(inputStream == null) { throw new FileNotFoundException(String.format("Managed file '%s' not found", managedFileName)); } return inputStream; } String fileExt = FilenameUtils.getExtension(fileName); // add default extension if needed if ("".equals(fileExt)) { fileName += extension; } // next we look in the workspace, this means the filename is relative to the root of the workspace if(workspace != null) { FilePath file = workspace.child(fileName); if(file.exists() && isChildOf(file, workspace)) { //Guard against .. escapes inputStream = new UserProvidedContentInputStream(file.read()); } } if(inputStream == null) { inputStream = getClass().getClassLoader().getResourceAsStream( "hudson/plugins/emailext/templates/" + fileName); if (inputStream == null) { File templateFile = new File(scriptsFolder(), fileName); // the file may have an extension, but not the correct one if(!templateFile.exists()) { fileName += extension; templateFile = new File(scriptsFolder(), fileName); } if (!templateFile.exists() || !isChildOf(new FilePath(templateFile), new FilePath(scriptsFolder()))) { //guard against .. escapes throw new FileNotFoundException(fileName); //Say whatever the user provided so we don't leak any information about the filesystem, but generateMissingFile should cover us. } else { inputStream = new FileInputStream(templateFile); } } } return inputStream; } @Restricted(NoExternalUse.class) public static boolean isChildOf(final FilePath potentialChild, final FilePath parent) throws IOException, InterruptedException { //TODO JENKINS-26838 use API when available in core return parent.act(new IsChildFileCallable(potentialChild)); } private InputStream getManagedFile(String fileName) throws UnsupportedEncodingException { InputStream stream = null; Plugin plugin = Jenkins.getActiveInstance().getPlugin("config-file-provider"); if (plugin != null) { Config config = null; ExtensionList<ConfigProvider> providers = ConfigProvider.all(); ConfigProvider provider = providers.get(getProviderClass()); assert provider != null; for (Config c : provider.getAllConfigs()) { if (c.name.equalsIgnoreCase(fileName)) { config = c; break; } } if (config != null) { stream = new ByteArrayInputStream(config.content.getBytes("UTF-8")); } } return stream; } protected String generateMissingFile(String type, String fileName) { return type + " file [" + fileName + "] was not found in $JENKINS_HOME/" + EMAIL_TEMPLATES_DIRECTORY + "."; } protected String getCharset(Run<?, ?> build) { return ExtendedEmailPublisher.descriptor().getCharset(); } @Restricted(NoExternalUse.class) @SuppressFBWarnings(value = "SE_BAD_FIELD", justification = "The callable is not going to be serialized") public static boolean isApprovedScript(final String script, final Language language) { final ScriptApproval approval = ScriptApproval.get(); try { //checking doesn't check if we are system or not since it assumed being called from doCheckField return ACL.impersonate(Jenkins.ANONYMOUS, new NotReallyRoleSensitiveCallable<Boolean, Exception>() { @Override public Boolean call() throws Exception { return approval.checking(script, language).kind == FormValidation.Kind.OK; } }); } catch (Exception e) { Logger.getLogger(AbstractEvalContent.class.getName()).log(Level.WARNING, "Could not determine approval state of script.", e); return false; } } private static class IsChildFileCallable extends MasterToSlaveFileCallable<Boolean> { private final FilePath potentialChild; private IsChildFileCallable(FilePath potentialChild) { this.potentialChild = potentialChild; } @Override public Boolean invoke(File parent, VirtualChannel channel) throws IOException, InterruptedException { if (potentialChild.isRemote()) { //Not on the same machine so can't be a child of the local file return false; } FilePath test = potentialChild.getParent(); FilePath target = new FilePath(parent); while(test != null && !target.equals(test)) { test = test.getParent(); } return target.equals(test); } } }