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