diff --git a/announcements/resources/schemas/comm.xml b/announcements/resources/schemas/comm.xml index d94d053eb08..902458f76ce 100644 --- a/announcements/resources/schemas/comm.xml +++ b/announcements/resources/schemas/comm.xml @@ -649,4 +649,11 @@ + + + + + + +
diff --git a/announcements/resources/schemas/dbscripts/postgresql/comm-22.000-22.001.sql b/announcements/resources/schemas/dbscripts/postgresql/comm-22.000-22.001.sql new file mode 100644 index 00000000000..5095b1d7be4 --- /dev/null +++ b/announcements/resources/schemas/dbscripts/postgresql/comm-22.000-22.001.sql @@ -0,0 +1,8 @@ +CREATE TABLE comm.PageAliases +( + Container ENTITYID NOT NULL, + Alias VARCHAR(255) NOT NULL, + RowId INT NOT NULL, + + CONSTRAINT PK_PageAliases PRIMARY KEY (Container, Alias) +); diff --git a/announcements/resources/schemas/dbscripts/postgresql/comm-22.001-22.002.sql b/announcements/resources/schemas/dbscripts/postgresql/comm-22.001-22.002.sql new file mode 100644 index 00000000000..64b26571d57 --- /dev/null +++ b/announcements/resources/schemas/dbscripts/postgresql/comm-22.001-22.002.sql @@ -0,0 +1,5 @@ +ALTER TABLE comm.PageAliases RENAME COLUMN RowId TO PageRowId; + +-- Aliases should be case-insensitive +ALTER TABLE comm.PageAliases DROP CONSTRAINT PK_PageAliases; +CREATE UNIQUE INDEX UQ_PageAliases ON comm.PageAliases (Container, LOWER(Alias)); diff --git a/announcements/resources/schemas/dbscripts/sqlserver/comm-22.000-22.001.sql b/announcements/resources/schemas/dbscripts/sqlserver/comm-22.000-22.001.sql new file mode 100644 index 00000000000..4b1151a629a --- /dev/null +++ b/announcements/resources/schemas/dbscripts/sqlserver/comm-22.000-22.001.sql @@ -0,0 +1,8 @@ +CREATE TABLE comm.PageAliases +( + Container ENTITYID NOT NULL, + Alias NVARCHAR(255) NOT NULL, + RowId INT NOT NULL, + + CONSTRAINT PK_PageAliases PRIMARY KEY (Container, Alias) +); diff --git a/announcements/resources/schemas/dbscripts/sqlserver/comm-22.001-22.002.sql b/announcements/resources/schemas/dbscripts/sqlserver/comm-22.001-22.002.sql new file mode 100644 index 00000000000..d129880aa7c --- /dev/null +++ b/announcements/resources/schemas/dbscripts/sqlserver/comm-22.001-22.002.sql @@ -0,0 +1 @@ +EXEC sp_rename 'comm.PageAliases.RowId', 'PageRowId', 'COLUMN'; diff --git a/announcements/resources/schemas/dbscripts/sqlserver/comm-22.002-22.003.sql b/announcements/resources/schemas/dbscripts/sqlserver/comm-22.002-22.003.sql new file mode 100644 index 00000000000..1866f84ce11 --- /dev/null +++ b/announcements/resources/schemas/dbscripts/sqlserver/comm-22.002-22.003.sql @@ -0,0 +1,3 @@ +-- Switch from PK to UNIQUE INDEX to match PostgreSQL +ALTER TABLE comm.PageAliases DROP CONSTRAINT PK_PageAliases; +CREATE UNIQUE INDEX UQ_PageAliases ON comm.PageAliases (Container, Alias); \ No newline at end of file diff --git a/announcements/src/org/labkey/announcements/AnnouncementModule.java b/announcements/src/org/labkey/announcements/AnnouncementModule.java index c2e745eda9f..ca22276163f 100644 --- a/announcements/src/org/labkey/announcements/AnnouncementModule.java +++ b/announcements/src/org/labkey/announcements/AnnouncementModule.java @@ -97,7 +97,7 @@ public String getName() @Override public @Nullable Double getSchemaVersion() { - return 22.000; + return 22.003; } @Override diff --git a/api/src/org/labkey/api/collections/LabKeyCollectors.java b/api/src/org/labkey/api/collections/LabKeyCollectors.java index cb89ad98513..417368b5fbf 100644 --- a/api/src/org/labkey/api/collections/LabKeyCollectors.java +++ b/api/src/org/labkey/api/collections/LabKeyCollectors.java @@ -47,6 +47,23 @@ public class LabKeyCollectors ); } + /** + * Returns a {@link Collector} that builds a {@link CaseInsensitiveHashMap} + */ + public static Collector> toCaseInsensitiveMap( + Function keyMapper, + Function valueMapper) + { + return toMap( + keyMapper, + valueMapper, + (u, v) -> { + throw new IllegalStateException(String.format("Duplicate key %s", u)); + }, + CaseInsensitiveHashMap::new + ); + } + /** * Returns a {@link Collector} that accumulates elements into a {@link MultiValuedMap} whose keys and values are the * result of applying the provided mapping functions to the input elements, an approach that mimics {@link Collectors#toMap(Function, Function)}. diff --git a/api/src/org/labkey/api/util/ExceptionUtil.java b/api/src/org/labkey/api/util/ExceptionUtil.java index 349c90c3780..fade8238d53 100644 --- a/api/src/org/labkey/api/util/ExceptionUtil.java +++ b/api/src/org/labkey/api/util/ExceptionUtil.java @@ -632,10 +632,10 @@ static ActionURL handleException(@NotNull HttpServletRequest request, @NotNull H } // Do redirects before response.reset() otherwise we'll lose cookies (e.g., login page) - if (ex instanceof RedirectException) + if (ex instanceof RedirectException rex) { - String url = ((RedirectException) ex).getURL(); - doErrorRedirect(response, url); + String url = rex.getURL(); + doErrorRedirect(response, url, rex.getHttpStatusCode()); return null; } @@ -1000,10 +1000,16 @@ private static void addDependenciesAndRender(int responseStatus, PageConfig page errorView.getView().render(errorView.getModel(), request, response); } - + // Temporary redirect public static void doErrorRedirect(HttpServletResponse response, String url) { - response.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY); + doErrorRedirect(response, url, HttpServletResponse.SC_MOVED_TEMPORARILY); + } + + // Pass in HTTP status code to designate temporary vs. permanent redirect + private static void doErrorRedirect(HttpServletResponse response, String url, int httpStatusCode) + { + response.setStatus(httpStatusCode); response.setDateHeader("Expires", 0); response.setHeader("Location", url); response.setContentType("text/html; charset=UTF-8"); @@ -1025,8 +1031,6 @@ public static void doErrorRedirect(HttpServletResponse response, String url) } } - - public enum ExceptionInfo { ResolveURL, // suggestion for where to fix this e.g. sourceQuery.view diff --git a/api/src/org/labkey/api/view/PermanentRedirectException.java b/api/src/org/labkey/api/view/PermanentRedirectException.java new file mode 100644 index 00000000000..8452f3d9f36 --- /dev/null +++ b/api/src/org/labkey/api/view/PermanentRedirectException.java @@ -0,0 +1,21 @@ +package org.labkey.api.view; + +import org.jetbrains.annotations.NotNull; +import org.labkey.api.util.URLHelper; + +import javax.servlet.http.HttpServletResponse; + +/** Use when we want search engines, browsers, etc to assume that the redirecting URL is defunct and the target URL should be used going forward */ +public class PermanentRedirectException extends RedirectException +{ + public PermanentRedirectException(@NotNull URLHelper url) + { + super(url); + } + + @Override + public int getHttpStatusCode() + { + return HttpServletResponse.SC_MOVED_PERMANENTLY; + } +} diff --git a/api/src/org/labkey/api/view/RedirectException.java b/api/src/org/labkey/api/view/RedirectException.java index 36098d09d62..139be336642 100644 --- a/api/src/org/labkey/api/view/RedirectException.java +++ b/api/src/org/labkey/api/view/RedirectException.java @@ -19,13 +19,15 @@ import org.labkey.api.util.SkipMothershipLogging; import org.labkey.api.util.URLHelper; +import javax.servlet.http.HttpServletResponse; + /** - * When thrown in the context of an HTTP request, sends the client a redirect in the HTTP response. Not treated - * as a loggable error. + * When thrown in the context of an HTTP request, sends the client a *temporary* redirect in the HTTP response. Not + * treated as a loggable error. See {@link PermanentRedirectException} if a permanent redirect is desired. */ public class RedirectException extends RuntimeException implements SkipMothershipLogging { - String _url; + private final String _url; public RedirectException(@NotNull URLHelper url) { @@ -41,4 +43,9 @@ public String getURL() { return _url; } + + public int getHttpStatusCode() + { + return HttpServletResponse.SC_MOVED_TEMPORARILY; + } } diff --git a/internal/src/org/labkey/api/announcements/CommSchema.java b/internal/src/org/labkey/api/announcements/CommSchema.java index 95f5763d374..a471426277b 100644 --- a/internal/src/org/labkey/api/announcements/CommSchema.java +++ b/internal/src/org/labkey/api/announcements/CommSchema.java @@ -93,5 +93,10 @@ public TableInfo getTableInfoTours() { return getSchema().getTable("Tours"); } + + public TableInfo getTableInfoPageAliases() + { + return getSchema().getTable("PageAliases"); + } } diff --git a/search/src/org/labkey/search/SearchController.java b/search/src/org/labkey/search/SearchController.java index bff87f8a378..23a87c77d04 100644 --- a/search/src/org/labkey/search/SearchController.java +++ b/search/src/org/labkey/search/SearchController.java @@ -318,7 +318,7 @@ public boolean handlePost(AdminForm form, BindException errors) SearchService ss = SearchService.get(); if (null == ss) { - errors.reject("Indexing service is not running"); + errors.reject(ERROR_MSG, "Indexing service is not running"); return false; } diff --git a/wiki/resources/web/wiki/internal/wikiEdit.js b/wiki/resources/web/wiki/internal/wikiEdit.js index 688bf5adba6..e8ee92ce34f 100644 --- a/wiki/resources/web/wiki/internal/wikiEdit.js +++ b/wiki/resources/web/wiki/internal/wikiEdit.js @@ -172,7 +172,7 @@ function tinyMceHandleEvent(evt) { var bindControls = function(props) { // form controls var setDirty = function(){LABKEY.setDirty(true)}; - $(_idSel + 'name').keypress(setDirty).change(onChangeName); + $(_idSel + 'name').keypress(setDirty).change(setDirty); $(_idSel + 'title').keypress(setDirty).change(setDirty); $(_idSel + 'parent').keypress(setDirty).change(setDirty); $(_idSel + 'body').keypress(setDirty).change(setDirty); @@ -363,33 +363,6 @@ function tinyMceHandleEvent(evt) { window.location = _cancelUrl ? _cancelUrl : getRedirUrl(); }; - var onChangeName = function() { - //if this is an existing page, warn the user about changing the name - if (_wikiProps.entityId) { - getExt4(function() { - Ext4.Msg.show({ - title: 'Warning', - msg: "Changing the name of this page will break any links to this page embedded in other pages. Are you sure you want to change the name?", - buttons: Ext4.MessageBox.YESNO, - icon: Ext4.MessageBox.WARNING, - fn: function(btnId) { - if (btnId == "yes") { - LABKEY.setDirty(true); - _redirUrl = ''; // clear the redir URL since it will be referring to the old name - onSave(); - } - else { - updateControl("name", _wikiProps.name); - } - } - }); - }); - } - else { - LABKEY.setDirty(true); - } - }; - var onConvertSuccess = function(response) { var respJson = LABKEY.Utils.decode(response.responseText); diff --git a/wiki/src/org/labkey/wiki/WikiCollections.java b/wiki/src/org/labkey/wiki/WikiCollections.java index 50d4049bebe..92b3988ab73 100644 --- a/wiki/src/org/labkey/wiki/WikiCollections.java +++ b/wiki/src/org/labkey/wiki/WikiCollections.java @@ -21,15 +21,20 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.labkey.api.announcements.CommSchema; +import org.labkey.api.collections.LabKeyCollectors; import org.labkey.api.data.Container; import org.labkey.api.data.SQLFragment; +import org.labkey.api.data.SimpleFilter; import org.labkey.api.data.SqlSelector; +import org.labkey.api.data.TableSelector; +import org.labkey.api.util.PageFlowUtil; import org.labkey.api.view.NavTree; import org.labkey.wiki.model.WikiTree; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.Comparator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.LinkedList; @@ -51,6 +56,8 @@ public class WikiCollections private final Map _treesByName; private final Map _nameTitleMap; private final List _names; + private final Map _namesByAlias; + private final MultiValuedMap _aliasesByRowsId; private final List _adminNavTree; private final List _nonAdminNavTree; @@ -115,8 +122,18 @@ public WikiCollections(Container c) _adminNavTree = createNavTree(c, true); _nonAdminNavTree = createNavTree(c, false); + + _aliasesByRowsId = new TableSelector(CommSchema.getInstance().getTableInfoPageAliases(), PageFlowUtil.set("Alias", "PageRowId"), SimpleFilter.createContainerFilter(c), null) + .mapStream() + .map(map->new Alias((Integer)map.get("PageRowId"), (String)map.get("Alias"))) + .sorted(Comparator.comparing(Alias::alias, String.CASE_INSENSITIVE_ORDER)) + .collect(LabKeyCollectors.toMultiValuedMap(record->record.pageRowId, record->record.alias)); + _namesByAlias = _aliasesByRowsId.entries().stream() + .filter(e->_treesByRowId.get(e.getKey()) != null) // Just in case - ignore orphaned aliases + .collect(LabKeyCollectors.toCaseInsensitiveMap(Map.Entry::getValue, e->_treesByRowId.get(e.getKey()).getName())); } + public record Alias(int pageRowId, String alias) {} private void populateWikiTree(WikiTree parent, MultiValuedMap childMap, Map treesByRowId) { @@ -135,7 +152,6 @@ private void populateWikiTree(WikiTree parent, MultiValuedMap } } - // Create name list in depth-first order private void populateNames(WikiTree root, List names) { @@ -177,19 +193,11 @@ private List createNavTree(Container c, String rootId, WikiTree tree, b return elements; } - int getPageCount() { return getNames().size(); } - - WikiTree getWikiTree() - { - return _root; - } - - @NotNull List getNames() { return _names; @@ -218,23 +226,6 @@ List getNonAdminNavTree() return _treesByRowId.get(rowId); } - // Returns null for non-existent wiki, empty collection for existing but no children - @Nullable Collection getChildren(@Nullable String parentName) - { - WikiTree parent = getWikiTree(parentName); - - if (null == parent) - return null; - - return parent.getChildren(); - } - - // Returns null for non-existent wiki, empty collection for existing but no children - @Nullable Collection getChildren(int rowId) - { - return _treesByRowId.get(rowId).getChildren(); - } - // TODO: Change to return the root WikiTree? Map getNameTitleMap() { @@ -275,4 +266,16 @@ private Set populateWikiTrees(WikiTree root, Set trees) return trees; } + + // Ordered by alias (case-insensitive) + Collection getAliases(int rowId) + { + return _aliasesByRowsId.get(rowId); + } + + // Returns null for no match + @Nullable String getNameForAlias(@Nullable String alias) + { + return _namesByAlias.get(alias); + } } diff --git a/wiki/src/org/labkey/wiki/WikiController.java b/wiki/src/org/labkey/wiki/WikiController.java index 506453352d6..22c6491e9b8 100644 --- a/wiki/src/org/labkey/wiki/WikiController.java +++ b/wiki/src/org/labkey/wiki/WikiController.java @@ -75,6 +75,7 @@ import org.labkey.api.view.NavTree; import org.labkey.api.view.NavTreeManager; import org.labkey.api.view.NotFoundException; +import org.labkey.api.view.PermanentRedirectException; import org.labkey.api.view.Portal; import org.labkey.api.view.RedirectException; import org.labkey.api.view.UnauthorizedException; @@ -113,6 +114,7 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Objects; import java.util.Set; import java.util.TreeMap; import java.util.TreeSet; @@ -122,7 +124,6 @@ public class WikiController extends SpringActionController { private static final Logger LOG = LogManager.getLogger(WikiController.class); private static final DefaultActionResolver _actionResolver = new DefaultActionResolver(WikiController.class); - private static final boolean SHOW_CHILD_REORDERING = false; public WikiController() { @@ -140,7 +141,6 @@ protected BaseWikiPermissions getPermissions() return new BaseWikiPermissions(getUser(), getContainer()); } - @Override protected @Nullable HttpView getTemplate(ViewContext context, ModelAndView mv, Controller action, PageConfig page) { @@ -180,7 +180,6 @@ protected BaseWikiPermissions getPermissions() return template; } - public static class CustomizeWikiPartView extends JspView { private List _containerList; @@ -275,19 +274,16 @@ private static void populateWikiContainerListRecursive(ViewContext context, Cont } } - private ActionURL getBeginURL(Container c) { return getPageURL(getDefaultPage(c), c); } - public static ActionURL getPageURL(Container c, @Nullable String name) { return getWikiURL(c, PageAction.class, name); } - public static ActionURL getEditWikiURL(Container c, Class actionClass, @Nullable String name, Boolean create) { ActionURL url = getWikiURL(c, actionClass, name); @@ -297,7 +293,6 @@ public static ActionURL getEditWikiURL(Container c, Class return url; } - public static ActionURL getWikiURL(Container c, Class actionClass, @Nullable String name) { ActionURL url = new ActionURL(actionClass, c); @@ -308,12 +303,11 @@ public static ActionURL getWikiURL(Container c, Class acti return url; } - /** * This method represents the point of entry into the pageflow */ @RequiresPermission(ReadPermission.class) - public class BeginAction extends SimpleViewAction + public class BeginAction extends SimpleViewAction { @SuppressWarnings("UnusedDeclaration") public BeginAction() @@ -343,7 +337,6 @@ public void addNavTrail(NavTree root) } } - public static Wiki getDefaultPage(Container c) { //look for page named "default" @@ -367,7 +360,6 @@ public static Wiki getDefaultPage(Container c) return wiki; } - @RequiresPermission(ReadPermission.class) //will test explicitly below public class DeleteAction extends ConfirmAction { @@ -465,6 +457,7 @@ public class ManageAction extends FormViewAction { private Wiki _wiki = null; private WikiVersion _wikiVersion = null; + private boolean _isRename = false; @SuppressWarnings({"UnusedDeclaration"}) public ManageAction() @@ -480,10 +473,10 @@ public ManageAction() @Override public ModelAndView getView(WikiManageForm form, boolean reshow, BindException errors) { - String name = form.getName(); + String name = form.getNewName(); if (name == null || (errors != null && errors.getErrorCount()>0)) - name = form.getOriginalName(); + name = form.getName(); _wiki = WikiSelectManager.getWiki(getContainer(), name); @@ -500,39 +493,58 @@ public ModelAndView getView(WikiManageForm form, boolean reshow, BindException e bean.pageNames = WikiSelectManager.getPageNames(getContainer()); bean.siblings = WikiSelectManager.getChildren(getContainer(), _wiki.getParent()); bean.possibleParents = WikiSelectManager.getPossibleParents(getContainer(), _wiki); - bean.showChildren = SHOW_CHILD_REORDERING; + bean.aliases = new ArrayList<>(WikiSelectManager.getAliases(getContainer(), _wiki.getRowId())); - JspView manageView = new JspView<>("/org/labkey/wiki/view/wikiManage.jsp", bean, errors); + JspView manageView = new JspView<>("/org/labkey/wiki/view/wikiManage.jsp", bean, errors); manageView.setTitle("Wiki Configuration"); getPageConfig().setFocusId("name"); return manageView; } - public class ManageBean { public Wiki wiki; public List pageNames; public Collection siblings; public Set possibleParents; - public boolean showChildren; + public Collection aliases; } + @Override + public void validateCommand(WikiManageForm form, Errors errors) + { + //check name + String originalName = form.getName(); + String newName = form.getNewName(); + if (originalName == null) + { + errors.rejectValue("name", ERROR_MSG, "You must provide a name for this page."); + } + else if (newName != null && !newName.equalsIgnoreCase(originalName)) + { + // rename - check for existing wiki with this name + if (WikiManager.wikiNameExists(getContainer(), newName)) + errors.rejectValue("name", ERROR_MSG, "A page with the name '" + newName + "' already exists in this folder. Please choose a different name."); + _isRename = true; + } + } @Override public boolean handlePost(WikiManageForm form, BindException errors) { - String originalName = form.getOriginalName(); - String newName = form.getName(); + String originalName = form.getName(); + String newName = _isRename ? form.getNewName() : originalName; Container c = getContainer(); - _wiki = WikiSelectManager.getWiki(c, originalName); + Wiki wiki = WikiSelectManager.getWiki(c, originalName); - if (null == _wiki) + if (null == wiki) { throw new NotFoundException(); } + _wiki = new Wiki(wiki); // Clone so we don't mutate the cached copy + BaseWikiPermissions perms = getPermissions(); if (!perms.allowUpdate(_wiki)) throw new UnauthorizedException("You do not have permissions to manage this wiki page"); @@ -540,7 +552,8 @@ public boolean handlePost(WikiManageForm form, BindException errors) // Get the latest version based on previous properties WikiVersion versionOld = _wiki.getLatestVersion(); - // Now update wiki with newly submitted properties TODO: Should clone wiki instead of changing cached copy (e.g., for concurrency and in case something goes wrong with update) + // Now update wiki with newly submitted properties + _wiki.setName(newName); _wiki.setParent(form.getParent()); _wiki.setShouldIndex(form.isShouldIndex()); @@ -558,15 +571,35 @@ public boolean handlePost(WikiManageForm form, BindException errors) _wikiVersion = null; } - getWikiManager().updateWiki(getUser(), _wiki, _wikiVersion, false); + List newAliases = new ArrayList<>(); - if (SHOW_CHILD_REORDERING) + // Update aliases + if (_isRename && form.isAddAlias()) { - int[] childOrder = form.getChildOrderArray(); - if (childOrder.length > 0) - updateDisplayOrder(_wiki.children(), childOrder); + newAliases.add(originalName); + } + if (null != form.getAliases()) + { + Arrays.stream(form.getAliases().split("\n")) + .map(StringUtils::trimToNull) + .filter(Objects::nonNull) + .forEach(newAliases::add); + newAliases.sort(String.CASE_INSENSITIVE_ORDER); + } + Collection existingAliases = WikiSelectManager.getAliases(getContainer(), _wiki.getRowId()); + if (!newAliases.equals(existingAliases)) + { + WikiManager mgr = WikiManager.get(); + mgr.deleteAliases(c, wiki); + + // Best effort for alias editing -- reshow with error message if duplicates are encountered, but + // regardless of errors, complete all other edits. + newAliases.forEach(alias->mgr.addAlias(getUser(), _wiki, alias, errors)); + WikiCache.uncache(c, wiki, true); } + getWikiManager().updateWiki(getUser(), _wiki, _wikiVersion, false); + int[] siblingOrder = form.getSiblingOrderArray(); if (siblingOrder.length > 0) @@ -575,13 +608,7 @@ public boolean handlePost(WikiManageForm form, BindException errors) updateDisplayOrder(siblings, siblingOrder); } - return true; - } - - @Override - public void validateCommand(WikiManageForm wikiManageForm, Errors errors) - { - wikiManageForm.validate(errors); + return !errors.hasErrors(); } @Override @@ -600,7 +627,7 @@ public ActionURL getSuccessURL(WikiManageForm form) } catch (IllegalArgumentException ignored) {} nextPage.deleteParameters(); - nextPage.addParameter("name", form.getName()); + nextPage.addParameter("name", _wiki.getName()); return nextPage; } @@ -620,7 +647,6 @@ public ActionURL getUrl() } } - private void updateDisplayOrder(List pages, int[] order) { if (!verifyOrder(pages, order)) @@ -640,7 +666,6 @@ private void updateDisplayOrder(List pages, int[] order) } } - private boolean verifyOrder(List wikis, int[] ids) { if (ids.length != wikis.size()) @@ -676,7 +701,7 @@ public class DownloadAction extends BaseDownloadAction } @RequiresPermission(ReadPermission.class) - public class PrintAllAction extends SimpleViewAction + public class PrintAllAction extends SimpleViewAction { @Override public ModelAndView getView(Object o, BindException errors) @@ -684,7 +709,7 @@ public ModelAndView getView(Object o, BindException errors) Container c = getContainer(); Set wikiTrees = WikiSelectManager.getWikiTrees(c); - JspView v = new JspView<>("/org/labkey/wiki/view/wikiPrintAll.jsp", new PrintAllBean(wikiTrees)); + JspView v = new JspView<>("/org/labkey/wiki/view/wikiPrintAll.jsp", new PrintAllBean(wikiTrees)); v.setFrame(WebPartView.FrameType.NONE); getPageConfig().setTemplate(Template.Print); @@ -719,7 +744,7 @@ public ModelAndView getView(WikiNameForm form, BindException errors) // build a set of all descendants of the root page Set wikiTrees = WikiSelectManager.getWikiTrees(c, _rootWiki); - JspView v = new JspView<>("/org/labkey/wiki/view/wikiPrintAll.jsp", new PrintAllBean(wikiTrees)); + JspView v = new JspView<>("/org/labkey/wiki/view/wikiPrintAll.jsp", new PrintAllBean(wikiTrees)); v.setFrame(WebPartView.FrameType.NONE); getPageConfig().setTemplate(Template.Print); @@ -733,7 +758,6 @@ public void addNavTrail(NavTree root) } } - public class PrintAllBean { public Set wikiTrees; @@ -745,7 +769,6 @@ private PrintAllBean(Set wikis) } } - @RequiresPermission(ReadPermission.class) public class PrintRawAction extends SimpleViewAction { @@ -763,7 +786,7 @@ public ModelAndView getView(WikiNameForm form, BindException errors) //just want to re-use same jsp Set wikis = Collections.singleton(tree); - JspView v = new JspView<>("/org/labkey/wiki/view/wikiPrintRaw.jsp", new PrintRawBean(wikis)); + JspView v = new JspView<>("/org/labkey/wiki/view/wikiPrintRaw.jsp", new PrintRawBean(wikis)); v.setFrame(WebPartView.FrameType.NONE); getPageConfig().setTemplate(Template.Print); @@ -777,16 +800,15 @@ public void addNavTrail(NavTree root) } } - @RequiresPermission(ReadPermission.class) - public class PrintAllRawAction extends SimpleViewAction + public class PrintAllRawAction extends SimpleViewAction { @Override public ModelAndView getView(Object o, BindException errors) { Container c = getContainer(); Set wikiTrees = WikiSelectManager.getWikiTrees(c); - JspView v = new JspView<>("/org/labkey/wiki/view/wikiPrintRaw.jsp", new PrintRawBean(wikiTrees)); + JspView v = new JspView<>("/org/labkey/wiki/view/wikiPrintRaw.jsp", new PrintRawBean(wikiTrees)); v.setFrame(WebPartView.FrameType.NONE); getPageConfig().setTemplate(Template.Print); @@ -800,7 +822,6 @@ public void addNavTrail(NavTree root) } } - private static List namesToWikis(Container c, List names) { LinkedList wikis = new LinkedList<>(); @@ -811,7 +832,6 @@ private static List namesToWikis(Container c, List names) return wikis; } - public class PrintRawBean { public final Set wikis; @@ -824,7 +844,6 @@ private PrintRawBean(Set wikis) } } - public static class CopyWikiForm { private String _path; @@ -886,7 +905,6 @@ public void setIsCopyingHistory(boolean isCopyingHistory) } } - private Container getSourceContainer(String source) { Container cSource; @@ -897,7 +915,6 @@ private Container getSourceContainer(String source) return cSource; } - private Container getDestContainer(String destContainer, String path, BindException errors) { if (destContainer == null) @@ -923,7 +940,6 @@ private Container getDestContainer(String destContainer, String path, BindExcept return c; } - private void displayWikiModuleInDestContainer(Container cDest) { Set activeModules = new HashSet<>(cDest.getActiveModules()); @@ -937,7 +953,6 @@ private void displayWikiModuleInDestContainer(Container cDest) } } - @RequiresPermission(AdminPermission.class) public class CopyWikiAction extends FormHandlerAction { @@ -1018,10 +1033,8 @@ public boolean handlePost(CopyWikiForm form, BindException errors) throws Except return true; } - } - @RequiresPermission(AdminPermission.class) public class CopySinglePageAction extends FormHandlerAction { @@ -1073,7 +1086,6 @@ public URLHelper getSuccessURL(CopyWikiForm copyWikiForm) } } - @RequiresPermission(AdminPermission.class) public class CopyWikiLocationAction extends SimpleViewAction { @@ -1109,8 +1121,7 @@ public void addNavTrail(NavTree root) } } - - public class CopyBean + public static class CopyBean { public HtmlString folderList; public String destContainer; @@ -1118,7 +1129,6 @@ public class CopyBean public ActionURL cancelURL; } - private ActionURL getSourceURL(String pageName, int version) { ActionURL url = new ActionURL(SourceAction.class, getContainer()); @@ -1127,7 +1137,6 @@ private ActionURL getSourceURL(String pageName, int version) return url; } - @RequiresPermission(ReadPermission.class) public class SourceAction extends PageAction { @@ -1137,7 +1146,6 @@ public SourceAction() } } - @RequiresPermission(ReadPermission.class) public class PageAction extends SimpleViewAction { @@ -1174,8 +1182,16 @@ public ModelAndView getView(WikiNameForm form, BindException errors) _wiki = WikiSelectManager.getWiki(getContainer(), name); boolean existing = _wiki != null; - if (null == _wiki) + if (!existing) { + // Redirect if name is an alias + String realName = WikiSelectManager.getNameForAlias(getContainer(), name); + if (null != realName) + { + LOG.debug("PageAction: requested wiki name, \"" + name + "\", is an alias; redirecting to \"" + realName + "\". Referrer: " + getViewContext().getRequest().getHeader("Referer")); + throw new PermanentRedirectException(getViewContext().getActionURL().clone().replaceParameter("name", realName)); + } + _wiki = new Wiki(getContainer(), name); _wikiversion = new WikiVersion(name); //set new page title to be name. @@ -1264,14 +1280,12 @@ ActionURL getUrl() } } - public static ActionURL getPageURL(Wiki wiki, Container c) { ActionURL url = new ActionURL(PageAction.class, c); return url.addParameter("name", wiki.getName()); } - @Nullable private HttpView getDiscussionView(String objectId, ActionURL pageURL, String title) { @@ -1279,7 +1293,6 @@ private HttpView getDiscussionView(String objectId, ActionURL pageURL, String ti return service.getDiscussionArea(getViewContext(), objectId, pageURL, title, true, false); } - private ActionURL getVersionURL(String name) { ActionURL url = new ActionURL(VersionAction.class, getContainer()); @@ -1287,7 +1300,6 @@ private ActionURL getVersionURL(String name) return url; } - @RequiresPermission(ReadPermission.class) public class VersionAction extends SimpleViewAction { @@ -1593,8 +1605,6 @@ public ActionURL getUrl() } } - - private ActionURL getMakeCurrentURL(String pageName, int version) { ActionURL url = new ActionURL(MakeCurrentAction.class, getContainer()); @@ -1604,7 +1614,6 @@ private ActionURL getMakeCurrentURL(String pageName, int version) return url; } - @RequiresPermission(ReadPermission.class) //will check in code below public class MakeCurrentAction extends FormViewAction { @@ -1656,46 +1665,25 @@ public void addNavTrail(NavTree root) } } - public static class WikiManageForm { - private String _originalName; private String _name; private String _title; private int _parent; - private String _childOrder; private String _siblingOrder; private String _nextAction; - private String _containerPath; private boolean _shouldIndex; + private boolean _addAlias; + private String _newName; + private String _aliases; - public String getContainerPath() - { - return _containerPath; - } - - @SuppressWarnings({"UnusedDeclaration"}) - public void setContainerPath(String containerPath) - { - _containerPath = containerPath; - } - - public String getChildOrder() - { - return _childOrder; - } - - public void setChildOrder(String childIdList) - { - _childOrder = childIdList; - } - - public boolean isShouldIndex() + public boolean isShouldIndex() { return _shouldIndex; } - public void setShouldIndex(boolean shouldIndex) + @SuppressWarnings({"UnusedDeclaration"}) + public void setShouldIndex(boolean shouldIndex) { _shouldIndex = shouldIndex; } @@ -1731,11 +1719,6 @@ public int[] getSiblingOrderArray() return breakIdList(_siblingOrder); } - public int[] getChildOrderArray() - { - return breakIdList(_childOrder); - } - public String getName() { return _name; @@ -1758,17 +1741,6 @@ public void setTitle(String title) _title = title; } - public String getOriginalName() - { - return _originalName; - } - - @SuppressWarnings({"UnusedDeclaration"}) - public void setOriginalName(String name) - { - _originalName = name; - } - public int getParent() { return _parent; @@ -1791,85 +1763,99 @@ public void setNextAction(String nextAction) _nextAction = nextAction; } - public void validate(Errors errors) + public String getNewName() { - //check name - String newName = getName(); - String oldName = getOriginalName(); - if (newName == null) - errors.rejectValue("name", ERROR_MSG, "You must provide a name for this page."); - else - { - //check for existing wiki with this name - Container c = ContainerManager.getForPath(getContainerPath()); - if (!newName.equalsIgnoreCase(oldName) && WikiManager.wikiNameExists(c, newName)) - errors.rejectValue("name", ERROR_MSG, "A page with the name '" + newName + "' already exists in this folder. Please choose a different name."); - } + return _newName; } - } + public void setNewName(String newName) + { + _newName = newName; + } - public static class WikiNameForm - { - private String _name; - private String _redirect; - private int _parent; - private int _version; - private boolean _isDeletingSubtree; + public boolean isAddAlias() + { + return _addAlias; + } - public int getVersion() + @SuppressWarnings({"UnusedDeclaration"}) + public void setAddAlias(boolean addAlias) + { + _addAlias = addAlias; + } + + public String getAliases() + { + return _aliases; + } + + @SuppressWarnings({"UnusedDeclaration"}) + public void setAliases(String aliases) + { + _aliases = aliases; + } + } + + public static class WikiNameForm + { + private String _name; + private String _redirect; + private int _parent; + private int _version; + private boolean _isDeletingSubtree; + + public int getVersion() { return _version; } - @SuppressWarnings({"UnusedDeclaration"}) - public void setVersion(int version) + @SuppressWarnings({"UnusedDeclaration"}) + public void setVersion(int version) { _version = version; } - public int getParent() + public int getParent() { return _parent; } - public void setParent(int parent) + public void setParent(int parent) { _parent = parent; } - public String getName() + public String getName() { return _name; } - @SuppressWarnings({"UnusedDeclaration"}) - public void setName(String name) + @SuppressWarnings({"UnusedDeclaration"}) + public void setName(String name) { _name = name; } - public String getRedirect() + public String getRedirect() { return _redirect; } - public void setRedirect(String redirect) + public void setRedirect(String redirect) { _redirect = redirect; } - public boolean getIsDeletingSubtree() + public boolean getIsDeletingSubtree() { return _isDeletingSubtree; } - public void setIsDeletingSubtree(boolean isDeletingSubtree) + public void setIsDeletingSubtree(boolean isDeletingSubtree) { _isDeletingSubtree = isDeletingSubtree; } - } - + } public static class ContainerForm { @@ -2022,7 +2008,6 @@ public ModelAndView getView(EditWikiForm form, BindException errors) { throw new NotFoundException("There is no wiki in the current folder named '" + form.getName() + "'!"); } - } //endregion @@ -2059,9 +2044,18 @@ public ModelAndView getView(EditWikiForm form, BindException errors) && null != defFormat && defFormat.length() > 0) form.setFormat(defFormat); - WikiEditModel model = new WikiEditModel(getContainer(), wiki, curVersion, - form.getRedirect(), form.getCancel(), form.getFormat(), form.getDefName(), useVisualEditor, - form.getWebPartId(), getUser()); + WikiEditModel model = new WikiEditModel( + getContainer(), + wiki, + curVersion, + form.getRedirect(), + form.getCancel(), + form.getFormat(), + form.getDefName(), + useVisualEditor, + form.getWebPartId(), + getUser() + ); //endregion //region stash the wiki so we can build the nav trail @@ -2463,6 +2457,7 @@ public String[] getToDelete() return _toDelete; } + @SuppressWarnings({"UnusedDeclaration"}) public void setToDelete(String[] toDelete) { _toDelete = toDelete; diff --git a/wiki/src/org/labkey/wiki/WikiManager.java b/wiki/src/org/labkey/wiki/WikiManager.java index f33464f6b4d..11340273d37 100644 --- a/wiki/src/org/labkey/wiki/WikiManager.java +++ b/wiki/src/org/labkey/wiki/WikiManager.java @@ -75,6 +75,7 @@ import org.labkey.wiki.model.WikiVersionsGrid; import org.labkey.wiki.model.WikiView; import org.labkey.wiki.query.WikiSchema; +import org.springframework.validation.BindException; import java.io.IOException; import java.sql.ResultSet; @@ -90,6 +91,7 @@ import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; +import static org.labkey.api.action.SpringActionController.ERROR_MSG; /** * User: mbellew @@ -228,7 +230,7 @@ public boolean updateWiki(User user, Wiki wikiNew, WikiVersion versionNew, boole * * @param copyHistory true to propagate the user and date created from the previous wiki version, else just use the current user * and current date. - * @param createNewVersion by default we create a new wiki version for each update, if this is set to false we will update + * @param createNewVersion by default, we create a new wiki version for each update, if this is set to false we will update * the latest wiki version. */ private boolean updateWiki(User user, Wiki wikiNew, WikiVersion versionNew, boolean copyHistory, boolean createNewVersion) @@ -311,7 +313,31 @@ private boolean updateWiki(User user, Wiki wikiNew, WikiVersion versionNew, bool return true; } + public void addAlias(User user, Wiki wiki, String alias, BindException errors) + { + assert null != wiki.getContainerId(); + Map map = new HashMap<>(Map.of("Container", wiki.getContainerId(), "Alias", alias, "PageRowId", wiki.getRowId())); + try + { + Table.insert(user, CommSchema.getInstance().getTableInfoPageAliases(), map); + } + catch (RuntimeSQLException e) + { + if (e.isConstraintException()) + errors.rejectValue("name", ERROR_MSG, "Warning: Alias '" + alias + "' already exists in this folder."); + } + } + // null == wiki ==> delete all aliases in a container + // null != wiki ==> delete all aliases associated with a wiki + public void deleteAliases(Container c, @Nullable Wiki wiki) + { + assert null != c; + SimpleFilter filter = SimpleFilter.createContainerFilter(c); + if (null != wiki) + filter.addCondition(FieldKey.fromParts("PageRowId"), wiki.getRowId()); + Table.delete(CommSchema.getInstance().getTableInfoPageAliases(), filter); + } public void deleteWiki(User user, Container c, Wiki wiki, boolean isDeletingSubtree) throws SQLException { @@ -329,6 +355,7 @@ public void deleteWiki(User user, Container c, Wiki wiki, boolean isDeletingSubt new SimpleFilter(FieldKey.fromParts("pageentityId"), wiki.getEntityId())); Table.delete(comm.getTableInfoPages(), new SimpleFilter(FieldKey.fromParts("entityId"), wiki.getEntityId())); + deleteAliases(c, wiki); getAttachmentService().deleteAttachments(wiki.getAttachmentParent()); @@ -419,17 +446,15 @@ private void handleChildren(User user, Container c, Wiki wiki, boolean isDeletin } } - public void purgeContainer(Container c) { - WikiCache.uncache(c); - DbScope scope = comm.getSchema().getScope(); try (DbScope.Transaction transaction = scope.ensureTransaction()) { new SqlExecutor(comm.getSchema()).execute("UPDATE " + comm.getTableInfoPages() + " SET PageVersionId = NULL WHERE Container = ?", c.getId()); new SqlExecutor(comm.getSchema()).execute("DELETE FROM " + comm.getTableInfoPageVersions() + " WHERE PageEntityId IN (SELECT EntityId FROM " + comm.getTableInfoPages() + " WHERE Container = ?)", c.getId()); + deleteAliases(c, null); // Clear all wiki webpart properties that refer to this container. This includes wiki and wiki TOC // webparts in this and potentially other containers. #13937 @@ -439,8 +464,9 @@ public void purgeContainer(Container c) transaction.commit(); } - } + WikiCache.uncache(c); + } public FormattedHtml formatWiki(Container c, Wiki wiki, WikiVersion wikiversion) { diff --git a/wiki/src/org/labkey/wiki/WikiModule.java b/wiki/src/org/labkey/wiki/WikiModule.java index 43dfeba4797..8e2a7232f0f 100644 --- a/wiki/src/org/labkey/wiki/WikiModule.java +++ b/wiki/src/org/labkey/wiki/WikiModule.java @@ -91,9 +91,9 @@ protected void init() protected Collection createWebPartFactories() { return List.of( - new WikiWebPartFactory(), + new MenuWikiWebPartFactory(), new WikiTOCFactory(), - new MenuWikiWebPartFactory() + new WikiWebPartFactory() ); } diff --git a/wiki/src/org/labkey/wiki/WikiSelectManager.java b/wiki/src/org/labkey/wiki/WikiSelectManager.java index 4a01aa0bc86..f504886b8d2 100644 --- a/wiki/src/org/labkey/wiki/WikiSelectManager.java +++ b/wiki/src/org/labkey/wiki/WikiSelectManager.java @@ -103,7 +103,7 @@ public static Wiki getWiki(Container c, final String name) { return null; } - return WikiCache.getWiki(c, name, new WikiCacheLoader() + return WikiCache.getWiki(c, name, new WikiCacheLoader<>() { @Override public Wiki load(String key, Container c) @@ -113,10 +113,22 @@ public Wiki load(String key, Container c) }); } + // Get a single wiki name based on its alias + public static String getNameForAlias(Container c, String alias) + { + return getWikiCollections(c).getNameForAlias(alias); + } + + // Get a wiki's aliases + public static Collection getAliases(Container c, int rowId) + { + return getWikiCollections(c).getAliases(rowId); + } private static WikiCollections getWikiCollections(Container c) { - return WikiCache.getWikiCollections(c, new WikiCacheLoader(){ + return WikiCache.getWikiCollections(c, new WikiCacheLoader<>() + { @Override WikiCollections load(String key, Container c) { diff --git a/wiki/src/org/labkey/wiki/model/Wiki.java b/wiki/src/org/labkey/wiki/model/Wiki.java index 71d7c966935..27f0b5491e0 100644 --- a/wiki/src/org/labkey/wiki/model/Wiki.java +++ b/wiki/src/org/labkey/wiki/model/Wiki.java @@ -59,26 +59,34 @@ public Wiki() { } - public Wiki(Container c, String name) { setContainerId(c.getId()); _name = name; } + public Wiki(Wiki wiki) + { + wiki.copyTo(this); + _rowId = wiki._rowId; + _name = wiki._name; + _parent = wiki._parent; + _displayOrder = wiki._displayOrder; + _pageVersionId = wiki._pageVersionId; + _showAttachments = wiki._showAttachments; + _shouldIndex = wiki._shouldIndex; + } public ActionURL getWikiURL(Class actionClass, String name) { return WikiController.getWikiURL(lookupContainer(), actionClass, name); } - public ActionURL getPageURL() { return getWikiURL(PageAction.class, _name); } - public @Nullable ActionURL getVersionsURL() { if (null == _name) @@ -86,7 +94,6 @@ public ActionURL getPageURL() return getWikiURL(VersionsAction.class, _name); } - public ActionURL getManageURL() { if (null == _name) @@ -95,13 +102,11 @@ public ActionURL getManageURL() return getWikiURL(ManageAction.class, _name); } - public int getRowId() { return _rowId; } - @SuppressWarnings({"UnusedDeclaration"}) public void setRowId(int rowId) { diff --git a/wiki/src/org/labkey/wiki/view/wikiEdit.jsp b/wiki/src/org/labkey/wiki/view/wikiEdit.jsp index fdba4ff9bce..893e9a6ad5a 100644 --- a/wiki/src/org/labkey/wiki/view/wikiEdit.jsp +++ b/wiki/src/org/labkey/wiki/view/wikiEdit.jsp @@ -42,6 +42,7 @@ <% JspView me = (JspView) HttpView.currentView(); WikiEditModel model = me.getModelBean(); + final boolean existingWiki = null != model.getEntityId(); final String ID_PREFIX = "wiki-input-"; final HtmlString H_ID_PREFIX = h("wiki-input-"); String sep; @@ -120,9 +121,9 @@ - + diff --git a/wiki/src/org/labkey/wiki/view/wikiManage.jsp b/wiki/src/org/labkey/wiki/view/wikiManage.jsp index 1eb5b0bc1ba..079499868e8 100644 --- a/wiki/src/org/labkey/wiki/view/wikiManage.jsp +++ b/wiki/src/org/labkey/wiki/view/wikiManage.jsp @@ -97,27 +97,41 @@ return false; } + + function rename() + { + document.getElementById('rename').style.display = ''; + document.getElementById('newName').focus(); + return false; + } -
- + />
<% - FieldError nameError = errors.getFieldError("name"); - if (null != nameError) + for (FieldError nameError : errors.getFieldErrors("name")) { %><% } %> - + - - + + + @@ -129,7 +143,7 @@ <% SelectBuilder parentBuilder = new SelectBuilder() .name("parent") - .id("id") + .id("parent") .addStyle("width:420px") .onChange("document.manage.nextAction.value = " + q(NextAction.manage.name()) + "; submit();"); parentBuilder.addOption(new OptionBuilder().value("-1").label("[none]").selected(wiki.getParent() == -1).build()); @@ -181,42 +195,22 @@ - <% - if (bean.showChildren && wiki.hasChildren()) - { - %> - - + - <% - } - %>
<%=h(context.getMessage(nameError))%>
+ + <%=button("Rename").style("width:100px").submit(true).onClick("return rename()") %> +
WARNING: Changing a page's name will break any links to the page.
- - - - -
- <% - SelectBuilder childrenBuilder = new SelectBuilder().name("children").id("children").size(10).addStyle("width:500px"); - wiki.children().forEach(child->childrenBuilder.addOption(new OptionBuilder() - .value(String.valueOf(child.getRowId())) - .label(child.getLatestVersion().getTitle() + " (" + child.getName() + ")") - .build())); - %> - <%=childrenBuilder%> - - <%= button("Move Up").style("width:100px;").submit(true).onClick("return orderModule('children', 0, 'childOrder')")%> -
- <%= button("Move Down").style("width:100px;").submit(true).onClick("return orderModule('children', 1, 'childOrder')")%> -
- +
+ + + + +
+ +
- <%= button("Save").submit(true).onClick("document.manage.nextAction.value = " + q(NextAction.page.name()) + "; return true;").title("Save Changes") %>