diff --git a/api/src/org/labkey/api/security/GroupManager.java b/api/src/org/labkey/api/security/GroupManager.java index a9d49fb0567..fff7ec7623a 100644 --- a/api/src/org/labkey/api/security/GroupManager.java +++ b/api/src/org/labkey/api/security/GroupManager.java @@ -205,7 +205,9 @@ private static void appendDotAttribute(StringBuilder sb, boolean prependComma, S if (prependComma) sb.append(", "); - sb.append(name).append("=\"").append(value).append("\""); + // Escape backslashes first, then quotes, to produce a valid DOT quoted string + String escaped = value.replace("\\", "\\\\").replace("\"", "\\\""); + sb.append(name).append("=\"").append(escaped).append("\""); } public static void exportGroupMembers(Group group, List memberGroups, List memberUsers, GroupType xmlGroupType) diff --git a/core/src/org/labkey/core/CoreController.java b/core/src/org/labkey/core/CoreController.java index d80b090fa49..acaf441e3aa 100644 --- a/core/src/org/labkey/core/CoreController.java +++ b/core/src/org/labkey/core/CoreController.java @@ -487,7 +487,7 @@ else if (form.getSchemaName() != null && form.getQueryName() != null && form.get { throw new NotFoundException("The file '" + file.getName() + "' attached to the object '" + identifiable.getName() + "' cannot be found. It may have been deleted."); } - throw new NotFoundException("File " + file.getPath() + " does not exist on the server file system. It may have been deleted."); + throw new NotFoundException("File " + file.getName() + " does not exist on the server file system. It may have been deleted."); } if (file.isDirectory()) @@ -654,6 +654,7 @@ private static byte[] compressCSS(String s) catch (StackOverflowError e) { // replaceAll() can blow up + _log.error("StackOverflowError compressing CSS"); } return Compress.compressGzip(c.trim()); } @@ -935,6 +936,11 @@ public void validateForm(SimpleApiJsonForm form, Errors errors) errors.reject(ERROR_MSG, "The container '" + parentIdentifier + "' is not a valid parent folder."); return; } + + if (!target.hasPermission(getUser(), AdminPermission.class)) + { + throw new UnauthorizedException("You must be an administrator for the target container"); + } } @Override @@ -2390,6 +2396,10 @@ public Object execute(Object o, BindException errors) } } + /** + * This action doesn't require any permissions, as the call to WarningService.getWarnings() + * only returns warnings appropriate for the user/guest + */ @RequiresNoPermission @AllowedDuringUpgrade public static class DisplayWarningsAction extends MutatingApiAction @@ -2721,7 +2731,7 @@ public void setToFormat(String toFormat) } @SuppressWarnings("unused") // Called from JavaScript: discuss.js, wikiEdit.js - @RequiresNoPermission + @RequiresPermission(ReadPermission.class) public static class TransformWikiAction extends MutatingApiAction { @Override diff --git a/core/src/org/labkey/core/admin/AdminController.java b/core/src/org/labkey/core/admin/AdminController.java index 61764f3c6b1..da74ae70ea1 100644 --- a/core/src/org/labkey/core/admin/AdminController.java +++ b/core/src/org/labkey/core/admin/AdminController.java @@ -302,7 +302,6 @@ import org.labkey.api.view.NavTree; import org.labkey.api.view.NotFoundException; import org.labkey.api.view.Portal; -import org.labkey.api.view.RedirectException; import org.labkey.api.view.ShortURLRecord; import org.labkey.api.view.ShortURLService; import org.labkey.api.view.TabStripView; @@ -314,6 +313,7 @@ import org.labkey.api.view.ViewServlet; import org.labkey.api.view.WebPartView; import org.labkey.api.view.template.EmptyView; +import org.labkey.api.view.template.ClientDependency; import org.labkey.api.view.template.PageConfig; import org.labkey.api.view.template.PageConfig.Template; import org.labkey.api.wiki.WikiRendererType; @@ -415,9 +415,11 @@ import static org.labkey.api.util.DOM.Attribute.title; import static org.labkey.api.util.DOM.Attribute.type; import static org.labkey.api.util.DOM.Attribute.value; +import static org.labkey.api.util.DOM.B; import static org.labkey.api.util.DOM.BR; import static org.labkey.api.util.DOM.DIV; import static org.labkey.api.util.DOM.LI; +import static org.labkey.api.util.DOM.P; import static org.labkey.api.util.DOM.SPAN; import static org.labkey.api.util.DOM.STYLE; import static org.labkey.api.util.DOM.TABLE; @@ -1657,7 +1659,7 @@ public static class SiteValidationForm { private List _providers; private boolean _includeSubfolders = false; - private transient Consumer _logger = s -> { + private transient Consumer _logger = _ -> { }; // No-op by default public List getProviders() @@ -2690,23 +2692,41 @@ public PostgresTableSizesAction() } @AdminConsoleAction - public class DumpHeapAction extends SimpleViewAction + public class DumpHeapAction extends ConfirmAction { + private File _destination; + @Override - public ModelAndView getView(Object o, BindException errors) throws Exception + public ModelAndView getConfirmView(Object o, BindException errors) { - File destination = DebugInfoDumper.dumpHeap(); - return new HtmlView(HtmlString.of("Heap dumped to " + destination.getAbsolutePath())); + setTitle("Dump Heap"); + return HtmlView.of("Are you sure you want to dump the JVM heap to disk? This may temporarily slow the server and will consume significant disk space."); } @Override - public void addNavTrail(NavTree root) + public boolean handlePost(Object o, BindException errors) throws Exception { - setHelpTopic("dumpHeap"); - addAdminNavTrail(root, "Heap dump", getClass()); + _destination = DebugInfoDumper.dumpHeap(); + return true; } - } + @Override + public ModelAndView getSuccessView(Object o) + { + return new HtmlView(HtmlString.of("Heap dumped to " + _destination.getAbsolutePath())); + } + + @Override + public void validateCommand(Object o, Errors errors) + { + } + + @Override + public @NotNull URLHelper getSuccessURL(Object o) + { + return getShowAdminURL(); + } + } public static class ThreadsBean { @@ -3189,32 +3209,10 @@ public class CachesAction extends SimpleViewAction @Override public ModelAndView getView(MemForm form, BindException errors) { - if (form.isClearCaches()) - { - LOG.info("Clearing Introspector caches"); - Introspector.flushCaches(); - LOG.info("Purging all caches"); - CacheManager.clearAllKnownCaches(); - ActionURL redirect = getViewContext().cloneActionURL().deleteParameter("clearCaches"); - throw new RedirectException(redirect); - } + getPageConfig().addClientDependency(ClientDependency.fromPath("admin/caches.js")); List> caches = CacheManager.getKnownCaches(); - if (form.getDebugName() != null) - { - for (TrackingCache cache : caches) - { - if (form.getDebugName().equals(cache.getDebugName())) - { - LOG.info("Purging cache: " + cache.getDebugName()); - cache.clear(); - } - } - ActionURL redirect = getViewContext().cloneActionURL().deleteParameter("debugName"); - throw new RedirectException(redirect); - } - List cacheStats = new ArrayList<>(); List transactionStats = new ArrayList<>(); @@ -3226,13 +3224,13 @@ public ModelAndView getView(MemForm form, BindException errors) HtmlStringBuilder html = HtmlStringBuilder.of(); - html.append(LinkBuilder.labkeyLink("Clear Caches and Refresh", getCachesURL(true, false))); + html.append(createHtmlFragment(A(cl("labkey-text-link").at(href, "#").id("clearAllCaches"), "Clear Caches and Refresh"))); html.append(LinkBuilder.labkeyLink("Refresh", getCachesURL(false, false))); - html.unsafeAppend("

\n"); + html.append(createHtmlFragment(BR(), BR())); appendStats(html, "Caches", cacheStats, false); - html.unsafeAppend("

\n"); + html.append(createHtmlFragment(BR(), BR())); appendStats(html, "Transaction Caches", transactionStats, true); return new HtmlView(html); @@ -3242,79 +3240,65 @@ private void appendStats(HtmlStringBuilder html, String title, List { List stats = skipUnusedCaches ? allStats.stream() - .filter(stat->stat.getMaxSize() > 0) + .filter(stat -> stat.getMaxSize() > 0) .collect(Collectors.toCollection((Supplier>) ArrayList::new)) : allStats; Collections.sort(stats); - html.unsafeAppend("

"); - html.append(title); - html.append(" (").append(stats.size()).unsafeAppend(")

\n"); - - html.unsafeAppend("\n"); - html.unsafeAppend(""); - html.unsafeAppend(""); - html.unsafeAppend(""); - html.unsafeAppend(""); - html.unsafeAppend(""); - html.unsafeAppend(""); - html.unsafeAppend(""); - html.unsafeAppend(""); - html.unsafeAppend(""); - html.unsafeAppend(""); - html.unsafeAppend(""); - html.unsafeAppend(""); - html.unsafeAppend(""); - - long size = 0; - long gets = 0; - long misses = 0; - long puts = 0; - long expirations = 0; - long evictions = 0; - long removes = 0; - long clears = 0; - int rowCount = 0; - - for (CacheStats stat : stats) - { - size += stat.getSize(); - gets += stat.getGets(); - misses += stat.getMisses(); - puts += stat.getPuts(); - expirations += stat.getExpirations(); - evictions += stat.getEvictions(); - removes += stat.getRemoves(); - clears += stat.getClears(); - - html.unsafeAppend(""); - - appendDescription(html, stat.getDescription(), stat.getCreationStackTrace()); - - Long limit = stat.getLimit(); - long maxSize = stat.getMaxSize(); - - appendLongs(html, limit, maxSize, stat.getSize(), stat.getGets(), stat.getMisses(), stat.getPuts(), stat.getExpirations(), stat.getEvictions(), stat.getRemoves(), stat.getClears()); - appendDoubles(html, stat.getMissRatio()); - - html.unsafeAppend("\n"); - - if (null != limit && maxSize >= limit) - html.unsafeAppend(""); - - html.unsafeAppend("\n"); - rowCount++; - } - - double ratio = 0 != gets ? misses / (double)gets : 0; - html.unsafeAppend(""); - - appendLongs(html, null, null, size, gets, misses, puts, expirations, evictions, removes, clears); - appendDoubles(html, ratio); - - html.unsafeAppend("\n"); - html.unsafeAppend("
Debug NameLimitMax SizeCurrent SizeGetsMissesPutsExpirationsEvictionsRemovesClearsMiss PercentageClear
").append(LinkBuilder.labkeyLink("Clear", getCacheURL(stat.getDescription()))).unsafeAppend("This cache has been limited
Total
\n"); + long size = stats.stream().mapToLong(CacheStats::getSize).sum(); + long gets = stats.stream().mapToLong(CacheStats::getGets).sum(); + long misses = stats.stream().mapToLong(CacheStats::getMisses).sum(); + long puts = stats.stream().mapToLong(CacheStats::getPuts).sum(); + long expirations = stats.stream().mapToLong(CacheStats::getExpirations).sum(); + long evictions = stats.stream().mapToLong(CacheStats::getEvictions).sum(); + long removes = stats.stream().mapToLong(CacheStats::getRemoves).sum(); + long clears = stats.stream().mapToLong(CacheStats::getClears).sum(); + double ratio = gets != 0 ? misses / (double) gets : 0; + + AtomicInteger rowCount = new AtomicInteger(); + html.append(createHtmlFragment( + P(B(title + " (" + stats.size() + ")")), + TABLE(cl("labkey-data-region-legacy", "labkey-show-borders", "labkey-data-region-header-lock"), + TR( + TD(cl("labkey-column-header"), "Debug Name"), + TD(cl("labkey-column-header"), "Limit"), + TD(cl("labkey-column-header"), "Max Size"), + TD(cl("labkey-column-header"), "Current Size"), + TD(cl("labkey-column-header"), "Gets"), + TD(cl("labkey-column-header"), "Misses"), + TD(cl("labkey-column-header"), "Puts"), + TD(cl("labkey-column-header"), "Expirations"), + TD(cl("labkey-column-header"), "Evictions"), + TD(cl("labkey-column-header"), "Removes"), + TD(cl("labkey-column-header"), "Clears"), + TD(cl("labkey-column-header"), "Miss Percentage"), + TD(cl("labkey-column-header"), "Clear") + ), + stats.stream().map(stat -> { + Long limit = stat.getLimit(); + long maxSize = stat.getMaxSize(); + + String clearId = "clear_" + UniqueID.getServerSessionScopedUID(); + HttpView.currentPageConfig().addHandler(clearId, "click", + "LABKEY.Admin.Caches.clearSingle(" + PageFlowUtil.jsString(stat.getDescription()) + "); return false;"); + + return TR(cl(rowCount.getAndIncrement() % 2 == 0 ? "labkey-alternate-row" : "labkey-row"), + descriptionCell(stat.getDescription(), stat.getCreationStackTrace()), + longCells(limit, maxSize, stat.getSize(), stat.getGets(), stat.getMisses(), stat.getPuts(), stat.getExpirations(), stat.getEvictions(), stat.getRemoves(), stat.getClears()), + doubleCell(stat.getMissRatio()), + TD(A(cl("labkey-text-link").at(href, "#").id(clearId), "Clear")), + null != limit && maxSize >= limit ? TD(SPAN(cl("labkey-error"), "This cache has been limited")) : null + ); + }), + TR(cl("labkey-row"), + TD(B("Total")), + longCells(null, null, size, gets, misses, puts, expirations, evictions, removes, clears), + doubleCell(ratio) + ) + ) + ) + ); } private static final List PREFIXES_TO_SKIP = List.of( @@ -3325,7 +3309,8 @@ private void appendStats(HtmlStringBuilder html, String title, List "org.labkey.api.module.ModuleResourceCache" ); - private void appendDescription(HtmlStringBuilder html, String description, @Nullable StackTraceElement[] creationStackTrace) + @Nullable + private Renderable descriptionCell(String description, @Nullable StackTraceElement[] creationStackTrace) { StringBuilder sb = new StringBuilder(); @@ -3337,7 +3322,7 @@ private void appendDescription(HtmlStringBuilder html, String description, @Null // Skip the first few uninteresting stack trace elements to highlight the caller we care about if (trimming) { - if (PREFIXES_TO_SKIP.stream().anyMatch(prefix->element.toString().startsWith(prefix))) + if (PREFIXES_TO_SKIP.stream().anyMatch(prefix -> element.toString().startsWith(prefix))) continue; trimming = false; @@ -3347,30 +3332,31 @@ private void appendDescription(HtmlStringBuilder html, String description, @Null } } - if (!sb.isEmpty()) - { - String message = PageFlowUtil.jsString(sb); - String id = "id" + UniqueID.getServerSessionScopedUID(); - html.append(DOM.createHtmlFragment(TD(A(at(href, "#").id(id), description)))); - HttpView.currentPageConfig().addHandler(id, "click", "alert(" + message + ");return false;"); - } + if (sb.isEmpty()) + return TD(description); + + String message = PageFlowUtil.jsString(sb); + String id = "id" + UniqueID.getServerSessionScopedUID(); + HttpView.currentPageConfig().addHandler(id, "click", "alert(" + message + ");return false;"); + return TD(A(at(href, "#").id(id), description)); } - private void appendLongs(HtmlStringBuilder html, Long... stats) + private List longCells(Long... stats) { + List cells = new ArrayList<>(); for (Long stat : stats) { if (null == stat) - html.unsafeAppend(" "); + cells.add(TD(NBSP)); else - html.unsafeAppend("").append(commaf0.format(stat)).unsafeAppend(""); + cells.add(TD(at(style, "text-align:right"), commaf0.format(stat))); } + return cells; } - private void appendDoubles(HtmlStringBuilder html, double... stats) + private Renderable doubleCell(double stat) { - for (double stat : stats) - html.unsafeAppend("").append(percent.format(stat)).unsafeAppend(""); + return TD(at(style, "text-align:right"), percent.format(stat)); } @Override @@ -3381,6 +3367,60 @@ public void addNavTrail(NavTree root) } } + @AdminConsoleAction + public static class ClearCachesAction extends MutatingApiAction + { + @Override + public ApiResponse execute(MemForm form, BindException errors) + { + if (form.getDebugName() != null) + { + for (TrackingCache cache : CacheManager.getKnownCaches()) + { + if (form.getDebugName().equals(cache.getDebugName())) + { + LOG.info("Purging cache: " + cache.getDebugName()); + cache.clear(); + } + } + } + else if (form.isClearCaches() && form.isGc()) + { + long before = doGc(); + doClearCaches(); + long cacheMemoryUsed = before - doGc(); + String cacheMemUsed = cacheMemoryUsed > 0 ? FileUtils.byteCountToDisplaySize(cacheMemoryUsed) : "Unknown"; + LOG.info("Estimate of cache memory used: " + cacheMemUsed); + lastCacheMemUsed = cacheMemUsed; + } + else if (form.isClearCaches()) + { + doClearCaches(); + } + else if (form.isGc()) + { + doGc(); + } + return new ApiSimpleResponse("success", true); + } + + private long doGc() + { + LOG.info("Garbage collecting"); + System.gc(); + return Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); + } + + private void doClearCaches() + { + LOG.info("Clearing Introspector caches"); + Introspector.flushCaches(); + LOG.info("Purging all caches"); + CacheManager.clearAllKnownCaches(); + SearchService.get().purgeQueues(); + } + } + @RequiresSiteAdmin public class EnvironmentVariablesAction extends SimpleViewAction { @@ -3725,15 +3765,32 @@ public static ActionURL getCachesURL(boolean clearCaches, boolean gc) return url; } - public static ActionURL getCacheURL(String debugName) + public static ActionURL getClearCachesURL(boolean clearCaches, boolean gc) { - ActionURL url = new ActionURL(CachesAction.class, ContainerManager.getRoot()); + ActionURL url = new ActionURL(ClearCachesAction.class, ContainerManager.getRoot()); + + if (clearCaches) + url.addParameter(MemForm.Params.clearCaches, "1"); - url.addParameter(MemForm.Params.debugName, debugName); + if (gc) + url.addParameter(MemForm.Params.gc, "1"); return url; } + public static ActionURL getClearCacheURL(String debugName) + { + return new ActionURL(ClearCachesAction.class, ContainerManager.getRoot()) + .addParameter(MemForm.Params.debugName, debugName); + } + + /** @deprecated Use {@link #getClearCacheURL(String)} — individual cache clearing now requires a POST confirmation */ + @Deprecated + public static ActionURL getCacheURL(String debugName) + { + return getClearCacheURL(debugName); + } + private static volatile String lastCacheMemUsed = null; @AdminConsoleAction @@ -3743,60 +3800,9 @@ public class MemTrackerAction extends SimpleViewAction public ModelAndView getView(MemForm form, BindException errors) { Set objectsToIgnore = MemTracker.getInstance().beforeReport(); - - boolean gc = form.isGc(); - boolean cc = form.isClearCaches(); - - if (getUser().hasRootAdminPermission() && (gc || cc)) - { - // If both are requested then try to determine and record cache memory usage - if (gc && cc) - { - // gc once to get an accurate free memory read - long before = gc(); - clearCaches(); - // gc again now that we cleared caches - long cacheMemoryUsed = before - gc(); - - // Difference could be < 0 if JVM or other threads have performed gc, in which case we can't guesstimate cache memory usage - String cacheMemUsed = cacheMemoryUsed > 0 ? FileUtils.byteCountToDisplaySize(cacheMemoryUsed) : "Unknown"; - LOG.info("Estimate of cache memory used: " + cacheMemUsed); - lastCacheMemUsed = cacheMemUsed; - } - else if (cc) - { - clearCaches(); - } - else - { - gc(); - } - - LOG.info("Cache clearing and garbage collecting complete"); - } - return new JspView<>("/org/labkey/core/admin/memTracker.jsp", new MemBean(getViewContext().getRequest(), objectsToIgnore)); } - /** @return estimated current memory usage, post-garbage collection */ - private long gc() - { - LOG.info("Garbage collecting"); - System.gc(); - // This is more reliable than relying on just free memory size, as the VM can grow/shrink the heap at will - return Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); - } - - private void clearCaches() - { - LOG.info("Clearing Introspector caches"); - Introspector.flushCaches(); - LOG.info("Purging all caches"); - CacheManager.clearAllKnownCaches(); - LOG.info("Purging SearchService queues"); - SearchService.get().purgeQueues(); - } - @Override public void addNavTrail(NavTree root) { @@ -3861,12 +3867,12 @@ private MemBean(HttpServletRequest request, Set objectsToIgnore) { MemTracker memTracker = MemTracker.getInstance(); List all = memTracker.getReferences(); - long threadId = Thread.currentThread().getId(); + long threadId = Thread.currentThread().threadId(); // Attempt to detect other threads running labkey code -- mem tracker page will warn if any are found for (Thread thread : new ThreadsBean().threads) { - if (thread.getId() == threadId) + if (thread.threadId() == threadId) continue; Thread.State state = thread.getState(); @@ -4963,7 +4969,7 @@ public void setOverrideDefault(String overrideDefault) public static class RConfigurationAction extends FolderManagementViewPostAction { @Override - protected HttpView getTabView(RConfigForm form, boolean reshow, BindException errors) + protected HttpView getTabView(RConfigForm form, boolean reshow, BindException errors) { return new JspView<>("/org/labkey/core/admin/rConfiguration.jsp", form, errors); } @@ -5396,7 +5402,7 @@ public class ImportFolderAction extends FolderManagementViewPostAction getTabView(ImportFolderForm form, boolean reshow, BindException errors) { // default the createSharedDatasets and validateQueries to true if this is not a form error reshow if (!errors.hasErrors()) @@ -5427,21 +5433,12 @@ public boolean handlePost(ImportFolderForm form, BindException errors) throws Ex Container container = getContainer(); PipeRoot pipelineRoot; FileLike pipelineUnzipDir; // Should be local & writable - PipelineUrls pipelineUrlProvider; if (form.getOrigin() == null) { form.setOrigin("Folder"); } - // make sure we have a pipeline url provider to use for the success URL redirect - pipelineUrlProvider = urlProvider(PipelineUrls.class); - if (pipelineUrlProvider == null) - { - errors.reject("folderImport", "Pipeline url provider does not exist."); - return false; - } - // make sure that the pipeline root is valid for this container pipelineRoot = PipelineService.get().findPipelineRoot(container); if (!PipelineService.get().hasValidPipelineRoot(container) || pipelineRoot == null) @@ -5490,7 +5487,7 @@ public boolean handlePost(ImportFolderForm form, BindException errors) throws Ex options.setActivity(ComplianceService.get().getCurrentActivity(getViewContext())); // finally, create the study or folder import pipeline job - _successURL = pipelineUrlProvider.urlBegin(container); + _successURL = urlProvider(PipelineUrls.class).urlBegin(container); PipelineService.get().runFolderImportJob(container, user, url, archiveXml, fiConfig.originalFileName, pipelineRoot, options); return !errors.hasErrors(); @@ -10269,6 +10266,10 @@ public ApiResponse execute(DeletedFoldersForm form, BindException errors) if (isBlank(form.getContainerPath())) throw new NotFoundException(); Container container = ContainerManager.getForPath(form.getContainerPath()); + if (!container.hasPermission(getUser(), AdminPermission.class)) + { + throw new UnauthorizedException(); + } for (String tabName : form.getResurrectFolders()) { ContainerManager.clearContainerTabDeleted(container, tabName, form.getNewFolderType()); @@ -11640,15 +11641,15 @@ private static boolean saveProjectSettings(Container c, User user, ProjectSettin } }); - boolean noErrors = !saveFolderSettings(c, user, props, form, errors); + boolean success = saveFolderSettings(c, user, props, form, errors); - if (noErrors) + if (success) { // Bump the look & feel revision so browsers retrieve the new theme stylesheet WriteableAppProps.incrementLookAndFeelRevisionAndSave(); } - return noErrors; + return success; } private static void setProperty(boolean inherited, Runnable clear, Runnable set) @@ -11889,7 +11890,7 @@ public static class LookAndFeelBean public final HtmlString customColumnRestrictionHelpLink = new HelpTopic("chartTrouble").getSimpleLinkHtml("more info..."); } - @RequiresPermission(AdminPermission.class) + @RequiresPermission(SiteAdminPermission.class) public static class AdjustSystemTimestampsAction extends FormViewAction { @Override diff --git a/core/src/org/labkey/core/admin/memTracker.jsp b/core/src/org/labkey/core/admin/memTracker.jsp index 9d70d5abf14..70f32873aae 100644 --- a/core/src/org/labkey/core/admin/memTracker.jsp +++ b/core/src/org/labkey/core/admin/memTracker.jsp @@ -26,8 +26,16 @@ <%@ page import="org.labkey.api.view.JspView" %> <%@ page import="org.labkey.core.admin.AdminController" %> <%@ page import="org.labkey.core.admin.AdminController.MemBean" %> +<%@ page import="org.labkey.api.view.template.ClientDependencies" %> <%@ page import="java.text.DecimalFormat" %> <%@ page extends="org.labkey.api.jsp.JspBase" %> +<%! + @Override + public void addClientDependencies(ClientDependencies dependencies) + { + dependencies.add("admin/caches.js"); + } +%> <% JspView me = HttpView.currentView(); MemBean bean = me.getModelBean(); @@ -45,8 +53,8 @@ %> <% if (hasAdminPerm) { %>

- <%=link("Clear Caches, GC and Refresh", AdminController.getMemTrackerURL(true, true))%> - <%=link("GC and Refresh", AdminController.getMemTrackerURL(false, true))%> + Clear Caches, GC and Refresh + GC and Refresh <%=link("Refresh", AdminController.getMemTrackerURL(false, false))%> <% if (getUser().hasSiteAdminPermission()) { %> <%=link("Memory Stress Test", new ActionURL(AdminController.MemoryStressTestAction.class, ContainerManager.getRoot()))%> <% } %>

diff --git a/core/webapp/admin/caches.js b/core/webapp/admin/caches.js new file mode 100644 index 00000000000..d4b34243592 --- /dev/null +++ b/core/webapp/admin/caches.js @@ -0,0 +1,40 @@ +LABKEY.Admin = LABKEY.Admin || {}; + +LABKEY.Admin.Caches = new function() { + var API_URL = LABKEY.ActionURL.buildURL('admin', 'clearCaches'); + + function doPost(params) { + LABKEY.Ajax.request({ + url: API_URL, + method: 'POST', + params: params, + success: reloadPage, + failure: LABKEY.Utils.getCallbackWrapper(null, null, true) + }); + } + + function reloadPage() { window.location.reload(); } + + // clearSingle is called from inline onclick handlers (addHandler), so must be defined immediately. + this.clearSingle = function(debugName) { + doPost({ debugName: debugName }); + }; + + // Bind button handlers once the DOM is ready — elements don't exist yet when this script runs. + window.addEventListener('DOMContentLoaded', function() { + function bindIfPresent(id, params) { + var el = document.getElementById(id); + if (el) { + el.addEventListener('click', function(e) { + e.preventDefault(); + el.disabled = true; + doPost(params); + }); + } + } + + bindIfPresent('clearAllCaches', { clearCaches: true }); + bindIfPresent('clearCachesGc', { clearCaches: true, gc: true }); + bindIfPresent('gcOnly', { gc: true }); + }); +}; diff --git a/study/src/org/labkey/study/controllers/security/SecurityController.java b/study/src/org/labkey/study/controllers/security/SecurityController.java index 0785f7377f2..643d2ae92ed 100644 --- a/study/src/org/labkey/study/controllers/security/SecurityController.java +++ b/study/src/org/labkey/study/controllers/security/SecurityController.java @@ -469,7 +469,7 @@ public List getTabList() } @Override - public HttpView getTabView(String tabId) + public HttpView getTabView(String tabId) { if (TAB_STUDY.equals(tabId)) { @@ -559,9 +559,7 @@ public void addNavTrail(NavTree root) if (getContainer().hasPermission(getUser(), AdminPermission.class)) root.addChild("Manage Views", urlProvider(ReportUrls.class).urlManageViews(getContainer()).getLocalURIString()); } - catch (Exception e) - { - } + catch (Exception _) {} root.addChild("Report and View Permissions"); } } @@ -701,7 +699,7 @@ public void setRemove(Integer remove) public int getAdd() { - return add == null ? 0 : remove.intValue(); + return add == null ? 0 : add.intValue(); } public void setAdd(Integer add) @@ -730,9 +728,9 @@ public String getTabId() } } - static class Overview extends WebPartView + static class Overview extends WebPartView { - private final HttpView _vbox; + private final HttpView _vbox; Overview(StudyImpl study) { @@ -773,7 +771,7 @@ static class Overview extends WebPartView @Override - protected void renderView(Object model, HtmlWriter out) throws Exception + protected void renderView(StudyImpl model, HtmlWriter out) throws Exception { include(_vbox, out.unwrap()); } @@ -783,7 +781,7 @@ protected void renderView(Object model, HtmlWriter out) throws Exception public static class StudySecurityViewFactory implements SecurityManager.ViewFactory { @Override - public HttpView createView(ViewContext context) + public StudySecurityPermissionsView createView(ViewContext context) { if (null != BaseStudyController.getStudy(context.getContainer())) return new StudySecurityPermissionsView();