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