*  @param reason an upload summary (defaults to <tt>contents</tt>, use ""
     *  to not specify one)
     *  @throws CredentialNotFoundException if not logged in
     *  @throws CredentialException if (page is protected OR file is on a central
     *  repository) and we can't upload
     *  @throws CredentialExpiredException if cookies have expired
     *  @throws IOException if a network/local filesystem error occurs
     *  @throws AccountLockedException if user is blocked
     *  @since 0.21
     */
    public synchronized void upload(File file, String filename, String contents, String reason) throws IOException, LoginException
    {
        // TODO: upload via URL

        // the usual stuff
        // throttle
        long start = System.currentTimeMillis();

        // check for log in
        if (user == null || !user.isAllowedTo("upload"))
        {
            CredentialNotFoundException ex = new CredentialNotFoundException("Permission denied: cannot upload files.");
            logger.logp(Level.SEVERE, "Wiki", "upload()", "[" + domain + "] Cannot upload - permission denied.", ex);
            throw ex;
        }
        statusCheck();
        filename = filename.replaceFirst("^(File|Image|" + namespaceIdentifier(FILE_NAMESPACE) + "):", "");

        // protection and token
        HashMap<String, Object> info = getPageInfo("File:" + filename);
        if (!checkRights(info, "upload"))
        {
            CredentialException ex = new CredentialException("Permission denied: page is protected.");
            logger.logp(Level.WARNING, "Wiki", "upload()", "[" + getDomain() + "] Cannot upload - permission denied.", ex);
            throw ex;
        }
        String wpEditToken = (String)info.get("token");

        // chunked upload setup
        long filesize = file.length();
        long chunks = (filesize >> LOG2_CHUNK_SIZE) + 1; 
        FileInputStream fi = new FileInputStream(file);
        String filekey = "";
        
        // upload the image
        for (int i = 0; i < chunks; i++)
        {
            HashMap<String, Object> params = new HashMap<String, Object>(50);
            params.put("filename", filename);
            params.put("token", wpEditToken);
            params.put("ignorewarnings", "true");
            if (chunks == 1)
            {
                // Chunks disabled due to a small filesize.
                // This is just a normal upload.
                params.put("text", contents);
                if (!reason.isEmpty())
                    params.put("comment", reason);
                byte[] by = new byte[fi.available()];
                fi.read(by);
                // Why this is necessary?
                params.put("file\"; filename=\"" + file.getName(), by);
            }
            else
            {
                long offset = i << LOG2_CHUNK_SIZE;
                params.put("stash", "1");
                params.put("offset", "" + offset);
                params.put("filesize", "" + filesize);
                if (i != 0)
                    params.put("filekey", filekey);
                
                // write the actual file
                long buffersize = Math.min(1 << LOG2_CHUNK_SIZE, filesize - offset);
                byte[] by = new byte[(int)buffersize]; // 32 bit problem. Why must array indices be ints?
                fi.read(by); 
                params.put("chunk\"; filename=\"" + file.getName(), by);
                
                // Each chunk presumably requires a new edit token
                wpEditToken = (String)getPageInfo("File:" + filename).get("token");
            }
                
            // done
            String response = multipartPost(apiUrl + "action=upload", params, "upload");
            try
            {
                // look for filekey
                if (chunks > 1)
                {
                    if (response.contains("filekey=\""))
                    {
                        filekey = parseAttribute(response, "filekey", 0);
                        continue;
                    }
                    else
                        throw new IOException("No filekey present! Server response was " + response);
                }
                // TODO: check for more specific errors here
                if (response.contains("error code=\"fileexists-shared-forbidden\""))
                {
                    CredentialException ex = new CredentialException("Cannot overwrite file hosted on central repository.");
                    logger.logp(Level.WARNING, "Wiki", "upload()", "[" + getDomain() + "] Cannot upload - permission denied.", ex);
                    throw ex;
                }
                checkErrors(response, "upload");
            }
            catch (IOException e)
            {
                fi.close();
                // don't bother retrying - uploading is a pain
                logger.logp(Level.SEVERE, "Wiki", "upload()", "[" + domain + "] EXCEPTION:  ", e);
                throw e;
            }
        }
        fi.close();
        
        // unstash upload if chunked
        if (chunks > 1)
        {
            HashMap<String, Object> params = new HashMap<String, Object>(50);
            params.put("filename", filename);
            params.put("token", wpEditToken);
            params.put("text", contents);
            if (!reason.isEmpty())
                params.put("comment", reason);
            params.put("ignorewarnings", "true");
            params.put("filekey", filekey);
            String response = multipartPost(apiUrl + "action=upload", params, "upload");
            checkErrors(response, "upload");
        }
                  
        // throttle
        try
        {
            long time = throttle - System.currentTimeMillis() + start;
            if (time > 0)
                Thread.sleep(time);
        }
        catch (InterruptedException e)
        {
            // nobody cares
        }
        log(Level.INFO, "Successfully uploaded to File:" + filename + ".", "upload");
    }