/* * MultipartSetTemplate.java * * Brazil project web application toolkit, * export version: 2.3 * Copyright (c) 2001-2006 Sun Microsystems, Inc. * * Sun Public License Notice * * The contents of this file are subject to the Sun Public License Version * 1.0 (the "License"). You may not use this file except in compliance with * the License. A copy of the License is included as the file "license.terms", * and also available at http://www.sun.com/ * * The Original Code is from: * Brazil project web application toolkit release 2.3. * The Initial Developer of the Original Code is: suhler. * Portions created by suhler are Copyright (C) Sun Microsystems, Inc. * All Rights Reserved. * * Contributor(s): suhler. * * Version: 2.5 * Created by suhler on 01/08/20 * Last modified by suhler on 06/04/25 14:16:24 * * Version Histories: * * 2.5 06/04/25-14:16:24 (suhler) * use consolidated mime type handling in FileHandler * * 2.4 04/12/15-13:10:27 (suhler) * redo automatic file saving. Specify a pattern to match file uploads * to be saved, and a name (containing ${...}) to specify the name to * save the file as * * 2.3 04/11/30-10:59:44 (suhler) * add direct file saves (not quite right) * * 2.2 03/08/01-16:18:45 (suhler) * fixes for javadoc * * 2.1 02/10/01-16:36:53 (suhler) * version change * * 1.5 02/05/01-11:28:58 (suhler) * fix sccs version info * * 1.4 02/01/29-14:33:12 (suhler) * doc lint * * 1.3 01/09/13-09:24:22 (suhler) * remove uneeded import * * 1.2 01/08/21-11:00:46 (suhler) * small bug in content-type guessing * * 1.2 01/08/20-20:38:33 (Codemgr) * SunPro Code Manager data about conflicts, renames, etc... * Name history : 1 0 handlers/templates/MultipartSetTemplate.java * * 1.1 01/08/20-20:38:32 (suhler) * date and time created 01/08/20 20:38:32 by suhler * */ package sunlabs.brazil.template; import sunlabs.brazil.server.Request; import sunlabs.brazil.server.Server; import sunlabs.brazil.server.FileHandler; import sunlabs.brazil.util.Format; import sunlabs.brazil.util.Base64; import sunlabs.brazil.util.Glob; import sunlabs.brazil.util.http.HttpInputStream; import java.io.ByteArrayInputStream; import java.io.InputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.util.Properties; /** * Version of the SetTemplate that reflects form/multipart data * in Request.props. * For ordinary forms, the values placed into request.props * are the same as for forms of type www-url-encoded, or * method=get, as long as the query option * is set. *

* For file input fields (e.g. <input type=file...>), * the file content is associated with the field name, and the * properties * name.filename, * name.type, * and * name.encoding * are set to to the name of the file uploaded, its type, and * (unless noEncode is set), the encoding, which is * either none (for text files), or Base64. *

* The file contents are automatically Base64 encoded for binary files. *

* Properties: *

*
query
If present, The form data is translated from form/multipart * and placed into the request properties, prefixed by the value * of query. *
noEncode
If present, no encoding is performed on file uploads. *
savePattern=[glob pattern] *
* If set, then the form is scanned for field names that match * glob pattern. If a match is found, then the next * form element of type file is saved to a file in the document * root instead of being loaded as a property. The name of the * file is specified by the value of the saveName * entry. *
saveName=name *
The name to use to save the file. May contain ${...} * substitutions. The variables ${fileName}, ${fieldName) and * ${prefix} may be used here as "special" variables to make creating * a file name easier. * saveNamedefaults to: * ${prefix}-${fieldName}-${fileName} * *
* [This has only been tested with Netscape Navigator and Mozilla.] * * @author Stephen Uhler * @version %W */ public class MultipartSetTemplate extends SetTemplate { public boolean init(RewriteContext hr) { String query = get(hr, "query"); boolean noEncode = isTrue(hr, "noEncode") ; String type = (String) hr.request.headers.get("content-type"); if (query != null && type != null && type.startsWith("multipart/form-data")) { processData(hr, query); } return super.init(hr); } /** * Extract the form/multipart data into request.props. * IF the content type is supplied with the file, use it. * Otherwise, consult our own configuration file for mime types. * See {@link sunlabs.brazil.server.FileHandler} for a * description of how to set mime types for file suffixes. * If no type can be determined, the "type" property will not be set. * * @param prefix: The prefix used to set properties */ void processData(RewriteContext hr, String prefix) { boolean noEncode = isTrue(hr, "noEncode"); String query = get(hr, "query"); String glob = get(hr, "savePattern"); // don't do ${...} yet String saveName = hr.request.props.getProperty(hr.prefix + "saveName", "${prefix}${fieldName}-${fileName}"); boolean saveAs = false; // save next file as this name /* * Use a local properties object to make it easier to specify * ${..} keys when defining the name of the file to save */ Properties local = new Properties(hr.request.props); local.put("prefix", hr.prefix); Split s = new Split(hr.request.postData); while (s.nextPart()) { String fieldName = s.name(); String type = s.type(); String fileName = s.fileName(); put(hr, prefix, fieldName + ".headers" , s.header().trim()); if (fileName != null) { // we have a file upload put(hr, prefix, fieldName + ".filename" , fileName); int index; if (type == null) { type = FileHandler.getMimeType(fileName, hr.request.props, hr.prefix); log(hr, Server.LOG_DIAGNOSTIC, "Guessing type: " + type); } if (type != null) { put(hr, prefix, fieldName + ".type", type); } } else if (Glob.match(glob, fieldName)) { saveAs = true; } // XXX allow option to write directly to a file // Use a property to match the file name in the form, and // re-write it if (saveAs && fileName != null) { // save the file local.put("fieldName", fieldName); local.put("fileName", fileName); String fn = Format.subst(local, saveName); put(hr, prefix, fieldName + ".file", fn); writeFile(hr, fn, s.stream()); saveAs = false; } else if (fileName==null || noEncode) { // set name=value put(hr, prefix, fieldName, s.content()); } else if (type != null && type.startsWith("text")) { put(hr, prefix, fieldName, s.content()); put(hr, prefix, fieldName + ".encoding", "none"); } else { put(hr, prefix, fieldName + ".encoding", "base64"); put(hr, prefix, fieldName, s.content(true)); } } } /** * Write out the file whose name is "name". If name * starts with "/" make it relative to the document root. * Otherwise assume it is relative to the directory implied * by the URL. */ void writeFile(RewriteContext hr, String name, InputStream in) { String root = get(hr, FileHandler.ROOT); if (root == null) { root = hr.request.props.getProperty(FileHandler.ROOT, "."); } File file; if (name.startsWith("/")) { file = new File(root, name.substring(1)); } else { String dir = FileHandler.urlToPath(hr.request.url); file = new File(root + dir); if (!file.isDirectory()) { file = new File(file.getParent()); } file = new File(file, name); } HttpInputStream hin = new HttpInputStream(in); try { FileOutputStream out = new FileOutputStream(file); hin.copyTo(out); log(hr, Server.LOG_DIAGNOSTIC, "wrote file: " + file); } catch (IOException e) { log(hr, Server.LOG_LOG, "Can't write file: " + e.getMessage()); } } // convenience methods 'cause we're not in a tag /** * Get the value of a request property. We can't use * hr.get() 'cause we're not in a tag. */ String get(RewriteContext hr, String key) { String value = hr.request.props.getProperty(hr.prefix + key); return Format.subst(hr.request.props, value); } /** * put a value into request props with our prefix */ void put(RewriteContext hr, String prefix, String key, String value) { hr.request.props.put(prefix + key , value); } boolean isTrue(RewriteContext hr, String key) { String value = get(hr, key); return (value != null && !Format.isFalse(value.trim().toLowerCase())); } void log(RewriteContext hr, int level, String msg) { hr.request.log(level, hr.prefix, msg); } /* * We need to define all template tags here, * as they are determined via reflection. */ public void tag_set(RewriteContext hr) { super.tag_set(hr); } public void tag_property(RewriteContext hr) { tag_get(hr); } public void tag_get(RewriteContext hr) { super.tag_get(hr); } public void tag_import(RewriteContext hr) { super.tag_import(hr); } public void tag_tag(RewriteContext hr) { super.tag_tag(hr); } public void tag_slash_tag(RewriteContext hr) { super.tag_slash_tag(hr); } /** * Shamelessly copied from PushHandler * Split multipart data into its constituent pieces. Use byte[] so we can * handle (potentially) large amounts of binary data. * This acts as an iterator, stepping through the parts, * extracting the appropriate * info for each part. * * This has been tested with the data Navigator sends as * encoding=form/multipart data. */ static class Split { byte[] bytes; // raw form/multipart data int bndryEnd; // end of the initial boundary line int partStart; // start index of this part int partEnd; // index to the end of this part int contentStart; // start of the content /** * create a new multipart form thingy */ public Split(byte[] bytes) { partEnd = 0; this.bytes = bytes; bndryEnd = indexOf(bytes, 0, bytes.length, "\r\n"); partStart=0; contentStart=0; } /** * Return true if there is a next part */ public boolean nextPart() { partStart = partEnd + bndryEnd+2; if (partStart >= bytes.length) { return false; } partEnd = indexOf(bytes, partStart, bytes.length, bytes, 0, bndryEnd); if (partEnd < 0) { return false; } partEnd -=2; // back over \r\n contentStart = indexOf(bytes, partStart, bytes.length,"\r\n\r\n")+4; return true; } /** * Get the content as a (base64) encoded string * @param encode: If true, base-64 encode, else no encoding */ public String content(boolean encode) { if (encode) { return Base64.encode(bytes, contentStart, partEnd-contentStart); } else { return new String(bytes, contentStart, partEnd-contentStart); } } /** * Get the content as an input stream */ public ByteArrayInputStream stream() { return new ByteArrayInputStream(bytes, contentStart, partEnd-contentStart); } /** * Get the content as a string */ public String content() { return content(false); } /** * Return the header as a string */ public String header() { return (new String(bytes, partStart, contentStart-partStart)); } /** * get the field name */ public String name() { return part("name=\"", "\""); } /** * Get the file name, if any */ public String fileName() { return part("filename=\"", "\""); } /** * Get the content type (if any) */ public String type() { return part("Content-Type: ", "\r"); } /** * find info in the header * @param start: String that matches portion of header before * what we extract * @param end: String that matches the termination of the part we * extract */ String part(String pre, String post) { int start = indexOf(bytes, partStart, contentStart, pre) + pre.length(); int end = indexOf(bytes, start, contentStart, post); if (start>=pre.length() && end >=0) { return (new String(bytes, start, end-start)); } else { return null; } } } /** * Find the index of dst in src or -1 if not found > * This is the byte array equivalent to string.indexOf() */ static int indexOf(byte[] src, int srcStart, int srcEnd, byte[] dst, int dstStart, int dstEnd) { int len = dstEnd - dstStart; // len of to look for srcEnd -= len; for (;srcStart < srcEnd; srcStart++) { boolean ok=true; for(int i=0; ok && i