Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@
import java.net.URLDecoder;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
Expand Down Expand Up @@ -176,9 +177,10 @@ protected SkylineTool getToolFromZip(MultipartFile zip) throws IOException
{
ZipEntry zipEntry;
while ((zipEntry = zipStream.getNextEntry()) != null &&
(tool == null || tool.getIcon() == null))
(tool == null || toolIcon == null))
{
if (zipEntry.getName().toLowerCase().startsWith("tool-inf/"))
String entryLower = zipEntry.getName().toLowerCase();
if (entryLower.startsWith("tool-inf/") && !entryLower.startsWith("tool-inf/docs/"))
{
String lowerBaseName = new File(zipEntry.getName()).getName().toLowerCase();

Expand Down Expand Up @@ -229,14 +231,46 @@ protected byte[] unzip(ZipInputStream stream)
}
}

protected static boolean extractDocsFromZip(Path zipFile, Path containerDir) throws IOException
{
Path docsDir = containerDir.resolve("docs");
boolean extracted = false;
try (ZipFile zf = new ZipFile(zipFile.toFile()))
{
Enumeration<? extends ZipEntry> entries = zf.entries();
while (entries.hasMoreElements())
{
ZipEntry entry = entries.nextElement();
String name = entry.getName();
if (!name.toLowerCase().startsWith("tool-inf/docs/") || entry.isDirectory())
continue;
// Strip "tool-inf/docs/" prefix to get relative path within docs dir
String relativePath = name.substring("tool-inf/docs/".length());
if (relativePath.isEmpty())
continue;
Path destPath = docsDir.resolve(relativePath).normalize();
// Zip-slip protection
if (!destPath.startsWith(docsDir.normalize()))
throw new IOException("Zip entry outside target directory: " + name);
Files.createDirectories(destPath.getParent());
try (InputStream in = zf.getInputStream(entry))
{
Files.copy(in, destPath, StandardCopyOption.REPLACE_EXISTING);
}
extracted = true;
}
}
return extracted;
}

public static File makeFile(Container c, String filename)
{
return new File(getLocalPath(c), FileUtil.makeLegalName(filename));
return getLocalPath(c).resolve(FileUtil.makeLegalName(filename)).toFile();
}

public static File getLocalPath(Container c)
public static Path getLocalPath(Container c)
{
return FileContentService.get().getFileRootPath(c, FileContentService.ContentType.files).toFile();
return FileContentService.get().getFileRootPath(c, FileContentService.ContentType.files);
}

protected Container makeContainer(Container parent, String folderName, List<User> users, Role role) throws IOException
Expand Down Expand Up @@ -383,7 +417,7 @@ public static ArrayList<String> getToolRelevantUsers(SkylineTool tool, Role[] ro
return new ArrayList<>(users);
}

public static HashMap<String, String> getSupplementaryFiles(SkylineTool tool)
public static HashMap<String, String> getSupplementaryFiles(SkylineTool tool) throws IOException
{
// Store supporting files in map <url, icon url>
final String[] knownExtensions = {"pdf", "zip"};
Expand All @@ -401,15 +435,15 @@ public static HashMap<String, String> getSupplementaryFiles(SkylineTool tool)
return suppFiles;
}

public static HashSet<String> getSupplementaryFileBasenames(SkylineTool tool)
public static HashSet<String> getSupplementaryFileBasenames(SkylineTool tool) throws IOException
{
HashSet<String> suppFiles = new HashSet<>();
File localToolDir = getLocalPath(tool.lookupContainer());
for (String suppFile : localToolDir.list())
Path localToolDir = getLocalPath(tool.lookupContainer());
try (var stream = Files.list(localToolDir))
{
final String basename = new File(suppFile).getName();
if (!basename.startsWith(".") && !basename.equals(tool.getZipName()) && !basename.equals("icon.png"))
suppFiles.add(suppFile);
stream.map(p -> p.getFileName().toString())
.filter(name -> !name.startsWith(".") && !name.equals(tool.getZipName()) && !name.equals("icon.png") && !name.equals("docs"))
.forEach(suppFiles::add);
}
return suppFiles;
}
Expand Down Expand Up @@ -583,9 +617,19 @@ else if (!getContainer().hasPermission(getUser(), InsertPermission.class))
{
Container c = makeContainer(getContainer(), folderName, toolOwnersUsers, RoleManager.getRole(EditorRole.class));
copyContainerPermissions(existingVersionContainer, c);
zip.transferTo(makeFile(c, zip.getOriginalFilename()));
File storedZip = makeFile(c, zip.getOriginalFilename());
zip.transferTo(storedZip);
tool.writeIconToFile(makeFile(c, "icon.png"), "png");

// Extract docs from tool-inf/docs/ in the ZIP; carry forward from previous version if absent
boolean hasDocs = extractDocsFromZip(storedZip.toPath(), getLocalPath(c));
if (!hasDocs && existingVersionContainer != null)
{
Path oldDocs = getLocalPath(existingVersionContainer).resolve("docs");
if (Files.isDirectory(oldDocs))
FileUtil.copyDirectory(oldDocs, getLocalPath(c).resolve("docs"));
}
Comment on lines +624 to +631
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@brendanx67 Should we verify that tool-inf/docs contains index.html so that we get an error at tool upload time if it is missing? Otherwise, the contents of the docs directory are silently ignored from the UI.


if (copyFiles != null && existingVersionContainer != null)
for (String copyFile : copyFiles)
FileUtils.copyFile(makeFile(existingVersionContainer, copyFile), makeFile(c, copyFile), true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,15 @@
import org.apache.commons.lang3.StringUtils;
import org.labkey.api.data.Container;
import org.labkey.api.data.Entity;
import org.labkey.api.files.FileContentService;
import org.labkey.api.settings.AppProps;
import org.labkey.api.util.Pair;
import org.labkey.api.webdav.WebdavService;
import org.labkey.skylinetoolsstore.SkylineToolsStoreController;

import java.nio.file.Files;
import java.nio.file.Path;

import javax.imageio.ImageIO;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
Expand Down Expand Up @@ -293,6 +298,21 @@ public String getFolderUrl()
return AppProps.getInstance().getContextPath() + "/files" + lookupContainer().getPath() + "/";
}

public boolean hasDocumentation()
{
Path localPath = SkylineToolsStoreController.getLocalPath(lookupContainer());
return localPath != null && Files.exists(localPath.resolve("docs/index.html"));
}

public String getDocsUrl()
{
org.labkey.api.util.Path path = WebdavService.getPath()
.append(lookupContainer().getParsedPath())
.append(FileContentService.FILES_LINK)
.append(new org.labkey.api.util.Path("docs", "index.html"));
return AppProps.getInstance().getContextPath() + path.encode();
Comment on lines +309 to +313
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LabKey's CSP is applied to all responses including WebDAV file serving. Scripts in user-uploaded HTML will be blocked - script-src uses strict-dynamic with a server-generated nonce, so any script without a valid nonce will be rejected by the browser.

}

public String getIconUrl()
{
return (SkylineToolsStoreController.makeFile(lookupContainer(), "icon.png").exists()) ?
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
<%@ page import="org.apache.commons.lang3.StringUtils" %>
<%@ page import="org.labkey.api.data.Container" %>
<%@ page import="org.labkey.api.data.ContainerManager" %>
<%@ page import="org.labkey.api.portal.ProjectUrls" %>
<%@ page import="org.labkey.api.security.permissions.DeletePermission" %>
<%@ page import="org.labkey.api.security.permissions.InsertPermission" %>
Expand Down Expand Up @@ -298,8 +300,21 @@ a { text-decoration: none; }
<% } %>
</div>

<%
Container supportContainer = getContainer().getChild("Support");
Container toolSupportBoard = supportContainer != null ? supportContainer.getChild(tool.getName()) : null;
Container supportTarget;
if (toolSupportBoard != null)
supportTarget = toolSupportBoard;
else
supportTarget = ContainerManager.getForPath("/home/support");
%>
<% if (supportTarget != null) { %>
<button id="tool-support-board-btn" class="banner-button-small">Support Board</button>
<% addHandler("tool-support-board-btn", "click", "window.open(" + q(urlProvider(ProjectUrls.class).getBeginURL(getContainer().getChild("Support").getChild(tool.getName()))) + ", '_blank', 'noopener,noreferrer')"); %>
<%
addHandler("tool-support-board-btn", "click", "window.open(" + q(urlProvider(ProjectUrls.class).getBeginURL(supportTarget)) + ", '_blank', 'noopener,noreferrer')");
%>
<% } %>
</div>
<% if (toolEditor) { %>
<div class="menuMouseArea sprocket">
Expand Down Expand Up @@ -332,9 +347,20 @@ a { text-decoration: none; }
</div>
</div>

<% if (suppIter.hasNext()) { %>
<%
boolean hasDocumentation = tool.hasDocumentation();
%>
<% if (hasDocumentation || suppIter.hasNext()) { %>
<div id="documentationbox" class="itemsbox">
<legend>Documentation</legend>
<% if (hasDocumentation) { %>
<div class="barItem">
<a href="<%=h(tool.getDocsUrl())%>" target="_blank" rel="noopener noreferrer">
<img src="<%= h(imgDir) %>link.png" alt="Documentation" />
<span>Online Documentation</span>
</a>
</div>
<% } %>
<%
while (suppIter.hasNext()) {
Map.Entry suppPair = (Map.Entry)suppIter.next();
Expand Down
Loading