1. Project Clover database Tue Dec 20 2016 21:24:09 CET
  2. Package com.xpn.xwiki.user.impl.xwiki

File XWikiAuthServiceImpl.java

 

Coverage histogram

../../../../../../img/srcFileCovDistChart6.png
69% of files have more coverage

Code metrics

132
230
11
1
609
417
90
0.39
20.91
11
8.18

Classes

Class Line # Actions
XWikiAuthServiceImpl 60 230 0% 90 170
0.5442359454.4%
 

Contributing tests

This file is covered by 12 tests. .

Source view

1    /*
2    * See the NOTICE file distributed with this work for additional
3    * information regarding copyright ownership.
4    *
5    * This is free software; you can redistribute it and/or modify it
6    * under the terms of the GNU Lesser General Public License as
7    * published by the Free Software Foundation; either version 2.1 of
8    * the License, or (at your option) any later version.
9    *
10    * This software is distributed in the hope that it will be useful,
11    * but WITHOUT ANY WARRANTY; without even the implied warranty of
12    * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13    * Lesser General Public License for more details.
14    *
15    * You should have received a copy of the GNU Lesser General Public
16    * License along with this software; if not, write to the Free
17    * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
18    * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
19    */
20    package com.xpn.xwiki.user.impl.xwiki;
21   
22    import java.io.IOException;
23    import java.net.URL;
24    import java.security.Principal;
25    import java.util.List;
26    import java.util.Map;
27    import java.util.concurrent.ConcurrentHashMap;
28   
29    import javax.servlet.http.HttpServletRequest;
30    import javax.servlet.http.HttpServletResponse;
31   
32    import org.apache.commons.lang3.StringUtils;
33    import org.securityfilter.authenticator.FormAuthenticator;
34    import org.securityfilter.config.SecurityConfig;
35    import org.securityfilter.filter.SecurityRequestWrapper;
36    import org.securityfilter.filter.URLPatternMatcher;
37    import org.securityfilter.realm.SimplePrincipal;
38    import org.slf4j.Logger;
39    import org.slf4j.LoggerFactory;
40    import org.xwiki.model.EntityType;
41    import org.xwiki.model.reference.DocumentReference;
42    import org.xwiki.model.reference.DocumentReferenceResolver;
43    import org.xwiki.model.reference.EntityReference;
44    import org.xwiki.model.reference.EntityReferenceSerializer;
45   
46    import com.xpn.xwiki.XWiki;
47    import com.xpn.xwiki.XWikiContext;
48    import com.xpn.xwiki.XWikiException;
49    import com.xpn.xwiki.doc.XWikiDocument;
50    import com.xpn.xwiki.objects.BaseObject;
51    import com.xpn.xwiki.objects.classes.PasswordClass;
52    import com.xpn.xwiki.user.api.XWikiUser;
53    import com.xpn.xwiki.web.Utils;
54   
55    /**
56    * Default implementation of {@link com.xpn.xwiki.user.api.XWikiAuthService}.
57    *
58    * @version $Id: 8cb4aca5efc0b1b873559827652f1d359da3f0e6 $
59    */
 
60    public class XWikiAuthServiceImpl extends AbstractXWikiAuthService
61    {
62    private static final Logger LOGGER = LoggerFactory.getLogger(XWikiAuthServiceImpl.class);
63   
64    private static final EntityReference USERCLASS_REFERENCE = new EntityReference("XWikiUsers", EntityType.DOCUMENT,
65    new EntityReference("XWiki", EntityType.SPACE));
66   
67    /**
68    * Used to convert a string into a proper Document Name.
69    */
70    private DocumentReferenceResolver<String> currentDocumentReferenceResolver = Utils.getComponent(
71    DocumentReferenceResolver.TYPE_STRING, "current");
72   
73    /**
74    * Used to convert a Document Reference to a username to a string. Note that we must be careful not to include the
75    * wiki name as part of the serialized name since user names are saved in the database (for example as the document
76    * author when you create a new document) and we're only supposed to save the wiki part when the user is from
77    * another wiki. This should probably be fixed in the future though but it requires changing existing code that
78    * depend on this behavior.
79    */
80    private EntityReferenceSerializer<String> compactWikiEntityReferenceSerializer = Utils.getComponent(
81    EntityReferenceSerializer.TYPE_STRING, "compactwiki");
82   
83    /**
84    * Each wiki has its own authenticator.
85    */
86    protected Map<String, XWikiAuthenticator> authenticators = new ConcurrentHashMap<String, XWikiAuthenticator>();
87   
 
88  13036 toggle protected XWikiAuthenticator getAuthenticator(XWikiContext context) throws XWikiException
89    {
90  13033 String wikiName = context.getWikiId();
91   
92  13029 if (wikiName != null) {
93  13024 wikiName = wikiName.toLowerCase();
94    }
95   
96  13015 XWikiAuthenticator authenticator = this.authenticators.get(wikiName);
97   
98  13030 if (authenticator != null) {
99  12989 return authenticator;
100    }
101   
102  36 try {
103  36 XWiki xwiki = context.getWiki();
104   
105  36 if ("basic".equals(xwiki.Param("xwiki.authentication"))) {
106  0 authenticator = new MyBasicAuthenticator();
107  0 SecurityConfig sconfig = new SecurityConfig(false);
108  0 sconfig.setAuthMethod("BASIC");
109  0 if (xwiki.Param("xwiki.authentication.realmname") != null) {
110  0 sconfig.setRealmName(xwiki.Param("xwiki.authentication.realmname"));
111    } else {
112  0 sconfig.setRealmName("XWiki");
113    }
114  0 authenticator.init(null, sconfig);
115    } else {
116  36 authenticator = new MyFormAuthenticator();
117  36 SecurityConfig sconfig = new SecurityConfig(false);
118   
119  36 sconfig.setAuthMethod("FORM");
120   
121  36 if (xwiki.Param("xwiki.authentication.realmname") != null) {
122  0 sconfig.setRealmName(xwiki.Param("xwiki.authentication.realmname"));
123    } else {
124  36 sconfig.setRealmName("XWiki");
125    }
126   
127  36 if (xwiki.Param("xwiki.authentication.defaultpage") != null) {
128  0 sconfig.setDefaultPage(xwiki.Param("xwiki.authentication.defaultpage"));
129    } else {
130  36 sconfig.setDefaultPage(stripContextPathFromURL(
131    context.getURLFactory().createURL(context.getWiki().getDefaultSpace(context),
132    context.getWiki().getDefaultPage(context), "view", context), context));
133    }
134   
135  36 if (xwiki.Param("xwiki.authentication.loginpage") != null) {
136  0 sconfig.setLoginPage(xwiki.Param("xwiki.authentication.loginpage"));
137    } else {
138  36 sconfig.setLoginPage(stripContextPathFromURL(
139    context.getURLFactory().createURL("XWiki", "XWikiLogin", "login", context), context));
140    }
141   
142  36 if (xwiki.Param("xwiki.authentication.logoutpage") != null) {
143  36 sconfig.setLogoutPage(xwiki.Param("xwiki.authentication.logoutpage"));
144    } else {
145  0 sconfig.setLogoutPage(stripContextPathFromURL(
146    context.getURLFactory().createURL("XWiki", "XWikiLogout", "logout", context), context));
147    }
148   
149  36 if (xwiki.Param("xwiki.authentication.errorpage") != null) {
150  0 sconfig.setErrorPage(xwiki.Param("xwiki.authentication.errorpage"));
151    } else {
152  36 sconfig.setErrorPage(stripContextPathFromURL(
153    context.getURLFactory().createURL("XWiki", "XWikiLogin", "loginerror", context), context));
154    }
155   
156  36 MyPersistentLoginManager persistent = new MyPersistentLoginManager();
157  36 if (xwiki.Param("xwiki.authentication.cookieprefix") != null) {
158  0 persistent.setCookiePrefix(xwiki.Param("xwiki.authentication.cookieprefix"));
159    }
160  36 if (xwiki.Param("xwiki.authentication.cookiepath") != null) {
161  0 persistent.setCookiePath(xwiki.Param("xwiki.authentication.cookiepath"));
162    }
163  36 if (xwiki.Param("xwiki.authentication.cookiedomains") != null) {
164  36 String[] cdomains = StringUtils.split(xwiki.Param("xwiki.authentication.cookiedomains"), ",");
165  36 persistent.setCookieDomains(cdomains);
166    }
167   
168  36 if (xwiki.Param("xwiki.authentication.cookielife") != null) {
169  0 persistent.setCookieLife(xwiki.Param("xwiki.authentication.cookielife"));
170    }
171   
172  36 if (xwiki.Param("xwiki.authentication.protection") != null) {
173  0 persistent.setProtection(xwiki.Param("xwiki.authentication.protection"));
174    }
175   
176  36 if (xwiki.Param("xwiki.authentication.useip") != null) {
177  0 persistent.setUseIP(xwiki.Param("xwiki.authentication.useip"));
178    }
179   
180  36 if (xwiki.Param("xwiki.authentication.encryptionalgorithm") != null) {
181  0 persistent.setEncryptionAlgorithm(xwiki.Param("xwiki.authentication.encryptionalgorithm"));
182    }
183   
184  36 if (xwiki.Param("xwiki.authentication.encryptionmode") != null) {
185  0 persistent.setEncryptionMode(xwiki.Param("xwiki.authentication.encryptionmode"));
186    }
187   
188  36 if (xwiki.Param("xwiki.authentication.encryptionpadding") != null) {
189  0 persistent.setEncryptionPadding(xwiki.Param("xwiki.authentication.encryptionpadding"));
190    }
191   
192  36 if (xwiki.Param("xwiki.authentication.validationKey") != null) {
193  36 persistent.setValidationKey(xwiki.Param("xwiki.authentication.validationKey"));
194    }
195   
196  36 if (xwiki.Param("xwiki.authentication.encryptionKey") != null) {
197  36 persistent.setEncryptionKey(xwiki.Param("xwiki.authentication.encryptionKey"));
198    }
199   
200  36 sconfig.setPersistentLoginManager(persistent);
201   
202  36 MyFilterConfig fconfig = new MyFilterConfig();
203  36 fconfig.setInitParameter(FormAuthenticator.LOGIN_SUBMIT_PATTERN_KEY,
204    xwiki.Param("xwiki.authentication.loginsubmitpage", "/loginsubmit/XWiki/XWikiLogin"));
205   
206  36 authenticator.init(fconfig, sconfig);
207    }
208   
209  36 this.authenticators.put(wikiName, authenticator);
210   
211  36 return authenticator;
212    } catch (Exception e) {
213  0 throw new XWikiException(XWikiException.MODULE_XWIKI_USER, XWikiException.ERROR_XWIKI_USER_INIT,
214    "Cannot initialize authentication system for wiki [" + wikiName + "]", e);
215    }
216    }
217   
 
218  13041 toggle @Override
219    public XWikiUser checkAuth(XWikiContext context) throws XWikiException
220    {
221    // Debug time taken.
222  13040 long time = System.currentTimeMillis();
223   
224  13025 HttpServletRequest request = null;
225  13034 HttpServletResponse response = context.getResponse();
226   
227  13035 if (context.getRequest() != null) {
228  13027 request = context.getRequest().getHttpServletRequest();
229    }
230   
231  13030 if (request == null) {
232  0 return null;
233    }
234   
235  13031 XWikiAuthenticator auth = getAuthenticator(context);
236  13020 SecurityRequestWrapper wrappedRequest = new SecurityRequestWrapper(request, null, null, auth.getAuthMethod());
237   
238  13027 try {
239  13018 if (auth.processLogin(wrappedRequest, response, context)) {
240  61 return null;
241    }
242   
243    // Process logout (this only works with Forms)
244  12974 if (auth.processLogout(wrappedRequest, response, new URLPatternMatcher())) {
245  1 if (LOGGER.isInfoEnabled()) {
246  0 LOGGER.info("User " + context.getUser() + " has been logged-out");
247    }
248  1 wrappedRequest.setUserPrincipal(null);
249  1 return null;
250    }
251   
252  12959 final String userName = getContextUserName(wrappedRequest.getUserPrincipal(), context);
253  12963 if (LOGGER.isInfoEnabled()) {
254  0 if (userName != null) {
255  0 LOGGER.info("User " + userName + " is authentified");
256    }
257    }
258   
259  12959 if (userName == null) {
260  2389 return null;
261    }
262   
263  10559 return new XWikiUser(userName);
264    } catch (Exception e) {
265  0 LOGGER.error("Failed to authenticate", e);
266   
267  0 return null;
268    } finally {
269  13023 LOGGER.debug("XWikiAuthServiceImpl.checkAuth(XWikiContext) took " + (System.currentTimeMillis() - time)
270    + " milliseconds to run.");
271    }
272    }
273   
274    /**
275    * Method to authenticate and set the cookie from a username and password passed as parameters
276    *
277    * @return null if the user is not authenticated properly
278    */
 
279  0 toggle @Override
280    public XWikiUser checkAuth(String username, String password, String rememberme, XWikiContext context)
281    throws XWikiException
282    {
283  0 HttpServletRequest request = null;
284  0 HttpServletResponse response = context.getResponse();
285   
286  0 if (context.getRequest() != null) {
287  0 request = context.getRequest().getHttpServletRequest();
288    }
289   
290  0 if (request == null) {
291  0 return null;
292    }
293   
294  0 XWikiAuthenticator auth = getAuthenticator(context);
295  0 SecurityRequestWrapper wrappedRequest = new SecurityRequestWrapper(request, null, null, auth.getAuthMethod());
296  0 try {
297  0 if (!auth.processLogin(username, password, rememberme, wrappedRequest, response, context)) {
298  0 return null;
299    }
300   
301  0 Principal principal = wrappedRequest.getUserPrincipal();
302  0 if (LOGGER.isInfoEnabled()) {
303  0 if (principal != null) {
304  0 LOGGER.info("User " + principal.getName() + " is authentified");
305    }
306    }
307   
308  0 if (principal == null) {
309  0 return null;
310    }
311   
312  0 return new XWikiUser(getContextUserName(principal, context));
313    } catch (Exception e) {
314  0 LOGGER.error("Failed to authenticate", e);
315   
316  0 return null;
317    }
318    }
319   
 
320  12979 toggle private String getContextUserName(Principal principal, XWikiContext context)
321    {
322  12978 String contextUserName;
323   
324  12978 if (principal != null) {
325    // Ensures that the wiki part is removed if specified in the Principal name and if it's not the same wiki
326    // as the current wiki.
327  10585 DocumentReference userDocumentReference =
328    this.currentDocumentReferenceResolver.resolve(principal.getName());
329  10587 contextUserName = this.compactWikiEntityReferenceSerializer.serialize(userDocumentReference);
330    } else {
331  2389 contextUserName = null;
332    }
333   
334  12976 return contextUserName;
335    }
336   
 
337  0 toggle @Override
338    public void showLogin(XWikiContext context) throws XWikiException
339    {
340  0 try {
341  0 if (context.getMode() == XWikiContext.MODE_SERVLET) {
342  0 getAuthenticator(context).showLogin(context.getRequest().getHttpServletRequest(), context.getResponse(),
343    context);
344    }
345    } catch (IOException e) {
346  0 LOGGER.error("Unknown failure when calling showLogin", e);
347    }
348    }
349   
 
350  8798 toggle @Override
351    public Principal authenticate(String username, String password, XWikiContext context) throws XWikiException
352    {
353    /*
354    * This method was returning null on failure so I preserved that behaviour, while adding the exact error
355    * messages to the context given as argument. However, the right way to do this would probably be to throw
356    * XWikiException-s.
357    */
358   
359  8798 if (username == null) {
360    // If we can't find the username field then we are probably on the login screen
361  2424 return null;
362    }
363   
364    // Check for empty usernames
365  6374 if (StringUtils.isBlank(username)) {
366  0 context.put("message", "nousername");
367  0 return null;
368    }
369   
370    // Check for empty passwords
371  6374 if (StringUtils.isBlank(password)) {
372  0 context.put("message", "nopassword");
373  0 return null;
374    }
375   
376    // Trim the username to allow users to enter their names with spaces before or after
377  6374 String cannonicalUsername = username.replaceAll(" ", "");
378   
379    // Check for superadmin
380  6374 if (isSuperAdmin(cannonicalUsername)) {
381  3733 return authenticateSuperAdmin(password, context);
382    }
383   
384    // If we have the context then we are using direct mode, and we should be able to specify the database
385    // This is needed for virtual mode to work
386  2641 if (context != null) {
387  2641 String susername = cannonicalUsername;
388  2641 String virtualXwikiName = null;
389  2641 int i = cannonicalUsername.indexOf(".");
390  2641 int j = cannonicalUsername.indexOf(":");
391   
392    // Extract the specified wiki name, if it exists
393  2641 if (j > 0) {
394  2 virtualXwikiName = cannonicalUsername.substring(0, j);
395    }
396   
397    // Use just the username, without a wiki or space prefix
398  2641 if (i != -1) {
399  2 susername = cannonicalUsername.substring(i + 1);
400  2639 } else if (j > 0) {
401    // The username could be in the format xwiki:Username, so strip the wiki prefix.
402  1 susername = cannonicalUsername.substring(j + 1);
403    }
404   
405  2641 String db = context.getWikiId();
406   
407  2641 try {
408    // Set the context database to the specified wiki, if any
409  2641 if (virtualXwikiName != null) {
410  2 context.setWikiId(virtualXwikiName);
411    }
412    // Check in the current database first
413  2641 try {
414  2641 String user = findUser(susername, context);
415  2641 if (user != null && checkPassword(user, password, context)) {
416  2640 return new SimplePrincipal(virtualXwikiName != null ? context.getWikiId() + ":" + user : user);
417    }
418    } catch (Exception e) {
419    // continue
420    }
421   
422  1 if (!context.isMainWiki()) {
423    // Then we check in the main database
424  0 context.setWikiId(context.getMainXWiki());
425  0 try {
426  0 String user = findUser(susername, context);
427  0 if (user != null && checkPassword(user, password, context)) {
428  0 return new SimplePrincipal(context.getWikiId() + ":" + user);
429    }
430    } catch (Exception e) {
431  0 context.put("message", "loginfailed");
432  0 return null;
433    }
434    }
435   
436    // No user found
437  1 context.put("message", "invalidcredentials");
438  1 return null;
439   
440    } finally {
441  2641 context.setWikiId(db);
442    }
443   
444    } else {
445  0 LOGGER.error("XWikiContext is null");
446   
447  0 return null;
448    }
449    }
450   
 
451  2641 toggle protected String findUser(String username, XWikiContext context) throws XWikiException
452    {
453  2641 String user;
454   
455    // First let's look in the cache
456  2641 if (context.getWiki().exists(new DocumentReference(context.getWikiId(), "XWiki", username), context)) {
457  2640 user = "XWiki." + username;
458    } else {
459    // Note: The result of this search depends on the Database. If the database is
460    // case-insensitive (like MySQL) then users will be able to log in by entering their
461    // username in any case. For case-sensitive databases (like HSQLDB) they'll need to
462    // enter it exactly as they've created it.
463  1 String sql = "select distinct doc.fullName from XWikiDocument as doc";
464  1 Object[][] whereParameters = new Object[][] { { "doc.space", "XWiki" }, { "doc.name", username } };
465   
466  1 List<String> list = context.getWiki().search(sql, whereParameters, context);
467  1 if (list.size() == 0) {
468  1 user = null;
469    } else {
470  0 user = list.get(0);
471    }
472    }
473   
474  2641 return user;
475    }
476   
 
477  2640 toggle protected boolean checkPassword(String username, String password, XWikiContext context) throws XWikiException
478    {
479  2640 long time = System.currentTimeMillis();
480  2640 try {
481  2640 boolean result = false;
482   
483  2640 final XWikiDocument doc = context.getWiki().getDocument(username, context);
484  2640 final BaseObject userObject = doc.getXObject(USERCLASS_REFERENCE);
485    // We only allow empty password from users having a XWikiUsers object.
486  2640 if (userObject != null) {
487  2640 final String stored = userObject.getStringValue("password");
488  2640 result = new PasswordClass().getEquivalentPassword(stored, password).equals(stored);
489    }
490   
491  2640 if (LOGGER.isDebugEnabled()) {
492  0 if (result) {
493  0 LOGGER.debug("Password check for user " + username + " successful");
494    } else {
495  0 LOGGER.debug("Password check for user " + username + " failed");
496    }
497  0 LOGGER.debug((System.currentTimeMillis() - time) + " milliseconds spent validating password.");
498    }
499   
500  2640 return result;
501    } catch (Throwable e) {
502  0 LOGGER.error("Failed to check password", e);
503   
504  0 return false;
505    }
506    }
507   
 
508  0 toggle protected String getParam(String name, XWikiContext context)
509    {
510  0 String param = "";
511  0 try {
512  0 param = context.getWiki().getXWikiPreference(name, context);
513    } catch (Exception e) {
514    }
515   
516  0 if (param == null || "".equals(param)) {
517  0 try {
518  0 param = context.getWiki().Param("xwiki.authentication." + StringUtils.replace(name, "auth_", ""));
519    } catch (Exception e) {
520    }
521    }
522   
523  0 if (param == null) {
524  0 param = "";
525    }
526   
527  0 return param;
528    }
529   
 
530  0 toggle protected String createUser(String user, XWikiContext context) throws XWikiException
531    {
532  0 String createuser = getParam("auth_createuser", context);
533   
534  0 if (LOGGER.isDebugEnabled()) {
535  0 LOGGER.debug("Create user param is " + createuser);
536    }
537   
538  0 if (createuser != null) {
539  0 String wikiname = context.getWiki().clearName(user, true, true, context);
540  0 XWikiDocument userdoc =
541    context.getWiki().getDocument(new DocumentReference(context.getWikiId(), "XWiki", wikiname), context);
542  0 if (userdoc.isNew()) {
543  0 if (LOGGER.isDebugEnabled()) {
544  0 LOGGER.debug("User page does not exist for user " + user);
545    }
546   
547  0 if ("empty".equals(createuser)) {
548  0 if (LOGGER.isDebugEnabled()) {
549  0 LOGGER.debug("Creating emptry user for user " + user);
550    }
551   
552  0 context.getWiki().createEmptyUser(wikiname, "edit", context);
553    }
554    } else {
555  0 if (LOGGER.isDebugEnabled()) {
556  0 LOGGER.debug("User page already exists for user " + user);
557    }
558    }
559   
560  0 return wikiname;
561    }
562   
563  0 return user;
564    }
565   
566    /**
567    * The authentication library we are using (SecurityFilter) requires that its URLs do not contain the context path,
568    * in order to be usable with <tt>RequestDispatcher.forward</tt>. Since our URL factory include the context path in
569    * the generated URLs, we use this method to remove (if needed) the context path.
570    *
571    * @param url The URL to process.
572    * @param context The ubiquitous XWiki request context.
573    * @return A <code>String</code> representation of the contextpath-free URL.
574    */
 
575  114 toggle protected String stripContextPathFromURL(URL url, XWikiContext context)
576    {
577  114 String contextPath = context.getWiki().getWebAppPath(context);
578    // XWiki uses contextPath in the wrong way, putting a / at the end, and not at the start. Fix this here.
579  114 if (contextPath.endsWith("/") && !contextPath.startsWith("/")) {
580  111 contextPath = "/" + StringUtils.chop(contextPath);
581  3 } else if ("/".equals(contextPath)) {
582  1 contextPath = "";
583    }
584   
585  114 String urlPrefix = url.getProtocol() + "://" + url.getAuthority() + contextPath;
586   
587    // Since the passed URL has been potentially modified by HttpServletResponse.encodeURL() we also need to call
588    // encodeURL on urlPrefix to have a matching result.
589    // Note that this allows rewrite filters to customize the URL as they wish (adding jsessionid,
590    // query string, etc). If the webapp is installed as root this means that they can add a leading slash,
591    // transforming for example "http://server" into "http://server/?jsessionid=xxx".
592  114 String encodedUrlPrefix = context.getResponse().encodeURL(urlPrefix);
593   
594    // Remove a potential jsessionid in the URL
595  114 encodedUrlPrefix = encodedUrlPrefix.replaceAll(";jsessionid=.*?(?=\\?|$)", "");
596   
597    // Also remove any query string that might have been added by an outbound URL rewrite rule
598  114 encodedUrlPrefix = StringUtils.substringBeforeLast(encodedUrlPrefix, "?");
599   
600    // Since the encodeURL can potentially add a trailing slash, make sure that the relative URL we return always
601    // start with a leadig slash.
602  114 String strippedURL = StringUtils.removeStart(url.toExternalForm(), encodedUrlPrefix);
603  114 if (!strippedURL.startsWith("/")) {
604  1 strippedURL = "/" + strippedURL;
605    }
606   
607  114 return strippedURL;
608    }
609    }