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