/* * See the NOTICE file distributed with this work for additional * information regarding copyright ownership. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package com.xpn.xwiki.web; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.net.URLEncoder; import java.util.Collections; import java.util.List; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.exception.ExceptionUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xwiki.model.EntityType; import org.xwiki.model.reference.DocumentReference; import org.xwiki.model.reference.EntityReference; import org.xwiki.model.reference.EntityReferenceResolver; import org.xwiki.model.reference.EntityReferenceSerializer; import org.xwiki.resource.internal.entity.EntityResourceActionLister; import com.xpn.xwiki.XWiki; import com.xpn.xwiki.XWikiContext; import com.xpn.xwiki.XWikiException; import com.xpn.xwiki.doc.DeletedAttachment; import com.xpn.xwiki.doc.XWikiAttachment; import com.xpn.xwiki.doc.XWikiDocument; public class XWikiServletURLFactory extends XWikiDefaultURLFactory { private static final Logger LOGGER = LoggerFactory.getLogger(XWikiServletURLFactory.class); private EntityReferenceResolver<String> relativeEntityReferenceResolver; private EntityResourceActionLister actionLister; /** * This is the URL which was requested by the user possibly with the host modified if x-forwarded-host header is set * or if xwiki.home parameter is set and we are viewing the main page. */ protected URL serverURL; protected String contextPath; public XWikiServletURLFactory() { } // Used by tests public XWikiServletURLFactory(URL serverURL, String contextPath, String actionPath) { this.serverURL = serverURL; this.contextPath = contextPath; } /** * Creates a new URL factory that uses the server URL and context path specified by the given XWiki context. This * constructor should be used only in tests. Make sure {@link XWikiContext#setURL(URL)} is called before this * constructor. * * @param context */ public XWikiServletURLFactory(XWikiContext context) { init(context); } @Override public void init(XWikiContext context) { this.contextPath = context.getWiki().getWebAppPath(context); try { this.serverURL = new URL(getProtocol(context) + "://" + getHost(context)); } catch (MalformedURLException e) { // This can't happen. } } /** * Returns the part of the URL identifying the web application. In a normal install, that is <tt>xwiki/</tt>. * * @return The configured context path. */ public String getContextPath() { return this.contextPath; } /** * @param context the XWiki context used to access the request object * @return the value of the {@code xwiki.url.protocol} configuration parameter, if defined, otherwise the protocol * used to make the request to the proxy server if we are behind one, otherwise the protocol of the URL used * to make the current request */ private String getProtocol(XWikiContext context) { // Tests usually set the context URL but don't set the request object. String protocol = context.getURL().getProtocol(); if (context.getRequest() != null) { protocol = context.getRequest().getScheme(); if ("http".equalsIgnoreCase(protocol) && context.getRequest().isSecure()) { // This can happen in reverse proxy mode, if the proxy server receives HTTPS requests and forwards them // as HTTP to the internal web server running XWiki. protocol = "https"; } } // Detected protocol can be overwritten by configuration. return context.getWiki().Param("xwiki.url.protocol", protocol); } /** * @param context the XWiki context used to access the request object * @return the proxy host, if we are behind one, otherwise the host of the URL used to make the current request */ private String getHost(XWikiContext context) { URL url = context.getURL(); // Check reverse-proxy mode (e.g. Apache's mod_proxy_http). String proxyHost = StringUtils.substringBefore(context.getRequest().getHeader("x-forwarded-host"), ","); if (!StringUtils.isEmpty(proxyHost)) { return proxyHost; } // If the reverse proxy does not support the x-forwarded-host parameter // we allow the user to force the the host name by using the xwiki.home param. final URL homeParam = getXWikiHomeParameter(context); if (homeParam != null && context.isMainWiki()) { url = homeParam; } return url.getHost() + (url.getPort() < 0 ? "" : (":" + url.getPort())); } /** @return a URL made from the xwiki.cfg parameter xwiki.home or null if undefined or unparsable. */ private static URL getXWikiHomeParameter(final XWikiContext context) { final String surl = getXWikiHomeParameterAsString(context); if (!StringUtils.isEmpty(surl)) { try { return normalizeURL(surl, context); } catch (MalformedURLException e) { LOGGER.warn("Could not create URL from xwiki.cfg xwiki.home parameter: " + surl + " Ignoring parameter."); } } return null; } /** @return the xwiki.home parameter or null if undefined. */ private static String getXWikiHomeParameterAsString(final XWikiContext context) { return context.getWiki().Param("xwiki.home", null); } @Override public URL getServerURL(XWikiContext context) throws MalformedURLException { return getServerURL(context.getWikiId(), context); } /** * Get the url of the server EG: http://www.xwiki.org/ This function sometimes will return a URL with a trailing / * and other times not. This is because the xwiki.home param is recommended to have a trailing / but this.serverURL * never does. * * @param wikiId the identifier of the wiki, if null it is assumed to be the same as the wiki which we are currently * displaying. * @param context the XWikiContext used to determine the current wiki and the value if the xwiki.home parameter if * needed as well as access the xwiki server document if in virtual mode. * @return a URL containing the protocol, host, and port (if applicable) of the server to use for the given * database. */ public URL getServerURL(String wikiId, XWikiContext context) throws MalformedURLException { if (wikiId == null || wikiId.equals(context.getOriginalWikiId())) { // This is the case if we are getting a URL for a page which is in // the same wiki as the page which is now being displayed. return this.serverURL; } if (context.isMainWiki(wikiId)) { // Not in the same wiki so we are in a subwiki and we want a URL which points to the main wiki. // if xwiki.home is set then lets return that. final URL homeParam = getXWikiHomeParameter(context); if (homeParam != null) { return homeParam; } } URL url = context.getWiki().getServerURL(wikiId, context); return url == null ? this.serverURL : url; } @Override public URL createURL(String spaces, String name, String action, boolean redirect, XWikiContext context) { return createURL(spaces, name, action, context); } @Override public URL createURL(String spaces, String name, String action, String querystring, String anchor, String xwikidb, XWikiContext context) { // Action and Query String transformers if (("view".equals(action)) && (context.getLinksAction() != null)) { action = context.getLinksAction(); } if (context.getLinksQueryString() != null) { if (querystring == null) { querystring = context.getLinksQueryString(); } else { querystring = querystring + "&" + context.getLinksQueryString(); } } StringBuilder path = new StringBuilder(this.contextPath); addServletPath(path, xwikidb, context); // Parse the spaces list into Space References EntityReference spaceReference = getRelativeEntityReferenceResolver().resolve(spaces, EntityType.SPACE); // For how to encode the various parts of the URL, see http://stackoverflow.com/a/29948396/153102 addAction(path, spaceReference, action, context); addSpaces(path, spaceReference, action, context); addName(path, name, action, context); if (!StringUtils.isEmpty(querystring)) { path.append("?"); path.append(StringUtils.removeEnd(StringUtils.removeEnd(querystring, "&"), "&")); } if (!StringUtils.isEmpty(anchor)) { path.append("#"); path.append(encodeWithinQuery(anchor, context)); } URL result; try { result = normalizeURL(new URL(getServerURL(xwikidb, context), path.toString()), context); } catch (MalformedURLException e) { // This should not happen result = null; } return result; } private void addServletPath(StringBuilder path, String xwikidb, XWikiContext context) { if (xwikidb == null) { xwikidb = context.getWikiId(); } path.append(context.getWiki().getServletPath(xwikidb, context)); } private void addAction(StringBuilder path, EntityReference spaceReference, String action, XWikiContext context) { boolean showViewAction = context.getWiki().showViewAction(context); // - Always output the action if it's not "view" or if showViewAction is true // - Output "view/<first space name>" when the first space name is an action name and the action is View // (and showViewAction = false) if ((!"view".equals(action) || (showViewAction)) || (!showViewAction && spaceReference != null && "view".equals(action) && getActionLister().listActions().contains( spaceReference.extractFirstReference(EntityType.SPACE).getName()))) { path.append(action).append("/"); } } /** * Add the spaces to the path. */ private void addSpaces(StringBuilder path, EntityReference spaceReference, String action, XWikiContext context) { for (EntityReference reference : spaceReference.getReversedReferenceChain()) { appendSpacePathSegment(path, reference, context); } } private void appendSpacePathSegment(StringBuilder path, EntityReference spaceReference, XWikiContext context) { path.append(encodeWithinPath(spaceReference.getName(), context)).append('/'); } /** * Add the page name to the path. */ private void addName(StringBuilder path, String name, String action, XWikiContext context) { XWiki xwiki = context.getWiki(); if ((xwiki.useDefaultAction(context)) || (!name.equals(xwiki.getDefaultPage(context)) || (!"view".equals(action)))) { path.append(encodeWithinPath(name, context)); } } protected void addFileName(StringBuilder path, String fileName, XWikiContext context) { addFileName(path, fileName, true, context); } protected void addFileName(StringBuilder path, String fileName, boolean encode, XWikiContext context) { path.append("/"); if (encode) { // Encode the given file name as a single path segment. path.append(encodeWithinPath(fileName, context).replace("+", "%20")); } else { try { // The given file name is actually a file path and so we need to encode each path segment separately. path.append(new URI(null, null, fileName, null)); } catch (URISyntaxException e) { LOGGER.debug("Failed to encode the file path [{}]. Root cause: [{}]", fileName, ExceptionUtils.getRootCauseMessage(e)); // Use the raw file path as a fall-back. path.append(fileName); } } } /** * Encode a URL path following the URL specification so that space is encoded as {@code %20} in the path * (and not as {@code +} wnich is not correct). Note that for all other characters we encode them even though some * don't need to be encoded. For example we encode the single quote even though it's not necessary * (see <a href="http://tinyurl.com/j6bjgaq">this explanation</a>). The reason is that otherwise it becomes * dangerous to use a returned URL in the HREF attribute in HTML. Imagine the following {@code <a href='$url'...} * and {@code #set ($url = $doc.getURL(...))}. Now let's assume that {@code $url}'s value is * {@code http://localhost:8080/xwiki/bin/view/A'/B}. This would generate a HTML of * {@code <a href='http://localhost:8080/xwiki/bin/view/A'/B'} which would generated a wrong link to * {@code http://localhost:8080/xwiki/bin/view/A}... Thus if we were only encoding the characters that require * encoding, we would need HMTL writers to encode the received URL and right now we don't do that anywhere in our * code. Thus in order to not introduce any problem and keep it safe we just handle the {@code +} character * specially and encode the rest. * * @param name the path to encode * @param context see {@link XWikiContext} * @return the URL-encoded path segment */ private String encodeWithinPath(String name, XWikiContext context) { // Note: Ideally the following would have been the correct way of writing this method but it causes the issues // mentioned in the javadoc of this method // String encodedName; // try { // encodedName = URIUtil.encodeWithinPath(name, "UTF-8"); // } catch (URIException e) { // throw new RuntimeException("Missing charset [UTF-8]", e); // } // return encodedName; String encodedName; try { encodedName = URLEncoder.encode(name, "UTF-8"); } catch (Exception e) { // Should not happen (UTF-8 is always available) throw new RuntimeException("Missing charset [UTF-8]", e); } // The previous call will convert " " into "+" (and "+" into "%2B") so we need to convert "+" into "%20" encodedName = encodedName.replaceAll("\\+", "%20"); return encodedName; } /** * Same rationale as {@link #encodeWithinPath(String, XWikiContext)}. Note that we also encode spaces as {@code %20} * even though we could also have encoded them as {@code +}. We do this for consistency (it allows to have the same * implementation for both URL paths and query string). * * @param name the query string part to encode * @param context see {@link XWikiContext} * @return the URL-encoded query string part */ private String encodeWithinQuery(String name, XWikiContext context) { // Note: Ideally the following would have been the correct way of writing this method but it causes the issues // mentioned in the javadoc of this method // String encodedName; // try { // encodedName = URIUtil.encodeWithinQuery(name, "UTF-8"); // } catch (URIException e) { // throw new RuntimeException("Missing charset [UTF-8]", e); // } // return encodedName; return encodeWithinPath(name, context); } @Override public URL createExternalURL(String spaces, String name, String action, String querystring, String anchor, String xwikidb, XWikiContext context) { return this.createURL(spaces, name, action, querystring, anchor, xwikidb, context); } @Override public URL createSkinURL(String filename, String skin, XWikiContext context) { StringBuilder path = new StringBuilder(this.contextPath); path.append("skins/"); path.append(skin); addFileName(path, filename, false, context); try { return normalizeURL(new URL(getServerURL(context), path.toString()), context); } catch (MalformedURLException e) { // This should not happen return null; } } @Override public URL createSkinURL(String filename, String spaces, String name, String xwikidb, XWikiContext context) { StringBuilder path = new StringBuilder(this.contextPath); addServletPath(path, xwikidb, context); // Parse the spaces list into Space References EntityReference spaceReference = getRelativeEntityReferenceResolver().resolve(spaces, EntityType.SPACE); addAction(path, null, "skin", context); addSpaces(path, spaceReference, "skin", context); addName(path, name, "skin", context); addFileName(path, filename, false, context); try { return normalizeURL(new URL(getServerURL(xwikidb, context), path.toString()), context); } catch (MalformedURLException e) { // This should not happen return null; } } @Override public URL createResourceURL(String filename, boolean forceSkinAction, XWikiContext context) { StringBuilder path = new StringBuilder(this.contextPath); if (forceSkinAction) { addServletPath(path, context.getWikiId(), context); addAction(path, null, "skin", context); } path.append("resources"); addFileName(path, filename, false, context); try { return normalizeURL(new URL(getServerURL(context), path.toString()), context); } catch (MalformedURLException e) { // This should not happen return null; } } public URL createTemplateURL(String fileName, XWikiContext context) { StringBuilder path = new StringBuilder(this.contextPath); path.append("templates"); addFileName(path, fileName, false, context); try { return normalizeURL(new URL(getServerURL(context), path.toString()), context); } catch (MalformedURLException e) { // This should not happen return null; } } @Override public URL createAttachmentURL(String filename, String spaces, String name, String action, String querystring, String xwikidb, XWikiContext context) { if ((context != null) && "viewrev".equals(context.getAction()) && context.get("rev") != null && isContextDoc(xwikidb, spaces, name, context)) { try { String docRevision = context.get("rev").toString(); XWikiAttachment attachment = findAttachmentForDocRevision(context.getDoc(), docRevision, filename, context); if (attachment == null) { action = "viewattachrev"; } else { long arbId = findDeletedAttachmentForDocRevision(context.getDoc(), docRevision, filename, context); return createAttachmentRevisionURL(filename, spaces, name, attachment.getVersion(), arbId, querystring, xwikidb, context); } } catch (XWikiException e) { if (LOGGER.isErrorEnabled()) { LOGGER.error("Exception while trying to get attachment version !", e); } } } StringBuilder path = new StringBuilder(this.contextPath); addServletPath(path, xwikidb, context); // Parse the spaces list into Space References EntityReference spaceReference = getRelativeEntityReferenceResolver().resolve(spaces, EntityType.SPACE); addAction(path, spaceReference, action, context); addSpaces(path, spaceReference, action, context); addName(path, name, action, context); addFileName(path, filename, context); if (!StringUtils.isEmpty(querystring)) { path.append("?"); path.append(StringUtils.removeEnd(StringUtils.removeEnd(querystring, "&"), "&")); } try { return normalizeURL(new URL(getServerURL(xwikidb, context), path.toString()), context); } catch (Exception e) { return null; } } /** * Check if a document is the original context document. This is needed when generating attachment revision URLs, * since only attachments of the context document should also be versioned. * * @param wiki the wiki name of the document to check * @param spaces the space names of the document to check * @param name the document name of the document to check * @param context the current request context * @return {@code true} if the provided document is the same as the current context document, {@code false} * otherwise */ protected boolean isContextDoc(String wiki, String spaces, String name, XWikiContext context) { if (context == null || context.getDoc() == null) { return false; } // Use the local serializer so that we don't serialize the wiki part since all we want to do is compare the // passed spaces represented as a String with the current doc's spaces. EntityReferenceSerializer<String> serializer = Utils.getComponent(EntityReferenceSerializer.TYPE_STRING, "local"); DocumentReference currentDocumentReference = context.getDoc().getDocumentReference(); return serializer.serialize(currentDocumentReference.getLastSpaceReference()).equals(spaces) && currentDocumentReference.getName().equals(name) && (wiki == null || currentDocumentReference.getWikiReference().getName().equals(wiki)); } @Override public URL createAttachmentRevisionURL(String filename, String spaces, String name, String revision, String querystring, String xwikidb, XWikiContext context) { return createAttachmentRevisionURL(filename, spaces, name, revision, -1, querystring, xwikidb, context); } public URL createAttachmentRevisionURL(String filename, String spaces, String name, String revision, long recycleId, String querystring, String xwikidb, XWikiContext context) { String action = "downloadrev"; StringBuilder path = new StringBuilder(this.contextPath); addServletPath(path, xwikidb, context); // Parse the spaces list into Space References EntityReference spaceReference = getRelativeEntityReferenceResolver().resolve(spaces, EntityType.SPACE); addAction(path, spaceReference, action, context); addSpaces(path, spaceReference, action, context); addName(path, name, action, context); addFileName(path, filename, context); String qstring = "rev=" + revision; if (recycleId >= 0) { qstring += "&rid=" + recycleId; } if (!StringUtils.isEmpty(querystring)) { qstring += "&" + querystring; } path.append("?"); path.append(StringUtils.removeEnd(StringUtils.removeEnd(qstring, "&"), "&")); try { return normalizeURL(new URL(getServerURL(xwikidb, context), path.toString()), context); } catch (MalformedURLException e) { // This should not happen return null; } } /** * Converts a URL to a relative URL if it's a XWiki URL (keeping only the path + query string + anchor) and leave * the URL unchanged if it's an external URL. * <p> * An URL is considered to be external if its server component doesn't match the server of the current request URL. * This means that URLs are made relative with respect to the current request URL rather than the current wiki set * on the XWiki context. Let's take an example: * * <pre> * {@code * request URL: http://playground.xwiki.org/xwiki/bin/view/Sandbox/TestURL * current wiki: code (code.xwiki.org) * URL (1): http://code.xwiki.org/xwiki/bin/view/Main/WebHome * URL (2): http://playground.xwiki.org/xwiki/bin/view/Spage/Page * * The result will be: * (1) http://code.xwiki.org/xwiki/bin/view/Main/WebHome * (2) /xwiki/bin/view/Spage/Page * } * </pre> * * @param url the URL to convert * @return the converted URL as a string * @see com.xpn.xwiki.web.XWikiDefaultURLFactory#getURL(java.net.URL, com.xpn.xwiki.XWikiContext) */ @Override public String getURL(URL url, XWikiContext context) { String relativeURL = ""; try { if (url != null) { String surl = url.toString(); if (!surl.startsWith(this.serverURL.toString())) { // External URL: leave it as is. relativeURL = surl; } else { // Internal XWiki URL: convert to relative. StringBuilder relativeURLBuilder = new StringBuilder(url.getPath()); String querystring = url.getQuery(); if (!StringUtils.isEmpty(querystring)) { relativeURLBuilder.append("?").append( StringUtils.removeEnd(StringUtils.removeEnd(querystring, "&"), "&")); } String anchor = url.getRef(); if (!StringUtils.isEmpty(anchor)) { relativeURLBuilder.append("#").append(anchor); } relativeURL = relativeURLBuilder.toString(); } } } catch (Exception e) { LOGGER.error("Failed to create URL", e); } return StringUtils.defaultIfEmpty(relativeURL, "/"); } @Override public URL getRequestURL(XWikiContext context) { final URL url = super.getRequestURL(context); try { final URL servurl = getServerURL(context); // if use apache mod_proxy we needed to know external host address return normalizeURL(new URL(servurl.getProtocol(), servurl.getHost(), servurl.getPort(), url.getFile()), context); } catch (MalformedURLException e) { // This should not happen LOGGER.error("Failed to create request URL", e); } return url; } public XWikiAttachment findAttachmentForDocRevision(XWikiDocument doc, String docRevision, String filename, XWikiContext context) throws XWikiException { XWikiAttachment attachment = null; XWikiDocument rdoc = context.getWiki().getDocument(doc, docRevision, context); if (filename != null) { attachment = rdoc.getAttachment(filename); } return attachment; } public long findDeletedAttachmentForDocRevision(XWikiDocument doc, String docRevision, String filename, XWikiContext context) throws XWikiException { XWikiAttachment attachment = null; XWikiDocument rdoc = context.getWiki().getDocument(doc, docRevision, context); if (context.getWiki().hasAttachmentRecycleBin(context) && filename != null) { attachment = rdoc.getAttachment(filename); if (attachment != null) { List<DeletedAttachment> deleted = context.getWiki().getAttachmentRecycleBinStore() .getAllDeletedAttachments(attachment, context, true); Collections.reverse(deleted); for (DeletedAttachment entry : deleted) { if (entry.getDate().after(rdoc.getDate())) { return entry.getId(); } } } } return -1; } /** * Encodes the passed URL and offers the possibility for Servlet Filter to perform URL rewriting (this is used for * example by Tuckey's URLRewriteFilter for rewriting outbound URLs, see * http://platform.xwiki.org/xwiki/bin/view/Main/ShortURLs). * <p> * However Servlet Container will also add a ";jsessionid=xxx" content to the URL while encoding the URL and we * strip it since we don't want to have that in our URLs as it can cause issues with: * <ul> * <li>security</li> * <li>SEO</li> * <li>clients not expecting jsessionid in URL, for example RSS feed readers which will think that articles are * different as they'll get different URLs everytime they call the XWiki server</li> * </ul> * See why jsessionid are considered harmful <a * href="https://randomcoder.org/articles/jsessionid-considered-harmful">here</a> and <a * href="http://java.dzone.com/articles/java-jsessionid-harmful">here</a> * * @param url the URL to encode and normalize * @param context the XWiki Context used to get access to the Response for encoding the URL * @return the normalized URL * @throws MalformedURLException if the passed URL is invalid */ protected static URL normalizeURL(URL url, XWikiContext context) throws MalformedURLException { return normalizeURL(url.toExternalForm(), context); } /** * Encodes the passed URL and offers the possibility for Servlet Filter to perform URL rewriting (this is used for * example by Tuckey's URLRewriteFilter for rewriting outbound URLs, see * http://platform.xwiki.org/xwiki/bin/view/Main/ShortURLs). * <p> * However Servlet Container will also add a ";jsessionid=xxx" content to the URL while encoding the URL and we * strip it since we don't want to have that in our URLs as it can cause issues with: * <ul> * <li>security</li> * <li>SEO</li> * <li>clients not expecting jsessionid in URL, for example RSS feed readers which will think that articles are * different as they'll get different URLs everytime they call the XWiki server</li> * </ul> * See why jsessionid are considered harmful <a * href="https://randomcoder.org/articles/jsessionid-considered-harmful">here</a> and <a * href="http://java.dzone.com/articles/java-jsessionid-harmful">here</a> * * @param url the URL to encode and normalize * @param context the XWiki Context used to get access to the Response for encoding the URL * @return the normalized URL * @throws MalformedURLException if the passed URL is invalid */ protected static URL normalizeURL(String url, XWikiContext context) throws MalformedURLException { // For robust session tracking, all URLs emitted by a servlet should be encoded. Otherwise, URL rewriting // cannot be used with browsers which do not support cookies. String encodedURLAsString = context.getResponse().encodeURL(url); // Remove a potential jsessionid in the URL encodedURLAsString = encodedURLAsString.replaceAll(";jsessionid=.*?(?=\\?|$)", ""); return new URL(encodedURLAsString); } private EntityReferenceResolver<String> getRelativeEntityReferenceResolver() { if (this.relativeEntityReferenceResolver == null) { this.relativeEntityReferenceResolver = Utils.getComponent(EntityReferenceResolver.TYPE_STRING, "relative"); } return this.relativeEntityReferenceResolver; } private EntityResourceActionLister getActionLister() { if (this.actionLister == null) { this.actionLister = Utils.getComponent(EntityResourceActionLister.class); } return this.actionLister; } }