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", "unknow-group");
265                result.put("success", false);
266                return result;
267            }
268            else if (isTypeUser && user == null)
269            {
270                result.put("message", "unknow-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", "unknow-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", "unknow-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 ? "unknow-group" : "unknow-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", "unknow-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", "unknow-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        Map<String, Object> result = new HashMap<>();
1421        
1422        MemberType memberType = MemberType.valueOf(type.toUpperCase());
1423        boolean isTypeUser = MemberType.USER == memberType;
1424        boolean isTypeGroup = MemberType.GROUP == memberType;
1425        UserIdentity user = Optional.ofNullable(identity)
1426                                    .filter(id -> id != null && isTypeUser)
1427                                    .map(UserIdentity::stringToUserIdentity)
1428                                    .orElse(null);
1429        GroupIdentity group = Optional.ofNullable(identity)
1430                                      .filter(id -> id != null && isTypeGroup)
1431                                      .map(GroupIdentity::stringToGroupIdentity)
1432                                      .orElse(null);
1433        
1434        if (isTypeGroup && group == null
1435            || isTypeUser && user == null)
1436        {
1437            result.put("success", false);
1438            result.put("message", isTypeGroup ? "unknow-group" : "unknow-user");
1439            return result;
1440        }
1441        
1442        Project project = _projectManager.getProject(projectName);
1443        if (project == null)
1444        {
1445            result.put("success", false);
1446            result.put("message", "unknow-project");
1447            return result;
1448        }
1449        
1450        if (_isCurrentUser(isTypeUser, user))
1451        {
1452            result.put("success", false);
1453            result.put("message", "current-user");
1454            return result;
1455        }
1456        
1457        // If there is only one manager, do not remove him from the project's members
1458        if (_isOnlyManager(project, isTypeUser, user))
1459        {
1460            result.put("success", false);
1461            result.put("message", "only-manager");
1462            return result;
1463        }
1464        
1465        if (!_projectRightHelper.canRemoveMember(project))
1466        {
1467            throw new AccessDeniedException("User '" + _currentUserProvider.getUser() + "' tried to remove member without convenient right [" + projectName + ", " + identity + "]");
1468        }
1469        
1470        JCRProjectMember projectMember = null;
1471        if (isTypeUser)
1472        {
1473            projectMember = _getProjectMember(project, user);
1474        }
1475        else if (isTypeGroup)
1476        {
1477            projectMember = _getProjectMember(project, group);
1478        }
1479        
1480        if (projectMember == null)
1481        {
1482            result.put("success", false);
1483            result.put("message", "unknow-member");
1484            return result;
1485        }
1486        
1487        _removeManager(project, isTypeUser, user);
1488        _removeMemberProfiles(project, projectMember);
1489
1490        projectMember.remove();
1491        project.saveChanges();
1492        
1493        _getCache().invalidate(ProjectMemberCacheKey.of(project.getId(), null));
1494        
1495        Map<String, Object> eventParams = new HashMap<>();
1496        eventParams.put(ObservationConstants.ARGS_MEMBER_IDENTITY, identity);
1497        eventParams.put(ObservationConstants.ARGS_MEMBER_IDENTITY_TYPE, memberType);
1498        eventParams.put(ObservationConstants.ARGS_PROJECT, project);
1499        eventParams.put(ObservationConstants.ARGS_PROJECT_ID, project.getId());
1500        _observationManager.notify(new Event(ObservationConstants.EVENT_MEMBER_DELETED, _currentUserProvider.getUser(), eventParams));
1501        
1502        result.put("success", true);
1503        return result;
1504    }
1505
1506    private boolean _isCurrentUser(boolean isTypeUser, UserIdentity user)
1507    {
1508        return isTypeUser && _currentUserProvider.getUser().equals(user);
1509    }
1510
1511    private boolean _isOnlyManager(Project project, boolean isTypeUser, UserIdentity user)
1512    {
1513        UserIdentity[] managers = project.getManagers();
1514        return isTypeUser && managers.length == 1 && managers[0].equals(user);
1515    }
1516
1517    private JCRProjectMember _getProjectMember(Project project, GroupIdentity group)
1518    {
1519        JCRProjectMember projectMember = null;
1520        ModifiableTraversableAmetysObject membersNode = _getProjectMembersNode(project);
1521        
1522        for (AmetysObject memberNode : membersNode.getChildren())
1523        {
1524            if (memberNode instanceof JCRProjectMember)
1525            {
1526                JCRProjectMember member = (JCRProjectMember) memberNode;
1527                if (MemberType.GROUP == member.getType() && group.equals(member.getGroup()))
1528                {
1529                    projectMember = (JCRProjectMember) memberNode;
1530                }
1531
1532            }
1533        }
1534        return projectMember;
1535    }
1536
1537    private JCRProjectMember _getProjectMember(Project project, UserIdentity user)
1538    {
1539        JCRProjectMember projectMember = null;
1540        ModifiableTraversableAmetysObject membersNode = _getProjectMembersNode(project);
1541        
1542        for (AmetysObject memberNode : membersNode.getChildren())
1543        {
1544            if (memberNode instanceof JCRProjectMember)
1545            {
1546                JCRProjectMember member = (JCRProjectMember) memberNode;
1547                if (MemberType.USER == member.getType() && user.equals(member.getUser()))
1548                {
1549                    projectMember = (JCRProjectMember) memberNode;
1550                }
1551            }
1552        }
1553        return projectMember;
1554    }
1555    
1556    private void _removeManager(Project project, boolean isTypeUser, UserIdentity user)
1557    {
1558        if (isTypeUser)
1559        {
1560            UserIdentity[] oldManagers = project.getManagers();
1561            
1562            // Remove the user from the project's managers
1563            UserIdentity[] managers = Arrays.stream(oldManagers)
1564                  .filter(manager -> !manager.equals(user))
1565                  .toArray(UserIdentity[]::new);
1566            
1567            project.setManagers(managers);
1568        }
1569    }
1570
1571    private void _removeMemberProfiles(Project project, JCRProjectMember projectMember)
1572    {
1573        for (WorkspaceModule module : _projectManager.getModules(project))
1574        {
1575            ModifiableResourceCollection moduleRootNode = module.getModuleRoot(project, false);
1576            _removeMemberProfiles(projectMember, moduleRootNode);
1577        }
1578    }
1579    
1580    /**
1581     * Retrieves the users node of the project
1582     * The users node will be created if necessary
1583     * @param project The project
1584     * @return The users node of the project
1585     */
1586    protected ModifiableTraversableAmetysObject _getProjectMembersNode(Project project)
1587    {
1588        if (project == null)
1589        {
1590            throw new AmetysRepositoryException("Error getting the project users node, project is null");
1591        }
1592        
1593        try
1594        {
1595            ModifiableTraversableAmetysObject membersNode;
1596            if (project.hasChild(__PROJECT_MEMBERS_NODE))
1597            {
1598                membersNode = project.getChild(__PROJECT_MEMBERS_NODE);
1599            }
1600            else
1601            {
1602                membersNode = project.createChild(__PROJECT_MEMBERS_NODE, __PROJECT_MEMBERS_NODE_TYPE);
1603            }
1604            
1605            return membersNode;
1606        }
1607        catch (AmetysRepositoryException e)
1608        {
1609            throw new AmetysRepositoryException("Error getting the project users node", e);
1610        }
1611    }
1612    
1613    /**
1614     * Get the JSON representation of a group
1615     * @param group The group
1616     * @return The group
1617     */
1618    protected Map<String, Object> group2Json(Group group)
1619    {
1620        Map<String, Object> infos = new HashMap<>();
1621        infos.put("id", GroupIdentity.groupIdentityToString(group.getIdentity()));
1622        infos.put("groupId", group.getIdentity().getId());
1623        infos.put("label", group.getLabel());
1624        infos.put("sortablename", group.getLabel());
1625        infos.put("groupDirectory", group.getIdentity().getDirectoryId());
1626        return infos;
1627    }
1628
1629    /**
1630     * Count the total of unique users in the project and in the project's group
1631     * @param project The project
1632     * @return The total of members
1633     */
1634    public Long getMembersCount(Project project)
1635    {
1636        Set<ProjectMember> projectMembers = getProjectMembers(project, true);
1637                
1638        return (long) projectMembers.size();
1639    }
1640
1641    /**
1642     * Get the users from a group that are part of the project. They can be filtered with a predicate
1643     * @param group The group
1644     * @param project The project
1645     * @param filteringPredicate The predicate to filter
1646     * @return The list of users
1647     */
1648    public List<User> getGroupUsersFromProject(Group group, Project project, BiPredicate<Project, UserIdentity> filteringPredicate)
1649    {
1650        Set<String> projectPopulations = _populationContextHelper.getUserPopulationsOnContexts(List.of("/sites/" + project.getSite().getName()), false, false);
1651        
1652        return group.getUsers().stream()
1653                .filter(user -> projectPopulations.contains(user.getPopulationId()))
1654                .filter(user -> filteringPredicate.test(project, user))
1655                .map(_userManager::getUser)
1656                .filter(Objects::nonNull)
1657                .collect(Collectors.toList());
1658    }
1659    
1660    private Cache<ProjectMemberCacheKey, Set<ProjectMember>> _getCache()
1661    {
1662        return _abstractCacheManager.get(__PROJECT_MEMBER_CACHE);
1663    }
1664    
1665    /**
1666     * This class represents a member of a project. Could be a user or a group
1667     *
1668     */
1669    public static class ProjectMember
1670    {
1671        private String _title;
1672        private String _sortableTitle;
1673        private MemberType _type;
1674        private String _role;
1675        private User _user;
1676        private Group _group;
1677        private boolean _isManager;
1678        
1679        /**
1680         * Create a project member as a group
1681         * @param group the group attached to this member. Cannot be null.
1682         */
1683        public ProjectMember(Group group)
1684        {
1685            _title = group.getLabel();
1686            _sortableTitle = group.getLabel();
1687            _type = MemberType.GROUP;
1688            _role = null;
1689            _isManager = false;
1690            _user = null;
1691            _group = group;
1692        }
1693        
1694        /**
1695         * Create a project member as a group
1696         * @param role the role
1697         * @param isManager true if the member is a manager of the project
1698         * @param user the user attached to this member. Cannot be null.
1699         */
1700        public ProjectMember(User user, String role, boolean isManager)
1701        {
1702            _title = user.getFullName();
1703            _sortableTitle = user.getSortableName();
1704            _type = MemberType.USER;
1705            _role = role;
1706            _isManager = isManager;
1707            _user = user;
1708            _group = null;
1709        }
1710        
1711        /**
1712         * Get the title of the member.
1713         * @return The title of the member
1714         */
1715        public String getTitle()
1716        {
1717            return _title;
1718        }
1719        
1720        /**
1721         * Get the sortable title of the member.
1722         * @return The sortable title of the member
1723         */
1724        public String getSortableTitle()
1725        {
1726            return _sortableTitle;
1727        }
1728
1729        /**
1730         * Get the type of the member. It can be a user or a group
1731         * @return The type of the member
1732         */
1733        public MemberType getType()
1734        {
1735            return _type;
1736        }
1737
1738        /**
1739         * Get the role of the member.
1740         * @return The role of the member
1741         */
1742        public String getRole()
1743        {
1744            return _role;
1745        }
1746
1747        /**
1748         * Test if the member is a manager of the project
1749         * @return True if this user is a manager of the project
1750         */
1751        public boolean isManager()
1752        {
1753            return _isManager;
1754        }
1755        
1756        /**
1757         * Get the user of the member.
1758         * @return The user of the member
1759         */
1760        public User getUser()
1761        {
1762            return _user;
1763        }
1764        
1765        /**
1766         * Get the group of the member.
1767         * @return The group of the member
1768         */
1769        public Group getGroup()
1770        {
1771            return _group;
1772        }
1773        
1774        @Override
1775        public boolean equals(Object obj)
1776        {
1777            if (obj == null || !(obj instanceof ProjectMember))
1778            {
1779                return false;
1780            }
1781            
1782            ProjectMember otherMember = (ProjectMember) obj;
1783            
1784            if (getType() != otherMember.getType())
1785            {
1786                return false;
1787            }
1788            
1789            if (getType() == MemberType.USER)
1790            {
1791                return getUser().equals(otherMember.getUser());
1792            }
1793            else
1794            {
1795                return getGroup().equals(otherMember.getGroup());
1796            }
1797        }
1798        
1799        @Override
1800        public int hashCode()
1801        {
1802            return getType() == MemberType.USER ? getUser().getIdentity().hashCode() : getGroup().getIdentity().hashCode();
1803        }
1804    }
1805    
1806    private static final class ProjectMemberCacheKey extends AbstractCacheKey
1807    {
1808        public ProjectMemberCacheKey(String projectId, Boolean extendGroup)
1809        {
1810            super(projectId, extendGroup);
1811        }
1812        
1813        public static ProjectMemberCacheKey of(String projectId, Boolean withExpandedGroup)
1814        {
1815            return new ProjectMemberCacheKey(projectId, withExpandedGroup);
1816        }
1817    }
1818}