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