Author: jacopoc
Date: Fri Dec 23 09:35:26 2016 New Revision: 1775807 URL: http://svn.apache.org/viewvc?rev=1775807&view=rev Log: Improved: moved the logic/implementation of OFBiz legacy authentication tokens from the LoginWorker class to a new class named ExternalLoginKeysManager. Improved Javadocs in the new class. Added: ofbiz/trunk/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ExternalLoginKeysManager.java (with props) Modified: ofbiz/trunk/framework/common/webcommon/WEB-INF/common-controller.xml ofbiz/trunk/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/LoginEventListener.java ofbiz/trunk/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/LoginWorker.java ofbiz/trunk/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/ScreenRenderer.java ofbiz/trunk/specialpurpose/ecommerce/webapp/ecommerce/WEB-INF/controller.xml ofbiz/trunk/specialpurpose/solr/webapp/solr/WEB-INF/controller.xml ofbiz/trunk/specialpurpose/webpos/webapp/webpos/WEB-INF/controller.xml Modified: ofbiz/trunk/framework/common/webcommon/WEB-INF/common-controller.xml URL: http://svn.apache.org/viewvc/ofbiz/trunk/framework/common/webcommon/WEB-INF/common-controller.xml?rev=1775807&r1=1775806&r2=1775807&view=diff ============================================================================== --- ofbiz/trunk/framework/common/webcommon/WEB-INF/common-controller.xml (original) +++ ofbiz/trunk/framework/common/webcommon/WEB-INF/common-controller.xml Fri Dec 23 09:35:26 2016 @@ -30,7 +30,7 @@ under the License. <event name="check509CertLogin" type="java" path="org.apache.ofbiz.webapp.control.LoginWorker" invoke="check509CertLogin"/> <event name="checkRequestHeaderLogin" type="java" path="org.apache.ofbiz.webapp.control.LoginWorker" invoke="checkRequestHeaderLogin"/> <event name="checkServletRequestRemoteUserLogin" type="java" path="org.apache.ofbiz.webapp.control.LoginWorker" invoke="checkServletRequestRemoteUserLogin"/> - <event name="checkExternalLoginKey" type="java" path="org.apache.ofbiz.webapp.control.LoginWorker" invoke="checkExternalLoginKey"/> + <event name="checkExternalLoginKey" type="java" path="org.apache.ofbiz.webapp.control.ExternalLoginKeysManager" invoke="checkExternalLoginKey"/> <event name="checkProtectedView" type="java" path="org.apache.ofbiz.webapp.control.ProtectViewWorker" invoke="checkProtectedView"/> <event name="extensionConnectLogin" type="java" path="org.apache.ofbiz.webapp.control.LoginWorker" invoke="extensionConnectLogin"/> </preprocessor> Added: ofbiz/trunk/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ExternalLoginKeysManager.java URL: http://svn.apache.org/viewvc/ofbiz/trunk/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ExternalLoginKeysManager.java?rev=1775807&view=auto ============================================================================== --- ofbiz/trunk/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ExternalLoginKeysManager.java (added) +++ ofbiz/trunk/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ExternalLoginKeysManager.java Fri Dec 23 09:35:26 2016 @@ -0,0 +1,157 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ +package org.apache.ofbiz.webapp.control; + +import org.apache.ofbiz.base.util.Debug; +import org.apache.ofbiz.entity.Delegator; +import org.apache.ofbiz.entity.DelegatorFactory; +import org.apache.ofbiz.entity.GenericValue; +import org.apache.ofbiz.entity.util.EntityUtilProperties; +import org.apache.ofbiz.service.LocalDispatcher; +import org.apache.ofbiz.webapp.WebAppUtil; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +/** + * This class manages the authentication tokens that provide single sign-on authentication to the OFBiz applications. + */ +public class ExternalLoginKeysManager { + private static final String module = ExternalLoginKeysManager.class.getName(); + private static final String EXTERNAL_LOGIN_KEY_ATTR = "externalLoginKey"; + // This Map is keyed by the randomly generated externalLoginKey and the value is a UserLogin GenericValue object + private static final Map<String, GenericValue> externalLoginKeys = new ConcurrentHashMap<>(); + + /** + * Gets (and creates if necessary) an authentication token to be used for an external login parameter. + * When a new token is created, it is persisted in the web session and in the web request and map entry keyed by the + * token and valued by a userLogin object is added to a map that is looked up for subsequent requests. + * + * @param request - the http request in which the authentication token is searched and stored + * @return the authentication token as persisted in the session and request objects + */ + public static String getExternalLoginKey(HttpServletRequest request) { + Delegator delegator = (Delegator) request.getAttribute("delegator"); + boolean externalLoginKeyEnabled = "true".equals(EntityUtilProperties.getPropertyValue("security", "security.login.externalLoginKey.enabled", "true", delegator)); + if (!externalLoginKeyEnabled) { + return null; + } + GenericValue userLogin = (GenericValue) request.getAttribute("userLogin"); + + String externalKey = (String) request.getAttribute(EXTERNAL_LOGIN_KEY_ATTR); + if (externalKey != null) return externalKey; + + HttpSession session = request.getSession(); + synchronized (session) { + // if the session has a previous key in place, remove it from the master list + String sesExtKey = (String) session.getAttribute(EXTERNAL_LOGIN_KEY_ATTR); + + if (sesExtKey != null) { + if (isAjax(request)) return sesExtKey; + + externalLoginKeys.remove(sesExtKey); + } + + //check the userLogin here, after the old session setting is set so that it will always be cleared + if (userLogin == null) return ""; + + //no key made yet for this request, create one + while (externalKey == null || externalLoginKeys.containsKey(externalKey)) { + UUID uuid = UUID.randomUUID(); + externalKey = "EL" + uuid.toString(); + } + + request.setAttribute(EXTERNAL_LOGIN_KEY_ATTR, externalKey); + session.setAttribute(EXTERNAL_LOGIN_KEY_ATTR, externalKey); + externalLoginKeys.put(externalKey, userLogin); + return externalKey; + } + } + + /** + * Removes the authentication token, if any, from the session. + * + * @param session - the http session from which the authentication token is removed + */ + static void cleanupExternalLoginKey(HttpSession session) { + String sesExtKey = (String) session.getAttribute(EXTERNAL_LOGIN_KEY_ATTR); + if (sesExtKey != null) { + externalLoginKeys.remove(sesExtKey); + } + } + + /** + * OFBiz controller event that performs the user authentication using the authentication token. + * The methods is designed to be used in a chain of controller preprocessor event: it always return &success& + * even when the authentication token is missing or the authentication fails in order to move the processing to the + * next event in the chain. + * + * @param request - the http request object + * @param response - the http response object + * @return - &success& in all the cases + */ + public static String checkExternalLoginKey(HttpServletRequest request, HttpServletResponse response) { + HttpSession session = request.getSession(); + + String externalKey = request.getParameter(EXTERNAL_LOGIN_KEY_ATTR); + if (externalKey == null) return "success"; + + GenericValue userLogin = externalLoginKeys.get(externalKey); + if (userLogin != null) { + //to check it's the right tenant + //in case username and password are the same in different tenants + Delegator delegator = (Delegator) request.getAttribute("delegator"); + String oldDelegatorName = delegator.getDelegatorName(); + if (!oldDelegatorName.equals(userLogin.getDelegator().getDelegatorName())) { + delegator = DelegatorFactory.getDelegator(userLogin.getDelegator().getDelegatorName()); + LocalDispatcher dispatcher = WebAppUtil.makeWebappDispatcher(session.getServletContext(), delegator); + LoginWorker.setWebContextObjects(request, response, delegator, dispatcher); + } + // found userLogin, do the external login... + + // if the user is already logged in and the login is different, logout the other user + GenericValue currentUserLogin = (GenericValue) session.getAttribute("userLogin"); + if (currentUserLogin != null) { + if (currentUserLogin.getString("userLoginId").equals(userLogin.getString("userLoginId"))) { + // is the same user, just carry on... + return "success"; + } + + // logout the current user and login the new user... + LoginWorker.logout(request, response); + // ignore the return value; even if the operation failed we want to set the new UserLogin + } + + LoginWorker.doBasicLogin(userLogin, request); + } else { + Debug.logWarning("Could not find userLogin for external login key: " + externalKey, module); + } + + return "success"; + } + + private static boolean isAjax(HttpServletRequest request) { + return "XMLHttpRequest".equals(request.getHeader("X-Requested-With")); + } + +} Propchange: ofbiz/trunk/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ExternalLoginKeysManager.java ------------------------------------------------------------------------------ svn:eol-style = native Propchange: ofbiz/trunk/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ExternalLoginKeysManager.java ------------------------------------------------------------------------------ svn:keywords = Date Rev Author URL Id Propchange: ofbiz/trunk/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/ExternalLoginKeysManager.java ------------------------------------------------------------------------------ svn:mime-type = text/plain Modified: ofbiz/trunk/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/LoginEventListener.java URL: http://svn.apache.org/viewvc/ofbiz/trunk/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/LoginEventListener.java?rev=1775807&r1=1775806&r2=1775807&view=diff ============================================================================== --- ofbiz/trunk/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/LoginEventListener.java (original) +++ ofbiz/trunk/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/LoginEventListener.java Fri Dec 23 09:35:26 2016 @@ -23,8 +23,6 @@ import javax.servlet.http.HttpSession; import javax.servlet.http.HttpSessionEvent; import javax.servlet.http.HttpSessionListener; -import org.apache.ofbiz.webapp.control.LoginWorker; - /** * HttpSessionListener that finalizes login information */ @@ -41,6 +39,6 @@ public class LoginEventListener implemen public void sessionDestroyed(HttpSessionEvent event) { HttpSession session = event.getSession(); - LoginWorker.cleanupExternalLoginKey(session); + ExternalLoginKeysManager.cleanupExternalLoginKey(session); } } Modified: ofbiz/trunk/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/LoginWorker.java URL: http://svn.apache.org/viewvc/ofbiz/trunk/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/LoginWorker.java?rev=1775807&r1=1775806&r2=1775807&view=diff ============================================================================== --- ofbiz/trunk/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/LoginWorker.java (original) +++ ofbiz/trunk/framework/webapp/src/main/java/org/apache/ofbiz/webapp/control/LoginWorker.java Fri Dec 23 09:35:26 2016 @@ -29,8 +29,6 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.ServiceLoader; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -90,13 +88,10 @@ public class LoginWorker { public final static String module = LoginWorker.class.getName(); public static final String resourceWebapp = "SecurityextUiLabels"; - public static final String EXTERNAL_LOGIN_KEY_ATTR = "externalLoginKey"; public static final String X509_CERT_ATTR = "SSLx509Cert"; public static final String securityProperties = "security.properties"; private static final String keyValue = UtilProperties.getPropertyValue(securityProperties, "login.secret_key_string"); - /** This Map is keyed by the randomly generated externalLoginKey and the value is a UserLogin GenericValue object */ - private static Map<String, GenericValue> externalLoginKeys = new ConcurrentHashMap<String, GenericValue>(); public static StringWrapper makeLoginUrl(PageContext pageContext) { return makeLoginUrl(pageContext, "checkLogin"); @@ -128,55 +123,6 @@ public class LoginWorker { return StringUtil.wrapString(loginUrl); } - /** - * Gets (and creates if necessary) a key to be used for an external login parameter - */ - public static String getExternalLoginKey(HttpServletRequest request) { - Delegator delegator = (Delegator) request.getAttribute("delegator"); - boolean externalLoginKeyEnabled = "true".equals(EntityUtilProperties.getPropertyValue("security", "security.login.externalLoginKey.enabled", "true", delegator)); - if (!externalLoginKeyEnabled) { - return null; - } - //Debug.logInfo("Running getExternalLoginKey, externalLoginKeys.size=" + externalLoginKeys.size(), module); - GenericValue userLogin = (GenericValue) request.getAttribute("userLogin"); - - String externalKey = (String) request.getAttribute(EXTERNAL_LOGIN_KEY_ATTR); - if (externalKey != null) return externalKey; - - HttpSession session = request.getSession(); - synchronized (session) { - // if the session has a previous key in place, remove it from the master list - String sesExtKey = (String) session.getAttribute(EXTERNAL_LOGIN_KEY_ATTR); - - if (sesExtKey != null) { - if (isAjax(request)) return sesExtKey; - - externalLoginKeys.remove(sesExtKey); - } - - //check the userLogin here, after the old session setting is set so that it will always be cleared - if (userLogin == null) return ""; - - //no key made yet for this request, create one - while (externalKey == null || externalLoginKeys.containsKey(externalKey)) { - UUID uuid = UUID.randomUUID(); - externalKey = "EL" + uuid.toString(); - } - - request.setAttribute(EXTERNAL_LOGIN_KEY_ATTR, externalKey); - session.setAttribute(EXTERNAL_LOGIN_KEY_ATTR, externalKey); - externalLoginKeys.put(externalKey, userLogin); - return externalKey; - } - } - - public static void cleanupExternalLoginKey(HttpSession session) { - String sesExtKey = (String) session.getAttribute(EXTERNAL_LOGIN_KEY_ATTR); - if (sesExtKey != null) { - externalLoginKeys.remove(sesExtKey); - } - } - public static void setLoggedOut(String userLoginId, Delegator delegator) { if (UtilValidate.isEmpty(userLoginId)) { Debug.logWarning("Called setLogged out with empty userLoginId", module); @@ -567,7 +513,7 @@ public class LoginWorker { } } - private static void setWebContextObjects(HttpServletRequest request, HttpServletResponse response, Delegator delegator, LocalDispatcher dispatcher) { + protected static void setWebContextObjects(HttpServletRequest request, HttpServletResponse response, Delegator delegator, LocalDispatcher dispatcher) { HttpSession session = request.getSession(); // NOTE: we do NOT want to set this in the servletContext, only in the request and session // We also need to setup the security objects since they are dependent on the delegator @@ -1018,46 +964,6 @@ public class LoginWorker { return count > 0; } - public static String checkExternalLoginKey(HttpServletRequest request, HttpServletResponse response) { - HttpSession session = request.getSession(); - - String externalKey = request.getParameter(LoginWorker.EXTERNAL_LOGIN_KEY_ATTR); - if (externalKey == null) return "success"; - - GenericValue userLogin = LoginWorker.externalLoginKeys.get(externalKey); - if (userLogin != null) { - //to check it's the right tenant - //in case username and password are the same in different tenants - Delegator delegator = (Delegator) request.getAttribute("delegator"); - String oldDelegatorName = delegator.getDelegatorName(); - if (!oldDelegatorName.equals(userLogin.getDelegator().getDelegatorName())) { - delegator = DelegatorFactory.getDelegator(userLogin.getDelegator().getDelegatorName()); - LocalDispatcher dispatcher = WebAppUtil.makeWebappDispatcher(session.getServletContext(), delegator); - setWebContextObjects(request, response, delegator, dispatcher); - } - // found userLogin, do the external login... - - // if the user is already logged in and the login is different, logout the other user - GenericValue currentUserLogin = (GenericValue) session.getAttribute("userLogin"); - if (currentUserLogin != null) { - if (currentUserLogin.getString("userLoginId").equals(userLogin.getString("userLoginId"))) { - // is the same user, just carry on... - return "success"; - } - - // logout the current user and login the new user... - logout(request, response); - // ignore the return value; even if the operation failed we want to set the new UserLogin - } - - doBasicLogin(userLogin, request); - } else { - Debug.logWarning("Could not find userLogin for external login key: " + externalKey, module); - } - - return "success"; - } - public static boolean isFlaggedLoggedOut(GenericValue userLogin, Delegator delegator) { if ("true".equalsIgnoreCase(EntityUtilProperties.getPropertyValue("security", "login.disable.global.logout", delegator))) { return false; @@ -1159,10 +1065,6 @@ public class LoginWorker { return userLoginSessionMap; } - public static boolean isAjax(HttpServletRequest request) { - return "XMLHttpRequest".equals(request.getHeader("X-Requested-With")); - } - public static String autoChangePassword(HttpServletRequest request, HttpServletResponse response) { Delegator delegator = (Delegator) request.getAttribute("delegator"); String userName = request.getParameter("USERNAME"); Modified: ofbiz/trunk/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/ScreenRenderer.java URL: http://svn.apache.org/viewvc/ofbiz/trunk/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/ScreenRenderer.java?rev=1775807&r1=1775806&r2=1775807&view=diff ============================================================================== --- ofbiz/trunk/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/ScreenRenderer.java (original) +++ ofbiz/trunk/framework/widget/src/main/java/org/apache/ofbiz/widget/renderer/ScreenRenderer.java Fri Dec 23 09:35:26 2016 @@ -21,7 +21,6 @@ package org.apache.ofbiz.widget.renderer import java.io.IOException; import java.io.StringWriter; import java.io.Writer; -import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Locale; @@ -51,6 +50,7 @@ import org.apache.ofbiz.security.Securit import org.apache.ofbiz.service.DispatchContext; import org.apache.ofbiz.service.GenericServiceException; import org.apache.ofbiz.service.LocalDispatcher; +import org.apache.ofbiz.webapp.control.ExternalLoginKeysManager; import org.apache.ofbiz.webapp.control.LoginWorker; import org.apache.ofbiz.webapp.website.WebSiteWorker; import org.apache.ofbiz.widget.cache.GenericWidgetOutput; @@ -257,7 +257,7 @@ public class ScreenRenderer { context.put("contextRoot", request.getAttribute("_CONTEXT_ROOT_")); context.put("serverRoot", request.getAttribute("_SERVER_ROOT_URL_")); context.put("checkLoginUrl", LoginWorker.makeLoginUrl(request)); - String externalLoginKey = LoginWorker.getExternalLoginKey(request); + String externalLoginKey = ExternalLoginKeysManager.getExternalLoginKey(request); String externalKeyParam = externalLoginKey == null ? "" : "&externalLoginKey=" + externalLoginKey; context.put("externalLoginKey", externalLoginKey); context.put("externalKeyParam", externalKeyParam); Modified: ofbiz/trunk/specialpurpose/ecommerce/webapp/ecommerce/WEB-INF/controller.xml URL: http://svn.apache.org/viewvc/ofbiz/trunk/specialpurpose/ecommerce/webapp/ecommerce/WEB-INF/controller.xml?rev=1775807&r1=1775806&r2=1775807&view=diff ============================================================================== --- ofbiz/trunk/specialpurpose/ecommerce/webapp/ecommerce/WEB-INF/controller.xml (original) +++ ofbiz/trunk/specialpurpose/ecommerce/webapp/ecommerce/WEB-INF/controller.xml Fri Dec 23 09:35:26 2016 @@ -49,7 +49,7 @@ under the License. <!-- Events to run on every request before security (chains exempt) --> <preprocessor> <!-- This event allows affilate/distributor entry on any page --> - <event name="checkExternalLoginKey" type="java" path="org.apache.ofbiz.webapp.control.LoginWorker" invoke="checkExternalLoginKey"/> + <event name="checkExternalLoginKey" type="java" path="org.apache.ofbiz.webapp.control.ExternalLoginKeysManager" invoke="checkExternalLoginKey"/> <event name="setAssociationId" type="java" path="org.apache.ofbiz.ecommerce.misc.ThirdPartyEvents" invoke="setAssociationId"/> <event name="checkTrackingCodeUrlParam" type="java" path="org.apache.ofbiz.marketing.tracking.TrackingCodeEvents" invoke="checkTrackingCodeUrlParam"/> <event name="checkPartnerTrackingCodeUrlParam" type="java" path="org.apache.ofbiz.marketing.tracking.TrackingCodeEvents" invoke="checkPartnerTrackingCodeUrlParam"/> Modified: ofbiz/trunk/specialpurpose/solr/webapp/solr/WEB-INF/controller.xml URL: http://svn.apache.org/viewvc/ofbiz/trunk/specialpurpose/solr/webapp/solr/WEB-INF/controller.xml?rev=1775807&r1=1775806&r2=1775807&view=diff ============================================================================== --- ofbiz/trunk/specialpurpose/solr/webapp/solr/WEB-INF/controller.xml (original) +++ ofbiz/trunk/specialpurpose/solr/webapp/solr/WEB-INF/controller.xml Fri Dec 23 09:35:26 2016 @@ -10,7 +10,7 @@ <!-- Events to run on every request before security (chains exempt) --> <preprocessor> - <event name="checkExternalLoginKey" type="java" path="org.apache.ofbiz.webapp.control.LoginWorker" invoke="checkExternalLoginKey" /> + <event name="checkExternalLoginKey" type="java" path="org.apache.ofbiz.webapp.control.ExternalLoginKeysManager" invoke="checkExternalLoginKey" /> </preprocessor> <!-- Security Mappings --> Modified: ofbiz/trunk/specialpurpose/webpos/webapp/webpos/WEB-INF/controller.xml URL: http://svn.apache.org/viewvc/ofbiz/trunk/specialpurpose/webpos/webapp/webpos/WEB-INF/controller.xml?rev=1775807&r1=1775806&r2=1775807&view=diff ============================================================================== --- ofbiz/trunk/specialpurpose/webpos/webapp/webpos/WEB-INF/controller.xml (original) +++ ofbiz/trunk/specialpurpose/webpos/webapp/webpos/WEB-INF/controller.xml Fri Dec 23 09:35:26 2016 @@ -40,7 +40,7 @@ <!-- Events to run on every request before security (chains exempt) --> <preprocessor> <!-- This event allows affilate/distributor entry on any page --> - <event name="checkExternalLoginKey" type="java" path="org.apache.ofbiz.webapp.control.LoginWorker" invoke="checkExternalLoginKey"/> + <event name="checkExternalLoginKey" type="java" path="org.apache.ofbiz.webapp.control.ExternalLoginKeysManager" invoke="checkExternalLoginKey"/> <event name="setAssociationId" type="java" path="org.apache.ofbiz.ecommerce.misc.ThirdPartyEvents" invoke="setAssociationId"/> <event name="checkTrackingCodeUrlParam" type="java" path="org.apache.ofbiz.marketing.tracking.TrackingCodeEvents" invoke="checkTrackingCodeUrlParam"/> <event name="checkPartnerTrackingCodeUrlParam" type="java" path="org.apache.ofbiz.marketing.tracking.TrackingCodeEvents" invoke="checkPartnerTrackingCodeUrlParam"/> |
Free forum by Nabble | Edit this page |