    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