package org.lobobrowser.html.js;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.AccessController;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.eclipse.jdt.annotation.NonNull;
import org.lobobrowser.html.js.Window.JSRunnableTask;
import org.lobobrowser.js.AbstractScriptableDelegate;
import org.lobobrowser.js.JavaScript;
import org.lobobrowser.ua.NetworkRequest;
import org.lobobrowser.ua.UserAgentContext;
import org.lobobrowser.ua.UserAgentContext.Request;
import org.lobobrowser.ua.UserAgentContext.RequestKind;
import org.lobobrowser.util.DOMExceptions;
import org.lobobrowser.util.Urls;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.Function;
import org.mozilla.javascript.ScriptRuntime;
import org.mozilla.javascript.Scriptable;
import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
public class XMLHttpRequest extends AbstractScriptableDelegate {
// TODO: See reference:
// http://www.xulplanet.com/references/objref/XMLHttpRequest.html
private static final Logger logger = Logger.getLogger(XMLHttpRequest.class.getName());
private final NetworkRequest request;
private final UserAgentContext pcontext;
private final Scriptable scope;
private final java.net.URL codeSource;
// TODO: This is a quick hack
private final Window window;
public XMLHttpRequest(final UserAgentContext pcontext, final java.net.URL codeSource, final Scriptable scope, final Window window) {
this.request = pcontext.createHttpRequest();
this.pcontext = pcontext;
this.scope = scope;
this.codeSource = codeSource;
this.window = window;
}
public void abort() {
request.abort();
}
// excluded as per https://dvcs.w3.org/hg/xhr/raw-file/default/xhr-1/Overview.html
private static final List<String> excludedResponseHeadersLowerCase = Arrays.asList(
"set-cookie",
"set-cookie2"
);
@NotGetterSetter
public String getAllResponseHeaders() {
// TODO: Need to also filter out based on CORS
return request.getAllResponseHeaders(excludedResponseHeadersLowerCase);
}
public int getReadyState() {
return request.getReadyState();
}
public byte[] getResponseBytes() {
return request.getResponseBytes();
}
public String getResponseHeader(final String headerName) {
// TODO: Need to also filter out based on CORS
if (excludedResponseHeadersLowerCase.contains(headerName.toLowerCase())) {
return request.getResponseHeader(headerName);
} else {
return null;
}
}
public String getResponseText() {
return request.getResponseText();
}
public Document getResponseXML() {
return request.getResponseXML();
}
public int getStatus() {
return request.getStatus();
}
public String getStatusText() {
return request.getStatusText();
}
private @NonNull URL getFullURL(final String relativeUrl) throws java.net.MalformedURLException {
return Urls.createURL(this.codeSource, relativeUrl);
}
public void open(final String method, final String url, final boolean asyncFlag, final String userName, final String password)
throws java.io.IOException {
final String adjustedMethod = checkAndAdjustMethod(method);
try {
request.open(adjustedMethod, this.getFullURL(url), asyncFlag, userName, password);
} catch (final MalformedURLException mfe) {
throw ScriptRuntime.typeError("url malformed");
}
}
private static String[] prohibitedMethods = {
"CONNECT", "TRACE", "TRACK"
};
private static String[] upperCaseMethods = {
"DELETE", "GET", "HEAD", "OPTIONS", "POST", "PUT"
};
private static String checkAndAdjustMethod(final String method) {
for (final String p : prohibitedMethods) {
if (p.equalsIgnoreCase(method)) {
throw DOMExceptions.ExtendedError.SecurityError.createException();
}
}
for (final String u : upperCaseMethods) {
if (u.equalsIgnoreCase(method)) {
return u;
}
}
return method;
}
public void open(final String method, final String url, final boolean asyncFlag, final String userName) throws java.io.IOException {
final String adjustedMethod = checkAndAdjustMethod(method);
request.open(adjustedMethod, this.getFullURL(url), asyncFlag, userName);
}
public void open(final String method, final String url, final boolean asyncFlag) throws java.io.IOException {
final String adjustedMethod = checkAndAdjustMethod(method);
request.open(adjustedMethod, this.getFullURL(url), asyncFlag);
}
public void open(final String method, final String url) throws java.io.IOException {
final String adjustedMethod = checkAndAdjustMethod(method);
request.open(adjustedMethod, this.getFullURL(url));
}
public void send(final String content) throws IOException {
final Optional<URL> urlOpt = request.getURL();
if (urlOpt.isPresent()) {
final URL url = urlOpt.get();
if (isSameOrigin(url, codeSource)) {
// final URLPermission urlPermission = new URLPermission(url.toExternalForm());
// final SocketPermission socketPermission = new SocketPermission(url.getHost() + ":" + Urls.getPort(url), "connect,resolve");
// final StoreHostPermission storeHostPermission = StoreHostPermission.forURL(url);
final PrivilegedExceptionAction<Object> action = new PrivilegedExceptionAction<Object>() {
@Override
public Object run() throws Exception {
request.send(content, new Request(url, RequestKind.XHR));
return null;
}
};
// if (request.isAsnyc()) {
// window.addJSTask(new JSRunnableTask(0, "xhr async request", () -> {
// try {
// // AccessController.doPrivileged(action, null, urlPermission, socketPermission, storeHostPermission);
// AccessController.doPrivileged(action);
// } catch (final PrivilegedActionException e) {
// e.printStackTrace();
// }
// }));
// } else {
try {
// AccessController.doPrivileged(action, null, urlPermission, socketPermission, storeHostPermission);
AccessController.doPrivileged(action);
} catch (final PrivilegedActionException e) {
throw (IOException) e.getCause();
}
// }
} else {
final String msg = String.format("Failed to execute 'send' on 'XMLHttpRequest': Failed to load '%s'", url.toExternalForm());
throw DOMExceptions.ExtendedError.NetworkError.createException(msg);
}
}
}
private static boolean isSameOrigin(final URL url1, final URL url2) {
return url1.getHost().equals(url2.getHost()) &&
(url1.getPort() == (url2.getPort())) &&
url1.getProtocol().equals(url2.getProtocol());
}
private Function onreadystatechange;
private boolean listenerAdded;
public Function getOnreadystatechange() {
synchronized (this) {
return this.onreadystatechange;
}
}
public void setOnreadystatechange(final Function value) {
synchronized (this) {
this.onreadystatechange = value;
if ((value != null) && !this.listenerAdded) {
this.request.addNetworkRequestListener(netEvent -> executeReadyStateChange());
this.listenerAdded = true;
}
}
}
private Function onLoad;
// private boolean listenerAddedLoad;
public void setOnload(final Function value) {
synchronized (this) {
this.onLoad = value;
if ((value != null) && !this.listenerAdded) {
this.request.addNetworkRequestListener(netEvent -> executeReadyStateChange());
this.listenerAdded = true;
}
}
}
private void executeReadyStateChange() {
// Not called in GUI thread to ensure consistency of readyState.
try {
final Function f = XMLHttpRequest.this.getOnreadystatechange();
if (f != null) {
window.addJSTask(new JSRunnableTask(0, "xhr ready state changed: " + request.getReadyState(), () -> {
final Context ctx = Executor.createContext(this.codeSource, this.pcontext, window.getContextFactory());
try {
final Scriptable newScope = (Scriptable) JavaScript.getInstance().getJavascriptObject(XMLHttpRequest.this, this.scope);
f.call(ctx, newScope, newScope, new Object[0]);
} finally {
Context.exit();
}
}));
}
} catch (final Exception err) {
logger.log(Level.WARNING, "Error processing ready state change.", err);
Executor.logJSException(err);
}
if (request.getReadyState() == NetworkRequest.STATE_COMPLETE) {
try {
final Function f = this.onLoad;
if (f != null) {
window.addJSTaskUnchecked(new JSRunnableTask(0, "xhr on load : ", () -> {
final Context ctx = Executor.createContext(this.codeSource, this.pcontext, window.getContextFactory());
try {
final Scriptable newScope = (Scriptable) JavaScript.getInstance().getJavascriptObject(XMLHttpRequest.this, this.scope);
f.call(ctx, newScope, newScope, new Object[0]);
} finally {
Context.exit();
}
}));
}
} catch (final Exception err) {
logger.log(Level.WARNING, "Error processing ready state change.", err);
}
}
}
// This list comes from https://dvcs.w3.org/hg/xhr/raw-file/tip/Overview.html#the-setrequestheader()-method
// It has been lower-cased for faster comparison
private static String[] prohibitedHeaders = {
"accept-charset",
"accept-encoding",
"access-control-request-headers",
"access-control-request-method",
"connection",
"content-length",
"cookie",
"cookie2",
"date",
"dnt",
"expect",
"host",
"keep-alive",
"origin",
"referer",
"te",
"trailer",
"transfer-encoding",
"upgrade",
"user-agent",
"via"
};
private static boolean isProhibited(final String header) {
final String headerTL = header.toLowerCase();
for (final String prohibitedHeader : prohibitedHeaders) {
if (prohibitedHeader.equals(headerTL)) {
return true;
}
}
final boolean prohibitedPrefixMatch = headerTL.startsWith("proxy-") || headerTL.startsWith("sec-");
return prohibitedPrefixMatch;
}
private static boolean isWellFormattedHeaderValue(final String header, final String value) {
// TODO Needs implementation as per https://dvcs.w3.org/hg/xhr/raw-file/tip/Overview.html#the-setrequestheader()-method
return true;
}
// As per: http://www.w3.org/TR/XMLHttpRequest2/#the-setrequestheader-method
public void setRequestHeader(final String header, final String value) {
final int readyState = request.getReadyState();
if (readyState == NetworkRequest.STATE_LOADING) {
if (isWellFormattedHeaderValue(header, value)) {
if (!isProhibited(header)) {
request.addRequestedHeader(header, value);
} else {
// TODO: Throw exception?
System.out.println("Prohibited header: " + header);
}
} else {
throw new DOMException(DOMException.SYNTAX_ERR, "header or value not well formatted");
}
} else {
throw new DOMException(DOMException.INVALID_STATE_ERR, "Can't set header when request state is: " + readyState);
}
}
}