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