/* ESXX - The friendly ECMAscript/XML Application Server Copyright (C) 2007-2015 Martin Blom <martin@blom.org> This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.esxx.js; import java.net.URI; import java.net.URL; import java.net.URISyntaxException; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Pattern; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import javax.mail.internet.ContentType; import org.esxx.ESXX; import org.esxx.Application; import org.esxx.util.StringUtil; import org.esxx.js.protocol.ProtocolHandler; import org.mozilla.javascript.*; import org.mozilla.javascript.regexp.NativeRegExp; public class JSURI extends ScriptableObject { public JSURI() { super(); } public JSURI(URI uri) throws URISyntaxException { super(); setURI(uri); protocolHandler = getProtocolHandler(); } public static JSURI newJSURI(Context cx, Application app, URI uri) { return (JSURI) cx.newObject(app.getJSGlobal(), "URI", new Object[] { uri }); } @Override public String toString() { return jsFunction_toString(); } @Override public String getClassName() { return "URI"; } @Override public Object getDefaultValue(Class<?> typeHint) { if (uri != null) { return uri.toString(); } else { return null; } } public URI getURI() { return uri; } public void setURI(URI uri) { this.uri = uri; } static public Object jsConstructor(Context cx, java.lang.Object[] args, Function ctorObj, boolean inNewExpr) throws URISyntaxException { JSURI prop_src_uri = null; URI uri = null; String uri_string = null; String uri_relative = null; Scriptable params = null; if (args.length < 1 || args[0] == Context.getUndefinedValue()) { throw Context.reportRuntimeError("Missing argument"); } // First argument is always the URI if (args.length >= 1 && args[0] != Context.getUndefinedValue()) { if (args[0] instanceof JSURI) { prop_src_uri = (JSURI) args[0]; uri = prop_src_uri.uri; } else if (args[0] instanceof URL) { uri = ((URL) args[0]).toURI(); } else if (args[0] instanceof URI) { uri = (URI) args[0]; } else { uri_string = Context.toString(args[0]); } } // Third argument can only by params if (args.length >= 3 && args[2] != Context.getUndefinedValue()) { params = (Scriptable) args[2]; } // Second argument can be relative URI or params if (args.length >= 2 && args[1] != Context.getUndefinedValue()) { if (args[1] instanceof Scriptable) { if (params != null) { throw Context.reportRuntimeError("Expected a String as second argument."); } params = (Scriptable) args[1]; } else { // args[1] should be resolved against args[0] uri_relative = Context.toString(args[1]); } } if (params != null) { // Replace {...} patterns in string arguments if params was supplied final Scriptable final_params = params; StringUtil.ParamResolver resolver = new StringUtil.ParamResolver() { public String resolveParam(String param) { Object obj; try { obj = final_params.get(Integer.parseInt(param), final_params); } catch (NumberFormatException ex) { obj = final_params.get(param, final_params); } try { String value = Context.toString(obj); return StringUtil.encodeURI(value, false /* == encodeURIComponent() */); } catch (URISyntaxException ex) { throw new WrappedException(ex); } } }; uri_string = StringUtil.format(uri_string, resolver); uri_relative = StringUtil.format(uri_relative, resolver); } if (uri_string != null) { // Resolve URI against current location, if possible JSESXX js_esxx = JSGlobal.getJSESXX(cx, ctorObj); if (js_esxx != null) { prop_src_uri = js_esxx.jsGet_wd(); if (prop_src_uri != null) { uri = resolveURI(prop_src_uri.uri, uri_string); } } if (uri == null) { // Fall back to non-relative uri = new URI(uri_string); } } if (uri_relative != null) { // Resolve relative part against first URI argument uri = resolveURI(uri, uri_relative); } JSURI rc = new JSURI(uri); if (prop_src_uri != null) { // Copy local properties from previous JSURI object for (Object o : prop_src_uri.getIds()) { if (o instanceof String) { String key = (String) o; rc.put(key, rc, prop_src_uri.get(key, prop_src_uri)); } else { int key = (Integer) o; rc.put(key, rc, prop_src_uri.get(key, prop_src_uri)); } } } return rc; } public static void finishInit(Scriptable scope, FunctionObject constructor, Scriptable prototype) { // Create and make these properties in the prototype visible Context cx = Context.getCurrentContext(); Scriptable jars = cx.newArray(prototype, 0); jars.put(0, jars, cx.newArray(jars, 0)); defineProperty(prototype, "params", cx.newArray(prototype, 0), ScriptableObject.PERMANENT); defineProperty(prototype, "auth", cx.newArray(prototype, 0), ScriptableObject.PERMANENT); defineProperty(prototype, "jars", jars, ScriptableObject.PERMANENT); defineProperty(prototype, "headers", cx.newArray(prototype, 0), ScriptableObject.PERMANENT); } public String jsFunction_valueOf() { if (uri != null) { return uri.toASCIIString(); } else { return null; } } public String jsFunction_toString() { return (String) getDefaultValue(String.class); } public String jsFunction_toSource() { return "(new URI(\"" + jsFunction_valueOf() + "\"))"; } public static Object jsFunction_load(Context cx, Scriptable thisObj, Object[] args, Function funObj) throws Exception { JSURI js_this = checkInstance(thisObj); ContentType recv_ct = null; if (args.length >= 1 && args[0] != Context.getUndefinedValue()) { recv_ct = new ContentType(Context.toString(args[0])); } return js_this.protocolHandler.load(cx, thisObj, recv_ct); } public static Object jsFunction_save(Context cx, Scriptable thisObj, Object[] args, Function funObj) throws Exception { JSURI js_this = checkInstance(thisObj); ContentType send_ct = null; ContentType recv_ct = null; if (args.length < 1 || args[0] == Context.getUndefinedValue()) { throw Context.reportRuntimeError("Missing save() argument"); } if (args.length >= 2 && args[1] != Context.getUndefinedValue()) { send_ct = new ContentType(Context.toString(args[1])); } if (args.length >= 3 && args[2] != Context.getUndefinedValue()) { recv_ct = new ContentType(Context.toString(args[2])); } return js_this.protocolHandler.save(cx, thisObj, args[0], send_ct, recv_ct); } public static Object jsFunction_append(Context cx, Scriptable thisObj, Object[] args, Function funObj) throws Exception { JSURI js_this = checkInstance(thisObj); ContentType send_ct = null; ContentType recv_ct = null; if (args.length < 1 || args[0] == Context.getUndefinedValue()) { throw Context.reportRuntimeError("Missing append() argument"); } if (args.length >= 2 && args[1] != Context.getUndefinedValue()) { send_ct = new ContentType(Context.toString(args[1])); } if (args.length >= 3 && args[2] != Context.getUndefinedValue()) { recv_ct = new ContentType(Context.toString(args[2])); } return js_this.protocolHandler.append(cx, thisObj, args[0], send_ct, recv_ct); } public static Object jsFunction_modify(Context cx, Scriptable thisObj, Object[] args, Function funObj) throws Exception { JSURI js_this = checkInstance(thisObj); ContentType send_ct = null; ContentType recv_ct = null; if (args.length < 1 || args[0] == Context.getUndefinedValue()) { throw Context.reportRuntimeError("Missing append() argument"); } if (args.length >= 2 && args[1] != Context.getUndefinedValue()) { send_ct = new ContentType(Context.toString(args[1])); } if (args.length >= 3 && args[2] != Context.getUndefinedValue()) { recv_ct = new ContentType(Context.toString(args[2])); } return js_this.protocolHandler.modify(cx, thisObj, args[0], send_ct, recv_ct); } public static Object jsFunction_remove(Context cx, Scriptable thisObj, Object[] args, Function funObj) throws Exception { JSURI js_this = checkInstance(thisObj); ContentType recv_ct = null; if (args.length >= 1 && args[0] != Context.getUndefinedValue()) { recv_ct = new ContentType(Context.toString(args[0])); } return js_this.protocolHandler.remove(cx, thisObj, recv_ct); } public static Object jsFunction_query(Context cx, Scriptable thisObj, Object[] args, Function funObj) throws Exception { JSURI js_this = checkInstance(thisObj); return js_this.protocolHandler.query(cx, thisObj, args); } public Properties getParams(Context cx, URI uri) { final Properties props = new Properties(); enumerateProperty(cx, "params", new PropEnumerator() { public void handleProperty(Scriptable p, int s) { props.setProperty(Context.toString(p.get("name", p)), Context.toString(p.get("value", p))); } }, uri, null, null); return props; } public URI jsGet_javaURI() { return uri; } public interface PropEnumerator { void handleProperty(Scriptable prop, int score); } public Scriptable getAuth(Context cx, URI req_uri, String realm, String mechanism) { String[] unp = getUsernameAndPassword(); // If the URI already carries authorization information, use it if (unp != null) { Scriptable res = cx.newObject(this); ScriptableObject.putProperty(res, "username", unp[0]); ScriptableObject.putProperty(res, "password", unp[1]); return res; } // Else, search the 'auth' property for matching entries return getBestProperty(cx, "auth", req_uri, realm, mechanism); } public String[] getAuthMechanisms(Context cx, URI req_uri, String realm, final String[] default_mechanisms) { final LinkedHashSet<String> result = new LinkedHashSet<String>(); // Order is important if (getUsernameAndPassword() != null) { result.addAll(Arrays.asList(default_mechanisms)); } enumerateProperty(cx, "auth", new PropEnumerator() { public void handleProperty(Scriptable p, int s) { Object mechanism = p.get("mechanism", p); if (mechanism == null || mechanism == Scriptable.NOT_FOUND || mechanism == Context.getUndefinedValue()) { // Any mechanism is OK result.addAll(Arrays.asList(default_mechanisms)); } else { result.add(Context.toString(mechanism).toLowerCase()); } } }, req_uri, realm, null); return result.toArray(new String[result.size()]); } public Scriptable getCookieJar(Context cx, URI req_uri) { return getBestProperty(cx, "jars", req_uri, null, null); } public void enumerateHeaders(Context cx, PropEnumerator pe, URI req_uri) { enumerateProperty(cx, "headers", pe, req_uri, null, null); } private String[] getUsernameAndPassword() { if (uri.getRawUserInfo() != null) { String[] unp = uri.getRawUserInfo().split(":", 2); if (unp.length == 2) { try { return new String[] { StringUtil.decodeURI(unp[0], false), StringUtil.decodeURI(unp[1], false) }; } catch (URISyntaxException ignored) {} } } return null; } private Scriptable getBestProperty(Context cx, String name, URI req_uri, String realm, String mechanism) { final Scriptable[] res = { null }; final int[] score = { -1 }; enumerateProperty(cx, name, new PropEnumerator() { public void handleProperty(Scriptable p, int s) { if (s > score[0]) { res[0] = p; score[0] = s; } } }, req_uri, realm, mechanism); return res[0]; } private void enumerateProperty(Context cx, String name, PropEnumerator pe, URI candidate, String realm, String mechanism) { String uri = candidate.toString(); String scheme = candidate.getScheme(); String user = candidate.getUserInfo(); String host = candidate.getHost(); Integer port = candidate.getPort(); String path = candidate.getPath(); Object p = ScriptableObject.getProperty(this, name); if (p instanceof Scriptable) { Scriptable params = (Scriptable) p; for (Object key : params.getIds()) { if (key instanceof Integer) { p = params.get((Integer) key, params); } else { p = params.get((String) key, params); } if (p instanceof Scriptable) { Scriptable param = (Scriptable) p; int score = 0; score += filterProperty(cx, param, "realm", realm) * 1; score += filterProperty(cx, param, "mechanism", mechanism) * 2; score += filterProperty(cx, param, "scheme", scheme) * 4; score += filterProperty(cx, param, "path", path) * 8; score += filterProperty(cx, param, "port", port) * 16; score += filterProperty(cx, param, "host", host) * 32; score += filterProperty(cx, param, "user-info", user) * 64; score += filterProperty(cx, param, "uri", uri) * 128; if (score >= 0) { pe.handleProperty(param, score); } } } } } private int filterProperty(Context cx, Scriptable param, String key, Object value) { Object rule = param.get(key, param); if (rule == null || rule == Scriptable.NOT_FOUND || value == null) { return 0; } if (rule instanceof Number && value instanceof Number) { return ((Number) rule).doubleValue() == ((Number) value).doubleValue() ? 1 : -1000; } else if (rule instanceof NativeRegExp) { return ((NativeRegExp) rule).call(cx, this, (NativeRegExp) rule, new Object[] { value }) != null ? 1 : -1000; } else { return Context.toString(rule).equals(value.toString()) ? 1 : -1000; } } private ProtocolHandler getProtocolHandler() throws URISyntaxException { String key = uri.getScheme(); String handler = "org.esxx.js.protocol." + uri.getScheme().toUpperCase() + "Handler"; ProtocolHandler res = getProtocolHandler(key, handler); if (res == null) { try { @SuppressWarnings("unused") java.net.URL url = uri.toURL(); // Throws if the is no protocol handler for this URL res = getProtocolHandler(key, "org.esxx.js.protocol.URLHandler"); } catch (java.net.MalformedURLException ignored) {} } if (res == null) { res = getProtocolHandler(key, "org.esxx.js.protocol.ProtocolHandler"); } if (res == null) { // This should never happen throw new IllegalStateException("Unable to create a ProtocolHandler for URI " + uri); } return res; } private ProtocolHandler getProtocolHandler(String key, String handler) throws URISyntaxException { try { Constructor<? extends ProtocolHandler> constr = schemeConstructors.get(key); if (constr == null) { Class<? extends ProtocolHandler> cls; cls = Class.forName(handler).asSubclass(ProtocolHandler.class); constr = cls.getConstructor(JSURI.class); schemeConstructors.put(key, constr); } return constr.newInstance(this); } catch (InvocationTargetException ex) { schemeConstructors.remove(key); if (ex.getCause() instanceof URISyntaxException) { throw (URISyntaxException) ex.getCause(); } return null; } catch (Exception ex) { schemeConstructors.remove(key); return null; } } protected static JSURI checkInstance(Scriptable obj) { if (obj == null || !(obj instanceof JSURI)) { throw Context.reportRuntimeError("Called on incompatible object"); } return (JSURI) obj; } private static Pattern fragmentPart = Pattern.compile("#.*"); private static Pattern queryPart = Pattern.compile("\\?.*"); private static URI resolveURI(URI base, String relative) throws URISyntaxException { URI rel = new URI(relative); URI res; if (relative.startsWith("#")) { // Make #frag resolve against non-hierachial URIs too res = new URI(fragmentPart.matcher(base.toString()).replaceFirst("") + relative); } else if (relative.startsWith("?")) { // Make ?query resolve correctly res = new URI(queryPart.matcher(base.toString()).replaceFirst("") + relative ); } else { res = base.resolve(rel); } return res; } private static ConcurrentHashMap<String, Constructor<? extends ProtocolHandler>> schemeConstructors = new ConcurrentHashMap<String, Constructor<? extends ProtocolHandler>>(); private ProtocolHandler protocolHandler; private URI uri; static final long serialVersionUID = -5445754832118781527L; }