001/* 002 * Copyright 2015 Anyware Services 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.ametys.plugins.linkdirectory; 017 018import java.text.Normalizer; 019import java.util.ArrayList; 020import java.util.Collections; 021import java.util.Comparator; 022import java.util.HashMap; 023import java.util.Iterator; 024import java.util.List; 025import java.util.Map; 026import java.util.regex.Pattern; 027import java.util.stream.Collectors; 028 029import org.apache.avalon.framework.component.Component; 030import org.apache.avalon.framework.context.Context; 031import org.apache.avalon.framework.context.ContextException; 032import org.apache.avalon.framework.context.Contextualizable; 033import org.apache.avalon.framework.service.ServiceException; 034import org.apache.avalon.framework.service.ServiceManager; 035import org.apache.avalon.framework.service.Serviceable; 036import org.apache.cocoon.components.ContextHelper; 037import org.apache.cocoon.environment.Request; 038import org.apache.cocoon.xml.AttributesImpl; 039import org.apache.cocoon.xml.XMLUtils; 040import org.apache.commons.collections.IteratorUtils; 041import org.apache.commons.lang3.ArrayUtils; 042import org.apache.commons.lang3.StringUtils; 043import org.apache.commons.lang3.tuple.ImmutablePair; 044import org.apache.commons.lang3.tuple.Pair; 045import org.apache.jackrabbit.util.ISO9075; 046import org.xml.sax.ContentHandler; 047import org.xml.sax.SAXException; 048 049import org.ametys.cms.data.Binary; 050import org.ametys.cms.tag.Tag; 051import org.ametys.core.right.RightManager; 052import org.ametys.core.user.CurrentUserProvider; 053import org.ametys.core.user.UserIdentity; 054import org.ametys.core.userpref.UserPreferencesException; 055import org.ametys.core.userpref.UserPreferencesManager; 056import org.ametys.plugins.explorer.resources.Resource; 057import org.ametys.plugins.linkdirectory.Link.LinkStatus; 058import org.ametys.plugins.linkdirectory.Link.LinkType; 059import org.ametys.plugins.linkdirectory.dynamic.DynamicInformationProviderExtensionPoint; 060import org.ametys.plugins.linkdirectory.link.LinkDAO; 061import org.ametys.plugins.linkdirectory.repository.DefaultLink; 062import org.ametys.plugins.linkdirectory.repository.DefaultLinkFactory; 063import org.ametys.plugins.linkdirectory.theme.ThemeExpression; 064import org.ametys.plugins.linkdirectory.theme.ThemesDAO; 065import org.ametys.plugins.repository.AmetysObject; 066import org.ametys.plugins.repository.AmetysObjectIterable; 067import org.ametys.plugins.repository.AmetysObjectResolver; 068import org.ametys.plugins.repository.AmetysRepositoryException; 069import org.ametys.plugins.repository.ModifiableTraversableAmetysObject; 070import org.ametys.plugins.repository.TraversableAmetysObject; 071import org.ametys.plugins.repository.UnknownAmetysObjectException; 072import org.ametys.plugins.repository.query.expression.Expression; 073import org.ametys.runtime.i18n.I18nizableText; 074import org.ametys.runtime.plugin.component.AbstractLogEnabled; 075import org.ametys.web.WebConstants; 076import org.ametys.web.WebHelper; 077import org.ametys.web.repository.page.Page; 078import org.ametys.web.repository.site.Site; 079import org.ametys.web.repository.site.SiteManager; 080import org.ametys.web.userpref.FOUserPreferencesConstants; 081 082/** 083 * Link directory helper. 084 */ 085public final class DirectoryHelper extends AbstractLogEnabled implements Component, Serviceable, Contextualizable 086{ 087 /** The component role */ 088 public static final String ROLE = DirectoryHelper.class.getName(); 089 090 private static final String __PLUGIN_NODE_NAME = "linkdirectory"; 091 092 private static final String __LINKS_NODE_NAME = "ametys:directoryLinks"; 093 094 private static final String __USER_LINKS_NODE_NAME = "user-favorites"; 095 096 /** Themes DAO */ 097 protected ThemesDAO _themesDAO; 098 099 /** The Ametys object resolver */ 100 private AmetysObjectResolver _ametysObjectResolver; 101 102 /** The site manager */ 103 private SiteManager _siteManager; 104 105 /** The user preferences manager */ 106 private UserPreferencesManager _userPreferencesManager; 107 108 /** The current user provider */ 109 private CurrentUserProvider _currentUserProvider; 110 111 /** The right manager */ 112 private RightManager _rightManager; 113 114 /** The link DAO */ 115 private LinkDAO _linkDAO; 116 117 /** The context */ 118 private Context _context; 119 120 private DynamicInformationProviderExtensionPoint _dynamicProviderEP; 121 122 @Override 123 public void service(ServiceManager manager) throws ServiceException 124 { 125 _ametysObjectResolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 126 _siteManager = (SiteManager) manager.lookup(SiteManager.ROLE); 127 _userPreferencesManager = (UserPreferencesManager) manager.lookup(UserPreferencesManager.ROLE + ".FO"); 128 _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE); 129 _dynamicProviderEP = (DynamicInformationProviderExtensionPoint) manager.lookup(DynamicInformationProviderExtensionPoint.ROLE); 130 _rightManager = (RightManager) manager.lookup(RightManager.ROLE); 131 _linkDAO = (LinkDAO) manager.lookup(LinkDAO.ROLE); 132 _themesDAO = (ThemesDAO) manager.lookup(ThemesDAO.ROLE); 133 } 134 135 @Override 136 public void contextualize(Context context) throws ContextException 137 { 138 _context = context; 139 } 140 141 /** 142 * Get the root plugin storage object. 143 * @param site the site. 144 * @return the root plugin storage object. 145 * @throws AmetysRepositoryException if a repository error occurs. 146 */ 147 public ModifiableTraversableAmetysObject getPluginNode(Site site) throws AmetysRepositoryException 148 { 149 try 150 { 151 ModifiableTraversableAmetysObject pluginsNode = site.getRootPlugins(); 152 153 return getOrCreateNode(pluginsNode, __PLUGIN_NODE_NAME, "ametys:unstructured"); 154 } 155 catch (AmetysRepositoryException e) 156 { 157 throw new AmetysRepositoryException("Error getting the link directory plugin node for site " + site.getName(), e); 158 } 159 } 160 161 /** 162 * Get the links root node. 163 * @param site the site 164 * @param language the language. 165 * @return the links root node. 166 * @throws AmetysRepositoryException if a repository error occurs. 167 */ 168 public ModifiableTraversableAmetysObject getLinksNode(Site site, String language) throws AmetysRepositoryException 169 { 170 try 171 { 172 // Get the root plugin node. 173 ModifiableTraversableAmetysObject pluginNode = getPluginNode(site); 174 175 // Get or create the language node. 176 ModifiableTraversableAmetysObject langNode = getOrCreateNode(pluginNode, language, "ametys:unstructured"); 177 178 // Get or create the definitions container node in the language node and return it. 179 return getOrCreateNode(langNode, __LINKS_NODE_NAME, DefaultLinkFactory.LINK_ROOT_NODE_TYPE); 180 } 181 catch (AmetysRepositoryException e) 182 { 183 throw new AmetysRepositoryException("Error getting the link directory root node for site " + site.getName() + " and language " + language, e); 184 } 185 } 186 187 /** 188 * Get the links root node for the given user. 189 * @param site the site 190 * @param language the language. 191 * @param user The user identity 192 * @return the links root node for the given user. 193 * @throws AmetysRepositoryException if a repository error occurs. 194 */ 195 public ModifiableTraversableAmetysObject getLinksForUserNode(Site site, String language, UserIdentity user) throws AmetysRepositoryException 196 { 197 try 198 { 199 // Get the root plugin node. 200 ModifiableTraversableAmetysObject pluginNode = getPluginNode(site); 201 202 // Get or create the user links node. 203 ModifiableTraversableAmetysObject userLinksNode = getOrCreateNode(pluginNode, __USER_LINKS_NODE_NAME, "ametys:unstructured"); 204 // Get or create the population node. 205 ModifiableTraversableAmetysObject populationNode = getOrCreateNode(userLinksNode, user.getPopulationId(), "ametys:unstructured"); 206 // Get or create the login node. 207 ModifiableTraversableAmetysObject loginNode = getOrCreateNode(populationNode, user.getLogin(), "ametys:unstructured"); 208 // Get or create the language node. 209 ModifiableTraversableAmetysObject langNode = getOrCreateNode(loginNode, language, "ametys:unstructured"); 210 211 // Get or create the definitions container node in the language node and return it. 212 return getOrCreateNode(langNode, __LINKS_NODE_NAME, DefaultLinkFactory.LINK_ROOT_NODE_TYPE); 213 } 214 catch (AmetysRepositoryException e) 215 { 216 throw new AmetysRepositoryException("Error getting the link directory root node for user " + user + " and for site " + site.getName() + " and language " + language, e); 217 } 218 } 219 220 /** 221 * Get the plugin node path 222 * @param siteName the site name. 223 * @return the plugin node path. 224 */ 225 public String getPluginNodePath(String siteName) 226 { 227 return String.format("//element(%s, ametys:site)/ametys-internal:plugins/%s", siteName, __PLUGIN_NODE_NAME); 228 } 229 230 /** 231 * Get the links root node path 232 * @param siteName the site name. 233 * @param language the language 234 * @return the links root node path. 235 */ 236 public String getLinksNodePath(String siteName, String language) 237 { 238 return getPluginNodePath(siteName) + "/" + language + "/" + __LINKS_NODE_NAME; 239 } 240 241 /** 242 * Get the links root node path for the given user 243 * @param siteName the site name. 244 * @param language the language 245 * @param user The user identity 246 * @return the links root node path for the given user. 247 */ 248 public String getLinksForUserNodePath(String siteName, String language, UserIdentity user) 249 { 250 return getPluginNodePath(siteName) + "/" + __USER_LINKS_NODE_NAME + "/" + ISO9075.encode(user.getPopulationId()) + "/" + ISO9075.encode(user.getLogin()) + "/" + language + "/" + __LINKS_NODE_NAME; 251 } 252 253 /** 254 * Get all the links 255 * @param siteName the site name. 256 * @param language the language. 257 * @return all the links' nodes 258 */ 259 public String getAllLinksQuery(String siteName, String language) 260 { 261 return getLinksNodePath(siteName, language) + "/element(*, " + DefaultLinkFactory.LINK_NODE_TYPE + ")"; 262 } 263 264 /** 265 * Get the link query corresponding to the expression passed as a parameter 266 * @param siteName the site name. 267 * @param language the language. 268 * @param expression the {@link Expression} of the links retrieval query 269 * @return the link corresponding to the expression passed as a parameter 270 */ 271 public String getLinksQuery(String siteName, String language, Expression expression) 272 { 273 return getLinksNodePath(siteName, language) + "/element(*, " + DefaultLinkFactory.LINK_NODE_TYPE + ")[" + expression.build() + "]"; 274 } 275 276 /** 277 * Get the user link query corresponding to the expression passed as a parameter 278 * @param siteName the site name. 279 * @param language the language. 280 * @param user the user 281 * @param expression the {@link Expression} of the links retrieval query 282 * @return the user link corresponding to the expression passed as a parameter 283 */ 284 public String getUserLinksQuery(String siteName, String language, UserIdentity user, Expression expression) 285 { 286 return getLinksForUserNodePath(siteName, language, user) + "/element(*, " + DefaultLinkFactory.LINK_NODE_TYPE + ")[" + expression.build() + "]"; 287 } 288 /** 289 * Get the query verifying the existence of an url 290 * @param siteName the site name. 291 * @param language the language. 292 * @param url the url to test. 293 * @return the query verifying the existence of an url 294 */ 295 public String getUrlExistsQuery(String siteName, String language, String url) 296 { 297 String lowerCaseUrl = StringUtils.replace(url, "'", "''").toLowerCase(); 298 return getLinksNodePath(siteName, language) + "/element(*, " + DefaultLinkFactory.LINK_NODE_TYPE + ")[fn:lower-case(@ametys-internal:url) = '" + lowerCaseUrl + "' or fn:lower-case(@ametys-internal:internal-url) = '" + lowerCaseUrl + "']"; 299 } 300 301 /** 302 * Get the query verifying the existence of an url for the given user 303 * @param siteName the site name. 304 * @param language the language. 305 * @param url the url to test. 306 * @param user The user identity 307 * @return the query verifying the existence of an url for the given user 308 */ 309 public String getUrlExistsForUserQuery(String siteName, String language, String url, UserIdentity user) 310 { 311 String lowerCaseUrl = StringUtils.replace(url, "'", "''").toLowerCase(); 312 return getLinksForUserNodePath(siteName, language, user) + "/element(*, " + DefaultLinkFactory.LINK_NODE_TYPE + ")[fn:lower-case(@ametys-internal:url) = '" + lowerCaseUrl + "' or fn:lower-case(@ametys-internal:internal-url) = '" + lowerCaseUrl + "']"; 313 } 314 315 /** 316 * Normalizes an input string in order to capitalize it, remove accents, and replace whitespaces with underscores 317 * @param s the string to normalize 318 * @return the normalized string 319 */ 320 public String normalizeString(String s) 321 { 322 // Strip accents 323 String normalizedLabel = Normalizer.normalize(s.toUpperCase(), Normalizer.Form.NFD).replaceAll("[^\\p{ASCII}]", ""); 324 325 // Upper case 326 String upperCaseLabel = normalizedLabel.replaceAll(" +", "_").replaceAll("[^\\w-]", "_").replaceAll("_+", "_").toUpperCase(); 327 328 return upperCaseLabel; 329 } 330 331 /** 332 * Get links of a given site and language 333 * @param siteName the site name 334 * @param language the language 335 * @return the links 336 */ 337 public AmetysObjectIterable<DefaultLink> getLinks(String siteName, String language) 338 { 339 Site site = _siteManager.getSite(siteName); 340 TraversableAmetysObject linksNode = getLinksNode(site, language); 341 return linksNode.getChildren(); 342 } 343 344 /** 345 * Get the list of links corresponding to the given theme ids 346 * @param themesIds the ids of the configured themes 347 * @param user the current user 348 * @param siteName the site's name 349 * @param language the site's language 350 * @return the list of default links corresponding to the given themes 351 */ 352 public List<DefaultLink> getLinks(List<String> themesIds, UserIdentity user, String siteName, String language) 353 { 354 Site site = _siteManager.getSite(siteName); 355 TraversableAmetysObject linksNode = getLinksNode(site, language); 356 AmetysObjectIterable<AmetysObject> links = linksNode.getChildren(); 357 Iterator<AmetysObject> it = links.iterator(); 358 359 if (themesIds.isEmpty()) 360 { 361 return IteratorUtils.toList(it); 362 } 363 else 364 { 365 List<DefaultLink> configuredThemeLinks = new ArrayList<> (); 366 367 while (it.hasNext()) 368 { 369 DefaultLink link = (DefaultLink) it.next(); 370 String[] linkThemes = link.getThemes(); 371 372 for (String themeId : themesIds) 373 { 374 if (ArrayUtils.contains(linkThemes, themeId)) 375 { 376 configuredThemeLinks.add(link); 377 break; 378 } 379 } 380 } 381 382 return configuredThemeLinks; 383 } 384 } 385 386 /** 387 * Get links of a given site and language, for the given user 388 * @param siteName the site name 389 * @param language the language 390 * @param user The user identity 391 * @return the links for the given user 392 */ 393 public AmetysObjectIterable<DefaultLink> getUserLinks(String siteName, String language, UserIdentity user) 394 { 395 return getUserLinks(siteName, language, user, null); 396 } 397 398 /** 399 * Get links of a given site and language, for the given user 400 * @param siteName the site name 401 * @param language the language 402 * @param user The user identity 403 * @param themeName the theme id to filter user links. If null, return all user links 404 * @return the links for the given user 405 */ 406 public AmetysObjectIterable<DefaultLink> getUserLinks(String siteName, String language, UserIdentity user, String themeName) 407 { 408 if (StringUtils.isNotBlank(themeName) && themeExists(themeName, siteName, language)) 409 { 410 ThemeExpression themeExpression = new ThemeExpression(themeName); 411 String linksQuery = getUserLinksQuery(siteName, language, user, themeExpression); 412 return _ametysObjectResolver.query(linksQuery); 413 } 414 else 415 { 416 Site site = _siteManager.getSite(siteName); 417 TraversableAmetysObject linksNode = getLinksForUserNode(site, language, user); 418 AmetysObjectIterable<DefaultLink> links = linksNode.getChildren(); 419 return links; 420 } 421 } 422 423 /** 424 * Checks if the links displayed in a link directory service has access restrictions 425 * @param siteName the name of the site 426 * @param language the language 427 * @param themesIds the list of selected theme ids 428 * @return true if the links of the service have access restrictions, false otherwise 429 */ 430 public boolean hasRestrictions(String siteName, String language, List<String> themesIds) 431 { 432 // No themes => we check all the links' access restrictions 433 if (themesIds.isEmpty()) 434 { 435 String allLinksQuery = getAllLinksQuery(siteName, language); 436 try (AmetysObjectIterable<AmetysObject> links = _ametysObjectResolver.query(allLinksQuery)) 437 { 438 if (isAccessRestricted(links)) 439 { 440 return true; 441 } 442 } 443 444 445 } 446 // The service has themes specified => we solely check the corresponding links' access restrictions 447 else 448 { 449 for (String themeId : themesIds) 450 { 451 String xPathQuery = getLinksQuery(siteName, language, new ThemeExpression(themeId)); 452 try (AmetysObjectIterable<AmetysObject> links = _ametysObjectResolver.query(xPathQuery)) 453 { 454 if (isAccessRestricted(links)) 455 { 456 return true; 457 } 458 } 459 } 460 } 461 462 // All the tested links have no restricted access 463 return false; 464 } 465 466 /** 467 * Checks if the links displayed in a link directory service has internal link 468 * @param siteName the name of the site 469 * @param language the language 470 * @param themesIds the list of selected theme ids 471 * @return true if the links of the service has internal link, false otherwise 472 */ 473 public boolean hasInternalUrl(String siteName, String language, List<String> themesIds) 474 { 475 Site site = _siteManager.getSite(siteName); 476 String allowedIdParameter = site.getValue("allowed-ip"); 477 if (StringUtils.isBlank(allowedIdParameter)) 478 { 479 return false; 480 } 481 482 UserIdentity user = _currentUserProvider.getUser(); 483 484 List<DefaultLink> links = getLinks(themesIds, user, siteName, language); 485 for (DefaultLink link : links) 486 { 487 if (StringUtils.isNotBlank(link.getInternalUrl())) 488 { 489 return true; 490 } 491 } 492 493 return false; 494 } 495 496 /** 497 * Check if the links' access is restricted or not 498 * @param links the links to be tested 499 * @return true if the link has a restricted access, false otherwise 500 */ 501 public boolean isAccessRestricted(AmetysObjectIterable<AmetysObject> links) 502 { 503 Iterator<AmetysObject> it = links.iterator(); 504 505 while (it.hasNext()) 506 { 507 DefaultLink link = (DefaultLink) it.next(); 508 509 // If any of the links has a limited access, the service declares itself non-cacheable 510 if (!_rightManager.hasAnonymousReadAccess(link)) 511 { 512 return true; 513 } 514 } 515 516 return false; 517 } 518 519 private ModifiableTraversableAmetysObject getOrCreateNode(ModifiableTraversableAmetysObject parentNode, String nodeName, String nodeType) throws AmetysRepositoryException 520 { 521 ModifiableTraversableAmetysObject node; 522 if (parentNode.hasChild(nodeName)) 523 { 524 node = parentNode.getChild(nodeName); 525 } 526 else 527 { 528 node = parentNode.createChild(nodeName, nodeType); 529 parentNode.saveChanges(); 530 } 531 return node; 532 } 533 534 /** 535 * Sax the directory links 536 * @param siteName the site name 537 * @param contentHandler the content handler 538 * @param links the list of links to sax (can be null 539 * @param userLinks the user links to sax (can be null) 540 * @param storageContext the storage context, null if there is no connected user 541 * @param contextVars the context variables 542 * @param user the user 543 * @throws SAXException If an error occurs while generating the SAX events 544 * @throws UserPreferencesException if an exception occurs while getting the user preferences 545 */ 546 public void saxLinks(String siteName, ContentHandler contentHandler, List<DefaultLink> links, List<DefaultLink> userLinks, Map<String, String> contextVars, String storageContext, UserIdentity user) throws SAXException, UserPreferencesException 547 { 548 // left : true if user link 549 // right : the link itself 550 List<Pair<Boolean, DefaultLink>> allLinks = new ArrayList<>(); 551 552 if (links != null) 553 { 554 for (DefaultLink link : links) 555 { 556 allLinks.add(new ImmutablePair<>(false, link)); 557 } 558 } 559 560 if (userLinks != null) 561 { 562 for (DefaultLink link : userLinks) 563 { 564 allLinks.add(new ImmutablePair<>(true, link)); 565 } 566 } 567 568 569 String[] orderedLinksPrefLinksIdsArray = null; 570 String[] hiddenLinksPrefLinksIdsArray = null; 571 if (user != null) 572 { 573 // TODO it would be nice to change the name of this user pref but the storage is still the same, so for the moment we avoid the SQL migration 574 // Cf issue LINKS-141 575 // Change in org.ametys.plugins.linkdirectory.LinkDirectorySetUserPreferencesAction#act too 576 577 Map<String, String> unTypedUserPrefs = _userPreferencesManager.getUnTypedUserPrefs(user, storageContext, contextVars); 578 579 String orderedLinksPrefValues = unTypedUserPrefs.get("checked-links"); 580 orderedLinksPrefLinksIdsArray = StringUtils.split(orderedLinksPrefValues, ","); 581 582 String hiddenLinksPrefValues = unTypedUserPrefs.get("hidden-links"); 583 hiddenLinksPrefLinksIdsArray = StringUtils.split(hiddenLinksPrefValues, ","); 584 585 } 586 587 Site site = _siteManager.getSite(siteName); 588 String ipRegexp = site.getValue("allowed-ip"); 589 Pattern ipRestriction = null; 590 if (StringUtils.isNotBlank(ipRegexp)) 591 { 592 ipRestriction = Pattern.compile(ipRegexp); 593 } 594 595 boolean hasIPRestriction = ipRestriction != null; 596 boolean isIPAuthorized = _isIPAuthorized(ipRestriction); 597 598 // Sort the list according to the orderedLinksPrefLinksIdsArray 599 if (ArrayUtils.isNotEmpty(orderedLinksPrefLinksIdsArray)) 600 { 601 DefaultLinkSorter defaultLinkSorter = new DefaultLinkSorter(allLinks, orderedLinksPrefLinksIdsArray); 602 allLinks.sort(defaultLinkSorter); 603 } 604 605 for (Pair<Boolean, DefaultLink> linkPair : allLinks) 606 { 607 DefaultLink link = linkPair.getRight(); 608 boolean userLink = linkPair.getLeft(); 609 610 // check the access granted if it is not a user link 611 if (userLink || _isCurrentUserGrantedAccess(link)) 612 { 613 boolean selected = ArrayUtils.contains(orderedLinksPrefLinksIdsArray, link.getId()); 614 boolean isHidden = ArrayUtils.contains(hiddenLinksPrefLinksIdsArray, link.getId()); // deprecated, only used for old views, isHidden should be used now 615 saxLink(siteName, contentHandler, link, selected, hasIPRestriction, isIPAuthorized, userLink, isHidden); 616 } 617 } 618 } 619 620 /** 621 * Generate a directory link. 622 * @param siteName the site name 623 * @param contentHandler the content handler 624 * @param link the link to generate. 625 * @param selected true if a front end user has checked this link as a user preference, false otherwise (deprecated, only used for old views, isHidden should be used now) 626 * @param hasIPRestriction true if we have IP restriction 627 * @param isIPAuthorized true if the IP is authorized 628 * @param userLink true if it is a user link 629 * @param isHidden true if the link is hidden 630 * @throws SAXException If an error occurs while generating the SAX events 631 */ 632 public void saxLink (String siteName, ContentHandler contentHandler, DefaultLink link, boolean selected, boolean hasIPRestriction, boolean isIPAuthorized, boolean userLink, boolean isHidden) throws SAXException 633 { 634 AttributesImpl attrs = new AttributesImpl(); 635 attrs.addCDATAAttribute("id", link.getId()); 636 attrs.addCDATAAttribute("lang", link.getLanguage()); 637 638 LinkType urlType = link.getUrlType(); 639 640 _addURLAttribute(link, hasIPRestriction, isIPAuthorized, attrs); 641 642 attrs.addCDATAAttribute("urlType", StringUtils.defaultString(urlType.toString())); 643 644 if (link.getStatus() != LinkStatus.BROKEN) 645 { 646 String dynInfoProviderId = StringUtils.defaultString(link.getDynamicInformationProvider()); 647 // Check if provider exists 648 if (StringUtils.isNotEmpty(dynInfoProviderId) && _dynamicProviderEP.hasExtension(dynInfoProviderId)) 649 { 650 attrs.addCDATAAttribute("dynamicInformationProvider", dynInfoProviderId); 651 } 652 } 653 attrs.addCDATAAttribute("title", StringUtils.defaultString(link.getTitle())); 654 attrs.addCDATAAttribute("content", StringUtils.defaultString(link.getContent())); 655 656 if (urlType == LinkType.PAGE) 657 { 658 String pageId = link.getUrl(); 659 try 660 { 661 Page page = _ametysObjectResolver.resolveById(pageId); 662 attrs.addCDATAAttribute("pageTitle", page.getTitle()); 663 } 664 catch (UnknownAmetysObjectException e) 665 { 666 attrs.addCDATAAttribute("unknownPage", "true"); 667 } 668 } 669 670 attrs.addCDATAAttribute("alternative", StringUtils.defaultString(link.getAlternative())); 671 attrs.addCDATAAttribute("pictureAlternative", StringUtils.defaultString(link.getPictureAlternative())); 672 673 attrs.addCDATAAttribute("user-selected", selected ? "true" : "false"); 674 675 attrs.addCDATAAttribute("color", _linkDAO.getLinkColor(link)); 676 677 String pictureType = link.getPictureType(); 678 attrs.addCDATAAttribute("pictureType", pictureType); 679 if (pictureType.equals("resource")) 680 { 681 String resourceId = link.getResourcePictureId(); 682 try 683 { 684 Resource resource = _ametysObjectResolver.resolveById(resourceId); 685 attrs.addCDATAAttribute("pictureId", resourceId); 686 attrs.addCDATAAttribute("pictureName", resource.getName()); 687 attrs.addCDATAAttribute("pictureSize", Long.toString(resource.getLength())); 688 attrs.addCDATAAttribute("imageType", "explorer"); 689 } 690 catch (UnknownAmetysObjectException e) 691 { 692 getLogger().error("The resource of id'{}' does not exist anymore. The picture for link of id '{}' will be ignored.", resourceId, link.getId(), e); 693 } 694 695 } 696 else if (pictureType.equals("external")) 697 { 698 Binary picMeta = link.getExternalPicture(); 699 attrs.addCDATAAttribute("picturePath", DefaultLink.PROPERTY_PICTURE); 700 attrs.addCDATAAttribute("pictureName", picMeta.getFilename()); 701 attrs.addCDATAAttribute("pictureSize", Long.toString(picMeta.getLength())); 702 attrs.addCDATAAttribute("imageType", "link-data"); 703 } 704 else if (pictureType.equals("glyph")) 705 { 706 attrs.addCDATAAttribute("pictureGlyph", link.getPictureGlyph()); 707 } 708 709 attrs.addCDATAAttribute("limitedAccess", String.valueOf(!_rightManager.hasAnonymousReadAccess(link))); 710 711 attrs.addCDATAAttribute("userLink", String.valueOf(userLink)); 712 attrs.addCDATAAttribute("isHidden", String.valueOf(isHidden)); 713 714 LinkStatus status = link.getStatus(); 715 if (status != null) 716 { 717 attrs.addCDATAAttribute("status", status.name()); 718 } 719 720 if (StringUtils.isNotBlank(link.getPage())) 721 { 722 attrs.addCDATAAttribute("page", link.getPage()); 723 } 724 725 XMLUtils.startElement(contentHandler, "link", attrs); 726 727 // Themes 728 _saxThemes(contentHandler, link); 729 730 XMLUtils.endElement(contentHandler, "link"); 731 } 732 733 /** 734 * Add the URL attribute to sax 735 * @param link the link 736 * @param hasIPRestriction true if we have IP restriction 737 * @param isIPAuthorized true if the IP is authorized 738 * @param attrs the attribute 739 */ 740 private void _addURLAttribute(DefaultLink link, boolean hasIPRestriction, boolean isIPAuthorized, AttributesImpl attrs) 741 { 742 String internalUrl = link.getInternalUrl(); 743 String externalUrl = link.getUrl(); 744 745 // If we have no internal URL or no IP restriction, just sax external URL 746 if (StringUtils.isBlank(internalUrl) || !hasIPRestriction) 747 { 748 attrs.addCDATAAttribute("url", StringUtils.defaultString(externalUrl)); 749 } 750 else 751 { 752 // If the IP is authorized, sax internal URL 753 if (isIPAuthorized) 754 { 755 attrs.addCDATAAttribute("url", StringUtils.defaultString(internalUrl)); 756 } 757 // else if we have external URL, we sax it 758 else if (StringUtils.isNotBlank(externalUrl)) 759 { 760 attrs.addCDATAAttribute("url", StringUtils.defaultString(externalUrl)); 761 } 762 // else we sax the internal URL and we disable it because the IP is not authorized 763 else 764 { 765 attrs.addCDATAAttribute("url", StringUtils.defaultString(internalUrl)); 766 attrs.addCDATAAttribute("disabled", "true"); 767 } 768 } 769 } 770 771 /** 772 * Get the actual ids of the themes configured properly, their names if they were not 773 * @param configuredThemesNames the normalized ids of the configured themes 774 * @param siteName the site's name 775 * @param language the site's language 776 * @return the actual ids of the configured themes 777 */ 778 public Map<String, List<String>> getThemesMap(List<String> configuredThemesNames, String siteName, String language) 779 { 780 Map<String, List<String>> themesMap = new HashMap<> (); 781 List<String> correctThemesList = new ArrayList<> (); 782 List<String> wrongThemesList = new ArrayList<> (); 783 784 for (int i = 0; i < configuredThemesNames.size(); i++) 785 { 786 String configuredThemeName = configuredThemesNames.get(i); 787 788 Map<String, Object> contextualParameters = new HashMap<>(); 789 contextualParameters.put("language", language); 790 contextualParameters.put("siteName", siteName); 791 Tag theme = _themesDAO.getTag(configuredThemeName, contextualParameters); 792 793 if (theme == null) 794 { 795 getLogger().warn("The theme '{}' was not found. It will be ignored.", configuredThemeName); 796 wrongThemesList.add(configuredThemeName); 797 } 798 else 799 { 800 correctThemesList.add(configuredThemeName); 801 } 802 } 803 804 themesMap.put("themes", correctThemesList); 805 themesMap.put("unknown-themes", wrongThemesList); 806 return themesMap; 807 } 808 809 /** 810 * Verify the existence of a theme 811 * @param themeName the id of the theme to verify 812 * @param siteName the site's name 813 * @param language the site's language 814 * @return true if the theme exists, false otherwise 815 */ 816 public boolean themeExists(String themeName, String siteName, String language) 817 { 818 if (StringUtils.isBlank(themeName)) 819 { 820 return false; 821 } 822 Map<String, Object> contextualParameters = new HashMap<>(); 823 contextualParameters.put("language", language); 824 contextualParameters.put("siteName", siteName); 825 List<String> checkTags = _themesDAO.checkTags(List.of(themeName), false, Collections.EMPTY_MAP, contextualParameters); 826 return !checkTags.isEmpty(); 827 } 828 829 /** 830 * Get theme's title from its name 831 * @param themeName the theme name 832 * @param siteName the site's name 833 * @param language the site's language 834 * @return the title of the theme. Null if the theme doesn't exist 835 */ 836 public I18nizableText getThemeTitle(String themeName, String siteName, String language) 837 { 838 Map<String, Object> contextualParameters = new HashMap<>(); 839 contextualParameters.put("language", language); 840 contextualParameters.put("siteName", siteName); 841 if (themeExists(themeName, siteName, language)) 842 { 843 Tag tag = _themesDAO.getTag(themeName, contextualParameters); 844 return tag.getTitle(); 845 } 846 else 847 { 848 getLogger().warn("Can't find theme with name {} for site {} and language {}", themeName, siteName, language); 849 } 850 851 return null; 852 } 853 854 /** 855 * Get the site's name 856 * @param request the request 857 * @return the site's name 858 */ 859 public String getSiteName(Request request) 860 { 861 return WebHelper.getSiteName(request, (Page) request.getAttribute(Page.class.getName())); 862 } 863 864 /** 865 * Get the site's language 866 * @param request the request 867 * @return the site's language 868 */ 869 public String getLanguage(Request request) 870 { 871 Page page = (Page) request.getAttribute(Page.class.getName()); 872 if (page != null) 873 { 874 return page.getSitemapName(); 875 } 876 877 String language = (String) request.getAttribute(WebConstants.REQUEST_ATTR_SITEMAP_NAME); 878 if (StringUtils.isEmpty(language)) 879 { 880 language = request.getParameter("language"); 881 } 882 883 return language; 884 } 885 886 /** 887 * Retrieve the context variables from the front 888 * @param request the request 889 * @return the map of context variables 890 */ 891 public Map<String, String> getContextVars(Request request) 892 { 893 Map<String, String> contextVars = new HashMap<> (); 894 895 contextVars.put(FOUserPreferencesConstants.CONTEXT_VAR_SITENAME, getSiteName(request)); 896 contextVars.put(FOUserPreferencesConstants.CONTEXT_VAR_LANGUAGE, getLanguage(request)); 897 898 return contextVars; 899 } 900 901 /** 902 * Get the appropriate storage context from request 903 * @param request the request 904 * @param zoneItemId the id of the zone item if we deal with a service, null for an input data 905 * @return the storage context in which the user preferences will be kept 906 */ 907 public String getStorageContext(Request request, String zoneItemId) 908 { 909 String siteName = getSiteName(request); 910 String language = getLanguage(request); 911 912 return StringUtils.isEmpty(zoneItemId) ? siteName + "/" + language : siteName + "/" + language + "/" + zoneItemId; 913 } 914 915 /** 916 * Get the appropriate storage context 917 * @param siteName the name of the site 918 * @param language the language 919 * @param zoneItemId the id of the zone item if we deal with a service, null for an input data 920 * @return the storage context in which the user preferences will be kept 921 */ 922 public String getStorageContext(String siteName, String language, String zoneItemId) 923 { 924 return StringUtils.isEmpty(zoneItemId) ? siteName + "/" + language : siteName + "/" + language + "/" + zoneItemId; 925 } 926 927 /** 928 * Sax the themes 929 * @param contentHandler the content handler 930 * @param link the link 931 * @throws SAXException If an error occurs while generating the SAX events 932 */ 933 private void _saxThemes (ContentHandler contentHandler, DefaultLink link) throws SAXException 934 { 935 XMLUtils.startElement(contentHandler, "themes"); 936 937 Map<String, Object> contextualParameters = new HashMap<>(); 938 contextualParameters.put("language", link.getLanguage()); 939 contextualParameters.put("siteName", link.getSiteName()); 940 941 for (String themeId : link.getThemes()) 942 { 943 try 944 { 945 Tag tag = _themesDAO.getTag(themeId, contextualParameters); 946 if (tag != null) 947 { 948 AttributesImpl attrs = new AttributesImpl(); 949 attrs.addCDATAAttribute("id", themeId); 950 attrs.addCDATAAttribute("name", tag.getName()); 951 952 XMLUtils.startElement(contentHandler, "theme", attrs); 953 tag.getTitle().toSAX(contentHandler, "label"); 954 XMLUtils.endElement(contentHandler, "theme"); 955 } 956 else 957 { 958 getLogger().error("Theme '{}' in link '{}' can not be found.", themeId, link.getId()); 959 } 960 } 961 catch (UnknownAmetysObjectException e) 962 { 963 // Theme does not exist anymore 964 } 965 } 966 967 968 XMLUtils.endElement(contentHandler, "themes"); 969 } 970 971 /** 972 * Determines if the current user is allowed to see the link or not 973 * @param link the link 974 * @return true if the current user is allowed to see the link, false otherwise 975 */ 976 private boolean _isCurrentUserGrantedAccess(DefaultLink link) 977 { 978 UserIdentity user = _currentUserProvider.getUser(); 979 980 // There is no access restriction 981 return _rightManager.hasReadAccess(user, link); 982 } 983 984 /** 985 * Checks if the IP is authorized for use link internal URL 986 * @param ipRestriction The ip restriction pattern 987 * @return true the IP is authorized for use link internal URL, false otherwise 988 */ 989 private boolean _isIPAuthorized(Pattern ipRestriction) 990 { 991 if (ipRestriction == null) 992 { 993 return true; 994 } 995 996 Request request = ContextHelper.getRequest(_context); 997 998 // The real client IP may have been put in the non-standard "X-Forwarded-For" request header, in case of reverse proxy 999 String xff = request.getHeader("X-Forwarded-For"); 1000 String ip = null; 1001 1002 if (xff != null) 1003 { 1004 ip = xff.split(",")[0]; 1005 } 1006 else 1007 { 1008 ip = request.getRemoteAddr(); 1009 } 1010 1011 boolean result = ipRestriction.matcher(ip).matches(); 1012 1013 if (getLogger().isDebugEnabled()) 1014 { 1015 getLogger().debug("Ip '{}' is considered {} with pattern {}", ip, result ? "internal" : "external", ipRestriction.pattern()); 1016 } 1017 1018 return result; 1019 } 1020 1021 /** 1022 * Helper class to sort links (DefaultLinkSorter implementation) 1023 * If both links are in the ordered links list, this order is used 1024 * If one of them is in it and not the other, the one in it will be before the other 1025 * If none of them is in the list, the initial order will be used 1026 */ 1027 private class DefaultLinkSorter implements Comparator<Pair<Boolean, DefaultLink>> 1028 { 1029 private String[] _orderedLinksPrefLinksIdsArray; 1030 private List<String> _initialList; 1031 /** 1032 * constructor for the helper 1033 * @param initialList initial list to keep track of the original order if no order is found 1034 * @param orderedLinksPrefLinksIdsArray ordered list of link ids 1035 */ 1036 public DefaultLinkSorter(List<Pair<Boolean, DefaultLink>> initialList, String[] orderedLinksPrefLinksIdsArray) 1037 { 1038 _orderedLinksPrefLinksIdsArray = orderedLinksPrefLinksIdsArray; 1039 _initialList = initialList.stream() 1040 .map(Pair::getRight) 1041 .map(DefaultLink::getId) 1042 .collect(Collectors.toList()); 1043 } 1044 public int compare(Pair<Boolean, DefaultLink> pair1, Pair<Boolean, DefaultLink> pair2) 1045 { 1046 DefaultLink link1 = pair1.getRight(); 1047 DefaultLink link2 = pair2.getRight(); 1048 if (ArrayUtils.isNotEmpty(_orderedLinksPrefLinksIdsArray)) 1049 { 1050 int pos1 = ArrayUtils.indexOf(_orderedLinksPrefLinksIdsArray, link1.getId()); 1051 int pos2 = ArrayUtils.indexOf(_orderedLinksPrefLinksIdsArray, link2.getId()); 1052 1053 if (pos1 == ArrayUtils.INDEX_NOT_FOUND && pos2 == ArrayUtils.INDEX_NOT_FOUND) 1054 { 1055 // if both are not found, we keep the original order 1056 pos1 = _initialList.indexOf(link1.getId()); 1057 pos2 = _initialList.indexOf(link2.getId()); 1058 } 1059 else 1060 { 1061 // if one of them is not found, we return the max value (to put them at the end) 1062 if (pos1 == ArrayUtils.INDEX_NOT_FOUND) 1063 { 1064 pos1 = Integer.MAX_VALUE; 1065 } 1066 if (pos2 == ArrayUtils.INDEX_NOT_FOUND) 1067 { 1068 pos2 = Integer.MAX_VALUE; 1069 } 1070 } 1071 return pos1 - pos2; 1072 } 1073 else 1074 { 1075 return 0; // No sorting if no sort array 1076 } 1077 } 1078 } 1079}