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