diff --git a/SkylineToolsStore/src/org/labkey/skylinetoolsstore/SkylineToolsStoreController.java b/SkylineToolsStore/src/org/labkey/skylinetoolsstore/SkylineToolsStoreController.java index ae44422c..c89fb7d1 100644 --- a/SkylineToolsStore/src/org/labkey/skylinetoolsstore/SkylineToolsStoreController.java +++ b/SkylineToolsStore/src/org/labkey/skylinetoolsstore/SkylineToolsStoreController.java @@ -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; @@ -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(); @@ -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 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 users, Role role) throws IOException @@ -383,7 +417,7 @@ public static ArrayList getToolRelevantUsers(SkylineTool tool, Role[] ro return new ArrayList<>(users); } - public static HashMap getSupplementaryFiles(SkylineTool tool) + public static HashMap getSupplementaryFiles(SkylineTool tool) throws IOException { // Store supporting files in map final String[] knownExtensions = {"pdf", "zip"}; @@ -401,15 +435,15 @@ public static HashMap getSupplementaryFiles(SkylineTool tool) return suppFiles; } - public static HashSet getSupplementaryFileBasenames(SkylineTool tool) + public static HashSet getSupplementaryFileBasenames(SkylineTool tool) throws IOException { HashSet 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; } @@ -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")); + } + if (copyFiles != null && existingVersionContainer != null) for (String copyFile : copyFiles) FileUtils.copyFile(makeFile(existingVersionContainer, copyFile), makeFile(c, copyFile), true); diff --git a/SkylineToolsStore/src/org/labkey/skylinetoolsstore/model/SkylineTool.java b/SkylineToolsStore/src/org/labkey/skylinetoolsstore/model/SkylineTool.java index 684f8397..95aab276 100644 --- a/SkylineToolsStore/src/org/labkey/skylinetoolsstore/model/SkylineTool.java +++ b/SkylineToolsStore/src/org/labkey/skylinetoolsstore/model/SkylineTool.java @@ -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; @@ -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(); + } + public String getIconUrl() { return (SkylineToolsStoreController.makeFile(lookupContainer(), "icon.png").exists()) ? diff --git a/SkylineToolsStore/src/org/labkey/skylinetoolsstore/view/SkylineToolDetails.jsp b/SkylineToolsStore/src/org/labkey/skylinetoolsstore/view/SkylineToolDetails.jsp index 86bd347a..77277605 100644 --- a/SkylineToolsStore/src/org/labkey/skylinetoolsstore/view/SkylineToolDetails.jsp +++ b/SkylineToolsStore/src/org/labkey/skylinetoolsstore/view/SkylineToolDetails.jsp @@ -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" %> @@ -298,8 +300,21 @@ a { text-decoration: none; } <% } %> + <% + 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) { %> - <% 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')"); + %> + <% } %> <% if (toolEditor) { %> -<% if (suppIter.hasNext()) { %> +<% + boolean hasDocumentation = tool.hasDocumentation(); +%> +<% if (hasDocumentation || suppIter.hasNext()) { %>
Documentation +<% if (hasDocumentation) { %> + +<% } %> <% while (suppIter.hasNext()) { Map.Entry suppPair = (Map.Entry)suppIter.next();