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