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