001/*
002 *  Copyright 2022 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 */
016
017package org.ametys.web;
018
019import java.io.File;
020import java.io.IOException;
021import java.io.InputStream;
022import java.lang.reflect.Array;
023import java.time.ZoneId;
024import java.time.ZonedDateTime;
025import java.time.chrono.IsoChronology;
026import java.time.format.DateTimeFormatter;
027import java.util.ArrayList;
028import java.util.HashMap;
029import java.util.List;
030import java.util.Map;
031import java.util.Optional;
032import java.util.stream.Collectors;
033
034import org.apache.avalon.framework.component.Component;
035import org.apache.avalon.framework.service.ServiceException;
036import org.apache.avalon.framework.service.ServiceManager;
037import org.apache.avalon.framework.service.Serviceable;
038import org.apache.cocoon.environment.Request;
039import org.apache.cocoon.servlet.multipart.Part;
040import org.apache.cocoon.servlet.multipart.PartOnDisk;
041import org.apache.cocoon.servlet.multipart.RejectedPart;
042import org.apache.commons.lang3.StringUtils;
043
044import org.ametys.cms.data.Binary;
045import org.ametys.cms.data.type.ModelItemTypeConstants;
046import org.ametys.cms.data.type.ResourceElementTypeHelper;
047import org.ametys.core.upload.Upload;
048import org.ametys.core.upload.UploadManager;
049import org.ametys.core.user.CurrentUserProvider;
050import org.ametys.core.user.UserIdentity;
051import org.ametys.plugins.repository.model.ViewHelper;
052import org.ametys.runtime.i18n.I18nizableText;
053import org.ametys.runtime.i18n.I18nizableTextParameter;
054import org.ametys.runtime.model.ElementDefinition;
055import org.ametys.runtime.model.ModelItem;
056import org.ametys.runtime.model.ModelViewItemGroup;
057import org.ametys.runtime.model.View;
058import org.ametys.runtime.model.ViewItemContainer;
059import org.ametys.runtime.model.type.DataContext;
060import org.ametys.runtime.model.type.ElementType;
061import org.ametys.runtime.plugin.component.AbstractLogEnabled;
062
063import com.google.common.collect.ArrayListMultimap;
064import com.google.common.collect.Multimap;
065
066/**
067 * Helper for creating and editing an ametys object from the submitted form
068 */
069public class FOAmetysObjectCreationHelper extends AbstractLogEnabled implements Serviceable, Component
070{
071    /** The component role. */
072    public static final String ROLE = FOAmetysObjectCreationHelper.class.getName();
073    
074    /** The upload manager */
075    protected UploadManager _uploadManager;
076
077    /** The current user provider */
078    protected CurrentUserProvider _currentUserProvider;
079    
080    /** The analyse of files for virus helper */
081    protected AnalyseFileForVirusHelper _analyseFileForVirusHelper;
082
083    @Override
084    public void service(ServiceManager manager) throws ServiceException
085    {
086        _uploadManager = (UploadManager) manager.lookup(UploadManager.ROLE);
087        _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
088        _analyseFileForVirusHelper = (AnalyseFileForVirusHelper) manager.lookup(AnalyseFileForVirusHelper.ROLE);
089    }
090
091    /**
092     * Get values from the request
093     * @param request the request
094     * @param viewItemContainer the view item container fo the ametys object
095     * @param prefix the prefix
096     * @param errors the errors
097     * @return the map of values
098     */
099    public Map<String, Object> getFormValues(Request request, ViewItemContainer viewItemContainer, String prefix, Multimap<String, I18nizableText> errors)
100    {
101        Map<String, Object> values = new HashMap<>();
102        
103        ViewHelper.visitView(viewItemContainer, 
104            (element, definition) -> {
105                // simple element
106                String name = definition.getName();
107                String dataPath = prefix + name;
108                _getElementValue(request, definition, dataPath, errors)
109                    .ifPresent(value -> values.put(name, value));
110            }, 
111            (group, definition) -> {
112                // composite
113                String name = definition.getName();
114                String updatedPrefix = prefix + name + ModelItem.ITEM_PATH_SEPARATOR;
115
116                values.put(name, getFormValues(request, group, updatedPrefix, errors));
117            }, 
118            (group, definition) -> {
119                // repeater
120                String name = definition.getName();
121                String dataPath = prefix + name;
122                
123                List<Map<String, Object>> entries = _getRepeaterEntries(request, group, dataPath, errors);
124                values.put(name, entries);
125            }, 
126            group -> {
127                values.putAll(getFormValues(request, group, prefix, errors));   
128            });
129
130        return values;
131    }
132    
133    /**
134     * Get the value from the request of given element
135     * @param request the request
136     * @param definition the definition of the given element
137     * @param dataPath the data path
138     * @param errors the errors
139     * @return the element values if exist
140     */
141    protected Optional<? extends Object> _getElementValue(Request request, ElementDefinition definition, String dataPath, Multimap<String, I18nizableText> errors)
142    {
143        Optional<? extends Object> value = Optional.empty();
144
145        String fieldName = StringUtils.replace(dataPath, ModelItem.ITEM_PATH_SEPARATOR, ".");
146        Object valueFromRequest = request.get(fieldName);
147        
148        if (valueFromRequest != null)
149        {
150            if (definition.isMultiple())
151            {
152                @SuppressWarnings("unchecked")
153                List<Object> multipleValue = valueFromRequest instanceof List
154                        ? (List<Object>) valueFromRequest
155                        : List.of(valueFromRequest);
156                
157                List<? extends Object> valuesAsList = multipleValue.stream()
158                        .map(v -> _getTypedValue(v, definition, dataPath, errors))
159                        .filter(Optional::isPresent)
160                        .map(Optional::get)
161                        .collect(Collectors.toList());
162                
163                value = Optional.of(valuesAsList.toArray((Object[]) Array.newInstance(definition.getType().getManagedClass(), valuesAsList.size())));
164            }
165            else
166            {
167                Object singleValue;
168                if (valueFromRequest instanceof List)
169                {
170                    @SuppressWarnings("unchecked")
171                    List<Object> valuesFromRequest = (List<Object>) valueFromRequest;
172                    singleValue = valuesFromRequest.isEmpty() ? null : valuesFromRequest.get(0);
173                }
174                else
175                {
176                    singleValue = valueFromRequest;
177                }
178                value = _getTypedValue(singleValue, definition, dataPath, errors);
179            }
180        }
181        
182        return value;
183    }
184    
185    /**
186     * Get the typed value from object
187     * @param formValue the object value
188     * @param definition the definition of the given element
189     * @param dataPath the data path
190     * @param errors the errors
191     * @return the typed value if exist
192     */
193    protected Optional<? extends Object> _getTypedValue(Object formValue, ElementDefinition definition, String dataPath, Multimap<String, I18nizableText> errors)
194    {
195        try
196        {
197            Optional<? extends Object> value;
198            ElementType type = definition.getType();
199            if (ModelItemTypeConstants.FILE_ELEMENT_TYPE_ID.equals(type.getId()) || ModelItemTypeConstants.BINARY_ELEMENT_TYPE_ID.equals(type.getId()))
200            {
201                value = _getUploadFileValue((Part) formValue, dataPath, errors);
202            }
203            else if (org.ametys.runtime.model.type.ModelItemTypeConstants.BOOLEAN_TYPE_ID.equals(type.getId()))
204            {
205                value = Optional.of("on".equals(formValue) || "true".equals(formValue));
206            }
207            else if (org.ametys.runtime.model.type.ModelItemTypeConstants.DATETIME_TYPE_ID.equals(type.getId()))
208            {
209                if (StringUtils.isNotBlank((String) formValue))
210                {
211                    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm")
212                            .withZone(ZoneId.systemDefault())
213                            .withChronology(IsoChronology.INSTANCE);
214                    
215                    value = Optional.of(ZonedDateTime.parse((String) formValue, formatter));
216                }
217                else
218                {
219                    value = Optional.empty();
220                }
221            }
222            else if (ModelItemTypeConstants.USER_ELEMENT_TYPE_ID.equals(type.getId()))
223            {
224                if (formValue instanceof String userIdentityAsString)
225                {
226                    value = Optional.ofNullable(UserIdentity.stringToUserIdentity(userIdentityAsString));
227                }
228                else
229                {
230                    value = Optional.ofNullable(type.fromJSONForClient(formValue, DataContext.newInstance().withDataPath(dataPath)));
231                }
232            }
233            else
234            {
235                value = Optional.ofNullable(type.fromJSONForClient(formValue, DataContext.newInstance().withDataPath(dataPath)));
236            }
237            
238            return value;
239        }
240        catch (Exception e)
241        {
242            Map<String, I18nizableTextParameter> i18nParams = new HashMap<>();
243            i18nParams.put("value", new I18nizableText(formValue.toString()));
244            i18nParams.put("datapath", new I18nizableText(dataPath.toString()));
245            errors.put(dataPath, new I18nizableText("plugin.web", "PLUGINS_WEB_FO_HELPER_GET_TYPED_VALUE_ERROR"));
246            getLogger().error("Unable to get typed value " + formValue + " at path" + dataPath, e);
247            return Optional.empty();
248        }
249    }
250    
251    /**
252     * Get the repeater entries from the request
253     * @param request the request
254     * @param viewItem the view item
255     * @param dataPath the data path
256     * @param errors the errors
257     * @return list of repeater entries
258     */
259    protected List<Map<String, Object>> _getRepeaterEntries(Request request, ModelViewItemGroup viewItem, String dataPath, Multimap<String, I18nizableText> errors)
260    {
261        List<Map<String, Object>> entries = new ArrayList<>();
262
263        String fieldName = StringUtils.replace(dataPath, ModelItem.ITEM_PATH_SEPARATOR, ".");
264        int repeaterSize = Optional.ofNullable(request.getParameter(fieldName + ".size"))
265                .map(Integer::valueOf)
266                .orElse(0);
267        
268        for (int position = 1; position <= repeaterSize; position++)
269        {
270            String updatedPrefix = dataPath + "[" + position + "]" + ModelItem.ITEM_PATH_SEPARATOR;
271            entries.add(getFormValues(request, viewItem, updatedPrefix, errors));
272        }
273        
274        return entries;
275    }
276    
277    /**
278     * Get the uploaded file value
279     * @param partUploaded the uploaded part
280     * @param dataPath the data path
281     * @param errors the errors
282     * @return the file binary if exist
283     */
284    protected Optional<Binary> _getUploadFileValue(Part partUploaded, String dataPath, Multimap<String, I18nizableText> errors)
285    {
286        // Checks if the part is a RejectedPart
287        if (partUploaded == null || partUploaded instanceof RejectedPart)
288        {
289            errors.put(dataPath, new I18nizableText("plugin.web", "PLUGINS_WEB_FO_HELPER_UPLOAD_FILE_ERROR"));
290            return Optional.empty();
291        }
292        
293        // Get the file from the part on disk
294        PartOnDisk uploadedFilePart = (PartOnDisk) partUploaded;
295        File file = uploadedFilePart.getFile();
296        
297        // Analyse the file for viruses
298        if (!_analyseFileForVirusHelper.analysefile(file.getAbsolutePath()))
299        {
300            errors.put(dataPath, new I18nizableText("plugin.web", "PLUGINS_WEB_ERROR_FILE_INFECTED"));
301            return Optional.empty();
302        }
303        
304        // If the file is not infected, continue with the upload
305        try (InputStream is = partUploaded.getInputStream())
306        {
307            Upload upload = _uploadManager.storeUpload(_currentUserProvider.getUser(), partUploaded.getFileName(), is);
308            return Optional.of(ResourceElementTypeHelper.binaryFromUpload(upload));
309        }
310        catch (IOException e)
311        {
312            getLogger().error("Unable to store uploaded file: " + partUploaded, e);
313            errors.put(dataPath, new I18nizableText("plugin.web", "PLUGINS_WEB_FO_HELPER_UPLOAD_FILE_ERROR"));
314            return Optional.empty();
315        }
316    }
317    
318    /**
319     * Validate the given values
320     * @param values the values to validate
321     * @param view the view of the ametys object
322     * @return The errors if some values are not valid
323     */
324    public Multimap<String, I18nizableText> validateValues(Map<String, Object> values, View view)
325    {
326        Multimap<String, I18nizableText> errors = ArrayListMultimap.create();
327
328        Map<String, List<I18nizableText>> errorsAsMap = ViewHelper.validateValues(view, Optional.ofNullable(values));
329        for (String dataPath : errorsAsMap.keySet())
330        {
331            errors.putAll(dataPath, errorsAsMap.get(dataPath));
332        }
333        
334        return errors;
335    }
336}