001/*
002 *  Copyright 2024 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.web.usermanagement;
017
018import java.io.IOException;
019import java.io.InputStream;
020import java.nio.charset.StandardCharsets;
021import java.util.HashMap;
022import java.util.List;
023import java.util.Map;
024
025import org.apache.avalon.framework.component.Component;
026import org.apache.avalon.framework.context.Context;
027import org.apache.avalon.framework.context.ContextException;
028import org.apache.avalon.framework.context.Contextualizable;
029import org.apache.avalon.framework.service.ServiceException;
030import org.apache.avalon.framework.service.ServiceManager;
031import org.apache.avalon.framework.service.Serviceable;
032import org.apache.cocoon.components.ContextHelper;
033import org.apache.cocoon.environment.Request;
034import org.apache.cocoon.servlet.multipart.Part;
035import org.apache.cocoon.servlet.multipart.RejectedPart;
036import org.apache.commons.io.FilenameUtils;
037import org.apache.commons.io.IOUtils;
038import org.apache.commons.io.input.BOMInputStream;
039import org.quartz.SchedulerException;
040
041import org.ametys.core.cocoon.JSonReader;
042import org.ametys.core.right.RightManager;
043import org.ametys.core.right.RightManager.RightResult;
044import org.ametys.core.ui.Callable;
045import org.ametys.core.user.CurrentUserProvider;
046import org.ametys.core.user.UserIdentity;
047import org.ametys.plugins.core.schedule.Scheduler;
048import org.ametys.runtime.plugin.component.AbstractLogEnabled;
049import org.ametys.web.WebHelper;
050
051/**
052 * Import user to invit from a CSV or text file.
053 */
054public class ImportInvitations extends AbstractLogEnabled implements Serviceable, Component, Contextualizable
055{
056    /** Avalon Role */
057    public static final String ROLE = ImportInvitations.class.getName();
058    
059    private static final String[] _ALLOWED_EXTENSIONS = new String[] {"txt", "csv"};
060    private Scheduler _scheduler;
061    private CurrentUserProvider _currentUserProvider;
062    private Context _context;
063    private RightManager _rightManager;
064    
065    @Override
066    public void service(ServiceManager smanager) throws ServiceException
067    {
068        _scheduler = (Scheduler) smanager.lookup(Scheduler.ROLE);
069        _currentUserProvider = (CurrentUserProvider) smanager.lookup(CurrentUserProvider.ROLE);
070        _rightManager = (RightManager) smanager.lookup(RightManager.ROLE);
071    }
072    
073    public void contextualize(Context context) throws ContextException
074    {
075        _context = context;
076    }
077    
078    /**
079     * Do the import of the file
080     * @param populationAndUserDirectory The population and user directory
081     * @param resendInvitation booloean to know if we resend invitation
082     * @param part The file
083     * @return The result of the import
084     * @throws Exception If an error occurs
085     */
086    @Callable (rights = Callable.SKIP_BUILTIN_CHECK)
087    public Map doImport (String populationAndUserDirectory, boolean resendInvitation, Part part) throws Exception
088    {
089        // Check user can access this feature
090        _checkUserRight(List.of("Web_Rights_HandleInvitations", "Web_Rights_HandleInvitations_OwnPopulation"), "/${WorkspaceName}");
091        
092        Request request = ContextHelper.getRequest(_context);
093        Map<String, Object> result = new HashMap<>();
094        
095        if (part instanceof RejectedPart)
096        {
097            result = Map.of("success", false, "error", "rejected-file");
098            
099            request.setAttribute(JSonReader.OBJECT_TO_READ, result);
100            return result;
101        }
102        
103        String filename = (part != null) ? part.getFileName().toLowerCase() : null;
104        
105        if (!FilenameUtils.isExtension(filename, _ALLOWED_EXTENSIONS))
106        {
107            result = Map.of("success", false, "error", "invalid-extension");
108            request.setAttribute(JSonReader.OBJECT_TO_READ, result);
109            return result;
110        }
111        
112        try (InputStream fileIS = part.getInputStream(); 
113                BOMInputStream bomIS = new BOMInputStream(fileIS))
114        {
115            List<String> guestLines = IOUtils.readLines(bomIS, StandardCharsets.UTF_8);
116            
117            String[] split = populationAndUserDirectory.split("#");
118            String populationId = split[0];
119            String userDirectoryId = split[1];
120            String siteName = WebHelper.getSiteName(request);
121            
122            // Schedule the sending of invitations
123            _scheduler.scheduleJob(new SendInvitationsRunnable(guestLines, siteName, populationId, userDirectoryId, resendInvitation, _currentUserProvider.getUser()));
124            
125        }
126        catch (IOException e)
127        {
128            getLogger().error("Failed to read file to send invitations from CSV file", e);
129            
130            result = Map.of("success", false, "error", "file-error");
131            request.setAttribute(JSonReader.OBJECT_TO_READ, result);
132            
133            return result;
134        }
135        catch (SchedulerException e)
136        {
137            getLogger().error("An exception occurred while trying to schedule the sending of invitations", e);
138            
139            result = Map.of("success", false, "error", "schedule-error");
140            request.setAttribute(JSonReader.OBJECT_TO_READ, result);
141
142            return result;
143        }
144        
145        result.put("success", true);
146        request.setAttribute(JSonReader.OBJECT_TO_READ, result);
147        return result;
148    }
149    
150    /**
151     * Check if the user right to access the feature
152     * @param rightIds The rights ids
153     * @param context The rights context
154     * @throws IllegalStateException if the user has no right
155     */
156    protected void _checkUserRight(List<String> rightIds, String context) throws IllegalStateException
157    {
158        UserIdentity user = _currentUserProvider.getUser();
159        for (String rightId : rightIds)
160        {
161            if (_rightManager.hasRight(user, rightId, context) != RightResult.RIGHT_ALLOW)
162            {
163                getLogger().error("User '" + user + "' tried to access a privileged feature without convenient right. Should have right '" + rightId + "' on context '" + context + "'");
164                throw new IllegalStateException("You have no right to access this feature.");
165            }
166        }
167    }
168}