001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.wicket.protocol.http.servlet; 018 019import java.io.IOException; 020import java.io.InputStream; 021import java.io.UnsupportedEncodingException; 022import java.util.ArrayList; 023import java.util.Collection; 024import java.util.HashMap; 025import java.util.List; 026import java.util.Map; 027 028import javax.servlet.ServletException; 029import javax.servlet.http.HttpServletRequest; 030import javax.servlet.http.Part; 031 032import org.apache.commons.fileupload.FileItem; 033import org.apache.commons.fileupload.FileItemFactory; 034import org.apache.commons.fileupload.FileUploadBase; 035import org.apache.commons.fileupload.FileUploadException; 036import org.apache.commons.fileupload.disk.DiskFileItemFactory; 037import org.apache.commons.fileupload.servlet.ServletFileUpload; 038import org.apache.commons.fileupload.servlet.ServletRequestContext; 039import org.apache.commons.io.FileCleaningTracker; 040import org.apache.wicket.Application; 041import org.apache.wicket.WicketRuntimeException; 042import org.apache.wicket.util.file.FileCleanerTrackerAdapter; 043import org.apache.wicket.util.file.IFileCleaner; 044import org.apache.wicket.util.lang.Args; 045import org.apache.wicket.util.lang.Bytes; 046import org.apache.wicket.util.string.StringValue; 047import org.apache.wicket.util.value.ValueMap; 048 049/** 050 * Servlet specific WebRequest subclass for multipart content uploads. 051 * 052 * @author Jonathan Locke 053 * @author Eelco Hillenius 054 * @author Cameron Braid 055 * @author Ate Douma 056 * @author Igor Vaynberg (ivaynberg) 057 */ 058public class MultipartServletWebRequestImpl extends MultipartServletWebRequest 059{ 060 /** Map of file items. */ 061 private final Map<String, List<FileItem>> files; 062 063 /** Map of parameters. */ 064 private final ValueMap parameters; 065 066 private final String upload; 067 private final FileItemFactory fileItemFactory; 068 069 /** 070 * total bytes uploaded (downloaded from server's pov) so far. used for upload notifications 071 */ 072 private int bytesUploaded; 073 074 /** content length cache, used for upload notifications */ 075 private int totalBytes; 076 077 /** 078 * Constructor. 079 * 080 * This constructor will use {@link DiskFileItemFactory} to store uploads. 081 * 082 * @param request 083 * the servlet request 084 * @param filterPrefix 085 * prefix to wicket filter mapping 086 * @param maxSize 087 * the maximum size allowed for this request 088 * @param upload 089 * upload identifier for {@link UploadInfo} 090 * @throws FileUploadException 091 * Thrown if something goes wrong with upload 092 */ 093 public MultipartServletWebRequestImpl(HttpServletRequest request, String filterPrefix, 094 Bytes maxSize, String upload) throws FileUploadException 095 { 096 this(request, filterPrefix, maxSize, upload, new DiskFileItemFactory() 097 { 098 @Override 099 public FileCleaningTracker getFileCleaningTracker() 100 { 101 IFileCleaner fileCleaner = Application.get() 102 .getResourceSettings() 103 .getFileCleaner(); 104 return new FileCleanerTrackerAdapter(fileCleaner); 105 } 106 }); 107 } 108 109 /** 110 * Constructor 111 * 112 * @param request 113 * the servlet request 114 * @param filterPrefix 115 * prefix to wicket filter mapping 116 * @param maxSize 117 * the maximum size allowed for this request 118 * @param upload 119 * upload identifier for {@link UploadInfo} 120 * @param factory 121 * {@link DiskFileItemFactory} to use when creating file items used to represent 122 * uploaded files 123 * @throws FileUploadException 124 * Thrown if something goes wrong with upload 125 */ 126 public MultipartServletWebRequestImpl(HttpServletRequest request, String filterPrefix, 127 Bytes maxSize, String upload, FileItemFactory factory) throws FileUploadException 128 { 129 super(request, filterPrefix); 130 131 Args.notNull(upload, "upload"); 132 this.upload = upload; 133 this.fileItemFactory = factory; 134 parameters = new ValueMap(); 135 files = new HashMap<>(); 136 137 // Check that request is multipart 138 final boolean isMultipart = ServletFileUpload.isMultipartContent(request); 139 if (!isMultipart) 140 { 141 throw new IllegalStateException( 142 "ServletRequest does not contain multipart content. One possible solution is to explicitly call Form.setMultipart(true), Wicket tries its best to auto-detect multipart forms but there are certain situation where it cannot."); 143 } 144 145 setMaxSize(maxSize); 146 } 147 148 @Override 149 public void parseFileParts() throws FileUploadException 150 { 151 HttpServletRequest request = getContainerRequest(); 152 153 // The encoding that will be used to decode the string parameters 154 // It should NOT be null at this point, but it may be 155 // especially if the older Servlet API 2.2 is used 156 String encoding = request.getCharacterEncoding(); 157 158 // The encoding can also be null when using multipart/form-data encoded forms. 159 // In that case we use the [application-encoding] which we always demand using 160 // the attribute 'accept-encoding' in wicket forms. 161 if (encoding == null) 162 { 163 encoding = Application.get().getRequestCycleSettings().getResponseRequestEncoding(); 164 } 165 166 FileUploadBase fileUpload = newFileUpload(encoding); 167 168 List<FileItem> items; 169 170 if (wantUploadProgressUpdates()) 171 { 172 ServletRequestContext ctx = new ServletRequestContext(request) 173 { 174 @Override 175 public InputStream getInputStream() throws IOException 176 { 177 return new CountingInputStream(super.getInputStream()); 178 } 179 }; 180 totalBytes = request.getContentLength(); 181 182 onUploadStarted(totalBytes); 183 try 184 { 185 items = fileUpload.parseRequest(ctx); 186 } 187 finally 188 { 189 onUploadCompleted(); 190 } 191 } 192 else 193 { 194 // try to parse the file uploads by using Apache Commons FileUpload APIs 195 // because they are feature richer (e.g. progress updates, cleaner) 196 items = fileUpload.parseRequest(new ServletRequestContext(request)); 197 if (items.isEmpty()) 198 { 199 // fallback to Servlet 3.0 APIs 200 items = readServlet3Parts(request); 201 } 202 } 203 204 // Loop through items 205 for (final FileItem item : items) 206 { 207 // Get next item 208 // If item is a form field 209 if (item.isFormField()) 210 { 211 // Set parameter value 212 final String value; 213 if (encoding != null) 214 { 215 try 216 { 217 value = item.getString(encoding); 218 } 219 catch (UnsupportedEncodingException e) 220 { 221 throw new WicketRuntimeException(e); 222 } 223 } 224 else 225 { 226 value = item.getString(); 227 } 228 229 addParameter(item.getFieldName(), value); 230 } 231 else 232 { 233 List<FileItem> fileItems = files.get(item.getFieldName()); 234 if (fileItems == null) 235 { 236 fileItems = new ArrayList<>(); 237 files.put(item.getFieldName(), fileItems); 238 } 239 // Add to file list 240 fileItems.add(item); 241 } 242 } 243 } 244 245 /** 246 * Reads the uploads' parts by using Servlet 3.0 APIs. 247 * 248 * <strong>Note</strong>: By using Servlet 3.0 APIs the application won't be able to use 249 * upload progress updates. 250 * 251 * @param request 252 * The http request with the upload data 253 * @return A list of {@link FileItem}s 254 * @throws FileUploadException 255 */ 256 private List<FileItem> readServlet3Parts(HttpServletRequest request) throws FileUploadException 257 { 258 List<FileItem> itemsFromParts = new ArrayList<>(); 259 try 260 { 261 Collection<Part> parts = request.getParts(); 262 if (parts != null) 263 { 264 for (Part part : parts) 265 { 266 FileItem fileItem = new ServletPartFileItem(part); 267 itemsFromParts.add(fileItem); 268 } 269 } 270 } catch (IOException | ServletException e) 271 { 272 throw new FileUploadException("An error occurred while reading the upload parts", e); 273 } 274 return itemsFromParts; 275 } 276 277 /** 278 * Factory method for creating new instances of FileUploadBase 279 * 280 * @param encoding 281 * The encoding to use while reading the data 282 * @return A new instance of FileUploadBase 283 */ 284 protected FileUploadBase newFileUpload(String encoding) { 285 // Configure the factory here, if desired. 286 ServletFileUpload fileUpload = new ServletFileUpload(fileItemFactory); 287 288 // set encoding specifically when we found it 289 if (encoding != null) 290 { 291 fileUpload.setHeaderEncoding(encoding); 292 } 293 294 fileUpload.setSizeMax(getMaxSize().bytes()); 295 296 Bytes fileMaxSize = getFileMaxSize(); 297 if (fileMaxSize != null) { 298 fileUpload.setFileSizeMax(fileMaxSize.bytes()); 299 } 300 301 fileUpload.setFileCountMax(getFileCountMax()); 302 303 return fileUpload; 304 } 305 306 /** 307 * Adds a parameter to the parameters value map 308 * 309 * @param name 310 * parameter name 311 * @param value 312 * parameter value 313 */ 314 private void addParameter(final String name, final String value) 315 { 316 final String[] currVal = (String[])parameters.get(name); 317 318 String[] newVal; 319 320 if (currVal != null) 321 { 322 newVal = new String[currVal.length + 1]; 323 System.arraycopy(currVal, 0, newVal, 0, currVal.length); 324 newVal[currVal.length] = value; 325 } 326 else 327 { 328 newVal = new String[] { value }; 329 330 } 331 332 parameters.put(name, newVal); 333 } 334 335 /** 336 * @return Returns the files. 337 */ 338 @Override 339 public Map<String, List<FileItem>> getFiles() 340 { 341 return files; 342 } 343 344 /** 345 * Gets the file that was uploaded using the given field name. 346 * 347 * @param fieldName 348 * the field name that was used for the upload 349 * @return the upload with the given field name 350 */ 351 @Override 352 public List<FileItem> getFile(final String fieldName) 353 { 354 return files.get(fieldName); 355 } 356 357 @Override 358 protected Map<String, List<StringValue>> generatePostParameters() 359 { 360 Map<String, List<StringValue>> res = new HashMap<>(); 361 for (Map.Entry<String, Object> entry : parameters.entrySet()) 362 { 363 String key = entry.getKey(); 364 String[] val = (String[])entry.getValue(); 365 if (val != null && val.length > 0) 366 { 367 List<StringValue> items = new ArrayList<>(); 368 for (String s : val) 369 { 370 items.add(StringValue.valueOf(s)); 371 } 372 res.put(key, items); 373 } 374 } 375 return res; 376 } 377 378 /** 379 * Subclasses that want to receive upload notifications should return true. By default it takes 380 * the value from {@link org.apache.wicket.settings.ApplicationSettings#isUploadProgressUpdatesEnabled()}. 381 * 382 * @return true if upload status update event should be invoked 383 */ 384 protected boolean wantUploadProgressUpdates() 385 { 386 return Application.get().getApplicationSettings().isUploadProgressUpdatesEnabled(); 387 } 388 389 /** 390 * Upload start callback 391 * 392 * @param totalBytes 393 */ 394 protected void onUploadStarted(int totalBytes) 395 { 396 UploadInfo info = new UploadInfo(totalBytes); 397 398 setUploadInfo(getContainerRequest(), upload, info); 399 } 400 401 /** 402 * Upload status update callback 403 * 404 * @param bytesUploaded 405 * @param total 406 */ 407 protected void onUploadUpdate(int bytesUploaded, int total) 408 { 409 HttpServletRequest request = getContainerRequest(); 410 UploadInfo info = getUploadInfo(request, upload); 411 if (info == null) 412 { 413 throw new IllegalStateException( 414 "could not find UploadInfo object in session which should have been set when uploaded started"); 415 } 416 info.setBytesUploaded(bytesUploaded); 417 418 setUploadInfo(request, upload, info); 419 } 420 421 /** 422 * Upload completed callback 423 */ 424 protected void onUploadCompleted() 425 { 426 clearUploadInfo(getContainerRequest(), upload); 427 } 428 429 /** 430 * An {@link InputStream} that updates total number of bytes read 431 * 432 * @author Igor Vaynberg (ivaynberg) 433 */ 434 private class CountingInputStream extends InputStream 435 { 436 437 private final InputStream in; 438 439 /** 440 * Constructs a new CountingInputStream. 441 * 442 * @param in 443 * InputStream to delegate to 444 */ 445 public CountingInputStream(InputStream in) 446 { 447 this.in = in; 448 } 449 450 /** 451 * @see java.io.InputStream#read() 452 */ 453 @Override 454 public int read() throws IOException 455 { 456 int read = in.read(); 457 bytesUploaded += (read < 0) ? 0 : 1; 458 onUploadUpdate(bytesUploaded, totalBytes); 459 return read; 460 } 461 462 /** 463 * @see java.io.InputStream#read(byte[]) 464 */ 465 @Override 466 public int read(byte[] b) throws IOException 467 { 468 int read = in.read(b); 469 bytesUploaded += (read < 0) ? 0 : read; 470 onUploadUpdate(bytesUploaded, totalBytes); 471 return read; 472 } 473 474 /** 475 * @see java.io.InputStream#read(byte[], int, int) 476 */ 477 @Override 478 public int read(byte[] b, int off, int len) throws IOException 479 { 480 int read = in.read(b, off, len); 481 bytesUploaded += (read < 0) ? 0 : read; 482 onUploadUpdate(bytesUploaded, totalBytes); 483 return read; 484 } 485 486 } 487 488 @Override 489 public MultipartServletWebRequest newMultipartWebRequest(Bytes maxSize, String upload) 490 throws FileUploadException 491 { 492 // FIXME mgrigorov: Why these checks are made here ?! 493 // Why they are not done also at org.apache.wicket.protocol.http.servlet.MultipartServletWebRequestImpl.newMultipartWebRequest(org.apache.wicket.util.lang.Bytes, java.lang.String, org.apache.wicket.util.upload.FileItemFactory)() ? 494 // Why there is no check that the summary of all files' sizes is less than the set maxSize ? 495 // Setting a breakpoint here never breaks with the standard upload examples. 496 497 Bytes fileMaxSize = getFileMaxSize(); 498 for (Map.Entry<String, List<FileItem>> entry : files.entrySet()) 499 { 500 List<FileItem> fileItems = entry.getValue(); 501 for (FileItem fileItem : fileItems) 502 { 503 if (fileMaxSize != null && fileItem.getSize() > fileMaxSize.bytes()) 504 { 505 String fieldName = entry.getKey(); 506 FileUploadException fslex = new FileUploadBase.FileSizeLimitExceededException("The field '" + 507 fieldName + "' exceeds its maximum permitted size of '" + 508 maxSize + "' characters.", fileItem.getSize(), fileMaxSize.bytes()); 509 throw fslex; 510 } 511 } 512 } 513 return this; 514 } 515 516 @Override 517 public MultipartServletWebRequest newMultipartWebRequest(Bytes maxSize, String upload, FileItemFactory factory) 518 throws FileUploadException 519 { 520 return this; 521 } 522 523 private static final String SESSION_KEY = MultipartServletWebRequestImpl.class.getName(); 524 525 private static String getSessionKey(String upload) 526 { 527 return SESSION_KEY + ":" + upload; 528 } 529 530 /** 531 * Retrieves {@link UploadInfo} from session, null if not found. 532 * 533 * @param req 534 * http servlet request, not null 535 * @param upload 536 * upload identifier 537 * @return {@link UploadInfo} object from session, or null if not found 538 */ 539 public static UploadInfo getUploadInfo(final HttpServletRequest req, String upload) 540 { 541 Args.notNull(req, "req"); 542 return (UploadInfo)req.getSession().getAttribute(getSessionKey(upload)); 543 } 544 545 /** 546 * Sets the {@link UploadInfo} object into session. 547 * 548 * @param req 549 * http servlet request, not null 550 * @param upload 551 * upload identifier 552 * @param uploadInfo 553 * {@link UploadInfo} object to be put into session, not null 554 */ 555 public static void setUploadInfo(final HttpServletRequest req, String upload, 556 final UploadInfo uploadInfo) 557 { 558 Args.notNull(req, "req"); 559 Args.notNull(upload, "upload"); 560 Args.notNull(uploadInfo, "uploadInfo"); 561 req.getSession().setAttribute(getSessionKey(upload), uploadInfo); 562 } 563 564 /** 565 * Clears the {@link UploadInfo} object from session if one exists. 566 * 567 * @param req 568 * http servlet request, not null 569 * @param upload 570 * upload identifier 571 */ 572 public static void clearUploadInfo(final HttpServletRequest req, String upload) 573 { 574 Args.notNull(req, "req"); 575 Args.notNull(upload, "upload"); 576 req.getSession().removeAttribute(getSessionKey(upload)); 577 } 578 579}