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.Link.LinkVisibility; 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. Can be null to get all user links 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 String query = getLinksForUserNodePath(siteName, language, user) + "/element(*, " + DefaultLinkFactory.LINK_NODE_TYPE + ")"; 337 if (expression != null) 338 { 339 query += "[" + expression.build() + "]"; 340 } 341 return query; 342 } 343 /** 344 * Get the query verifying the existence of an url 345 * @param siteName the site name. 346 * @param language the language. 347 * @param url the url to test. 348 * @return the query verifying the existence of an url 349 */ 350 public String getUrlExistsQuery(String siteName, String language, String url) 351 { 352 String lowerCaseUrl = StringUtils.replace(url, "'", "''").toLowerCase(); 353 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 + "']"; 354 } 355 356 /** 357 * Get the query verifying the existence of an url for the given user 358 * @param siteName the site name. 359 * @param language the language. 360 * @param url the url to test. 361 * @param user The user identity 362 * @return the query verifying the existence of an url for the given user 363 */ 364 public String getUrlExistsForUserQuery(String siteName, String language, String url, UserIdentity user) 365 { 366 String lowerCaseUrl = StringUtils.replace(url, "'", "''").toLowerCase(); 367 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 + "']"; 368 } 369 370 /** 371 * Normalizes an input string in order to capitalize it, remove accents, and replace whitespaces with underscores 372 * @param s the string to normalize 373 * @return the normalized string 374 */ 375 public String normalizeString(String s) 376 { 377 // Strip accents 378 String normalizedLabel = Normalizer.normalize(s.toUpperCase(), Normalizer.Form.NFD).replaceAll("[^\\p{ASCII}]", ""); 379 380 // Upper case 381 String upperCaseLabel = normalizedLabel.replaceAll(" +", "_").replaceAll("[^\\w-]", "_").replaceAll("_+", "_").toUpperCase(); 382 383 return upperCaseLabel; 384 } 385 386 /** 387 * Get links of a given site and language 388 * @param siteName the site name 389 * @param language the language 390 * @return the links 391 */ 392 public AmetysObjectIterable<DefaultLink> getLinks(String siteName, String language) 393 { 394 Site site = _siteManager.getSite(siteName); 395 TraversableAmetysObject linksNode = getLinksNode(site, language); 396 return linksNode.getChildren(); 397 } 398 399 /** 400 * Get the list of links corresponding to the given theme ids 401 * @param themesIds the ids of the configured themes 402 * @param siteName the site's name 403 * @param language the site's language 404 * @return the list of default links corresponding to the given themes 405 */ 406 public List<DefaultLink> getLinks(List<String> themesIds, String siteName, String language) 407 { 408 Site site = _siteManager.getSite(siteName); 409 TraversableAmetysObject linksNode = getLinksNode(site, language); 410 AmetysObjectIterable<DefaultLink> links = linksNode.getChildren(); 411 412 return links.stream() 413 .filter(l -> themesIds.isEmpty() || !Collections.disjoint(Arrays.asList(l.getThemes()), themesIds)) 414 .collect(Collectors.toList()); 415 } 416 417 /** 418 * Get links of a given site and language, for the given user 419 * @param siteName the site name 420 * @param language the language 421 * @param user The user identity 422 * @return the links for the given user 423 */ 424 public AmetysObjectIterable<DefaultLink> getUserLinks(String siteName, String language, UserIdentity user) 425 { 426 return getUserLinks(siteName, language, user, null); 427 } 428 429 /** 430 * Get links of a given site and language, for the given user 431 * @param siteName the site name 432 * @param language the language 433 * @param user The user identity 434 * @param themeName the theme id to filter user links. If null, return all user links 435 * @return the links for the given user 436 */ 437 public AmetysObjectIterable<DefaultLink> getUserLinks(String siteName, String language, UserIdentity user, String themeName) 438 { 439 ThemeExpression themeExpression = null; 440 if (StringUtils.isNotBlank(themeName) && themeExists(themeName, siteName, language)) 441 { 442 themeExpression = new ThemeExpression(themeName); 443 } 444 445 String linksQuery = getUserLinksQuery(siteName, language, user, themeExpression); 446 return _ametysObjectResolver.query(linksQuery); 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 restrictedThemes If not empty, only link's themes among this restricted list will be saxed 581 * @param userLinks the user links to sax (can be null) 582 * @param storageContext the storage context, null if there is no connected user 583 * @param isConfigurable true if links are configurable 584 * @param contextVars the context variables 585 * @param user the user 586 * @throws SAXException If an error occurs while generating the SAX events 587 * @throws UserPreferencesException if an exception occurs while getting the user preferences 588 */ 589 public void saxLinks(String siteName, ContentHandler contentHandler, List<DefaultLink> links, List<DefaultLink> userLinks, List<String> restrictedThemes, boolean isConfigurable, Map<String, String> contextVars, String storageContext, UserIdentity user) throws SAXException, UserPreferencesException 590 { 591 // left : true if user link 592 // right : the link itself 593 List<Pair<Boolean, DefaultLink>> allLinks = new ArrayList<>(); 594 595 if (links != null) 596 { 597 for (DefaultLink link : links) 598 { 599 allLinks.add(new ImmutablePair<>(false, link)); 600 } 601 } 602 603 if (userLinks != null) 604 { 605 for (DefaultLink link : userLinks) 606 { 607 allLinks.add(new ImmutablePair<>(true, link)); 608 } 609 } 610 611 612 String[] orderedLinksPrefLinksIdsArray = null; 613 String[] hiddenLinksPrefLinksIdsArray = null; 614 if (user != null && isConfigurable) 615 { 616 // 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 617 // Cf issue LINKS-141 618 // Change in org.ametys.plugins.linkdirectory.LinkDirectorySetUserPreferencesAction#act too 619 620 Map<String, String> unTypedUserPrefs = _userPreferencesManager.getUnTypedUserPrefs(user, storageContext, contextVars); 621 622 String orderedLinksPrefValues = unTypedUserPrefs.get("checked-links"); 623 orderedLinksPrefLinksIdsArray = StringUtils.split(orderedLinksPrefValues, ","); 624 625 String hiddenLinksPrefValues = unTypedUserPrefs.get("hidden-links"); 626 hiddenLinksPrefLinksIdsArray = StringUtils.split(hiddenLinksPrefValues, ","); 627 } 628 629 Site site = _siteManager.getSite(siteName); 630 631 boolean hasIPRestriction = hasIPRestriction(site); 632 boolean isIPAuthorized = isInternalIP(site); 633 634 // Sort the list according to the orderedLinksPrefLinksIdsArray 635 if (ArrayUtils.isNotEmpty(orderedLinksPrefLinksIdsArray)) 636 { 637 DefaultLinkSorter defaultLinkSorter = new DefaultLinkSorter(allLinks, orderedLinksPrefLinksIdsArray); 638 allLinks.sort(defaultLinkSorter); 639 } 640 641 for (Pair<Boolean, DefaultLink> linkPair : allLinks) 642 { 643 DefaultLink link = linkPair.getRight(); 644 boolean userLink = linkPair.getLeft(); 645 646 LinkVisibility defaultVisibility = link.getDefaultVisibility(); 647 648 // check the access granted if it is not a user link 649 if (userLink || _isCurrentUserGrantedAccess(link)) 650 { 651 boolean selected = isConfigurable && ArrayUtils.contains(orderedLinksPrefLinksIdsArray, link.getId()); // deprecated, only used for old views, isHidden should be used now 652 boolean isHidden = isConfigurable && (ArrayUtils.contains(hiddenLinksPrefLinksIdsArray, link.getId()) || LinkVisibility.HIDDEN.equals(defaultVisibility) && !selected); 653 saxLink(siteName, contentHandler, link, restrictedThemes, selected, hasIPRestriction, isIPAuthorized, userLink, isHidden); 654 } 655 } 656 } 657 658 /** 659 * SAX a directory link. 660 * @param siteName the site name 661 * @param contentHandler the content handler 662 * @param link the link to sax. 663 * @param restrictedThemes If not empty, only link's themes among this restricted list of themes will be saxed 664 * @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) 665 * @param hasIPRestriction true if we have IP restriction 666 * @param isIPAuthorized true if the IP is authorized 667 * @param userLink true if it is a user link 668 * @param isHidden true if the link is hidden 669 * @throws SAXException If an error occurs while generating the SAX events 670 */ 671 public void saxLink (String siteName, ContentHandler contentHandler, DefaultLink link, List<String> restrictedThemes, boolean selected, boolean hasIPRestriction, boolean isIPAuthorized, boolean userLink, boolean isHidden) throws SAXException 672 { 673 AttributesImpl attrs = new AttributesImpl(); 674 attrs.addCDATAAttribute("id", link.getId()); 675 attrs.addCDATAAttribute("lang", link.getLanguage()); 676 677 LinkType urlType = link.getUrlType(); 678 679 _addURLAttribute(link, hasIPRestriction, isIPAuthorized, attrs); 680 681 attrs.addCDATAAttribute("urlType", StringUtils.defaultString(urlType.toString())); 682 683 if (link.getStatus() != LinkStatus.BROKEN) 684 { 685 String dynInfoProviderId = StringUtils.defaultString(link.getDynamicInformationProvider()); 686 // Check if provider exists 687 if (StringUtils.isNotEmpty(dynInfoProviderId) && _dynamicProviderEP.hasExtension(dynInfoProviderId)) 688 { 689 attrs.addCDATAAttribute("dynamicInformationProvider", dynInfoProviderId); 690 } 691 } 692 attrs.addCDATAAttribute("title", StringUtils.defaultString(link.getTitle())); 693 attrs.addCDATAAttribute("content", StringUtils.defaultString(link.getContent())); 694 695 if (urlType == LinkType.PAGE) 696 { 697 String pageId = link.getUrl(); 698 try 699 { 700 Page page = _ametysObjectResolver.resolveById(pageId); 701 attrs.addCDATAAttribute("pageTitle", page.getTitle()); 702 } 703 catch (UnknownAmetysObjectException e) 704 { 705 attrs.addCDATAAttribute("unknownPage", "true"); 706 } 707 } 708 709 attrs.addCDATAAttribute("alternative", StringUtils.defaultString(link.getAlternative())); 710 attrs.addCDATAAttribute("pictureAlternative", StringUtils.defaultString(link.getPictureAlternative())); 711 712 attrs.addCDATAAttribute("user-selected", selected ? "true" : "false"); 713 714 attrs.addCDATAAttribute("color", _linkDAO.getLinkColor(link)); 715 716 String pictureType = link.getPictureType(); 717 attrs.addCDATAAttribute("pictureType", pictureType); 718 if (pictureType.equals("resource")) 719 { 720 String resourceId = link.getResourcePictureId(); 721 try 722 { 723 Resource resource = _ametysObjectResolver.resolveById(resourceId); 724 attrs.addCDATAAttribute("pictureId", resourceId); 725 attrs.addCDATAAttribute("pictureName", resource.getName()); 726 attrs.addCDATAAttribute("pictureSize", Long.toString(resource.getLength())); 727 attrs.addCDATAAttribute("imageType", "explorer"); 728 } 729 catch (UnknownAmetysObjectException e) 730 { 731 getLogger().error("The resource of id'{}' does not exist anymore. The picture for link of id '{}' will be ignored.", resourceId, link.getId(), e); 732 } 733 734 } 735 else if (pictureType.equals("external")) 736 { 737 Binary picMeta = link.getExternalPicture(); 738 attrs.addCDATAAttribute("picturePath", DefaultLink.PROPERTY_PICTURE); 739 attrs.addCDATAAttribute("pictureName", picMeta.getFilename()); 740 attrs.addCDATAAttribute("pictureSize", Long.toString(picMeta.getLength())); 741 attrs.addCDATAAttribute("imageType", "link-data"); 742 } 743 else if (pictureType.equals("glyph")) 744 { 745 attrs.addCDATAAttribute("pictureGlyph", link.getPictureGlyph()); 746 } 747 748 attrs.addCDATAAttribute("limitedAccess", String.valueOf(!_rightManager.hasAnonymousReadAccess(link))); 749 750 attrs.addCDATAAttribute("userLink", String.valueOf(userLink)); 751 attrs.addCDATAAttribute("isHidden", String.valueOf(isHidden)); 752 753 LinkStatus status = link.getStatus(); 754 if (status != null) 755 { 756 attrs.addCDATAAttribute("status", status.name()); 757 } 758 759 if (StringUtils.isNotBlank(link.getPage())) 760 { 761 attrs.addCDATAAttribute("page", link.getPage()); 762 } 763 764 XMLUtils.startElement(contentHandler, "link", attrs); 765 766 // Themes 767 _saxThemes(contentHandler, link, restrictedThemes); 768 769 XMLUtils.endElement(contentHandler, "link"); 770 } 771 772 /** 773 * Add the URL attribute to sax 774 * @param link the link 775 * @param hasIPRestriction true if we have IP restriction 776 * @param isIPAuthorized true if the IP is authorized 777 * @param attrs the attribute 778 */ 779 private void _addURLAttribute(DefaultLink link, boolean hasIPRestriction, boolean isIPAuthorized, AttributesImpl attrs) 780 { 781 String internalUrl = link.getInternalUrl(); 782 String externalUrl = link.getUrl(); 783 784 // If we have no internal URL or no IP restriction, just sax external URL 785 if (StringUtils.isBlank(internalUrl) || !hasIPRestriction) 786 { 787 attrs.addCDATAAttribute("url", StringUtils.defaultString(externalUrl)); 788 } 789 else 790 { 791 // If the IP is authorized, sax internal URL 792 if (isIPAuthorized) 793 { 794 attrs.addCDATAAttribute("url", StringUtils.defaultString(internalUrl)); 795 } 796 // else if we have external URL, we sax it 797 else if (StringUtils.isNotBlank(externalUrl)) 798 { 799 attrs.addCDATAAttribute("url", StringUtils.defaultString(externalUrl)); 800 } 801 // else link is disabled it because the IP is not authorized 802 else 803 { 804 attrs.addCDATAAttribute("disabled", "true"); 805 } 806 } 807 } 808 809 /** 810 * Get the actual ids of the themes configured properly, their names if they were not 811 * @param configuredThemesNames the normalized ids of the configured themes 812 * @param siteName the site's name 813 * @param language the site's language 814 * @return the actual ids of the configured themes 815 */ 816 public Map<String, List<String>> getThemesMap(List<String> configuredThemesNames, String siteName, String language) 817 { 818 Map<String, List<String>> themesMap = new HashMap<> (); 819 List<String> correctThemesList = new ArrayList<> (); 820 List<String> wrongThemesList = new ArrayList<> (); 821 822 for (int i = 0; i < configuredThemesNames.size(); i++) 823 { 824 String configuredThemeName = configuredThemesNames.get(i); 825 826 Map<String, Object> contextualParameters = new HashMap<>(); 827 contextualParameters.put("language", language); 828 contextualParameters.put("siteName", siteName); 829 Tag theme = _themesDAO.getTag(configuredThemeName, contextualParameters); 830 831 if (theme == null) 832 { 833 getLogger().warn("The theme '{}' was not found. It will be ignored.", configuredThemeName); 834 wrongThemesList.add(configuredThemeName); 835 } 836 else 837 { 838 correctThemesList.add(configuredThemeName); 839 } 840 } 841 842 themesMap.put("themes", correctThemesList); 843 themesMap.put("unknown-themes", wrongThemesList); 844 return themesMap; 845 } 846 847 /** 848 * Verify the existence of a theme 849 * @param themeName the id of the theme to verify 850 * @param siteName the site's name 851 * @param language the site's language 852 * @return true if the theme exists, false otherwise 853 */ 854 public boolean themeExists(String themeName, String siteName, String language) 855 { 856 if (StringUtils.isBlank(themeName)) 857 { 858 return false; 859 } 860 Map<String, Object> contextualParameters = new HashMap<>(); 861 contextualParameters.put("language", language); 862 contextualParameters.put("siteName", siteName); 863 List<String> checkTags = _themesDAO.checkTags(List.of(themeName), false, Collections.EMPTY_MAP, contextualParameters); 864 return !checkTags.isEmpty(); 865 } 866 867 /** 868 * Get theme's title from its name 869 * @param themeName the theme name 870 * @param siteName the site's name 871 * @param language the site's language 872 * @return the title of the theme. Null if the theme doesn't exist 873 */ 874 public I18nizableText getThemeTitle(String themeName, String siteName, String language) 875 { 876 Map<String, Object> contextualParameters = new HashMap<>(); 877 contextualParameters.put("language", language); 878 contextualParameters.put("siteName", siteName); 879 if (themeExists(themeName, siteName, language)) 880 { 881 Tag tag = _themesDAO.getTag(themeName, contextualParameters); 882 return tag.getTitle(); 883 } 884 else 885 { 886 getLogger().warn("Can't find theme with name {} for site {} and language {}", themeName, siteName, language); 887 } 888 889 return null; 890 } 891 892 /** 893 * Get the site's name 894 * @param request the request 895 * @return the site's name 896 */ 897 public String getSiteName(Request request) 898 { 899 return WebHelper.getSiteName(request, (Page) request.getAttribute(Page.class.getName())); 900 } 901 902 /** 903 * Get the site's language 904 * @param request the request 905 * @return the site's language 906 */ 907 public String getLanguage(Request request) 908 { 909 Page page = (Page) request.getAttribute(Page.class.getName()); 910 if (page != null) 911 { 912 return page.getSitemapName(); 913 } 914 915 String language = (String) request.getAttribute(WebConstants.REQUEST_ATTR_SITEMAP_NAME); 916 if (StringUtils.isEmpty(language)) 917 { 918 language = request.getParameter("language"); 919 } 920 921 return language; 922 } 923 924 /** 925 * Retrieve the context variables from the front 926 * @param request the request 927 * @return the map of context variables 928 */ 929 public Map<String, String> getContextVars(Request request) 930 { 931 Map<String, String> contextVars = new HashMap<> (); 932 933 contextVars.put(FOUserPreferencesConstants.CONTEXT_VAR_SITENAME, getSiteName(request)); 934 contextVars.put(FOUserPreferencesConstants.CONTEXT_VAR_LANGUAGE, getLanguage(request)); 935 936 return contextVars; 937 } 938 939 /** 940 * Get the appropriate storage context from request 941 * @param request the request 942 * @param zoneItemId the id of the zone item if we deal with a service, null for an input data 943 * @return the storage context in which the user preferences will be kept 944 */ 945 public String getStorageContext(Request request, String zoneItemId) 946 { 947 String siteName = getSiteName(request); 948 String language = getLanguage(request); 949 950 return StringUtils.isEmpty(zoneItemId) ? siteName + "/" + language : siteName + "/" + language + "/" + zoneItemId; 951 } 952 953 /** 954 * Get the appropriate storage context 955 * @param siteName the name of the site 956 * @param language the language 957 * @param zoneItemId the id of the zone item if we deal with a service, null for an input data 958 * @return the storage context in which the user preferences will be kept 959 */ 960 public String getStorageContext(String siteName, String language, String zoneItemId) 961 { 962 return StringUtils.isEmpty(zoneItemId) ? siteName + "/" + language : siteName + "/" + language + "/" + zoneItemId; 963 } 964 965 /** 966 * Sax the themes 967 * @param contentHandler the content handler 968 * @param link the link 969 * @param restrictedThemes If not empty, only link's themes among this restricted list will be saxed 970 * @throws SAXException If an error occurs while generating the SAX events 971 */ 972 private void _saxThemes (ContentHandler contentHandler, DefaultLink link, List<String> restrictedThemes) throws SAXException 973 { 974 XMLUtils.startElement(contentHandler, "themes"); 975 976 Map<String, Object> contextualParameters = new HashMap<>(); 977 contextualParameters.put("language", link.getLanguage()); 978 contextualParameters.put("siteName", link.getSiteName()); 979 980 for (String themeId : link.getThemes()) 981 { 982 try 983 { 984 Tag tag = _themesDAO.getTag(themeId, contextualParameters); 985 if (tag != null) 986 { 987 if (restrictedThemes.isEmpty() || restrictedThemes.contains(themeId)) 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 } 998 else 999 { 1000 getLogger().error("Theme '{}' in link '{}' can not be found.", themeId, link.getId()); 1001 } 1002 } 1003 catch (UnknownAmetysObjectException e) 1004 { 1005 // Theme does not exist anymore 1006 } 1007 } 1008 1009 1010 XMLUtils.endElement(contentHandler, "themes"); 1011 } 1012 1013 /** 1014 * Determines if the current user is allowed to see the link or not 1015 * @param link the link 1016 * @return true if the current user is allowed to see the link, false otherwise 1017 */ 1018 private boolean _isCurrentUserGrantedAccess(DefaultLink link) 1019 { 1020 UserIdentity user = _currentUserProvider.getUser(); 1021 1022 // There is no access restriction 1023 return _rightManager.hasReadAccess(user, link); 1024 } 1025 1026 /** 1027 * Determines if the site has IP restriction for internal links 1028 * @param site the site 1029 * @return true if the site has IP restriction 1030 */ 1031 public boolean hasIPRestriction(Site site) 1032 { 1033 return site.getValue("allowed-ip") != null; 1034 } 1035 1036 /** 1037 * Determines if the user IP matches the configured internal IP range 1038 * @param site the site 1039 * @return true if the user IP is an authorized IP for internal links or if no IP restriction is configured 1040 */ 1041 public boolean isInternalIP(Site site) 1042 { 1043 String ipRegexp = site.getValue("allowed-ip"); 1044 if (StringUtils.isNotBlank(ipRegexp)) 1045 { 1046 Pattern ipRestriction = Pattern.compile(ipRegexp); 1047 1048 Request request = ContextHelper.getRequest(_context); 1049 1050 // The real client IP may have been put in the non-standard "X-Forwarded-For" request header, in case of reverse proxy 1051 String xff = request.getHeader("X-Forwarded-For"); 1052 String ip = null; 1053 1054 if (xff != null) 1055 { 1056 ip = xff.split(",")[0]; 1057 } 1058 else 1059 { 1060 ip = request.getRemoteAddr(); 1061 } 1062 1063 boolean internalIP = ipRestriction.matcher(ip).matches(); 1064 1065 if (getLogger().isDebugEnabled()) 1066 { 1067 getLogger().debug("Ip '{}' is considered {} with pattern {}", ip, internalIP ? "internal" : "external", ipRestriction.pattern()); 1068 } 1069 1070 return internalIP; 1071 } 1072 1073 // There is no IP restriction, considered user IP is authorized 1074 return true; 1075 } 1076 1077 /** 1078 * Helper class to sort links (DefaultLinkSorter implementation) 1079 * If both links are in the ordered links list, this order is used 1080 * If one of them is in it and not the other, the one in it will be before the other 1081 * If none of them is in the list, the initial order will be used 1082 */ 1083 private class DefaultLinkSorter implements Comparator<Pair<Boolean, DefaultLink>> 1084 { 1085 private String[] _orderedLinksPrefLinksIdsArray; 1086 private List<String> _initialList; 1087 /** 1088 * constructor for the helper 1089 * @param initialList initial list to keep track of the original order if no order is found 1090 * @param orderedLinksPrefLinksIdsArray ordered list of link ids 1091 */ 1092 public DefaultLinkSorter(List<Pair<Boolean, DefaultLink>> initialList, String[] orderedLinksPrefLinksIdsArray) 1093 { 1094 _orderedLinksPrefLinksIdsArray = orderedLinksPrefLinksIdsArray; 1095 _initialList = initialList.stream() 1096 .map(Pair::getRight) 1097 .map(DefaultLink::getId) 1098 .collect(Collectors.toList()); 1099 } 1100 public int compare(Pair<Boolean, DefaultLink> pair1, Pair<Boolean, DefaultLink> pair2) 1101 { 1102 DefaultLink link1 = pair1.getRight(); 1103 DefaultLink link2 = pair2.getRight(); 1104 if (ArrayUtils.isNotEmpty(_orderedLinksPrefLinksIdsArray)) 1105 { 1106 int nbOrderedLinks = _orderedLinksPrefLinksIdsArray.length; 1107 int pos1 = ArrayUtils.indexOf(_orderedLinksPrefLinksIdsArray, link1.getId()); 1108 if (pos1 == ArrayUtils.INDEX_NOT_FOUND) 1109 { 1110 pos1 = nbOrderedLinks + _initialList.indexOf(link1.getId()); // if not found, keep orginal order after user's order 1111 } 1112 1113 int pos2 = ArrayUtils.indexOf(_orderedLinksPrefLinksIdsArray, link2.getId()); 1114 if (pos2 == ArrayUtils.INDEX_NOT_FOUND) 1115 { 1116 pos2 = nbOrderedLinks + _initialList.indexOf(link1.getId()); // if not found, keep orginal order after user's order 1117 } 1118 1119 return pos1 - pos2; 1120 } 1121 else 1122 { 1123 return 0; // No sorting if no sort array 1124 } 1125 } 1126 } 1127}