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