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