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