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.time.ZonedDateTime; 021import java.util.ArrayList; 022import java.util.Arrays; 023import java.util.Collections; 024import java.util.HashMap; 025import java.util.HashSet; 026import java.util.Iterator; 027import java.util.List; 028import java.util.Map; 029import java.util.Objects; 030import java.util.Set; 031import java.util.function.Predicate; 032import java.util.stream.Collectors; 033import java.util.stream.Stream; 034 035import org.apache.avalon.framework.component.Component; 036import org.apache.avalon.framework.context.Context; 037import org.apache.avalon.framework.context.ContextException; 038import org.apache.avalon.framework.context.Contextualizable; 039import org.apache.avalon.framework.logger.AbstractLogEnabled; 040import org.apache.avalon.framework.service.ServiceException; 041import org.apache.avalon.framework.service.ServiceManager; 042import org.apache.avalon.framework.service.Serviceable; 043import org.apache.cocoon.servlet.multipart.Part; 044import org.apache.cocoon.xml.AttributesImpl; 045import org.apache.cocoon.xml.XMLUtils; 046import org.apache.commons.collections.CollectionUtils; 047import org.apache.commons.lang.StringUtils; 048import org.apache.commons.lang3.tuple.Pair; 049import org.apache.excalibur.source.Source; 050import org.apache.excalibur.source.SourceResolver; 051import org.apache.tika.io.FilenameUtils; 052import org.xml.sax.ContentHandler; 053import org.xml.sax.SAXException; 054 055import org.ametys.cms.FilterNameHelper; 056import org.ametys.cms.languages.LanguagesManager; 057import org.ametys.cms.transformation.xslt.ResolveURIComponent; 058import org.ametys.core.group.GroupDirectoryContextHelper; 059import org.ametys.core.observation.Event; 060import org.ametys.core.observation.ObservationManager; 061import org.ametys.core.right.ProfileAssignmentStorageExtensionPoint; 062import org.ametys.core.right.RightManager; 063import org.ametys.core.right.RightManager.RightResult; 064import org.ametys.core.ui.Callable; 065import org.ametys.core.user.CurrentUserProvider; 066import org.ametys.core.user.User; 067import org.ametys.core.user.UserIdentity; 068import org.ametys.core.user.UserManager; 069import org.ametys.core.user.population.PopulationContextHelper; 070import org.ametys.core.util.I18nUtils; 071import org.ametys.core.util.URIUtils; 072import org.ametys.core.util.mail.SendMailHelper; 073import org.ametys.plugins.core.user.UserHelper; 074import org.ametys.plugins.repository.AmetysObjectResolver; 075import org.ametys.plugins.repository.data.holder.ModelAwareDataHolder; 076import org.ametys.plugins.workspaces.about.AboutWorkspaceModule; 077import org.ametys.plugins.workspaces.alert.AlertWorkspaceModule; 078import org.ametys.plugins.workspaces.categories.Category; 079import org.ametys.plugins.workspaces.categories.CategoryHelper; 080import org.ametys.plugins.workspaces.categories.CategoryProviderExtensionPoint; 081import org.ametys.plugins.workspaces.keywords.KeywordProviderExtensionPoint; 082import org.ametys.plugins.workspaces.keywords.KeywordsDAO; 083import org.ametys.plugins.workspaces.members.MembersWorkspaceModule; 084import org.ametys.plugins.workspaces.members.ProjectMemberManager; 085import org.ametys.plugins.workspaces.members.ProjectMemberManager.ProjectMember; 086import org.ametys.plugins.workspaces.news.NewsWorkspaceModule; 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.rights.ProjectRightHelper; 091import org.ametys.plugins.workspaces.tags.ProjectTagProviderExtensionPoint; 092import org.ametys.runtime.config.Config; 093import org.ametys.runtime.i18n.I18nizableText; 094import org.ametys.runtime.i18n.I18nizableTextParameter; 095import org.ametys.web.ObservationConstants; 096import org.ametys.web.repository.page.Page; 097import org.ametys.web.repository.page.ZoneItem; 098import org.ametys.web.repository.site.Site; 099import org.ametys.web.repository.site.SiteDAO; 100import org.ametys.web.site.SiteConfigurationManager; 101 102import jakarta.mail.MessagingException; 103 104/** 105 * Manager for the Projects Catalogue service 106 */ 107public class ProjectsCatalogueManager extends AbstractLogEnabled implements Serviceable, Component, Contextualizable 108{ 109 /** Avalon Role */ 110 public static final String ROLE = ProjectsCatalogueManager.class.getName(); 111 112 /** The identifier of modules that are always active */ 113 public static final Set<String> DEFAULT_MODULES = Set.of(MembersWorkspaceModule.MEMBERS_MODULE_ID, 114 AboutWorkspaceModule.ABOUT_MODULE_ID, 115 NewsWorkspaceModule.NEWS_MODULE_ID, 116 AlertWorkspaceModule.ALERT_MODULE_ID); 117 118 /** List of allowed field received from the front */ 119 private static final String[] __ALLOWED_FORM_DATA = {"description", "emailList", "inscriptionStatus", "defaultProfile", "tags", "categoryTags", "keywords"}; 120 121 /** Ametys Object Resolver */ 122 protected AmetysObjectResolver _resolver; 123 /** Current user provider */ 124 protected CurrentUserProvider _currentUserProvider; 125 /** The project members' manager */ 126 protected ProjectMemberManager _projectMemberManager; 127 128 /** The right manager */ 129 protected RightManager _rightManager; 130 /** The project manager */ 131 protected ProjectManager _projectManager; 132 /** Helper for project's rights */ 133 protected ProjectRightHelper _projectRightHelper; 134 /** The language manager */ 135 protected LanguagesManager _languagesManager; 136 /** The source resolver */ 137 protected SourceResolver _sourceResolver; 138 /** The site dao */ 139 protected SiteDAO _siteDAO; 140 /** The site's configuration manager */ 141 protected SiteConfigurationManager _siteConfigurationManager; 142 /** Helper for user population */ 143 protected PopulationContextHelper _populationContextHelper; 144 /** The extension point for workspace's modules */ 145 protected WorkspaceModuleExtensionPoint _moduleManagerEP; 146 /** The extension point for profiles' storage */ 147 protected ProfileAssignmentStorageExtensionPoint _profileAssignmentStorageExtensionPoint; 148 /** The user manager */ 149 protected UserManager _userManager; 150 /** Utils for i18n */ 151 protected I18nUtils _i18nUtils; 152 /** The observation manager */ 153 protected ObservationManager _observationManager; 154 /** Helper for group directory's context */ 155 protected GroupDirectoryContextHelper _groupDirectoryContextHelper; 156 /** The extension point for project's tags */ 157 protected ProjectTagProviderExtensionPoint _projectTagProviderEP; 158 /** The extension point for project's categories */ 159 protected CategoryProviderExtensionPoint _categoryProviderEP; 160 /** The extension point for project's keywords */ 161 protected KeywordProviderExtensionPoint _keywordProviderEP; 162 163 /** The avalon context */ 164 protected Context _context; 165 166 private CategoryHelper _categoryHelper; 167 168 private ProjectMemberManager _projectMembers; 169 170 private UserHelper _userHelper; 171 172 private KeywordsDAO _keywordsDAO; 173 174 public void service(ServiceManager manager) throws ServiceException 175 { 176 _userHelper = (UserHelper) manager.lookup(UserHelper.ROLE); 177 _rightManager = (RightManager) manager.lookup(RightManager.ROLE); 178 _projectManager = (ProjectManager) manager.lookup(ProjectManager.ROLE); 179 _projectMemberManager = (ProjectMemberManager) manager.lookup(ProjectMemberManager.ROLE); 180 _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE); 181 _projectRightHelper = (ProjectRightHelper) manager.lookup(ProjectRightHelper.ROLE); 182 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 183 _languagesManager = (LanguagesManager) manager.lookup(LanguagesManager.ROLE); 184 _sourceResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE); 185 _siteConfigurationManager = (SiteConfigurationManager) manager.lookup(SiteConfigurationManager.ROLE); 186 _siteDAO = (SiteDAO) manager.lookup(SiteDAO.ROLE); 187 _populationContextHelper = (PopulationContextHelper) manager.lookup(PopulationContextHelper.ROLE); 188 _moduleManagerEP = (WorkspaceModuleExtensionPoint) manager.lookup(WorkspaceModuleExtensionPoint.ROLE); 189 _profileAssignmentStorageExtensionPoint = (ProfileAssignmentStorageExtensionPoint) manager.lookup(ProfileAssignmentStorageExtensionPoint.ROLE); 190 _userManager = (UserManager) manager.lookup(UserManager.ROLE); 191 _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE); 192 _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE); 193 _groupDirectoryContextHelper = (GroupDirectoryContextHelper) manager.lookup(GroupDirectoryContextHelper.ROLE); 194 _projectTagProviderEP = (ProjectTagProviderExtensionPoint) manager.lookup(ProjectTagProviderExtensionPoint.ROLE); 195 _categoryProviderEP = (CategoryProviderExtensionPoint) manager.lookup(CategoryProviderExtensionPoint.ROLE); 196 _keywordProviderEP = (KeywordProviderExtensionPoint) manager.lookup(KeywordProviderExtensionPoint.ROLE); 197 _categoryHelper = (CategoryHelper) manager.lookup(CategoryHelper.ROLE); 198 _projectMembers = (ProjectMemberManager) manager.lookup(ProjectMemberManager.ROLE); 199 _keywordsDAO = (KeywordsDAO) manager.lookup(KeywordsDAO.ROLE); 200 } 201 202 public void contextualize(Context context) throws ContextException 203 { 204 _context = context; 205 } 206 207 private Pair<List<String>, List<Map<String, Object>>> _createMissingKeywords(List<Object> keywords) throws IllegalAccessException 208 { 209 List<Map<String, Object>> newKeywordsInfo = Collections.emptyList(); 210 String[] keywordsToCreate = keywords.stream().filter(t -> t instanceof Map).map(t -> (String) ((Map) t).get("text")).toArray(String[]::new); 211 if (keywordsToCreate.length > 0) 212 { 213 // check right 214 if (_rightManager.currentUserHasRight(ProjectConstants.RIGHT_PROJECT_HANDLE_PROJECTKEYWORDS, "/cms") != RightResult.RIGHT_ALLOW) 215 { 216 throw new IllegalAccessException("User " + _currentUserProvider.getUser() + " tried to create a project tag without the convinient rights"); 217 } 218 219 newKeywordsInfo = _keywordsDAO.addTags(keywordsToCreate); 220 } 221 222 Iterator<Map<String, Object>> newKeywordsInfoIterator = newKeywordsInfo.iterator(); 223 List<String> keywordsToSet = keywords.stream().map(t -> t instanceof String ? (String) t : (String) newKeywordsInfoIterator.next().get("name")).collect(Collectors.toList()); 224 return Pair.of(keywordsToSet, newKeywordsInfo); 225 } 226 227 /** 228 * Create a project 229 * @param zoneItemId The id of the zoneitem holding the catalog service 230 * @param title The title 231 * @param description The description (can be empty) 232 * @param illustration The illustration (can be a File or a local path) 233 * @param category The category 234 * @param keywords The project keywords 235 * @param visibility The visibility 236 * @param defaultProfile For public projects, profile for self registered users 237 * @param language The language code 238 * @param managers The managers url 239 * @param modules The selected modules 240 * @return Information about the new project 241 * @throws IllegalAccessException If user has no right to create tags and ask to 242 */ 243 @Callable 244 public Map<String, Object> createProject(String zoneItemId, String title, String description, Object illustration, String category, List<Object> keywords, Integer visibility, String defaultProfile, String language, List<String> managers, List<String> modules) throws IllegalAccessException 245 { 246 InscriptionStatus inscriptionStatus = visibility == 1 247 ? InscriptionStatus.PRIVATE 248 : (visibility == 2 249 ? InscriptionStatus.MODERATED 250 : InscriptionStatus.OPEN); 251 _projectManager.checkRightsForProjectCreation(inscriptionStatus); 252 253 // Get service parameters 254 ZoneItem zoneItem = _resolver.resolveById(zoneItemId); 255 ModelAwareDataHolder serviceDataHolder = zoneItem.getServiceParameters(); 256 String titlePrefix = serviceDataHolder.getValue("titlePrefix", false, ""); 257 String urlPrefix = serviceDataHolder.getValue("urlPrefix", false, ""); 258 String[] availableLanguages = serviceDataHolder.getValue("availableLanguages"); 259 String skin = serviceDataHolder.getValue("skin", false, ""); 260 String forceAcceptCookie = serviceDataHolder.getValue("force_accept_cookies", false, ""); 261 262 Site catalogSite = zoneItem.getZone().getPage().getSite(); 263 264 // Check language 265 if (Arrays.stream(availableLanguages).filter(language::equals).findFirst().isEmpty()) 266 { 267 throw new IllegalArgumentException("Cannot create project with language '" + language + "' since it is not part of the available languages " + availableLanguages); 268 } 269 // Check profile 270 String defaultManagerProfile = Config.getInstance().getValue("workspaces.profile.managerdefault", false, null); 271 if (StringUtils.isBlank(defaultManagerProfile)) 272 { 273 throw new IllegalArgumentException("The general configuration parameter 'workspaces.profile.managerdefault' cannot be empty"); 274 } 275 276 // Create tags if necessary 277 Pair<List<String>, List<Map<String, Object>>> keywordsInfo = _createMissingKeywords(keywords); 278 List<String> keywordsToSet = keywordsInfo.getLeft(); 279 List<Map<String, Object>> newKeywordsInfo = keywordsInfo.getRight(); 280 281 // Create project object in repo 282 Map<String, Object> additionalValues = new HashMap<>(); 283 additionalValues.put("description", StringUtils.defaultString(description)); 284 additionalValues.put("categoryTags", Collections.singletonList(category)); 285 additionalValues.put("inscriptionStatus", inscriptionStatus.toString()); 286 additionalValues.put("defaultProfile", defaultProfile); 287 additionalValues.put("keywords", keywordsToSet); 288 additionalValues.put("language", language); 289 290 List<String> errors = new ArrayList<>(); 291 String prefixedTitle = (titlePrefix + " " + title).trim(); 292 Project project = _projectManager.createProject(_findName(prefixedTitle), 293 prefixedTitle, 294 additionalValues, 295 _withDefaultModules(modules), 296 errors); 297 298 if (!CollectionUtils.isEmpty(errors)) 299 { 300 Map<String, Object> result = new HashMap<>(); 301 result.put("success", false); 302 303 return result; 304 } 305 306 // Add site infos 307 _updateSiteInfos(project, urlPrefix, skin, forceAcceptCookie, catalogSite, illustration, language); 308 309 // Assign populations and manager 310 _assignPopulations(project, catalogSite); 311 _assignManagers(project, managers, defaultManagerProfile); 312 project.saveChanges(); 313 314 Map<String, Object> result = new HashMap<>(); 315 result.put("success", true); 316 result.put("project", _detailedMyProject2json(project)); 317 result.put("keywords", newKeywordsInfo); 318 return result; 319 } 320 321 private void _assignPopulations(Project project, Site catalogSite) 322 { 323 Site site = project.getSite(); 324 325 Set<String> populations = _populationContextHelper.getUserPopulationsOnContext("/sites/" + catalogSite.getName(), false); 326 Set<String> frontPopulations = _populationContextHelper.getUserPopulationsOnContext("/sites-fo/" + catalogSite.getName(), false); 327 Set<String> groupDirectories = _groupDirectoryContextHelper.getGroupDirectoriesOnContext("/sites/" + catalogSite.getName()); 328 Set<String> frontGroupDirectories = _groupDirectoryContextHelper.getGroupDirectoriesOnContext("/sites-fo/" + catalogSite.getName()); 329 330 _populationContextHelper.link("/sites/" + site.getName(), populations); 331 _populationContextHelper.link("/sites-fo/" + site.getName(), frontPopulations); 332 _groupDirectoryContextHelper.link("/sites/" + site.getName(), new ArrayList<>(groupDirectories)); 333 _groupDirectoryContextHelper.link("/sites-fo/" + site.getName(), new ArrayList<>(frontGroupDirectories)); 334 } 335 336 private void _assignManagers(Project project, List<String> managers, String defaultManagerProfile) 337 { 338 Site site = project.getSite(); 339 340 Set<String> populations = _populationContextHelper.getUserPopulationsOnContext("/sites/" + site.getName(), false); 341 Set<String> frontPopulations = _populationContextHelper.getUserPopulationsOnContext("/sites-fo/" + site.getName(), false); 342 343 Predicate<UserIdentity> isInPopulations = user -> populations.contains(user.getPopulationId()) || frontPopulations.contains(user.getPopulationId()); 344 345 List<UserIdentity> projectManagers = managers.stream() 346 .map(UserIdentity::stringToUserIdentity) 347 .filter(Objects::nonNull) 348 .filter(isInPopulations) 349 .collect(Collectors.toList()); 350 351 _projectMemberManager.setProjectManager(project.getName(), defaultManagerProfile, projectManagers); 352 } 353 354 private void _updateSiteInfos(Project project, String urlPrefix, String skin, String forceAcceptCookie, Site catalogSite, Object illustration, String language) 355 { 356 Site site = project.getSite(); 357 358 site.setUrl(urlPrefix + "/" + project.getName()); 359 site.setValue("skin", skin); 360 if (site.hasDefinition("force-accept-cookies")) 361 { 362 site.setValue("force-accept-cookies", forceAcceptCookie); 363 } 364 365 site.setValue("display-restricted-pages", false); 366 site.setValue("ping_activated", false); 367 site.setValue("site-mail-from", catalogSite.getValue("site-mail-from")); 368 site.setValue("site-contents-comments-postvalidation", catalogSite.getValue("site-contents-comments-postvalidation")); 369 site.setValue("color", catalogSite.getValue("color")); 370 371 _setIllustration(site, illustration); 372 373 _siteDAO.setLanguages(site, Collections.singletonList(language)); 374 375 site.saveChanges(); 376 377 Map<String, Object> eventParams = new HashMap<>(); 378 eventParams = new HashMap<>(); 379 eventParams.put(org.ametys.web.ObservationConstants.ARGS_SITE, site); 380 _observationManager.notify(new Event(org.ametys.web.ObservationConstants.EVENT_SITE_UPDATED, _currentUserProvider.getUser(), eventParams)); 381 } 382 383 private void _setIllustration(Site site, Object illustration) 384 { 385 Object illustrationObject = null; 386 try 387 { 388 illustrationObject = _getIllustrationSource(illustration); 389 if (illustrationObject instanceof Source) 390 { 391 Source illustrationSource = (Source) illustrationObject; 392 try (InputStream is = illustrationSource.getInputStream()) 393 { 394 site.setIllustration(is, illustrationSource.getMimeType(), FilenameUtils.getName(illustrationSource.getURI()), ZonedDateTime.now()); 395 } 396 } 397 else if (illustrationObject instanceof Part) 398 { 399 Part illustrationPart = (Part) illustrationObject; 400 try (InputStream is = illustrationPart.getInputStream()) 401 { 402 site.setIllustration(is, illustrationPart.getMimeType(), illustrationPart.getUploadName(), ZonedDateTime.now()); 403 } 404 } 405 } 406 catch (IOException e) 407 { 408 throw new IllegalArgumentException("Cannot not get illustration", e); 409 } 410 finally 411 { 412 if (illustrationObject instanceof Source) 413 { 414 _sourceResolver.release((Source) illustrationObject); 415 } 416 } 417 418 } 419 420 private Object _getIllustrationSource(Object illustration) throws IOException 421 { 422 if (illustration instanceof String) 423 { 424 String illustrationAsString = (String) illustration; 425 if (illustrationAsString.contains("/") || illustrationAsString.contains("\\")) 426 { 427 throw new IllegalArgumentException("Cannot choose an illustration outside the library directory"); 428 } 429 430 return _sourceResolver.resolveURI("plugin:workspaces://resources/img/catalog/library/" + illustrationAsString); 431 } 432 else if (illustration instanceof Part) 433 { 434 Part illustrationAsFile = (Part) illustration; 435 return illustrationAsFile; 436 } 437 else // boolean => unchanged 438 { 439 return null; 440 } 441 } 442 443 private Set<String> _withDefaultModules(List<String> modules) 444 { 445 Set<String> modulesToActivate = new HashSet<>(); 446 447 modulesToActivate.addAll(DEFAULT_MODULES); 448 modulesToActivate.addAll(modules); 449 450 return modulesToActivate; 451 } 452 453 private String _findName(String title) 454 { 455 String originalName = FilterNameHelper.filterName(title); 456 String name = originalName; 457 458 int index = 2; 459 while (_projectManager.hasProject(name)) 460 { 461 name = originalName + "-" + (index++); 462 } 463 464 return name; 465 } 466 467 /** 468 * Edit an existing project 469 * @param projectId The id of the project 470 * @param title New title 471 * @param description New description 472 * @param illustration New illustration 473 * @param category New category 474 * @param keywords The project keywords 475 * @param visibility New visibility 476 * @param defaultProfile New default profile 477 * @param managers New managers 478 * @param modules New modules 479 * @return The success map with project description 480 * @throws IllegalAccessException if user has not the convenient rights 481 */ 482 @Callable 483 public Map<String, Object> editProject(String projectId, String title, String description, Object illustration, String category, List<Object> keywords, Integer visibility, String defaultProfile, List<String> managers, List<String> modules) throws IllegalAccessException 484 { 485 Project project = _resolver.resolveById(projectId); 486 if (project == null) 487 { 488 throw new IllegalArgumentException("Unable to edit a project, invalid project id received '" + projectId + "'"); 489 } 490 491 InscriptionStatus inscriptionStatus = visibility == 1 492 ? InscriptionStatus.PRIVATE 493 : (visibility == 2 494 ? InscriptionStatus.MODERATED 495 : InscriptionStatus.OPEN); 496 _projectManager.checkRightsForProjectEdition(project, inscriptionStatus); 497 498 // Check profile 499 String defaultManagerProfile = Config.getInstance().getValue("workspaces.profile.managerdefault", false, null); 500 if (StringUtils.isBlank(defaultManagerProfile)) 501 { 502 throw new IllegalArgumentException("The general configuration parameter 'workspaces.profile.managerdefault' cannot be empty"); 503 } 504 505 506 if (_rightManager.currentUserHasRight(ProjectConstants.RIGHT_PROJECT_EDIT, project) != RightResult.RIGHT_ALLOW) 507 { 508 throw new IllegalAccessException("User '" + _currentUserProvider.getUser() + "' tried to edit the project '" + projectId + "' without sufficient rights"); 509 } 510 511 // Create keywords if necessary 512 Pair<List<String>, List<Map<String, Object>>> keywordsInfo = _createMissingKeywords(keywords); 513 List<String> keywordsToSet = keywordsInfo.getLeft(); 514 List<Map<String, Object>> newKeywordsInfo = keywordsInfo.getRight(); 515 516 project.setTitle(title); 517 project.setDescription(StringUtils.defaultString(description)); 518 project.setInscriptionStatus(inscriptionStatus.toString()); 519 project.setDefaultProfile(defaultProfile); 520 project.setCategoryTags(Collections.singletonList(category)); 521 project.setKeywords(keywordsToSet.toArray(new String[keywordsToSet.size()])); 522 523 Site site = project.getSite(); 524 525 _projectManager.setProjectSiteTitle(site, project.getTitle()); 526 _setIllustration(site, illustration); 527 if (site.needsSave()) 528 { 529 site.saveChanges(); 530 531 Map<String, Object> eventParams = new HashMap<>(); 532 eventParams.put(ObservationConstants.ARGS_SITE, site); 533 _observationManager.notify(new Event(ObservationConstants.EVENT_SITE_UPDATED, _currentUserProvider.getUser(), eventParams)); 534 } 535 536 _updateModules(project, _withDefaultModules(modules)); 537 _assignManagers(project, managers, defaultManagerProfile); 538 539 if (project.needsSave()) 540 { 541 project.saveChanges(); 542 543 // Notify observers 544 Map<String, Object> eventParams = new HashMap<>(); 545 eventParams.put(org.ametys.plugins.workspaces.ObservationConstants.ARGS_PROJECT, project); 546 _observationManager.notify(new Event(org.ametys.plugins.workspaces.ObservationConstants.EVENT_PROJECT_UPDATED, _currentUserProvider.getUser(), eventParams)); 547 } 548 549 Map<String, Object> result = new HashMap<>(); 550 result.put("success", true); 551 result.put("project", _detailedMyProject2json(project)); 552 result.put("keywords", newKeywordsInfo); 553 return result; 554 } 555 556 private void _updateModules(Project project, Set<String> modules) 557 { 558 Set<String> modulesToActivate = new HashSet<>(modules); 559 Set<String> modulesToDeactivate = new HashSet<>(Arrays.asList(project.getModules())); 560 modulesToActivate.removeAll(new HashSet<>(Arrays.asList(project.getModules()))); 561 modulesToDeactivate.removeAll(modules); 562 563 if (!modulesToActivate.isEmpty()) 564 { 565 Map<String, Object> additionalValues = new HashMap<>(); 566 additionalValues.put("description", project.getDescription()); 567 additionalValues.put("inscriptionStatus", project.getInscriptionStatus()); 568 additionalValues.put("emailList", project.getMailingList()); 569 additionalValues.put("defaultProfile", project.getDefaultProfile()); 570 additionalValues.put("categoryTags", project.getCategories()); 571 additionalValues.put("keywords", project.getKeywords()); 572 573 Site site = project.getSite(); 574 if (site != null) 575 { 576 additionalValues.put("language", site.getSitemaps().iterator().next().getName()); 577 } 578 579 _projectManager.activateModules(project, modulesToActivate, additionalValues); 580 } 581 if (!modulesToDeactivate.isEmpty()) 582 { 583 _projectManager.deactivateModules(project, modulesToDeactivate); 584 } 585 } 586 587 /** 588 * Delete a project 589 * @param projectId The project id 590 * @return The result 591 * @throws IllegalAccessException if an error occurred 592 */ 593 @Callable 594 public Map<String, Object> deleteProject(String projectId) throws IllegalAccessException 595 { 596 Project project = _resolver.resolveById(projectId); 597 598 if (project == null) 599 { 600 throw new IllegalArgumentException("Unable to delete a project, invalid project id received '" + projectId + "'"); 601 } 602 603 if (_rightManager.currentUserHasRight(ProjectConstants.RIGHT_PROJECT_DELETE, project) != RightResult.RIGHT_ALLOW) 604 { 605 throw new IllegalAccessException("User '" + _currentUserProvider.getUser() + "' tried to delete the project '" + projectId + "' without sufficient rights"); 606 } 607 608 Map<String, Object> result = new HashMap<>(); 609 result.put("sites", _projectManager.deleteProject(project)); 610 result.put("success", true); 611 return result; 612 } 613 614 /** 615 * Add the current user to the project, if the project's inscriptions are opened 616 * @param projectId The project id 617 * @return The result 618 * @throws MessagingException If an error occurred sending a notification mail to the project manager 619 * @throws IOException If an error occurred sending the email to the project's manager 620 */ 621 @Callable 622 public Map<String, Object> joinProject(String projectId) throws MessagingException, IOException 623 { 624 Map<String, Object> result = new HashMap<>(); 625 Project project = _resolver.resolveById(projectId); 626 627 if (project == null) 628 { 629 throw new IllegalArgumentException("Unable to join a project, invalid project id received '" + projectId + "'"); 630 } 631 632 UserIdentity currentUser = _currentUserProvider.getUser(); 633 634 Site site = project.getSite(); 635 String siteName = (site != null && StringUtils.isNotEmpty(site.getUrl())) ? site.getName() : null; 636 if (site == null 637 || siteName == null 638 || !_populationContextHelper.getUserPopulationsOnContext("/sites/" + siteName, false).contains(currentUser.getPopulationId()) 639 && !_populationContextHelper.getUserPopulationsOnContext("/sites-fo/" + siteName, false).contains(currentUser.getPopulationId())) 640 { 641 // User is not in the project site populations, cannot be added 642 result.put("success", false); 643 return result; 644 } 645 646 647 boolean success = _projectMemberManager.addProjectMember(project, currentUser); 648 649 result.put("success", success); 650 if (success) 651 { 652 String url = site.getUrl(); 653 result.put("url", url); 654 655 String mailFrom = Config.getInstance().getValue("smtp.mail.from"); 656 657 List<String> managersEmails = Arrays.stream(project.getManagers()) 658 .map(manager -> _userManager.getUser(manager)) 659 .filter(Objects::nonNull) 660 .map(User::getEmail) 661 .filter(StringUtils::isNotEmpty) 662 .collect(Collectors.toList()); 663 664 if (managersEmails.size() > 0 && mailFrom != null) 665 { 666 Map<String, I18nizableTextParameter> params = new HashMap<>(); 667 User current = _userManager.getUser(currentUser); 668 params.put("user", new I18nizableText(current != null ? current.getFullName() : currentUser.getLogin())); 669 params.put("project", new I18nizableText(project.getTitle())); 670 params.put("url", new I18nizableText(url != null ? url : "")); 671 String subject = _i18nUtils.translate(new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_CATALOGUE_JOINPROJECT_MAIL_TITLE", params)); 672 String textBody = _i18nUtils.translate(new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_CATALOGUE_JOINPROJECT_MAIL_BODY", params)); 673 674 for (String managerMail : managersEmails) 675 { 676 SendMailHelper.newMail() 677 .withSubject(subject) 678 .withTextBody(textBody) 679 .withSender(mailFrom) 680 .withRecipient(managerMail) 681 .sendMail(); 682 } 683 } 684 } 685 return result; 686 } 687 688 /** 689 * Send a demand to join a project to the project's manager, if the project's inscriptions are moderated 690 * @param projectId The project to join 691 * @param message A message to send to the project's manager. 692 * @return The result 693 * @throws MessagingException If an error occurred sending the email to the project's manager 694 * @throws IOException If an error occurred sending the email to the project's manager 695 */ 696 @Callable 697 public Map<String, Object> askToJoinProject(String projectId, String message) throws MessagingException, IOException 698 { 699 Map<String, Object> result = new HashMap<>(); 700 701 Project project = _resolver.resolveById(projectId); 702 if (project == null) 703 { 704 throw new IllegalArgumentException("Unable to join a project, invalid project id received '" + projectId + "'"); 705 } 706 707 UserIdentity currentUser = _currentUserProvider.getUser(); 708 709 Site site = project.getSite(); 710 String siteName = site != null ? site.getName() : null; 711 712 if (siteName == null 713 || !_populationContextHelper.getUserPopulationsOnContext("/sites/" + siteName, false).contains(currentUser.getPopulationId()) 714 && !_populationContextHelper.getUserPopulationsOnContext("/sites-fo/" + siteName, false).contains(currentUser.getPopulationId())) 715 { 716 // User is not in the project site populations, cannot be added 717 result.put("success", false); 718 return result; 719 } 720 721 _sendAskToJoinMail(message, project, currentUser); 722 result.put("success", true); 723 result.put("added-notification", Config.getInstance().getValue("workspaces.member.added.send.notification")); 724 return result; 725 } 726 727 private void _sendAskToJoinMail(String message, Project project, UserIdentity joiningUser) throws MessagingException, IOException 728 { 729 String url = getAddUserUrl(project, joiningUser); 730 731 String mailFrom = Config.getInstance().getValue("smtp.mail.from"); 732 733 List<String> managersEmails = Arrays.stream(project.getManagers()) 734 .map(manager -> _userManager.getUser(manager)) 735 .filter(Objects::nonNull) 736 .map(User::getEmail) 737 .filter(StringUtils::isNotEmpty) 738 .collect(Collectors.toList()); 739 740 if (managersEmails.size() > 0 && mailFrom != null) 741 { 742 String escapedMessage = StringUtils.isEmpty(message) ? null : message.replaceAll("<", "<").replaceAll(">", ">").replaceAll("\n", "<br/>"); 743 744 Map<String, I18nizableTextParameter> params = new HashMap<>(); 745 User current = _userManager.getUser(joiningUser); 746 params.put("user", new I18nizableText(current != null ? current.getFullName() : joiningUser.getLogin())); 747 params.put("project", new I18nizableText(project.getTitle())); 748 params.put("url", new I18nizableText(url)); 749 params.put("message", new I18nizableText(escapedMessage != null ? escapedMessage : "")); 750 String subject = _i18nUtils.translate(new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_CATALOGUE_JOINPROJECT_ASK_MAIL_TITLE", params)); 751 String htmlBody = _i18nUtils.translate(new I18nizableText("plugin.workspaces", escapedMessage != null ? "PLUGINS_WORKSPACES_CATALOGUE_JOINPROJECT_ASK_MAIL_BODY" : "PLUGINS_WORKSPACES_CATALOGUE_JOINPROJECT_ASK_MAIL_BODY_EMPTY" , params)); 752 753 SendMailHelper.newMail() 754 .withSubject(subject) 755 .withHTMLBody(htmlBody) 756 .withSender(mailFrom) 757 .withRecipients(managersEmails) 758 .sendMail(); 759 } 760 } 761 762 /** 763 * Get the absolute url to add a user to a project 764 * @param project The project 765 * @param user the identity of user to add 766 * @return the absolute page url 767 */ 768 protected String getAddUserUrl(Project project, UserIdentity user) 769 { 770 String memberPage = ""; 771 772 Set<Page> membersPages = _projectManager.getModulePages(project, MembersWorkspaceModule.MEMBERS_MODULE_ID); 773 if (!membersPages.isEmpty()) 774 { 775 memberPage = ResolveURIComponent.resolve("page", membersPages.iterator().next().getId(), false, true); 776 } 777 778 Site site = project.getSite(); 779 String siteURL = site.getUrl(); 780 String urlWithoutScheme = StringUtils.substringAfter(siteURL, "://"); 781 String relativeURL = StringUtils.contains(urlWithoutScheme, "/") ? "/" + StringUtils.substringAfter(urlWithoutScheme, "/") : ""; 782 return siteURL + "/_authenticate?requestedURL=" + URIUtils.encodeParameter(relativeURL + "/plugins/workspaces/add-member?redirect=" + URIUtils.encodeParameter(memberPage + "?added=" + URIUtils.encodeParameter(UserIdentity.userIdentityToString(user))) + "&user=" + URIUtils.encodeParameter(UserIdentity.userIdentityToString(user)) + "&project=" + URIUtils.encodeParameter(project.getName())); 783 } 784 785 /** 786 * Get the list of allowed data in the form 787 * @return the list of allowed data in the form 788 */ 789 protected String[] getAllowedFormData() 790 { 791 return __ALLOWED_FORM_DATA; 792 } 793 794 795 /** 796 * Callable to get projects of the user and the public projects he can subscribe. 797 * @return A map with three entries an entry for user projects, another one for public projects and finally one for the project's creation right 798 */ 799 @Callable 800 public Map<String, Object> getUserAndPublicProjects() 801 { 802 Map<String, Object> result = new HashMap<>(); 803 804 UserIdentity user = _currentUserProvider.getUser(); 805 806 boolean canCreatePrivateProjet = _rightManager.currentUserHasRight(ProjectConstants.RIGHT_PROJECT_CREATE_PRIVATE, "/cms") == RightResult.RIGHT_ALLOW; 807 boolean canCreatePublicProjetWithModeration = _rightManager.currentUserHasRight(ProjectConstants.RIGHT_PROJECT_CREATE_PUBLIC_MODERATED, "/cms") == RightResult.RIGHT_ALLOW; 808 boolean canCreatePublicProjet = _rightManager.currentUserHasRight(ProjectConstants.RIGHT_PROJECT_CREATE_PUBLIC_OPENED, "/cms") == RightResult.RIGHT_ALLOW; 809 810 result.put("canCreate", canCreatePrivateProjet || canCreatePublicProjetWithModeration || canCreatePublicProjet); 811 result.put("canCreatePrivateProject", canCreatePrivateProjet); 812 result.put("canCreatePublicProjectWithModeration", canCreatePublicProjetWithModeration); 813 result.put("canCreatePublicProject", canCreatePublicProjet); 814 815 List<Map<String, Object>> userProjects = new ArrayList<>(); 816 List<Map<String, Object>> publicProjects = new ArrayList<>(); 817 818 for (Project project : _projectManager.getProjects()) 819 { 820 if (_projectMembers.isProjectMember(project, user)) 821 { 822 Map<String, Object> json = _detailedMyProject2json(project); 823 userProjects.add(json); 824 } 825 else if (project.getInscriptionStatus() != InscriptionStatus.PRIVATE) 826 { 827 Map<String, Object> json = detailedProject2json(project); 828 publicProjects.add(json); 829 } 830 } 831 832 result.put("userProjects", userProjects); 833 result.put("availablePublicProjects", publicProjects); 834 835 return result; 836 } 837 838 /** 839 * Callable to get projects of the user. 840 * @return A map with the user projects 841 */ 842 @Callable 843 public List<Map<String, Object>> getUserProjects() 844 { 845 846 UserIdentity user = _currentUserProvider.getUser(); 847 848 849 List<Map<String, Object>> userProjects = new ArrayList<>(); 850 851 for (Project project : _projectManager.getProjects()) 852 { 853 if (_projectMembers.isProjectMember(project, user)) 854 { 855 Map<String, Object> json = _detailedMyProject2json(project); 856 userProjects.add(json); 857 } 858 } 859 860 return userProjects; 861 } 862 863 private Map<String, Object> _detailedMyProject2json(Project project) 864 { 865 Map<String, Object> json = detailedProject2json(project); 866 json.put("favorite", false); 867 json.put("notification", true); 868 json.put("canEdit", _rightManager.currentUserHasRight(ProjectConstants.RIGHT_PROJECT_EDIT, project) == RightResult.RIGHT_ALLOW); 869 json.put("canDelete", _rightManager.currentUserHasRight(ProjectConstants.RIGHT_PROJECT_DELETE, project) == RightResult.RIGHT_ALLOW); 870 json.put("canAccessBO", _projectManager.canAccessBO(project)); 871 return json; 872 } 873 874 /** 875 * Transform a {@link Project} into a json map 876 * @param project the project to parse 877 * @return a json map 878 */ 879 public Map<String, Object> detailedProject2json(Project project) 880 { 881 Map<String, Object> json = new HashMap<>(); 882 883 json.put("id", project.getId()); 884 json.put("name", project.getName()); 885 json.put("title", project.getTitle()); 886 json.put("url", _projectManager.getProjectUrl(project, StringUtils.EMPTY)); 887 888 json.put("defaultProfile", project.getDefaultProfile()); 889 890 Set<String> categories = project.getCategories(); 891 if (categories.size() != 1) 892 { 893 getLogger().warn("Project " + project.getTitle() + " (" + project.getId() + ") should have one and only one category"); 894 } 895 896 if (!categories.isEmpty()) 897 { 898 String c = categories.iterator().next(); 899 Category category = _categoryProviderEP.getTag(c, new HashMap<>()); 900 901 if (category != null) 902 { 903 Map<String, Object> map = new HashMap<>(); 904 map.put("id", category.getId()); 905 map.put("name", category.getName()); 906 map.put("title", category.getTitle()); 907 map.put("color", _categoryHelper.getCategoryColor(category).get("main")); 908 909 json.put("category", map); 910 } 911 } 912 913 String[] keywords = Stream.of(project.getKeywords()) 914 .filter(k -> _keywordProviderEP.hasTag(k, new HashMap<>())) 915 .toArray(String[]::new); 916 json.put("keywords", keywords); 917 918 UserIdentity[] managers = project.getManagers(); 919 if (managers.length > 0) 920 { 921 json.put("managers", Arrays.stream(managers) 922 .map(_userHelper::user2json) 923 .filter(userAsJson -> !userAsJson.equals(Collections.EMPTY_MAP)) 924 .collect(Collectors.toList())); 925 } 926 927 List<Map<String, Object>> members = new ArrayList<>(); 928 for (int i = 1; i < Math.min(managers.length, 4); i++) 929 { 930 members.add(_userHelper.user2json(managers[i])); 931 } 932 933 if (members.size() < 3) 934 { 935 List<UserIdentity> managersList = Arrays.asList(managers); 936 937 members.addAll( 938 _projectMembers.getProjectMembers(project, true) 939 .stream() 940 .map(ProjectMember::getUser) 941 .map(User::getIdentity) 942 .filter(Predicate.not(managersList::contains)) 943 .limit(3 - members.size()) 944 .map(_userHelper::user2json) 945 .collect(Collectors.toList()) 946 ); 947 } 948 949 json.put("members", members); 950 json.put("membersCount", _projectMembers.getMembersCount(project)); 951 952 json.put("modules", Arrays.asList(project.getModules())); 953 954 switch (project.getInscriptionStatus()) 955 { 956 case PRIVATE: json.put("visibility", 1); break; 957 case MODERATED: json.put("visibility", 2); break; 958 case OPEN: 959 // fallthrought 960 default: 961 json.put("visibility", 3); break; 962 } 963 964 json.put("description", project.getDescription()); 965 966 Site site = project.getSite(); 967 if (site != null) 968 { 969 json.put("site", site.getName()); 970 json.put("language", site.getSitemaps().iterator().next().getName()); 971 972 if (site.getIllustration() != null) 973 { 974 String illustration = ResolveURIComponent.resolveCroppedImage("site-parameter", site.getName() + ";illustration", 252, 389, false, true); 975 json.put("illustration", illustration); 976 } 977 } 978 979 return json; 980 } 981 982 /** 983 * SAX a project 984 * @param contentHandler The content handler to sax into 985 * @param project the project 986 * @throws SAXException if an error occurred while saxing 987 */ 988 public void saxProject(ContentHandler contentHandler, Project project) throws SAXException 989 { 990 AttributesImpl attrs = new AttributesImpl(); 991 attrs.addCDATAAttribute("id", project.getId()); 992 attrs.addCDATAAttribute("name", project.getName()); 993 994 XMLUtils.startElement(contentHandler, "project", attrs); 995 996 XMLUtils.createElement(contentHandler, "title", project.getTitle()); 997 998 XMLUtils.createElement(contentHandler, "inscriptionStatus", project.getInscriptionStatus().name()); 999 1000 String description = project.getDescription(); 1001 if (description != null) 1002 { 1003 XMLUtils.createElement(contentHandler, "description", description); 1004 } 1005 XMLUtils.createElement(contentHandler, "url", _projectManager.getProjectUrl(project, StringUtils.EMPTY)); 1006 1007 saxCategory(contentHandler, project); 1008 1009 for (String keyword : project.getKeywords()) 1010 { 1011 XMLUtils.createElement(contentHandler, "keyword", keyword); 1012 } 1013 1014 UserIdentity[] managers = project.getManagers(); 1015 if (managers.length > 0) 1016 { 1017 for (UserIdentity userIdentity : managers) 1018 { 1019 _userHelper.saxUserIdentity(userIdentity, contentHandler, "manager"); 1020 } 1021 } 1022 1023 Site site = project.getSite(); 1024 if (site != null) 1025 { 1026 attrs.clear(); 1027 attrs.addCDATAAttribute("name", site.getName()); 1028 attrs.addCDATAAttribute("language", site.getSitemaps().iterator().next().getName()); 1029 1030 XMLUtils.createElement(contentHandler, "site", attrs); 1031 } 1032 1033 XMLUtils.endElement(contentHandler, "project"); 1034 } 1035 1036 /** 1037 * SAX the project's category 1038 * @param contentHandler the content handler to sax into 1039 * @param project the project 1040 * @throws SAXException if an error occurred while saxing 1041 */ 1042 public void saxCategory(ContentHandler contentHandler, Project project) throws SAXException 1043 { 1044 saxCategory(contentHandler, project, "category"); 1045 } 1046 1047 /** 1048 * SAX the project's category 1049 * @param contentHandler the content handler to sax into 1050 * @param project the project 1051 * @param tagName the tag name for category 1052 * @throws SAXException if an error occurred while saxing 1053 */ 1054 public void saxCategory(ContentHandler contentHandler, Project project, String tagName) throws SAXException 1055 { 1056 Set<String> categories = project.getCategories(); 1057 if (!categories.isEmpty()) 1058 { 1059 String c = categories.iterator().next(); 1060 Category category = _categoryProviderEP.getTag(c, new HashMap<>()); 1061 1062 if (category != null) 1063 { 1064 AttributesImpl attrs = new AttributesImpl(); 1065 attrs.addCDATAAttribute("id", category.getId()); 1066 attrs.addCDATAAttribute("name", category.getName()); 1067 attrs.addCDATAAttribute("color", _categoryHelper.getCategoryColor(category).get("main")); 1068 1069 XMLUtils.startElement(contentHandler, tagName, attrs); 1070 category.getTitle().toSAX(contentHandler); 1071 XMLUtils.endElement(contentHandler, tagName); 1072 } 1073 } 1074 } 1075}