001/* 002 * Copyright 2012 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 */ 016 017package org.ametys.core.util; 018 019import java.io.IOException; 020import java.io.InputStream; 021import java.net.MalformedURLException; 022import java.util.ArrayList; 023import java.util.Collection; 024import java.util.Date; 025import java.util.HashMap; 026import java.util.List; 027import java.util.Map; 028import java.util.Set; 029 030import org.apache.avalon.framework.context.Context; 031import org.apache.avalon.framework.context.ContextException; 032import org.apache.avalon.framework.context.Contextualizable; 033import org.apache.avalon.framework.service.ServiceException; 034import org.apache.avalon.framework.service.ServiceManager; 035import org.apache.avalon.framework.service.Serviceable; 036import org.apache.cocoon.components.ContextHelper; 037import org.apache.cocoon.environment.Request; 038import org.apache.cocoon.xml.dom.DOMBuilder; 039import org.apache.commons.lang3.StringUtils; 040import org.apache.commons.text.StringEscapeUtils; 041import org.apache.commons.text.translate.LookupTranslator; 042import org.apache.excalibur.source.Source; 043import org.apache.excalibur.source.SourceResolver; 044import org.slf4j.Logger; 045import org.slf4j.LoggerFactory; 046import org.w3c.dom.Node; 047import org.w3c.dom.NodeList; 048import org.xml.sax.SAXException; 049 050import org.ametys.core.DevMode; 051import org.ametys.core.DevMode.DEVMODE; 052import org.ametys.core.group.Group; 053import org.ametys.core.group.GroupIdentity; 054import org.ametys.core.group.GroupManager; 055import org.ametys.core.right.RightManager; 056import org.ametys.core.right.RightManager.RightResult; 057import org.ametys.core.user.CurrentUserProvider; 058import org.ametys.core.user.User; 059import org.ametys.core.user.UserIdentity; 060import org.ametys.core.user.UserManager; 061import org.ametys.core.user.directory.NotUniqueUserException; 062import org.ametys.core.util.dom.AmetysNodeList; 063import org.ametys.core.util.dom.MapElement; 064import org.ametys.core.util.dom.StringElement; 065import org.ametys.core.version.Version; 066import org.ametys.core.version.VersionsHandler; 067import org.ametys.plugins.core.user.UserHelper; 068import org.ametys.runtime.config.Config; 069import org.ametys.runtime.i18n.I18nizableText; 070import org.ametys.runtime.plugin.PluginsManager; 071import org.ametys.runtime.servlet.RuntimeConfig; 072import org.ametys.runtime.workspace.WorkspaceManager; 073import org.ametys.runtime.workspace.WorkspaceMatcher; 074 075/** 076 * Helper component to be used from XSL stylesheets. 077 */ 078public class AmetysXSLTHelper implements Contextualizable, Serviceable 079{ 080 /** The logger */ 081 protected static final Logger _LOGGER = LoggerFactory.getLogger(AmetysXSLTHelper.class.getName()); 082 083 /** The i18n utils instance */ 084 protected static I18nUtils _i18nUtils; 085 086 /** The versions handler */ 087 protected static VersionsHandler _versionHandler; 088 089 /** The current user provider */ 090 protected static CurrentUserProvider _currentUserProvider; 091 /** The groups manager */ 092 protected static GroupManager _groupManager; 093 /** The user helper */ 094 protected static UserHelper _userHelper; 095 /** The json utils */ 096 protected static JSONUtils _jsonUtils; 097 /** The right manager */ 098 protected static RightManager _rightManager; 099 /** The user manager */ 100 protected static UserManager _userManager; 101 /** The source resolver */ 102 protected static SourceResolver _sourceResolver; 103 104 private static Context _context; 105 106 @Override 107 public void contextualize(Context context) throws ContextException 108 { 109 _context = context; 110 } 111 112 public void service(ServiceManager manager) throws ServiceException 113 { 114 _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE); 115 _versionHandler = (VersionsHandler) manager.lookup(VersionsHandler.ROLE); 116 117 _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE); 118 _groupManager = (GroupManager) manager.lookup(GroupManager.ROLE); 119 _userHelper = (UserHelper) manager.lookup(UserHelper.ROLE); 120 _jsonUtils = (JSONUtils) manager.lookup(JSONUtils.ROLE); 121 _rightManager = (RightManager) manager.lookup(RightManager.ROLE); 122 _userManager = (UserManager) manager.lookup(UserManager.ROLE); 123 _sourceResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE); 124 } 125 126 /** 127 * Returns the current URI prefix. 128 * @return the current URI prefix. 129 */ 130 public static String uriPrefix() 131 { 132 return uriPrefix(true); 133 } 134 135 /** 136 * Returns the current URI prefix. 137 * @param withWorkspaceURI true to add the workspace URI (recommended) 138 * @return the current URI prefix. 139 */ 140 public static String uriPrefix(boolean withWorkspaceURI) 141 { 142 return getUriPrefix(withWorkspaceURI); 143 } 144 145 /** 146 * Returns the absolute URI prefix. 147 * @return the absolute URI prefix. 148 */ 149 public static String absoluteUriPrefix() 150 { 151 return absoluteUriPrefix(true); 152 } 153 154 /** 155 * Returns the absolute URI prefix. 156 * @param withWorkspaceURI true to add the workspace URI (recommended) 157 * @return the absolute URI prefix. 158 */ 159 public static String absoluteUriPrefix(boolean withWorkspaceURI) 160 { 161 return getAbsoluteUriPrefix(withWorkspaceURI); 162 } 163 164 /** 165 * Return the current workspace name 166 * @return The workspace name. Cannot be empty. 167 */ 168 public static String workspaceName() 169 { 170 return getWorkspaceName(); 171 } 172 173 /** 174 * Return the current workspace URI 175 * @return The workspace name. Can be empty. 176 */ 177 public static String workspacePrefix() 178 { 179 return getWorkspacePrefix(); 180 } 181 182 /** 183 * Return the current workspace theme name 184 * @return The name 185 */ 186 public static String workspaceTheme() 187 { 188 Request request = ContextHelper.getRequest(_context); 189 return (String) request.getAttribute(WorkspaceMatcher.WORKSPACE_THEME); 190 } 191 192 /** 193 * Return the current workspace theme url 194 * @return The url without any prefix 195 */ 196 public static String workspaceThemeURL() 197 { 198 Request request = ContextHelper.getRequest(_context); 199 200 String workspaceThemeUrl = (String) request.getAttribute(WorkspaceMatcher.WORKSPACE_THEME_URL); 201 if (workspaceThemeUrl == null) 202 { 203 // fallback to the default workspace 204 String workspaceName = RuntimeConfig.getInstance().getDefaultWorkspace(); 205 WorkspaceManager wm = WorkspaceManager.getInstance(); 206 if (wm.getWorkspaceNames().contains(workspaceName)) 207 { 208 workspaceThemeUrl = wm.getWorkspaces().get(workspaceName).getThemeURL(); 209 } 210 } 211 212 return workspaceThemeUrl; 213 } 214 215 /** 216 * Get the application context path. Can be empty if the application 217 * resides in the root context. Use it to create a link beginning with 218 * the application root. 219 * @param withWorkspaceURI true to add the workspace URI (recommended) 220 * @return The application context path with workspace URI 221 * @see Request#getContextPath() 222 */ 223 protected static String getUriPrefix(boolean withWorkspaceURI) 224 { 225 Request request = ContextHelper.getRequest(_context); 226 String workspaceURI = withWorkspaceURI ? getWorkspacePrefix() : ""; 227 228 return request.getContextPath() + workspaceURI; 229 } 230 231 /** 232 * Get the absolutized version of the context path. Use it to create an absolute 233 * link beginning with the application root, for instance when sending a mail 234 * linking to the application. 235 * @param withWorkspaceURI true to add the workspace URI (recommended) 236 * @return The absolute context path. 237 */ 238 protected static String getAbsoluteUriPrefix(boolean withWorkspaceURI) 239 { 240 Request request = ContextHelper.getRequest(_context); 241 242 String uriPrefix = getUriPrefix(withWorkspaceURI); 243 244 if (!uriPrefix.startsWith("http")) 245 { 246 uriPrefix = request.getScheme() + "://" + request.getServerName() + (request.getServerPort() != 80 ? ":" + request.getServerPort() : "") + uriPrefix; 247 } 248 249 return uriPrefix; 250 } 251 252 /** 253 * Return the current workspace name 254 * @return The workspace name. Cannot be empty. 255 */ 256 protected static String getWorkspaceName() 257 { 258 Request request = ContextHelper.getRequest(_context); 259 return (String) request.getAttribute(WorkspaceMatcher.WORKSPACE_NAME); 260 } 261 262 /** 263 * Return the current workspace URI 264 * @return The workspace name. Can be empty for the default workspace. 265 */ 266 protected static String getWorkspacePrefix() 267 { 268 Request request = ContextHelper.getRequest(_context); 269 return (String) request.getAttribute(WorkspaceMatcher.WORKSPACE_URI); 270 } 271 272 /** 273 * Returns the configuration value associated with the given parameter. 274 * @param id the configuration parameter. 275 * @return the configuration value associated with the given parameter. 276 */ 277 public static Object config(String id) 278 { 279 if (Config.getInstance() != null) 280 { 281 return Config.getInstance().getValue(id); 282 } 283 else 284 { 285 return null; 286 } 287 } 288 289 /** 290 * Return the value of a request parameter. 291 * @param parameter the parameter name. 292 * @return the request parameter. 293 */ 294 public static String requestParameter(String parameter) 295 { 296 Request request = ContextHelper.getRequest(_context); 297 return request.getParameter(parameter); 298 } 299 300 /** 301 * Translate an i18n key using current user language. 302 * @param key The key to translate. Specify the catalog this way: "catalogue:KEY" 303 * @return The translation or null. 304 */ 305 public static String translate(String key) 306 { 307 return translate(key, null, null); 308 } 309 310 /** 311 * Translate an i18n key 312 * @param key The key to translate. Specify the catalog this way: "catalogue:KEY" 313 * @param lang The language. Can be null to use current user language. 314 * @return The translation or null. 315 */ 316 public static String translate(String key, String lang) 317 { 318 return translate(key, lang, null); 319 } 320 321 /** 322 * Translate an i18n key 323 * @param key The key to translate. Specify the catalog this way: "catalogue:KEY" 324 * @param lang The language. Can be null to use current user language. 325 * @param parameters The key parameters. Can be empty. 326 * @return The translation or null. 327 */ 328 public static String translate(String key, String lang, NodeList parameters) 329 { 330 List<String> i18nparams = new ArrayList<>(); 331 if (parameters != null && parameters.getLength() == 1) 332 { 333 NodeList childNodes = parameters.item(0).getChildNodes(); 334 for (int i = 0; i < childNodes.getLength(); i++) 335 { 336 i18nparams.add(childNodes.item(i).getTextContent()); 337 } 338 } 339 340 I18nizableText i18nKey = new I18nizableText(null, key, i18nparams); 341 return _i18nUtils.translate(i18nKey, lang); 342 } 343 344 /** 345 * Escape the given string to be used as JS variable. 346 * @param str the string to escape. 347 * @return the escaped String. 348 */ 349 public static String escapeJS(String str) 350 { 351 return StringEscapeUtils.escapeEcmaScript(str); 352 } 353 354 /** 355 * Escape the given string to be used as text, without interpreting HTML inside of it. 356 * @param str the string to escape. 357 * @return the escaped String. 358 */ 359 public static String escapeHTML(String str) 360 { 361 return org.ametys.core.util.StringUtils.escapeHTML(str); 362 } 363 364 /** 365 * Escape the given URL, meant to be used in eg. a background-image CSS style 366 * @param url the URL to escape 367 * @return the escaped URL 368 */ 369 public static String escapeURLforCSS(String url) 370 { 371 Map<CharSequence, CharSequence> escapeMap = Map.of("'", "\\'", 372 "\"", "\\\"", 373 "(", "\\(", 374 ")", "\\)"); 375 376 return new LookupTranslator(escapeMap).translate(url); 377 } 378 379 /** 380 * Split the text with the given delimiter and return a NodeList of StringElement with "item" as tag name and the split item as value. 381 * @param textToSplit the text to split. 382 * @param delimiter the delimiter to use for splitting the text. 383 * @return a NodeList of StringElement with "item" as tag name and the split item as value. 384 */ 385 public static NodeList split(String textToSplit, String delimiter) 386 { 387 List<Node> items = new ArrayList<>(); 388 389 String[] results = StringUtils.split(textToSplit, delimiter); 390 for (String item : results) 391 { 392 items.add(new StringElement("item", item.trim())); 393 } 394 395 return new AmetysNodeList(items); 396 } 397 398 /** 399 * Split the text. 400 * @param textToSplit the text to split. 401 * @param tokenizers the tokenizer characters. 402 * @param startIndex the minimum number of characters of the result string 403 * @return the split text. 404 */ 405 public static String splitText(String textToSplit, String tokenizers, int startIndex) 406 { 407 String tokenizableText = textToSplit.substring(startIndex != 0 ? startIndex - 1 : 0, textToSplit.length()); 408 409 int tokenPlace = StringUtils.indexOfAny(tokenizableText, tokenizers); 410 411 if (tokenPlace == -1) 412 { 413 return textToSplit; 414 } 415 else 416 { 417 return textToSplit.substring(0, startIndex - 1 + tokenPlace); 418 } 419 } 420 421 /** 422 * Split the text. 423 * @param textToSplit the text to split. 424 * @param tokenizers the tokenizer characters. 425 * @param maxCharacters the maximum number of characters of the result string 426 * @param currentCharactersNumber the current character number. 427 * @return the split text. 428 */ 429 @Deprecated 430 public static String splitText(String textToSplit, String tokenizers, int maxCharacters, int currentCharactersNumber) 431 { 432 int tokenStartIndex = maxCharacters - currentCharactersNumber - 1; 433 String tokenizableText = textToSplit.substring(tokenStartIndex, textToSplit.length()); 434 435 int tokenPlace = StringUtils.indexOfAny(tokenizableText, tokenizers); 436 437 if (tokenPlace == -1) 438 { 439 return textToSplit; 440 } 441 else 442 { 443 return textToSplit.substring(0, tokenStartIndex + tokenPlace); 444 } 445 } 446 447 /** 448 * Get the versions of the application. 449 * Default VersionsHandler impl will return Ametys and Application versions 450 * @return The versions <Version><Version><Name>X</Name><Version>X</Version><Date>X</Date></Version></Versions> (empty tags are removed) 451 */ 452 public static Node versions() 453 { 454 Map<String, Object> versionsMap = new HashMap<>(); 455 456 List<Object> versionList = new ArrayList<>(); 457 458 for (Version version : _versionHandler.getVersions()) 459 { 460 Map<String, Object> versionMap = new HashMap<>(); 461 462 String componentName = version.getName(); 463 String componentVersion = version.getVersion(); 464 String componentDate = DateUtils.dateToString(version.getDate()); 465 466 if (StringUtils.isNotEmpty(componentName)) 467 { 468 versionMap.put("Name", componentName); 469 } 470 if (StringUtils.isNotEmpty(componentVersion)) 471 { 472 versionMap.put("Version", componentVersion); 473 } 474 if (StringUtils.isNotEmpty(componentDate)) 475 { 476 versionMap.put("Date", componentDate); 477 } 478 479 versionList.add(versionMap); 480 } 481 482 versionsMap.put("Component", versionList); 483 484 return new MapElement("Versions", versionsMap); 485 } 486 487 /** 488 * Get the current mode of the application for the current user. 489 * @return True if the application is in developer mode, false if in production mode. 490 */ 491 public static boolean isDeveloperMode() 492 { 493 return isDeveloperMode(true); 494 } 495 496 /** 497 * Get the current mode of the application for the current user. 498 * @param allowRequestOverride true to take the request into account when determining the mode 499 * @return True if the application is in developer mode, false if in production mode. 500 */ 501 public static boolean isDeveloperMode(boolean allowRequestOverride) 502 { 503 Request request = allowRequestOverride ? ContextHelper.getRequest(_context) : null; 504 505 DEVMODE developerMode = DevMode.getDeveloperMode(request); 506 return developerMode == DEVMODE.DEVELOPMENT 507 || developerMode == DEVMODE.SUPER_DEVELOPPMENT; 508 } 509 510 /** 511 * Return the user 512 * @return The current connected user object or null 513 * @throws SAXException if a problem occured while getting the user 514 */ 515 public static Node user() throws SAXException 516 { 517 UserIdentity userIdentity = _currentUserProvider.getUser(); 518 if (userIdentity != null) 519 { 520 return user(userIdentity.getLogin(), userIdentity.getPopulationId()); 521 } 522 523 return null; 524 } 525 526 /** 527 * Return the given user 528 * @param userIdentity the concerned user's login + population 529 * @return The informations about the given user 530 * @throws SAXException If an error occurred while saxing the user 531 */ 532 public static Node user(String userIdentity) throws SAXException 533 { 534 UserIdentity userIdentityObject = UserIdentity.stringToUserIdentity(userIdentity); 535 if (userIdentityObject == null) 536 { 537 return null; 538 } 539 else 540 { 541 return user(userIdentityObject.getLogin(), userIdentityObject.getPopulationId()); 542 } 543 } 544 545 /** 546 * Return the given user 547 * @param login the concerned user's login 548 * @param populationId the concerned user's population id 549 * @return The informations about the given user 550 * @throws SAXException If an error occurred while saxing the user 551 */ 552 public static Node user(String login, String populationId) throws SAXException 553 { 554 DOMBuilder domBuilder = new DOMBuilder(); 555 556 UserIdentity userIdentity = new UserIdentity(login, populationId); 557 _userHelper.saxUserIdentity(userIdentity, domBuilder); 558 559 return domBuilder.getDocument(); 560 } 561 562 /** 563 * Return the given user 564 * @param email the concerned user's email 565 * @param populationId the concerned user's population id 566 * @return The informations about the given user 567 * @throws SAXException If an error occurred while saxing the user 568 */ 569 public static Node userByMail(String email, String populationId) throws SAXException 570 { 571 try 572 { 573 User user = _userManager.getUserByEmail(populationId, email); 574 if (user != null) 575 { 576 return user(UserIdentity.userIdentityToString(user.getIdentity())); 577 } 578 } 579 catch (NotUniqueUserException e) 580 { 581 return null; 582 } 583 return null; 584 } 585 586 /** 587 * Returns the list of the current user's groups. 588 * @return the list of the current user's groups. Can be null if there is no connected user. 589 */ 590 public static NodeList groups() 591 { 592 UserIdentity userIdentity = _currentUserProvider.getUser(); 593 return userIdentity != null ? groups(userIdentity.getLogin(), userIdentity.getPopulationId()) : null; 594 } 595 596 /** 597 * Returns the of the given user's group. 598 * @param userIdentity the concerned user's login + population 599 * @return the of the given user's group. 600 */ 601 public static NodeList groups(String userIdentity) 602 { 603 UserIdentity userIdentityObject = UserIdentity.stringToUserIdentity(userIdentity); 604 if (userIdentityObject == null) 605 { 606 return null; 607 } 608 else 609 { 610 return groups(userIdentityObject.getLogin(), userIdentityObject.getPopulationId()); 611 } 612 } 613 614 /** 615 * Returns the of the given user's group. 616 * @param login the concerned user's login. 617 * @param populationId the concerned user's population. 618 * @return the of the given user's group. 619 */ 620 public static NodeList groups(String login, String populationId) 621 { 622 ArrayList<Node> groups = new ArrayList<>(); 623 624 Set<GroupIdentity> userGroups = _groupManager.getUserGroups(new UserIdentity(login, populationId)); 625 for (GroupIdentity groupId : userGroups) 626 { 627 Group group = _groupManager.getGroup(groupId); 628 if (group != null) 629 { 630 Map<String, String> attributes = new HashMap<>(); 631 attributes.put("name", groupId.getId()); 632 attributes.put("directory", groupId.getDirectoryId()); 633 groups.add(new StringElement("group", attributes, group.getLabel())); 634 } 635 } 636 637 return new AmetysNodeList(groups); 638 } 639 640 /** 641 * Parse a JSON string as a Map and return the value with the desired key 642 * @param jsonString the JSON representation of the object. 643 * @param key name of the value to return 644 * @return the value as a String, or empty string if an error occurred or the key was not found. 645 */ 646 public static String getValueFromJsonObject(String jsonString, String key) 647 { 648 try 649 { 650 Map<String, Object> jsonMap = _jsonUtils.convertJsonToMap(jsonString); 651 if (jsonMap.containsKey(key)) 652 { 653 Object value = jsonMap.get(key); 654 if (value instanceof Map || value instanceof Collection) 655 { 656 _LOGGER.warn("Unable to get string value for key '{}' from json object {}: the value can not be a map nor collection", key, jsonString); 657 } 658 else if (value instanceof Date) 659 { 660 return DateUtils.dateToString((Date) value); 661 } 662 else 663 { 664 return value.toString(); 665 } 666 } 667 } 668 catch (Exception e) 669 { 670 _LOGGER.warn("Unable to parse json object {}", jsonString, e); 671 } 672 673 return ""; 674 } 675 676 /** 677 * Determines if the current logged user has right on a String context 678 * @param rightId The id of right 679 * @param objectCtx the context. Can be null to search on any context. 680 * @return true if the current user is allowed, false otherwise 681 */ 682 public static boolean hasRight(String rightId, String objectCtx) 683 { 684 return _rightManager.currentUserHasRight(rightId, objectCtx) == RightResult.RIGHT_ALLOW; 685 } 686 687 /** 688 * Check if a given plugin is enabled 689 * @param plugin the plugin's name. 690 * @return the plugin is enabled 691 */ 692 public static boolean isPluginEnabled(String plugin) 693 { 694 return PluginsManager.getInstance().getPluginNames().contains(plugin); 695 } 696 697 /** 698 * Computes the base 64 representation of the image at the specified path in the given plugin. 699 * @param plugin the plugin's name. 700 * @param path the resource path. 701 * @return the base 64 encoding for the given resource. 702 * @throws IOException if an error occurs when trying to get the file 703 * @throws MalformedURLException if the url is invalid 704 */ 705 public static String pluginImageBase64(String plugin, String path) throws MalformedURLException, IOException 706 { 707 Source source = null; 708 try 709 { 710 source = _sourceResolver.resolveURI("plugin:" + plugin + "://resources/" + path); 711 return _getResourceBase64(source); 712 } 713 finally 714 { 715 if (source != null) 716 { 717 _sourceResolver.release(source); 718 } 719 } 720 } 721 722 /** 723 * Get the base 64 encoding for the given source 724 * @param source the source 725 * @return the base 64 encoding of the source 726 */ 727 protected static String _getResourceBase64(Source source) 728 { 729 if (source.exists()) 730 { 731 732 try (InputStream dataIs = source.getInputStream()) 733 { 734 return ImageResolverHelper.resolveImageAsBase64(dataIs, source.getMimeType(), 0, 0, 0, 0); 735 } 736 catch (Exception e) 737 { 738 throw new IllegalStateException(e); 739 } 740 } 741 742 return ""; 743 } 744 745 /** 746 * Determine if a plugin with the given name is available 747 * @param pluginName the plugin name 748 * @return true if the plugin is available 749 */ 750 public boolean pluginAvailable(String pluginName) 751 { 752 return PluginsManager.getInstance().getPluginLocation(pluginName) != null; 753 } 754}