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}