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