From f36759b2c5fbda33139d8048410377f872af2c75 Mon Sep 17 00:00:00 2001 From: vagisha Date: Sun, 8 Oct 2023 10:13:05 -0700 Subject: [PATCH 1/2] Display short URLs in a LabKey grid. --- .../labkey/core/admin/AdminController.java | 82 ++++++---- .../labkey/core/admin/existingShortURLs.jsp | 60 -------- .../org/labkey/core/admin/updateShortURL.jsp | 37 +++++ .../labkey/core/query/CoreQuerySchema.java | 4 + .../labkey/core/query/ShortUrlTableInfo.java | 145 ++++++++++++++++++ 5 files changed, 242 insertions(+), 86 deletions(-) delete mode 100644 core/src/org/labkey/core/admin/existingShortURLs.jsp create mode 100644 core/src/org/labkey/core/admin/updateShortURL.jsp create mode 100644 core/src/org/labkey/core/query/ShortUrlTableInfo.java diff --git a/core/src/org/labkey/core/admin/AdminController.java b/core/src/org/labkey/core/admin/AdminController.java index ce12f8388f7..8e52b3f3011 100644 --- a/core/src/org/labkey/core/admin/AdminController.java +++ b/core/src/org/labkey/core/admin/AdminController.java @@ -9463,39 +9463,14 @@ public boolean isDelete() { return _delete; } - - public List getSavedShortURLs() - { - return _savedShortURLs; - } - - public void setSavedShortURLs(List savedShortURLs) - { - _savedShortURLs = savedShortURLs; - } } - @AdminConsoleAction @RequiresPermission(AdminPermission.class) - public class ShortURLAdminAction extends FormViewAction + public abstract class AbstractShortURLAdminAction extends FormViewAction { @Override public void validateCommand(ShortURLForm target, Errors errors) {} - @Override - public ModelAndView getView(ShortURLForm form, boolean reshow, BindException errors) - { - form.setSavedShortURLs(ShortURLService.get().getAllShortURLs()); - JspView newView = new JspView<>("/org/labkey/core/admin/createNewShortURL.jsp", form, errors); - newView.setTitle("Create New Short URL"); - newView.setFrame(WebPartView.FrameType.PORTAL); - JspView existingView = new JspView<>("/org/labkey/core/admin/existingShortURLs.jsp", form, errors); - existingView.setTitle("Existing Short URLs"); - existingView.setFrame(WebPartView.FrameType.PORTAL); - - return new VBox(newView, existingView); - } - @Override public boolean handlePost(ShortURLForm form, BindException errors) throws Exception { @@ -9572,6 +9547,27 @@ public boolean handlePost(ShortURLForm form, BindException errors) throws Except } return true; } + } + + @AdminConsoleAction + @RequiresPermission(AdminPermission.class) + public class ShortURLAdminAction extends AbstractShortURLAdminAction + { + @Override + public ModelAndView getView(ShortURLForm form, boolean reshow, BindException errors) + { + JspView newView = new JspView<>("/org/labkey/core/admin/createNewShortURL.jsp", form, errors); + newView.setTitle("Create New Short URL"); + newView.setFrame(WebPartView.FrameType.PORTAL); + + QuerySettings qSettings = new QuerySettings(getViewContext(), "ShortURL", "ShortURL"); + qSettings.setBaseSort(new Sort("-Created")); + QueryView existingView = new QueryView(new CoreQuerySchema(getUser(), getContainer()), qSettings, errors); + existingView.setTitle("Existing Short URLs"); + existingView.setFrame(WebPartView.FrameType.PORTAL); + + return new VBox(newView, existingView); + } @Override public URLHelper getSuccessURL(ShortURLForm form) @@ -9587,6 +9583,40 @@ public void addNavTrail(NavTree root) } } + @RequiresPermission(AdminOperationsPermission.class) + public class UpdateShortURLAction extends AbstractShortURLAdminAction + { + @Override + public ModelAndView getView(ShortURLForm form, boolean reshow, BindException errors) + { + var shortUrlRecord = ShortURLService.get().resolveShortURL(form.getShortURL()); + if (shortUrlRecord == null) + { + errors.addError(new LabKeyError("Short URL does not exist: " + form.getShortURL())); + return new SimpleErrorView(errors); + } + form.setFullURL(shortUrlRecord.getFullURL()); + + JspView view = new JspView<>("/org/labkey/core/admin/updateShortURL.jsp", form, errors); + view.setTitle("Update Short URL"); + view.setFrame(WebPartView.FrameType.PORTAL); + return view; + } + + @Override + public URLHelper getSuccessURL(ShortURLForm form) + { + return new ActionURL(ShortURLAdminAction.class, getContainer()); + } + + @Override + public void addNavTrail(NavTree root) + { + setHelpTopic("shortURL"); + addAdminNavTrail(root, "Update Short URL", getClass()); + } + } + // API for reporting client-side exceptions. // UNDONE: Throttle by IP to avoid DOS from buggy clients. @Marshal(Marshaller.Jackson) diff --git a/core/src/org/labkey/core/admin/existingShortURLs.jsp b/core/src/org/labkey/core/admin/existingShortURLs.jsp deleted file mode 100644 index 0a5a9b4a773..00000000000 --- a/core/src/org/labkey/core/admin/existingShortURLs.jsp +++ /dev/null @@ -1,60 +0,0 @@ -<% -/* - * Copyright (c) 2014-2019 LabKey Corporation - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -%> -<%@ page import="org.labkey.api.view.HttpView" %> -<%@ page import="org.labkey.api.view.ShortURLRecord" %> -<%@ page import="org.labkey.api.view.template.ClientDependencies" %> -<%@ page import="org.labkey.core.admin.AdminController" %> -<%@ page import="java.util.Collections" %> -<%@ page extends="org.labkey.api.jsp.JspBase" %> -<%@ taglib prefix="labkey" uri="http://www.labkey.org/taglib" %> -<%! - @Override - public void addClientDependencies(ClientDependencies dependencies) - { - dependencies.add("internal/clipboard/clipboard-1.5.9.min.js"); - } -%> - -<% - AdminController.ShortURLForm bean = (AdminController.ShortURLForm) HttpView.currentModel(); -%> - - - - - - - - - - <% if (bean.getSavedShortURLs().isEmpty()) { %> <% } %> - <% int index = 0; - for (ShortURLRecord shortURLRecord : bean.getSavedShortURLs()) { - index++; %> - - - - - - - - <% } %> -
Short URLTest LinkTarget URL
No short URLs have been configured.
<%= h(shortURLRecord.getShortURL())%><%= link("test").href(shortURLRecord.renderShortURL()) %> - <%= link("copy to clipboard").onClick("return false;").id("copyToClipboardId" + index).attributes(Collections.singletonMap("data-clipboard-text", shortURLRecord.renderShortURL())) %> - - <%= button("Update").submit(true) %><%= button("Delete").submit(true) %>
diff --git a/core/src/org/labkey/core/admin/updateShortURL.jsp b/core/src/org/labkey/core/admin/updateShortURL.jsp new file mode 100644 index 00000000000..549e562a46d --- /dev/null +++ b/core/src/org/labkey/core/admin/updateShortURL.jsp @@ -0,0 +1,37 @@ +<%@ page import="org.labkey.api.view.HttpView" %> +<%@ page import="org.labkey.core.admin.AdminController" %> +<%@ page import="org.labkey.api.view.ActionURL" %> +<%@ page extends="org.labkey.api.jsp.JspBase" %> +<%@ taglib prefix="labkey" uri="http://www.labkey.org/taglib" %> + + + +<% + AdminController.ShortURLForm bean = (AdminController.ShortURLForm) HttpView.currentModel(); +%> + + + + + + + + + + + +
Short URL: + + <%= h(bean.getShortURL()) %> +
Target URL:
+
+ <%= button("Update").submit(true) %> + <%= button("Cancel").href(new ActionURL(AdminController.ShortURLAdminAction.class, getContainer())) %> +
+
+ +
+ <%= button("Delete") + .usePost("Are you sure you want to delete the short URL " + bean.getShortURL() + "?") + .href(urlFor(AdminController.UpdateShortURLAction.class).addParameter("shortURL", bean.getShortURL()).addParameter("delete", true)) %> +
\ No newline at end of file diff --git a/core/src/org/labkey/core/query/CoreQuerySchema.java b/core/src/org/labkey/core/query/CoreQuerySchema.java index a3626441339..f1002d1ab68 100644 --- a/core/src/org/labkey/core/query/CoreQuerySchema.java +++ b/core/src/org/labkey/core/query/CoreQuerySchema.java @@ -93,6 +93,7 @@ public class CoreQuerySchema extends UserSchema public static final String USERS_MSG_SETTINGS_TABLE_NAME = "UsersMsgPrefs"; public static final String SCHEMA_DESCR = "Contains data about the system users and groups."; public static final String VIEW_CATEGORY_TABLE_NAME = "ViewCategory"; + public static final String SHORTURL_TABLE_NAME = "ShortURL"; public CoreQuerySchema(User user, Container c) { @@ -177,6 +178,9 @@ public TableInfo createTable(String name, ContainerFilter cf) return new ViewCategoryTable(ViewCategoryManager.getInstance().getTableInfoCategories(), this, cf); if (MISSING_VALUE_INDICATOR_TABLE_NAME.equalsIgnoreCase(name)) return getMVIndicatorTable(cf); + if (SHORTURL_TABLE_NAME.equalsIgnoreCase(name)) + return new ShortUrlTableInfo(this); + return null; } diff --git a/core/src/org/labkey/core/query/ShortUrlTableInfo.java b/core/src/org/labkey/core/query/ShortUrlTableInfo.java new file mode 100644 index 00000000000..32f18f2e9ce --- /dev/null +++ b/core/src/org/labkey/core/query/ShortUrlTableInfo.java @@ -0,0 +1,145 @@ +package org.labkey.core.query; + +import org.jetbrains.annotations.NotNull; +import org.labkey.api.data.ColumnInfo; +import org.labkey.api.data.CoreSchema; +import org.labkey.api.data.DataColumn; +import org.labkey.api.data.DisplayColumn; +import org.labkey.api.data.DisplayColumnFactory; +import org.labkey.api.data.RenderContext; +import org.labkey.api.query.FieldKey; +import org.labkey.api.query.FilteredTable; +import org.labkey.api.util.DOM; +import org.labkey.api.util.HtmlString; +import org.labkey.api.util.PageFlowUtil; +import org.labkey.api.view.ActionURL; +import org.labkey.api.view.ShortURLRecord; +import org.labkey.api.view.template.ClientDependency; +import org.labkey.core.admin.AdminController; + +import java.io.IOException; +import java.io.Writer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +public class ShortUrlTableInfo extends FilteredTable +{ + private static final String SHORT_URL_COL = "shorturl"; + private static final String FULL_URL_COL = "fullurl"; + private static final String ROWID_COL = "rowid"; + private static final String UPDATE_SHORT_URL_COL = "UpdateShortUrl"; + private static final String TO_CLIPBOARD_COL = "CopyToClipboard"; + + public ShortUrlTableInfo(@NotNull CoreQuerySchema userSchema) + { + super(CoreSchema.getInstance().getTableInfoShortURL(), userSchema); + wrapAllColumns(true); + setInsertURL(LINK_DISABLER); + setImportURL(LINK_DISABLER); + setDeleteURL(LINK_DISABLER); + setUpdateURL(LINK_DISABLER); + setDetailsURL(LINK_DISABLER); + + var fullUrlCol = getMutableColumn(FieldKey.fromParts(FULL_URL_COL)); + if (fullUrlCol != null ) fullUrlCol.setLabel("Target URL"); + + // Hyperlinked short URL column + var shortUrlCol = getMutableColumn(FieldKey.fromParts(SHORT_URL_COL)); + if (shortUrlCol != null) + { + shortUrlCol.setDisplayColumnFactory(new DisplayColumnFactory() + { + @Override + public DisplayColumn createRenderer(ColumnInfo colInfo) + { + return new DataColumn(colInfo) + { + @Override + public String renderURL(RenderContext ctx) + { + String shortUrl = (String) getValue(ctx); + return shortUrl != null ? ShortURLRecord.renderShortURL(shortUrl) : super.renderURL(ctx); + } + }; + } + }); + } + + // Update and Delete column + var updateCol = addWrapColumn(UPDATE_SHORT_URL_COL, getRealTable().getColumn(SHORT_URL_COL)); + updateCol.setLabel(""); + updateCol.setDisplayColumnFactory(colInfo -> new DataColumn(colInfo) + { + @Override + public void renderGridCellContents(RenderContext ctx, Writer out) + { + String shortUrl = (String) getValue(ctx); + if (shortUrl != null) + { + PageFlowUtil.link("Update") + .href(new ActionURL(AdminController.UpdateShortURLAction.class, getContainer()).addParameter("shortURL", shortUrl)) + .appendTo(out); + + PageFlowUtil.link("Delete") + .href(new ActionURL(AdminController.UpdateShortURLAction.class, getContainer()).addParameter("shortURL", shortUrl).addParameter("delete", true)) + .usePost("Are you sure you want to delete the short URL " + shortUrl + "?") + .style("margin-left:5px;") + .appendTo(out); + } + } + }); + + // Copy to Clipboard column + var copyToClipboardCol = addWrapColumn(TO_CLIPBOARD_COL, getRealTable().getColumn(SHORT_URL_COL)); + copyToClipboardCol.setLabel(""); + copyToClipboardCol.setDisplayColumnFactory(colInfo -> new DataColumn(colInfo) + { + @Override + public @NotNull Set getClientDependencies() + { + Set dependencies = super.getClientDependencies(); + dependencies.add(ClientDependency.fromPath("internal/clipboard/clipboard-1.5.9.min.js")); + return dependencies; + } + + @Override + public void renderGridCellContents(RenderContext ctx, Writer out) throws IOException + { + String shortUrl = (String) getValue(ctx); + Integer rowId = ctx.get(FieldKey.fromParts(ROWID_COL), Integer.class); + if (shortUrl != null && rowId != null) + { + var elementId = "copyToClipboardId" + rowId; + PageFlowUtil.link("copy to clipboard") + .onClick("return false;") + .id(elementId) + .attributes(Collections.singletonMap("data-clipboard-text", ShortURLRecord.renderShortURL(shortUrl))) + .appendTo(out); + DOM.SCRIPT(HtmlString.unsafe("new Clipboard('#" + elementId + "');")).appendTo(out); + } + else + { + super.renderGridCellContents(ctx, out); + } + } + + @Override + public void addQueryFieldKeys(Set keys) + { + super.addQueryFieldKeys(keys); + keys.add(FieldKey.fromString(ROWID_COL)); + } + }); + + List defaultCols = new ArrayList<>(); + defaultCols.add(FieldKey.fromParts("Created")); + defaultCols.add(FieldKey.fromParts("CreatedBy")); + defaultCols.add(FieldKey.fromParts(SHORT_URL_COL)); + defaultCols.add(FieldKey.fromParts(TO_CLIPBOARD_COL)); + defaultCols.add(FieldKey.fromParts(UPDATE_SHORT_URL_COL)); + defaultCols.add(FieldKey.fromParts(FULL_URL_COL)); + setDefaultVisibleColumns(defaultCols); + } +} From df506a074395935618732a767168ab4ada2bf096 Mon Sep 17 00:00:00 2001 From: vagisha Date: Tue, 10 Oct 2023 10:06:13 -0700 Subject: [PATCH 2/2] Code review changes: - Display ShortURL table only if container is root and user has AdminOperationsPermission - Enable standard update and delete --- .../labkey/core/query/CoreQuerySchema.java | 2 +- .../labkey/core/query/ShortUrlTableInfo.java | 111 +++++++++++++----- 2 files changed, 82 insertions(+), 31 deletions(-) diff --git a/core/src/org/labkey/core/query/CoreQuerySchema.java b/core/src/org/labkey/core/query/CoreQuerySchema.java index f1002d1ab68..8f9222062a6 100644 --- a/core/src/org/labkey/core/query/CoreQuerySchema.java +++ b/core/src/org/labkey/core/query/CoreQuerySchema.java @@ -178,7 +178,7 @@ public TableInfo createTable(String name, ContainerFilter cf) return new ViewCategoryTable(ViewCategoryManager.getInstance().getTableInfoCategories(), this, cf); if (MISSING_VALUE_INDICATOR_TABLE_NAME.equalsIgnoreCase(name)) return getMVIndicatorTable(cf); - if (SHORTURL_TABLE_NAME.equalsIgnoreCase(name)) + if (SHORTURL_TABLE_NAME.equalsIgnoreCase(name) && ShortUrlTableInfo.canDisplayTable(getUser(), getContainer())) return new ShortUrlTableInfo(this); return null; diff --git a/core/src/org/labkey/core/query/ShortUrlTableInfo.java b/core/src/org/labkey/core/query/ShortUrlTableInfo.java index 32f18f2e9ce..2c3a425741a 100644 --- a/core/src/org/labkey/core/query/ShortUrlTableInfo.java +++ b/core/src/org/labkey/core/query/ShortUrlTableInfo.java @@ -1,19 +1,33 @@ package org.labkey.core.query; +import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.NotNull; import org.labkey.api.data.ColumnInfo; +import org.labkey.api.data.Container; import org.labkey.api.data.CoreSchema; import org.labkey.api.data.DataColumn; import org.labkey.api.data.DisplayColumn; import org.labkey.api.data.DisplayColumnFactory; import org.labkey.api.data.RenderContext; +import org.labkey.api.data.TableSelector; +import org.labkey.api.query.DetailsURL; import org.labkey.api.query.FieldKey; import org.labkey.api.query.FilteredTable; +import org.labkey.api.query.QueryUpdateService; +import org.labkey.api.query.QueryUpdateServiceException; +import org.labkey.api.query.RowIdQueryUpdateService; +import org.labkey.api.query.ValidationException; +import org.labkey.api.security.User; +import org.labkey.api.security.UserPrincipal; +import org.labkey.api.security.permissions.AdminOperationsPermission; +import org.labkey.api.security.permissions.Permission; import org.labkey.api.util.DOM; import org.labkey.api.util.HtmlString; import org.labkey.api.util.PageFlowUtil; import org.labkey.api.view.ActionURL; +import org.labkey.api.view.NotFoundException; import org.labkey.api.view.ShortURLRecord; +import org.labkey.api.view.ShortURLService; import org.labkey.api.view.template.ClientDependency; import org.labkey.core.admin.AdminController; @@ -29,7 +43,6 @@ public class ShortUrlTableInfo extends FilteredTable private static final String SHORT_URL_COL = "shorturl"; private static final String FULL_URL_COL = "fullurl"; private static final String ROWID_COL = "rowid"; - private static final String UPDATE_SHORT_URL_COL = "UpdateShortUrl"; private static final String TO_CLIPBOARD_COL = "CopyToClipboard"; public ShortUrlTableInfo(@NotNull CoreQuerySchema userSchema) @@ -38,9 +51,10 @@ public ShortUrlTableInfo(@NotNull CoreQuerySchema userSchema) wrapAllColumns(true); setInsertURL(LINK_DISABLER); setImportURL(LINK_DISABLER); - setDeleteURL(LINK_DISABLER); - setUpdateURL(LINK_DISABLER); - setDetailsURL(LINK_DISABLER); + + DetailsURL updateUrl = new DetailsURL(new ActionURL(AdminController.UpdateShortURLAction.class, getContainer()), + Collections.singletonMap("shortURL", SHORT_URL_COL)); + setUpdateURL(updateUrl); var fullUrlCol = getMutableColumn(FieldKey.fromParts(FULL_URL_COL)); if (fullUrlCol != null ) fullUrlCol.setLabel("Target URL"); @@ -67,30 +81,6 @@ public String renderURL(RenderContext ctx) }); } - // Update and Delete column - var updateCol = addWrapColumn(UPDATE_SHORT_URL_COL, getRealTable().getColumn(SHORT_URL_COL)); - updateCol.setLabel(""); - updateCol.setDisplayColumnFactory(colInfo -> new DataColumn(colInfo) - { - @Override - public void renderGridCellContents(RenderContext ctx, Writer out) - { - String shortUrl = (String) getValue(ctx); - if (shortUrl != null) - { - PageFlowUtil.link("Update") - .href(new ActionURL(AdminController.UpdateShortURLAction.class, getContainer()).addParameter("shortURL", shortUrl)) - .appendTo(out); - - PageFlowUtil.link("Delete") - .href(new ActionURL(AdminController.UpdateShortURLAction.class, getContainer()).addParameter("shortURL", shortUrl).addParameter("delete", true)) - .usePost("Are you sure you want to delete the short URL " + shortUrl + "?") - .style("margin-left:5px;") - .appendTo(out); - } - } - }); - // Copy to Clipboard column var copyToClipboardCol = addWrapColumn(TO_CLIPBOARD_COL, getRealTable().getColumn(SHORT_URL_COL)); copyToClipboardCol.setLabel(""); @@ -112,7 +102,7 @@ public void renderGridCellContents(RenderContext ctx, Writer out) throws IOExcep if (shortUrl != null && rowId != null) { var elementId = "copyToClipboardId" + rowId; - PageFlowUtil.link("copy to clipboard") + PageFlowUtil.iconLink("fa fa-clipboard", "copy to clipboard") .onClick("return false;") .id(elementId) .attributes(Collections.singletonMap("data-clipboard-text", ShortURLRecord.renderShortURL(shortUrl))) @@ -138,8 +128,69 @@ public void addQueryFieldKeys(Set keys) defaultCols.add(FieldKey.fromParts("CreatedBy")); defaultCols.add(FieldKey.fromParts(SHORT_URL_COL)); defaultCols.add(FieldKey.fromParts(TO_CLIPBOARD_COL)); - defaultCols.add(FieldKey.fromParts(UPDATE_SHORT_URL_COL)); defaultCols.add(FieldKey.fromParts(FULL_URL_COL)); setDefaultVisibleColumns(defaultCols); } + + @Override + public QueryUpdateService getUpdateService() + { + return new RowIdQueryUpdateService(this) + { + @Override + protected ShortURLRecord createNewBean() + { + throw new UnsupportedOperationException(); + } + + @Override + protected ShortURLRecord insert(User user, Container container, ShortURLRecord bean) + { + throw new UnsupportedOperationException(); + } + + @Override + protected ShortURLRecord update(User user, Container container, ShortURLRecord bean, Integer oldKey) + { + throw new UnsupportedOperationException(); + } + + @Override + public ShortURLRecord get(User user, Container container, int key) + { + return new TableSelector(CoreSchema.getInstance().getTableInfoShortURL()).getObject(key, ShortURLRecord.class); + } + + @Override + public void delete(User user, Container container, int key) throws QueryUpdateServiceException + { + ShortURLRecord shortUrl = get(user, container, key); + if (shortUrl == null) + { + throw new NotFoundException("No short URL record found for rowId " + key); + } + + try + { + ShortURLService.get().deleteShortURL(shortUrl, user); + } + catch (ValidationException e) + { + String err = "Unable to delete short URL " + shortUrl.getShortURL(); + throw new QueryUpdateServiceException(err + ". " + StringUtils.join(e.getAllErrors(), ". "), e); + } + } + }; + } + + @Override + public boolean hasPermission(@NotNull UserPrincipal user, @NotNull Class perm) + { + return canDisplayTable(user, getContainer()) && getContainer().hasPermission(user, perm); + } + + public static boolean canDisplayTable(@NotNull UserPrincipal user, @NotNull Container container) + { + return container.isRoot() && container.hasPermission(user, AdminOperationsPermission.class); + } }