001/* 002 * Copyright 2017 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.workspaces.project; 017 018import java.io.IOException; 019import java.io.InputStream; 020import java.util.ArrayList; 021import java.util.Arrays; 022import java.util.Collection; 023import java.util.Date; 024import java.util.HashMap; 025import java.util.List; 026import java.util.Map; 027import java.util.Map.Entry; 028import java.util.Objects; 029import java.util.Optional; 030import java.util.Set; 031import java.util.function.Function; 032import java.util.function.Predicate; 033import java.util.stream.Collectors; 034 035import javax.jcr.Node; 036import javax.jcr.RepositoryException; 037import javax.jcr.Session; 038import javax.mail.MessagingException; 039 040import org.apache.avalon.framework.component.Component; 041import org.apache.avalon.framework.configuration.Configuration; 042import org.apache.avalon.framework.configuration.ConfigurationException; 043import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder; 044import org.apache.avalon.framework.context.Context; 045import org.apache.avalon.framework.context.ContextException; 046import org.apache.avalon.framework.context.Contextualizable; 047import org.apache.avalon.framework.logger.AbstractLogEnabled; 048import org.apache.avalon.framework.service.ServiceException; 049import org.apache.avalon.framework.service.ServiceManager; 050import org.apache.avalon.framework.service.Serviceable; 051import org.apache.cocoon.components.ContextHelper; 052import org.apache.cocoon.environment.Request; 053import org.apache.cocoon.servlet.multipart.Part; 054import org.apache.cocoon.servlet.multipart.PartOnDisk; 055import org.apache.commons.lang.ArrayUtils; 056import org.apache.commons.lang.StringUtils; 057import org.apache.excalibur.source.Source; 058import org.apache.excalibur.source.SourceResolver; 059import org.xml.sax.SAXException; 060 061import org.ametys.cms.languages.LanguagesManager; 062import org.ametys.cms.transformation.xslt.ResolveURIComponent; 063import org.ametys.core.group.GroupDirectoryContextHelper; 064import org.ametys.core.observation.Event; 065import org.ametys.core.observation.ObservationManager; 066import org.ametys.core.right.ProfileAssignmentStorageExtensionPoint; 067import org.ametys.core.right.RightManager; 068import org.ametys.core.right.RightManager.RightResult; 069import org.ametys.core.ui.Callable; 070import org.ametys.core.user.CurrentUserProvider; 071import org.ametys.core.user.User; 072import org.ametys.core.user.UserIdentity; 073import org.ametys.core.user.UserManager; 074import org.ametys.core.user.population.PopulationContextHelper; 075import org.ametys.core.util.I18nUtils; 076import org.ametys.core.util.URLEncoder; 077import org.ametys.core.util.mail.SendMailHelper; 078import org.ametys.plugins.repository.AmetysObject; 079import org.ametys.plugins.repository.AmetysObjectIterable; 080import org.ametys.plugins.repository.AmetysObjectResolver; 081import org.ametys.plugins.repository.AmetysRepositoryException; 082import org.ametys.plugins.repository.ModifiableAmetysObject; 083import org.ametys.plugins.repository.ModifiableTraversableAmetysObject; 084import org.ametys.plugins.repository.data.holder.ModelAwareDataHolder; 085import org.ametys.plugins.repository.jcr.SimpleAmetysObject; 086import org.ametys.plugins.workspaces.members.ProjectMemberManager; 087import org.ametys.plugins.workspaces.project.modules.WorkspaceModuleExtensionPoint; 088import org.ametys.plugins.workspaces.project.objects.Project; 089import org.ametys.plugins.workspaces.project.objects.Project.InscriptionStatus; 090import org.ametys.plugins.workspaces.project.objects.ProjectCategory; 091import org.ametys.plugins.workspaces.project.rights.ProjectRightHelper; 092import org.ametys.runtime.config.Config; 093import org.ametys.runtime.i18n.I18nizableText; 094import org.ametys.web.ObservationConstants; 095import org.ametys.web.repository.page.Page; 096import org.ametys.web.repository.page.ZoneItem; 097import org.ametys.web.repository.site.Site; 098import org.ametys.web.repository.site.SiteDAO; 099import org.ametys.web.repository.sitemap.Sitemap; 100import org.ametys.web.site.SiteConfigurationExtensionPoint; 101import org.ametys.web.site.SiteParameter; 102 103import com.google.common.collect.Iterables; 104 105/** 106 * Manager for the Projects Catalogue service 107 */ 108public class ProjectsCatalogueManager extends AbstractLogEnabled implements Serviceable, Component, Contextualizable 109{ 110 /** Avalon Role */ 111 public static final String ROLE = ProjectsCatalogueManager.class.getName(); 112 113 private static final String __RIGHT_PROJECT_CREATE = "Plugins_Workspaces_Rights_Project_Create"; 114 private static final String __RIGHT_PROJECT_EDIT = "Plugins_Workspaces_Rights_Project_Edit"; 115 private static final String __RIGHT_PROJECT_DELETE = "Plugins_Workspaces_Rights_Project_Delete"; 116 117 private static final String __RIGHT_PROJECT_FO_CREATE = "Plugins_Workspaces_Rights_Project_FO_Create"; 118 private static final String __RIGHT_PROJECT_FO_EDIT = "Plugins_Workspaces_Rights_Project_FO_Edit"; 119 private static final String __RIGHT_PROJECT_FO_DELETE = "Plugins_Workspaces_Rights_Project_FO_Delete"; 120 121 /** List of allowed field received from the front */ 122 private static final String[] __ALLOWED_FORM_DATA = {"description", "emailList", "inscriptionStatus", "defaultProfile", "tags", "categoryTags", "keywords"}; 123 124 /** Ametys Object Resolver */ 125 protected AmetysObjectResolver _resolver; 126 /** Current user provider */ 127 protected CurrentUserProvider _currentUserProvider; 128 /** The project members' manager */ 129 protected ProjectMemberManager _projectMemberManager; 130 /** The right manager */ 131 protected RightManager _rightManager; 132 /** The project manager */ 133 protected ProjectManager _projectManager; 134 /** Helper for project's rights */ 135 protected ProjectRightHelper _projectRightHelper; 136 /** The language manager */ 137 protected LanguagesManager _languagesManager; 138 /** The source resolver */ 139 protected SourceResolver _sourceResolver; 140 /** The site dao */ 141 protected SiteDAO _siteDAO; 142 /** The site's configuration handler */ 143 protected SiteConfigurationExtensionPoint _siteConfigurationEP; 144 /** Helper for user population */ 145 protected PopulationContextHelper _populationContextHelper; 146 /** The extension point for workspace's modules */ 147 protected WorkspaceModuleExtensionPoint _moduleManagerEP; 148 /** The extension point for profiles' storage */ 149 protected ProfileAssignmentStorageExtensionPoint _profileAssignmentStorageExtensionPoint; 150 /** The user manager */ 151 protected UserManager _userManager; 152 /** Utils for i18n */ 153 protected I18nUtils _i18nUtils; 154 /** The observation manager */ 155 protected ObservationManager _observationManager; 156 /** Helper for group directory's context */ 157 protected GroupDirectoryContextHelper _groupDirectoryContextHelper; 158 159 /** The avalon context */ 160 protected Context _context; 161 162 public void service(ServiceManager manager) throws ServiceException 163 { 164 _rightManager = (RightManager) manager.lookup(RightManager.ROLE); 165 _projectManager = (ProjectManager) manager.lookup(ProjectManager.ROLE); 166 _projectMemberManager = (ProjectMemberManager) manager.lookup(ProjectMemberManager.ROLE); 167 _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE); 168 _projectRightHelper = (ProjectRightHelper) manager.lookup(ProjectRightHelper.ROLE); 169 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 170 _languagesManager = (LanguagesManager) manager.lookup(LanguagesManager.ROLE); 171 _sourceResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE); 172 _siteConfigurationEP = (SiteConfigurationExtensionPoint) manager.lookup(SiteConfigurationExtensionPoint.ROLE); 173 _siteDAO = (SiteDAO) manager.lookup(SiteDAO.ROLE); 174 _populationContextHelper = (PopulationContextHelper) manager.lookup(PopulationContextHelper.ROLE); 175 _moduleManagerEP = (WorkspaceModuleExtensionPoint) manager.lookup(WorkspaceModuleExtensionPoint.ROLE); 176 _profileAssignmentStorageExtensionPoint = (ProfileAssignmentStorageExtensionPoint) manager.lookup(ProfileAssignmentStorageExtensionPoint.ROLE); 177 _userManager = (UserManager) manager.lookup(UserManager.ROLE); 178 _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE); 179 _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE); 180 _groupDirectoryContextHelper = (GroupDirectoryContextHelper) manager.lookup(GroupDirectoryContextHelper.ROLE); 181 } 182 183 public void contextualize(Context context) throws ContextException 184 { 185 _context = context; 186 } 187 188 /** 189 * Get the rights to create, edit or delete projects, for the current user 190 * @return The map of rights for the current user 191 */ 192 @Callable 193 public Map<String, Object> getRights() 194 { 195 Map<String, Object> rights = new HashMap<>(); 196 197 rights.put("create", _rightManager.currentUserHasRight(__RIGHT_PROJECT_CREATE, null) == RightResult.RIGHT_ALLOW 198 || _rightManager.currentUserHasRight(__RIGHT_PROJECT_FO_CREATE, null) == RightResult.RIGHT_ALLOW); 199 rights.put("edit", _rightManager.currentUserHasRight(__RIGHT_PROJECT_EDIT, null) == RightResult.RIGHT_ALLOW); 200 rights.put("delete", _rightManager.currentUserHasRight(__RIGHT_PROJECT_DELETE, null) == RightResult.RIGHT_ALLOW); 201 202 return rights; 203 } 204 205 /** 206 * Get the categories tree 207 * @param id The tree root. Can be null 208 * @return The tree data 209 */ 210 @Callable 211 public List<Map<String, Object>> getCategoriesTree(String id) 212 { 213 return _projectManager.getProjectTree(id, 0, false, false, null); 214 } 215 216 /** 217 * Get the node information of a specific project or category from the project tree 218 * @param id The category or project id 219 * @param includeProjects False to only get the categories tree 220 * @return The node informations 221 */ 222 @Callable 223 public List<Map<String, Object>> getProjectTreeNode(String id, boolean includeProjects) 224 { 225 return getProjectTreeNode(id, includeProjects, false); 226 } 227 228 /** 229 * Get the node information of a specific project or category from the project tree 230 * @param id The category or project id 231 * @param zoneItemId The zone item of the project catalogue 232 * @param includeProjects False to only get the categories tree 233 * @return The node informations 234 */ 235 @Callable 236 public List<Map<String, Object>> getProjectTreeNode(String id, String zoneItemId, boolean includeProjects) 237 { 238 ZoneItem zoneItem = _resolver.resolveById(zoneItemId); 239 ModelAwareDataHolder serviceParameters = zoneItem.getServiceParameters(); 240 241 String[] filterCategoriesArray = serviceParameters.getValue("filterCategories", false, new String[0]); 242 List<String> filterCategories = filterCategoriesArray.length > 0 ? Arrays.asList(filterCategoriesArray) : null; 243 244 return getProjectTreeNode(id, includeProjects, serviceParameters.getValue("memberOnly", false, true), filterCategories); 245 } 246 247 /** 248 * Get the node information of a specific project or category from the project tree 249 * @param id The category or project id 250 * @param includeProjects False to only get the categories tree 251 * @param memberOnly Only return projects for which the current user is a member 252 * @return The node informations 253 */ 254 @Callable 255 public List<Map<String, Object>> getProjectTreeNode(String id, boolean includeProjects, boolean memberOnly) 256 { 257 return getProjectTreeNode(id, includeProjects, memberOnly, null); 258 } 259 260 /** 261 * Get the node information of a specific project or category from the project tree 262 * @param id The category or project id 263 * @param includeProjects False to only get the categories tree 264 * @param memberOnly Only return projects for which the current user is a member 265 * @param filterCategories The list of categories to filter. Can be null to ignore 266 * @return The node informations 267 */ 268 @Callable 269 public List<Map<String, Object>> getProjectTreeNode(String id, boolean includeProjects, boolean memberOnly, List<String> filterCategories) 270 { 271 return _projectManager.getProjectTreeNodes(id, 1, 0, includeProjects, memberOnly, filterCategories).stream() 272 .map(node -> this._node2Json(node, includeProjects, filterCategories)) 273 .filter(Objects::nonNull) 274 .collect(Collectors.toList()); 275 } 276 277 private Map<String, Object> _node2Json(Object treeNode, boolean includeProjects, List<String> filterCategories) 278 { 279 if (treeNode instanceof Project) 280 { 281 Project project = (Project) treeNode; 282 Map<String, Object> projectProperties = _projectManager.getProjectProperties(project); 283 boolean hasAccess = _projectMemberManager.isProjectMember(project, _currentUserProvider.getUser()); 284 projectProperties.put("hasAccess", hasAccess); 285 286 Map<String, Boolean> rights = new HashMap<>(); 287 rights.put("edit", _rightManager.currentUserHasRight(__RIGHT_PROJECT_FO_EDIT, project) == RightResult.RIGHT_ALLOW); 288 rights.put("delete", _rightManager.currentUserHasRight(__RIGHT_PROJECT_FO_DELETE, project) == RightResult.RIGHT_ALLOW); 289 projectProperties.put("rights", rights); 290 291 InscriptionStatus inscriptionStatus = project.getInscriptionStatus(); 292 projectProperties.put("inscriptionStatus", inscriptionStatus.toString()); 293 UserIdentity userIdentity = _currentUserProvider.getUser(); 294 if (!hasAccess && !inscriptionStatus.equals(InscriptionStatus.PRIVATE) && userIdentity != null) 295 { 296 String siteName = Iterables.getFirst(_projectManager.getProjectNames(project), null); 297 boolean inPopulations = siteName != null && (_populationContextHelper.getUserPopulationsOnContext("/sites/" + siteName, false).contains(userIdentity.getPopulationId()) 298 || _populationContextHelper.getUserPopulationsOnContext("/sites-fo/" + siteName, false).contains(userIdentity.getPopulationId())); 299 projectProperties.put("inPopulations", inPopulations); 300 } 301 302 return projectProperties; 303 } 304 else if (treeNode instanceof Map) 305 { 306 Map categoryTreeNode = (Map) treeNode; 307 if (categoryTreeNode.containsKey("category")) 308 { 309 ProjectCategory projectCategory = (ProjectCategory) categoryTreeNode.get("category"); 310 Map<String, Object> categoryProperties = _projectManager.getCategoryProperties(projectCategory); 311 boolean hasChildren = projectCategory.getChildren() 312 .stream() 313 .filter(node -> (includeProjects || node instanceof ProjectCategory) 314 && (filterCategories == null 315 || filterCategories.contains(projectCategory.getId()) 316 || _projectManager.isCategoryInFilters(node.getId(), filterCategories, includeProjects))) 317 .count() > 0; 318 categoryProperties.put("hasChild", hasChildren); 319 320 return categoryProperties; 321 } 322 } 323 324 return null; 325 } 326 327 /** 328 * Retrieve the data required to create a new project 329 * @param zoneItemId The zone item of the project catalogue 330 * @return The new project data 331 */ 332 @Callable 333 public Map<String, Object> getNewProjectData(String zoneItemId) 334 { 335 return getProjectData(null, zoneItemId); 336 } 337 338 /** 339 * Retrieve the data of a project to edit, or the data for a new project 340 * @param projectId The project id. Can be null. 341 * @param zoneItemId The zone item of the project catalog 342 * @return The project data 343 */ 344 @Callable 345 public Map<String, Object> getProjectData(String projectId, String zoneItemId) 346 { 347 Map<String, Object> result = new HashMap<>(); 348 349 ZoneItem zoneItem = _resolver.resolveById(zoneItemId); 350 ModelAwareDataHolder serviceParameters = zoneItem.getServiceParameters(); 351 352 String[] availableLanguages = serviceParameters.getValue("availableLanguages", false, new String[0]); 353 List languages = Arrays.stream(availableLanguages).map(code -> 354 { 355 Map<String, Object> data = new HashMap<>(); 356 data.put("id", code); 357 data.put("label", _languagesManager.getLanguage(code).getLabel()); 358 return data; 359 }).collect(Collectors.toList()); 360 result.put("availableLanguages", languages); 361 362 result.putAll(_projectRightHelper.getProjectRightsData(null)); 363 364 boolean memberOnly = serviceParameters.getValue("memberOnly", false, true); 365 result.put("categories", _projectManager.getProjectTree(null, 0, false, memberOnly, null)); 366 367 result.put("project", _getProjectParameters(projectId, serviceParameters)); 368 369 return result; 370 } 371 372 /** 373 * Retrieves the parameters of the given project 374 * @param projectId Identifier of the project 375 * @param serviceDataHolder data holder of the project catalog service 376 * @return a Map containing the parameters of the project 377 */ 378 protected Map<String, Object> _getProjectParameters(String projectId, ModelAwareDataHolder serviceDataHolder) 379 { 380 Map<String, Object> projectParameters = new HashMap<>(); 381 382 if (projectId != null) 383 { 384 Project project = _resolver.resolveById(projectId); 385 386 Collection<Site> sites = project.getSites(); 387 if (sites.size() > 0) 388 { 389 Site site = sites.iterator().next(); 390 391 String titlePrefix = serviceDataHolder.getValue("titlePrefix", false, ""); 392 String urlPrefix = serviceDataHolder.getValue("urlPrefix", false, ""); 393 394 String title = site.getTitle(); 395 String url = site.getUrl(); 396 397 if (title != null) 398 { 399 projectParameters.put("title", title.startsWith(titlePrefix) ? title.substring(titlePrefix.length()) : title); 400 } 401 if (url != null) 402 { 403 projectParameters.put("url", url.startsWith(urlPrefix) ? url.substring(urlPrefix.length()) : url); 404 } 405 projectParameters.put("languages", site.getSitemaps().stream().map(Sitemap::getName).collect(Collectors.toList())); 406 407 AmetysObject parent = project.getParent(); 408 if (parent instanceof ProjectCategory) 409 { 410 projectParameters.put("category", parent.getId()); 411 } 412 413 projectParameters.put("description", project.getDescription()); 414 if (site.getIllustration() != null) 415 { 416 String illustrationURI = ResolveURIComponent.resolveBoundedImage("site-metadata", site.getName() + ";illustration", 120, 80, false, true); 417 projectParameters.put("illustration", illustrationURI); 418 } 419 if (project.getCoverImage() != null) 420 { 421 String coverImageURI = ResolveURIComponent.resolveBoundedImage("project-metadata", project.getName() + ";coverImage", 120, 550, false, true); 422 projectParameters.put("coverImage", coverImageURI); 423 } 424 projectParameters.put("emailList", project.getMailingList()); 425 projectParameters.put("inscriptionStatus", project.getInscriptionStatus().toString()); 426 projectParameters.put("defaultProfile", project.getDefaultProfile()); 427 UserIdentity[] managers = project.getManagers(); 428 projectParameters.put("projectManagers", Arrays.stream(managers) 429 .map(UserIdentity::userIdentityToString) 430 .collect(Collectors.toList())); 431 if (managers.length > 0) 432 { 433 // get first manager profile as default suggested profile affected to managers 434 Set<String> managerProfiles = _profileAssignmentStorageExtensionPoint.getAllowedProfilesForUser(project, managers[0]); 435 managerProfiles.remove(RightManager.READER_PROFILE_ID); 436 projectParameters.put("profile", managerProfiles.size() > 0 ? managerProfiles.iterator().next() : null); 437 } 438 439 projectParameters.put("emailSender", site.getValue("site-mail-from")); 440 projectParameters.put("modules", _moduleManagerEP.getModules().stream() 441 .collect(Collectors.toMap(module -> module.getId(), module -> _projectManager.isModuleActivated(project, module.getId())))); 442 443 } 444 } 445 446 return projectParameters; 447 } 448 449 /** 450 * Create a new project 451 * @param formData The project data 452 * @param zoneItemId The zone item of the project catalogue 453 * @return The result 454 * @throws IllegalAccessException If an error occurred 455 * @throws IOException If an error occurred 456 * @throws AmetysRepositoryException If an error occurred 457 */ 458 @Callable 459 public Map<String, Object> createProject(Map<String, Object> formData, String zoneItemId) throws IllegalAccessException, AmetysRepositoryException, IOException 460 { 461 return createProject(null, null, formData, zoneItemId); 462 } 463 464 465 /** 466 * Create a new project 467 * @param image An image, can be either the illustration or the coverImage, which can be identify through formData 468 * @param formData The project data 469 * @param zoneItemId The zone item of the project catalogue 470 * @return The result 471 * @throws IllegalAccessException If an error occurred 472 * @throws IOException If an error occurred 473 * @throws AmetysRepositoryException If an error occurred 474 */ 475 @Callable 476 public Map<String, Object> createProject(Part image, Map<String, Object> formData, String zoneItemId) throws IllegalAccessException, AmetysRepositoryException, IOException 477 { 478 if (image != null) 479 { 480 if ((boolean) formData.getOrDefault("illustrationUpdated", false)) 481 { 482 return createProject(image, null, formData, zoneItemId); 483 } 484 else if ((boolean) formData.getOrDefault("coverImageUpdated", false)) 485 { 486 return createProject(null, image, formData, zoneItemId); 487 } 488 } 489 return createProject(null, null, formData, zoneItemId); 490 } 491 492 /** 493 * Create a new project 494 * @param illustration The project illustration file. Can be null 495 * @param coverImage The cover image. Can be null 496 * @param formData The project data 497 * @param zoneItemId The zone item of the project catalogue 498 * @return The result 499 * @throws IllegalAccessException If an error occurred 500 * @throws IOException If an error occurred 501 * @throws AmetysRepositoryException If an error occurred 502 */ 503 @Callable 504 public Map<String, Object> createProject(Part illustration, Part coverImage, Map<String, Object> formData, String zoneItemId) throws IllegalAccessException, AmetysRepositoryException, IOException 505 { 506 Map<String, Object> result = new HashMap<>(); 507 508 if (_rightManager.currentUserHasRight(__RIGHT_PROJECT_CREATE, null) != RightResult.RIGHT_ALLOW 509 && _rightManager.currentUserHasRight(__RIGHT_PROJECT_FO_CREATE, null) != RightResult.RIGHT_ALLOW) 510 { 511 throw new IllegalAccessException("User '" + _currentUserProvider.getUser() + "' tried to create a project without sufficient rights"); 512 } 513 514 _validateImageUpload("Unable to create the project", illustration, coverImage); 515 516 ZoneItem zoneItem = _resolver.resolveById(zoneItemId); 517 ModelAwareDataHolder serviceDataHolder = zoneItem.getServiceParameters(); 518 519 Map<String, Object> siteConfiguration = _getNewSiteConfiguration(formData, serviceDataHolder); 520 521 String projectCategory = (String) formData.getOrDefault("category", null); 522 523 List<String> errors = new ArrayList<>(); 524 Project project = _createProject(projectCategory, formData, serviceDataHolder, siteConfiguration, errors); 525 526 if (project != null) 527 { 528 _assignPopulationsAndManager(project, formData); 529 _setProjectIllustration(project, formData, illustration); 530 _setProjectCoverImage(project, formData, coverImage); 531 project.saveChanges(); 532 } 533 534 result.put("success", errors.size() == 0); 535 if (errors.size() > 0) 536 { 537 result.put("error", errors); 538 } 539 540 return result; 541 } 542 543 private void _validateImageUpload(String error, Part illustration, Part coverImage) 544 { 545 if (illustration != null && !(illustration instanceof PartOnDisk && illustration.getMimeType() != null && illustration.getMimeType().startsWith("image/"))) 546 { 547 throw new IllegalArgumentException(error + ", upload failed or invalid upload type '" + illustration.getMimeType() + "' for the illustration"); 548 } 549 550 if (coverImage != null && !(coverImage instanceof PartOnDisk && coverImage.getMimeType() != null && coverImage.getMimeType().startsWith("image/"))) 551 { 552 throw new IllegalArgumentException(error + ", upload failed or invalid upload type '" + coverImage.getMimeType() + "' for the cover image"); 553 } 554 } 555 556 557 private Map<String, Object> _getNewSiteConfiguration(Map<String, Object> formData, ModelAwareDataHolder serviceDataHolder) 558 { 559 Map<String, Object> siteConfiguration = new HashMap<>(); 560 561 try 562 { 563 Source src = _sourceResolver.resolveURI("context://WEB-INF/param/workspace-default-config.xml"); 564 565 if (src.exists()) 566 { 567 try (InputStream is = src.getInputStream()) 568 { 569 if (getLogger().isDebugEnabled()) 570 { 571 getLogger().debug("ProjectsCatalogueManager : WEB-INF/param/workspace-default-config.xml imported"); 572 } 573 574 Configuration workspaceDefaultConfig = new DefaultConfigurationBuilder().build(is); 575 for (Configuration configuration : workspaceDefaultConfig.getChildren()) 576 { 577 siteConfiguration.put(configuration.getName(), configuration.getValue()); 578 } 579 } 580 } 581 } 582 catch (IOException | ConfigurationException | SAXException e) 583 { 584 // Config does not exist, ignore 585 } 586 587 siteConfiguration.put("skin", serviceDataHolder.getValue("skin")); 588 siteConfiguration.put("force-accept-cookies", serviceDataHolder.getValue("force_accept_cookies")); 589 siteConfiguration.put("display-restricted-pages", serviceDataHolder.getValue("display_restricted_pages")); 590 siteConfiguration.put("ping_activated", serviceDataHolder.getValue("ping_activated")); 591 siteConfiguration.put("site-mail-from", formData.getOrDefault("emailSender", null)); 592 593 return siteConfiguration; 594 } 595 596 @SuppressWarnings("unchecked") 597 private Project _createProject(String parentId, Map<String, Object> formData, ModelAwareDataHolder serviceDataHolder, Map<String, Object> siteConfiguration, List<String> errors) 598 { 599 String title = serviceDataHolder.getValue("titlePrefix", false, "") + formData.get("title"); 600 String url = serviceDataHolder.getValue("urlPrefix", false, "") + formData.get("url"); 601 602 if (url.endsWith("/")) 603 { 604 url = StringUtils.substring(url, 0, -1); 605 } 606 String name = StringUtils.substring(url, url.lastIndexOf('/') + 1); 607 if (name.contains(":")) 608 { 609 name = StringUtils.substring(name, 0, name.indexOf(":")); 610 } 611 612 String[] availableLanguages = serviceDataHolder.getValue("availableLanguages"); 613 List<String> languages = ((List<String>) formData.get("languages")).stream() 614 .filter(lang -> ArrayUtils.contains(availableLanguages, lang)) 615 .collect(Collectors.toList()); 616 617 if (languages.size() == 0) 618 { 619 throw new IllegalArgumentException("Error while creating a new workspace : invalid argument languages"); 620 } 621 siteConfiguration.put("lang", languages); 622 siteConfiguration.put("title", title); 623 siteConfiguration.put("url", url); 624 625 Map<String, Boolean> modules = (Map<String, Boolean>) formData.getOrDefault("modules", null); 626 Set<String> modulesToActivate = modules != null ? modules.entrySet().stream() 627 .filter(e -> e.getValue()) 628 .map(e -> e.getKey()) 629 .collect(Collectors.toSet()) : null; 630 631 // Filter formData values to transmit, to prevent form injection with invalid keys 632 Predicate<String> isFormKeyAllowed = key -> ArrayUtils.contains(getAllowedFormData(), key); 633 634 Map<String, Object> filteredFormData = new HashMap<>(); 635 formData.keySet().stream() 636 .filter(isFormKeyAllowed) 637 .forEach(key -> filteredFormData.put(key, formData.get(key))); 638 639 String projectId = _projectManager.createProject(parentId, name, title, filteredFormData, modulesToActivate, errors); 640 641 if (errors.size() == 0) 642 { 643 Project project = (Project) _resolver.resolveById(projectId); 644 645 return _postProjectCreation(project, filteredFormData, serviceDataHolder, siteConfiguration, errors); 646 } 647 648 return null; 649 } 650 651 /** 652 * some post action when the project is created in the repo 653 * @param project the project 654 * @param formData the forms data 655 * @param serviceDataHolder service data holder 656 * @param siteConfiguration site configuration 657 * @param errors list of errors 658 * @return the project 659 */ 660 protected Project _postProjectCreation(Project project, Map<String, Object> formData, ModelAwareDataHolder serviceDataHolder, Map<String, Object> siteConfiguration, List<String> errors) 661 { 662 for (Site site : project.getSites()) 663 { 664 try 665 { 666 // Add default values for missing values 667 _siteConfigurationEP.getParameters(site.getName()).values().stream() 668 .filter(param -> param.getDefaultValue() != null && !siteConfiguration.containsKey(param.getName())) 669 .forEach(param -> siteConfiguration.put(param.getName(), param.getType().valueToJSONForClient(param.getDefaultValue()))); 670 671 _siteDAO.configureSite(site.getName(), siteConfiguration); 672 } 673 catch (Exception e) 674 { 675 errors.add("invalid-site-configuration"); 676 getLogger().error("Unable to configure the site when creating a new project", e); 677 } 678 } 679 680 return project; 681 } 682 683 private void _assignPopulationsAndManager(Project project, Map<String, Object> formData) 684 { 685 Request request = ContextHelper.getRequest(_context); 686 String siteName = (String) request.getAttribute("siteName"); 687 688 Set<String> populations = _populationContextHelper.getUserPopulationsOnContext("/sites/" + siteName, false); 689 Set<String> frontPopulations = _populationContextHelper.getUserPopulationsOnContext("/sites-fo/" + siteName, false); 690 691 Set<String> groupDirectories = _groupDirectoryContextHelper.getGroupDirectoriesOnContext("/sites/" + siteName); 692 Set<String> frontGroupDirectories = _groupDirectoryContextHelper.getGroupDirectoriesOnContext("/sites-fo/" + siteName); 693 694 for (Site site : project.getSites()) 695 { 696 _populationContextHelper.link("/sites/" + site.getName(), populations); 697 _populationContextHelper.link("/sites-fo/" + site.getName(), frontPopulations); 698 _groupDirectoryContextHelper.link("/sites/" + site.getName(), new ArrayList<>(groupDirectories)); 699 _groupDirectoryContextHelper.link("/sites-fo/" + site.getName(), new ArrayList<>(frontGroupDirectories)); 700 } 701 702 @SuppressWarnings("unchecked") 703 List<String> projectManagers = (List<String>) formData.get("projectManagers"); 704 705 Predicate<UserIdentity> isInPopulations = user -> populations.contains(user.getPopulationId()) || frontPopulations.contains(user.getPopulationId()); 706 707 List<UserIdentity> managers = projectManagers.stream() 708 .map(UserIdentity::stringToUserIdentity) 709 .filter(Objects::nonNull) 710 .filter(isInPopulations) 711 .collect(Collectors.toList()); 712 713 String profile = (String) formData.get("profile"); 714 _projectMemberManager.setProjectManager(project.getName(), profile, managers); 715 } 716 717 718 /** 719 * Edit a project 720 * @param projectId the project id 721 * @param formData The project data 722 * @param zoneItemId The zone item of the project catalogue 723 * @return The result 724 * @throws IllegalAccessException If an error occurred 725 * @throws RepositoryException If an error occurred 726 * @throws AmetysRepositoryException If an error occurred 727 * @throws IOException If an error occurred 728 */ 729 @Callable 730 public Map<String, Object> editProject(String projectId, Map<String, Object> formData, String zoneItemId) throws IllegalAccessException, AmetysRepositoryException, RepositoryException, IOException 731 { 732 return editProject(null, null, projectId, formData, zoneItemId); 733 } 734 735 736 /** 737 * Edit a project 738 * @param image An image, can be either the illustration or the coverImage, which can be identify through formData 739 * @param projectId the project id 740 * @param formData The project data 741 * @param zoneItemId The zone item of the project catalogue 742 * @return The result 743 * @throws IllegalAccessException If an error occurred 744 * @throws RepositoryException If an error occurred 745 * @throws AmetysRepositoryException If an error occurred 746 * @throws IOException If an error occurred 747 */ 748 @Callable 749 public Map<String, Object> editProject(Part image, String projectId, Map<String, Object> formData, String zoneItemId) throws IllegalAccessException, AmetysRepositoryException, RepositoryException, IOException 750 { 751 if (image != null) 752 { 753 if ((boolean) formData.getOrDefault("illustrationUpdated", false)) 754 { 755 return editProject(image, null, projectId, formData, zoneItemId); 756 } 757 else if ((boolean) formData.getOrDefault("coverImageUpdated", false)) 758 { 759 return editProject(null, image, projectId, formData, zoneItemId); 760 } 761 } 762 return editProject(null, null, projectId, formData, zoneItemId); 763 } 764 765 /** 766 * edit a project 767 * @param illustration The project illustration file. Can be null 768 * @param coverImage The cover image. Can be null 769 * @param projectId the project id 770 * @param formData The project data 771 * @param zoneItemId The zone item of the project catalogue 772 * @return The result 773 * @throws IllegalAccessException If an error occurred 774 * @throws RepositoryException If an error occurred 775 * @throws AmetysRepositoryException If an error occurred 776 * @throws IOException If an error occurred 777 */ 778 @Callable 779 public Map<String, Object> editProject(Part illustration, Part coverImage, String projectId, Map<String, Object> formData, String zoneItemId) throws IllegalAccessException, AmetysRepositoryException, RepositoryException, IOException 780 { 781 Map<String, Object> result = new HashMap<>(); 782 List<String> errors = new ArrayList<>(); 783 784 Project project = _resolver.resolveById(projectId); 785 786 if (project == null) 787 { 788 throw new IllegalArgumentException("Unable to edit a project, invalid project id received '" + projectId + "'"); 789 } 790 791 _validateImageUpload("Unable to edit a project", illustration, coverImage); 792 793 if (_rightManager.currentUserHasRight(__RIGHT_PROJECT_EDIT, null) != RightResult.RIGHT_ALLOW 794 && _rightManager.currentUserHasRight(__RIGHT_PROJECT_FO_EDIT, project) != RightResult.RIGHT_ALLOW) 795 { 796 throw new IllegalAccessException("User '" + _currentUserProvider.getUser() + "' tried to edit the project '" + projectId + "' without sufficient rights"); 797 } 798 799 ZoneItem zoneItem = _resolver.resolveById(zoneItemId); 800 ModelAwareDataHolder serviceDataHolder = zoneItem.getServiceParameters(); 801 802 // update category first, as the move needs to have no pending changes, for the live to be successfully updated 803 _updateCategory(project, formData); 804 805 _updateProject(project, formData, serviceDataHolder); 806 _updateSiteConfiguration(project, formData, serviceDataHolder, errors); 807 _updateModules(project, formData); 808 _setProjectIllustration(project, formData, illustration); 809 _setProjectCoverImage(project, formData, coverImage); 810 811 if (project.needsSave()) 812 { 813 project.saveChanges(); 814 } 815 816 // Notify observers 817 Map<String, Object> eventParams = new HashMap<>(); 818 eventParams.put(org.ametys.plugins.workspaces.ObservationConstants.ARGS_PROJECT, project); 819 _observationManager.notify(new Event(org.ametys.plugins.workspaces.ObservationConstants.EVENT_PROJECT_UPDATED, _currentUserProvider.getUser(), eventParams)); 820 821 result.put("success", errors.size() == 0); 822 if (errors.size() > 0) 823 { 824 result.put("error", errors); 825 } 826 827 return result; 828 } 829 830 /** 831 * Update a project 832 * @param project the project to update 833 * @param formData form data 834 * @param serviceDataHolder service parameters 835 */ 836 protected void _updateProject(Project project, Map<String, Object> formData, ModelAwareDataHolder serviceDataHolder) 837 { 838 String title = serviceDataHolder.getValue("titlePrefix", false, "") + formData.get("title"); 839 String description = (String) formData.getOrDefault("description", null); 840 String mailingList = (String) formData.getOrDefault("emailList", null); 841 String inscriptionStatus = (String) formData.getOrDefault("inscriptionStatus", null); 842 String defaultProfile = (String) formData.getOrDefault("defaultProfile", null); 843 844 if (!title.equals(project.getTitle())) 845 { 846 project.setTitle(title); 847 } 848 849 String projectDescription = project.getDescription(); 850 if (projectDescription != null ? !projectDescription.equals(description) : description != null) 851 { 852 project.setDescription(description); 853 } 854 855 String projectMailingList = project.getMailingList(); 856 if (projectMailingList != null ? !projectMailingList.equals(mailingList) : mailingList != null) 857 { 858 project.setMailingList(mailingList); 859 } 860 861 String projectInscriptionStatus = project.getInscriptionStatus().toString(); 862 if (projectInscriptionStatus != null ? !projectInscriptionStatus.equals(inscriptionStatus) : inscriptionStatus != null) 863 { 864 project.setInscriptionStatus(inscriptionStatus); 865 } 866 867 String projectDefaultProfile = project.getDefaultProfile(); 868 if (projectDefaultProfile != null ? !projectDefaultProfile.equals(defaultProfile) : defaultProfile != null) 869 { 870 project.setDefaultProfile(defaultProfile); 871 } 872 } 873 874 @SuppressWarnings("unchecked") 875 private void _updateSiteConfiguration(Project project, Map<String, Object> formData, ModelAwareDataHolder serviceDataHolder, List<String> errors) 876 { 877 String title = serviceDataHolder.getValue("titlePrefix", false, "") + formData.get("title"); 878 String url = serviceDataHolder.getValue("urlPrefix", false, "") + formData.get("url"); 879 880 String[] availableLanguages = serviceDataHolder.getValue("availableLanguages"); 881 List<String> languages = ((List<String>) formData.get("languages")).stream() 882 .filter(lang -> ArrayUtils.contains(availableLanguages, lang)) 883 .collect(Collectors.toList()); 884 885 if (languages.size() == 0) 886 { 887 throw new IllegalArgumentException("Error while creating a new workspace : invalid argument languages"); 888 } 889 for (Site site : project.getSites()) 890 { 891 Map<String, Object> siteConfiguration = _getSiteConfiguration(site); 892 siteConfiguration.put("lang", languages); 893 siteConfiguration.put("title", title); 894 siteConfiguration.put("url", url); 895 siteConfiguration.put("site-mail-from", formData.get("emailSender")); 896 897 try 898 { 899 _siteDAO.configureSite(site.getName(), siteConfiguration); 900 } 901 catch (Exception e) 902 { 903 errors.add("invalid-site-configuration"); 904 getLogger().error("Unable to configure the site when creating a new project", e); 905 } 906 907 if (site.needsSave()) 908 { 909 site.saveChanges(); 910 } 911 } 912 } 913 914 private Map<String, Object> _getSiteConfiguration(Site site) 915 { 916 Map<String, Object> values = new HashMap<>(); 917 String siteName = site.getName(); 918 Map<String, SiteParameter> parameters = _siteConfigurationEP.getParameters(siteName); 919 for (String name : parameters.keySet()) 920 { 921 Object siteParameterValue = site.getValue(name); 922 if (siteParameterValue != null) 923 { 924 values.put(name, siteParameterValue); 925 } 926 } 927 928 // Add default values for missing values 929 parameters.values().stream() 930 .filter(param -> param.getDefaultValue() != null && !values.containsKey(param.getName())) 931 .forEach(param -> values.put(param.getName(), param.getType().valueToJSONForClient(param.getDefaultValue()))); 932 933 return values; 934 } 935 936 private void _updateCategory(Project project, Map<String, Object> formData) throws AmetysRepositoryException, RepositoryException 937 { 938 String projectCategory = (String) formData.getOrDefault("category", null); 939 ModifiableTraversableAmetysObject newParent; 940 if (projectCategory == null) 941 { 942 newParent = _projectManager.getProjectsRoot(); 943 } 944 else 945 { 946 newParent = _resolver.resolveById(projectCategory); 947 } 948 949 ModifiableAmetysObject oldParent = project.getParent(); 950 if (!oldParent.equals(newParent)) 951 { 952 String newName = project.getName(); 953 int index = 1; 954 while (newParent.hasChild(newName)) 955 { 956 newName = project.getName() + "-" + ++index; 957 } 958 959 Node projectNode = project.getNode(); 960 Node parentNode = ((SimpleAmetysObject) newParent).getNode(); 961 Session session = projectNode.getSession(); 962 session.move(projectNode.getPath(), parentNode.getPath() + "/" + newName); 963 session.save(); 964 } 965 } 966 967 @SuppressWarnings("unchecked") 968 private void _updateModules(Project project, Map<String, Object> formData) 969 { 970 String[] oldModules = project.getModules(); 971 972 Map<String, Boolean> modules = (Map<String, Boolean>) formData.getOrDefault("modules", null); 973 if (modules != null) 974 { 975 Predicate<Entry<String, Boolean>> isModuleActivated = Entry::getValue; 976 Function<Entry<String, Boolean>, String> getModule = Entry::getKey; 977 978 Set<String> activeModules = modules.entrySet().stream() 979 .filter(isModuleActivated) 980 .map(getModule) 981 .collect(Collectors.toSet()); 982 983 Set<String> inactiveModules = modules.entrySet().stream() 984 .filter(isModuleActivated.negate()) 985 .map(getModule) 986 .collect(Collectors.toSet()); 987 988 _projectManager.activateModules(project, activeModules); 989 _projectManager.deactivateModules(project, inactiveModules); 990 } 991 else 992 { 993 _projectManager.deactivateModules(project, Arrays.stream(oldModules).collect(Collectors.toSet())); 994 } 995 996 String profile = (String) formData.get("profile"); 997 List<String> projectManagers = (List<String>) formData.get("projectManagers"); 998 999 List<UserIdentity> managers = projectManagers.stream() 1000 .map(UserIdentity::stringToUserIdentity) 1001 .filter(Objects::nonNull) 1002 .collect(Collectors.toList()); 1003 1004 _projectMemberManager.setProjectManager(project.getName(), profile, managers); 1005 } 1006 1007 1008 private void _setProjectIllustration(Project project, Map<String, Object> formData, Part illustration) throws AmetysRepositoryException, IOException 1009 { 1010 if ((Boolean) formData.getOrDefault("illustrationUpdated", false)) 1011 { 1012 for (Site site : project.getSites()) 1013 { 1014 if (illustration == null) 1015 { 1016 site.setIllustration(null, null, null, null); 1017 } 1018 else 1019 { 1020 try (InputStream is = illustration.getInputStream()) 1021 { 1022 site.setIllustration(is, illustration.getMimeType(), illustration.getFileName(), new Date()); 1023 } 1024 } 1025 1026 if (site.needsSave()) 1027 { 1028 site.saveChanges(); 1029 1030 Map<String, Object> eventParams = new HashMap<>(); 1031 eventParams.put(ObservationConstants.ARGS_SITE, site); 1032 _observationManager.notify(new Event(ObservationConstants.EVENT_SITE_UPDATED, _currentUserProvider.getUser(), eventParams)); 1033 } 1034 } 1035 } 1036 } 1037 1038 1039 private void _setProjectCoverImage(Project project, Map<String, Object> formData, Part coverImage) throws AmetysRepositoryException, IOException 1040 { 1041 if ((Boolean) formData.getOrDefault("coverImageUpdated", false)) 1042 { 1043 if (coverImage == null) 1044 { 1045 project.setCoverImage(null, null, null, null); 1046 } 1047 else 1048 { 1049 try (InputStream is = coverImage.getInputStream()) 1050 { 1051 project.setCoverImage(is, coverImage.getMimeType(), coverImage.getFileName(), new Date()); 1052 } 1053 } 1054 } 1055 } 1056 1057 /** 1058 * Delete a project 1059 * @param projectId The project id 1060 * @return The result 1061 * @throws IllegalAccessException if an error occurred 1062 */ 1063 @Callable 1064 public Map<String, Object> deleteProject(String projectId) throws IllegalAccessException 1065 { 1066 Project project = _resolver.resolveById(projectId); 1067 1068 if (project == null) 1069 { 1070 throw new IllegalArgumentException("Unable to delete a project, invalid project id received '" + projectId + "'"); 1071 } 1072 1073 if (_rightManager.currentUserHasRight(__RIGHT_PROJECT_DELETE, null) != RightResult.RIGHT_ALLOW 1074 && _rightManager.currentUserHasRight(__RIGHT_PROJECT_FO_DELETE, project) != RightResult.RIGHT_ALLOW) 1075 { 1076 throw new IllegalAccessException("User '" + _currentUserProvider.getUser() + "' tried to delete the project '" + projectId + "' without sufficient rights"); 1077 } 1078 1079 Map<String, Object> result = new HashMap<>(); 1080 result.put("sites", _projectManager.deleteProject(project)); 1081 result.put("success", true); 1082 return result; 1083 } 1084 1085 /** 1086 * Add the current user to the project, if the project's inscriptions are opened 1087 * @param projectId The project id 1088 * @return The result 1089 * @throws MessagingException If an error occurred sending a notification mail to the project manager 1090 */ 1091 @Callable 1092 public Map<String, Object> joinProject(String projectId) throws MessagingException 1093 { 1094 Map<String, Object> result = new HashMap<>(); 1095 Project project = _resolver.resolveById(projectId); 1096 1097 if (project == null) 1098 { 1099 throw new IllegalArgumentException("Unable to join a project, invalid project id received '" + projectId + "'"); 1100 } 1101 1102 UserIdentity currentUser = _currentUserProvider.getUser(); 1103 1104 Optional<Site> projectSite = project.getSites().stream().filter(site -> StringUtils.isNotEmpty(site.getUrl())).findFirst(); 1105 String siteName = projectSite.isPresent() ? projectSite.get().getName() : null; 1106 if (siteName == null || (!_populationContextHelper.getUserPopulationsOnContext("/sites/" + siteName, false).contains(currentUser.getPopulationId()) 1107 && !_populationContextHelper.getUserPopulationsOnContext("/sites-fo/" + siteName, false).contains(currentUser.getPopulationId()))) 1108 { 1109 // User is not in the project site populations, cannot be added 1110 result.put("success", false); 1111 return result; 1112 } 1113 1114 1115 boolean success = _projectMemberManager.addProjectMember(project, currentUser); 1116 1117 result.put("success", success); 1118 if (success) 1119 { 1120 String url = null; 1121 if (projectSite.isPresent()) 1122 { 1123 url = projectSite.get().getUrl(); 1124 result.put("url", url); 1125 } 1126 1127 String mailFrom = Config.getInstance().getValue("smtp.mail.from"); 1128 1129 List<String> managersEmails = Arrays.stream(project.getManagers()) 1130 .map(manager -> _userManager.getUser(manager)) 1131 .filter(Objects::nonNull) 1132 .map(User::getEmail) 1133 .filter(StringUtils::isNotEmpty) 1134 .collect(Collectors.toList()); 1135 1136 if (managersEmails.size() > 0 && mailFrom != null) 1137 { 1138 Map<String, I18nizableText> params = new HashMap<>(); 1139 User current = _userManager.getUser(currentUser); 1140 params.put("user", new I18nizableText(current != null ? current.getFullName() : currentUser.getLogin())); 1141 params.put("project", new I18nizableText(project.getTitle())); 1142 params.put("url", new I18nizableText(url != null ? url : "")); 1143 String subject = _i18nUtils.translate(new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_SERVICE_PROJECTS_CATALOGUE_PROJECT_JOIN_MAIL_TITLE", params)); 1144 String textBody = _i18nUtils.translate(new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_SERVICE_PROJECTS_CATALOGUE_PROJECT_JOIN_MAIL_BODY", params)); 1145 1146 for (String managerMail : managersEmails) 1147 { 1148 SendMailHelper.sendMail(subject, null, textBody, managerMail, mailFrom); 1149 } 1150 } 1151 } 1152 return result; 1153 } 1154 1155 /** 1156 * Send a demand to join a project to the project's manager, if the project's inscriptions are moderated 1157 * @param projectId The project to join 1158 * @param message A message to send to the project's manager. 1159 * @return The result 1160 * @throws MessagingException If an error occurred sending the email to the project's manager 1161 */ 1162 @Callable 1163 public Map<String, Object> askToJoinProject(String projectId, String message) throws MessagingException 1164 { 1165 Map<String, Object> result = new HashMap<>(); 1166 1167 Project project = _resolver.resolveById(projectId); 1168 1169 if (project == null) 1170 { 1171 throw new IllegalArgumentException("Unable to join a project, invalid project id received '" + projectId + "'"); 1172 } 1173 1174 UserIdentity currentUser = _currentUserProvider.getUser(); 1175 1176 String siteName = Iterables.getFirst(_projectManager.getProjectNames(project), null); 1177 if (siteName == null || (!_populationContextHelper.getUserPopulationsOnContext("/sites/" + siteName, false).contains(currentUser.getPopulationId()) 1178 && !_populationContextHelper.getUserPopulationsOnContext("/sites-fo/" + siteName, false).contains(currentUser.getPopulationId()))) 1179 { 1180 // User is not in the project site populations, cannot be added 1181 result.put("success", false); 1182 return result; 1183 } 1184 1185 _sendAskToJoinMail(message, project, currentUser); 1186 result.put("success", true); 1187 result.put("added-notification", Config.getInstance().getValue("workspaces.member.added.send.notification")); 1188 return result; 1189 } 1190 1191 private void _sendAskToJoinMail(String message, Project project, UserIdentity joiningUser) throws MessagingException 1192 { 1193 String url = getAddUserUrl(project, joiningUser); 1194 1195 String mailFrom = Config.getInstance().getValue("smtp.mail.from"); 1196 1197 List<String> managersEmails = Arrays.stream(project.getManagers()) 1198 .map(manager -> _userManager.getUser(manager)) 1199 .filter(Objects::nonNull) 1200 .map(User::getEmail) 1201 .filter(StringUtils::isNotEmpty) 1202 .collect(Collectors.toList()); 1203 1204 if (managersEmails.size() > 0 && mailFrom != null) 1205 { 1206 String escapedMessage = StringUtils.isEmpty(message) ? null : message.replaceAll("<", "<").replaceAll(">", ">").replaceAll("\n", "<br/>"); 1207 1208 Map<String, I18nizableText> params = new HashMap<>(); 1209 User current = _userManager.getUser(joiningUser); 1210 params.put("user", new I18nizableText(current != null ? current.getFullName() : joiningUser.getLogin())); 1211 params.put("project", new I18nizableText(project.getTitle())); 1212 params.put("url", new I18nizableText(url)); 1213 params.put("message", new I18nizableText(escapedMessage != null ? escapedMessage : "")); 1214 String subject = _i18nUtils.translate(new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_SERVICE_PROJECTS_CATALOGUE_PROJECT_ASKTOJOIN_MAIL_TITLE", params)); 1215 String htmlBody = _i18nUtils.translate(new I18nizableText("plugin.workspaces", escapedMessage != null ? "PLUGINS_WORKSPACES_SERVICE_PROJECTS_CATALOGUE_PROJECT_ASKTOJOIN_MAIL_BODY" : "PLUGINS_WORKSPACES_SERVICE_PROJECTS_CATALOGUE_PROJECT_ASKTOJOIN_MAIL_BODY_EMPTY" , params)); 1216 1217 for (String managerMail : managersEmails) 1218 { 1219 SendMailHelper.sendMail(subject, htmlBody, null, managerMail, mailFrom); 1220 } 1221 } 1222 } 1223 1224 /** 1225 * Get the absolute url to add a user to a project 1226 * @param project The project 1227 * @param user the identity of user to add 1228 * @return the absolute page url 1229 */ 1230 protected String getAddUserUrl(Project project, UserIdentity user) 1231 { 1232 AmetysObjectIterable<Page> projectDashboardPages = _projectManager.getProjectDashboardPage(project, null); 1233 if (projectDashboardPages.getSize() > 0) 1234 { 1235 return ResolveURIComponent.resolve("page", projectDashboardPages.iterator().next().getId(), false, true) + "?askToJoin=" + URLEncoder.encodeParameter(UserIdentity.userIdentityToString(user)); 1236 } 1237 1238 return ""; 1239 } 1240 1241 /** 1242 * Get the list of allowed data in the form 1243 * @return the list of allowed data in the form 1244 */ 1245 protected String[] getAllowedFormData() 1246 { 1247 return __ALLOWED_FORM_DATA; 1248 } 1249 1250}