package com.atlassian.labs.speakeasy.manager;
import com.atlassian.labs.speakeasy.git.GitRepositoryManager;
import com.atlassian.labs.speakeasy.util.RepositoryDirectoryUtil;
import com.atlassian.labs.speakeasy.util.exec.ReadOnlyOperation;
import com.atlassian.plugin.JarPluginArtifact;
import com.atlassian.plugin.PluginArtifact;
import com.atlassian.templaterenderer.TemplateRenderer;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.eclipse.jgit.lib.Repository;
import org.osgi.framework.Bundle;
import java.io.*;
import java.net.URL;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;
import static com.atlassian.labs.speakeasy.util.ExtensionValidate.isValidExtensionKey;
import static com.atlassian.labs.speakeasy.util.KeyExtractor.createExtractableTempFile;
import static java.util.Arrays.asList;
/**
*
*/
public abstract class AbstractOsgiPluginTypeHandler implements PluginTypeHandler
{
protected static final Iterable<Pattern> CORE_WHITELIST = asList(
Pattern.compile(".*[._]js", Pattern.CASE_INSENSITIVE),
Pattern.compile(".*[._]eot", Pattern.CASE_INSENSITIVE),
Pattern.compile(".*[._]ttf", Pattern.CASE_INSENSITIVE),
Pattern.compile(".*[._]woff", Pattern.CASE_INSENSITIVE),
Pattern.compile(".*[._]svg", Pattern.CASE_INSENSITIVE),
Pattern.compile(".*[._]svgz", Pattern.CASE_INSENSITIVE),
Pattern.compile(".*[._]mu", Pattern.CASE_INSENSITIVE),
Pattern.compile(".*[._]json", Pattern.CASE_INSENSITIVE),
Pattern.compile(".*[._]gif", Pattern.CASE_INSENSITIVE),
Pattern.compile(".*[._]png", Pattern.CASE_INSENSITIVE),
Pattern.compile(".*[._]jpg", Pattern.CASE_INSENSITIVE),
Pattern.compile(".*[._]jpeg", Pattern.CASE_INSENSITIVE),
// Pattern.compile(".*\\.xml", Pattern.CASE_INSENSITIVE), // excluded for now as you could add a spring XML file and load other classes
Pattern.compile(".*[._]css", Pattern.CASE_INSENSITIVE),
Pattern.compile(".*/\\._[^.]+", Pattern.CASE_INSENSITIVE),
Pattern.compile(".*/$"),
Pattern.compile("META-INF/MANIFEST.MF"),
Pattern.compile(".*/pom.xml"),
Pattern.compile(".*/pom.properties"),
Pattern.compile("README.txt"),
Pattern.compile("LICENSE.txt"),
Pattern.compile("README.md"),
Pattern.compile("\\.gitignore"),
Pattern.compile(".*\\.DS_Store"));
private final TemplateRenderer templateRenderer;
private final GitRepositoryManager gitRepositoryManager;
public AbstractOsgiPluginTypeHandler(TemplateRenderer templateRenderer, GitRepositoryManager repositoryManager)
{
this.templateRenderer = templateRenderer;
this.gitRepositoryManager = repositoryManager;
}
public final boolean allowEntryPath(String path)
{
Iterable<Pattern> whitelistPatterns = getWhitelistPatterns();
for (Pattern whitelist : whitelistPatterns)
{
if (whitelist.matcher(path).matches())
{
return true;
}
}
return false;
}
public final File createTempFile(String pluginKey) throws IOException
{
return createExtractableTempFile(pluginKey, "." + getExtension());
}
public final String canInstall(File file)
{
PluginArtifact artifact = new JarPluginArtifact(file);
if (artifact.doesResourceExist(getDescriptorPath()))
{
String key = extractPluginKey(artifact);
if (isValidExtensionKey(key))
{
return key;
}
}
return null;
}
public final PluginArtifact createArtifact(File uploadedFile)
{
PluginArtifact pluginArtifact = new JarPluginArtifact(uploadedFile);
verifyContents(pluginArtifact);
pluginArtifact = validatePluginArtifact(pluginArtifact);
return pluginArtifact;
}
public String getPluginFile(final String pluginKey, final String fileName) throws IOException
{
return gitRepositoryManager.operateOnRepository(pluginKey, new ReadOnlyOperation<Repository, String>()
{
public String operateOn(Repository repo) throws Exception
{
File dir = repo.getWorkTree();
File file = new File(dir, fileName);
if (file.exists())
{
return FileUtils.readFileToString(file);
}
throw new FileNotFoundException(fileName);
}
});
}
public File getPluginArtifact(String pluginKey) throws IOException
{
return gitRepositoryManager.buildJarFromRepository(pluginKey);
}
public List<String> getPluginFileNames(String pluginKey)
{
return gitRepositoryManager.operateOnRepository(pluginKey, new ReadOnlyOperation<Repository, List<String>>()
{
public List<String> operateOn(Repository repo) throws Exception
{
final File dir = repo.getWorkTree();
return RepositoryDirectoryUtil.getEntries(dir);
}
});
}
public File getPluginAsProject(String pluginKey, final Map<String, Object> context)
{
return gitRepositoryManager.operateOnRepository(pluginKey, new ReadOnlyOperation<Repository, File>()
{
public File operateOn(Repository repo) throws Exception
{
FileOutputStream fout = null;
ZipOutputStream zout = null;
File file = null;
try
{
file = File.createTempFile("speakeasy-plugin-project", ".zip");
fout = new FileOutputStream(file);
zout = new ZipOutputStream(fout);
zout.putNextEntry(new ZipEntry("src/"));
zout.putNextEntry(new ZipEntry("src/main/"));
zout.putNextEntry(new ZipEntry("src/main/resources/"));
List<String> paths = RepositoryDirectoryUtil.getEntries(repo.getWorkTree());
for (String path : paths)
{
String actualPath = "src/main/resources/" + path;
ZipEntry entry = new ZipEntry(actualPath);
zout.putNextEntry(entry);
if (!path.endsWith("/"))
{
byte[] data = FileUtils.readFileToByteArray(new File(repo.getWorkTree(), path));
zout.write(data, 0, data.length);
}
}
zout.putNextEntry(new ZipEntry("pom.xml"));
String pomContents = renderPom(context);
IOUtils.copy(new StringReader(pomContents), zout);
}
catch (IOException e)
{
throw new RuntimeException("Unable to create plugin project", e);
}
finally
{
IOUtils.closeQuietly(zout);
IOUtils.closeQuietly(fout);
}
return file;
}
});
}
public File createExample(String pluginKey, String name, String description) throws IOException
{
ZipOutputStream zout = null;
File tmpFile = null;
try
{
tmpFile = createTempFile(pluginKey);
zout = new ZipOutputStream(new FileOutputStream(tmpFile));
createExampleContents(zout, pluginKey, name, description);
zout.close();
}
finally
{
IOUtils.closeQuietly(zout);
}
return tmpFile;
}
public File createFork(String pluginKey, final String forkPluginKey, String user, final String description) throws IOException
{
return gitRepositoryManager.operateOnRepository(pluginKey, new ReadOnlyOperation<Repository, File>()
{
public File operateOn(Repository repo) throws Exception
{
ZipOutputStream zout = null;
File tmpFile = null;
try
{
tmpFile = createTempFile(forkPluginKey);
zout = new ZipOutputStream(new FileOutputStream(tmpFile));
final File repoDir = repo.getWorkTree();
List<String> bundlePaths = RepositoryDirectoryUtil.getEntries(repoDir);
bundlePaths.remove(getDescriptorPath());
for (String path : bundlePaths)
{
ZipEntry entry = new ZipEntry(path);
zout.putNextEntry(entry);
if (!path.endsWith("/"))
{
byte[] data = FileUtils.readFileToByteArray(new File(repoDir, path));
zout.write(data, 0, data.length);
}
}
zout.putNextEntry(new ZipEntry(getDescriptorPath()));
forkDescriptor(new ByteArrayInputStream(FileUtils.readFileToByteArray(new File(repoDir, getDescriptorPath()))),
zout, forkPluginKey, description);
zout.close();
}
finally
{
IOUtils.closeQuietly(zout);
}
return tmpFile;
}
});
}
public File rebuildPlugin(final String pluginKey, final String fileName, final String contents) throws IOException
{
return gitRepositoryManager.operateOnRepository(pluginKey, new ReadOnlyOperation<Repository, File>()
{
public File operateOn(Repository repo) throws Exception
{
ZipOutputStream zout = null;
File tmpFile = null;
try
{
tmpFile = createTempFile(pluginKey);
zout = new ZipOutputStream(new FileOutputStream(tmpFile));
final File repoDir = repo.getWorkTree();
for (String path : RepositoryDirectoryUtil.getEntries(repoDir))
{
if (!path.equals(fileName) && !path.contains("-min."))
{
ZipEntry entry = new ZipEntry(path);
zout.putNextEntry(entry);
if (!path.endsWith("/"))
{
byte[] data = FileUtils.readFileToByteArray(new File(repoDir, path));
zout.write(data, 0, data.length);
}
}
}
ZipEntry entry = new ZipEntry(fileName);
byte[] data = contents.getBytes();
entry.setSize(data.length);
zout.putNextEntry(entry);
zout.write(data);
zout.close();
}
finally
{
IOUtils.closeQuietly(zout);
}
return tmpFile;
}
});
}
private String renderPom(Map<String,Object> context) throws IOException
{
StringWriter writer = new StringWriter();
templateRenderer.render("templates/pom.vm", context, writer);
return writer.toString();
}
protected abstract Iterable<Pattern> getWhitelistPatterns();
protected abstract void createExampleContents(ZipOutputStream zout, String pluginKey, String name, String description) throws IOException;
protected abstract String extractPluginKey(PluginArtifact artifact);
protected abstract String getExtension();
protected abstract PluginArtifact validatePluginArtifact(PluginArtifact pluginArtifact);
protected abstract String getDescriptorPath();
protected abstract void forkDescriptor(InputStream byteArrayInputStream, OutputStream zout, String key, String description) throws IOException;
private void verifyContents(PluginArtifact plugin) throws PluginOperationFailedException
{
ZipFile zip = null;
try
{
zip = new ZipFile(plugin.toFile());
for (Enumeration<? extends ZipEntry> e = zip.entries(); e.hasMoreElements();)
{
ZipEntry entry = e.nextElement();
if (!allowEntryPath(entry.getName()))
{
throw new PluginOperationFailedException("Invalid plugin entry: " + entry.getName(), null);
}
}
}
catch (IOException e)
{
throw new PluginOperationFailedException("Unable to open plugin zip", e, null);
}
finally
{
if (zip != null)
{
try
{
zip.close();
}
catch (IOException e)
{
// ignore
}
}
}
}
private byte[] readEntry(Bundle bundle, String path)
throws IOException
{
ByteArrayOutputStream bout = new ByteArrayOutputStream();
URL url = bundle.getEntry(path);
InputStream urlIn = null;
try
{
urlIn = url.openStream();
IOUtils.copy(urlIn, bout);
}
finally
{
IOUtils.closeQuietly(urlIn);
}
return bout.toByteArray();
}
}