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