001/* 002 Licensed to the Apache Software Foundation (ASF) under one 003 or more contributor license agreements. See the NOTICE file 004 distributed with this work for additional information 005 regarding copyright ownership. The ASF licenses this file 006 to you under the Apache License, Version 2.0 (the 007 "License"); you may not use this file except in compliance 008 with the License. You may obtain a copy of the License at 009 010 http://www.apache.org/licenses/LICENSE-2.0 011 012 Unless required by applicable law or agreed to in writing, 013 software distributed under the License is distributed on an 014 "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 KIND, either express or implied. See the License for the 016 specific language governing permissions and limitations 017 under the License. 018 */ 019package org.apache.wiki.attachment; 020 021import java.io.File; 022import java.io.IOException; 023import java.io.InputStream; 024import java.io.OutputStream; 025import java.net.SocketException; 026import java.nio.charset.StandardCharsets; 027import java.security.Permission; 028import java.security.Principal; 029import java.util.ArrayList; 030import java.util.List; 031import java.util.Properties; 032 033import org.apache.commons.fileupload2.core.DiskFileItemFactory; 034import org.apache.commons.fileupload2.core.FileItem; 035import org.apache.commons.fileupload2.core.FileItemFactory; 036import org.apache.commons.fileupload2.core.FileUploadException; 037import org.apache.commons.fileupload2.core.ProgressListener; 038import org.apache.commons.fileupload2.jakarta.servlet6.JakartaServletFileUpload; 039import org.apache.logging.log4j.LogManager; 040import org.apache.logging.log4j.Logger; 041import org.apache.wiki.api.core.Attachment; 042import org.apache.wiki.api.core.Context; 043import org.apache.wiki.api.core.ContextEnum; 044import org.apache.wiki.api.core.Engine; 045import org.apache.wiki.api.core.Page; 046import org.apache.wiki.api.core.Session; 047import org.apache.wiki.api.exceptions.ProviderException; 048import org.apache.wiki.api.exceptions.RedirectException; 049import org.apache.wiki.api.exceptions.WikiException; 050import org.apache.wiki.api.providers.WikiProvider; 051import org.apache.wiki.api.spi.Wiki; 052import org.apache.wiki.auth.AuthorizationManager; 053import org.apache.wiki.auth.permissions.PermissionFactory; 054import org.apache.wiki.i18n.InternationalizationManager; 055import org.apache.wiki.preferences.Preferences; 056import org.apache.wiki.ui.progress.ProgressItem; 057import org.apache.wiki.ui.progress.ProgressManager; 058import org.apache.wiki.util.HttpUtil; 059import org.apache.wiki.util.TextUtil; 060 061import jakarta.servlet.ServletConfig; 062import jakarta.servlet.ServletContext; 063import jakarta.servlet.ServletException; 064import jakarta.servlet.ServletInputStream; 065import jakarta.servlet.http.HttpServlet; 066import jakarta.servlet.http.HttpServletRequest; 067import jakarta.servlet.http.HttpServletResponse; 068import org.apache.commons.fileupload2.core.FileUploadByteCountLimitException; 069import java.util.ResourceBundle; 070import org.apache.wiki.tags.MaxUploadTag; 071 072 073/** 074 * This is the chief JSPWiki attachment management servlet. It is used for 075 * both uploading new content and downloading old content. It can handle 076 * most common cases, e.g. check for modifications and return 304's as necessary. 077 * <p> 078 * Authentication is done using JSPWiki's normal AAA framework. 079 * <p> 080 * This servlet is also capable of managing dynamically created attachments. 081 * 082 * 083 * @since 1.9.45. 084 */ 085public class AttachmentServlet extends HttpServlet { 086 087 private static final long serialVersionUID = 3257282552187531320L; 088 private static final int BUFFER_SIZE = 8192; 089 090 private Engine m_engine; 091 private static final Logger LOG = LogManager.getLogger( AttachmentServlet.class ); 092 private static final String HDR_VERSION = "version"; 093 094 /** The maximum size that an attachment can be. */ 095 private int m_maxSize = Integer.MAX_VALUE; 096 097 /** List of attachment types which are allowed */ 098 private String[] m_allowedPatterns; 099 private String[] m_forbiddenPatterns; 100 101 // 102 // Not static as DateFormat objects are not thread safe. 103 // Used to handle the RFC date format = Sat, 13 Apr 2002 13:23:01 GMT 104 // 105 //private final DateFormat rfcDateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z"); 106 107 /** 108 * Initializes the servlet from Engine properties. 109 * @param config 110 * @throws jakarta.servlet.ServletException 111 */ 112 @Override 113 public void init( final ServletConfig config ) throws ServletException { 114 m_engine = Wiki.engine().find( config ); 115 final Properties props = m_engine.getWikiProperties(); 116 final String tmpDir = m_engine.getWorkDir() + File.separator + "attach-tmp"; 117 final String allowed = TextUtil.getStringProperty( props, AttachmentManager.PROP_ALLOWEDEXTENSIONS, null ); 118 m_maxSize = TextUtil.getIntegerProperty( props, AttachmentManager.PROP_MAXSIZE, Integer.MAX_VALUE ); 119 120 if( allowed != null && !allowed.isEmpty() ) { 121 m_allowedPatterns = allowed.toLowerCase().split( "\\s" ); 122 } else { 123 m_allowedPatterns = new String[ 0 ]; 124 } 125 126 final String forbidden = TextUtil.getStringProperty( props, AttachmentManager.PROP_FORBIDDENEXTENSIONS,null ); 127 if( forbidden != null && !forbidden.isEmpty() ) { 128 m_forbiddenPatterns = forbidden.toLowerCase().split("\\s"); 129 } else { 130 m_forbiddenPatterns = new String[0]; 131 } 132 133 final File f = new File( tmpDir ); 134 if( !f.exists() ) { 135 f.mkdirs(); 136 } else if( !f.isDirectory() ) { 137 LOG.fatal( "A file already exists where the temporary dir is supposed to be: {}. Please remove it.", tmpDir ); 138 } 139 140 LOG.debug( "UploadServlet initialized. Using {} for temporary storage.", tmpDir ); 141 } 142 143 private boolean isTypeAllowed( String name ) 144 { 145 if( name == null || name.isEmpty() ) return false; 146 147 name = name.toLowerCase(); 148 149 for( final String m_forbiddenPattern : m_forbiddenPatterns ) { 150 if( name.endsWith( m_forbiddenPattern ) && !m_forbiddenPattern.isEmpty() ) 151 return false; 152 } 153 154 for( final String m_allowedPattern : m_allowedPatterns ) { 155 if( name.endsWith( m_allowedPattern ) && !m_allowedPattern.isEmpty() ) 156 return true; 157 } 158 159 return m_allowedPatterns.length == 0; 160 } 161 162 /** 163 * Implements the OPTIONS method. 164 * 165 * @param req The servlet request 166 * @param res The servlet response 167 */ 168 169 @Override 170 protected void doOptions( final HttpServletRequest req, final HttpServletResponse res ) { 171 res.setHeader( "Allow", "GET, PUT, POST, OPTIONS, PROPFIND, PROPPATCH, MOVE, COPY, DELETE"); 172 res.setStatus( HttpServletResponse.SC_OK ); 173 } 174 175 /** 176 * Serves a GET with two parameters: 'wikiname' specifying the wikiname 177 * of the attachment, 'version' specifying the version indicator. 178 * 179 */ 180 // FIXME: Messages would need to be localized somehow. 181 @Override 182 public void doGet( final HttpServletRequest req, final HttpServletResponse res ) throws IOException { 183 final Context context = Wiki.context().create( m_engine, req, ContextEnum.PAGE_ATTACH.getRequestContext() ); 184 final AttachmentManager mgr = m_engine.getManager( AttachmentManager.class ); 185 final AuthorizationManager authmgr = m_engine.getManager( AuthorizationManager.class ); 186 final String version = req.getParameter( HDR_VERSION ); 187 final String nextPage = req.getParameter( "nextpage" ); 188 final String page = context.getPage().getName(); 189 int ver = WikiProvider.LATEST_VERSION; 190 191 if( page == null ) { 192 LOG.info( "Invalid attachment name." ); 193 res.sendError( HttpServletResponse.SC_BAD_REQUEST ); 194 return; 195 } 196 197 try( final OutputStream out = res.getOutputStream() ) { 198 LOG.debug("Attempting to download att "+page+", version "+version); 199 if( version != null ) { 200 ver = Integer.parseInt( version ); 201 } 202 203 final Attachment att = mgr.getAttachmentInfo( page, ver ); 204 if( att != null ) { 205 // 206 // Check if the user has permission for this attachment 207 // 208 209 final Permission permission = PermissionFactory.getPagePermission( att, "view" ); 210 if( !authmgr.checkPermission( context.getWikiSession(), permission ) ) { 211 LOG.debug("User does not have permission for this"); 212 res.sendError( HttpServletResponse.SC_FORBIDDEN ); 213 return; 214 } 215 216 // 217 // Check if the client already has a version of this attachment. 218 // 219 if( HttpUtil.checkFor304( req, att.getName(), att.getLastModified() ) ) { 220 LOG.debug( "Client has latest version already, sending 304..." ); 221 res.sendError( HttpServletResponse.SC_NOT_MODIFIED ); 222 return; 223 } 224 225 final String mimetype = getMimeType( context, att.getFileName() ); 226 res.setContentType( mimetype ); 227 228 final String contentDisposition = getContentDisposition( att ); 229 res.addHeader( "Content-Disposition", contentDisposition ); 230 res.addDateHeader("Last-Modified",att.getLastModified().getTime()); 231 232 if( !att.isCacheable() ) { 233 res.addHeader( "Pragma", "no-cache" ); 234 res.addHeader( "Cache-control", "no-cache" ); 235 } 236 237 // If a size is provided by the provider, report it. 238 if( att.getSize() >= 0 ) { 239 // LOG.info("size:"+att.getSize()); 240 res.setContentLength( (int)att.getSize() ); 241 } 242 243 try( final InputStream in = mgr.getAttachmentStream( context, att ) ) { 244 int read; 245 final byte[] buffer = new byte[ BUFFER_SIZE ]; 246 247 while( ( read = in.read( buffer ) ) > -1 ) { 248 out.write( buffer, 0, read ); 249 } 250 } 251 LOG.debug( "Attachment {} sent to {} on {}", att.getFileName(), req.getRemoteUser(), HttpUtil.getRemoteAddress(req) ); 252 if( nextPage != null ) { 253 res.sendRedirect( 254 validateNextPage( 255 TextUtil.urlEncodeUTF8(nextPage), 256 m_engine.getURL( ContextEnum.WIKI_ERROR.getRequestContext(), "", null ) 257 ) 258 ); 259 } 260 261 } else { 262 final String msg = "Attachment '" + page + "', version " + ver + " does not exist."; 263 LOG.info( msg ); 264 res.sendError( HttpServletResponse.SC_NOT_FOUND, msg ); 265 } 266 } catch( final ProviderException pe ) { 267 LOG.warn("Provider failed while reading", pe); 268 // 269 // This might fail, if the response is already committed. So in that 270 // case we just log it. 271 // 272 final ResourceBundle rb = ResourceBundle.getBundle( InternationalizationManager.CORE_BUNDLE, req.getLocale() ); 273 sendError( res, rb.getString("operation.failed") ); 274 } catch( final NumberFormatException nfe ) { 275 LOG.warn( "Invalid version number: " + version ); 276 res.sendError( HttpServletResponse.SC_BAD_REQUEST, "Invalid version number" ); 277 } catch( final SocketException se ) { 278 // 279 // These are very common in download situations due to aggressive 280 // clients. No need to try and send an error. 281 // 282 LOG.debug( "I/O exception during download", se ); 283 } catch( final IOException ioe ) { 284 // 285 // Client dropped the connection or something else happened. 286 // We don't know where the error came from, so we'll at least 287 // try to send an error and catch it quietly if it doesn't quite work. 288 // 289 LOG.debug( "I/O exception during download", ioe ); 290 final ResourceBundle rb = ResourceBundle.getBundle( InternationalizationManager.CORE_BUNDLE, req.getLocale() ); 291 sendError( res, rb.getString("operation.failed") ); 292 } 293 } 294 295 String getContentDisposition( final Attachment att ) { 296 // We use 'inline' instead of 'attachment' so that user agents can try to automatically open the file, 297 // except those cases in which we want to enforce the file download. 298 String contentDisposition = "inline; filename=\""; 299 if( m_engine.getManager( AttachmentManager.class ).forceDownload( att.getFileName() ) ) { 300 contentDisposition = "attachment; filename=\""; 301 } 302 contentDisposition += att.getFileName() + "\";"; 303 return contentDisposition; 304 } 305 306 void sendError( final HttpServletResponse res, final String message ) throws IOException { 307 try { 308 res.sendError( HttpServletResponse.SC_INTERNAL_SERVER_ERROR, message ); 309 } catch( final IllegalStateException e ) { 310 LOG.debug(e.getMessage(), e); 311 } 312 } 313 314 /** 315 * Returns the mime type for this particular file. Case does not matter. 316 * 317 * @param ctx WikiContext; required to access the ServletContext of the request. 318 * @param fileName The name to check for. 319 * @return A valid mime type, or application/binary, if not recognized 320 */ 321 private static String getMimeType( final Context ctx, final String fileName ) { 322 String mimetype = null; 323 324 final HttpServletRequest req = ctx.getHttpRequest(); 325 if( req != null ) { 326 final ServletContext s = req.getSession().getServletContext(); 327 328 if( s != null ) { 329 mimetype = s.getMimeType( fileName.toLowerCase() ); 330 } 331 } 332 333 if( mimetype == null ) { 334 mimetype = "application/binary"; 335 } 336 337 return mimetype; 338 } 339 340 341 /** 342 * Grabs mime/multipart data and stores it into the temporary area. 343 * Uses other parameters to determine which name to store as. 344 * 345 * <p>The input to this servlet is generated by an HTML FORM with 346 * two parts. The first, named 'page', is the WikiName identifier 347 * for the parent file. The second, named 'content', is the binary 348 * content of the file. 349 * 350 */ 351 @Override 352 public void doPost( final HttpServletRequest req, final HttpServletResponse res ) throws IOException { 353 try { 354 final String nextPage = upload( req ); 355 req.getSession().removeAttribute("msg"); 356 res.sendRedirect( nextPage ); 357 } catch( final RedirectException e ) { 358 final Session session = Wiki.session().find( m_engine, req ); 359 session.addMessage( e.getMessage() ); 360 //drain the request body 361 try (ServletInputStream inputStream = req.getInputStream();) { 362 int data; 363 while ((data = inputStream.read()) != -1) { 364 //we are just reading the stream to the end 365 } 366 } catch (Exception err) { 367 //ignore it 368 LOG.debug(e.getMessage(), e); 369 } 370 371 372 req.getSession().setAttribute("msg", e.getMessage()); 373 res.sendRedirect( e.getRedirect() ); 374 } 375 } 376 377 /** 378 * Validates the next page to be on the same server as this webapp. 379 * Fixes [JSPWIKI-46]. 380 */ 381 private String validateNextPage( String nextPage, final String errorPage ) { 382 if( nextPage.contains( "://" ) ) { 383 // It's an absolute link, so unless it starts with our address, we'll log an error. 384 if( !nextPage.startsWith( m_engine.getBaseURL() ) ) { 385 LOG.warn("Detected phishing attempt by redirecting to an unsecure location: "+nextPage); 386 nextPage = errorPage; 387 } 388 } 389 390 return nextPage; 391 } 392 393 /** 394 * Uploads a specific mime multipart input set, intercepts exceptions. 395 * 396 * If the total request size is too big, the user will be redirected to the Error.jsp page 397 * 398 * @param req The servlet request 399 * @return The page to which we should go next. 400 * @throws RedirectException If there's an error and a redirection is needed 401 * @throws IOException If upload fails 402 */ 403 protected String upload( final HttpServletRequest req ) throws RedirectException, IOException { 404 final String msg; 405 final String attName = "(unknown)"; 406 final String errorPage = m_engine.getURL( ContextEnum.WIKI_ERROR.getRequestContext(), "", null ); // If something bad happened, Upload should be able to take care of most stuff 407 String nextPage = errorPage; 408 final String progressId = req.getParameter( "progressid" ); 409 410 // Check that we have a file upload request 411 if( !JakartaServletFileUpload.isMultipartContent(req) ) { 412 throw new RedirectException( "Not a file upload", nextPage ); 413 } 414 415 try { 416 final FileItemFactory factory = DiskFileItemFactory.builder().get(); 417 418 // Create the context _before_ Multipart operations, otherwise strict servlet containers may fail when setting encoding. 419 final Context context = Wiki.context().create( m_engine, req, ContextEnum.PAGE_ATTACH.getRequestContext() ); 420 final UploadListener pl = new UploadListener(); 421 422 m_engine.getManager( ProgressManager.class ).startProgress( pl, progressId ); 423 424 if (req.getContentLengthLong() > m_maxSize) { 425 //we don't want total upload size to be larger than the max 426 //this is to prevent resource exhaustion 427 //TODO i18n this error message 428 throw new RedirectException("Request too big " + 429 MaxUploadTag.humanReadableByteCountBin(req.getContentLengthLong()) + " vs " + 430 MaxUploadTag.humanReadableByteCountBin(m_maxSize), errorPage +"?Error=true"); 431 } 432 final JakartaServletFileUpload upload = new JakartaServletFileUpload( factory ); 433 upload.setHeaderCharset(StandardCharsets.UTF_8); 434 upload.setProgressListener( pl ); 435 final List<FileItem> items; 436 try { 437 items = upload.parseRequest(req); 438 } catch (FileUploadByteCountLimitException ex) { 439 throw new RedirectException( "Request too big " + ex.getMessage(), nextPage ); 440 } 441 String wikipage = null; 442 String changeNote = null; 443 //FileItem actualFile = null; 444 final List<FileItem> fileItems = new ArrayList<>(); 445 446 for( final FileItem item : items ) { 447 if( item.isFormField() ) { 448 switch( item.getFieldName() ) { 449 case "page": 450 // FIXME: Kludge alert. We must end up with the parent page name, if this is an upload of a new revision 451 wikipage = item.getString( StandardCharsets.UTF_8); 452 final int x = wikipage.indexOf( "/" ); 453 if( x != -1 ) { 454 wikipage = wikipage.substring( 0, x ); 455 } 456 break; 457 case "changenote": 458 changeNote = item.getString( StandardCharsets.UTF_8 ); 459 if( changeNote != null ) { 460 changeNote = TextUtil.replaceEntities( changeNote ); 461 } 462 break; 463 case "nextpage": 464 nextPage = validateNextPage( item.getString( StandardCharsets.UTF_8 ), errorPage ); 465 break; 466 } 467 } else { 468 fileItems.add( item ); 469 } 470 } 471 472 if(fileItems.isEmpty()) { 473 throw new RedirectException( "Broken file upload", nextPage ); 474 475 } else { 476 for( final FileItem actualFile : fileItems ) { 477 if( !context.hasAdminPermissions() ) { 478 if (actualFile.getSize()> m_maxSize) { 479 //TODO i18n this error message 480 throw new RedirectException("Attachment too big " + actualFile.getName() + " " + 481 MaxUploadTag.humanReadableByteCountBin(actualFile.getSize()) + " vs " + 482 MaxUploadTag.humanReadableByteCountBin(m_maxSize), nextPage); 483 } 484 } 485 486 final String filename = actualFile.getName(); 487 final long fileSize = actualFile.getSize(); 488 try( final InputStream in = actualFile.getInputStream() ) { 489 executeUpload( context, in, filename, nextPage, wikipage, changeNote, fileSize ); 490 } 491 } 492 } 493 494 } catch( final ProviderException e ) { 495 msg = "Upload failed because the provider failed: "+e.getMessage(); 496 LOG.warn( msg + " (attachment: " + attName + ")", e ); 497 498 throw new IOException( msg ); 499 } catch( final FileUploadException e ) { 500 // Show the submit page again, but with a bit more intimidating output. 501 msg = "Upload failure: " + e.getMessage(); 502 LOG.warn( msg + " (attachment: " + attName + ")", e ); 503 504 throw new IOException( msg, e ); 505 } catch( final IOException e ) { 506 // Show the submit page again, but with a bit more intimidating output. 507 msg = "Upload failure: " + e.getMessage(); 508 LOG.warn( msg + " (attachment: " + attName + ")", e ); 509 510 throw e; 511 } finally { 512 m_engine.getManager( ProgressManager.class ).stopProgress( progressId ); 513 // FIXME: In case of exceptions should absolutely remove the uploaded file. 514 } 515 516 return nextPage; 517 } 518 519 /** 520 * 521 * @param context the wiki context 522 * @param data the input stream data 523 * @param filename the name of the file to upload 524 * @param errorPage the place to which you want to get a redirection 525 * @param parentPage the page to which the file should be attached 526 * @param changenote The change note 527 * @param contentLength The content length 528 * @return <code>true</code> if upload results in the creation of a new page; 529 * <code>false</code> otherwise 530 * @throws RedirectException If the content needs to be redirected 531 * @throws IOException If there is a problem in the upload. 532 * @throws ProviderException If there is a problem in the backend. 533 */ 534 protected boolean executeUpload( final Context context, final InputStream data, 535 String filename, final String errorPage, 536 final String parentPage, final String changenote, 537 final long contentLength ) 538 throws RedirectException, IOException, ProviderException { 539 boolean created = false; 540 541 try { 542 filename = AttachmentManager.validateFileName( filename ); 543 } catch( final WikiException e ) { 544 // this is a kludge, the exception that is caught here contains the i18n key 545 // here we have the context available, so we can internationalize it properly : 546 throw new RedirectException (Preferences.getBundle( context, InternationalizationManager.CORE_BUNDLE ) 547 .getString( e.getMessage() ), errorPage ); 548 } 549 550 // 551 // FIXME: This has the unfortunate side effect that it will receive the 552 // contents. But we can't figure out the page to redirect to 553 // before we receive the file, due to the stupid constructor of MultipartRequest. 554 // 555 556 if( !context.hasAdminPermissions() ) { 557 if( contentLength > m_maxSize ) { 558 // FIXME: Does not delete the received files. 559 throw new RedirectException( "File exceeds maximum size ("+m_maxSize+" bytes)", errorPage ); 560 } 561 562 if( !isTypeAllowed(filename) ) { 563 throw new RedirectException( "Files of this type may not be uploaded to this wiki", errorPage ); 564 } 565 } 566 567 final Principal user = context.getCurrentUser(); 568 final AttachmentManager mgr = m_engine.getManager( AttachmentManager.class ); 569 570 LOG.debug("file="+filename); 571 572 if( data == null ) { 573 LOG.error("File could not be opened."); 574 throw new RedirectException("File could not be opened.", errorPage); 575 } 576 577 // Check whether we already have this kind of page. If the "page" parameter already defines an attachment 578 // name for an update, then we just use that file. Otherwise, we create a new attachment, and use the 579 // filename given. Incidentally, this will also mean that if the user uploads a file with the exact 580 // same name than some other previous attachment, then that attachment gains a new version. 581 Attachment att = mgr.getAttachmentInfo( context.getPage().getName() ); 582 if( att == null ) { 583 att = new org.apache.wiki.attachment.Attachment( m_engine, parentPage, filename ); 584 created = true; 585 } 586 att.setSize( contentLength ); 587 588 // Check if we're allowed to do this? 589 final Permission permission = PermissionFactory.getPagePermission( att, "upload" ); 590 if( m_engine.getManager( AuthorizationManager.class ).checkPermission( context.getWikiSession(), permission ) ) { 591 if( user != null ) { 592 att.setAuthor( user.getName() ); 593 } 594 595 if( changenote != null && !changenote.isEmpty() ) { 596 att.setAttribute( Page.CHANGENOTE, changenote ); 597 } 598 599 try { 600 m_engine.getManager( AttachmentManager.class ).storeAttachment( att, data ); 601 } catch( final ProviderException pe ) { 602 // this is a kludge, the exception that is caught here contains the i18n key 603 // here we have the context available, so we can internationalize it properly : 604 throw new ProviderException( Preferences.getBundle( context, InternationalizationManager.CORE_BUNDLE ).getString( pe.getMessage() ) ); 605 } 606 607 LOG.info( "User " + user + " uploaded attachment to " + parentPage + " called "+filename+", size " + att.getSize() ); 608 } else { 609 throw new RedirectException( "No permission to upload a file", errorPage ); 610 } 611 612 return created; 613 } 614 615 /** 616 * Provides tracking for upload progress. 617 * 618 */ 619 private static class UploadListener extends ProgressItem implements ProgressListener { 620 public long m_currentBytes; 621 public long m_totalBytes; 622 623 @Override 624 public void update( final long recvdBytes, final long totalBytes, final int item) { 625 m_currentBytes = recvdBytes; 626 m_totalBytes = totalBytes; 627 } 628 629 @Override 630 public int getProgress() { 631 return ( int )( ( ( float )m_currentBytes / m_totalBytes ) * 100 + 0.5 ); 632 } 633 } 634 635} 636 637