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