/* (c) 2014 - 2016 Open Source Geospatial Foundation - all rights reserved * (c) 2001 - 2013 OpenPlans * This code is licensed under the GPL 2.0 license, available at the root * application directory. */ package org.geoserver.web; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import org.apache.wicket.AttributeModifier; import org.apache.wicket.Component; import org.apache.wicket.Page; import org.apache.wicket.RuntimeConfigurationType; import org.apache.wicket.ajax.IAjaxIndicatorAware; import org.apache.wicket.markup.head.CssReferenceHeaderItem; import org.apache.wicket.markup.head.IHeaderResponse; import org.apache.wicket.markup.head.JavaScriptHeaderItem; import org.apache.wicket.markup.head.PriorityHeaderItem; import org.apache.wicket.markup.html.WebMarkupContainer; import org.apache.wicket.markup.html.WebPage; import org.apache.wicket.markup.html.basic.Label; import org.apache.wicket.markup.html.image.Image; import org.apache.wicket.markup.html.link.BookmarkablePageLink; import org.apache.wicket.markup.html.link.ExternalLink; import org.apache.wicket.markup.html.list.ListItem; import org.apache.wicket.markup.html.list.ListView; import org.apache.wicket.markup.html.panel.FeedbackPanel; import org.apache.wicket.model.LoadableDetachableModel; import org.apache.wicket.model.StringResourceModel; import org.apache.wicket.request.cycle.RequestCycle; import org.apache.wicket.request.resource.PackageResourceReference; import org.geoserver.catalog.Catalog; import org.geoserver.config.GeoServer; import org.geoserver.platform.GeoServerExtensions; import org.geoserver.web.spring.security.GeoServerSession; import org.geoserver.web.wicket.ParamResourceModel; import org.geotools.util.logging.Logging; import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.core.Authentication; /** * Base class for web pages in GeoServer web application. * <ul> * <li>The basic layout</li> * <li>An OO infrastructure for common elements location</li> * <li>An infrastructure for locating subpages in the Spring context and * creating links</li> * </ul> * * @author Andrea Aaime, The Open Planning Project * @author Justin Deoliveira, The Open Planning Project */ public class GeoServerBasePage extends WebPage implements IAjaxIndicatorAware { /** * The id of the panel sitting in the page-header, right below the page description */ protected static final String HEADER_PANEL = "headerPanel"; protected static final Logger LOGGER = Logging.getLogger(GeoServerBasePage.class); protected static volatile GeoServerNodeInfo NODE_INFO; /** * feedback panel for subclasses to report errors and information. */ protected FeedbackPanel feedbackPanel; /** * page for this page to return to when the page is finished, could be null. */ protected Page returnPage; /** * page class for this page to return to when the page is finished, could be null. */ protected Class<? extends Page> returnPageClass; @SuppressWarnings("serial") public GeoServerBasePage() { // lookup for a pluggable favicon PackageResourceReference faviconReference = null; List<HeaderContribution> cssContribs = getGeoServerApplication().getBeansOfType(HeaderContribution.class); for (HeaderContribution csscontrib : cssContribs) { try { if (csscontrib.appliesTo(this)) { PackageResourceReference ref = csscontrib.getFavicon(); if(ref != null) { faviconReference = ref; } } } catch( Throwable t ) { LOGGER.log(Level.WARNING, "Problem adding header contribution", t ); } } // favicon if(faviconReference == null) { faviconReference = new PackageResourceReference(GeoServerBasePage.class, "favicon.ico"); } String faviconUrl = RequestCycle.get().urlFor(faviconReference, null).toString(); add(new ExternalLink("faviconLink", faviconUrl, null)); // page title add(new Label("pageTitle", new LoadableDetachableModel<String>() { @Override protected String load() { return getPageTitle(); } })); // login / logout stuff List<String> securityFilters = getGeoServerApplication().getSecurityManager().getSecurityConfig().getFilterChain().filtersFor("/web/**"); final Authentication user = GeoServerSession.get().getAuthentication(); final boolean anonymous = user == null || user instanceof AnonymousAuthenticationToken; // login forms List<LoginFormInfo> loginforms = filterByAuth(getGeoServerApplication().getBeansOfType(LoginFormInfo.class)); add(new ListView<LoginFormInfo>("loginforms", loginforms) { public void populateItem(ListItem<LoginFormInfo> item) { LoginFormInfo info = item.getModelObject(); WebMarkupContainer loginForm = new WebMarkupContainer("loginform") { protected void onComponentTag(org.apache.wicket.markup.ComponentTag tag) { String path = getRequest().getUrl().getPath(); StringBuilder loginPath = new StringBuilder(); if(path.isEmpty()) { // home page loginPath.append("../" + info.getLoginPath()); } else { // boomarkable page of sorts String[] pathElements = path.split("/"); for (String pathElement : pathElements) { if(!pathElement.isEmpty()) { loginPath.append("../"); } } loginPath.append(info.getLoginPath()); } tag.put("action", loginPath); }; }; Image image; if(info.getIcon() != null) { image = new Image("link.icon", new PackageResourceReference(info.getComponentClass(), info.getIcon())); } else { image = new Image("link.icon", new PackageResourceReference(GeoServerBasePage.class, "img/icons/silk/door-in.png")); } loginForm.add(image); if (info.getTitleKey() != null && !info.getTitleKey().isEmpty()) { loginForm.add(new Label("link.label", new StringResourceModel(info.getTitleKey(), (Component) null, null))); image.add(AttributeModifier.replace("alt", new ParamResourceModel(info.getTitleKey(), null))); } else { loginForm.add(new Label("link.label", "")); } LoginFormHTMLInclude include; if (info.getInclude() != null) { include = new LoginFormHTMLInclude("login.include", new PackageResourceReference(info.getComponentClass(), info.getInclude())); } else { include = new LoginFormHTMLInclude("login.include", new PackageResourceReference(GeoServerBasePage.class, "")); } loginForm.add(include); item.add(loginForm); boolean filterInChain = false; for (String filterName : securityFilters) { if (filterName.toLowerCase().contains(info.getName())) { filterInChain = true; break; } } loginForm.setVisible(anonymous && filterInChain); } }); // logout forms WebMarkupContainer loggedInAsForm = new WebMarkupContainer("loggedinasform"); loggedInAsForm.add(new Label("loggedInUsername", anonymous ? "Nobody" : user.getName())); loggedInAsForm.setVisible(!anonymous); add(loggedInAsForm); List<LogoutFormInfo> logoutforms = filterByAuth(getGeoServerApplication().getBeansOfType(LogoutFormInfo.class)); add(new ListView<LogoutFormInfo>("logoutforms", logoutforms) { public void populateItem(ListItem<LogoutFormInfo> item) { LogoutFormInfo info = item.getModelObject(); WebMarkupContainer logoutForm = new WebMarkupContainer("logoutform") { protected void onComponentTag(org.apache.wicket.markup.ComponentTag tag) { String path = getRequest().getUrl().getPath(); StringBuilder logoutPath = new StringBuilder(); if(path.isEmpty()) { // home page logoutPath.append("../" + info.getLogoutPath()); } else { // boomarkable page of sorts String[] pathElements = path.split("/"); for (String pathElement : pathElements) { if(!pathElement.isEmpty()) { logoutPath.append("../"); } } logoutPath.append(info.getLogoutPath()); } tag.put("action", logoutPath); }; }; Image image; if(info.getIcon() != null) { image = new Image("link.icon", new PackageResourceReference(info.getComponentClass(), info.getIcon())); } else { image = new Image("link.icon", new PackageResourceReference(GeoServerBasePage.class, "img/icons/silk/door-out.png")); } logoutForm.add(image); if (info.getTitleKey() != null && !info.getTitleKey().isEmpty()) { logoutForm.add(new Label("link.label", new StringResourceModel(info.getTitleKey(), (Component) null, null))); image.add(AttributeModifier.replace("alt", new ParamResourceModel(info.getTitleKey(), null))); } else { logoutForm.add(new Label("link.label", "")); } item.add(logoutForm); boolean filterInChain = false; for (String filterName : securityFilters) { if (filterName.toLowerCase().contains(info.getName())) { filterInChain = true; break; } } logoutForm.setVisible(!anonymous && filterInChain); } }); // home page link add( new BookmarkablePageLink( "home", GeoServerHomePage.class ) .add( new Label( "label", new StringResourceModel( "home", (Component)null, null ) ) ) ); // dev buttons DeveloperToolbar devToolbar = new DeveloperToolbar("devButtons"); add(devToolbar); devToolbar.setVisible(RuntimeConfigurationType.DEVELOPMENT.equals( getApplication().getConfigurationType())); final Map<Category,List<MenuPageInfo>> links = splitByCategory( filterByAuth(getGeoServerApplication().getBeansOfType(MenuPageInfo.class)) ); List<MenuPageInfo> standalone = links.containsKey(null) ? links.get(null) : new ArrayList<MenuPageInfo>(); links.remove(null); List<Category> categories = new ArrayList<>(links.keySet()); Collections.sort(categories); add(new ListView<Category>("category", categories){ public void populateItem(ListItem<Category> item){ Category category = item.getModelObject(); item.add(new Label("category.header", new StringResourceModel(category.getNameKey(), (Component) null, null))); item.add(new ListView<MenuPageInfo>("category.links", links.get(category)){ public void populateItem(ListItem<MenuPageInfo> item){ MenuPageInfo info = item.getModelObject(); BookmarkablePageLink<Page> link = new BookmarkablePageLink<>("link", info.getComponentClass()); link.add(AttributeModifier.replace("title", new StringResourceModel(info.getDescriptionKey(), (Component) null, null))); link.add(new Label("link.label", new StringResourceModel(info.getTitleKey(), (Component) null, null))); Image image; if(info.getIcon() != null) { image = new Image("link.icon", new PackageResourceReference(info.getComponentClass(), info.getIcon())); } else { image = new Image("link.icon", new PackageResourceReference(GeoServerBasePage.class, "img/icons/silk/wrench.png")); } image.add(AttributeModifier.replace("alt", new ParamResourceModel(info.getTitleKey(), null))); link.add(image); item.add(link); } }); } }); add(new ListView<MenuPageInfo>("standalone", standalone) { public void populateItem(ListItem<MenuPageInfo> item) { MenuPageInfo info = item.getModelObject(); BookmarkablePageLink<Page> link = new BookmarkablePageLink<>("link", info.getComponentClass()); link.add(AttributeModifier.replace("title", new StringResourceModel(info.getDescriptionKey(), (Component) null, null))); link.add(new Label("link.label", new StringResourceModel(info.getTitleKey(), (Component) null, null))); item.add(link); } }); add(feedbackPanel = new FeedbackPanel("feedback")); feedbackPanel.setOutputMarkupId( true ); // ajax feedback image add(new Image("ajaxFeedbackImage", new PackageResourceReference(GeoServerBasePage.class, "img/ajax-loader.gif"))); add(new WebMarkupContainer(HEADER_PANEL)); // allow the subclasses to initialize before getTitle/getDescription are called add(new Label("gbpTitle", new LoadableDetachableModel<String>() { @Override protected String load() { return getTitle(); } })); add(new Label("gbpDescription", new LoadableDetachableModel<String>() { @Override protected String load() { return getDescription(); } })); // node id handling WebMarkupContainer container = new WebMarkupContainer("nodeIdContainer"); add(container); String id = getNodeInfo().getId(); Label label = new Label("nodeId", id); container.add(label); NODE_INFO.customize(container); if(id == null) { container.setVisible(false); } } @Override public void renderHead(IHeaderResponse response) { // includes jquery, required by the placeholder plugin (wicket only include jquery if he need it) response.render(new PriorityHeaderItem(JavaScriptHeaderItem.forReference( getApplication().getJavaScriptLibrarySettings().getJQueryReference()))); List<HeaderContribution> cssContribs = getGeoServerApplication() .getBeansOfType(HeaderContribution.class); for (HeaderContribution csscontrib : cssContribs) { try { if (csscontrib.appliesTo(this)) { PackageResourceReference ref = csscontrib.getCSS(); if (ref != null) { response.render(CssReferenceHeaderItem.forReference(ref)); } ref = csscontrib.getJavaScript(); if (ref != null) { response.render(JavaScriptHeaderItem.forReference(ref)); } ref = csscontrib.getFavicon(); } } catch (Throwable t) { LOGGER.log(Level.WARNING, "Problem adding header contribution", t); } } } private GeoServerNodeInfo getNodeInfo() { // we don't synch on this one, worst it can happen, we create // two instances of DefaultGeoServerNodeInfo, and one wil be gc-ed soon if (NODE_INFO == null) { // see if someone plugged a custom node info bean, otherwise use the default one GeoServerNodeInfo info = GeoServerExtensions.bean(GeoServerNodeInfo.class); if (info == null) { info = new DefaultGeoServerNodeInfo(); } NODE_INFO = info; } return NODE_INFO; } protected String getTitle() { return new ParamResourceModel("title", this).getString(); } protected String getDescription() { return new ParamResourceModel("description", this).getString(); } /** * Gets the page title from the PageName.title resource, falling back on "GeoServer" if not found * */ String getPageTitle() { try { return "GeoServer: " + getTitle(); } catch(Exception e) { LOGGER.warning(getClass().getSimpleName() + " does not have a title set"); } return "GeoServer"; } /** * The base page is built with an empty panel in the page-header section that can be filled by * subclasses calling this method * * @param component * The component to be placed at the bottom of the page-header section. The component * must have "page-header" id */ protected void setHeaderPanel(Component component) { if (!HEADER_PANEL.equals(component.getId())) throw new IllegalArgumentException( "The header panel component must have 'headerPanel' id"); remove(HEADER_PANEL); add(component); } /** * Returns the application instance. */ protected GeoServerApplication getGeoServerApplication() { return (GeoServerApplication) getApplication(); } @Override public GeoServerSession getSession() { return (GeoServerSession) super.getSession(); } /** * Convenience method for pages to get access to the geoserver * configuration. */ protected GeoServer getGeoServer() { return getGeoServerApplication().getGeoServer(); } /** * Convenience method for pages to get access to the catalog. */ protected Catalog getCatalog() { return getGeoServerApplication().getCatalog(); } /** * Splits up the pages by category, turning the list into a map keyed by category * @param pages * */ private Map<Category,List<MenuPageInfo>> splitByCategory(List<MenuPageInfo> pages){ Collections.sort(pages); HashMap<Category,List<MenuPageInfo>> map = new HashMap<Category,List<MenuPageInfo>>(); for (MenuPageInfo page : pages){ Category cat = page.getCategory(); if (!map.containsKey(cat)) map.put(cat, new ArrayList<MenuPageInfo>()); map.get(cat).add(page); } return map; } /** * Filters a set of component descriptors based on the current authenticated user. */ protected <T extends ComponentInfo> List<T> filterByAuth(List<T> list) { Authentication user = getSession().getAuthentication(); List<T> result = new ArrayList<T>(); for (T component : list) { if (component.getAuthorizer() == null) { continue; } final Class<?> clazz = component.getComponentClass(); if(!component.getAuthorizer().isAccessAllowed(clazz, user)) continue; result.add(component); } return result; } /** * Returns the id for the component used as a veil for the whole page while Wicket is processing * an ajax request, so it is impossible to trigger the same ajax action multiple times (think of * saving/deleting a resource, etc) * * @see IAjaxIndicatorAware#getAjaxIndicatorMarkupId() */ public String getAjaxIndicatorMarkupId() { return "ajaxFeedback"; } /** * Returns the feedback panel included in the GeoServer base page * */ public FeedbackPanel getFeedbackPanel() { return feedbackPanel; } /** * Sets the return page to navigate to when this page is done its task. * @see #doReturn() */ public GeoServerBasePage setReturnPage(Page returnPage) { this.returnPage = returnPage; return this; } /** * Sets the return page class to navigate to when this page is done its task. * @see #doReturn() */ public GeoServerBasePage setReturnPage(Class<? extends Page> returnPageClass) { this.returnPageClass = returnPageClass; return this; } /** * Returns from the page by navigating to one of {@link #returnPage} or {@link #returnPageClass}, * processed in that order. * <p> * This method should be called by pages that must return after doing some task on a form submit * such as a save or a cancel. If no return page has been set via {@link #setReturnPage(Page)} * or {@link #setReturnPageClass(Class)} then {@link GeoServerHomePage} is used. * </p> */ protected void doReturn() { doReturn(null); } /** * Returns from the page by navigating to one of {@link #returnPage} or {@link #returnPageClass}, * processed in that order. * <p> * This method accepts a parameter to use as a default in cases where {@link #returnPage} and * {@link #returnPageClass} are not set and a default other than {@link GeoServerHomePage} should * be used. * </p> * <p> * This method should be called by pages that must return after doing some task on a form submit * such as a save or a cancel. If no return page has been set via {@link #setReturnPage(Page)} * or {@link #setReturnPageClass(Class)} then {@link GeoServerHomePage} is used. * </p> */ protected void doReturn(Class<? extends Page> defaultPageClass) { if (returnPage != null) { setResponsePage(returnPage); return; } if (returnPageClass != null) { setResponsePage(returnPageClass); return; } defaultPageClass = defaultPageClass != null ? defaultPageClass : GeoServerHomePage.class; setResponsePage(defaultPageClass); } }