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