001/*
002 *  Copyright 2016 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.members;
017
018import java.util.ArrayList;
019import java.util.Arrays;
020import java.util.Collection;
021import java.util.Comparator;
022import java.util.HashMap;
023import java.util.List;
024import java.util.Map;
025import java.util.Map.Entry;
026import java.util.Objects;
027import java.util.Optional;
028import java.util.Set;
029import java.util.TreeSet;
030import java.util.function.BiPredicate;
031import java.util.function.Predicate;
032import java.util.regex.Matcher;
033import java.util.regex.Pattern;
034import java.util.stream.Collectors;
035
036import org.apache.avalon.framework.activity.Initializable;
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.service.ServiceException;
042import org.apache.avalon.framework.service.ServiceManager;
043import org.apache.avalon.framework.service.Serviceable;
044import org.apache.cocoon.components.ContextHelper;
045import org.apache.cocoon.environment.Request;
046import org.apache.commons.collections.CollectionUtils;
047import org.apache.commons.lang3.StringUtils;
048import org.apache.http.annotation.Obsolete;
049
050import org.ametys.cms.languages.Language;
051import org.ametys.cms.languages.LanguagesManager;
052import org.ametys.cms.repository.Content;
053import org.ametys.cms.transformation.URIResolverExtensionPoint;
054import org.ametys.core.cache.AbstractCacheManager;
055import org.ametys.core.cache.Cache;
056import org.ametys.core.group.Group;
057import org.ametys.core.group.GroupDirectoryContextHelper;
058import org.ametys.core.group.GroupIdentity;
059import org.ametys.core.group.GroupManager;
060import org.ametys.core.observation.Event;
061import org.ametys.core.observation.ObservationManager;
062import org.ametys.core.right.ProfileAssignmentStorage.UserOrGroup;
063import org.ametys.core.right.ProfileAssignmentStorageExtensionPoint;
064import org.ametys.core.right.RightManager;
065import org.ametys.core.right.RightProfilesDAO;
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.directory.NotUniqueUserException;
072import org.ametys.plugins.core.impl.cache.AbstractCacheKey;
073import org.ametys.plugins.core.user.UserHelper;
074import org.ametys.plugins.explorer.resources.ModifiableResourceCollection;
075import org.ametys.plugins.repository.AmetysObject;
076import org.ametys.plugins.repository.AmetysObjectResolver;
077import org.ametys.plugins.repository.AmetysRepositoryException;
078import org.ametys.plugins.repository.ModifiableAmetysObject;
079import org.ametys.plugins.repository.ModifiableTraversableAmetysObject;
080import org.ametys.plugins.repository.RepositoryConstants;
081import org.ametys.plugins.userdirectory.UserDirectoryHelper;
082import org.ametys.plugins.userdirectory.page.UserDirectoryPageResolver;
083import org.ametys.plugins.userdirectory.page.UserPage;
084import org.ametys.plugins.workspaces.ObservationConstants;
085import org.ametys.plugins.workspaces.members.JCRProjectMember.MemberType;
086import org.ametys.plugins.workspaces.project.ProjectManager;
087import org.ametys.plugins.workspaces.project.ProjectsCatalogueManager;
088import org.ametys.plugins.workspaces.project.modules.WorkspaceModule;
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.runtime.authentication.AccessDeniedException;
094import org.ametys.runtime.config.Config;
095import org.ametys.runtime.i18n.I18nizableText;
096import org.ametys.runtime.plugin.component.AbstractLogEnabled;
097import org.ametys.web.WebConstants;
098import org.ametys.web.WebHelper;
099import org.ametys.web.population.PopulationContextHelper;
100import org.ametys.web.repository.site.Site;
101import org.ametys.web.usermanagement.UserManagementException;
102import org.ametys.web.usermanagement.UserSignupManager;
103
104/**
105 * Helper component for managing project's users
106 */
107public class ProjectMemberManager extends AbstractLogEnabled implements Serviceable, Component, Contextualizable, Initializable
108{
109    /** Avalon Role */
110    public static final String ROLE = ProjectMemberManager.class.getName();
111    
112    /** The id of the members service */
113    public static final String __WORKSPACES_SERVICE_MEMBERS = "org.ametys.plugins.workspaces.module.Members";
114    
115    private static final String __PROJECT_MEMBER_CACHE = "projectMemberCache";
116    
117    @Obsolete // For v1 project only
118    private static final String __PROJECT_RIGHT_PROFILE = "PROJECT";
119    
120    /** Constants for users project node */
121    private static final String __PROJECT_MEMBERS_NODE = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":members";
122    
123    /** The type of the project users node type */
124    private static final String __PROJECT_MEMBERS_NODE_TYPE = RepositoryConstants.NAMESPACE_PREFIX + ":unstructured";
125
126    /** The type of a project user node type */
127    private static final String __PROJECT_MEMBER_NODE_TYPE = RepositoryConstants.NAMESPACE_PREFIX + ":project-member";
128
129    private static Pattern __MAIL_BETWEEN_BRACKETS_PATTERN = Pattern.compile("^[^<]*<(.*@.*)>$");
130    
131    /** Avalon context */
132    protected Context _context;
133    
134    /** Project manager */
135    protected ProjectManager _projectManager;
136
137    /** Project rights helper */
138    protected ProjectRightHelper _projectRightHelper;
139
140    /** Profiles right manager */
141    protected RightProfilesDAO _rightProfilesDAO;
142
143    /** Profile assignment storage */
144    protected ProfileAssignmentStorageExtensionPoint _profileAssignmentStorageExtensionPoint;
145    
146    /** Ametys object resolver */
147    protected AmetysObjectResolver _resolver;
148
149    /** Rights manager */
150    protected RightManager _rightManager;
151
152    /** Current user provider */
153    protected CurrentUserProvider _currentUserProvider;
154
155    /** Users manager */
156    protected UserManager _userManager;
157
158    /** The observation manager */
159    protected ObservationManager _observationManager;
160
161    /** Module managers EP */
162    protected WorkspaceModuleExtensionPoint _moduleManagerEP;
163    
164    /** The user helper */
165    protected UserHelper _userHelper;
166
167    /** The groups manager */
168    protected GroupManager _groupManager;
169
170    /** The population context helper */
171    protected PopulationContextHelper _populationContextHelper;
172
173    /** The user directory helper */
174    protected UserDirectoryHelper _userDirectoryHelper;
175    
176    /** The project invitation helper */
177    protected ProjectInvitationHelper _projectInvitationHelper;
178
179    /** The language manager */
180    protected LanguagesManager _languagesManager;
181        
182    /** The resolver for user directory pages */
183    protected UserDirectoryPageResolver _userDirectoryPageResolver;
184
185    /** The page URI resolver. */
186    protected URIResolverExtensionPoint _uriResolver;
187
188    /** The group directory context helper */
189    protected GroupDirectoryContextHelper _groupDirectoryContextHelper;
190
191    /** The cache manager */
192    protected AbstractCacheManager _abstractCacheManager;
193    
194    /** The user signup manager */
195    protected UserSignupManager _userSignupManager;
196
197    /** The project catalogue manager component */
198    protected ProjectsCatalogueManager _projectsCatalogueManager;
199
200    @Override
201    public void contextualize(Context context) throws ContextException
202    {
203        _context = context;
204    }
205    
206    @Override
207    public void service(ServiceManager manager) throws ServiceException
208    {
209        _abstractCacheManager = (AbstractCacheManager) manager.lookup(AbstractCacheManager.ROLE);
210        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
211        _projectManager = (ProjectManager) manager.lookup(ProjectManager.ROLE);
212        _projectRightHelper = (ProjectRightHelper) manager.lookup(ProjectRightHelper.ROLE);
213        _rightProfilesDAO = (RightProfilesDAO) manager.lookup(RightProfilesDAO.ROLE);
214        _profileAssignmentStorageExtensionPoint = (ProfileAssignmentStorageExtensionPoint) manager.lookup(ProfileAssignmentStorageExtensionPoint.ROLE);
215        _rightManager = (RightManager) manager.lookup(RightManager.ROLE);
216        _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
217        _userManager = (UserManager) manager.lookup(UserManager.ROLE);
218        _groupManager = (GroupManager) manager.lookup(GroupManager.ROLE);
219        _userHelper = (UserHelper) manager.lookup(UserHelper.ROLE);
220        _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE);
221        _moduleManagerEP = (WorkspaceModuleExtensionPoint) manager.lookup(WorkspaceModuleExtensionPoint.ROLE);
222        _populationContextHelper = (PopulationContextHelper) manager.lookup(org.ametys.core.user.population.PopulationContextHelper.ROLE);
223        _userDirectoryHelper = (UserDirectoryHelper) manager.lookup(UserDirectoryHelper.ROLE);
224        _projectInvitationHelper = (ProjectInvitationHelper) manager.lookup(ProjectInvitationHelper.ROLE);
225        _languagesManager = (LanguagesManager) manager.lookup(LanguagesManager.ROLE);
226        _userDirectoryPageResolver = (UserDirectoryPageResolver) manager.lookup(UserDirectoryPageResolver.ROLE);
227        _uriResolver = (URIResolverExtensionPoint) manager.lookup(URIResolverExtensionPoint.ROLE);
228        _groupDirectoryContextHelper = (GroupDirectoryContextHelper) manager.lookup(GroupDirectoryContextHelper.ROLE);
229        _userSignupManager = (UserSignupManager) manager.lookup(UserSignupManager.ROLE);
230        _projectsCatalogueManager = (ProjectsCatalogueManager) manager.lookup(ProjectsCatalogueManager.ROLE);
231    }
232    
233    public void initialize() throws Exception
234    {
235        _abstractCacheManager.createMemoryCache(__PROJECT_MEMBER_CACHE,
236                new I18nizableText("plugin.workspaces", "PLUGIN_WORKSPACES_CACHE_PROJECT_MEMBER_LABEL"),
237                new I18nizableText("plugin.workspaces", "PLUGIN_WORKSPACES_CACHE_PROJECT_MEMBER_DESCRIPTION"),
238                true,
239                null);
240    }
241    
242    /**
243     * Retrieve the data of a member of a project, or the default data if no user is provided
244     * @param projectName The name of the project
245     * @param identity The user or group identity. If null, return the default profiles for a new user
246     * @param type The type of the identity. Can be "user" or "group"
247     * @return The map of profiles per module for the user
248     */
249    @Callable
250    public Map<String, Object> getProjectMemberData(String projectName, String identity, String type)
251    {
252        Map<String, Object> result = new HashMap<>();
253        
254        boolean isTypeUser = JCRProjectMember.MemberType.USER.name().equals(type.toUpperCase());
255        boolean isTypeGroup = JCRProjectMember.MemberType.GROUP.name().equals(type.toUpperCase());
256        UserIdentity user = Optional.ofNullable(identity)
257                                    .filter(id -> id != null && isTypeUser)
258                                    .map(UserIdentity::stringToUserIdentity)
259                                    .orElse(null);
260        GroupIdentity group = Optional.ofNullable(identity)
261                                      .filter(id -> id != null && isTypeGroup)
262                                      .map(GroupIdentity::stringToGroupIdentity)
263                                      .orElse(null);
264        
265        if (identity != null)
266        {
267            if (isTypeGroup && group == null)
268            {
269                result.put("message", "unknown-group");
270                result.put("success", false);
271                return result;
272            }
273            else if (isTypeUser && user == null)
274            {
275                result.put("message", "unknown-user");
276                result.put("success", false);
277                return result;
278            }
279        }
280        
281        Project project = _projectManager.getProject(projectName);
282        if (project == null)
283        {
284            result.put("message", "unknown-project");
285            result.put("success", false);
286            return result;
287        }
288        
289        if (!_projectRightHelper.canAddMember(project))
290        {
291            throw new AccessDeniedException("User '" + _currentUserProvider.getUser() + "' tried to get member's rights without convenient right [" + projectName + ", " + identity + "]");
292        }
293        
294        boolean newMember = true;
295        Map<String, String> userProfiles;
296        
297        if (user != null || group != null)
298        {
299            JCRProjectMember projectMember = user != null ? _getOrCreateJCRProjectMember(project, user) : _getOrCreateJCRProjectMember(project, group);
300            
301            newMember = projectMember.needsSave();
302            
303            String role = projectMember.getRole();
304            if (role != null)
305            {
306                result.put("role", role);
307            }
308            
309            userProfiles = _getMemberProfiles(projectMember, project);
310        }
311        else
312        {
313            userProfiles = new HashMap<>();
314        }
315        
316        result.put("profiles", userProfiles);
317        result.put("status", newMember ? "new" : "edit");
318        result.put("success", true);
319        
320        return result;
321    }
322
323    
324    /**
325     * Get right profile of a member
326     * @param member The member
327     * @param project The project name
328     * @return a map of the right profile
329     */
330    private Map<String, String> _getMemberProfiles(JCRProjectMember member, Project project)
331    {
332        Map<String, String> userProfiles = new HashMap<>();
333        
334        // Get allowed profile on modules (among the project members's profiles)
335        for (WorkspaceModule module : _projectManager.getModules(project))
336        {
337            String allowedProfileOnProject = _getAllowedProfileOnModule(project, module, member);
338            userProfiles.put(module.getId(), allowedProfileOnProject);
339        }
340        
341        return userProfiles;
342    }
343    
344    private String _getAllowedProfileOnModule (Project project, WorkspaceModule module, JCRProjectMember member)
345    {
346        Set<String> profileIds = _projectRightHelper.getProfilesIds();
347        
348        AmetysObject moduleObject = module.getModuleRoot(project, false);
349        Set<String> allowedProfilesForMember = _getAllowedProfile(member, moduleObject);
350        
351        for (String allowedProfile : allowedProfilesForMember)
352        {
353            if (profileIds.contains(allowedProfile))
354            {
355                // Get the first allowed profile among the project's members profiles
356                return allowedProfile;
357            }
358        }
359        
360        return null;
361    }
362    
363    /**
364     * Add new members and invitation by email
365     * @param projectName The project name
366     * @param newMembers The members to add (users or groups)
367     * @param invitEmails The invitation emails
368     * @return the result with errors
369     */
370    @SuppressWarnings("unchecked")
371    @Callable
372    public Map<String, Object> addMembers(String projectName, List<Map<String, String>> newMembers, List<String> invitEmails)
373    {
374        Map<String, Object> result = new HashMap<>();
375        
376        Request request = ContextHelper.getRequest(_context);
377        String siteName = WebHelper.getSiteName(request);
378        
379        boolean hasError = false;
380        boolean inviteError = false;
381        boolean unknownProject = false;
382        List<String> unknownGroups = new ArrayList<>();
383        List<String> unknownUsers = new ArrayList<>();
384        List<Map<String, Object>> existingUsers = new ArrayList<>();
385        List<Map<String, Object>> membersAdded = new ArrayList<>();
386        
387        List<String> filteredInvitEmails = new ArrayList<>();
388        if (invitEmails != null)
389        {
390            try
391            {
392                for (String invitEmail : invitEmails)
393                {
394                    
395                    
396                    String filteredInvitEmail = invitEmail;
397
398                    // Regexp pattern to extract "email@domain.com" from "FirstName LastName <email@domain.com>"
399                    // ^[^<]*<(.*@.*)>$
400                    // ^  => asserts position at start of a line
401                    // [^<]* => match any characters that are not '<', so the matched group start at the first bracket
402                    // <(.*@.*)> => match text between brackets, containing '@'
403                    Matcher matcher =  __MAIL_BETWEEN_BRACKETS_PATTERN.matcher(invitEmail);
404                    if (matcher.matches() && matcher.groupCount() == 1)
405                    {
406                        filteredInvitEmail = matcher.group(1);
407                    }
408                    
409                    Optional<User> userIfExists = _userSignupManager.getUserIfHeExists(filteredInvitEmail, siteName);
410                    if (userIfExists.isPresent())
411                    {
412                        newMembers.add(Map.of(
413                            "id", UserIdentity.userIdentityToString(userIfExists.get().getIdentity()),
414                            "type", "user"
415                        ));
416                    }
417                    else
418                    {
419                        filteredInvitEmails.add(filteredInvitEmail);
420                    }
421                }
422            }
423            catch (UserManagementException e)
424            {
425                hasError = true;
426                inviteError = true;
427                getLogger().error("Impossible to send email invitations", e);
428            }
429            catch (NotUniqueUserException e)
430            {
431                hasError = true;
432                inviteError = true;
433                getLogger().error("Impossible to send email invitations, some user already exist", e);
434            }
435        }
436        
437        for (Map<String, String> newMember : newMembers)
438        {
439            Map<String, Object> addResult = addMember(projectName, newMember.get("id"), newMember.get("type"));
440            boolean success = (boolean) addResult.get("success");
441            if (!success)
442            {
443                String error = (String) addResult.get("message");
444                if ("unknown-user".equals(error))
445                {
446                    hasError = true;
447                    unknownUsers.add(newMember.get("id"));
448                }
449                else if ("unknown-group".equals(error))
450                {
451                    hasError = true;
452                    unknownGroups.add(newMember.get("id"));
453                }
454                else if ("unknown-project".equals(error))
455                {
456                    hasError = true;
457                    unknownProject = true;
458                }
459                else if ("existing-user".equals(error))
460                {
461                    existingUsers.add((Map<String, Object>) addResult.get("existing-user"));
462                }
463            }
464            else
465            {
466                membersAdded.add((Map<String, Object>) addResult.get("member"));
467            }
468        }
469        
470        if (!filteredInvitEmails.isEmpty())
471        {
472            Map<String, String> newProfiles = _getDefaultProfilesByModule();
473            
474            try
475            {
476                Map<String, Object> inviteEmails = _projectInvitationHelper.inviteEmails(projectName, filteredInvitEmails, newProfiles);
477                List<String> errors = (List<String>) inviteEmails.get("email-error");
478                if (!errors.isEmpty())
479                {
480                    hasError = true;
481                    inviteError = true;
482                }
483                existingUsers.addAll((List<Map<String, Object>>) inviteEmails.get("existing-users"));
484            }
485            catch (UserManagementException e)
486            {
487                hasError = true;
488                inviteError = true;
489                getLogger().error("Impossible to send email invitations", e);
490            }
491            catch (NotUniqueUserException e)
492            {
493                hasError = true;
494                inviteError = true;
495                getLogger().error("Impossible to send email invitations, some user already exist", e);
496            }
497            
498        }
499        
500        result.put("invite-error", inviteError);
501        result.put("existing-users", existingUsers);
502        result.put("unknown-groups", unknownGroups);
503        result.put("unknown-users", unknownUsers);
504        result.put("unknown-project", unknownProject);
505        result.put("members-added", membersAdded);
506        result.put("success", !hasError);
507        
508        return result;
509    }
510    
511    /**
512     * Add a new member
513     * @param projectName The project name
514     * @param identity The user or group identity.
515     * @param type The type of the identity. Can be "user" or "group"
516     * @return the result
517     */
518    @Callable
519    public Map<String, Object> addMember(String projectName, String identity, String type)
520    {
521        Map<String, String> newProfiles = _getDefaultProfilesByModule();
522        
523        return _setProjectMemberData(projectName, identity, type, newProfiles, null, true);
524    }
525    
526    /**
527     * Get a map of each available module, with the default profile
528     * @return A map with moduleId : profileId
529     */
530    protected Map<String, String> _getDefaultProfilesByModule()
531    {
532        Map<String, String> newProfiles = new HashMap<>();
533        
534        String defaultProfile = StringUtils.defaultString(Config.getInstance().getValue("workspaces.profile.default"));
535        for (String moduleId : _moduleManagerEP.getExtensionsIds())
536        {
537            newProfiles.put(moduleId, defaultProfile);
538        }
539        
540        return newProfiles;
541    }
542    
543    /**
544     * Set the user data in the project
545     * @param projectName The project name
546     * @param identity The user or group identity.
547     * @param type The type of the identity. Can be "user" or "group"
548     * @param newProfiles The profiles to affect, mapped by module
549     * @param role The user role inside the project
550     * @return The result
551     */
552    @Callable
553    public Map<String, Object> setProjectMemberData(String projectName, String identity, String type, Map<String, String> newProfiles, String role)
554    {
555        return _setProjectMemberData(projectName, identity, type, newProfiles, role, false);
556    }
557    
558    /**
559     * Set the user data in the project
560     * @param projectName The project name
561     * @param identity The user or group identity.
562     * @param type The type of the identity. Can be "user" or "group"
563     * @param newProfiles The profiles to affect, mapped by module
564     * @param role The user role inside the project
565     * @param isNewUser <code>true</code> if the user is just added
566     * @return The result
567     */
568    protected Map<String, Object> _setProjectMemberData(String projectName, String identity, String type, Map<String, String> newProfiles, String role, boolean isNewUser)
569    {
570        Map<String, Object> result = new HashMap<>();
571        Project project = _projectManager.getProject(projectName);
572        if (project == null)
573        {
574            result.put("success", false);
575            result.put("message", "unknown-project");
576            return result;
577        }
578        
579        boolean isTypeUser = JCRProjectMember.MemberType.USER.name().equals(type.toUpperCase());
580        boolean isTypeGroup = JCRProjectMember.MemberType.GROUP.name().equals(type.toUpperCase());
581        UserIdentity user = Optional.ofNullable(identity)
582                                    .filter(id -> id != null && isTypeUser)
583                                    .map(UserIdentity::stringToUserIdentity)
584                                    .orElse(null);
585        GroupIdentity group = Optional.ofNullable(identity)
586                                      .filter(id -> id != null && isTypeGroup)
587                                      .map(GroupIdentity::stringToGroupIdentity)
588                                      .orElse(null);
589        
590        if (group == null && user == null)
591        {
592            result.put("success", false);
593            result.put("message", isTypeGroup ? "unknown-group" : "unknown-user");
594            return result;
595        }
596        
597        if (isNewUser && !_projectRightHelper.canAddMember(project) || !isNewUser && !_projectRightHelper.canEditMember(project))
598        {
599            throw new AccessDeniedException("User '" + _currentUserProvider.getUser() + "' tried to set member rights without convenient right [" + projectName + ", " + identity + "]");
600        }
601        
602        if (isNewUser && isTypeUser && _getProjectMember(project, user) != null)
603        {
604            result.put("success", false);
605            result.put("message", "existing-user");
606            result.put("existing-user", _userHelper.user2json(user, true));
607            
608            return result;
609        }
610        
611        JCRProjectMember projectMember  = isTypeUser ? addOrUpdateProjectMember(project, user, newProfiles) : addOrUpdateProjectMember(project, group, newProfiles);
612        if (projectMember != null)
613        {
614            Request request = ContextHelper.getRequest(_context);
615            String lang = (String) request.getAttribute(WebConstants.REQUEST_ATTR_SITEMAP_NAME);
616            
617            ProjectMember member = isTypeUser ? new ProjectMember(_userManager.getUser(user), projectMember.getRole(), false) : new ProjectMember(_groupManager.getGroup(group));
618            result.put("member", _member2Json(member, lang));
619        }
620        
621        result.put("success", projectMember != null);
622        return result;
623    }
624    
625    /**
626     * Add a user to a project with open inscriptions, using the default values
627     * @param project The project
628     * @param user The user
629     * @return the added member in case of success, null otherwise
630     */
631    public JCRProjectMember addProjectMember(Project project, UserIdentity user)
632    {
633        InscriptionStatus inscriptionStatus = project.getInscriptionStatus();
634        if (!inscriptionStatus.equals(InscriptionStatus.OPEN))
635        {
636            return null;
637        }
638        
639        return addOrUpdateProjectMember(project, user, Map.of());
640    }
641    
642    /**
643     * Add a user to a project, using the provided profile values
644     * @param project The project
645     * @param user The user
646     * @param allowedProfiles the profile values
647     * @return the added member in case of success, null otherwise
648     */
649    public JCRProjectMember addOrUpdateProjectMember(Project project, UserIdentity user, Map<String, String> allowedProfiles)
650    {
651        return addOrUpdateProjectMember(project, user, allowedProfiles, _currentUserProvider.getUser());
652    }
653    /**
654     * Add a user to a project, using the provided profile values
655     * @param project The project
656     * @param user The user
657     * @param allowedProfiles the profile values
658     * @param issuer identity of the user that approved the member
659     * @return the added member in case of success, null otherwise
660     */
661    public JCRProjectMember addOrUpdateProjectMember(Project project, UserIdentity user, Map<String, String> allowedProfiles, UserIdentity issuer)
662    {
663        if (user == null)
664        {
665            return null;
666        }
667        
668        JCRProjectMember projectMember = _getOrCreateJCRProjectMember(project, user);
669        _setMemberProfiles(allowedProfiles, projectMember, project);
670        _saveAndNotifyProjectMemberUpdate(project, projectMember, UserIdentity.userIdentityToString(user), issuer);
671        return projectMember;
672    }
673    
674    /**
675     * Add a group to a project, using the provided profile values
676     * @param project The project
677     * @param group The group
678     * @param allowedProfiles the profile values
679     * @return the added member in case of success, null otherwise
680     */
681    public JCRProjectMember addOrUpdateProjectMember(Project project, GroupIdentity group, Map<String, String> allowedProfiles)
682    {
683        if (group == null)
684        {
685            return null;
686        }
687        
688        JCRProjectMember projectMember = _getOrCreateJCRProjectMember(project, group);
689        _setMemberProfiles(allowedProfiles, projectMember, project);
690        _saveAndNotifyProjectMemberUpdate(project, projectMember, GroupIdentity.groupIdentityToString(group), _currentUserProvider.getUser());
691        return projectMember;
692    }
693
694    private void _saveAndNotifyProjectMemberUpdate(Project project, JCRProjectMember projectMember, String userIdentityString, UserIdentity issuer)
695    {
696        project.saveChanges();
697        
698        _getCache().invalidate(ProjectMemberCacheKey.of(project.getId(), null));
699        
700        // Notify listeners
701        Map<String, Object> eventParams = new HashMap<>();
702        eventParams.put(ObservationConstants.ARGS_MEMBER, projectMember);
703        eventParams.put(ObservationConstants.ARGS_MEMBER_ID, projectMember.getId());
704        eventParams.put(ObservationConstants.ARGS_PROJECT, project);
705        eventParams.put(ObservationConstants.ARGS_PROJECT_ID, project.getId());
706        eventParams.put(ObservationConstants.ARGS_MEMBER_IDENTITY, userIdentityString);
707        eventParams.put(ObservationConstants.ARGS_MEMBER_IDENTITY_TYPE, projectMember.getType());
708        
709        _observationManager.notify(new Event(ObservationConstants.EVENT_MEMBER_ADDED, issuer, eventParams));
710    }
711
712    /**
713     * Set the profiles for a member
714     * @param newProfiles The allowed profile by module
715     * @param projectMember The member
716     * @param project The project
717     */
718    private void _setMemberProfiles(Map<String, String> newProfiles, JCRProjectMember projectMember, Project project)
719    {
720        String defaultProfile = project.getDefaultProfile();
721        Set<String> defaultProfiles;
722        if (StringUtils.isEmpty(defaultProfile))
723        {
724            defaultProfiles = Set.of();
725        }
726        else
727        {
728            defaultProfiles = Set.of(defaultProfile);
729        }
730        
731        for (WorkspaceModule module : _moduleManagerEP.getModules())
732        {
733            Set<String> moduleProfiles;
734            if (newProfiles.containsKey(module.getId()))
735            {
736                String profile = newProfiles.get(module.getId());
737                moduleProfiles = StringUtils.isEmpty(profile) ? Set.of() : Set.of(profile);
738            }
739            else
740            {
741                moduleProfiles = defaultProfiles;
742            }
743            setProfileOnModule(projectMember, project, module, moduleProfiles);
744        }
745    }
746    
747    /**
748     * Affect profiles for a member on a given module
749     * @param member The member
750     * @param project The project
751     * @param module The module
752     * @param allowedProfiles The allowed profiles for the module
753     */
754    public void setProfileOnModule(JCRProjectMember member, Project project, WorkspaceModule module, Set<String> allowedProfiles)
755    {
756        if (module != null && _projectManager.isModuleActivated(project, module.getId()))
757        {
758            AmetysObject moduleObject = module.getModuleRoot(project, false);
759            _setMemberProfiles(member, allowedProfiles, moduleObject);
760        }
761    }
762    
763    private Set<String> _getAllowedProfile(JCRProjectMember member, AmetysObject object)
764    {
765        if (MemberType.GROUP == member.getType())
766        {
767            Map<GroupIdentity, Map<UserOrGroup, Set<String>>> profilesForGroups = _profileAssignmentStorageExtensionPoint.getProfilesForGroups(object, Set.of(member.getGroup()));
768            return Optional.ofNullable(profilesForGroups.get(member.getGroup())).map(a -> a.get(UserOrGroup.ALLOWED)).orElse(Set.of());
769        }
770        else
771        {
772            Map<UserIdentity, Map<UserOrGroup, Set<String>>> profilesForUsers = _profileAssignmentStorageExtensionPoint.getProfilesForUsers(object, member.getUser());
773            return Optional.ofNullable(profilesForUsers.get(member.getUser())).map(a -> a.get(UserOrGroup.ALLOWED)).orElse(Set.of());
774        }
775    }
776
777    private void _setMemberProfiles(JCRProjectMember member, Set<String> allowedProfiles, AmetysObject object)
778    {
779        Set<String> currentAllowedProfiles = _getAllowedProfile(member, object);
780        
781        Collection<String> profilesToRemove  = CollectionUtils.removeAll(currentAllowedProfiles, allowedProfiles);
782        
783        Collection<String> profilesToAdd  = CollectionUtils.removeAll(allowedProfiles, currentAllowedProfiles);
784        
785        for (String profileId : profilesToRemove)
786        {
787            _removeProfile(member, profileId, object);
788        }
789        
790        for (String profileId : profilesToAdd)
791        {
792            _addProfile(member, profileId, object);
793        }
794        
795        Collection<String> updatedProfiles = CollectionUtils.union(profilesToAdd, profilesToRemove);
796        
797        if (updatedProfiles.size() > 0)
798        {
799            _notifyAclUpdated(_currentUserProvider.getUser(), object, updatedProfiles);
800        }
801    }
802    
803    private void _removeProfile(JCRProjectMember member, String profileId, AmetysObject aclObject)
804    {
805        if (MemberType.GROUP == member.getType())
806        {
807            _profileAssignmentStorageExtensionPoint.removeAllowedProfileFromGroup(member.getGroup(), profileId, aclObject);
808        }
809        else
810        {
811            _profileAssignmentStorageExtensionPoint.removeAllowedProfileFromUser(member.getUser(), profileId, aclObject);
812        }
813    }
814    
815    private void _addProfile(JCRProjectMember member, String profileId, AmetysObject aclObject)
816    {
817        if (MemberType.GROUP == member.getType())
818        {
819            _profileAssignmentStorageExtensionPoint.allowProfileToGroup(member.getGroup(), profileId, aclObject);
820        }
821        else
822        {
823            _profileAssignmentStorageExtensionPoint.allowProfileToUser(member.getUser(), profileId, aclObject);
824        }
825    }
826
827    private void _removeMemberProfiles(JCRProjectMember member, AmetysObject object)
828    {
829        Set<String> currentAllowedProfiles = _getAllowedProfile(member, object);
830        
831        for (String allowedProfile : currentAllowedProfiles)
832        {
833            _removeProfile(member, allowedProfile, object);
834        }
835        
836        if (currentAllowedProfiles.size() > 0)
837        {
838            ((ModifiableAmetysObject) object).saveChanges();
839            
840            Map<String, Object> eventParams = new HashMap<>();
841            eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_CONTEXT, object);
842            eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_CONTEXT_IDENTIFIER, object.getId());
843            eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_PROFILES, currentAllowedProfiles);
844            eventParams.put(org.ametys.cms.ObservationConstants.ARGS_ACL_SOLR_CACHE_UNINFLUENTIAL, true);
845            
846            _observationManager.notify(new Event(org.ametys.core.ObservationConstants.EVENT_ACL_UPDATED, _currentUserProvider.getUser(), eventParams));
847        }
848    }
849    
850    /**
851     * Get the current user information
852     * @return The user
853     */
854    @Callable
855    public Map<String, Object> getCurrentUser()
856    {
857        Map<String, Object> result = new HashMap<>();
858        result.put("user", _userHelper.user2json(_currentUserProvider.getUser()));
859        return result;
860    }
861    
862    /**
863     * Get the members of current project or all the members of all projects in where is no current project
864     * @return The members
865     */
866    @Callable
867    public Map<String, Object> getProjectMembers()
868    {
869        Map<String, Object> result = new HashMap<>();
870        
871        Request request = ContextHelper.getRequest(_context);
872        String projectName = (String) request.getAttribute("projectName");
873        
874        Collection<Project> projects = new ArrayList<>();
875        
876        if (StringUtils.isNotEmpty(projectName))
877        {
878            projects.add(_projectManager.getProject(projectName));
879        }
880        else
881        {
882            _projectManager.getProjects()
883                           .stream()
884                           .forEach(project -> projects.add(project));
885        }
886                                        
887        result.put("users", projects.stream()
888                                    .map(project -> getProjectMembers(project, true))
889                                    .flatMap(Set::stream)
890                                    .map(ProjectMember::getUser)
891                                    .distinct()
892                                    .map(user -> _userHelper.user2json(user, true))
893                                    .collect(Collectors.toList()));
894        
895        return result;
896    }
897    
898    /**
899     * Get the members of a project, sorted by managers, non empty role and name
900     * @param projectName the project's name
901     * @param lang the language to get user content
902     * @return the members of project
903     * @throws IllegalAccessException if an error occurred
904     * @throws AmetysRepositoryException if an error occurred
905     */
906    @Callable
907    public Map<String, Object> getProjectMembers(String projectName, String lang) throws IllegalAccessException, AmetysRepositoryException
908    {
909        return getProjectMembers(projectName, lang, false);
910    }
911    
912    /**
913     * Get the members of a project, sorted by managers, non empty role and name
914     * @param projectName the project's name
915     * @param lang the language to get user content
916     * @param expandGroup true if groups are expanded
917     * @return the members of project
918     * @throws AmetysRepositoryException if an error occurred
919     */
920    @Callable
921    public Map<String, Object> getProjectMembers(String projectName, String lang, boolean expandGroup) throws AmetysRepositoryException
922    {
923        Map<String, Object> result = new HashMap<>();
924        
925        Project project = _projectManager.getProject(projectName);
926        if (project == null)
927        {
928            result.put("message", "unknown-project");
929            result.put("success", false);
930            return result;
931        }
932        if (!_projectRightHelper.hasReadAccess(project))
933        {
934            throw new AccessDeniedException("User '" + _currentUserProvider.getUser() + "' tried to access a privilege feature without reader right in the project " + project.getPath());
935        }
936        
937        List<Map<String, Object>> membersData = new ArrayList<>();
938        
939        Set<ProjectMember> projectMembers = getProjectMembers(project, expandGroup);
940        
941        for (ProjectMember projectMember : projectMembers)
942        {
943            membersData.add(_member2Json(projectMember, lang));
944        }
945        
946        result.put("members", membersData);
947        result.put("success", true);
948        
949        return result;
950    }
951    
952    private Map<String, Object> _member2Json(ProjectMember projectMember, String lang)
953    {
954        Map<String, Object> memberData = new HashMap<>();
955        
956        memberData.put("type", projectMember.getType().name().toLowerCase());
957        memberData.put("title", projectMember.getTitle());
958        memberData.put("sortabletitle", projectMember.getSortableTitle());
959        memberData.put("manager", projectMember.isManager());
960        
961        String role = projectMember.getRole();
962        if (StringUtils.isNotEmpty(role))
963        {
964            memberData.put("role", role);
965        }
966        
967        User user = projectMember.getUser();
968        if (user != null)
969        {
970            memberData.put("id", UserIdentity.userIdentityToString(user.getIdentity()));
971            memberData.putAll(_userHelper.user2json(user));
972
973            Content userContent = getUserContent(lang, user);
974            
975            if (userContent != null)
976            {
977                if (userContent.hasValue("function"))
978                {
979                    memberData.put("function", userContent.getValue("function"));
980                }
981
982                if (userContent.hasValue("organisation-accronym"))
983                {
984                    memberData.put("organisationAcronym", userContent.getValue("organisation-accronym"));
985                }
986                String usersDirectorySiteName = _projectManager.getUsersDirectorySiteName();
987                String[] contentTypes = userContent.getTypes();
988                for (String contentType : contentTypes)
989                {
990                    // Try to see if a user page exists for this content type
991                    UserPage userPage = _userDirectoryPageResolver.getUserPage(userContent, usersDirectorySiteName, lang, contentType);
992                    if (userPage != null)
993                    {
994                        memberData.put("link", _uriResolver.getResolverForType("page").resolve(userPage.getId(), false, true, false));
995                    }
996                }
997                    
998            }
999            else if (getLogger().isDebugEnabled())
1000            {
1001                getLogger().debug("User content not found for user : " + user);
1002            }
1003        }
1004        
1005        Group group = projectMember.getGroup();
1006        if (group != null)
1007        {
1008            memberData.putAll(group2Json(group));
1009        }
1010        
1011        return memberData;
1012    }
1013
1014    /**
1015     * Get user content
1016     * @param lang the lang
1017     * @param user the user
1018     * @return the user content or null if no exist
1019     */
1020    public Content getUserContent(String lang, User user)
1021    {
1022        Content userContent = _userDirectoryHelper.getUserContent(user.getIdentity(), lang);
1023
1024        if (userContent == null)
1025        {
1026            userContent = _userDirectoryHelper.getUserContent(user.getIdentity(), "en");
1027        }
1028        
1029        if (userContent == null)
1030        {
1031            Map<String, Language> availableLanguages = _languagesManager.getAvailableLanguages();
1032            for (Language availableLanguage : availableLanguages.values())
1033            {
1034                if (userContent == null)
1035                {
1036                    userContent = _userDirectoryHelper.getUserContent(user.getIdentity(), availableLanguage.getCode());
1037                }
1038            }
1039        }
1040        return userContent;
1041    }
1042
1043    /**
1044     * Get the members of a project, sorted by managers, non empty role and name
1045     * @param project the project
1046     * @param expandGroup true to expand the user of a group
1047     * @return the members of project
1048     * @throws AmetysRepositoryException if an error occurred
1049     */
1050    public Set<ProjectMember> getProjectMembers(Project project, boolean expandGroup) throws AmetysRepositoryException
1051    {
1052        return getProjectMembers(project, expandGroup, Set.of());
1053    }
1054
1055    /**
1056     * Get the members of a project, sorted by managers, non empty role and name
1057     * @param project the project
1058     * @param expandGroup true to expand the user of a group
1059     * @param defaultSet default set to return when project has no site
1060     * @return the members of project
1061     * @throws AmetysRepositoryException if an error occurred
1062     */
1063    public Set<ProjectMember> getProjectMembers(Project project, boolean expandGroup, Set<ProjectMember> defaultSet) throws AmetysRepositoryException
1064    {
1065        Cache<ProjectMemberCacheKey, Set<ProjectMember>> cache = _getCache();
1066        
1067        ProjectMemberCacheKey cacheKey = ProjectMemberCacheKey.of(project.getId(), expandGroup);
1068        if (cache.hasKey(cacheKey))
1069        {
1070            Set<ProjectMember> projectMembers = cache.get(cacheKey);
1071            return projectMembers != null ? projectMembers : defaultSet;
1072        }
1073        else
1074        {
1075            Set<ProjectMember> projectMembers = _getProjectMembers(project, expandGroup);
1076            cache.put(cacheKey, projectMembers);
1077            return projectMembers != null ? projectMembers : defaultSet;
1078        }
1079    }
1080
1081    private Set<ProjectMember> _getProjectMembers(Project project, boolean expandGroup)
1082    {
1083        Comparator<ProjectMember> managerComparator = Comparator.comparing(m -> m.isManager() ? 0 : 1);
1084        Comparator<ProjectMember> roleComparator = Comparator.comparing(m -> StringUtils.isNotBlank(m.getRole()) ? 0 : 1);
1085        // Use sortable title for sort, and concatenate it with hash code of user, so that homonyms do not appear equals
1086        Comparator<ProjectMember> nameComparator = (m1, m2) -> (m1.getSortableTitle() + m1.hashCode()).compareToIgnoreCase(m2.getSortableTitle() + m2.hashCode());
1087        
1088        Set<ProjectMember> members = new TreeSet<>(managerComparator.thenComparing(roleComparator).thenComparing(nameComparator));
1089        
1090        Map<JCRProjectMember, Object> jcrMembers = getJCRProjectMembers(project);
1091        List<UserIdentity> managers = Arrays.asList(project.getManagers());
1092        
1093        Site site = project.getSite();
1094        if (site == null)
1095        {
1096            getLogger().error("Can not compute members in the project " + project.getName() + " because it can not be linked to an existing site");
1097            return null;
1098        }
1099        String projectSiteName = site.getName();
1100
1101        Set<String> projectGroupDirectory = _groupDirectoryContextHelper.getGroupDirectoriesOnContext("/sites/" + projectSiteName);
1102        
1103        for (Entry<JCRProjectMember, Object> entry : jcrMembers.entrySet())
1104        {
1105            JCRProjectMember jcrMember = entry.getKey();
1106            if (MemberType.USER == jcrMember.getType())
1107            {
1108                User user = (User) entry.getValue();
1109                boolean isManager = managers.contains(jcrMember.getUser());
1110                
1111                ProjectMember projectMember = new ProjectMember(user, jcrMember.getRole(), isManager);
1112                if (!members.add(projectMember) && _projectManager.isUserInProjectPopulations(project, user.getIdentity()))
1113                {
1114                    //if set already contains the user, override it (users always take over users' group)
1115                    members.remove(projectMember); // remove the one in  the set
1116                    members.add(projectMember); // add the new one
1117                }
1118            }
1119            else if (MemberType.GROUP == jcrMember.getType())
1120            {
1121                Group group = (Group) entry.getValue();
1122                if (projectGroupDirectory.contains(group.getGroupDirectory().getId()))
1123                {
1124                    if (expandGroup)
1125                    {
1126                        for (UserIdentity userIdentity : group.getUsers())
1127                        {
1128                            User user = _userManager.getUser(userIdentity);
1129                            if (user != null && _projectManager.isUserInProjectPopulations(project, userIdentity))
1130                            {
1131                                ProjectMember projectMember = new ProjectMember(user, null, false);
1132                                members.add(projectMember); // add if does not exist yet
1133                            }
1134                        }
1135                    }
1136                    else
1137                    {
1138                        // Add the member as group
1139                        members.add(new ProjectMember(group));
1140                    }
1141                }
1142            }
1143        }
1144        return members;
1145    }
1146    
1147    /**
1148     * Retrieves the rights for the current user in the project
1149     * @param projectName The project Name
1150     * @return The project
1151     */
1152    @Callable
1153    public Map<String, Object> getMemberModuleRights(String projectName)
1154    {
1155        Map<String, Object> results = new HashMap<>();
1156        Map<String, Object> rights = new HashMap<>();
1157        
1158        Project project = _projectManager.getProject(projectName);
1159        if (project == null)
1160        {
1161            results.put("message", "unknown-project");
1162            results.put("success", false);
1163        }
1164        else
1165        {
1166            rights.put("view", _projectRightHelper.canViewMembers(project));
1167            rights.put("add", _projectRightHelper.canAddMember(project));
1168            rights.put("edit", _projectRightHelper.canEditMember(project));
1169            rights.put("delete", _projectRightHelper.canRemoveMember(project));
1170            results.put("rights", rights);
1171            results.put("success", true);
1172        }
1173        
1174        return results;
1175    }
1176    
1177    /**
1178     * Get the list of users of the project
1179     * @param project The project
1180     * @return The list of users
1181     */
1182    public Map<JCRProjectMember, Object> getJCRProjectMembers(Project project)
1183    {
1184        Map<JCRProjectMember, Object> projectMembers = new HashMap<>();
1185        
1186        if (project != null)
1187        {
1188            ModifiableTraversableAmetysObject membersNode = _getProjectMembersNode(project);
1189            
1190            for (AmetysObject memberNode : membersNode.getChildren())
1191            {
1192                if (memberNode instanceof JCRProjectMember)
1193                {
1194                    JCRProjectMember jCRProjectMember = (JCRProjectMember) memberNode;
1195                    if (jCRProjectMember.getType() == MemberType.USER)
1196                    {
1197                        UserIdentity userIdentity = jCRProjectMember.getUser();
1198                        User user = _userManager.getUser(userIdentity);
1199                        if (user != null)
1200                        {
1201                            projectMembers.put((JCRProjectMember) memberNode, user);
1202                        }
1203                    }
1204                    else
1205                    {
1206                        GroupIdentity groupIdentity = jCRProjectMember.getGroup();
1207                        Group group = _groupManager.getGroup(groupIdentity);
1208                        if (group != null)
1209                        {
1210                            projectMembers.put((JCRProjectMember) memberNode, group);
1211                        }
1212                    }
1213                }
1214            }
1215        }
1216        
1217        return projectMembers;
1218    }
1219    
1220    /**
1221     * Test if an user is a member of a project (directly or by a group)
1222     * @param project The project
1223     * @param userIdentity The user identity
1224     * @return True if this user is a member of this project
1225     */
1226    public boolean isProjectMember(Project project, UserIdentity userIdentity)
1227    {
1228        return getProjectMember(project, userIdentity) != null;
1229    }
1230    
1231    /**
1232     * Retrieve the member of a project corresponding to the user identity
1233     * @param project The project
1234     * @param userIdentity The user identity
1235     * @return The member of this project, which can be of type "user" or "group", or null if the user is not in the project
1236     */
1237    public ProjectMember getProjectMember(Project project, UserIdentity userIdentity)
1238    {
1239        return getProjectMember(project, userIdentity, null);
1240    }
1241    
1242    /**
1243     * Retrieve the member of a project corresponding to the user identity
1244     * @param project The project
1245     * @param userIdentity The user identity
1246     * @param userGroups The user groups. If null the user's groups will be expanded.
1247     * @return The member of this project, which can be of type "user" or "group", or null if the user is not in the project
1248     */
1249    public ProjectMember getProjectMember(Project project, UserIdentity userIdentity, Set<GroupIdentity> userGroups)
1250    {
1251        if (userIdentity == null)
1252        {
1253            return null;
1254        }
1255                
1256        Set<ProjectMember> members = getProjectMembers(project, true);
1257        
1258        ProjectMember projectMember = members.stream()
1259                .filter(member -> MemberType.USER == member.getType())
1260                .filter(member -> userIdentity.equals(member.getUser().getIdentity()))
1261                .findFirst()
1262                .orElse(null);
1263        
1264        if (projectMember != null)
1265        {
1266            return projectMember;
1267        }
1268        
1269        Set<GroupIdentity> groups = userGroups == null ? _groupManager.getUserGroups(userIdentity) : userGroups; // get user's groups
1270
1271        if (!groups.isEmpty())
1272        {
1273            return members.stream()
1274                    .filter(member -> MemberType.GROUP == member.getType())
1275                    .filter(member -> groups.contains(member.getGroup().getIdentity()))
1276                    .findFirst()
1277                    .orElse(null);
1278        }
1279        
1280        return null;
1281    }
1282    
1283    /**
1284     * Set the manager of a project
1285     * @param projectName The project name
1286     * @param profileId The profile id to affect
1287     * @param managers The managers' user identity
1288     */
1289    public void setProjectManager(String projectName, String profileId, List<UserIdentity> managers)
1290    {
1291        Project project = _projectManager.getProject(projectName);
1292        if (project == null)
1293        {
1294            return;
1295        }
1296        
1297        project.setManagers(managers.toArray(new UserIdentity[managers.size()]));
1298        
1299        for (UserIdentity userIdentity : managers)
1300        {
1301            JCRProjectMember member = _getOrCreateJCRProjectMember(project, userIdentity);
1302            
1303            Set<String> allowedProfiles = Set.of(profileId);
1304            for (WorkspaceModule module : _projectManager.getModules(project))
1305            {
1306                setProfileOnModule(member, project, module, allowedProfiles);
1307            }
1308        }
1309        
1310        project.saveChanges();
1311        
1312        _getCache().invalidate(ProjectMemberCacheKey.of(project.getId(), null));
1313        
1314        // Clear rights manager cache (if I remove my own rights)
1315        _rightManager.clearCaches();
1316        
1317//        Request request = ContextHelper.getRequest(_context);
1318//        if (request != null)
1319//        {
1320//            request.removeAttribute(RightManager.CACHE_REQUEST_ATTRIBUTE_NAME);
1321//        }
1322    }
1323    
1324    private void _notifyAclUpdated(UserIdentity userIdentity, AmetysObject aclContext, Collection<String> aclProfiles)
1325    {
1326        Map<String, Object> eventParams = new HashMap<>();
1327        eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_CONTEXT, aclContext);
1328        eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_CONTEXT_IDENTIFIER, aclContext.getId());
1329        eventParams.put(org.ametys.core.ObservationConstants.ARGS_ACL_PROFILES, aclProfiles);
1330        eventParams.put(org.ametys.cms.ObservationConstants.ARGS_ACL_SOLR_CACHE_UNINFLUENTIAL, true);
1331        
1332        _observationManager.notify(new Event(org.ametys.core.ObservationConstants.EVENT_ACL_UPDATED, userIdentity, eventParams));
1333    }
1334
1335    /**
1336     * Retrieve or create a user in a project
1337     * @param project The project
1338     * @param userIdentity the user
1339     * @return The user
1340     */
1341    private JCRProjectMember _getOrCreateJCRProjectMember(Project project, UserIdentity userIdentity)
1342    {
1343        Predicate<? super AmetysObject> findMemberPredicate = memberNode -> MemberType.USER == ((JCRProjectMember) memberNode).getType()
1344                && userIdentity.equals(((JCRProjectMember) memberNode).getUser());
1345        JCRProjectMember projectMember = _getOrCreateJCRProjectMember(project, findMemberPredicate);
1346        
1347        if (projectMember.needsSave())
1348        {
1349            projectMember.setUser(userIdentity);
1350            projectMember.setType(MemberType.USER);
1351        }
1352        
1353        return projectMember;
1354    }
1355
1356    /**
1357     * Retrieve or create a group as a member in a project
1358     * @param project The project
1359     * @param groupIdentity the group
1360     * @return The user
1361     */
1362    private JCRProjectMember _getOrCreateJCRProjectMember(Project project, GroupIdentity groupIdentity)
1363    {
1364        Predicate<? super AmetysObject> findMemberPredicate = memberNode -> MemberType.GROUP == ((JCRProjectMember) memberNode).getType()
1365                && groupIdentity.equals(((JCRProjectMember) memberNode).getGroup());
1366        JCRProjectMember projectMember = _getOrCreateJCRProjectMember(project, findMemberPredicate);
1367        
1368        if (projectMember.needsSave())
1369        {
1370            projectMember.setGroup(groupIdentity);
1371            projectMember.setType(MemberType.GROUP);
1372        }
1373        
1374        return projectMember;
1375    }
1376    
1377    /**
1378     * Retrieve or create a member in a project
1379     * @param project The project
1380     * @param findMemberPredicate The predicate to find the member node
1381     * @return The member node. A new node is created if the member node was not found
1382     */
1383    protected JCRProjectMember _getOrCreateJCRProjectMember(Project project, Predicate<? super AmetysObject> findMemberPredicate)
1384    {
1385        ModifiableTraversableAmetysObject membersNode = _getProjectMembersNode(project);
1386        
1387        Optional<AmetysObject> member = _getProjectMembersNode(project).getChildren()
1388                                                                       .stream()
1389                                                                       .filter(memberNode -> memberNode instanceof JCRProjectMember)
1390                                                                       .filter(findMemberPredicate)
1391                                                                       .findFirst();
1392        if (member.isPresent())
1393        {
1394            return (JCRProjectMember) member.get();
1395        }
1396        
1397        String baseName = "member";
1398        String name = baseName;
1399        int index = 1;
1400        while (membersNode.hasChild(name))
1401        {
1402            index++;
1403            name = baseName + "-" + index;
1404        }
1405        
1406        JCRProjectMember jcrProjectMember = membersNode.createChild(name, __PROJECT_MEMBER_NODE_TYPE);
1407        
1408        // we invalidate the cache has we had to create a new user
1409        _getCache().invalidate(ProjectMemberCacheKey.of(project.getId(), null));
1410        
1411        return jcrProjectMember;
1412    }
1413
1414    
1415    /**
1416     * Remove a user from a project
1417     * @param projectName The project name
1418     * @param identity The identity of the user or group, who must be a member of the project
1419     * @param type The type of the member, user or group
1420     * @return The error code, if an error occurred
1421     */
1422    @Callable
1423    public Map<String, Object> removeMember(String projectName, String identity, String type)
1424    {
1425        return _removeMember(projectName, identity, type, true, true);
1426    }
1427    
1428    private Map<String, Object> _removeMember(String projectName, String identity, String type, boolean checkCurrentUser, boolean chekRights)
1429    {
1430        Map<String, Object> result = new HashMap<>();
1431        
1432        MemberType memberType = MemberType.valueOf(type.toUpperCase());
1433        boolean isTypeUser = MemberType.USER == memberType;
1434        boolean isTypeGroup = MemberType.GROUP == memberType;
1435        UserIdentity user = Optional.ofNullable(identity)
1436                                    .filter(id -> id != null && isTypeUser)
1437                                    .map(UserIdentity::stringToUserIdentity)
1438                                    .orElse(null);
1439        GroupIdentity group = Optional.ofNullable(identity)
1440                                      .filter(id -> id != null && isTypeGroup)
1441                                      .map(GroupIdentity::stringToGroupIdentity)
1442                                      .orElse(null);
1443        
1444        if (isTypeGroup && group == null
1445            || isTypeUser && user == null)
1446        {
1447            result.put("success", false);
1448            result.put("message", isTypeGroup ? "unknown-group" : "unknown-user");
1449            return result;
1450        }
1451        
1452        Project project = _projectManager.getProject(projectName);
1453        if (project == null)
1454        {
1455            result.put("success", false);
1456            result.put("message", "unknown-project");
1457            return result;
1458        }
1459        
1460        if (checkCurrentUser && _isCurrentUser(isTypeUser, user))
1461        {
1462            result.put("success", false);
1463            result.put("message", "current-user");
1464            return result;
1465        }
1466        
1467        // If there is only one manager, do not remove him from the project's members
1468        if (isTypeUser && isOnlyManager(project, user))
1469        {
1470            result.put("success", false);
1471            result.put("message", "only-manager");
1472            return result;
1473        }
1474        
1475        if (chekRights && !_projectRightHelper.canRemoveMember(project))
1476        {
1477            throw new AccessDeniedException("User '" + _currentUserProvider.getUser() + "' tried to remove member without convenient right [" + projectName + ", " + identity + "]");
1478        }
1479        
1480        JCRProjectMember projectMember = null;
1481        if (isTypeUser)
1482        {
1483            projectMember = _getProjectMember(project, user);
1484        }
1485        else if (isTypeGroup)
1486        {
1487            projectMember = _getProjectMember(project, group);
1488        }
1489        
1490        if (projectMember == null)
1491        {
1492            result.put("success", false);
1493            result.put("message", "unknown-member");
1494            return result;
1495        }
1496        
1497        _removeManager(project, isTypeUser, user);
1498        _removeMemberProfiles(project, projectMember);
1499
1500        projectMember.remove();
1501        project.saveChanges();
1502        
1503        _getCache().invalidate(ProjectMemberCacheKey.of(project.getId(), null));
1504        
1505        Map<String, Object> eventParams = new HashMap<>();
1506        eventParams.put(ObservationConstants.ARGS_MEMBER_IDENTITY, identity);
1507        eventParams.put(ObservationConstants.ARGS_MEMBER_IDENTITY_TYPE, memberType);
1508        eventParams.put(ObservationConstants.ARGS_PROJECT, project);
1509        eventParams.put(ObservationConstants.ARGS_PROJECT_ID, project.getId());
1510        _observationManager.notify(new Event(ObservationConstants.EVENT_MEMBER_DELETED, _currentUserProvider.getUser(), eventParams));
1511        
1512        result.put("success", true);
1513        return result;
1514    }
1515
1516    private boolean _isCurrentUser(boolean isTypeUser, UserIdentity user)
1517    {
1518        return isTypeUser && _currentUserProvider.getUser().equals(user);
1519    }
1520
1521    /**
1522     * Check if a user is the only manager of a project
1523     * @param project the project
1524     * @param user the user
1525     * @return true if the user is the only manager of the project
1526     */
1527    public boolean isOnlyManager(Project project, UserIdentity user)
1528    {
1529        UserIdentity[] managers = project.getManagers();
1530        return managers.length == 1 && managers[0].equals(user);
1531    }
1532
1533    private JCRProjectMember _getProjectMember(Project project, GroupIdentity group)
1534    {
1535        JCRProjectMember projectMember = null;
1536        ModifiableTraversableAmetysObject membersNode = _getProjectMembersNode(project);
1537        
1538        for (AmetysObject memberNode : membersNode.getChildren())
1539        {
1540            if (memberNode instanceof JCRProjectMember)
1541            {
1542                JCRProjectMember member = (JCRProjectMember) memberNode;
1543                if (MemberType.GROUP == member.getType() && group.equals(member.getGroup()))
1544                {
1545                    projectMember = (JCRProjectMember) memberNode;
1546                }
1547
1548            }
1549        }
1550        return projectMember;
1551    }
1552
1553    private JCRProjectMember _getProjectMember(Project project, UserIdentity user)
1554    {
1555        JCRProjectMember projectMember = null;
1556        ModifiableTraversableAmetysObject membersNode = _getProjectMembersNode(project);
1557        
1558        for (AmetysObject memberNode : membersNode.getChildren())
1559        {
1560            if (memberNode instanceof JCRProjectMember)
1561            {
1562                JCRProjectMember member = (JCRProjectMember) memberNode;
1563                if (MemberType.USER == member.getType() && user.equals(member.getUser()))
1564                {
1565                    projectMember = (JCRProjectMember) memberNode;
1566                }
1567            }
1568        }
1569        return projectMember;
1570    }
1571    
1572    private void _removeManager(Project project, boolean isTypeUser, UserIdentity user)
1573    {
1574        if (isTypeUser)
1575        {
1576            UserIdentity[] oldManagers = project.getManagers();
1577            
1578            // Remove the user from the project's managers
1579            UserIdentity[] managers = Arrays.stream(oldManagers)
1580                  .filter(manager -> !manager.equals(user))
1581                  .toArray(UserIdentity[]::new);
1582            
1583            project.setManagers(managers);
1584        }
1585    }
1586
1587    private void _removeMemberProfiles(Project project, JCRProjectMember projectMember)
1588    {
1589        for (WorkspaceModule module : _projectManager.getModules(project))
1590        {
1591            ModifiableResourceCollection moduleRootNode = module.getModuleRoot(project, false);
1592            _removeMemberProfiles(projectMember, moduleRootNode);
1593        }
1594    }
1595    
1596    /**
1597     * Retrieves the users node of the project
1598     * The users node will be created if necessary
1599     * @param project The project
1600     * @return The users node of the project
1601     */
1602    protected ModifiableTraversableAmetysObject _getProjectMembersNode(Project project)
1603    {
1604        if (project == null)
1605        {
1606            throw new AmetysRepositoryException("Error getting the project users node, project is null");
1607        }
1608        
1609        try
1610        {
1611            ModifiableTraversableAmetysObject membersNode;
1612            if (project.hasChild(__PROJECT_MEMBERS_NODE))
1613            {
1614                membersNode = project.getChild(__PROJECT_MEMBERS_NODE);
1615            }
1616            else
1617            {
1618                membersNode = project.createChild(__PROJECT_MEMBERS_NODE, __PROJECT_MEMBERS_NODE_TYPE);
1619            }
1620            
1621            return membersNode;
1622        }
1623        catch (AmetysRepositoryException e)
1624        {
1625            throw new AmetysRepositoryException("Error getting the project users node", e);
1626        }
1627    }
1628    
1629    /**
1630     * Get the JSON representation of a group
1631     * @param group The group
1632     * @return The group
1633     */
1634    protected Map<String, Object> group2Json(Group group)
1635    {
1636        Map<String, Object> infos = new HashMap<>();
1637        infos.put("id", GroupIdentity.groupIdentityToString(group.getIdentity()));
1638        infos.put("groupId", group.getIdentity().getId());
1639        infos.put("label", group.getLabel());
1640        infos.put("sortablename", group.getLabel());
1641        infos.put("groupDirectory", group.getIdentity().getDirectoryId());
1642        return infos;
1643    }
1644
1645    /**
1646     * Count the total of unique users in the project and in the project's group
1647     * @param project The project
1648     * @return The total of members
1649     */
1650    public Long getMembersCount(Project project)
1651    {
1652        Set<ProjectMember> projectMembers = getProjectMembers(project, true);
1653                
1654        return (long) projectMembers.size();
1655    }
1656
1657    /**
1658     * Get the users from a group that are part of the project. They can be filtered with a predicate
1659     * @param group The group
1660     * @param project The project
1661     * @param filteringPredicate The predicate to filter
1662     * @return The list of users
1663     */
1664    public List<User> getGroupUsersFromProject(Group group, Project project, BiPredicate<Project, UserIdentity> filteringPredicate)
1665    {
1666        Set<String> projectPopulations = _populationContextHelper.getUserPopulationsOnContexts(List.of("/sites/" + project.getSite().getName()), false, false);
1667        
1668        return group.getUsers().stream()
1669                .filter(user -> projectPopulations.contains(user.getPopulationId()))
1670                .filter(user -> filteringPredicate.test(project, user))
1671                .map(_userManager::getUser)
1672                .filter(Objects::nonNull)
1673                .collect(Collectors.toList());
1674    }
1675
1676    /**
1677     * Make the current user leave the project
1678     * @param projectName The project name
1679     * @return The error code, if an error occurred
1680     */
1681    @Callable
1682    public Map<String, Object> leaveProject(String projectName)
1683    {
1684        UserIdentity currentUser = _currentUserProvider.getUser();
1685        String identity = UserIdentity.userIdentityToString(currentUser);
1686        
1687        Map<String, Object> result = _removeMember(projectName, identity, MemberType.USER.toString(), false, false);
1688        
1689        Project project = _projectManager.getProject(projectName);
1690        result.put("project", _projectsCatalogueManager.detailedProject2json(project));
1691        
1692        return result;
1693    }
1694    
1695    private Cache<ProjectMemberCacheKey, Set<ProjectMember>> _getCache()
1696    {
1697        return _abstractCacheManager.get(__PROJECT_MEMBER_CACHE);
1698    }
1699    
1700    /**
1701     * This class represents a member of a project. Could be a user or a group
1702     *
1703     */
1704    public static class ProjectMember
1705    {
1706        private String _title;
1707        private String _sortableTitle;
1708        private MemberType _type;
1709        private String _role;
1710        private User _user;
1711        private Group _group;
1712        private boolean _isManager;
1713        
1714        /**
1715         * Create a project member as a group
1716         * @param group the group attached to this member. Cannot be null.
1717         */
1718        public ProjectMember(Group group)
1719        {
1720            _title = group.getLabel();
1721            _sortableTitle = group.getLabel();
1722            _type = MemberType.GROUP;
1723            _role = null;
1724            _isManager = false;
1725            _user = null;
1726            _group = group;
1727        }
1728        
1729        /**
1730         * Create a project member as a group
1731         * @param role the role
1732         * @param isManager true if the member is a manager of the project
1733         * @param user the user attached to this member. Cannot be null.
1734         */
1735        public ProjectMember(User user, String role, boolean isManager)
1736        {
1737            _title = user.getFullName();
1738            _sortableTitle = user.getSortableName();
1739            _type = MemberType.USER;
1740            _role = role;
1741            _isManager = isManager;
1742            _user = user;
1743            _group = null;
1744        }
1745        
1746        /**
1747         * Get the title of the member.
1748         * @return The title of the member
1749         */
1750        public String getTitle()
1751        {
1752            return _title;
1753        }
1754        
1755        /**
1756         * Get the sortable title of the member.
1757         * @return The sortable title of the member
1758         */
1759        public String getSortableTitle()
1760        {
1761            return _sortableTitle;
1762        }
1763
1764        /**
1765         * Get the type of the member. It can be a user or a group
1766         * @return The type of the member
1767         */
1768        public MemberType getType()
1769        {
1770            return _type;
1771        }
1772
1773        /**
1774         * Get the role of the member.
1775         * @return The role of the member
1776         */
1777        public String getRole()
1778        {
1779            return _role;
1780        }
1781
1782        /**
1783         * Test if the member is a manager of the project
1784         * @return True if this user is a manager of the project
1785         */
1786        public boolean isManager()
1787        {
1788            return _isManager;
1789        }
1790        
1791        /**
1792         * Get the user of the member.
1793         * @return The user of the member
1794         */
1795        public User getUser()
1796        {
1797            return _user;
1798        }
1799        
1800        /**
1801         * Get the group of the member.
1802         * @return The group of the member
1803         */
1804        public Group getGroup()
1805        {
1806            return _group;
1807        }
1808        
1809        @Override
1810        public boolean equals(Object obj)
1811        {
1812            if (obj == null || !(obj instanceof ProjectMember))
1813            {
1814                return false;
1815            }
1816            
1817            ProjectMember otherMember = (ProjectMember) obj;
1818            
1819            if (getType() != otherMember.getType())
1820            {
1821                return false;
1822            }
1823            
1824            if (getType() == MemberType.USER)
1825            {
1826                return getUser().equals(otherMember.getUser());
1827            }
1828            else
1829            {
1830                return getGroup().equals(otherMember.getGroup());
1831            }
1832        }
1833        
1834        @Override
1835        public int hashCode()
1836        {
1837            return getType() == MemberType.USER ? getUser().getIdentity().hashCode() : getGroup().getIdentity().hashCode();
1838        }
1839    }
1840    
1841    private static final class ProjectMemberCacheKey extends AbstractCacheKey
1842    {
1843        public ProjectMemberCacheKey(String projectId, Boolean extendGroup)
1844        {
1845            super(projectId, extendGroup);
1846        }
1847        
1848        public static ProjectMemberCacheKey of(String projectId, Boolean withExpandedGroup)
1849        {
1850            return new ProjectMemberCacheKey(projectId, withExpandedGroup);
1851        }
1852    }
1853}