diff --git a/src/Common/org/lobobrowser/util/WeakValueHashMap.java b/src/Common/org/lobobrowser/util/WeakValueHashMap.java index 23116493..931ee498 100644 --- a/src/Common/org/lobobrowser/util/WeakValueHashMap.java +++ b/src/Common/org/lobobrowser/util/WeakValueHashMap.java @@ -109,8 +109,8 @@ public Collection values() { checkQueue(); final Stream m = this.map.values().stream() - .map(t -> t == null ? null : t.get()) - .filter(t -> t != null); + .map(t -> t == null ? null : t.get()) + .filter(t -> t != null); return m.collect(Collectors.toList()); } diff --git a/src/Common/org/lobobrowser/util/io/IORoutines.java b/src/Common/org/lobobrowser/util/io/IORoutines.java index 2b4e60e9..e4890260 100644 --- a/src/Common/org/lobobrowser/util/io/IORoutines.java +++ b/src/Common/org/lobobrowser/util/io/IORoutines.java @@ -75,7 +75,7 @@ public static byte[] load(final File file) throws IOException { throw new IOException("File '" + file.getName() + "' too big"); } try ( - final InputStream in = new FileInputStream(file)) { + final InputStream in = new FileInputStream(file)) { return loadExact(in, (int) fileLength); } } @@ -137,7 +137,7 @@ public static boolean equalContent(final File file, final byte[] content) throws } try ( - final InputStream in = new FileInputStream(file);) { + final InputStream in = new FileInputStream(file);) { final byte[] fileContent = loadExact(in, (int) length); return java.util.Arrays.equals(content, fileContent); } @@ -145,7 +145,7 @@ public static boolean equalContent(final File file, final byte[] content) throws public static void save(final File file, final byte[] content) throws IOException { try ( - final FileOutputStream out = new FileOutputStream(file);) { + final FileOutputStream out = new FileOutputStream(file);) { out.write(content); } } @@ -179,9 +179,9 @@ public static void touch(final File file) { public static void saveStrings(final File file, final Collection list) throws IOException { try ( - final FileOutputStream fout = new FileOutputStream(file); - final BufferedOutputStream bout = new BufferedOutputStream(fout); - final PrintWriter writer = new PrintWriter(bout)) { + final FileOutputStream fout = new FileOutputStream(file); + final BufferedOutputStream bout = new BufferedOutputStream(fout); + final PrintWriter writer = new PrintWriter(bout)) { list.forEach(text -> writer.println(text)); writer.flush(); } @@ -189,8 +189,8 @@ public static void saveStrings(final File file, final Collection list) t public static java.util.List loadStrings(final File file) throws IOException { try ( - final InputStream in = new FileInputStream(file); - final BufferedReader reader = new BufferedReader(new InputStreamReader(in))) { + final InputStream in = new FileInputStream(file); + final BufferedReader reader = new BufferedReader(new InputStreamReader(in))) { return reader.lines().collect(Collectors.toList()); } } diff --git a/src/HTML_Renderer/org/lobobrowser/html/BrowserFrame.java b/src/HTML_Renderer/org/lobobrowser/html/BrowserFrame.java index 8c9c2c23..e590681c 100644 --- a/src/HTML_Renderer/org/lobobrowser/html/BrowserFrame.java +++ b/src/HTML_Renderer/org/lobobrowser/html/BrowserFrame.java @@ -26,6 +26,9 @@ import java.awt.Component; import java.net.URL; +import org.lobobrowser.ua.ParameterInfo; +import org.lobobrowser.ua.RequestType; +import org.lobobrowser.ua.TargetType; import org.w3c.dom.Document; /** @@ -78,4 +81,7 @@ public interface BrowserFrame { * See constants in {@link org.lobobrowser.html.style.RenderState}. */ public void setDefaultOverflowY(int overflowY); + + // Trying out a way for a frame's target to be set to an iframe. for issue #96 + public void navigate(URL url, String method, ParameterInfo pinfo, TargetType targetType, RequestType form); } diff --git a/src/HTML_Renderer/org/lobobrowser/html/domimpl/DescendentHTMLCollection.java b/src/HTML_Renderer/org/lobobrowser/html/domimpl/DescendentHTMLCollection.java index ce4a8271..d8c79f6a 100644 --- a/src/HTML_Renderer/org/lobobrowser/html/domimpl/DescendentHTMLCollection.java +++ b/src/HTML_Renderer/org/lobobrowser/html/domimpl/DescendentHTMLCollection.java @@ -31,7 +31,9 @@ import java.util.Map; import org.lobobrowser.js.AbstractScriptableDelegate; +import org.lobobrowser.js.JavaScript; import org.lobobrowser.util.Nodes; +import org.lobobrowser.util.Objects; import org.w3c.dom.Node; import org.w3c.dom.html.HTMLCollection; @@ -116,6 +118,31 @@ public Node item(final int index) { } } + public Node item(final Object obj) { + // Assuming that non integer calls are supposed to be treated as zero, as per this test: + // https://github.com/w3c/web-platform-tests/blob/master/dom/nodes/Element-children.html#L10 + // Reported bug https://github.com/w3c/web-platform-tests/issues/1264 + if (obj instanceof Integer) { + final Integer index = (Integer) obj; + return item((int) index); + } + return item(0); + } + + /* Quick hack to get w3c tests working. This function very likely needs to + * be included for every JS object. */ + public boolean hasOwnProperty(final Object obj) { + System.out.println("Checking " + obj); + if (Objects.isAssignableOrBox(obj, Integer.TYPE)) { + // if (obj instanceof Integer) { + final Integer i = (Integer) JavaScript.getInstance().getJavaObject(obj, Integer.TYPE); + // final Integer i = (Integer) obj; + return i < getLength(); + } else { + return false; + } + } + public Node namedItem(final String name) { synchronized (this.treeLock) { this.ensurePopulatedImpl(); diff --git a/src/HTML_Renderer/org/lobobrowser/html/domimpl/DocumentFragmentImpl.java b/src/HTML_Renderer/org/lobobrowser/html/domimpl/DocumentFragmentImpl.java index cce44ff4..b0caf180 100644 --- a/src/HTML_Renderer/org/lobobrowser/html/domimpl/DocumentFragmentImpl.java +++ b/src/HTML_Renderer/org/lobobrowser/html/domimpl/DocumentFragmentImpl.java @@ -27,9 +27,9 @@ import org.w3c.dom.DocumentFragment; import org.w3c.dom.Node; -public class DocumentFragmentImpl extends NodeImpl implements DocumentFragment { +public class DocumentFragmentImpl extends ElementImpl implements DocumentFragment { public DocumentFragmentImpl() { - super(); + super("#document-fragment"); } @Override diff --git a/src/HTML_Renderer/org/lobobrowser/html/domimpl/ElementFactory.java b/src/HTML_Renderer/org/lobobrowser/html/domimpl/ElementFactory.java index c53acc7e..aaa46cef 100644 --- a/src/HTML_Renderer/org/lobobrowser/html/domimpl/ElementFactory.java +++ b/src/HTML_Renderer/org/lobobrowser/html/domimpl/ElementFactory.java @@ -123,6 +123,9 @@ public final HTMLElement createElement(final HTMLDocumentImpl document, final St final HTMLElementBuilder builder = this.builders.get(normalName); if (builder == null) { // TODO: IE would assume name is html text here? + // TODO: ^^ Other browsers throw an exception if there are illegal characters in the name. + // But am not sure what the legal character set is. Characters like angle-brackets + // do throw an exception in Chromium and Firefox. - hrj final HTMLElementImpl element = new HTMLElementImpl(name); element.setOwnerDocument(document); return element; diff --git a/src/HTML_Renderer/org/lobobrowser/html/domimpl/ElementImpl.java b/src/HTML_Renderer/org/lobobrowser/html/domimpl/ElementImpl.java index 582d68b8..2303e4b9 100644 --- a/src/HTML_Renderer/org/lobobrowser/html/domimpl/ElementImpl.java +++ b/src/HTML_Renderer/org/lobobrowser/html/domimpl/ElementImpl.java @@ -139,10 +139,10 @@ public void setDir(final String dir) { public final String getAttribute(final String name) { final String normalName = normalizeAttributeName(name); - synchronized (this) { - final Map attributes = this.attributes; - return attributes == null ? null : attributes.get(normalName); - } + // synchronized (this) { + final Map attributes = this.attributes; + return attributes == null ? null : attributes.get(normalName); + // } } private Attr getAttr(final String normalName, final String value) { @@ -171,6 +171,7 @@ protected static boolean isTagName(final Node node, final String name) { return node.getNodeName().equalsIgnoreCase(name); } + @Override public NodeList getElementsByTagName(final String name) { final boolean matchesAll = "*".equals(name); final List descendents = new LinkedList<>(); @@ -206,15 +207,18 @@ public TypeInfo getSchemaTypeInfo() { } public String getTagName() { - return this.getNodeName(); + // In HTML, tag names are supposed to be returned in upper-case, but in XHTML they are returned in original case + // as per https://developer.mozilla.org/en-US/docs/Web/API/Element.tagName + return this.getNodeName().toUpperCase(); } public boolean hasAttribute(final String name) { final String normalName = normalizeAttributeName(name); - synchronized (this) { - final Map attributes = this.attributes; - return attributes == null ? false : attributes.containsKey(normalName); - } + // This was causing deadlocks, hence removed the sync + // synchronized (this) { + final Map attributes = this.attributes; + return attributes == null ? false : attributes.containsKey(normalName); + // } } public boolean hasAttributeNS(final String namespaceURI, final String localName) throws DOMException { @@ -238,6 +242,8 @@ public void removeAttributeNS(final String namespaceURI, final String localName) } protected void assignAttributeField(final String normalName, final String value) { + // Not sure why id and name were conflated in this code: they are orthogonal. + // Note: overriders assume that processing here is only done after // checking attribute names, i.e. they may not call the super // implementation if an attribute is already taken care of. @@ -531,4 +537,41 @@ private void updateIdMap(final String oldIdValue, final String newIdValue) { } } } + + // TODO: Need to implement these for Document and DocumentFragment as well as per https://developer.mozilla.org/en-US/docs/Web/API/ParentNode + public Element getFirstElementChild() { + final ArrayList nl = this.nodeList; + for (final Node n : nl) { + if (n instanceof Element) { + return (Element) n; + } + } + + return null; + } + + public Element getLastElementChild() { + final ArrayList nl = this.nodeList; + final int N = nl.size(); + for (int i = N - 1; i >= 0; i--) { + final Node n = nl.get(i); + if (n instanceof Element) { + return (Element) n; + } + } + + return null; + } + + public int getChildElementCount() { + final ArrayList nl = this.nodeList; + int count = 0; + for (final Node n : nl) { + if (n instanceof Element) { + count++; + } + } + + return count; + } } diff --git a/src/HTML_Renderer/org/lobobrowser/html/domimpl/HTMLAbstractUIElement.java b/src/HTML_Renderer/org/lobobrowser/html/domimpl/HTMLAbstractUIElement.java index 7a0d1365..d4b6e405 100644 --- a/src/HTML_Renderer/org/lobobrowser/html/domimpl/HTMLAbstractUIElement.java +++ b/src/HTML_Renderer/org/lobobrowser/html/domimpl/HTMLAbstractUIElement.java @@ -5,6 +5,7 @@ import java.util.logging.Level; import org.lobobrowser.html.js.Executor; +import org.lobobrowser.html.js.Window; import org.lobobrowser.js.JavaScript; import org.lobobrowser.ua.UserAgentContext; import org.mozilla.javascript.Context; @@ -170,12 +171,12 @@ protected Function getEventFunction(final Function varValue, final String attrib if (doc == null) { throw new IllegalStateException("Element does not belong to a document."); } - final Context ctx = Executor.createContext(this.getDocumentURL(), uac); + final Window window = ((HTMLDocumentImpl) doc).getWindow(); + final Context ctx = Executor.createContext(this.getDocumentURL(), uac, window.windowFactory); try { - final Scriptable scope = (Scriptable) doc.getUserData(Executor.SCOPE_KEY); + final Scriptable scope = window.getWindowScope(); if (scope == null) { - throw new IllegalStateException("Scriptable (scope) instance was expected to be keyed as UserData to document using " - + Executor.SCOPE_KEY); + throw new IllegalStateException("Scriptable (scope) instance was null"); } final Scriptable thisScope = (Scriptable) JavaScript.getInstance().getJavascriptObject(this, scope); try { diff --git a/src/HTML_Renderer/org/lobobrowser/html/domimpl/HTMLBaseInputElement.java b/src/HTML_Renderer/org/lobobrowser/html/domimpl/HTMLBaseInputElement.java index ee790eb9..e921630b 100644 --- a/src/HTML_Renderer/org/lobobrowser/html/domimpl/HTMLBaseInputElement.java +++ b/src/HTML_Renderer/org/lobobrowser/html/domimpl/HTMLBaseInputElement.java @@ -26,7 +26,8 @@ import java.util.ArrayList; import org.lobobrowser.html.FormInput; -import org.lobobrowser.html.js.Executor; +import org.lobobrowser.html.js.Event; +import org.lobobrowser.html.js.NotGetterSetter; import org.mozilla.javascript.Function; import org.w3c.dom.Node; import org.w3c.dom.html.HTMLFormElement; @@ -381,11 +382,16 @@ private void dispatchEvent(final String expectedImgSrc, final ImageEvent event) // Inform listener, holding no lock. listenerArray[i].imageLoaded(event); } + + // TODO: With this change, setOnLoad method should add a listener with dispatch mechanism. Best implemented in a parent class. + dispatchEvent(new Event("load", this)); + + /* final Function onload = this.getOnload(); if (onload != null) { // TODO: onload event object? Executor.executeFunction(HTMLBaseInputElement.this, onload, null); - } + }*/ } private class LocalImageListener implements ImageListener { @@ -398,6 +404,14 @@ public LocalImageListener(final String imgSrc) { public void imageLoaded(final ImageEvent event) { dispatchEvent(this.expectedImgSrc, event); } + + public void imageAborted() { + // Do nothing + } } + @NotGetterSetter + public void setCustomValidity(final String message) { + System.out.println("HTMLBaseInputElement.setCustomValidity() " + message); + } } diff --git a/src/HTML_Renderer/org/lobobrowser/html/domimpl/HTMLButtonElementImpl.java b/src/HTML_Renderer/org/lobobrowser/html/domimpl/HTMLButtonElementImpl.java index 7711934a..fa4797e8 100644 --- a/src/HTML_Renderer/org/lobobrowser/html/domimpl/HTMLButtonElementImpl.java +++ b/src/HTML_Renderer/org/lobobrowser/html/domimpl/HTMLButtonElementImpl.java @@ -24,4 +24,10 @@ public class HTMLButtonElementImpl extends HTMLBaseInputElement { public HTMLButtonElementImpl(final String name) { super(name); } + + public void click() { + // TODO: see issue #95 + System.out.println("Button clicked. TODO"); + // inputContext.click(); + } } diff --git a/src/HTML_Renderer/org/lobobrowser/html/domimpl/HTMLDocumentImpl.java b/src/HTML_Renderer/org/lobobrowser/html/domimpl/HTMLDocumentImpl.java index fc491c69..e330482a 100644 --- a/src/HTML_Renderer/org/lobobrowser/html/domimpl/HTMLDocumentImpl.java +++ b/src/HTML_Renderer/org/lobobrowser/html/domimpl/HTMLDocumentImpl.java @@ -37,6 +37,9 @@ import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.concurrent.Semaphore; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; import java.util.logging.Logger; @@ -52,9 +55,10 @@ import org.lobobrowser.html.domimpl.NodeFilter.TagNameFilter; import org.lobobrowser.html.io.WritableLineReader; import org.lobobrowser.html.js.Event; -import org.lobobrowser.html.js.Executor; +import org.lobobrowser.html.js.EventTargetManager; import org.lobobrowser.html.js.Location; import org.lobobrowser.html.js.Window; +import org.lobobrowser.html.js.Window.JSRunnableTask; import org.lobobrowser.html.parser.HtmlParser; import org.lobobrowser.html.style.RenderState; import org.lobobrowser.html.style.StyleElements; @@ -85,6 +89,9 @@ import org.w3c.dom.Text; import org.w3c.dom.UserDataHandler; import org.w3c.dom.css.CSSStyleSheet; +import org.w3c.dom.events.EventException; +import org.w3c.dom.events.EventListener; +import org.w3c.dom.events.EventTarget; import org.w3c.dom.html.HTMLCollection; import org.w3c.dom.html.HTMLDocument; import org.w3c.dom.html.HTMLElement; @@ -102,7 +109,7 @@ /** * Implementation of the W3C HTMLDocument interface. */ -public class HTMLDocumentImpl extends NodeImpl implements HTMLDocument, DocumentView, DocumentStyle { +public class HTMLDocumentImpl extends NodeImpl implements HTMLDocument, DocumentView, DocumentStyle, EventTarget { private static final Logger logger = Logger.getLogger(HTMLDocumentImpl.class.getName()); private final ElementFactory factory; private final HtmlRendererContext rcontext; @@ -138,7 +145,10 @@ public HTMLDocumentImpl(final UserAgentContext ucontext, final HtmlRendererConte // no permission to connect to the host of the URL. // This is so that cookies cannot be written arbitrarily // with setCookie() method. - sm.checkPermission(new java.net.SocketPermission(docURL.getHost(), "connect")); + final String protocol = docURL.getProtocol(); + if ("http".equalsIgnoreCase(protocol) || "https".equalsIgnoreCase(protocol)) { + sm.checkPermission(new java.net.SocketPermission(docURL.getHost(), "connect")); + } } this.documentURL = docURL; this.domain = docURL.getHost(); @@ -237,10 +247,15 @@ public void setDefaultTarget(final String value) { this.defaultTarget = value; } + // TODO: Is this required? Check JS DOM specs. public AbstractView getDefaultView() { return this.window; } + public Window getWindow() { + return this.window; + } + @Override public String getTextContent() throws DOMException { return null; @@ -368,16 +383,19 @@ public String getCookie() { } public void setCookie(final String cookie) throws DOMException { + // Update in gngr: Whoa! No, we won't allow the privilege escalation below until better justification is presented ;) + // Chagned for Issue #78 + // Justification: A caller (e.g. Google Analytics script) // might want to set cookies on the parent document. // If the caller has access to the document, it appears // they should be able to set cookies on that document. // Note that this Document instance cannot be created // with an arbitrary URL. - SecurityUtil.doPrivileged(() -> { - ucontext.setCookie(documentURL, cookie); - return null; - }); + // SecurityUtil.doPrivileged(() -> { + ucontext.setCookie(documentURL, cookie); + // return null; + // }); } public void open() { @@ -594,6 +612,7 @@ public EntityReference createEntityReference(final String name) throws DOMExcept * The element tag name or an asterisk character (*) to match all * elements. */ + @Override public NodeList getElementsByTagName(final String tagname) { if ("*".equals(tagname)) { return this.getNodeList(new ElementFilter()); @@ -607,7 +626,10 @@ public Node importNode(final Node importedNode, final boolean deep) throws DOMEx } public Element createElementNS(final String namespaceURI, final String qualifiedName) throws DOMException { - System.out.println("request to create element: " + namespaceURI + " : " + qualifiedName); + if ("http://www.w3.org/1999/xhtml".equalsIgnoreCase(namespaceURI)) { + return createElement(qualifiedName); + } + System.out.println("unhandled request to create element in NS: " + namespaceURI + " with tag: " + qualifiedName); return null; // TODO // throw new DOMException(DOMException.NOT_SUPPORTED_ERR, "Not implemented: createElementNS"); @@ -1151,17 +1173,34 @@ protected void loadImage(final String relativeUri, final ImageListener imageList // Must remove from map in the locked block // that got the listeners. Otherwise a new // listener might miss the event?? - map.remove(urlText); + map.remove(urlText); + } + if (listeners != null) { + final int llength = listeners.length; + for (int i = 0; i < llength; i++) { + // Call holding no locks + listeners[i].imageLoaded(newEvent); } - if (listeners != null) { - final int llength = listeners.length; - for (int i = 0; i < llength; i++) { - // Call holding no locks - listeners[i].imageLoaded(newEvent); - } + } + } else if (httpRequest.getReadyState() == NetworkRequest.STATE_ABORTED) { + ImageListener[] listeners; + synchronized (map) { + newInfo.loaded = true; + listeners = newInfo.getListeners(); + // Must remove from map in the locked block + // that got the listeners. Otherwise a new + // listener might miss the event?? + map.remove(urlText); + } + if (listeners != null) { + final int llength = listeners.length; + for (int i = 0; i < llength; i++) { + // Call holding no locks + listeners[i].imageAborted(); } } - }); + } + }); SecurityUtil.doPrivileged(() -> { try { @@ -1203,14 +1242,22 @@ private void dispatchLoadEvent() { final Function onloadHandler = this.onloadHandler; if (onloadHandler != null) { // TODO: onload event object? - Executor.executeFunction(this, onloadHandler, null); + throw new UnsupportedOperationException(); + // TODO: Use the event dispatcher + // Executor.executeFunction(this, onloadHandler, null); } final Event loadEvent = new Event("load", getBody()); // TODO: What should be the target for this event? - dispatchEventToHandlers(loadEvent, onloadHandlers); + // dispatchEventToHandlers(loadEvent, onloadHandlers); final Event domContentLoadedEvent = new Event("DOMContentLoaded", getBody()); // TODO: What should be the target for this event? dispatchEvent(domContentLoadedEvent); + + window.domContentLoaded(domContentLoadedEvent); + } + + protected EventTargetManager getEventTargetManager() { + return window.getEventTargetManager(); } @Override @@ -1274,16 +1321,74 @@ public void removeLoadHandler(final Function handler) { } private List jobs = new LinkedList<>(); + // private int registeredJobs = 0; + private final AtomicInteger registeredJobs = new AtomicInteger(0); + private final Semaphore doneAllJobs = new Semaphore(0); + private final AtomicBoolean stopRequested = new AtomicBoolean(false); + private int oldPendingTaskId = -1; + + // TODO: This should be hidden from JS + public void stopEverything() { + if (stopRequested.get()) { + throw new IllegalStateException("Stop requested twice!"); + } + stopRequested.set(true); + if (modificationsStarted.get()) { + boolean done = false; + while (!done) { + try { + System.out.println("In : " + getBaseURI() + " Acquiring Semaphore: " + doneAllJobs); + doneAllJobs.acquire(); + done = true; + } catch (final InterruptedException e) { + System.err.println("Was interrupted but will keep trying!"); + e.printStackTrace(); + } + } + } + } + // TODO: This should be hidden from JS public void addJob(final Runnable job) { + addJob(job, 1); + } + + // TODO: This should be hidden from JS + public void addJob(final Runnable job, final int incr) { synchronized (jobs) { + registeredJobs.addAndGet(incr); jobs.add(job); + + // Added into synch block because of the JS Uniq task change. (old Id should be protected from parallel mod) + if (modificationsOver.get()) { + // TODO: temp hack. Not sure if spawning an entirely new thread is right. But it helps with a deadlock in + // test_script_iframe_load (test number 3) + // new Thread() { + // public void run() { + // runAllPending(); + // }; + // }.start(); + + // TODO: temp hack 2. This seems more legitimate than hack #1. + /* + window.addJSTask(new JSRunnableTask(0, "todo: quick check to run all pending jobs", () -> { + runAllPending(); + })); + */ + + // TODO: temp hack 3. This seems more legitimate than hack #1 and optimisation over #2. + oldPendingTaskId = window.addJSUniqueTask(oldPendingTaskId, new JSRunnableTask(0, "todo: quick check to run all pending jobs", + () -> { + runAllPending(); + })); + // runAllPending(); + } } } private void runAllPending() { boolean done = false; - while (!done) { + while (!done && !stopRequested.get()) { List jobsCopy; synchronized (jobs) { jobsCopy = jobs; @@ -1294,12 +1399,42 @@ private void runAllPending() { done = jobs.size() == 0; } } + doneAllJobs.release(); + } + + // TODO: This should be hidden from JS + public void markJobsFinished(final int numJobs) { + final int curr = registeredJobs.addAndGet(-numJobs); + if (curr < 0) { + throw new IllegalStateException("More jobs over than registered!"); + } else if (curr == 0) { + if (!stopRequested.get() && !loadOver.get()) { + loadOver.set(true); + dispatchLoadEvent(); + System.out.println("In " + baseURI); + System.out.println(" calling window.jobsFinished()"); + window.jobsFinished(); + } + } } + private final AtomicBoolean modificationsStarted = new AtomicBoolean(false); + private final AtomicBoolean modificationsOver = new AtomicBoolean(false); + private final AtomicBoolean loadOver = new AtomicBoolean(false); + + // TODO: This should be hidden from JS public void finishModifications() { StyleElements.normalizeHTMLTree(this); - runAllPending(); - dispatchLoadEvent(); + // Not sure if this should be run in new thread. But this blocks the UI sometimes when it is in the same thread, and a network request hangs. + new Thread(() -> { + modificationsStarted.set(true); + runAllPending(); + modificationsOver.set(true); + }).start(); + + // This is to trigger a check in the no external resource case. + // On second thoughts, this may not be required. The window load event need only be fired if there is a script + // markJobsFinished(0); /* Nodes.forEachNode(document, node -> { if (node instanceof NodeImpl) { @@ -1344,12 +1479,15 @@ public List getDocStyleSheets() { }; private List getDocStyleSheetList() { - synchronized (treeLock) { + synchronized (this) { if (styleSheets == null) { styleSheets = new ArrayList<>(); final List docStyles = new ArrayList<>(); - scanElementStyleSheets(docStyles, HTMLDocumentImpl.this); + synchronized (treeLock) { + scanElementStyleSheets(docStyles, HTMLDocumentImpl.this); + } styleSheets.addAll(docStyles); + System.out.println("Found stylesheets: " + this.styleSheets.size()); } return this.styleSheets; } @@ -1372,23 +1510,36 @@ private void scanElementStyleSheets(final List styles, final } } + private volatile List enabledJStyleSheets = null; + // TODO enabled style sheets can be cached List getEnabledJStyleSheets() { - final List documentStyles = this.getDocStyleSheetList(); - final List jStyleSheets = new ArrayList<>(); - for (final JStyleSheetWrapper style : documentStyles) { - if ((!style.getDisabled()) && (style.getJStyleSheet() != null)) { - jStyleSheets.add(style.getJStyleSheet()); + synchronized (this) { + if (enabledJStyleSheets != null) { + return enabledJStyleSheets; + } + final List documentStyles = this.getDocStyleSheetList(); + final List jStyleSheets = new ArrayList<>(); + for (final JStyleSheetWrapper style : documentStyles) { + if ((!style.getDisabled()) && (style.getJStyleSheet() != null)) { + jStyleSheets.add(style.getJStyleSheet()); + } } + enabledJStyleSheets = jStyleSheets; + return jStyleSheets; } - return jStyleSheets; } void invalidateStyles() { synchronized (treeLock) { this.styleSheets = null; + getDocStyleSheetList(); + } + synchronized (this) { + this.enabledJStyleSheets = null; } - allInvalidated(); + System.out.println("Stylesheets set to null"); + allInvalidated(true); } StyleSheetList constructStyleSheetList() { @@ -1396,4 +1547,22 @@ StyleSheetList constructStyleSheetList() { } } + + @Override + public void addEventListener(final String type, final EventListener listener, final boolean useCapture) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException(); + } + + @Override + public void removeEventListener(final String type, final EventListener listener, final boolean useCapture) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException(); + } + + @Override + public boolean dispatchEvent(final org.w3c.dom.events.Event evt) throws EventException { + // TODO Auto-generated method stub + return false; + } } diff --git a/src/HTML_Renderer/org/lobobrowser/html/domimpl/HTMLElementImpl.java b/src/HTML_Renderer/org/lobobrowser/html/domimpl/HTMLElementImpl.java index 4ff5654f..9d7a9a6c 100644 --- a/src/HTML_Renderer/org/lobobrowser/html/domimpl/HTMLElementImpl.java +++ b/src/HTML_Renderer/org/lobobrowser/html/domimpl/HTMLElementImpl.java @@ -26,6 +26,8 @@ import java.io.Reader; import java.io.StringReader; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import java.util.List; @@ -50,19 +52,25 @@ import org.w3c.dom.DOMException; import org.w3c.dom.Node; import org.w3c.dom.NodeList; +import org.w3c.dom.events.Event; +import org.w3c.dom.events.EventException; +import org.w3c.dom.events.EventListener; +import org.w3c.dom.events.EventTarget; import org.w3c.dom.html.HTMLElement; import org.w3c.dom.html.HTMLFormElement; import cz.vutbr.web.css.CSSException; import cz.vutbr.web.css.CSSFactory; +import cz.vutbr.web.css.CSSProperty; import cz.vutbr.web.css.NodeData; import cz.vutbr.web.css.Selector; import cz.vutbr.web.css.Selector.PseudoDeclaration; import cz.vutbr.web.css.StyleSheet; +import cz.vutbr.web.css.Term; import cz.vutbr.web.csskit.MatchConditionOnElements; import cz.vutbr.web.domassign.DirectAnalyzer; -public class HTMLElementImpl extends ElementImpl implements HTMLElement, CSS2PropertiesContext { +public class HTMLElementImpl extends ElementImpl implements HTMLElement, CSS2PropertiesContext, EventTarget { private final boolean noStyleSheet; private static final MatchConditionOnElements elementMatchCondition = new MatchConditionOnElements(); private static final StyleSheet recommendedStyle = parseStyle(CSSNorm.stdStyleSheet(), StyleSheet.Origin.AGENT); @@ -81,18 +89,23 @@ public HTMLElementImpl(final String name) { protected final void forgetLocalStyle() { synchronized (this) { //TODO to be reconsidered in issue #41 + + this.currentStyle = null; + this.cachedNodeData = null; + //TODO to be removed during code cleanup /* this.currentStyleDeclarationState = null; this.localStyleDeclarationState = null; this.computedStyles = null; */ } + } protected final void forgetStyle(final boolean deep) { // TODO: OPTIMIZATION: If we had a ComputedStyle map in // window (Mozilla model) the map could be cleared in one shot. - synchronized (this) { + synchronized (treeLock) { //TODO to be reconsidered in issue #41 /* this.currentStyleDeclarationState = null; @@ -100,6 +113,8 @@ protected final void forgetStyle(final boolean deep) { this.isHoverStyle = null; this.hasHoverStyleByElement = null; */ + this.currentStyle = null; + this.cachedNodeData = null; if (deep) { final java.util.ArrayList nl = this.nodeList; if (nl != null) { @@ -115,6 +130,8 @@ protected final void forgetStyle(final boolean deep) { } } + private volatile JStyleProperties currentStyle = null; + /** * Gets the style object associated with the element. It may return null only * if the type of element does not handle stylesheets. @@ -123,7 +140,11 @@ protected final void forgetStyle(final boolean deep) { // TODO hide from JS public JStyleProperties getCurrentStyle() { synchronized (this) { - return new ComputedJStyleProperties(this, getNodeData(null), true); + if (currentStyle != null) { + return currentStyle; + } + currentStyle = new ComputedJStyleProperties(this, getNodeData(null), true); + return currentStyle; } } @@ -137,35 +158,44 @@ private static StyleSheet parseStyle(final String cssdata, final StyleSheet.Orig } } + private NodeData cachedNodeData = null; + // TODO Cache this method private NodeData getNodeData(final Selector.PseudoDeclaration psuedoElement) { - final HTMLDocumentImpl doc = (HTMLDocumentImpl) this.document; - final List jSheets = new ArrayList<>(); - jSheets.add(recommendedStyle); - jSheets.add(userAgentStyle); - jSheets.addAll(doc.styleSheetManager.getEnabledJStyleSheets()); + synchronized (this) { + if (cachedNodeData != null) { + return cachedNodeData; + } - final StyleSheet attributeStyle = StyleElements.convertAttributesToStyles(this); - if (attributeStyle != null) { - jSheets.add(attributeStyle); - } + final HTMLDocumentImpl doc = (HTMLDocumentImpl) this.document; + final List jSheets = new ArrayList<>(); + jSheets.add(recommendedStyle); + jSheets.add(userAgentStyle); + jSheets.addAll(doc.styleSheetManager.getEnabledJStyleSheets()); - final StyleSheet inlineStyle = this.getInlineJStyle(); - if (inlineStyle != null) { - jSheets.add(inlineStyle); - } + final StyleSheet attributeStyle = StyleElements.convertAttributesToStyles(this); + if (attributeStyle != null) { + jSheets.add(attributeStyle); + } - final DirectAnalyzer domAnalyser = new cz.vutbr.web.domassign.DirectAnalyzer(jSheets); - domAnalyser.registerMatchCondition(elementMatchCondition); + final StyleSheet inlineStyle = this.getInlineJStyle(); + if (inlineStyle != null) { + jSheets.add(inlineStyle); + } - final NodeData nodeData = domAnalyser.getElementStyle(this, psuedoElement, "screen"); - final Node parent = this.parentNode; - if ((parent != null) && (parent instanceof HTMLElementImpl)) { - final HTMLElementImpl parentElement = (HTMLElementImpl) parent; - nodeData.inheritFrom(parentElement.getNodeData(psuedoElement)); - nodeData.concretize(); + final DirectAnalyzer domAnalyser = new cz.vutbr.web.domassign.DirectAnalyzer(jSheets); + domAnalyser.registerMatchCondition(elementMatchCondition); + + final NodeData nodeData = domAnalyser.getElementStyle(this, psuedoElement, "screen"); + final Node parent = this.parentNode; + if ((parent != null) && (parent instanceof HTMLElementImpl)) { + final HTMLElementImpl parentElement = (HTMLElementImpl) parent; + nodeData.inheritFrom(parentElement.getNodeData(psuedoElement)); + nodeData.concretize(); + } + cachedNodeData = nodeData; + return nodeData; } - return nodeData; } /** @@ -272,6 +302,9 @@ protected void assignAttributeField(final String normalName, final String value) } else { if ("style".equals(normalName)) { this.forgetLocalStyle(); + // informDocumentInvalid(); + // informLayoutInvalid(); + // invalidateDescendentsForHover(); } } super.assignAttributeField(normalName, value); @@ -327,14 +360,51 @@ private void invalidateDescendentsForHoverImpl(final HTMLElementImpl ancestor) { } } + // TODO: request this feature from upstream + private static boolean isSameNodeData(final NodeData a, final NodeData b) { + final Collection aProps = a.getPropertyNames(); + final Collection bProps = b.getPropertyNames(); + if (aProps.size() == bProps.size()) { + for (final String ap : aProps) { + final Term aVal = a.getValue(ap, true); + final Term bVal = b.getValue(ap, true); + if (aVal != null) { + if (!aVal.equals(bVal)) { + return false; + } + } + final CSSProperty aProp = a.getProperty(ap); + final CSSProperty bProp = b.getProperty(ap); + if (!aProp.equals(bProp)) { + return false; + } + } + return true; + } + return false; + } + //TODO: need to optimize it by checking if there is hover style for the given element private boolean hasHoverStyle() { - return true; + final NodeData newNodeData = getNodeData(null); + if (currentStyle != null) { + return !isSameNodeData(newNodeData, currentStyle.getNodeData()); + } else { + return newNodeData == null; + } + // return false; } //TODO: need to optimize it by checking if there is hover style for the given element private boolean hasHoverStyle(final HTMLElementImpl ancestor) { - return true; + /* + final NodeData newNodeData = getNodeData(null); + if (currentStyle != null) { + return !isSameNodeData(newNodeData, currentStyle.getNodeData()); + } else { + return newNodeData == null; + }*/ + return false; } /** @@ -687,4 +757,111 @@ protected void handleDocumentAttachmentChanged() { } super.handleDocumentAttachmentChanged(); } + + public DOMTokenList getClassList() { + return new DOMTokenList(); + } + + // Based on http://www.w3.org/TR/dom/#domtokenlist + public final class DOMTokenList { + + private String[] getClasses() { + return getAttribute("class").split(" "); + } + + private String[] getClasses(final int max) { + return getAttribute("class").split(" ", max); + } + + public long getLength() { + return getClasses().length; + } + + public String item(final long index) { + final int indexInt = (int) index; + return getClasses(indexInt + 1)[0]; + } + + public boolean contains(final String token) { + return Arrays.stream(getClasses()).anyMatch(t -> t.equals(token)); + } + + public void add(final String token) { + add(new String[] { token }); + } + + public void add(final String[] tokens) { + final StringBuilder sb = new StringBuilder(); + for (final String token : tokens) { + if (token.length() == 0) { + throw new DOMException(DOMException.SYNTAX_ERR, "empty token"); + } + // TODO: Check for whitespace and throw IllegalCharacterError + + sb.append(' '); + sb.append(token); + } + setAttribute("class", getAttribute("class") + sb.toString()); + } + + public void remove(final String tokenToRemove) { + remove(new String[] { tokenToRemove }); + } + + public void remove(final String[] tokensToRemove) { + final String[] existingClasses = getClasses(); + final StringBuilder sb = new StringBuilder(); + for (final String clazz : existingClasses) { + if (!Arrays.stream(tokensToRemove).anyMatch(tr -> tr.equals(clazz))) { + sb.append(' '); + sb.append(clazz); + } + } + setAttribute("class", sb.toString()); + } + + public boolean toggle(final String tokenToToggle) { + final String[] existingClasses = getClasses(); + for (final String clazz : existingClasses) { + if (tokenToToggle.equals(clazz)) { + remove(tokenToToggle); + return false; + } + } + + // Not found, hence add + add(tokenToToggle); + return true; + } + + public boolean toggle(final String token, final boolean force) { + if (force) { + add(token); + } else { + remove(token); + } + return force; + } + + /* TODO: stringifier; */ + } + + @Override + public void addEventListener(final String type, final EventListener listener, final boolean useCapture) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException(); + } + + @Override + public void removeEventListener(final String type, final EventListener listener, final boolean useCapture) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException(); + } + + @Override + public boolean dispatchEvent(final Event evt) throws EventException { + // TODO Auto-generated method stub + throw new UnsupportedOperationException(); + // return false; + } } diff --git a/src/HTML_Renderer/org/lobobrowser/html/domimpl/HTMLFormElementImpl.java b/src/HTML_Renderer/org/lobobrowser/html/domimpl/HTMLFormElementImpl.java index 98563f0e..5dedae5d 100644 --- a/src/HTML_Renderer/org/lobobrowser/html/domimpl/HTMLFormElementImpl.java +++ b/src/HTML_Renderer/org/lobobrowser/html/domimpl/HTMLFormElementImpl.java @@ -30,6 +30,8 @@ import org.lobobrowser.html.FormInput; import org.lobobrowser.html.HtmlRendererContext; import org.lobobrowser.html.js.Executor; +import org.lobobrowser.html.js.Window; +import org.lobobrowser.html.js.Window.JSSupplierTask; import org.mozilla.javascript.Function; import org.w3c.dom.Element; import org.w3c.dom.Node; @@ -176,10 +178,27 @@ public final void submit(final FormInput[] extraFormInputs) { final Function onsubmit = this.getOnsubmit(); if (onsubmit != null) { // TODO: onsubmit event object? + /* if (!Executor.executeFunction(this, onsubmit, null)) { return; - } + } */ + // dispatchEvent(new Event("submit", this)); + final Window window = ((HTMLDocumentImpl) document).getWindow(); + window.addJSTask(new JSSupplierTask<>(0, () -> { + return Executor.executeFunction(this, onsubmit, null, window.windowFactory); + }, (result) -> { + System.out.println("Result of form submission function: " + result); + if (result) { + submitFormImpl(extraFormInputs); + } + })); + } else { + submitFormImpl(extraFormInputs); } + + } + + private void submitFormImpl(final FormInput[] extraFormInputs) { final HtmlRendererContext context = this.getHtmlRendererContext(); if (context != null) { final ArrayList formInputs = new ArrayList<>(); diff --git a/src/HTML_Renderer/org/lobobrowser/html/domimpl/HTMLIFrameElementImpl.java b/src/HTML_Renderer/org/lobobrowser/html/domimpl/HTMLIFrameElementImpl.java index 6926aa23..0d578d89 100644 --- a/src/HTML_Renderer/org/lobobrowser/html/domimpl/HTMLIFrameElementImpl.java +++ b/src/HTML_Renderer/org/lobobrowser/html/domimpl/HTMLIFrameElementImpl.java @@ -3,11 +3,17 @@ import java.net.URL; import org.lobobrowser.html.BrowserFrame; +import org.lobobrowser.html.js.Executor; import org.lobobrowser.html.js.Window; +import org.lobobrowser.html.js.Window.JSRunnableTask; import org.lobobrowser.html.style.IFrameRenderState; import org.lobobrowser.html.style.RenderState; +import org.lobobrowser.ua.ParameterInfo; +import org.lobobrowser.ua.RequestType; +import org.lobobrowser.ua.TargetType; import org.lobobrowser.ua.UserAgentContext.Request; import org.lobobrowser.ua.UserAgentContext.RequestKind; +import org.mozilla.javascript.Function; import org.w3c.dom.Document; import org.w3c.dom.html.HTMLIFrameElement; @@ -19,10 +25,40 @@ public HTMLIFrameElementImpl(final String name) { } public void setBrowserFrame(final BrowserFrame frame) { + System.out.println("iframe: Set browser frame"); this.browserFrame = frame; + createJob(); + /* final String src = this.getAttribute("src"); if (src != null) { - loadURLIntoFrame(src); + ((HTMLDocumentImpl) document).addJob(() -> loadURLIntoFrame(src)); + // loadURLIntoFrame(src); + }*/ + } + + // private AtomicBoolean jobCreated = new AtomicBoolean(false); + private boolean jobCreated = false; + + private void createJob() { + synchronized (this) { + final String src = this.getAttribute("src"); + System.out.println("Creating job with src: " + src); + if (src != null) { + if (!jobCreated) { + ((HTMLDocumentImpl) document).addJob(() -> loadURLIntoFrame(src)); + jobCreated = true; + } else { + ((HTMLDocumentImpl) document).addJob(() -> loadURLIntoFrame(src), 0); + // loadURLIntoFrame(src); + } + } + } + } + + private void markJobDone() { + synchronized (this) { + ((HTMLDocumentImpl) document).markJobsFinished(1); + jobCreated = false; } } @@ -142,14 +178,34 @@ public void setWidth(final String width) { this.setAttribute("width", width); } - /* + @Override protected void assignAttributeField(final String normalName, final String value) { + // if ("src".equals(normalName)) { + // loadURLIntoFrame(value); + // } + // } else { + super.assignAttributeField(normalName, value); + // } + if ("src".equals(normalName)) { - loadURLIntoFrame(value); - } else { - super.assignAttributeField(normalName, value); + /* + if (value != null) { + ((HTMLDocumentImpl) document).addJob(() -> loadURLIntoFrame(value)); + } */ + createJob(); + // loadURLIntoFrame(value); } - }*/ + } + + private Function onload; + + public Function getOnload() { + return this.getEventFunction(this.onload, "onload"); + } + + public void setOnload(final Function onload) { + this.onload = onload; + } private void loadURLIntoFrame(final String value) { final BrowserFrame frame = this.browserFrame; @@ -157,10 +213,32 @@ private void loadURLIntoFrame(final String value) { try { final URL fullURL = this.getFullURL(value); if (getUserAgentContext().isRequestPermitted(new Request(fullURL, RequestKind.Frame))) { + getContentWindow().setJobFinishedHandler(new Runnable() { + public void run() { + System.out.println("Iframes window's job over!"); + if (onload != null) { + // TODO: onload event object? + final Window window = ((HTMLDocumentImpl) document).getWindow(); + window.addJSTask(new JSRunnableTask(0, "IFrame onload handler", () -> { + Executor.executeFunction(HTMLIFrameElementImpl.this, onload, null, window.windowFactory); + })); + } + markJobDone(); + } + }); frame.loadURL(fullURL); } } catch (final java.net.MalformedURLException mfu) { this.warn("loadURLIntoFrame(): Unable to navigate to src.", mfu); + } finally { + /* TODO: Implement an onload handler + // Copied from image element + final Function onload = this.getOnload(); + System.out.println("onload: " + onload); + if (onload != null) { + // TODO: onload event object? + Executor.executeFunction(HTMLIFrameElementImpl.this, onload, null); + }*/ } } } @@ -169,4 +247,34 @@ private void loadURLIntoFrame(final String value) { protected RenderState createRenderState(final RenderState prevRenderState) { return new IFrameRenderState(prevRenderState, this); } + + // Trying out a way for a frame's target to be set to an iframe. for issue #96 + + public void navigate(final URL url, final String method, final ParameterInfo pinfo, final TargetType targetType, final RequestType form) { + final Window window = ((HTMLDocumentImpl) document).getWindow(); + window.addJSTask(new JSRunnableTask(0, "Frame navigation to " + url, () -> { + final BrowserFrame frame = this.browserFrame; + if (frame != null) { + if (getUserAgentContext().isRequestPermitted(new Request(url, RequestKind.Frame))) { + getContentWindow().setJobFinishedHandler(new Runnable() { + public void run() { + System.out.println("Iframes window's job over!"); + if (onload != null) { + // TODO: onload event object? + final Window window = ((HTMLDocumentImpl) document).getWindow(); + window.addJSTask(new JSRunnableTask(0, "IFrame onload handler", () -> { + Executor.executeFunction(HTMLIFrameElementImpl.this, onload, null, window.windowFactory); + })); + } + // markJobDone(); + } + }); + // frame.loadURL(fullURL); + browserFrame.navigate(url, method, pinfo, targetType, form); + } + // browserFrame.navigate(url, method, pinfo, targetType, form); + } + } + )); + } } diff --git a/src/HTML_Renderer/org/lobobrowser/html/domimpl/HTMLImageElementImpl.java b/src/HTML_Renderer/org/lobobrowser/html/domimpl/HTMLImageElementImpl.java index ccfe5d16..5ccf3944 100644 --- a/src/HTML_Renderer/org/lobobrowser/html/domimpl/HTMLImageElementImpl.java +++ b/src/HTML_Renderer/org/lobobrowser/html/domimpl/HTMLImageElementImpl.java @@ -26,6 +26,7 @@ import java.util.ArrayList; import org.lobobrowser.html.js.Executor; +import org.lobobrowser.html.js.Window; import org.lobobrowser.html.style.ImageRenderState; import org.lobobrowser.html.style.RenderState; import org.mozilla.javascript.Function; @@ -186,9 +187,12 @@ protected void assignAttributeField(final String normalName, final String value) super.assignAttributeField(normalName, value); // Commenting out for TODO: #3 - /* if ("src".equals(normalName)) { - this.loadImage(value); - }*/ + // Uncommenting because we have a job queue now + if ("src".equals(normalName)) { + // Converting to deffered job + ((HTMLDocumentImpl) document).addJob(() -> loadImage(getSrc())); + // this.loadImage(value); + } } private Function onload; @@ -282,7 +286,8 @@ private void dispatchEvent(final String expectedImgSrc, final ImageEvent event) final Function onload = this.getOnload(); if (onload != null) { // TODO: onload event object? - Executor.executeFunction(HTMLImageElementImpl.this, onload, null); + final Window window = ((HTMLDocumentImpl) document).getWindow(); + Executor.executeFunction(HTMLImageElementImpl.this, onload, null, window.windowFactory); } } @@ -300,6 +305,17 @@ public LocalImageListener(final String imgSrc) { public void imageLoaded(final ImageEvent event) { dispatchEvent(this.expectedImgSrc, event); + if (document instanceof HTMLDocumentImpl) { + final HTMLDocumentImpl htmlDocumentImpl = (HTMLDocumentImpl) document; + htmlDocumentImpl.markJobsFinished(1); + } + } + + public void imageAborted() { + if (document instanceof HTMLDocumentImpl) { + final HTMLDocumentImpl htmlDocumentImpl = (HTMLDocumentImpl) document; + htmlDocumentImpl.markJobsFinished(1); + } } } diff --git a/src/HTML_Renderer/org/lobobrowser/html/domimpl/HTMLLinkElementImpl.java b/src/HTML_Renderer/org/lobobrowser/html/domimpl/HTMLLinkElementImpl.java index 3ebf6b4f..5f7880fd 100644 --- a/src/HTML_Renderer/org/lobobrowser/html/domimpl/HTMLLinkElementImpl.java +++ b/src/HTML_Renderer/org/lobobrowser/html/domimpl/HTMLLinkElementImpl.java @@ -22,10 +22,12 @@ import java.net.MalformedURLException; import java.net.URL; +import java.util.Optional; import java.util.logging.Level; import java.util.logging.Logger; import org.lobobrowser.html.HtmlRendererContext; +import org.lobobrowser.html.js.Window; import org.lobobrowser.html.style.CSSUtilities; import org.lobobrowser.ua.UserAgentContext; import org.lobobrowser.util.Urls; @@ -142,26 +144,108 @@ private boolean isWellFormedURL() { } } + /*private Optional getAbsoluteURL() { + final HtmlRendererContext rcontext = this.getHtmlRendererContext(); + if (rcontext != null) { + final String href = this.getHref(); + if (href != null && href.length() > 0) { + if (href.startsWith("javascript:")) { + return Optional.empty(); + } else { + try { + return Optional.ofNullable(this.getFullURL(href)); + } catch (final MalformedURLException mfu) { + this.warn("Malformed URI: [" + href + "].", mfu); + } + } + } + } + return Optional.empty(); + }*/ + + // simplified version + private Optional getAbsoluteURL() { + final String href = this.getHref(); + if (href.startsWith("javascript:")) { + return Optional.empty(); + } else { + try { + return Optional.ofNullable(this.getFullURL(href)); + } catch (final MalformedURLException mfu) { + this.warn("Malformed URI: [" + href + "].", mfu); + } + } + return Optional.empty(); + } + public String getAbsoluteHref() { + // TODO: Use Either in getAbsoluteURL and use the branch type for javascript + return getAbsoluteURL().map(u -> u.toExternalForm()).orElse(getHref()); + + /* final HtmlRendererContext rcontext = this.getHtmlRendererContext(); if (rcontext != null) { final String href = this.getHref(); if ((href != null) && (href.length() > 0)) { - try { - final URL url = this.getFullURL(href); - return url == null ? null : url.toExternalForm(); - } catch (final MalformedURLException mfu) { - this.warn("Malformed URI: [" + href + "].", mfu); + if (href.startsWith("javascript:")) { + return href; + } else { + try { + final URL url = this.getFullURL(href); + return url == null ? null : url.toExternalForm(); + } catch (final MalformedURLException mfu) { + this.warn("Malformed URI: [" + href + "].", mfu); + } } } } - return null; + return null; */ } - public void navigate() { + // TODO: Hide from JS + // TODO: Should HTMLLinkElement actually support navigation? The Link element seems to be conflated with elements + public boolean navigate() { + + // If there is no href attribute, chromium only dispatches the handlers without starting a navigation + final String hrefAttr = this.getAttribute("href"); + if (hrefAttr == null) { + // final boolean eventResult = dispatchEvent(new Event("click", this)); + // System.out.println("event result: " + eventResult); + return false; + } + if (this.disabled) { - return; + return false; + } + System.out.println("Consider Navigating to: " + getHref()); + // final String href = getAbsoluteHref(); + final String href = getHref(); + if (href.startsWith("#")) { + // TODO: Scroll to the element. Issue #101 + } else if (href.startsWith("javascript:")) { + + final String script = href.substring(11); + System.out.println("Evaling script: " + script); + + // evalInScope adds the JS task + ((Window) (((HTMLDocumentImpl) document).getDefaultView())).evalInScope(script); + + /* + ((Window) (((HTMLDocumentImpl) document).getDefaultView())).addJSTask(new JSRunnableTask(0, () -> { + Executor.executeFunction(this, f, null); + }));*/ + } else { + final Optional urlOpt = getAbsoluteURL(); + if (urlOpt.isPresent()) { + final HtmlRendererContext rcontext = this.getHtmlRendererContext(); + final String target = this.getTarget(); + rcontext.linkClicked(this, urlOpt.get(), target); + return true; + + } } + return false; + /* final HtmlRendererContext rcontext = this.getHtmlRendererContext(); if (rcontext != null) { final String href = this.getHref(); @@ -178,7 +262,7 @@ public void navigate() { this.warn("Malformed URI: [" + href + "].", mfu); } } - } + }*/ } /* @@ -223,7 +307,9 @@ protected RenderState createRenderState(RenderState prevRenderState) { public String toString() { // Javascript code often depends on this being exactly href. See js9.html. // To change, perhaps add method to AbstractScriptableDelegate. - return this.getHref(); + // Chromium 37 and FF 32 both return the full url + // return this.getHref(); + return getAbsoluteHref(); } /** @@ -291,31 +377,39 @@ private boolean isAllowedType() { } private void processLink() { - final UserAgentContext uacontext = this.getUserAgentContext(); - if (uacontext.isExternalCSSEnabled()) { - final HTMLDocumentImpl doc = (HTMLDocumentImpl) this.getOwnerDocument(); - try { - final String href = this.getHref(); - final StyleSheet jSheet = CSSUtilities.jParse(this, href, doc, doc.getBaseURI(), false); - if (this.styleSheet != null) { - this.styleSheet.setJStyleSheet(jSheet); - } else { - final JStyleSheetWrapper styleSheet = new JStyleSheetWrapper(jSheet, this.getMedia(), href, this.getType(), this.getTitle(), - this, doc.styleSheetManager.bridge); - this.styleSheet = styleSheet; + final HTMLDocumentImpl doc = (HTMLDocumentImpl) this.getOwnerDocument(); + try { + final UserAgentContext uacontext = this.getUserAgentContext(); + if (uacontext.isExternalCSSEnabled()) { + try { + final String href = this.getHref(); + final StyleSheet jSheet = CSSUtilities.jParse(this, href, doc, doc.getBaseURI(), false); + if (this.styleSheet != null) { + this.styleSheet.setJStyleSheet(jSheet); + } else { + final JStyleSheetWrapper styleSheet = new JStyleSheetWrapper(jSheet, this.getMedia(), href, this.getType(), this.getTitle(), + this, doc.styleSheetManager.bridge); + this.styleSheet = styleSheet; + } + this.styleSheet.setDisabled(this.isAltStyleSheet() | this.disabled); + doc.styleSheetManager.invalidateStyles(); + } catch (final MalformedURLException mfe) { + this.detachStyleSheet(); + this.warn("Will not parse CSS. URI=[" + this.getHref() + "] with BaseURI=[" + doc.getBaseURI() + + "] does not appear to be a valid URI."); + } catch (final Throwable err) { + this.warn("Unable to parse CSS. URI=[" + this.getHref() + "].", err); } - this.styleSheet.setDisabled(this.isAltStyleSheet() | this.disabled); - doc.styleSheetManager.invalidateStyles(); - } catch (final MalformedURLException mfe) { - this.detachStyleSheet(); - this.warn("Will not parse CSS. URI=[" + this.getHref() + "] with BaseURI=[" + doc.getBaseURI() - + "] does not appear to be a valid URI."); - } catch (final Throwable err) { - this.warn("Unable to parse CSS. URI=[" + this.getHref() + "].", err); } + } finally { + doc.markJobsFinished(1); } } + private void deferredProcess() { + processLinkHelper(true); + } + private void processLinkHelper(final boolean defer) { // according to firefox, whenever the URL is not well formed, the style sheet has to be null // and in all other cases an empty style sheet has to be set till the link resource can be fetched @@ -333,6 +427,9 @@ private void processLinkHelper(final boolean defer) { } } else { this.detachStyleSheet(); + if (!defer) { + doc.markJobsFinished(1); + } } } @@ -348,7 +445,7 @@ public CSSStyleSheet getSheet() { @Override protected void handleDocumentAttachmentChanged() { - this.processLinkHelper(true); + deferredProcess(); } @Override diff --git a/src/HTML_Renderer/org/lobobrowser/html/domimpl/HTMLScriptElementImpl.java b/src/HTML_Renderer/org/lobobrowser/html/domimpl/HTMLScriptElementImpl.java index 0d7e105d..98b8262c 100644 --- a/src/HTML_Renderer/org/lobobrowser/html/domimpl/HTMLScriptElementImpl.java +++ b/src/HTML_Renderer/org/lobobrowser/html/domimpl/HTMLScriptElementImpl.java @@ -27,13 +27,14 @@ import java.util.logging.Logger; import org.lobobrowser.html.js.Executor; +import org.lobobrowser.html.js.Window; +import org.lobobrowser.html.js.Window.JSRunnableTask; 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.SecurityUtil; import org.mozilla.javascript.Context; -import org.mozilla.javascript.EcmaError; import org.mozilla.javascript.Scriptable; import org.w3c.dom.Document; import org.w3c.dom.html.HTMLScriptElement; @@ -141,8 +142,8 @@ protected final void processScript() { // Code might have restrictions on accessing // items from elsewhere. try { - request.open("GET", scriptURI, false); - request.send(null, new Request(scriptURL, RequestKind.JavaScript)); + request.open("GET", scriptURI, false); + request.send(null, new Request(scriptURL, RequestKind.JavaScript)); } catch (final java.io.IOException thrown) { logger.log(Level.WARNING, "processScript()", thrown); } @@ -156,26 +157,37 @@ protected final void processScript() { text = request.getResponseText(); baseLineNumber = 1; } - final Context ctx = Executor.createContext(this.getDocumentURL(), bcontext); - try { - final Scriptable scope = (Scriptable) doc.getUserData(Executor.SCOPE_KEY); - if (scope == null) { - throw new IllegalStateException("Scriptable (scope) instance was expected to be keyed as UserData to document using " - + Executor.SCOPE_KEY); - } - try { - if (text != null) { - ctx.evaluateString(scope, text, scriptURI, baseLineNumber, null); + + final Window window = ((HTMLDocumentImpl) doc).getWindow(); + if (text != null) { + final String textSub = text.substring(0, Math.min(50, text.length())).replaceAll("\n", ""); + window.addJSTaskUnchecked(new JSRunnableTask(0, "script: " + textSub, new Runnable() { + public void run() { + // final Context ctx = Executor.createContext(HTMLScriptElementImpl.this.getDocumentURL(), bcontext); + final Context ctx = Executor.createContext(HTMLScriptElementImpl.this.getDocumentURL(), bcontext, window.windowFactory); + try { + final Scriptable scope = window.getWindowScope(); + if (scope == null) { + throw new IllegalStateException("Scriptable (scope) instance was null"); + } + try { + if (text != null) { + ctx.evaluateString(scope, text, scriptURI, baseLineNumber, null); + } + // Why catch this? + // } catch (final EcmaError ecmaError) { + // logger.log(Level.WARNING, + // "Javascript error at " + ecmaError.sourceName() + ":" + ecmaError.lineNumber() + ": " + ecmaError.getMessage(), + // ecmaError); + } catch (final Throwable err) { + Executor.logJSException(err); + } + } finally { + Context.exit(); + ((HTMLDocumentImpl) HTMLScriptElementImpl.this.document).markJobsFinished(1); + } } - } catch (final EcmaError ecmaError) { - logger.log(Level.WARNING, - "Javascript error at " + ecmaError.sourceName() + ":" + ecmaError.lineNumber() + ": " + ecmaError.getMessage(), - ecmaError); - } catch (final Throwable err) { - logger.log(Level.WARNING, "Unable to evaluate Javascript code", err); - } - } finally { - Context.exit(); + })); } } } diff --git a/src/HTML_Renderer/org/lobobrowser/html/domimpl/HTMLTableElementImpl.java b/src/HTML_Renderer/org/lobobrowser/html/domimpl/HTMLTableElementImpl.java index 717cdf44..50caa6a8 100644 --- a/src/HTML_Renderer/org/lobobrowser/html/domimpl/HTMLTableElementImpl.java +++ b/src/HTML_Renderer/org/lobobrowser/html/domimpl/HTMLTableElementImpl.java @@ -25,6 +25,7 @@ import java.util.ArrayList; +import org.lobobrowser.html.js.PropertyName; import org.lobobrowser.html.style.HtmlLength; import org.lobobrowser.html.style.HtmlValues; import org.lobobrowser.html.style.JStyleProperties; @@ -82,6 +83,7 @@ public HTMLCollection getRows() { return new DescendentHTMLCollection(this, new NodeNameFilter("TR"), this.treeLock, false); } + @PropertyName("tBodies") public HTMLCollection getTBodies() { return new DescendentHTMLCollection(this, new NodeNameFilter("TBODY"), this.treeLock, false); } diff --git a/src/HTML_Renderer/org/lobobrowser/html/domimpl/ImageListener.java b/src/HTML_Renderer/org/lobobrowser/html/domimpl/ImageListener.java index c7bdb695..b8097981 100644 --- a/src/HTML_Renderer/org/lobobrowser/html/domimpl/ImageListener.java +++ b/src/HTML_Renderer/org/lobobrowser/html/domimpl/ImageListener.java @@ -24,4 +24,6 @@ public interface ImageListener extends java.util.EventListener { public static final ImageListener[] EMPTY_ARRAY = new ImageListener[0]; public void imageLoaded(ImageEvent event); + + public void imageAborted(); } diff --git a/src/HTML_Renderer/org/lobobrowser/html/domimpl/NodeImpl.java b/src/HTML_Renderer/org/lobobrowser/html/domimpl/NodeImpl.java index 9078086e..831d10a0 100644 --- a/src/HTML_Renderer/org/lobobrowser/html/domimpl/NodeImpl.java +++ b/src/HTML_Renderer/org/lobobrowser/html/domimpl/NodeImpl.java @@ -23,9 +23,11 @@ */ package org.lobobrowser.html.domimpl; +import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -35,10 +37,10 @@ import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.stream.Collectors; import org.lobobrowser.html.HtmlRendererContext; import org.lobobrowser.html.js.Event; -import org.lobobrowser.html.js.Executor; import org.lobobrowser.html.style.RenderState; import org.lobobrowser.html.style.StyleSheetRenderState; import org.lobobrowser.js.AbstractScriptableDelegate; @@ -58,8 +60,16 @@ import org.w3c.dom.ProcessingInstruction; import org.w3c.dom.Text; import org.w3c.dom.UserDataHandler; +import org.w3c.dom.html.HTMLCollection; import org.w3c.dom.html.HTMLDocument; +import cz.vutbr.web.css.CSSException; +import cz.vutbr.web.css.CSSFactory; +import cz.vutbr.web.css.CombinedSelector; +import cz.vutbr.web.css.RuleSet; +import cz.vutbr.web.css.Selector; +import cz.vutbr.web.css.StyleSheet; + // TODO: Implement org.w3c.dom.events.EventTarget ? public abstract class NodeImpl extends AbstractScriptableDelegate implements Node, ModelNode { private static final NodeImpl[] EMPTY_ARRAY = new NodeImpl[0]; @@ -105,37 +115,52 @@ public UINode findUINode() { } public Node appendChild(final Node newChild) throws DOMException { - synchronized (this.treeLock) { - if (isInclusiveAncestorOf(newChild)) { - final Node prevParent = newChild.getParentNode(); - if (prevParent instanceof NodeImpl) { - ((NodeImpl) prevParent).removeChild(newChild); + if (newChild != null) { + synchronized (this.treeLock) { + if (isInclusiveAncestorOf(newChild)) { + final Node prevParent = newChild.getParentNode(); + if (prevParent instanceof NodeImpl) { + ((NodeImpl) prevParent).removeChild(newChild); + } + } else if ((newChild instanceof NodeImpl) && ((NodeImpl) newChild).isInclusiveAncestorOf(this)) { + throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR, "Trying to append an ancestor element."); } - } else if ((newChild instanceof NodeImpl) && ((NodeImpl) newChild).isInclusiveAncestorOf(this)) { - throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR, "Trying to append an ancestor element."); - } - ArrayList nl = this.nodeList; - if (nl == null) { - nl = new ArrayList<>(3); - this.nodeList = nl; - } - nl.add(newChild); - if (newChild instanceof NodeImpl) { - ((NodeImpl) newChild).handleAddedToParent(this); + ArrayList nl = this.nodeList; + if (nl == null) { + nl = new ArrayList<>(3); + this.nodeList = nl; + } + nl.add(newChild); + if (newChild instanceof NodeImpl) { + ((NodeImpl) newChild).handleAddedToParent(this); + } } - } - this.postChildListChanged(); + this.postChildListChanged(); - return newChild; + /* + System.out.println("Appending child. " + newChild); + System.out.println(" modifying: " + newChild.getUserData(org.lobobrowser.html.parser.HtmlParser.MODIFYING_KEY)); + + if(newChild.getUserData(org.lobobrowser.html.parser.HtmlParser.MODIFYING_KEY) == null) { + System.out.println("Setting uesr data "); + newChild.setUserData(org.lobobrowser.html.parser.HtmlParser.MODIFYING_KEY, Boolean.FALSE, null); + }*/ + + return newChild; + } else { + System.out.println("\nTrying to append a null child!\n"); + throw new DOMException(DOMException.INVALID_ACCESS_ERR, "Trying to append a null child!"); + // throw new WrappedException(new DOMException(DOMException.INVALID_ACCESS_ERR, "Trying to append a null child!")); + // throw ScriptRuntime.typeError("Trying to append a null child"); + // throw Context.throwAsScriptRuntimeEx(ScriptRuntime.typeError("Trying to append a null child")); + } } // TODO not used by anyone protected void removeAllChildren() { - synchronized (this.treeLock) { - this.removeAllChildrenImpl(); - } + this.removeAllChildrenImpl(); } protected void removeAllChildrenImpl() { @@ -184,9 +209,11 @@ int getChildCount() { } } - private ChildHTMLCollection childrenCollection; + private DescendentHTMLCollection childrenCollection; - public ChildHTMLCollection getChildren() { + // TODO: This is needed to be implemented only by Element, Document and DocumentFragment as per https://developer.mozilla.org/en-US/docs/Web/API/ParentNode + public HTMLCollection getChildren() { + /* This older implementation returns all children. Whereas we need to return only Element children as per https://developer.mozilla.org/en-US/docs/Web/API/ParentNode // Method required by JavaScript synchronized (this) { ChildHTMLCollection collection = this.childrenCollection; @@ -195,7 +222,8 @@ public ChildHTMLCollection getChildren() { this.childrenCollection = collection; } return collection; - } + }*/ + return new DescendentHTMLCollection(this, new NodeFilter.ElementFilter(), this.treeLock); } /** @@ -418,10 +446,46 @@ void visit(final NodeVisitor visitor) { } } + /* public Node insertBefore(final Node newChild, final Node refChild) throws DOMException { synchronized (this.treeLock) { - final ArrayList nl = this.nodeList; - final int idx = nl == null ? -1 : nl.indexOf(refChild); + final ArrayList nl = getNonEmptyNodeList(); + // int idx = nl == null ? -1 : nl.indexOf(refChild); + int idx = nl.indexOf(refChild); + // System.out.println("inserting in " + this); + // System.out.println(" new child: " + newChild); + // System.out.println(" ref child: " + refChild); + if (idx == -1) { + // The exception was misleading. -1 could have resulted from an empty node list too. (but that is no more the case) + // throw new DOMException(DOMException.NOT_FOUND_ERR, "refChild not found"); + + // From what I understand from https://developer.mozilla.org/en-US/docs/Web/API/Node.insertBefore + // an invalid refChild will add the new child at the end of the list + + idx = nl.size(); + } + nl.add(idx, newChild); + if (newChild instanceof NodeImpl) { + ((NodeImpl) newChild).handleAddedToParent(this); + } + } + + this.postChildListChanged(); + + return newChild; + }*/ + + // Ongoing issue : 152 + // This is a changed and better version of the above. It gives the same number of pass / failures on http://web-platform.test:8000/dom/nodes/Node-insertBefore.html + // Pass 2: FAIL: 24 + public Node insertBefore(final Node newChild, final Node refChild) throws DOMException { + synchronized (this.treeLock) { + // From what I understand from https://developer.mozilla.org/en-US/docs/Web/API/Node.insertBefore + // a null or undefined refChild will cause the new child to be appended at the end of the list + // otherwise, this function will throw an exception if refChild is not found in the child list + + final ArrayList nl = refChild == null ? getNonEmptyNodeList() : this.nodeList; + final int idx = refChild == null ? nl.size() : (nl == null ? -1 : nl.indexOf(refChild)); if (idx == -1) { throw new DOMException(DOMException.NOT_FOUND_ERR, "refChild not found"); } @@ -436,13 +500,19 @@ public Node insertBefore(final Node newChild, final Node refChild) throws DOMExc return newChild; } + // TODO: Use this wherever nodeList needs to be non empty + private ArrayList getNonEmptyNodeList() { + ArrayList nl = this.nodeList; + if (nl == null) { + nl = new ArrayList<>(); + this.nodeList = nl; + } + return nl; + } + protected Node insertAt(final Node newChild, final int idx) throws DOMException { synchronized (this.treeLock) { - ArrayList nl = this.nodeList; - if (nl == null) { - nl = new ArrayList<>(); - this.nodeList = nl; - } + final ArrayList nl = getNonEmptyNodeList(); nl.add(idx, newChild); if (newChild instanceof NodeImpl) { ((NodeImpl) newChild).handleAddedToParent(this); @@ -1142,7 +1212,10 @@ public RenderState getRenderState() { // offset properties. synchronized (this.treeLock) { RenderState rs = this.renderState; + rs = this.renderState; + // This is a workaround to null pointer exception if (rs != INVALID_RENDER_STATE) { + // if (rs != INVALID_RENDER_STATE && rs != null) { return rs; } final Object parent = this.parentNode; @@ -1154,7 +1227,7 @@ public RenderState getRenderState() { } else { // Return null without caching. // Scenario is possible due to Javascript. - return null; + return INVALID_RENDER_STATE; } } } @@ -1163,7 +1236,7 @@ protected final static RenderState getParentRenderState(final Object parent) { if (parent instanceof NodeImpl) { return ((NodeImpl) parent).getRenderState(); } else { - return null; + return INVALID_RENDER_STATE; } } @@ -1255,6 +1328,7 @@ protected void appendInnerTextImpl(final StringBuffer buffer) { } } + /* protected void dispatchEventToHandlers(final Event event, final List handlers) { if (handlers != null) { // We clone the collection and check if original collection still contains @@ -1272,9 +1346,13 @@ protected void dispatchEventToHandlers(final Event event, final List h private final Map> onEventHandlers = new HashMap<>(); + public void addEventListener(final String type, final Function listener) { + addEventListener(type, listener, false); + } + public void addEventListener(final String type, final Function listener, final boolean useCapture) { // TODO - System.out.println("node Event listener: " + type); + System.out.println("node by name: " + getNodeName() + " adding Event listener of type: " + type); List handlerList = null; if (onEventHandlers.containsKey(type)) { @@ -1295,9 +1373,10 @@ public void removeEventListener(final String type, final Function listener, fina } public boolean dispatchEvent(final Event evt) { + System.out.println("Dispatching event: " + evt); dispatchEventToHandlers(evt, onEventHandlers.get(evt.getType())); return false; - } + }*/ private volatile boolean attachedToDocument = this instanceof HTMLDocument; @@ -1388,4 +1467,158 @@ private void postChildListChanged() { } } + /* + public void addEventListener(final String type, final EventListener listener) { + addEventListener(type, listener, false); + } + + public void addEventListener(final String type, final EventListener listener, final boolean useCapture) { + if (useCapture) { + throw new UnSupportedOperationException(); + } + } + + public void removeEventListener(final String type, final EventListener listener, final boolean useCapture) { + // TODO Auto-generated method stub + + } + + public boolean dispatchEvent(final org.w3c.dom.events.Event evt) throws EventException { + // TODO Auto-generated method stub + return false; + }*/ + + public void addEventListener(final String type, final Function listener) { + addEventListener(type, listener, false); + } + + public void addEventListener(final String type, final Function listener, final boolean useCapture) { + // TODO + System.out.println("node by name: " + getNodeName() + " adding Event listener of type: " + type); + // System.out.println(" txt content: " + getInnerText()); + ((HTMLDocumentImpl) getOwnerDocument()).getEventTargetManager().addEventListener(this, type, listener); + } + + public void removeEventListener(final String type, final Function listener, final boolean useCapture) { + // TODO + System.out.println("node remove Event listener: " + type); + ((HTMLDocumentImpl) getOwnerDocument()).getEventTargetManager().removeEventListener(this, type, listener, useCapture); + } + + public boolean dispatchEvent(final Event evt) { + System.out.println("Dispatching event: " + evt); + // dispatchEventToHandlers(evt, onEventHandlers.get(evt.getType())); + ((HTMLDocumentImpl) getOwnerDocument()).getEventTargetManager().dispatchEvent(this, evt); + return false; + } + + /* + public void addEventListener(final String type, final EventListener listener) { + addEventListener(type, listener, false); + } + + public void addEventListener(final String type, final EventListener listener, final boolean useCapture) { + if (useCapture) { + throw new UnSupportedOperationException(); + } + } + + public void removeEventListener(final String type, final EventListener listener, final boolean useCapture) { + // TODO Auto-generated method stub + + } + + public boolean dispatchEvent(final org.w3c.dom.events.Event evt) throws EventException { + // TODO Auto-generated method stub + return false; + }*/ + + public Element querySelector(final String query) { + // TODO: Optimize: Avoid getting all matches. Only first match is sufficient. + final NodeList matchingElements = querySelectorAll(query); + if (matchingElements.getLength() > 0) { + return (Element) matchingElements.item(0); + } else { + return null; + } + } + + private static CombinedSelector makeSelector(final String query) throws IOException, CSSException { + // this is quick way to parse the selectors. TODO: check if jStyleParser supports a better option. + final String tempBlock = query + " { }"; + final StyleSheet styleSheet = CSSFactory.parse(tempBlock); + final RuleSet firstRuleBlock = (RuleSet) styleSheet.get(0); + final List selectors = firstRuleBlock.getSelectors(); + final CombinedSelector firstSelector = selectors.get(0); + return firstSelector; + } + + /* + protected Collection getMatchingChildren(CombinedSelector selectors) { + final Collection matchingElements = new LinkedList<>(); + final NodeImpl[] childrenArray = getChildrenArray(); + if (childrenArray != null) { + for (final NodeImpl n : childrenArray) { + if (n instanceof ElementImpl) { + final ElementImpl element = (ElementImpl) n; + if (selectors.stream().anyMatch(selector -> selector.matches(element))) { + System.out.println("Found match: " + element + " of class: " + element.getClass()); + matchingElements.add(element); + } + matchingElements.addAll(element.getMatchingChildren(selectors)); + } + } + } + return matchingElements; + }*/ + + protected Collection getMatchingChildren(final List selectors) { + final Collection matchingElements = new LinkedList<>(); + if (selectors.size() > 0) { + final Selector firstSelector = selectors.get(0); + final List tailSelectors = selectors.subList(1, selectors.size()); + final boolean moreSelectorsPresent = tailSelectors.size() > 0; + final NodeImpl[] childrenArray = getChildrenArray(); + if (childrenArray != null) { + for (final NodeImpl n : childrenArray) { + if (n instanceof ElementImpl) { + final ElementImpl element = (ElementImpl) n; + if (firstSelector.matches(element)) { + if (moreSelectorsPresent) { + matchingElements.addAll(element.getMatchingChildren(tailSelectors)); + } else { + matchingElements.add(element); + } + } + matchingElements.addAll(element.getMatchingChildren(selectors)); + } + } + } + } + return matchingElements; + } + + public NodeList querySelectorAll(final String query) { + try { + final CombinedSelector firstSelector = makeSelector(query); + return new NodeListImpl(getMatchingChildren(firstSelector)); + } catch (final IOException | CSSException e) { + e.printStackTrace(); + throw new DOMException(DOMException.SYNTAX_ERR, "Couldn't parse selector: " + query); + } + } + + public NodeList getElementsByClassName(final String classNames) { + final String[] classNamesArray = classNames.split("\\s"); + // TODO: escape commas in class-names + final String query = Arrays.stream(classNamesArray).map(cn -> "." + cn).collect(Collectors.joining(",")); + return querySelectorAll(query); + } + + public NodeList getElementsByTagName(final String classNames) { + final String[] classNamesArray = classNames.split("\\s"); + // TODO: escape commas in class-names + final String query = Arrays.stream(classNamesArray).collect(Collectors.joining(",")); + return querySelectorAll(query); + } } diff --git a/src/HTML_Renderer/org/lobobrowser/html/domimpl/NodeListImpl.java b/src/HTML_Renderer/org/lobobrowser/html/domimpl/NodeListImpl.java index ca9504c9..23df3f75 100644 --- a/src/HTML_Renderer/org/lobobrowser/html/domimpl/NodeListImpl.java +++ b/src/HTML_Renderer/org/lobobrowser/html/domimpl/NodeListImpl.java @@ -27,16 +27,21 @@ import java.util.Collection; import org.lobobrowser.js.AbstractScriptableDelegate; +import org.lobobrowser.js.JavaScript; +import org.lobobrowser.util.Objects; +import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; +// TODO: This needs to be live (dynamic) not a static store of nodes. public class NodeListImpl extends AbstractScriptableDelegate implements NodeList { // Note: class must be public for reflection to work. - private final ArrayList nodeList = new ArrayList<>(); + private final ArrayList nodeList; + // TODO: Add more constructors that take arrays for example public NodeListImpl(final Collection collection) { super(); - nodeList.addAll(collection); + nodeList = new ArrayList<>(collection); } public int getLength() { @@ -50,4 +55,39 @@ public Node item(final int index) { return null; } } + + /* Quick hack to get w3c tests working. This function very likely needs to + * be included for every JS object. */ + public boolean hasOwnProperty(final Object obj) { + System.out.println("Checking " + obj); + if (Objects.isAssignableOrBox(obj, Integer.TYPE)) { + // if (obj instanceof Integer) { + final Integer i = (Integer) JavaScript.getInstance().getJavaObject(obj, Integer.TYPE); + // final Integer i = (Integer) obj; + return i < getLength(); + } else { + return false; + } + } + + /* Described here: http://www.w3.org/TR/dom/#dom-htmlcollection-nameditem. This actually needs to be in a separate class that implements HTMLCollection */ + public Node namedItem(final String key) { + final int length = getLength(); + for (int i = 0; i < length; i++) { + final Node n = item(0); + if (n instanceof Element) { + final Element element = (Element) n; + if (key.equals(element.getAttribute("id")) || key.equals(element.getAttribute("name"))) { + return n; + } + } + + } + return null; + } + + @Override + public String toString() { + return nodeList.toString(); + } } diff --git a/src/HTML_Renderer/org/lobobrowser/html/gui/HtmlBlockPanel.java b/src/HTML_Renderer/org/lobobrowser/html/gui/HtmlBlockPanel.java index 33f6f6eb..c211ab7f 100644 --- a/src/HTML_Renderer/org/lobobrowser/html/gui/HtmlBlockPanel.java +++ b/src/HTML_Renderer/org/lobobrowser/html/gui/HtmlBlockPanel.java @@ -125,6 +125,7 @@ public HtmlBlockPanel(final Color background, final boolean opaque, final UserAg final ActionListener actionListener = new ActionListener() { public void actionPerformed(final ActionEvent e) { final String command = e.getActionCommand(); + System.out.println("Action: " + command); if ("copy".equals(command)) { copy(); } @@ -588,9 +589,10 @@ private void onMouseMoved(final MouseEvent event) { * * @see javax.swing.JComponent#paintComponent(java.awt.Graphics) */ - // protected void paintComponent(Graphics g) { @Override - public void paint(final Graphics g) { + protected void paintComponent(final Graphics g) { + // public void paint(final Graphics g) { + // Update to below: paintComponent seems to work fine too // We go against Sun's advice and override // paint() instead of paintComponent(). Scrollbars // do not repaint correctly if we use diff --git a/src/HTML_Renderer/org/lobobrowser/html/js/Event.java b/src/HTML_Renderer/org/lobobrowser/html/js/Event.java index 63c3d802..26602308 100644 --- a/src/HTML_Renderer/org/lobobrowser/html/js/Event.java +++ b/src/HTML_Renderer/org/lobobrowser/html/js/Event.java @@ -25,19 +25,22 @@ import java.awt.event.MouseEvent; import org.lobobrowser.js.AbstractScriptableDelegate; +import org.w3c.dom.Node; +import org.w3c.dom.events.EventTarget; import org.w3c.dom.html.HTMLElement; // TODO: Implement org.w3c.events.Event ? -public class Event extends AbstractScriptableDelegate { +public class Event extends AbstractScriptableDelegate implements org.w3c.dom.events.Event { private boolean cancelBubble; private HTMLElement fromElement, toElement; private int leafX, leafY; private boolean returnValue; - private HTMLElement srcElement; + private Node srcElement; private String type; private final java.awt.event.InputEvent inputEvent; + private boolean propagationStopped = false; - public Event(final String type, final HTMLElement srcElement, final java.awt.event.InputEvent mouseEvent, final int leafX, final int leafY) { + public Event(final String type, final Node srcElement, final java.awt.event.InputEvent mouseEvent, final int leafX, final int leafY) { this.type = type; this.srcElement = srcElement; this.leafX = leafX; @@ -45,13 +48,13 @@ public Event(final String type, final HTMLElement srcElement, final java.awt.eve this.inputEvent = mouseEvent; } - public Event(final String type, final HTMLElement srcElement, final java.awt.event.KeyEvent keyEvent) { + public Event(final String type, final Node srcElement, final java.awt.event.KeyEvent keyEvent) { this.type = type; this.srcElement = srcElement; this.inputEvent = keyEvent; } - public Event(final String type, final HTMLElement srcElement) { + public Event(final String type, final Node srcElement) { this.type = type; this.srcElement = srcElement; this.inputEvent = null; @@ -75,7 +78,9 @@ public boolean getCtrlKey() { public int getButton() { final InputEvent ie = this.inputEvent; if (ie instanceof MouseEvent) { - return ((MouseEvent) ie).getButton(); + // return ((MouseEvent) ie).getButton(); + // range of button is 0 to N in DOM spec, but 1 to N in AWT + return ((MouseEvent) ie).getButton() - 1; } else { return 0; } @@ -86,6 +91,7 @@ public boolean isCancelBubble() { } public void setCancelBubble(final boolean cancelBubble) { + System.out.println("Event.setCancelBubble()"); this.cancelBubble = cancelBubble; } @@ -146,7 +152,7 @@ public void setReturnValue(final boolean returnValue) { this.returnValue = returnValue; } - public HTMLElement getSrcElement() { + public Node getSrcElement() { return srcElement; } @@ -185,4 +191,75 @@ public int getLeafY() { public void setLeafY(final int leafY) { this.leafY = leafY; } + + @Override + public EventTarget getTarget() { + System.out.println("Event.getTarget()"); + // TODO: Target and source may not be always same. Need to add a constructor param for target. + return (EventTarget) srcElement; + } + + @Override + public EventTarget getCurrentTarget() { + System.out.println("Event.getCurrentTarget()"); + return null; + } + + private short currentPhase = 0; + + @Override + public short getEventPhase() { + System.out.println("Event.getEventPhase() : " + currentPhase); + return currentPhase; + } + + @HideFromJS + public void setPhase(final short newPhase) { + currentPhase = newPhase; + } + + @Override + public boolean getBubbles() { + System.out.println("Event.getBubbles()"); + return false; + } + + @Override + public boolean getCancelable() { + System.out.println("Event.getCancelable()"); + return false; + } + + @Override + public long getTimeStamp() { + System.out.println("Event.getTimeStamp()"); + return 0; + } + + @Override + public void stopPropagation() { + propagationStopped = true; + System.out.println("Event.stopPropagation()"); + } + + // TODO: Hide from JS + public boolean isPropagationStopped() { + return propagationStopped; + } + + @Override + public void preventDefault() { + System.out.println("Event.preventDefault()"); + } + + @Override + public void initEvent(final String eventTypeArg, final boolean canBubbleArg, final boolean cancelableArg) { + System.out.println("Event.initEvent()"); + } + + @Override + public String toString() { + return "Event [phase=" + currentPhase + ", type=" + type + ", leafX=" + leafX + ", leafY=" + leafY + ", srcElement=" + srcElement + "]"; + } + } diff --git a/src/HTML_Renderer/org/lobobrowser/html/js/EventTargetManager.java b/src/HTML_Renderer/org/lobobrowser/html/js/EventTargetManager.java new file mode 100644 index 00000000..84517f6e --- /dev/null +++ b/src/HTML_Renderer/org/lobobrowser/html/js/EventTargetManager.java @@ -0,0 +1,245 @@ +package org.lobobrowser.html.js; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import org.lobobrowser.html.domimpl.NodeImpl; +import org.lobobrowser.html.js.Window.JSRunnableTask; +import org.mozilla.javascript.Function; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.events.EventException; +import org.w3c.dom.events.EventListener; + +public final class EventTargetManager { + + private final Map>> nodeOnEventListeners = new IdentityHashMap<>(); + // private NodeImpl node; + private final Window window; + + // public EventTargetManager(final NodeImpl node, final Window window) { + public EventTargetManager(final Window window) { + // this.node = node; + this.window = window; + } + + public void addEventListener(final NodeImpl node, final String type, final EventListener listener, final boolean useCapture) { + final List handlerList = getListenerList(type, node, true); + handlerList.add(listener); + } + + private List getListenerList(final String type, final NodeImpl node, final boolean createIfNotExist) { + final Map> onEventListeners = getEventListeners(node, createIfNotExist); + + if ((onEventListeners != null) && onEventListeners.containsKey(type)) { + return onEventListeners.get(type); + } else { + if (createIfNotExist) { + final List handlerList = new ArrayList<>(); + onEventListeners.put(type, handlerList); + return handlerList; + } else { + return null; + } + } + } + + private Map> getEventListeners(final NodeImpl node, final boolean createIfNotExist) { + if (nodeOnEventListeners.containsKey(node)) { + return nodeOnEventListeners.get(node); + } else { + if (createIfNotExist) { + final Map> onEventListeners = new HashMap<>(); + nodeOnEventListeners.put(node, onEventListeners); + return onEventListeners; + } else { + return null; + } + } + } + + public void removeEventListener(final NodeImpl node, final String type, final EventListener listener, final boolean useCapture) { + final Map> onEventListeners = getEventListeners(node, false); + if (onEventListeners != null) { + if (onEventListeners.containsKey(type)) { + onEventListeners.get(type).remove(listener); + } + } + + } + + private List getFunctionList(final String type, final NodeImpl node, final boolean createIfNotExist) { + final Map> onEventListeners = getEventFunctions(node, createIfNotExist); + + if ((onEventListeners != null) && onEventListeners.containsKey(type)) { + return onEventListeners.get(type); + } else { + if (createIfNotExist) { + final List handlerList = new ArrayList<>(); + onEventListeners.put(type, handlerList); + return handlerList; + } else { + return null; + } + } + } + + private Map> getEventFunctions(final NodeImpl node, final boolean createIfNotExist) { + if (nodeOnEventFunctions.containsKey(node)) { + return nodeOnEventFunctions.get(node); + } else { + if (createIfNotExist) { + final Map> onEventListeners = new HashMap<>(); + nodeOnEventFunctions.put(node, onEventListeners); + return onEventListeners; + } else { + return null; + } + } + } + + public boolean dispatchEvent(final NodeImpl node, final Event evt) throws EventException { + // dispatchEventToHandlers(node, evt, onEventListeners.get(evt.getType())); + // dispatchEventToJSHandlers(node, evt, onEventHandlers.get(evt.getType())); + + // TODO: Event Bubbling + // TODO: get Window into the propagation path + final List propagationPath = getPropagationPath(node); + + // TODO: Capture phase, and distinction between target phase and bubbling phase + evt.setPhase(org.w3c.dom.events.Event.AT_TARGET); + // TODO: The JS Task should be added with the correct base URL + window.addJSTask(new JSRunnableTask(0, "Event dispatch for " + evt, () -> { + for (int i = 0; (i < propagationPath.size()) && !evt.isPropagationStopped(); i++) { + final NodeImpl currNode = propagationPath.get(i); + // System.out.println("Dipatching " + i + " to: " + currNode); + // TODO: Make request manager checks here. + dispatchEventToHandlers(currNode, evt); + dispatchEventToJSHandlers(currNode, evt); + evt.setPhase(org.w3c.dom.events.Event.BUBBLING_PHASE); + } + } + )); + + // dispatchEventToHandlers(node, evt); + // dispatchEventToJSHandlers(node, evt); + return false; + } + + private static List getPropagationPath(NodeImpl node) { + final List nodes = new LinkedList<>(); + while (node != null) { + if ((node instanceof Element) || (node instanceof Document)) { // TODO || node instanceof Window) { + nodes.add(node); + } + node = (NodeImpl) node.getParentNode(); + } + + // TODO + // nodes.add(window); + + return nodes; + } + + // private void dispatchEventToHandlers(final NodeImpl node, final Event event, final List handlers) { + private void dispatchEventToHandlers(final NodeImpl node, final Event event) { + final List handlers = getListenerList(event.getType(), node, false); + if (handlers != null) { + // We clone the collection and check if original collection still contains + // the handler before dispatching + // This is to avoid ConcurrentModificationException during dispatch + final ArrayList handlersCopy = new ArrayList<>(handlers); + for (final EventListener h : handlersCopy) { + // TODO: Not sure if we should stop calling handlers after propagation is stopped + // if (event.isPropagationStopped()) { + // return; + // } + + if (handlers.contains(h)) { + // window.addJSTask(new JSRunnableTask(0, "Event dispatch for: " + event, new Runnable(){ + // public void run() { + h.handleEvent(event); + // } + // })); + // h.handleEvent(event); + + // Executor.executeFunction(node, h, event); + } + } + } + } + + // protected void dispatchEventToJSHandlers(final NodeImpl node, final Event event, final List handlers) { + protected void dispatchEventToJSHandlers(final NodeImpl node, final Event event) { + final List handlers = getFunctionList(event.getType(), node, false); + if (handlers != null) { + // We clone the collection and check if original collection still contains + // the handler before dispatching + // This is to avoid ConcurrentModificationException during dispatch + final ArrayList handlersCopy = new ArrayList<>(handlers); + for (final Function h : handlersCopy) { + // TODO: Not sure if we should stop calling handlers after propagation is stopped + // if (event.isPropagationStopped()) { + // return; + // } + + if (handlers.contains(h)) { + // window.addJSTask(new JSRunnableTask(0, "Event dispatch for " + event, new Runnable(){ + // public void run() { + Executor.executeFunction(node, h, event, window.windowFactory); + // } + // })); + // Executor.executeFunction(node, h, event); + } + } + } + } + + // private final Map> onEventHandlers = new HashMap<>(); + private final Map>> nodeOnEventFunctions = new IdentityHashMap<>(); + + public void addEventListener(final NodeImpl node, final String type, final Function listener) { + addEventListener(node, type, listener, false); + } + + public void addEventListener(final NodeImpl node, final String type, final Function listener, final boolean useCapture) { + // TODO + // System.out.println("node by name: " + node.getNodeName() + " adding Event listener of type: " + type); + + /* + List handlerList = null; + if (onEventHandlers.containsKey(type)) { + handlerList = onEventHandlers.get(type); + } else { + handlerList = new ArrayList<>(); + onEventHandlers.put(type, handlerList); + }*/ + // final Map> handlerList = getEventFunctions(node, true); + final List handlerList = getFunctionList(type, node, true); + handlerList.add(listener); + } + + public void removeEventListener(final NodeImpl node, final String type, final Function listener, final boolean useCapture) { + final Map> onEventListeners = getEventFunctions(node, false); + if (onEventListeners != null) { + if (onEventListeners.containsKey(type)) { + onEventListeners.get(type).remove(listener); + } + } + } + + // public void setNode(NodeImpl node) { + // this.node = node; + // } + + public void reset() { + // node = null; + nodeOnEventFunctions.clear(); + nodeOnEventListeners.clear(); + } + +} diff --git a/src/HTML_Renderer/org/lobobrowser/html/js/Executor.java b/src/HTML_Renderer/org/lobobrowser/html/js/Executor.java index 8d65cef0..b9111396 100644 --- a/src/HTML_Renderer/org/lobobrowser/html/js/Executor.java +++ b/src/HTML_Renderer/org/lobobrowser/html/js/Executor.java @@ -23,13 +23,16 @@ import java.util.logging.Level; import java.util.logging.Logger; +import org.lobobrowser.html.domimpl.HTMLDocumentImpl; import org.lobobrowser.html.domimpl.NodeImpl; import org.lobobrowser.js.JavaScript; import org.lobobrowser.ua.UserAgentContext; import org.lobobrowser.ua.UserAgentContext.Request; import org.lobobrowser.ua.UserAgentContext.RequestKind; import org.mozilla.javascript.Context; +import org.mozilla.javascript.ContextFactory; import org.mozilla.javascript.Function; +import org.mozilla.javascript.RhinoException; import org.mozilla.javascript.Scriptable; import org.w3c.dom.Document; @@ -42,24 +45,35 @@ public class Executor { * @param codeSource * @param ucontext */ - public static Context createContext(final java.net.URL codeSource, final UserAgentContext ucontext) { + public static Context createContext(final java.net.URL codeSource, final UserAgentContext ucontext, final ContextFactory factory) { final Context prev = Context.getCurrentContext(); - final Context ctx = Context.enter(); - ctx.setOptimizationLevel(ucontext.getScriptingOptimizationLevel()); - if (prev == null) { - // If there was a previous context, this one must be nested. - // We still need to create a context because of exit() but - // we cannot set a new security controller. - ctx.setSecurityController(new SecurityControllerImpl(codeSource, ucontext.getSecurityPolicy())); + + // final Context ctx = Context.enter(); + final Context ctx = factory.enterContext(); + + if (!ctx.isSealed()) { + ctx.setOptimizationLevel(ucontext.getScriptingOptimizationLevel()); + if (prev == null) { + // If there was a previous context, this one must be nested. + // We still need to create a context because of exit() but + // we cannot set a new security controller. + ctx.setSecurityController(new SecurityControllerImpl(codeSource, ucontext.getSecurityPolicy())); + } + // Sealing is recommended for untrusted scripts + ctx.seal(null); } return ctx; } - public static boolean executeFunction(final NodeImpl element, final Function f, final Event event) { - return Executor.executeFunction(element, element, f, event); + // public static boolean executeFunction(final NodeImpl element, final Function f, final org.w3c.dom.events.Event event, final ContextFactory contextFactory) { + public static boolean executeFunction(final NodeImpl element, final Function f, final Object event, final ContextFactory contextFactory) { + return Executor.executeFunction(element, element, f, event, contextFactory); } - public static boolean executeFunction(final NodeImpl element, final Object thisObject, final Function f, final Event event) { + // private static boolean executeFunction(final NodeImpl element, final Object thisObject, final Function f, final org.w3c.dom.events.Event event, final ContextFactory contextFactory) { + private static boolean executeFunction(final NodeImpl element, final Object thisObject, final Function f, final Object event, + final ContextFactory contextFactory) { + // System.out.println(" of type : " + event.getClass()); final Document doc = element.getOwnerDocument(); if (doc == null) { throw new IllegalStateException("Element does not belong to a document."); @@ -67,52 +81,68 @@ public static boolean executeFunction(final NodeImpl element, final Object thisO final UserAgentContext uaContext = element.getUserAgentContext(); if (uaContext.isRequestPermitted(new Request(element.getDocumentURL(), RequestKind.JavaScript))) { - final Context ctx = createContext(element.getDocumentURL(), element.getUserAgentContext()); + System.out.println("Executing " + f); + System.out.println(" with args: " + event); + final Context ctx = createContext(element.getDocumentURL(), element.getUserAgentContext(), contextFactory); + // ctx.setGenerateObserverCount(true); try { - final Scriptable scope = (Scriptable) doc.getUserData(Executor.SCOPE_KEY); + final Scriptable scope = ((HTMLDocumentImpl) doc).getWindow().getWindowScope(); if (scope == null) { - throw new IllegalStateException("Scriptable (scope) instance was expected to be keyed as UserData to document using " - + Executor.SCOPE_KEY); + throw new IllegalStateException("Scriptable (scope) instance is null"); } final JavaScript js = JavaScript.getInstance(); final Scriptable thisScope = (Scriptable) js.getJavascriptObject(thisObject, scope); try { - final Scriptable eventScriptable = (Scriptable) js.getJavascriptObject(event, thisScope); + // final Scriptable eventScriptable = (Scriptable) js.getJavascriptObject(event, thisScope); + final Object eventScriptable = js.getJavascriptObject(event, thisScope); + scope.put("event", thisScope, eventScriptable); // ScriptableObject.defineProperty(thisScope, "event", // eventScriptable, // ScriptableObject.READONLY); + System.out.println("Calling with param: " + eventScriptable); final Object result = f.call(ctx, thisScope, thisScope, new Object[] { eventScriptable }); + System.out.println(" Result: " + result); if (!(result instanceof Boolean)) { return true; } return ((Boolean) result).booleanValue(); } catch (final Throwable thrown) { - logger.log(Level.WARNING, "executeFunction(): There was an error in Javascript code.", thrown); + logJSException(thrown); return true; } } finally { Context.exit(); } } else { - // TODO: Should this be true. I am copying the return from the exception - // clause above. + // TODO: Should this be true? I am copying the return from the exception clause above. + System.out.println("Rejected request to execute script"); return true; } } + public static void logJSException(final Throwable err) { + logger.log(Level.WARNING, "Unable to evaluate Javascript code", err); + if (err instanceof RhinoException) { + final RhinoException rhinoException = (RhinoException) err; + logger.log(Level.WARNING, "JS Error: " + rhinoException.details() + "\nJS Stack:\n" + rhinoException.getScriptStackTrace()); + } + } + public static boolean executeFunction(final Scriptable thisScope, final Function f, final java.net.URL codeSource, - final UserAgentContext ucontext) { - final Context ctx = createContext(codeSource, ucontext); + final UserAgentContext ucontext, final ContextFactory contextFactory) { + // System.out.println("Started: " + debugCount.getAndIncrement()); + final Context ctx = createContext(codeSource, ucontext, contextFactory); try { try { final Object result = f.call(ctx, thisScope, thisScope, new Object[0]); + // System.out.println("Finished: " + debugCount.decrementAndGet()); if (!(result instanceof Boolean)) { return true; } return ((Boolean) result).booleanValue(); } catch (final Throwable err) { - logger.log(Level.WARNING, "executeFunction(): Unable to execute Javascript function " + f.getClassName() + ".", err); + logJSException(err); return true; } } finally { @@ -120,9 +150,4 @@ public static boolean executeFunction(final Scriptable thisScope, final Function } } - /** - * A document UserData key used to map Javascript scope in the - * HTML document. - */ - public static final String SCOPE_KEY = "cobra.js.scope"; } diff --git a/src/HTML_Renderer/org/lobobrowser/html/js/HideFromJS.java b/src/HTML_Renderer/org/lobobrowser/html/js/HideFromJS.java new file mode 100644 index 00000000..afc0a454 --- /dev/null +++ b/src/HTML_Renderer/org/lobobrowser/html/js/HideFromJS.java @@ -0,0 +1,12 @@ +package org.lobobrowser.html.js; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Ensures that a function or property is hidden from JS + */ +@Retention(RetentionPolicy.RUNTIME) +public @interface HideFromJS { + +} diff --git a/src/HTML_Renderer/org/lobobrowser/html/js/Location.java b/src/HTML_Renderer/org/lobobrowser/html/js/Location.java index ac6ddc12..c7e351c7 100644 --- a/src/HTML_Renderer/org/lobobrowser/html/js/Location.java +++ b/src/HTML_Renderer/org/lobobrowser/html/js/Location.java @@ -94,6 +94,7 @@ public String getHref() { } public void setHref(final String uri) { + System.out.println("Setting location to : " + uri); final HtmlRendererContext rcontext = this.window.getHtmlRendererContext(); if (rcontext != null) { try { diff --git a/src/HTML_Renderer/org/lobobrowser/html/js/Window.java b/src/HTML_Renderer/org/lobobrowser/html/js/Window.java index 462a5732..46be57cf 100644 --- a/src/HTML_Renderer/org/lobobrowser/html/js/Window.java +++ b/src/HTML_Renderer/org/lobobrowser/html/js/Window.java @@ -26,15 +26,25 @@ import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.lang.ref.WeakReference; +import java.net.URL; +import java.security.AccessControlContext; +import java.security.AccessController; +import java.security.PrivilegedAction; import java.util.HashMap; import java.util.Map; import java.util.WeakHashMap; +import java.util.concurrent.PriorityBlockingQueue; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.function.Supplier; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.Timer; import org.lobobrowser.html.HtmlRendererContext; +import org.lobobrowser.html.domimpl.CommentImpl; +import org.lobobrowser.html.domimpl.HTMLDivElementImpl; import org.lobobrowser.html.domimpl.HTMLDocumentImpl; import org.lobobrowser.html.domimpl.HTMLElementImpl; import org.lobobrowser.html.domimpl.HTMLIFrameElementImpl; @@ -49,8 +59,12 @@ import org.lobobrowser.js.JavaObjectWrapper; import org.lobobrowser.js.JavaScript; import org.lobobrowser.ua.UserAgentContext; +import org.lobobrowser.ua.UserAgentContext.Request; +import org.lobobrowser.ua.UserAgentContext.RequestKind; import org.lobobrowser.util.ID; +import org.mozilla.javascript.ClassShutter; import org.mozilla.javascript.Context; +import org.mozilla.javascript.ContextFactory; import org.mozilla.javascript.Function; import org.mozilla.javascript.Scriptable; import org.mozilla.javascript.ScriptableObject; @@ -59,12 +73,14 @@ import org.w3c.dom.Node; import org.w3c.dom.css.CSS2Properties; import org.w3c.dom.events.EventException; +import org.w3c.dom.events.EventListener; +import org.w3c.dom.events.EventTarget; import org.w3c.dom.html.HTMLCollection; import org.w3c.dom.html.HTMLElement; import org.w3c.dom.views.AbstractView; import org.w3c.dom.views.DocumentView; -public class Window extends AbstractScriptableDelegate implements AbstractView { +public class Window extends AbstractScriptableDelegate implements AbstractView, EventTarget { private static final Logger logger = Logger.getLogger(Window.class.getName()); private static final Map> CONTEXT_WINDOWS = new WeakHashMap<>(); // private static final JavaClassWrapper IMAGE_WRAPPER = @@ -72,7 +88,10 @@ public class Window extends AbstractScriptableDelegate implements AbstractView { private static final JavaClassWrapper XMLHTTPREQUEST_WRAPPER = JavaClassWrapperFactory.getInstance() .getClassWrapper(XMLHttpRequest.class); - private static int timerIdCounter = 0; + // Timer ids should begin counting from 1 or more. + // jQuery's ajax polling handler relies on a non-zero value (uses it as a boolean condition) + // Chromium 37 starts counting from 1 while Firefox 32 starts counting from 2 (from developer consoles and plugins installed) + private static int timerIdCounter = 1; private final HtmlRendererContext rcontext; private final UserAgentContext uaContext; @@ -107,6 +126,15 @@ public UserAgentContext getUserAgentContext() { private void clearState() { synchronized (this) { + // windowClosing = true; + document.stopEverything(); + jsScheduler.stopAndWindUp(); + jsScheduler = new JSScheduler(this); + eventTargetManager.reset(); + this.onWindowLoadHandler = null; + + this.forgetAllTasks(); + // Commenting out call to getWindowScope() since that creates a new scope which is wasteful // if we are going to destroy it anyway. // final Scriptable s = this.getWindowScope(); @@ -122,37 +150,39 @@ private void clearState() { } } - document.setUserData(Executor.SCOPE_KEY, null, null); - // This will ensure that a fresh scope will be created by getWindowScope() on the next call this.windowScope = null; + jobFinishedHandler = null; } } public void setDocument(final HTMLDocumentImpl document) { - final Document prevDocument = this.document; - if (prevDocument != document) { - final Function onunload = this.onunload; - if (onunload != null) { - final HTMLDocumentImpl oldDoc = (HTMLDocumentImpl) prevDocument; - Executor.executeFunction(this.getWindowScope(), onunload, oldDoc.getDocumentURL(), this.uaContext); - this.onunload = null; - } - - // TODO: Should clearing of the state be done when window "unloads"? - if (prevDocument != null) { - // Only clearing when the previous document was not null - // because state might have been set on the window before - // the very first document is added. - this.clearState(); - } - this.forgetAllTasks(); - this.initWindowScope(document); + synchronized (this) { + + final Document prevDocument = this.document; + if (prevDocument != document) { + final Function onunload = this.onunload; + if (onunload != null) { + final HTMLDocumentImpl oldDoc = (HTMLDocumentImpl) prevDocument; + Executor.executeFunction(this.getWindowScope(), onunload, oldDoc.getDocumentURL(), this.uaContext, windowFactory); + this.onunload = null; + } - // Set up Javascript scope - document.setUserData(Executor.SCOPE_KEY, getWindowScope(), null); + // TODO: Should clearing of the state be done when window "unloads"? + if (prevDocument != null) { + // Only clearing when the previous document was not null + // because state might have been set on the window before + // the very first document is added. + this.clearState(); + } + // this.forgetAllTasks(); + this.initWindowScope(document); + + jsScheduler.start(); - this.document = document; + this.document = document; + // eventTargetManager.setNode(document); + } } } @@ -164,6 +194,239 @@ public Document getDocumentNode() { return this.document; } + private abstract static class JSTask implements Comparable { + protected final int priority; + protected final long creationTime; + protected final String description; + private final AccessControlContext context; + + // TODO: Add a context parameter that will be combined with current context, to help with creation of timer tasks + // public JSTask(final int priority, final Runnable runnable) { + public JSTask(final int priority, final String description) { + this.priority = priority; + this.description = description; + this.context = AccessController.getContext(); + this.creationTime = System.nanoTime(); + } + + // TODO: Add a way to stop a task. It should return false if the task can't be stopped in which case a thread kill will be performed by the task scheduler. + + // TODO: Sorting by priority + public int compareTo(final JSTask o) { + final long diffCreation = (o.creationTime - creationTime); + if (diffCreation < 0) { + return 1; + } else if (diffCreation == 0) { + return 0; + } else { + return -1; + } + } + + public abstract void run(); + + } + + public final static class JSRunnableTask extends JSTask { + private final Runnable runnable; + + public JSRunnableTask(final int priority, final Runnable runnable) { + this(priority, "", runnable); + } + + public JSRunnableTask(final int priority, final String description, final Runnable runnable) { + super(priority, description); + this.runnable = runnable; + } + + @Override + public String toString() { + // return "JSRunnableTask [priority=" + priority + ", runnable=" + runnable + ", creationTime=" + creationTime + "]"; + return "JSRunnableTask [priority=" + priority + ", description=" + description + ", creationTime=" + creationTime + "]"; + } + + @Override + public void run() { + runnable.run(); + } + + } + + public final static class JSSupplierTask extends JSTask { + private final Supplier supplier; + private final Consumer consumer; + + public JSSupplierTask(final int priority, final Supplier supplier, final Consumer consumer) { + super(priority, "supplier description TODO"); + this.supplier = supplier; + this.consumer = consumer; + } + + @Override + public void run() { + final T result = supplier.get(); + consumer.accept(result); + } + } + + private static final class JSScheduler extends Thread { + private static final class ScheduledTask implements Comparable { + final int id; + final JSTask task; + + public ScheduledTask(final int id, final JSTask task) { + this.id = id; + this.task = task; + } + + public int compareTo(final ScheduledTask other) { + return task.compareTo(other.task); + } + + @Override + public boolean equals(final Object o) { + if (o instanceof Integer) { + final Integer oId = (Integer) o; + return oId == id; + } + return false; + } + + @Override + public String toString() { + return "Scheduled Task (" + id + ", " + task + ")"; + } + } + + private final PriorityBlockingQueue jsQueue = new PriorityBlockingQueue<>(); + + private volatile boolean windowClosing = false; + + // TODO: This is not water tight for one reason, Windows are reused for different documents. + // If they are always freshly created, the taskIdCounter will be more reliable. + private volatile AtomicInteger taskIdCounter = new AtomicInteger(0); + + // TODO: Remove, added just for debugging + private final Window window; + + public JSScheduler(final Window window) { + super("JS Scheduler"); + this.window = window; + } + + @Override + public void run() { + System.out.println("\n\nIn " + window.document.getBaseURI() + " Running loop"); + while (!windowClosing) { + try { + ScheduledTask scheduledTask; + // TODO: uncomment if synchronization is necessary with the add methods + // synchronized (this) { + scheduledTask = jsQueue.take(); + // } + final PrivilegedAction action = new PrivilegedAction() { + public Object run() { + // System.out.println("In " + window.document.getBaseURI() + "\n Running task: " + scheduledTask); + scheduledTask.task.run(); + // System.out.println("Done task: " + scheduledTask); + // System.out.println(" Remaining tasks: " + jsQueue.size()); + return null; + } + }; + AccessController.doPrivileged(action, scheduledTask.task.context); + } catch (final InterruptedException e) { + final int queueSize = jsQueue.size(); + if (queueSize > 0) { + System.err.println("JS Scheduler was interrupted. Tasks remaining: " + jsQueue.size()); + } + } catch (final Exception e) { + e.printStackTrace(); + } + } + System.out.println("Exiting loop\n\n"); + } + + public void stopAndWindUp() { + System.out.println("Going to stop JS scheduler"); + windowClosing = true; + + // TODO: Check if interrupt is needed if stop is anyway being called. + this.interrupt(); + try { + this.join(10); + } catch (final InterruptedException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + this.stop(); + System.out.println("Finished interrupting"); + } + + public void addJSTask(final JSTask task) { + // synchronized (this) { + jsQueue.add(new ScheduledTask(0, task)); + // } + } + + public int addUniqueJSTask(final int oldId, final JSTask task) { + // synchronized (this) { + if (oldId != -1) { + if (jsQueue.contains(oldId)) { + return oldId; + } + /* + for (ScheduledTask t : jsQueue) { + if (t.id == oldId) { + // Task found + return oldId; + } + }*/ + } + final int newId = taskIdCounter.addAndGet(1); + jsQueue.add(new ScheduledTask(newId, task)); + return newId; + // } + } + } + + private volatile JSScheduler jsScheduler = new JSScheduler(this); + + @HideFromJS + public void addJSTask(final JSTask task) { + final URL url = document.getDocumentURL(); + if (uaContext.isRequestPermitted(new Request(url, RequestKind.JavaScript))) { + // System.out.println("Adding task: " + task); + synchronized (this) { + jsScheduler.addJSTask(task); + } + } + } + + // TODO: Try to refactor this so that all tasks are checked here rather than in caller + // TODO: Some tasks are added unchecked for various reasons that need to be reviewed: + // 1. Timer task. The logic is that a script that was permitted to create the timer already has the permission to execute it. + // But it would be better if their permission is checked again to account for subsequent changes through RequestManager, + // or if RequestManager assures that page is reloaded for *any* permission change. + // 2. Event listeners. Logic is similar to Timer task + // 3. Script elements. They are doing the checks themselves, but it would better to move the check here. + // 4. XHR handler. Logic similar to timer task. + @HideFromJS + public void addJSTaskUnchecked(final JSTask task) { + // System.out.println("Adding task: " + task); + synchronized (this) { + jsScheduler.addJSTask(task); + } + } + + @HideFromJS + public int addJSUniqueTask(final int oldId, final JSTask task) { + System.out.println("Adding unique task: " + task); + + synchronized (this) { + return jsScheduler.addUniqueJSTask(oldId, task); + } + } + private void putAndStartTask(final Integer timeoutID, final Timer timer, final Object retained) { TaskWrapper oldTaskWrapper = null; synchronized (this) { @@ -226,7 +489,8 @@ private void forgetAllTasks() { * @param aFunction * Javascript function to invoke on each loop. * @param aTimeInMs - * Time in millisecund between each loop. + * Time in millisecund between each loop. TODO: Can this be converted + * to long type? * @return Return the timer ID to use as reference * @see Window.setInterval @@ -239,6 +503,7 @@ public int setInterval(final Function aFunction, final double aTimeInMs) { throw new IllegalArgumentException("Timeout value " + aTimeInMs + " is not supported."); } final int timeID = generateTimerID(); + System.out.println("Created interval timer: " + timeID); final Integer timeIDInt = new Integer(timeID); final ActionListener task = new FunctionTimerTask(this, timeIDInt, aFunction, false); int t = (int) aTimeInMs; @@ -292,6 +557,19 @@ public void clearInterval(final int aTimerID) { this.forgetTask(key, true); } + public void clearInterval(final Object unused) { + // Happens when jQuery calls this with a null parameter; + // TODO: Check if there are other cases + if (unused instanceof Integer) { + final Integer id = (Integer) unused; + clearInterval((int) id); + return; + } + System.out.println("Clear interval : ignoring " + unused); + // TODO: Should this be throwing an exception? + // throw new UnsupportedOperationException(); + } + public void alert(final String message) { if (this.rcontext != null) { this.rcontext.alert(message); @@ -312,7 +590,17 @@ public void blur() { } } - public void clearTimeout(final int timeoutID) { + public void clearTimeout(final Object someObj) { + if (someObj instanceof Integer) { + final Integer id = (Integer) someObj; + clearTimeout(id.intValue()); + } else { + System.out.println("Window.clearTimeout() : Ignoring: " + someObj); + } + } + + private void clearTimeout(final int timeoutID) { + System.out.println("Clearing timeout: " + timeoutID); final Integer key = new Integer(timeoutID); this.forgetTask(key, true); } @@ -333,6 +621,23 @@ public boolean confirm(final String message) { } } + // TODO: Hide from JS + // Making public for link element + public void evalInScope(final String javascript) { + addJSTask(new JSRunnableTask(0, new Runnable() { + public void run() { + try { + final String scriptURI = "window.eval"; + final Context ctx = Executor.createContext(document.getDocumentURL(), Window.this.uaContext, windowFactory); + ctx.evaluateString(getWindowScope(), javascript, scriptURI, 1, null); + } finally { + Context.exit(); + } + } + })); + } + + /* private Object evalInScope(final String javascript) { final Context ctx = Executor.createContext(document.getDocumentURL(), this.uaContext); try { @@ -375,7 +680,103 @@ public void focus() { } } + static class MyContextFactory extends ContextFactory { + static final private ClassShutter myClassShutter = new ClassShutter() { + public boolean visibleToScripts(final String fullClassName) { + // System.out.println("class shutter Checking: " + fullClassName); + if (fullClassName.startsWith("java")) { + final boolean isException = (fullClassName.startsWith("java.lang") && fullClassName.endsWith("Exception")); + if (fullClassName.equals("java.lang.Object") || isException) { + return true; + } + System.out.println("Warning: Something tried to access java classes from javascript."); + Thread.dumpStack(); + return false; + } + + // TODO: Change the default to false + return true; + } + }; + + // Override {@link #makeContext()} + @Override + protected Context makeContext() + { + final Context cx = super.makeContext(); + cx.setClassShutter(myClassShutter); + // cx.setOptimizationLevel(9); + cx.setOptimizationLevel(-1); + // System.out.println("Opt level: " + cx.getOptimizationLevel()); + + // Make Rhino runtime to call observeInstructionCount + // each 100000 bytecode instructions + // cx.setInstructionObserverThreshold(1000000); + + // cx.setMaximumInterpreterStackDepth(100); + // cx.seal(null); + + //cx.setDebugger(myDebugger, null); + return cx; + } + + @Override + protected void observeInstructionCount(final Context cx, final int instructionCount) { + System.out.println("Context: " + cx + " Instruction count: " + instructionCount); + } + + /* + final private Debugger myDebugger = new Debugger() { + + @Override + public void handleCompilationDone(Context cx, DebuggableScript fnOrScript, String source) { + // TODO Auto-generated method stub + + } + + @Override + public DebugFrame getFrame(Context cx, DebuggableScript fnOrScript) { + // TODO Auto-generated method stub + return null; + } + };*/ + + } + + // TODO: make private + public MyContextFactory windowFactory = new MyContextFactory(); + + // final private org.mozilla.javascript.tools.debugger.Main debuggerMain = new org.mozilla.javascript.tools.debugger.Main("Debugger"); + private void initWindowScope(final Document doc) { + /* + ContextFactory.getGlobal().addListener(new org.mozilla.javascript.ContextFactory.Listener() { + + @Override + public void contextCreated(Context cx) { + // cx.setGenerateObserverCount(true); + cx.setInstructionObserverThreshold(10); + } + + @Override + public void contextReleased(Context cx) { + // TODO Auto-generated method stub + + }});*/ + + // Initialize GlobalFactory with custom factory + /* + if (!ContextFactory.hasExplicitGlobal()) { + ContextFactory.initGlobal(new MyFactory()); + }*/ + + // TODO: Attaching to the context helps remove the getParentScope() infinite loop + // But, need to check if this is a race condition in our own code. + // Also, debugger needs lots of extra permissions! + // debuggerMain.attachTo(ContextFactory.getGlobal()); + // debuggerMain.attachTo(windowFactory); + // debuggerMain.setVisible(true); + // Special Javascript class: XMLHttpRequest final Scriptable ws = this.getWindowScope(); final JavaInstantiator xi = new JavaInstantiator() { @@ -390,30 +791,47 @@ public Object newInstance() { } catch (final ClassCastException err) { throw new IllegalStateException("Cannot perform operation with documents of type " + d.getClass().getName() + "."); } - return new XMLHttpRequest(uaContext, hd.getDocumentURL(), ws); + return new XMLHttpRequest(uaContext, hd.getDocumentURL(), ws, Window.this); } }; final Function xmlHttpRequestC = JavaObjectWrapper.getConstructor("XMLHttpRequest", XMLHTTPREQUEST_WRAPPER, ws, xi); ScriptableObject.defineProperty(ws, "XMLHttpRequest", xmlHttpRequestC, ScriptableObject.READONLY); + /* + { + // Define the Window object + final JavaClassWrapper windowWrapper = JavaClassWrapperFactory.getInstance().getClassWrapper(Window.class); + final JavaInstantiator wi = () -> { throw new UnsupportedOperationException(); }; + ScriptableObject.defineProperty(ws, "Window", JavaObjectWrapper.getConstructor("Window", windowWrapper, ws, wi), + ScriptableObject.READONLY); + }*/ + + // ScriptableObject.defineClass(ws, org.mozilla.javascript.ast.Comment.class); + defineElementClass(ws, doc, "Comment", "comment", CommentImpl.class); + // HTML element classes defineElementClass(ws, doc, "Image", "img", HTMLImageElementImpl.class); defineElementClass(ws, doc, "Script", "script", HTMLScriptElementImpl.class); defineElementClass(ws, doc, "IFrame", "iframe", HTMLIFrameElementImpl.class); defineElementClass(ws, doc, "Option", "option", HTMLOptionElementImpl.class); defineElementClass(ws, doc, "Select", "select", HTMLSelectElementImpl.class); + + // TODO: Add all similar elements + defineElementClass(ws, doc, "HTMLDivElement", "div", HTMLDivElementImpl.class); } private Scriptable windowScope; - private Scriptable getWindowScope() { + @HideFromJS + public Scriptable getWindowScope() { synchronized (this) { Scriptable windowScope = this.windowScope; if (windowScope != null) { return windowScope; } // Context.enter() OK in this particular case. - final Context ctx = Context.enter(); + // final Context ctx = Context.enter(); + final Context ctx = windowFactory.enterContext(); try { // Window scope needs to be top-most scope. windowScope = (Scriptable) JavaScript.getInstance().getJavascriptObject(this, null); @@ -548,6 +966,7 @@ public void resizeBy(final int byWidth, final int byHeight) { } } + @NotGetterSetter public int setTimeout(final String expr, final double millis) { if ((millis > Integer.MAX_VALUE) || (millis < 0)) { throw new IllegalArgumentException("Timeout value " + millis + " is not supported."); @@ -565,11 +984,13 @@ public int setTimeout(final String expr, final double millis) { return timeID; } + @NotGetterSetter public int setTimeout(final Function function, final double millis) { if ((millis > Integer.MAX_VALUE) || (millis < 0)) { throw new IllegalArgumentException("Timeout value " + millis + " is not supported."); } final int timeID = generateTimerID(); + System.out.println("Creating timer with id: " + timeID + " in " + document.getBaseURI()); final Integer timeIDInt = new Integer(timeID); final ActionListener task = new FunctionTimerTask(this, timeIDInt, function, true); int t = (int) millis; @@ -582,10 +1003,12 @@ public int setTimeout(final Function function, final double millis) { return timeID; } + @NotGetterSetter public int setTimeout(final Function function) { return setTimeout(function, 0); } + @NotGetterSetter public int setTimeout(final String expr) { return setTimeout(expr, 0); } @@ -653,7 +1076,12 @@ public void setName(final String newName) { public Window getParent() { final HtmlRendererContext rcontext = this.rcontext; if (rcontext != null) { - return Window.getWindow(rcontext.getParent()); + final HtmlRendererContext rcontextParent = rcontext.getParent(); + if (rcontextParent == null) { + return this; + } else { + return Window.getWindow(rcontextParent); + } } else { return null; } @@ -782,10 +1210,12 @@ public Function getOnload() { public void setOnload(final Function onload) { // Note that body.onload overrides // window.onload. + /* final Document doc = this.document; if (doc instanceof HTMLDocumentImpl) { - ((HTMLDocumentImpl) doc).setOnloadHandler(onload); - } + ((HTMLDocumentImpl) doc).setWindowOnloadHandler(onload); + }*/ + onWindowLoadHandler = onload; } private Function onunload; @@ -844,6 +1274,7 @@ public FunctionTimerTask(final Window window, final Integer timeIDInt, final Fun } public void actionPerformed(final ActionEvent e) { + System.out.println("Timer ID fired: " + timeIDInt + ", oneshot: " + removeTask); // This executes in the GUI thread and that's good. try { final Window window = this.getWindow(); @@ -864,7 +1295,11 @@ public void actionPerformed(final ActionEvent e) { if (function == null) { throw new IllegalStateException("Cannot perform operation. Function is no longer available."); } - Executor.executeFunction(window.getWindowScope(), function, doc.getDocumentURL(), window.getUserAgentContext()); + window.addJSTaskUnchecked(new JSRunnableTask(0, "timer task for id: " + timeIDInt + ", oneshot: " + removeTask, () -> { + Executor.executeFunction(window.getWindowScope(), function, doc.getDocumentURL(), window.getUserAgentContext(), + window.windowFactory); + })); + // Executor.executeFunction(window.getWindowScope(), function, doc.getDocumentURL(), window.getUserAgentContext(), window.windowFactory); } catch (final Throwable err) { logger.log(Level.WARNING, "actionPerformed()", err); } @@ -903,7 +1338,10 @@ public void actionPerformed(final ActionEvent e) { if (doc == null) { throw new IllegalStateException("Cannot perform operation when document is unset."); } - window.evalInScope(this.expression); + window.addJSTaskUnchecked(new JSRunnableTask(0, "timer task for id: " + timeIDInt, () -> { + window.evalInScope(this.expression); + })); + // window.evalInScope(this.expression); } catch (final Throwable err) { logger.log(Level.WARNING, "actionPerformed()", err); } @@ -921,11 +1359,29 @@ public TaskWrapper(final Timer timer, final Object retained) { } } + public void addEventListener(final String type, final Function listener) { + addEventListener(type, listener, false); + } + + private final EventTargetManager eventTargetManager = new EventTargetManager(this); + + public EventTargetManager getEventTargetManager() { + return eventTargetManager; + } + public void addEventListener(final String type, final Function listener, final boolean useCapture) { + if (useCapture) { + throw new UnsupportedOperationException(); + } + /* // TODO: Should this delegate completely to document if ("load".equals(type)) { document.addLoadHandler(listener); - } + } else { + document.addEventListener(type, listener); + }*/ + System.out.println("window Added listener for: " + type); + eventTargetManager.addEventListener(document, type, listener); } public void removeEventListener(final String type, final Function listener, final boolean useCapture) { @@ -933,21 +1389,103 @@ public void removeEventListener(final String type, final Function listener, fina if ("load".equals(type)) { document.removeLoadHandler(listener); } + eventTargetManager.removeEventListener(document, type, listener, useCapture); } public boolean dispatchEvent(final Event evt) throws EventException { // TODO - System.out.println("window dispatch event"); + System.out.println("TODO: window dispatch event"); + eventTargetManager.dispatchEvent(document, evt); return false; } + // TODO: Hide from JS + public void domContentLoaded(final Event domContentLoadedEvent) { + eventTargetManager.dispatchEvent(document, domContentLoadedEvent); + } + + private Function onWindowLoadHandler; + + // private Function windowLoadListeners; + + // TODO: Hide from JS + // TODO: Move job scheduling logic into Window class + // private AtomicBoolean jobsOver = new AtomicBoolean(false); + public void jobsFinished() { + final Event windowLoadEvent = new Event("load", document); + eventTargetManager.dispatchEvent(document, windowLoadEvent); + + final Function handler = this.onWindowLoadHandler; + if (handler != null) { + addJSTask(new JSRunnableTask(0, new Runnable() { + public void run() { + Executor.executeFunction(document, handler, windowLoadEvent, windowFactory); + } + })); + // Executor.executeFunction(document, handler, windowLoadEvent); + } + + if (jobFinishedHandler != null) { + jobFinishedHandler.run(); + } + // jobsOver.set(true); + } + + private volatile Runnable jobFinishedHandler = null; + + // TODO: ensure not accessible from JS + public void setJobFinishedHandler(final Runnable handler) { + jobFinishedHandler = handler; + } + + /* @PropertyName("Element") public Class getElement() { return Element.class; + }*/ + + /* changed from above For prototype.js */ + private Object element = Element.class; + + @PropertyName("Element") + public Object getElement() { + return element; + } + + @PropertyName("Element") + public void setElement(final Object o) { + System.out.println("Setting element to: " + o); + element = o; } @PropertyName("Node") public Class getNode() { + return Node.class; } + + public void addEventListener(final String type, final EventListener listener) { + addEventListener(type, listener, false); + } + + public void addEventListener(final String type, final EventListener listener, final boolean useCapture) { + if (useCapture) { + throw new UnsupportedOperationException(); + } + // TODO Auto-generated method stub + // throw new UnsupportedOperationException(); + eventTargetManager.addEventListener(document, type, listener, useCapture); + } + + public void removeEventListener(final String type, final EventListener listener, final boolean useCapture) { + // TODO Auto-generated method stub + throw new UnsupportedOperationException(); + } + + @Override + public boolean dispatchEvent(final org.w3c.dom.events.Event evt) throws EventException { + // TODO Auto-generated method stub + throw new UnsupportedOperationException(); + } + } diff --git a/src/HTML_Renderer/org/lobobrowser/html/js/XMLHttpRequest.java b/src/HTML_Renderer/org/lobobrowser/html/js/XMLHttpRequest.java index 5b68af17..89ac1264 100644 --- a/src/HTML_Renderer/org/lobobrowser/html/js/XMLHttpRequest.java +++ b/src/HTML_Renderer/org/lobobrowser/html/js/XMLHttpRequest.java @@ -13,8 +13,10 @@ import java.util.logging.Level; import java.util.logging.Logger; +import org.lobobrowser.html.js.Window.JSRunnableTask; import org.lobobrowser.js.AbstractScriptableDelegate; import org.lobobrowser.js.JavaScript; +import org.lobobrowser.security.StoreHostPermission; import org.lobobrowser.ua.NetworkRequest; import org.lobobrowser.ua.UserAgentContext; import org.lobobrowser.ua.UserAgentContext.Request; @@ -36,11 +38,15 @@ public class XMLHttpRequest extends AbstractScriptableDelegate { private final Scriptable scope; private final java.net.URL codeSource; - public XMLHttpRequest(final UserAgentContext pcontext, final java.net.URL codeSource, final Scriptable scope) { + // 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() { @@ -114,21 +120,38 @@ public void open(final String method, final String url) throws java.io.IOExcepti } public void send(final String content) throws IOException { + System.out.println(" Sending " + content); final Optional 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 PrivilegedExceptionAction action = () -> { - request.send(content, new Request(url, RequestKind.XHR)); - return null; + final StoreHostPermission storeHostPermission = StoreHostPermission.forURL(url); + final PrivilegedExceptionAction action = new PrivilegedExceptionAction() { + @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); + // 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()); // TODO: The code 19 is being hard-coded here to match Chromium's code. Better to declare a static constant in a subclass of DOMException. @@ -163,48 +186,85 @@ public void setOnreadystatechange(final Function value) { } } + private Function onLoad; + private boolean listenerAddedLoad; + + public void setOnload(final Function value) { + System.out.println("Setting onload: " + value); + synchronized (this) { + this.onLoad = value; + if ((value != null) && !this.listenerAdded) { + this.request.addNetworkRequestListener(netEvent -> executeReadyStateChange()); + this.listenerAdded = true; + } + } + } + private void executeReadyStateChange() { + System.out.println(" Ready state: " + request.getReadyState()); // Not called in GUI thread to ensure consistency of readyState. try { final Function f = XMLHttpRequest.this.getOnreadystatechange(); if (f != null) { - final Context ctx = Executor.createContext(this.codeSource, this.pcontext); + // window.addJSTask(new JSRunnableTask(0, "xhr ready state changed: " + request.getReadyState(), () -> { + final Context ctx = Executor.createContext(this.codeSource, this.pcontext, window.windowFactory); 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.windowFactory); + 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" + "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) { diff --git a/src/HTML_Renderer/org/lobobrowser/html/parser/HtmlParser.java b/src/HTML_Renderer/org/lobobrowser/html/parser/HtmlParser.java index 85980d9d..5ea4a587 100644 --- a/src/HTML_Renderer/org/lobobrowser/html/parser/HtmlParser.java +++ b/src/HTML_Renderer/org/lobobrowser/html/parser/HtmlParser.java @@ -622,7 +622,7 @@ public void parse(final LineNumberReader reader, final Node parent) throws IOExc */ private final int parseToken(final Node parent, final LineNumberReader reader, final Set stopTags, final LinkedList ancestors) - throws IOException, StopException, SAXException { + throws IOException, StopException, SAXException { final Document doc = this.document; final StringBuffer textSb = this.readUpToTagBegin(reader); if (textSb == null) { @@ -852,7 +852,7 @@ private final StringBuffer readUpToTagBegin(final LineNumberReader reader) throw */ private final int parseForEndTag(final Node parent, final LineNumberReader reader, final String tagName, final boolean addTextNode, final boolean decodeEntities) - throws IOException, SAXException { + throws IOException, SAXException { final Document doc = this.document; int intCh; StringBuffer sb = new StringBuffer(); diff --git a/src/HTML_Renderer/org/lobobrowser/html/renderer/HtmlController.java b/src/HTML_Renderer/org/lobobrowser/html/renderer/HtmlController.java index efb3f26a..156bb157 100644 --- a/src/HTML_Renderer/org/lobobrowser/html/renderer/HtmlController.java +++ b/src/HTML_Renderer/org/lobobrowser/html/renderer/HtmlController.java @@ -2,6 +2,7 @@ import java.awt.Cursor; import java.awt.event.InputEvent; +import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.util.Optional; import java.util.logging.Level; @@ -9,8 +10,11 @@ import org.lobobrowser.html.FormInput; import org.lobobrowser.html.HtmlRendererContext; +import org.lobobrowser.html.domimpl.ElementImpl; import org.lobobrowser.html.domimpl.HTMLAbstractUIElement; import org.lobobrowser.html.domimpl.HTMLButtonElementImpl; +import org.lobobrowser.html.domimpl.HTMLDocumentImpl; +import org.lobobrowser.html.domimpl.HTMLElementImpl; import org.lobobrowser.html.domimpl.HTMLInputElementImpl; import org.lobobrowser.html.domimpl.HTMLLinkElementImpl; import org.lobobrowser.html.domimpl.HTMLSelectElementImpl; @@ -18,8 +22,13 @@ import org.lobobrowser.html.domimpl.NodeImpl; import org.lobobrowser.html.js.Event; import org.lobobrowser.html.js.Executor; +import org.lobobrowser.html.js.Window; +import org.lobobrowser.html.js.Window.JSRunnableTask; import org.lobobrowser.html.style.RenderState; +import org.mozilla.javascript.ContextFactory; import org.mozilla.javascript.Function; +import org.w3c.dom.Document; +import org.w3c.dom.Element; class HtmlController { private static final Logger logger = Logger.getLogger(HtmlController.class.getName()); @@ -44,25 +53,70 @@ public boolean onEnterPressed(final ModelNode node, final InputEvent event) { return false; } + // Quick hack + private ContextFactory getWindowFactory(final ElementImpl e) { + final HTMLDocumentImpl doc = (HTMLDocumentImpl) e.getOwnerDocument(); + return doc.getWindow().windowFactory; + } + + // Quick hack + private static boolean runFunction(final ElementImpl e, final Function f, final Event event) { + final HTMLDocumentImpl doc = (HTMLDocumentImpl) e.getOwnerDocument(); + final Window window = doc.getWindow(); + window.addJSTask(new JSRunnableTask(0, "function from HTMLController", () -> { + Executor.executeFunction(e, f, event, window.windowFactory); + })); + return false; + } + + public boolean onMouseClick(final ModelNode node, final MouseEvent event, final int x, final int y) { + return onMouseClick(node, event, x, y, false); + } + /** * @return True to propagate further and false if the event was consumed. */ - public boolean onMouseClick(final ModelNode node, final MouseEvent event, final int x, final int y) { + public boolean onMouseClick(final ModelNode node, final MouseEvent event, final int x, final int y, boolean eventDispatched) { if (logger.isLoggable(Level.INFO)) { logger.info("onMouseClick(): node=" + node + ",class=" + node.getClass().getName()); } + // System.out.println("HtmlController.onMouseClick(): " + node + " already dispatched: " + eventDispatched); + + // Get the node which is a valid Event target + /*{ + NodeImpl target = (NodeImpl)node; + while(target.getParentNode() != null) { + if (target instanceof Element || target instanceof Document) { // TODO || node instanceof Window) { + break; + } + target = (NodeImpl) target.getParentNode(); + } + final Event jsEvent = new Event("click", target, event, x, y); + target.dispatchEvent(jsEvent); + }*/ + if (node instanceof HTMLAbstractUIElement) { final HTMLAbstractUIElement uiElement = (HTMLAbstractUIElement) node; final Event jsEvent = new Event("click", uiElement, event, x, y); - uiElement.dispatchEvent(jsEvent); + // System.out.println("Ui element: " + uiElement.getId()); + // uiElement.dispatchEvent(jsEvent); final Function f = uiElement.getOnclick(); + /* TODO: This is the original code which would return immediately if f returned false. */ if (f != null) { - if (!Executor.executeFunction(uiElement, f, jsEvent)) { + // Changing argument to uiElement instead of event + if (!Executor.executeFunction(uiElement, f, jsEvent, getWindowFactory(uiElement))) { + // if (!Executor.executeFunction(uiElement, f, uiElement, getWindowFactory(uiElement))) { return false; } } + /* + // Alternate JS Task version: + if (f != null) { + runFunction(uiElement, f, jsEvent); + }*/ + final HtmlRendererContext rcontext = uiElement.getHtmlRendererContext(); if (rcontext != null) { if (!rcontext.onMouseClick(uiElement, event)) { @@ -71,8 +125,10 @@ public boolean onMouseClick(final ModelNode node, final MouseEvent event, final } } if (node instanceof HTMLLinkElementImpl) { - ((HTMLLinkElementImpl) node).navigate(); - return false; + final boolean navigated = ((HTMLLinkElementImpl) node).navigate(); + if (navigated) { + return false; + } } else if (node instanceof HTMLButtonElementImpl) { final HTMLButtonElementImpl button = (HTMLButtonElementImpl) node; final String rawType = button.getAttribute("type"); @@ -91,18 +147,41 @@ public boolean onMouseClick(final ModelNode node, final MouseEvent event, final formInputs = new FormInput[] { new FormInput(name, button.getValue()) }; } button.submitForm(formInputs); + return false; } else if ("reset".equals(type)) { button.resetForm(); + return false; + } else if ("button".equals(type)) { + System.out.println("Button TODO;"); } else { // NOP for "button"! } - return false; } + if (!eventDispatched) { + // Get the node which is a valid Event target + if ((node instanceof Element) || (node instanceof Document)) { // TODO || node instanceof Window) { + // System.out.println("Click accepted on " + node); + final NodeImpl target = (NodeImpl) node; + final Event jsEvent = new Event("click", target, event, x, y); + target.dispatchEvent(jsEvent); + eventDispatched = true; + } + } + // } else { + // System.out.println("Bumping click to parent"); + final ModelNode parent = node.getParentModelNode(); + if (parent == null) { + return true; + } + return this.onMouseClick(parent, event, x, y, eventDispatched); + // } + // return false; + /* final ModelNode parent = node.getParentModelNode(); if (parent == null) { return true; } - return this.onMouseClick(parent, event, x, y); + return this.onMouseClick(parent, event, x, y);*/ } public boolean onContextMenu(final ModelNode node, final MouseEvent event, final int x, final int y) { @@ -114,7 +193,7 @@ public boolean onContextMenu(final ModelNode node, final MouseEvent event, final final Function f = uiElement.getOncontextmenu(); if (f != null) { final Event jsEvent = new Event("contextmenu", uiElement, event, x, y); - if (!Executor.executeFunction(uiElement, f, jsEvent)) { + if (!Executor.executeFunction(uiElement, f, jsEvent, getWindowFactory(uiElement))) { return false; } } @@ -148,7 +227,7 @@ public void onMouseOver(final BaseBoundableRenderable renderable, final ModelNod final Function f = uiElement.getOnmouseover(); if (f != null) { final Event jsEvent = new Event("mouseover", uiElement, event, x, y); - Executor.executeFunction(uiElement, f, jsEvent); + Executor.executeFunction(uiElement, f, jsEvent, getWindowFactory(uiElement)); } final HtmlRendererContext rcontext = uiElement.getHtmlRendererContext(); if (rcontext != null) { @@ -174,7 +253,7 @@ private static void setMouseOnMouseOver(final BaseBoundableRenderable renderable final HtmlRendererContext rcontext = uiElement.getHtmlRendererContext(); final RenderState rs = uiElement.getRenderState(); final Optional cursorOpt = rs.getCursor(); - if (cursorOpt.isPresent()) { + if (cursorOpt.isPresent() && (rcontext != null)) { rcontext.setCursor(cursorOpt); break; } else { @@ -204,7 +283,7 @@ public void onMouseOut(final ModelNode nodeStart, final MouseEvent event, final final Function f = uiElement.getOnmouseout(); if (f != null) { final Event jsEvent = new Event("mouseout", uiElement, event, x, y); - Executor.executeFunction(uiElement, f, jsEvent); + Executor.executeFunction(uiElement, f, jsEvent, getWindowFactory(uiElement)); } final HtmlRendererContext rcontext = uiElement.getHtmlRendererContext(); if (rcontext != null) { @@ -241,7 +320,9 @@ private static void resetCursorOnMouseOut(final ModelNode nodeStart, final Model final NodeImpl uiElement = (NodeImpl) nodeStart; final HtmlRendererContext rcontext = uiElement.getHtmlRendererContext(); // rcontext.setCursor(Optional.empty()); - rcontext.setCursor(foundCursorOpt); + if (rcontext != null) { + rcontext.setCursor(foundCursorOpt); + } } } @@ -257,7 +338,7 @@ public boolean onDoubleClick(final ModelNode node, final MouseEvent event, final final Function f = uiElement.getOndblclick(); if (f != null) { final Event jsEvent = new Event("dblclick", uiElement, event, x, y); - if (!Executor.executeFunction(uiElement, f, jsEvent)) { + if (!Executor.executeFunction(uiElement, f, jsEvent, getWindowFactory(uiElement))) { return false; } } @@ -300,7 +381,7 @@ public boolean onMouseDown(final ModelNode node, final MouseEvent event, final i final Function f = uiElement.getOnmousedown(); if (f != null) { final Event jsEvent = new Event("mousedown", uiElement, event, x, y); - pass = Executor.executeFunction(uiElement, f, jsEvent); + pass = Executor.executeFunction(uiElement, f, jsEvent, getWindowFactory(uiElement)); } } if (node instanceof HTMLLinkElementImpl) { @@ -327,7 +408,7 @@ public boolean onMouseUp(final ModelNode node, final MouseEvent event, final int final Function f = uiElement.getOnmouseup(); if (f != null) { final Event jsEvent = new Event("mouseup", uiElement, event, x, y); - pass = Executor.executeFunction(uiElement, f, jsEvent); + pass = Executor.executeFunction(uiElement, f, jsEvent, getWindowFactory(uiElement)); } } if (node instanceof HTMLLinkElementImpl) { @@ -359,7 +440,7 @@ public boolean onPressed(final ModelNode node, final InputEvent event, final int final Function f = uiElement.getOnclick(); if (f != null) { final Event jsEvent = new Event("click", uiElement, event, x, y); - if (!Executor.executeFunction(uiElement, f, jsEvent)) { + if (!Executor.executeFunction(uiElement, f, jsEvent, getWindowFactory(uiElement))) { return false; } } @@ -385,6 +466,11 @@ public boolean onPressed(final ModelNode node, final InputEvent event, final int hie.resetForm(); } } + if (node instanceof HTMLElementImpl) { + final HTMLElementImpl htmlElem = (HTMLElementImpl) node; + final Event evt = new Event("click", htmlElem, event, x, y); + htmlElem.dispatchEvent(evt); + } // No propagate return false; } @@ -395,7 +481,7 @@ public boolean onChange(final ModelNode node) { final Function f = uiElement.getOnchange(); if (f != null) { final Event jsEvent = new Event("change", uiElement); - if (!Executor.executeFunction(uiElement, f, jsEvent)) { + if (!Executor.executeFunction(uiElement, f, jsEvent, getWindowFactory(uiElement))) { return false; } } @@ -403,4 +489,24 @@ public boolean onChange(final ModelNode node) { // No propagate return false; } + + public boolean onKeyUp(final ModelNode node, final KeyEvent ke) { + boolean pass = true; + if (node instanceof NodeImpl) { + final NodeImpl uiElement = (NodeImpl) node; + final Event jsEvent = new Event("keyup", uiElement, ke); + pass = uiElement.dispatchEvent(jsEvent); + System.out.println("Dispatch result: " + pass); + } + /* + if (node instanceof HTMLAbstractUIElement) { + final HTMLAbstractUIElement uiElement = (HTMLAbstractUIElement) node; + final Function f = uiElement.getOnmouseup(); + if (f != null) { + final Event jsEvent = new Event("keyup", uiElement, ke); + pass = Executor.executeFunction(uiElement, f, jsEvent); + } + }*/ + return pass; + } } diff --git a/src/HTML_Renderer/org/lobobrowser/html/renderer/ImgControl.java b/src/HTML_Renderer/org/lobobrowser/html/renderer/ImgControl.java index d339bfac..4a4d28b5 100644 --- a/src/HTML_Renderer/org/lobobrowser/html/renderer/ImgControl.java +++ b/src/HTML_Renderer/org/lobobrowser/html/renderer/ImgControl.java @@ -211,13 +211,19 @@ public void imageLoaded(final ImageEvent event) { // Implementation of ImageListener. Invoked in a request thread most likely. final Image image = event.image; this.image = image; - final int width = image.getWidth(this); - final int height = image.getHeight(this); - if ((width != -1) && (height != -1)) { - this.imageUpdate(image, width, height); + if (image != null) { + final int width = image.getWidth(this); + final int height = image.getHeight(this); + if ((width != -1) && (height != -1)) { + this.imageUpdate(image, width, height); + } } } + public void imageAborted() { + // do nothing + } + @Override public String toString() { return "ImgControl[src=" + this.lastSrc + "]"; diff --git a/src/HTML_Renderer/org/lobobrowser/html/renderer/InputImageControl.java b/src/HTML_Renderer/org/lobobrowser/html/renderer/InputImageControl.java index 5c452a3e..198ff145 100644 --- a/src/HTML_Renderer/org/lobobrowser/html/renderer/InputImageControl.java +++ b/src/HTML_Renderer/org/lobobrowser/html/renderer/InputImageControl.java @@ -234,6 +234,10 @@ public void imageLoaded(final ImageEvent event) { } } + public void imageAborted() { + // do nothing + } + public void resetInput() { // NOP } diff --git a/src/HTML_Renderer/org/lobobrowser/html/renderer/InputTextAreaControl.java b/src/HTML_Renderer/org/lobobrowser/html/renderer/InputTextAreaControl.java index 40596773..47288059 100644 --- a/src/HTML_Renderer/org/lobobrowser/html/renderer/InputTextAreaControl.java +++ b/src/HTML_Renderer/org/lobobrowser/html/renderer/InputTextAreaControl.java @@ -26,6 +26,10 @@ import java.awt.Font; import java.awt.FontMetrics; import java.awt.Insets; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; +import java.security.AccessController; +import java.security.PrivilegedAction; import javax.swing.JScrollPane; import javax.swing.JTextArea; @@ -42,6 +46,23 @@ public InputTextAreaControl(final HTMLBaseInputElement modelNode) { super(modelNode); this.setLayout(WrapperLayout.getInstance()); final JTextComponent widget = this.createTextField(); + widget.addKeyListener(new KeyListener() { + + @Override + public void keyTyped(final KeyEvent e) { + System.out.println("InputTextAreaControl.InputTextAreaControl(...).new KeyListener() {...}.keyTyped()" + e); + } + + @Override + public void keyReleased(final KeyEvent e) { + System.out.println("InputTextAreaControl.InputTextAreaControl(...).new KeyListener() {...}.keyReleased()" + e); + } + + @Override + public void keyPressed(final KeyEvent e) { + System.out.println("InputTextAreaControl.InputTextAreaControl(...).new KeyListener() {...}.keyPressed(): " + e); + } + }); this.widget = widget; this.add(new JScrollPane(widget)); @@ -78,7 +99,16 @@ public void reset(final int availWidth, final int availHeight) { } protected JTextComponent createTextField() { - return new JTextArea(); + // Creating with privileges; otherwise the AWT events generated for this component + // capture the current stack of priviliges. If the component is created from javascript + // it fails for AccessController.checkPermission('accessClipboard') + return AccessController.doPrivileged(new PrivilegedAction() { + + @Override + public JTextComponent run() { + return new JTextArea(); + } + }); } /* diff --git a/src/HTML_Renderer/org/lobobrowser/html/renderer/InputTextControl.java b/src/HTML_Renderer/org/lobobrowser/html/renderer/InputTextControl.java index 478d0007..c306722e 100644 --- a/src/HTML_Renderer/org/lobobrowser/html/renderer/InputTextControl.java +++ b/src/HTML_Renderer/org/lobobrowser/html/renderer/InputTextControl.java @@ -25,6 +25,8 @@ import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; import javax.swing.JTextField; import javax.swing.text.JTextComponent; @@ -35,8 +37,25 @@ class InputTextControl extends BaseInputTextControl { public InputTextControl(final HTMLBaseInputElement modelNode) { super(modelNode); final JTextField w = (JTextField) this.widget; + w.addKeyListener(new KeyListener() { + @Override + public void keyTyped(final KeyEvent e) { + // System.out.println("typed: " + e); + } + + @Override + public void keyReleased(final KeyEvent e) { + HtmlController.getInstance().onKeyUp(modelNode, e); + } + + @Override + public void keyPressed(final KeyEvent e) { + // System.out.println("pressed: " + e); + } + }); w.addActionListener(new ActionListener() { public void actionPerformed(final ActionEvent event) { + System.out.println("Action performed: " + event); HtmlController.getInstance().onEnterPressed(modelNode, null); } }); diff --git a/src/HTML_Renderer/org/lobobrowser/html/renderer/RBlock.java b/src/HTML_Renderer/org/lobobrowser/html/renderer/RBlock.java index 2c557a67..9dda14df 100644 --- a/src/HTML_Renderer/org/lobobrowser/html/renderer/RBlock.java +++ b/src/HTML_Renderer/org/lobobrowser/html/renderer/RBlock.java @@ -442,9 +442,11 @@ private final LayoutValue forceLayout(final RenderState renderState, final int a // Expected to be invoked in the GUI thread. // TODO: Not necessary to do full layout if only expandWidth or // expandHeight change (specifically in tables). - RenderState rs = renderState; + final RenderState rs = renderState; if (rs == null) { - rs = new BlockRenderState(null); + // Doesn't seem to be null ever + throw new IllegalStateException("rs was null"); + // rs = new BlockRenderState(null); } // // Clear adjust() cache. diff --git a/src/HTML_Renderer/org/lobobrowser/html/renderer/RBlockViewport.java b/src/HTML_Renderer/org/lobobrowser/html/renderer/RBlockViewport.java index 42855140..9ccc79d5 100644 --- a/src/HTML_Renderer/org/lobobrowser/html/renderer/RBlockViewport.java +++ b/src/HTML_Renderer/org/lobobrowser/html/renderer/RBlockViewport.java @@ -1430,8 +1430,6 @@ public Iterator getRenderables(final int pointx, final int // Try to find in other renderables with z-index >= 0 first. int index = 0; if (size != 0) { - final int px = pointx; - final int py = pointy; // Must go in reverse order for (index = size; --index >= 0;) { final PositionedRenderable pr = otherArray[index]; @@ -1442,7 +1440,7 @@ public Iterator getRenderables(final int pointx, final int if (r instanceof BoundableRenderable) { final BoundableRenderable br = r; final Rectangle rbounds = br.getBounds(); - if (rbounds.contains(px, py)) { + if (rbounds.contains(pointx, pointy)) { if (result == null) { result = new LinkedList<>(); } @@ -2162,7 +2160,8 @@ public void layoutMarkup(final RBlockViewport bodyLayout, final HTMLElementImpl final String display = style.getDisplay(); if (display != null) { if ("none".equalsIgnoreCase(display)) { - return; + /* Dromeao tests use an iframe with display set to none; If we return here, then scripts in the iframe don't load. */ + // return; } else if ("block".equalsIgnoreCase(display)) { currMethod = ADD_AS_BLOCK; } else if ("inline".equalsIgnoreCase(display)) { @@ -2230,10 +2229,10 @@ public void layoutMarkup(final RBlockViewport bodyLayout, final HTMLElementImpl } } } + final UINode node = markupElement.getUINode(); switch (display) { case DISPLAY_NONE: // skip it completely. - final UINode node = markupElement.getUINode(); if (node instanceof BaseBoundableRenderable) { // This is necessary so that if the element is made // visible again, it can be invalidated. @@ -2242,16 +2241,24 @@ public void layoutMarkup(final RBlockViewport bodyLayout, final HTMLElementImpl break; case DISPLAY_BLOCK: //TODO refer issue #87 + if (node instanceof RTable) { + bodyLayout.layoutRTable(markupElement); + } else { + bodyLayout.layoutRBlock(markupElement); + } + break; + case DISPLAY_LIST_ITEM: final String tagName = markupElement.getTagName(); if ("UL".equalsIgnoreCase(tagName) || "OL".equalsIgnoreCase(tagName)) { bodyLayout.layoutList(markupElement); } else { - bodyLayout.layoutRBlock(markupElement); + // bodyLayout.layoutRBlock(markupElement); + bodyLayout.layoutListItem(markupElement); } break; - case DISPLAY_LIST_ITEM: + /*case DISPLAY_LIST_ITEM: bodyLayout.layoutListItem(markupElement); - break; + break;*/ case DISPLAY_TABLE: bodyLayout.layoutRTable(markupElement); break; diff --git a/src/HTML_Renderer/org/lobobrowser/html/style/BackgroundInfo.java b/src/HTML_Renderer/org/lobobrowser/html/style/BackgroundInfo.java index 9e9a9a0a..7a7d7f0c 100644 --- a/src/HTML_Renderer/org/lobobrowser/html/style/BackgroundInfo.java +++ b/src/HTML_Renderer/org/lobobrowser/html/style/BackgroundInfo.java @@ -35,4 +35,11 @@ public class BackgroundInfo { public static final int BR_NO_REPEAT = 1; public static final int BR_REPEAT_X = 2; public static final int BR_REPEAT_Y = 3; + + @Override + public String toString() { + return "BackgroundInfo [color=" + backgroundColor + ", img=" + backgroundImage + ", xposAbs=" + + backgroundXPositionAbsolute + ", xpos=" + backgroundXPosition + ", yposAbs=" + + backgroundYPositionAbsolute + ", ypos=" + backgroundYPosition + ", repeat=" + backgroundRepeat + "]"; + } } diff --git a/src/HTML_Renderer/org/lobobrowser/html/style/CSSUtilities.java b/src/HTML_Renderer/org/lobobrowser/html/style/CSSUtilities.java index 539d1b0b..45b633bf 100644 --- a/src/HTML_Renderer/org/lobobrowser/html/style/CSSUtilities.java +++ b/src/HTML_Renderer/org/lobobrowser/html/style/CSSUtilities.java @@ -95,10 +95,6 @@ public static InputSource getCssInputSourceForStyleSheet(final String text, fina return is; } - /* public static CSSOMParser mkParser() { - return new CSSOMParser(new SACParserCSS3()); - } */ - public static StyleSheet jParseStyleSheet(final org.w3c.dom.Node ownerNode, final String baseURI, final String stylesheetStr) { return jParseCSS2(ownerNode, baseURI, stylesheetStr); } diff --git a/src/HTML_Renderer/org/lobobrowser/html/style/ComputedJStyleProperties.java b/src/HTML_Renderer/org/lobobrowser/html/style/ComputedJStyleProperties.java index 616540f1..8bde3799 100644 --- a/src/HTML_Renderer/org/lobobrowser/html/style/ComputedJStyleProperties.java +++ b/src/HTML_Renderer/org/lobobrowser/html/style/ComputedJStyleProperties.java @@ -624,8 +624,9 @@ public void setZIndex(final String zIndex) throws DOMException { throw new UnsupportedOperationException(); } + // TODO: Temporary made public @Override - protected NodeData getNodeData() { + public NodeData getNodeData() { return this.nodeData; } } diff --git a/src/HTML_Renderer/org/lobobrowser/html/style/HtmlValues.java b/src/HTML_Renderer/org/lobobrowser/html/style/HtmlValues.java index d3e3f05b..7d873327 100644 --- a/src/HTML_Renderer/org/lobobrowser/html/style/HtmlValues.java +++ b/src/HTML_Renderer/org/lobobrowser/html/style/HtmlValues.java @@ -39,7 +39,7 @@ public class HtmlValues { public static final Map SYSTEM_FONTS = new HashMap<>(); private static final Logger logger = Logger.getLogger(HtmlValues.class.getName()); - public static final float DEFAULT_FONT_SIZE = 14.0f; + public static final float DEFAULT_FONT_SIZE = 16.0f; public static final int DEFAULT_FONT_SIZE_INT = (int) DEFAULT_FONT_SIZE; public static final Float DEFAULT_FONT_SIZE_BOX = new Float(DEFAULT_FONT_SIZE); public static final int DEFAULT_BORDER_WIDTH = 2; diff --git a/src/HTML_Renderer/org/lobobrowser/html/style/JStyleProperties.java b/src/HTML_Renderer/org/lobobrowser/html/style/JStyleProperties.java index a0e5230e..a5406460 100644 --- a/src/HTML_Renderer/org/lobobrowser/html/style/JStyleProperties.java +++ b/src/HTML_Renderer/org/lobobrowser/html/style/JStyleProperties.java @@ -629,7 +629,8 @@ public String getFloat() { return helperGetProperty("float"); } - abstract protected NodeData getNodeData(); + // TODO: temporary made public + abstract public NodeData getNodeData(); private String helperGetValue(final String propertyName) { final NodeData nodeData = getNodeData(); diff --git a/src/HTML_Renderer/org/lobobrowser/html/style/LocalJStyleProperties.java b/src/HTML_Renderer/org/lobobrowser/html/style/LocalJStyleProperties.java index 67196cd6..0bdd99af 100644 --- a/src/HTML_Renderer/org/lobobrowser/html/style/LocalJStyleProperties.java +++ b/src/HTML_Renderer/org/lobobrowser/html/style/LocalJStyleProperties.java @@ -631,8 +631,9 @@ public void setZIndex(final String zIndex) throws DOMException { updateInlineStyle("z-index", zIndex); } + // TODO: Temp made public @Override - protected NodeData getNodeData() { + public NodeData getNodeData() { final HTMLElementImpl ele = this.element; final String inlineStyle = ele.getAttribute("style"); if ((inlineStyle != null) && (inlineStyle.length() > 0)) { diff --git a/src/HTML_Renderer/org/lobobrowser/html/style/StyleElements.java b/src/HTML_Renderer/org/lobobrowser/html/style/StyleElements.java index cad15649..4fc14cc2 100644 --- a/src/HTML_Renderer/org/lobobrowser/html/style/StyleElements.java +++ b/src/HTML_Renderer/org/lobobrowser/html/style/StyleElements.java @@ -23,10 +23,11 @@ public static StyleSheet convertAttributesToStyles(final Node n) { //Analyze HTML attributes String attrs = ""; final String tagName = el.getTagName(); - if ("table".equals(tagName)) { + // Converting to uppercase because getTagName() is now returning upper case + if ("TABLE".equals(tagName)) { //setting table and cell borders attrs = getTableElementStyle(el, attrs); - } else if ("font".equals(tagName)) { + } else if ("FONT".equals(tagName)) { //Text properties attrs = getFontElementStyle(el, attrs); } diff --git a/src/HTML_Renderer/org/lobobrowser/js/JavaClassWrapper.java b/src/HTML_Renderer/org/lobobrowser/js/JavaClassWrapper.java index ecd1a072..394ccdb7 100644 --- a/src/HTML_Renderer/org/lobobrowser/js/JavaClassWrapper.java +++ b/src/HTML_Renderer/org/lobobrowser/js/JavaClassWrapper.java @@ -20,18 +20,25 @@ */ package org.lobobrowser.js; +import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.util.HashMap; import java.util.Map; +import org.lobobrowser.html.js.HideFromJS; import org.lobobrowser.html.js.NotGetterSetter; import org.lobobrowser.html.js.PropertyName; import org.mozilla.javascript.Function; +import org.mozilla.javascript.Scriptable; public class JavaClassWrapper { private final Class javaClass; private final Map functions = new HashMap<>(); private final Map properties = new HashMap<>(); + + private final Map staticFinalProperties = new HashMap<>(); + private PropertyInfo nameIndexer; private PropertyInfo integerIndexer; @@ -51,6 +58,10 @@ public String getClassName() { return lastDotIdx == -1 ? className : className.substring(lastDotIdx + 1); } + public String getCanonicalClassName() { + return this.javaClass.getCanonicalName(); + } + public Function getFunction(final String name) { return this.functions.get(name); } @@ -60,25 +71,35 @@ public PropertyInfo getProperty(final String name) { } private void scanMethods() { + final Field[] fields = javaClass.getFields(); + for (final Field f : fields) { + final int modifiers = f.getModifiers(); + if (Modifier.isPublic(modifiers) && Modifier.isStatic(modifiers) && Modifier.isFinal(modifiers)) { + staticFinalProperties.put(f.getName(), f); + } + } + final Method[] methods = this.javaClass.getMethods(); final int len = methods.length; for (int i = 0; i < len; i++) { final Method method = methods[i]; - final String name = method.getName(); - if (isPropertyMethod(name, method)) { - this.ensurePropertyKnown(name, method); - } else { - if (isNameIndexer(name, method)) { - this.updateNameIndexer(name, method); - } else if (isIntegerIndexer(name, method)) { - this.updateIntegerIndexer(name, method); + if (!method.isAnnotationPresent(HideFromJS.class)) { + final String name = method.getName(); + if (isPropertyMethod(name, method)) { + this.ensurePropertyKnown(name, method); + } else { + if (isNameIndexer(name, method)) { + this.updateNameIndexer(name, method); + } else if (isIntegerIndexer(name, method)) { + this.updateIntegerIndexer(name, method); + } + JavaFunctionObject f = this.functions.get(name); + if (f == null) { + f = new JavaFunctionObject(name, javaClass.getName()); + this.functions.put(name, f); + } + f.addMethod(method); } - JavaFunctionObject f = this.functions.get(name); - if (f == null) { - f = new JavaFunctionObject(name); - this.functions.put(name, f); - } - f.addMethod(method); } } } @@ -94,10 +115,8 @@ private static boolean isIntegerIndexer(final String name, final Method method) } private void updateNameIndexer(final String methodName, final Method method) { - boolean getter = true; - if (methodName.startsWith("set")) { - getter = false; - } + System.out.println("name indexer : " + methodName + " in " + javaClass); + final boolean getter = !methodName.startsWith("set"); PropertyInfo indexer = this.nameIndexer; if (indexer == null) { indexer = new PropertyInfo("$item", Object.class); @@ -111,10 +130,8 @@ private void updateNameIndexer(final String methodName, final Method method) { } private void updateIntegerIndexer(final String methodName, final Method method) { - boolean getter = true; - if (methodName.startsWith("set")) { - getter = false; - } + final boolean getter = !methodName.startsWith("set"); + System.out.println(" getter : " + getter); PropertyInfo indexer = this.integerIndexer; if (indexer == null) { final Class pt = getter ? method.getReturnType() : method.getParameterTypes()[1]; @@ -166,11 +183,13 @@ private static String propertyUncapitalize(final String text) { private void ensurePropertyKnown(final String methodName, final Method method) { String capPropertyName; boolean getter = false; + boolean setter = false; if (methodName.startsWith("get")) { capPropertyName = methodName.substring(3); getter = true; } else if (methodName.startsWith("set")) { capPropertyName = methodName.substring(3); + setter = method.getReturnType() == Void.TYPE; } else if (methodName.startsWith("is")) { capPropertyName = methodName.substring(2); getter = true; @@ -189,7 +208,8 @@ private void ensurePropertyKnown(final String methodName, final Method method) { } if (getter) { pinfo.setGetter(method); - } else { + } + if (setter) { pinfo.setSetter(method); } } @@ -198,4 +218,20 @@ private void ensurePropertyKnown(final String methodName, final Method method) { public String toString() { return this.javaClass.getName(); } + + public Map getProperties() { + return properties; + } + + public boolean hasInstance(final Scriptable instance) { + if (instance instanceof JavaObjectWrapper) { + final JavaObjectWrapper javaObjectWrapper = (JavaObjectWrapper) instance; + return javaClass.isInstance(javaObjectWrapper.getJavaObject()); + } + return javaClass.isInstance(instance); + } + + public Map getStaticFinalProperties() { + return staticFinalProperties; + } } diff --git a/src/HTML_Renderer/org/lobobrowser/js/JavaConstructorObject.java b/src/HTML_Renderer/org/lobobrowser/js/JavaConstructorObject.java index b86e7ebb..b4c0cd70 100644 --- a/src/HTML_Renderer/org/lobobrowser/js/JavaConstructorObject.java +++ b/src/HTML_Renderer/org/lobobrowser/js/JavaConstructorObject.java @@ -64,7 +64,9 @@ public Scriptable construct(final Context cx, final Scriptable scope, final Obje @Override public java.lang.Object getDefaultValue(final java.lang.Class hint) { - if (String.class.equals(hint)) { + // if (String.class.equals(hint)) { + // null is passed as hint when converting to string, hence adding it as an extra condition. + if (String.class.equals(hint) || (hint == null)) { return "function " + this.name; } else { return super.getDefaultValue(hint); @@ -83,4 +85,12 @@ public Object newInstance() throws InstantiationException, IllegalAccessExceptio return this.classWrapper.newInstance(); } } + + @Override + public boolean hasInstance(final Scriptable instance) { + // System.out.println("\nChecking if " + instance + " is instance of " + classWrapper); + // System.out.println(" result: " + classWrapper.hasInstance(instance)); + return classWrapper.hasInstance(instance); + // return super.hasInstance(instance); + } } diff --git a/src/HTML_Renderer/org/lobobrowser/js/JavaFunctionObject.java b/src/HTML_Renderer/org/lobobrowser/js/JavaFunctionObject.java index ea64b50d..3abcb25a 100644 --- a/src/HTML_Renderer/org/lobobrowser/js/JavaFunctionObject.java +++ b/src/HTML_Renderer/org/lobobrowser/js/JavaFunctionObject.java @@ -20,13 +20,16 @@ */ package org.lobobrowser.js; +import java.lang.reflect.Array; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; +import java.util.Arrays; import java.util.logging.Level; import java.util.logging.Logger; import org.lobobrowser.util.Objects; +import org.mozilla.javascript.Callable; import org.mozilla.javascript.Context; import org.mozilla.javascript.EvaluatorException; import org.mozilla.javascript.Function; @@ -35,30 +38,68 @@ import org.mozilla.javascript.WrappedException; public class JavaFunctionObject extends ScriptableObject implements Function { + // public class JavaFunctionObject extends BaseFunction implements Function { private static final Logger logger = Logger.getLogger(JavaFunctionObject.class.getName()); private static final boolean loggableInfo = logger.isLoggable(Level.INFO); + private final String methodName; private final String className; private final ArrayList methods = new ArrayList<>(); - public JavaFunctionObject(final String name) { + public JavaFunctionObject(final String name, final String className) { super(); - this.className = name; + this.methodName = name; + this.className = className; + + // Quick hack for issue #98 + defineProperty("call", new Callable() { + + public Object call(final Context cx, final Scriptable scope, final Scriptable thisObj, final Object[] args) { + System.out.println("Calling via function object: " + methodName); + if ((args.length > 0) && (args[0] instanceof JavaObjectWrapper)) { + final JavaObjectWrapper javaObjectWrapper = (JavaObjectWrapper) args[0]; + return JavaFunctionObject.this.call(cx, scope, javaObjectWrapper, Arrays.copyOfRange(args, 1, args.length)); + } else { + throw new RuntimeException("Unexpected condition"); + } + } + + }, org.mozilla.javascript.ScriptableObject.READONLY); } public void addMethod(final Method m) { + // System.out.println("Adding method:" + m); this.methods.add(m); } @Override public String getClassName() { - return this.className; + return this.methodName; } private static String getTypeName(final Object object) { return object == null ? "[null]" : object.getClass().getName(); } - private Method getExactMethod(final Object[] args) { + private final static class MethodAndArguments { + private final Method method; + private final Object[] args; + + public MethodAndArguments(final Method method, final Object[] args) { + this.method = method; + this.args = args; + } + + public Object invoke(final Object javaObject) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { + return method.invoke(javaObject, args); + } + + @Override + public String toString() { + return "MethodAndArguments [method=" + method + ", args=" + Arrays.toString(args) + "]"; + } + } + + private MethodAndArguments getExactMethod(final Object[] args) { final ArrayList methods = this.methods; final int size = methods.size(); for (int i = 0; i < size; i++) { @@ -66,20 +107,39 @@ private Method getExactMethod(final Object[] args) { final Class[] parameterTypes = m.getParameterTypes(); if (args == null) { if ((parameterTypes == null) || (parameterTypes.length == 0)) { - return m; + return new MethodAndArguments(m, null); } - } else if ((parameterTypes != null) && (args.length == parameterTypes.length)) { - if (Objects.areSameTo(args, parameterTypes)) { - return m; + } else if (parameterTypes != null) { + if (args.length == parameterTypes.length) { + if (Objects.areSameTo(args, parameterTypes)) { + return new MethodAndArguments(m, args); + } + } else if ((parameterTypes.length == 1) && parameterTypes[0].isArray()) { + final Class arrayType = parameterTypes[0].getComponentType(); + final boolean allSame = true; + for (int j = 0; j < args.length; j++) { + if (!Objects.isSameOrBox(args[j], arrayType)) { + break; + } + } + if (allSame) { + final Object[] argsInArray = (Object[]) Array.newInstance(arrayType, args.length); + for (int j = 0; j < args.length; j++) { + argsInArray[j] = args[j]; + } + return new MethodAndArguments(m, new Object[] { argsInArray }); + } + } } } return null; } - private Method getBestMethod(final Object[] args) { - final Method exactMethod = getExactMethod(args); + private MethodAndArguments getBestMethod(final Object[] args) { + final MethodAndArguments exactMethod = getExactMethod(args); if (exactMethod != null) { + // System.out.println("Found exact method: " + exactMethod); return exactMethod; } @@ -92,11 +152,14 @@ private Method getBestMethod(final Object[] args) { final Class[] parameterTypes = m.getParameterTypes(); if (args == null) { if ((parameterTypes == null) || (parameterTypes.length == 0)) { - return m; + return new MethodAndArguments(m, new Object[0]); } } else if ((parameterTypes != null) && (args.length >= parameterTypes.length)) { if (Objects.areAssignableTo(args, parameterTypes)) { - return m; + final Object[] actualArgs = convertArgs(args, parameterTypes.length, parameterTypes); + // System.out.println("Actual args: " + Arrays.toString(actualArgs)); + // System.out.println(" original args: " + Arrays.toString(args)); + return new MethodAndArguments(m, actualArgs); } if ((matchingMethod == null) || (parameterTypes.length > matchingNumParams)) { matchingNumParams = parameterTypes.length; @@ -107,19 +170,38 @@ private Method getBestMethod(final Object[] args) { if (size == 0) { throw new IllegalStateException("zero methods"); } - return matchingMethod; + if (matchingMethod == null) { + return null; + } else { + final Class[] actualArgTypes = matchingMethod.getParameterTypes(); + final Object[] actualArgs = convertArgs(args, matchingNumParams, actualArgTypes); + return new MethodAndArguments(matchingMethod, actualArgs); + } + } + + private static Object[] convertArgs(final Object[] args, final int numConvert, final Class[] actualArgTypes) { + final JavaScript manager = JavaScript.getInstance(); + final Object[] actualArgs = args == null ? new Object[0] : new Object[numConvert]; + for (int i = 0; i < numConvert; i++) { + final Object arg = args[i]; + actualArgs[i] = manager.getJavaObject(arg, actualArgTypes[i]); + } + return actualArgs; } public Object call(final Context cx, final Scriptable scope, final Scriptable thisObj, final Object[] args) { - final Method method = this.getBestMethod(args); - if (method == null) { - throw new EvaluatorException("No method matching " + this.className + " with " + (args == null ? 0 : args.length) + " arguments."); + // System.out.println("Going to call " + methodName + " in scope: " + Arrays.toString(scope.getIds())); + final MethodAndArguments methodAndArguments = this.getBestMethod(args); + if (methodAndArguments == null) { + throw new EvaluatorException("No method matching " + this.methodName + " with " + (args == null ? 0 : args.length) + " arguments in " + + className + " ."); } + final JavaScript manager = JavaScript.getInstance(); + /* final Class[] actualArgTypes = method.getParameterTypes(); final int numParams = actualArgTypes.length; final Object[] actualArgs = args == null ? new Object[0] : new Object[numParams]; final boolean linfo = loggableInfo; - final JavaScript manager = JavaScript.getInstance(); for (int i = 0; i < numParams; i++) { final Object arg = args[i]; final Object actualArg = manager.getJavaObject(arg, actualArgTypes[i]); @@ -128,7 +210,7 @@ public Object call(final Context cx, final Scriptable scope, final Scriptable th + ") into actualArg=" + actualArg + ". Type expected by method is " + actualArgTypes[i].getName() + "."); } actualArgs[i] = actualArg; - } + }*/ try { if (thisObj instanceof JavaObjectWrapper) { final JavaObjectWrapper jcw = (JavaObjectWrapper) thisObj; @@ -138,7 +220,8 @@ public Object call(final Context cx, final Scriptable scope, final Scriptable th // " on object " + javaObject + " of type " + // this.getTypeName(javaObject)); // } - final Object raw = method.invoke(jcw.getJavaObject(), actualArgs); + // final Object raw = method.invoke(jcw.getJavaObject(), actualArgs); + final Object raw = methodAndArguments.invoke(jcw.getJavaObject()); // System.out.println("Invoked."); return manager.getJavascriptObject(raw, scope); } else { @@ -148,7 +231,8 @@ public Object call(final Context cx, final Scriptable scope, final Scriptable th // args.length)); // return manager.getJavascriptObject(raw, scope); // } else { - final Object raw = method.invoke(thisObj, actualArgs); + // final Object raw = method.invoke(thisObj, actualArgs); + final Object raw = methodAndArguments.invoke(thisObj); return manager.getJavascriptObject(raw, scope); // } @@ -156,21 +240,23 @@ public Object call(final Context cx, final Scriptable scope, final Scriptable th // return call(cx, scope, getParentScope(), args); } } catch (final IllegalAccessException iae) { - throw new IllegalStateException("Unable to call " + this.className + ".", iae); + throw new IllegalStateException("Unable to call " + this.methodName + ".", iae); } catch (final InvocationTargetException ite) { // throw new WrappedException(new // InvocationTargetException(ite.getCause(), "Unable to call " + // this.className + " on " + jcw.getJavaObject() + ".")); - throw new WrappedException(new InvocationTargetException(ite.getCause(), "Unable to call " + this.className + " on " + thisObj + ".")); + throw new WrappedException( + new InvocationTargetException(ite.getCause(), "Unable to call " + this.methodName + " on " + thisObj + ".")); } catch (final IllegalArgumentException iae) { final StringBuffer argTypes = new StringBuffer(); - for (int i = 0; i < actualArgs.length; i++) { + for (int i = 0; i < methodAndArguments.args.length; i++) { if (i > 0) { argTypes.append(", "); } - argTypes.append(actualArgs[i] == null ? "" : actualArgs[i].getClass().getName()); + argTypes.append(methodAndArguments.args[i] == null ? "" : methodAndArguments.args[i].getClass().getName()); } - throw new WrappedException(new IllegalArgumentException("Unable to call " + this.className + ". Argument types: " + argTypes + ".", + throw new WrappedException(new IllegalArgumentException("Unable to call " + this.methodName + " in " + className + + ". Argument types: " + argTypes + "." + "\n on method: " + methodAndArguments.method, iae)); } } @@ -181,7 +267,7 @@ public java.lang.Object getDefaultValue(final java.lang.Class hint) { logger.info("getDefaultValue(): hint=" + hint + ",this=" + this); } if ((hint == null) || String.class.equals(hint)) { - return "function " + this.className; + return "function " + this.methodName; } else { return super.getDefaultValue(hint); } diff --git a/src/HTML_Renderer/org/lobobrowser/js/JavaObjectWrapper.java b/src/HTML_Renderer/org/lobobrowser/js/JavaObjectWrapper.java index cb69d317..7da73d23 100644 --- a/src/HTML_Renderer/org/lobobrowser/js/JavaObjectWrapper.java +++ b/src/HTML_Renderer/org/lobobrowser/js/JavaObjectWrapper.java @@ -20,15 +20,20 @@ */ package org.lobobrowser.js; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.security.AccessController; +import java.security.PrivilegedAction; import java.util.logging.Level; import java.util.logging.Logger; +import org.lobobrowser.html.js.Window; import org.mozilla.javascript.EvaluatorException; import org.mozilla.javascript.Function; import org.mozilla.javascript.Scriptable; import org.mozilla.javascript.ScriptableObject; import org.mozilla.javascript.WrappedException; +import org.mozilla.javascript.arrays.ExternalArray; public class JavaObjectWrapper extends ScriptableObject { private static final Logger logger = Logger.getLogger(JavaObjectWrapper.class.getName()); @@ -38,6 +43,11 @@ public class JavaObjectWrapper extends ScriptableObject { @Override public void setParentScope(final Scriptable m) { + // Don't allow Window's parent scope to be changed. Fixes GH #29 + if (classWrapper.getCanonicalClassName().equals(Window.class.getCanonicalName())) { + return; + } + if (m == this) { // TODO: This happens when running jQuery 2 super.setParentScope(null); @@ -53,6 +63,7 @@ public JavaObjectWrapper(final JavaClassWrapper classWrapper) throws Instantiati // and weak values. final Object delegate = this.classWrapper.newInstance(); this.delegate = delegate; + setupProperties(); } public JavaObjectWrapper(final JavaClassWrapper classWrapper, final Object delegate) { @@ -64,6 +75,70 @@ public JavaObjectWrapper(final JavaClassWrapper classWrapper, final Object deleg // that the object wrapper map uses weak keys // and weak values. this.delegate = delegate; + setupProperties(); + } + + private void setupProperties() { + final PropertyInfo integerIndexer = classWrapper.getIntegerIndexer(); + if (integerIndexer != null) { + setExternalArray(new ExternalArray() { + + @Override + public int getLength() { + try { + // TODO: Some length() methods are returning integer while others return length. A good test case is http://web-platform.test:8000/dom/nodes/Element-classlist.html + // Check if length() methods can be converted to return a single type. + final Object lengthObj = classWrapper.getProperty("length").getGetter().invoke(delegate, (Object[]) null); + if (lengthObj instanceof Long) { + + final long lengthLong = (long) lengthObj; + final int lengthInt = (int) lengthLong; + // TODO: Check for overflow when casting to int and throw an exception + return lengthInt; + } else if (lengthObj instanceof Integer) { + return (int) lengthObj; + } else { + // TODO: Throw exception + throw new RuntimeException("Can't represent length as an integer type"); + } + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + return 0; + } + } + + @Override + protected Object getElement(final int index) { + try { + final Object result = JavaScript.getInstance().getJavascriptObject( + integerIndexer.getGetter().invoke(delegate, new Object[] { index }), null); + return result; + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + throw new RuntimeException("Error accessing a indexed element"); + } + } + + @Override + protected void putElement(final int index, final Object value) { + // TODO: Can this be supported? Needs a setter. + throw new UnsupportedOperationException("Writing to an indexed object"); + } + }); + } + classWrapper.getProperties().forEach((name, property) -> { + // System.out.println("In " + classWrapper.getClassName() + " Defining " + name + " : " + property); + // TODO: Don't setup properties if getter is null? Are write-only properties supported in JS? + defineProperty(name, null, property.getGetter(), property.getSetter(), 0); + }); + classWrapper.getStaticFinalProperties().forEach((name, field) -> { + // System.out.println("In " + classWrapper.getClassName() + " Defining " + name + " : " + property); + try { + defineProperty(name, field.get(null), READONLY); + } catch (final Exception e) { + throw new RuntimeException(e); + } + }); } /** @@ -81,33 +156,35 @@ public String getClassName() { return this.classWrapper.getClassName(); } + /* @Override public Object get(final int index, final Scriptable start) { - final PropertyInfo pinfo = this.classWrapper.getIntegerIndexer(); - if (pinfo == null) { - return super.get(index, start); - } else { - try { - final Method getter = pinfo.getGetter(); - if (getter == null) { - throw new EvaluatorException("Indexer is write-only"); - } - // Cannot retain delegate with a strong reference. - final Object javaObject = this.getJavaObject(); - if (javaObject == null) { - throw new IllegalStateException("Java object (class=" + this.classWrapper + ") is null."); - } - final Object raw = getter.invoke(javaObject, new Object[] { new Integer(index) }); - if (raw == null) { - // Return this instead of null. - return Scriptable.NOT_FOUND; - } - return JavaScript.getInstance().getJavascriptObject(raw, this.getParentScope()); - } catch (final Exception err) { - throw new WrappedException(err); + System.out.println("indexing " + index + " into " + classWrapper.getClassName()); + final PropertyInfo pinfo = this.classWrapper.getIntegerIndexer(); + if (pinfo == null) { + return super.get(index, start); + } else { + try { + final Method getter = pinfo.getGetter(); + if (getter == null) { + throw new EvaluatorException("Indexer is write-only"); + } + // Cannot retain delegate with a strong reference. + final Object javaObject = this.getJavaObject(); + if (javaObject == null) { + throw new IllegalStateException("Java object (class=" + this.classWrapper + ") is null."); } + final Object raw = getter.invoke(javaObject, new Object[] { new Integer(index) }); + if (raw == null) { + // Return this instead of null. + return Scriptable.NOT_FOUND; + } + return JavaScript.getInstance().getJavascriptObject(raw, this.getParentScope()); + } catch (final Exception err) { + throw new WrappedException(err); } } + }*/ @Override public Object get(final String name, final Scriptable start) { @@ -117,17 +194,27 @@ public Object get(final String name, final Scriptable start) { if (getter == null) { throw new EvaluatorException("Property '" + name + "' is not readable"); } - try { - // Cannot retain delegate with a strong reference. - final Object javaObject = this.getJavaObject(); - if (javaObject == null) { - throw new IllegalStateException("Java object (class=" + this.classWrapper + ") is null."); - } - final Object val = getter.invoke(javaObject, (Object[]) null); - return JavaScript.getInstance().getJavascriptObject(val, start.getParentScope()); - } catch (final Exception err) { - throw new WrappedException(err); + // try { + // Cannot retain delegate with a strong reference. + final Object javaObject = this.getJavaObject(); + if (javaObject == null) { + throw new IllegalStateException("Java object (class=" + this.classWrapper + ") is null."); } + final Object val = AccessController.doPrivileged(new PrivilegedAction() { + + public Object run() { + try { + return getter.invoke(javaObject, (Object[]) null); + } catch (final Exception err) { + throw new WrappedException(err); + } + } + }); + // final Object val = getter.invoke(javaObject, (Object[]) null); + return JavaScript.getInstance().getJavascriptObject(val, start.getParentScope()); + // } catch (final Exception err) { + // throw new WrappedException(err); + // } } else { final Function f = this.classWrapper.getFunction(name); if (f != null) { @@ -283,4 +370,10 @@ public boolean hasInstance(final Scriptable instance) { return super.hasInstance(instance); } } + + /* + @Override + public Object[] getIds() { + return getAllIds(); + }*/ } diff --git a/src/HTML_Renderer/org/lobobrowser/js/JavaScript.java b/src/HTML_Renderer/org/lobobrowser/js/JavaScript.java index 2998f32d..c3d65a51 100644 --- a/src/HTML_Renderer/org/lobobrowser/js/JavaScript.java +++ b/src/HTML_Renderer/org/lobobrowser/js/JavaScript.java @@ -24,6 +24,8 @@ import java.util.WeakHashMap; import org.lobobrowser.util.Objects; +import org.mozilla.javascript.Context; +import org.mozilla.javascript.ContextFactory; import org.mozilla.javascript.Scriptable; import org.mozilla.javascript.Undefined; @@ -38,6 +40,15 @@ public static JavaScript getInstance() { return instance; } + public static void init() { + ContextFactory.initGlobal(new ContextFactory() { + @Override + protected Context makeContext() { + throw new UnsupportedOperationException("Internal error. Global context factory should not be used."); + } + }); + } + /** * Returns an object that may be used by the Javascript engine. * @@ -56,14 +67,16 @@ public Object getJavascriptObject(final Object raw, final Scriptable scope) { // the JavaScript object. Reciprocal linking cannot // be done with weak hash maps and without leaking. synchronized (this) { - Scriptable javascriptObject = ((ScriptableDelegate) raw).getScriptable(); + final ScriptableDelegate delegate = (ScriptableDelegate) raw; + Scriptable javascriptObject = delegate.getScriptable(); if (javascriptObject == null) { final JavaObjectWrapper jow = new JavaObjectWrapper(JavaClassWrapperFactory.getInstance().getClassWrapper(raw.getClass()), raw); javascriptObject = jow; jow.setParentScope(scope); - ((ScriptableDelegate) raw).setScriptable(jow); + delegate.setScriptable(jow); + } else { + javascriptObject.setParentScope(scope); } - javascriptObject.setParentScope(scope); return javascriptObject; } } else if (Objects.isBoxClass(raw.getClass())) { @@ -100,6 +113,11 @@ private static String getStringValue(final Object object) { } public Object getJavaObject(final Object javascriptObject, final Class type) { + if (type == Boolean.TYPE) { + if ((javascriptObject == null) || (javascriptObject == Undefined.instance)) { + return Boolean.FALSE; + } + } if (javascriptObject instanceof JavaObjectWrapper) { final Object rawJavaObject = ((JavaObjectWrapper) javascriptObject).getJavaObject(); if (String.class == type) { diff --git a/src/Platform_Core/info/gngr/db/tables/records/CookiesRecord.java b/src/Platform_Core/info/gngr/db/tables/records/CookiesRecord.java index d3ee99c1..10b6cc60 100644 --- a/src/Platform_Core/info/gngr/db/tables/records/CookiesRecord.java +++ b/src/Platform_Core/info/gngr/db/tables/records/CookiesRecord.java @@ -14,8 +14,8 @@ comments = "This class is generated by jOOQ") @java.lang.SuppressWarnings({ "all", "unchecked", "rawtypes" }) public class CookiesRecord extends org.jooq.impl.UpdatableRecordImpl - implements - org.jooq.Record8 { +implements +org.jooq.Record8 { private static final long serialVersionUID = -1249579634; diff --git a/src/Platform_Core/info/gngr/db/tables/records/GlobalsRecord.java b/src/Platform_Core/info/gngr/db/tables/records/GlobalsRecord.java index c8e74552..2e75a385 100644 --- a/src/Platform_Core/info/gngr/db/tables/records/GlobalsRecord.java +++ b/src/Platform_Core/info/gngr/db/tables/records/GlobalsRecord.java @@ -14,7 +14,7 @@ comments = "This class is generated by jOOQ") @java.lang.SuppressWarnings({ "all", "unchecked", "rawtypes" }) public class GlobalsRecord extends org.jooq.impl.UpdatableRecordImpl implements - org.jooq.Record3 { +org.jooq.Record3 { private static final long serialVersionUID = 1572132991; diff --git a/src/Platform_Core/info/gngr/db/tables/records/PermissionsRecord.java b/src/Platform_Core/info/gngr/db/tables/records/PermissionsRecord.java index 1674e7cd..5f4ff027 100644 --- a/src/Platform_Core/info/gngr/db/tables/records/PermissionsRecord.java +++ b/src/Platform_Core/info/gngr/db/tables/records/PermissionsRecord.java @@ -14,7 +14,7 @@ comments = "This class is generated by jOOQ") @java.lang.SuppressWarnings({ "all", "unchecked", "rawtypes" }) public class PermissionsRecord extends org.jooq.impl.UpdatableRecordImpl implements - org.jooq.Record3 { +org.jooq.Record3 { private static final long serialVersionUID = 457680487; diff --git a/src/Platform_Core/org/lobobrowser/async/AsyncResultImpl.java b/src/Platform_Core/org/lobobrowser/async/AsyncResultImpl.java index 1b0d9258..83bd8f1a 100644 --- a/src/Platform_Core/org/lobobrowser/async/AsyncResultImpl.java +++ b/src/Platform_Core/org/lobobrowser/async/AsyncResultImpl.java @@ -53,17 +53,17 @@ public void addResultListener(final AsyncResultListener listener) { final Throwable exception = this.exception; EventQueue.invokeLater(() -> { // Invoke holding no locks - final AsyncResultEvent are = new AsyncResultEvent<>(AsyncResultImpl.this, exception); - listener.exceptionReceived(are); - }); + final AsyncResultEvent are = new AsyncResultEvent<>(AsyncResultImpl.this, exception); + listener.exceptionReceived(are); + }); } else { final TResult result = this.result; EventQueue.invokeLater(() -> { // Invoke holding no locks - final AsyncResultEvent are = new AsyncResultEvent<>(AsyncResultImpl.this, result); - listener.resultReceived(are); - }); + final AsyncResultEvent are = new AsyncResultEvent<>(AsyncResultImpl.this, result); + listener.resultReceived(are); + }); } } evtResult.addListener(new EventListenerWrapper<>(listener)); @@ -93,17 +93,17 @@ public void signal() { final Throwable exception = this.exception; EventQueue.invokeLater(() -> { // Invoke holding no locks - final AsyncResultEvent are = new AsyncResultEvent<>(AsyncResultImpl.this, exception); - evtResult.fireEvent(are); - }); + final AsyncResultEvent are = new AsyncResultEvent<>(AsyncResultImpl.this, exception); + evtResult.fireEvent(are); + }); } else { final TResult result = this.result; EventQueue.invokeLater(() -> { // Invoke holding no locks - final AsyncResultEvent are = new AsyncResultEvent<>(AsyncResultImpl.this, result); - evtResult.fireEvent(are); - }); + final AsyncResultEvent are = new AsyncResultEvent<>(AsyncResultImpl.this, result); + evtResult.fireEvent(are); + }); } } } diff --git a/src/Platform_Core/org/lobobrowser/context/NetworkRequestImpl.java b/src/Platform_Core/org/lobobrowser/context/NetworkRequestImpl.java index 17618a63..d5c01127 100644 --- a/src/Platform_Core/org/lobobrowser/context/NetworkRequestImpl.java +++ b/src/Platform_Core/org/lobobrowser/context/NetworkRequestImpl.java @@ -129,6 +129,9 @@ public String getStatusText() { private volatile RequestHandler currentRequestHandler; public void abort() { + this.readyState = NetworkRequest.STATE_ABORTED; + this.READY_STATE_CHANGE.fireEvent(new NetworkRequestEvent(this, this.readyState)); + final RequestHandler rhToDelete = this.currentRequestHandler; if (rhToDelete != null) { RequestEngine.getInstance().cancelRequest(rhToDelete); @@ -202,6 +205,8 @@ public void send(final String content, final Request requestType) throws IOExcep } catch (final Exception err) { logger.log(Level.SEVERE, "open()", err); } + } else { + abort(); } } @@ -213,81 +218,93 @@ public void processEvent(final EventObject event) { }); } + public boolean isAsnyc() { + return isAsynchronous; + } + private void changeReadyState(final int newState) { this.readyState = newState; this.READY_STATE_CHANGE.fireEvent(new NetworkRequestEvent(this, newState)); } private void setResponse(final ClientletResponse response) { - if (response.isFromCache()) { - final Object cachedResponse = response.getTransientCachedObject(); - if (cachedResponse instanceof CacheableResponse) { - // It can be of a different type. - final CacheableResponse cr = (CacheableResponse) cachedResponse; - this.changeReadyState(NetworkRequest.STATE_LOADING); - this.localResponse = cr.newLocalResponse(response); - this.changeReadyState(NetworkRequest.STATE_LOADED); - this.changeReadyState(NetworkRequest.STATE_INTERACTIVE); - this.changeReadyState(NetworkRequest.STATE_COMPLETE); - return; - } - } - try { - this.changeReadyState(NetworkRequest.STATE_LOADING); - final LocalResponse newResponse = new LocalResponse(response); - this.localResponse = newResponse; - this.changeReadyState(NetworkRequest.STATE_LOADED); - final int cl = response.getContentLength(); - final InputStream in = response.getInputStream(); - final int bufferSize = cl == -1 ? 8192 : Math.min(cl, 8192); - final byte[] buffer = new byte[bufferSize]; - int numRead; - int readSoFar = 0; - boolean firstTime = true; - final ClientletContext threadContext = ClientletAccess.getCurrentClientletContext(); - NavigatorProgressEvent prevProgress = null; - if (threadContext != null) { - prevProgress = threadContext.getProgressEvent(); + final Runnable runnable = () -> { + if (response.isFromCache()) { + final Object cachedResponse = response.getTransientCachedObject(); + if (cachedResponse instanceof CacheableResponse) { + // It can be of a different type. + final CacheableResponse cr = (CacheableResponse) cachedResponse; + this.changeReadyState(NetworkRequest.STATE_LOADING); + this.localResponse = cr.newLocalResponse(response); + this.changeReadyState(NetworkRequest.STATE_LOADED); + this.changeReadyState(NetworkRequest.STATE_INTERACTIVE); + this.changeReadyState(NetworkRequest.STATE_COMPLETE); + return; + } } try { - long lastProgress = 0; - while ((numRead = in.read(buffer)) != -1) { - if (numRead == 0) { - if (logger.isLoggable(Level.INFO)) { - logger.info("setResponse(): Read zero bytes from " + response.getResponseURL()); + this.changeReadyState(NetworkRequest.STATE_LOADING); + final LocalResponse newResponse = new LocalResponse(response); + this.localResponse = newResponse; + this.changeReadyState(NetworkRequest.STATE_LOADED); + final int cl = response.getContentLength(); + final InputStream in = response.getInputStream(); + final int bufferSize = cl == -1 ? 8192 : Math.min(cl, 8192); + final byte[] buffer = new byte[bufferSize]; + int numRead; + int readSoFar = 0; + boolean firstTime = true; + final ClientletContext threadContext = ClientletAccess.getCurrentClientletContext(); + NavigatorProgressEvent prevProgress = null; + if (threadContext != null) { + prevProgress = threadContext.getProgressEvent(); + } + try { + long lastProgress = 0; + while ((numRead = in.read(buffer)) != -1) { + if (numRead == 0) { + if (logger.isLoggable(Level.INFO)) { + logger.info("setResponse(): Read zero bytes from " + response.getResponseURL()); + } + break; } - break; - } - readSoFar += numRead; - if (threadContext != null) { - final long currentTime = System.currentTimeMillis(); - if ((currentTime - lastProgress) > 500) { - lastProgress = currentTime; - threadContext.setProgressEvent(ProgressType.CONTENT_LOADING, readSoFar, cl, response.getResponseURL()); + readSoFar += numRead; + if (threadContext != null) { + final long currentTime = System.currentTimeMillis(); + if ((currentTime - lastProgress) > 500) { + lastProgress = currentTime; + threadContext.setProgressEvent(ProgressType.CONTENT_LOADING, readSoFar, cl, response.getResponseURL()); + } + } + newResponse.writeBytes(buffer, 0, numRead); + if (firstTime) { + firstTime = false; + this.changeReadyState(NetworkRequest.STATE_INTERACTIVE); } } - newResponse.writeBytes(buffer, 0, numRead); - if (firstTime) { - firstTime = false; - this.changeReadyState(NetworkRequest.STATE_INTERACTIVE); + } finally { + if (threadContext != null) { + threadContext.setProgressEvent(prevProgress); } } - } finally { - if (threadContext != null) { - threadContext.setProgressEvent(prevProgress); + newResponse.setComplete(true); + // The following should return non-null if the response is complete. + final CacheableResponse cacheable = newResponse.getCacheableResponse(); + if (cacheable != null) { + response.setNewTransientCachedObject(cacheable, cacheable.getEstimatedSize()); } + this.changeReadyState(NetworkRequest.STATE_COMPLETE); + } catch (final IOException ioe) { + logger.log(Level.WARNING, "setResponse()", ioe); + this.localResponse = null; + this.changeReadyState(NetworkRequest.STATE_COMPLETE); } - newResponse.setComplete(true); - // The following should return non-null if the response is complete. - final CacheableResponse cacheable = newResponse.getCacheableResponse(); - if (cacheable != null) { - response.setNewTransientCachedObject(cacheable, cacheable.getEstimatedSize()); - } - this.changeReadyState(NetworkRequest.STATE_COMPLETE); - } catch (final IOException ioe) { - logger.log(Level.WARNING, "setResponse()", ioe); - this.localResponse = null; - this.changeReadyState(NetworkRequest.STATE_COMPLETE); + }; + if (isAsynchronous) { + // TODO: Use the JS queue to schedule this + runnable.run(); + } else { + runnable.run(); } } diff --git a/src/Platform_Core/org/lobobrowser/gui/AbstractBrowserWindow.java b/src/Platform_Core/org/lobobrowser/gui/AbstractBrowserWindow.java index c110d224..ff0b748c 100644 --- a/src/Platform_Core/org/lobobrowser/gui/AbstractBrowserWindow.java +++ b/src/Platform_Core/org/lobobrowser/gui/AbstractBrowserWindow.java @@ -26,11 +26,6 @@ * Browser windows should extend this class. */ public abstract class AbstractBrowserWindow extends JFrame implements BrowserWindow { - /** - * Gets the root {@link FramePanel} of the window. - */ - public abstract FramePanel getTopFramePanel(); - /** * Gets a {@link WindowCallback} instance that receives navigation * notifications. This method may return null. diff --git a/src/Platform_Core/org/lobobrowser/gui/FramePanel.java b/src/Platform_Core/org/lobobrowser/gui/FramePanel.java index 4dff8129..1c1c5147 100644 --- a/src/Platform_Core/org/lobobrowser/gui/FramePanel.java +++ b/src/Platform_Core/org/lobobrowser/gui/FramePanel.java @@ -110,7 +110,8 @@ public class FramePanel extends JPanel implements NavigatorFrame { private final Object propertiesMonitor = new Object(); private NavigatorFrame openerFrame; - private Window topFrameWindow; + + // private Window topFrameWindow; /** * Constructs a FramePanel specifying a "window" ID. @@ -577,6 +578,7 @@ protected void replaceContentImpl(final ClientletResponse response, final Compon this.updateContentProperties(content); } + System.out.println("Response: " + response); if (response != null) { final String title = content == null ? null : content.getTitle(); final String description = content == null ? null : content.getDescription(); @@ -715,6 +717,11 @@ private void navigate(final NavigationEvent event) { case SELF: this.navigateLocal(event); break; + case NAMED: + // TODO + final NavigatorFrame namedFrame = null; + namedFrame.navigate(url, method, paramInfo, TargetType.SELF, requestType, this); + break; case BLANK: this.open(url, method, paramInfo); break; @@ -765,9 +772,9 @@ private void navigateLocal(final NavigationEvent event) { SecurityUtil.doPrivileged(() -> { // Justification: While requests by untrusted code are generally only // allowed on certain hosts, navigation is an exception. - RequestEngine.getInstance().scheduleRequest(handler); - return null; - }); + RequestEngine.getInstance().scheduleRequest(handler); + return null; + }); } /** @@ -899,9 +906,9 @@ public void run() { SecurityUtil.doPrivileged(() -> { // Justification: While requests by untrusted code are generally only allowed on certain hosts, // navigation is an exception. - RequestEngine.getInstance().scheduleRequest(handler); - return null; - }); + RequestEngine.getInstance().scheduleRequest(handler); + return null; + }); return newFrame; } @@ -1237,13 +1244,14 @@ public void resizeWindowTo(final int width, final int height) { } } + /* public Window getTopFrameWindow() { return topFrameWindow; } public void setTopFrameWindow(final Window topFrameWindow) { this.topFrameWindow = topFrameWindow; - } + }*/ /** * Gets an object that is used to represent the current frame content. For diff --git a/src/Platform_Core/org/lobobrowser/gui/NavigatorWindowImpl.java b/src/Platform_Core/org/lobobrowser/gui/NavigatorWindowImpl.java index 1c234597..5d041a4e 100644 --- a/src/Platform_Core/org/lobobrowser/gui/NavigatorWindowImpl.java +++ b/src/Platform_Core/org/lobobrowser/gui/NavigatorWindowImpl.java @@ -381,7 +381,8 @@ public void toBack() { } public NavigatorFrame getTopFrame() { - return this.framePanel; + // TODO: This needs to be checked. In some quick tests like loading reddit, things seem to work fine whether we return framePanel or framePanel.getTopFrame() + return this.framePanel.getTopFrame(); } public void statusUpdated(final NavigatorFrame clientletFrame, final String value) { diff --git a/src/Platform_Core/org/lobobrowser/main/Extension.java b/src/Platform_Core/org/lobobrowser/main/Extension.java index 3ca77607..dd20e4f0 100644 --- a/src/Platform_Core/org/lobobrowser/main/Extension.java +++ b/src/Platform_Core/org/lobobrowser/main/Extension.java @@ -103,7 +103,7 @@ public static boolean isExtension(final File root) { return propsFile.exists(); } else { try ( - final JarFile jarFile = new JarFile(root)) { + final JarFile jarFile = new JarFile(root)) { final JarEntry jarEntry = jarFile.getJarEntry(EXTENSION_PROPERTIES_FILE); return jarEntry != null; } catch (final IOException e) { @@ -192,7 +192,7 @@ public boolean isPrimaryExtension() { private NavigatorExtension platformExtension; public void initClassLoader(final ClassLoader parentClassLoader) throws java.net.MalformedURLException, ClassNotFoundException, - IllegalAccessException, InstantiationException { + IllegalAccessException, InstantiationException { ClassLoader classLoader; if (extRoot != null) { final URL url = this.extRoot.toURI().toURL(); @@ -278,14 +278,14 @@ protected V doWithClassLoader(final Callable r) { final ClassLoader prevClassLoader = currentThread.getContextClassLoader(); final ClassLoader loader = this.classLoader; if (loader != null) { - currentThread.setContextClassLoader(loader); + // currentThread.setContextClassLoader(loader); } try { return r.call(); } catch (final Exception e) { throw new Error(e); } finally { - currentThread.setContextClassLoader(prevClassLoader); + // currentThread.setContextClassLoader(prevClassLoader); } } diff --git a/src/Platform_Core/org/lobobrowser/main/ExtensionManager.java b/src/Platform_Core/org/lobobrowser/main/ExtensionManager.java index 1cfe3fbc..56bfebd6 100644 --- a/src/Platform_Core/org/lobobrowser/main/ExtensionManager.java +++ b/src/Platform_Core/org/lobobrowser/main/ExtensionManager.java @@ -171,7 +171,7 @@ private void createExtensionsAndLibraries(final File[] extDirs, final File[] ext if (extDir.isFile()) { // Check if it is a jar. We will load jars from inside this jar. try ( - final JarFile jf = new JarFile(extDir);) { + final JarFile jf = new JarFile(extDir);) { // We can't close jf, because the class loader will load files lazily. for (final JarEntry jarEntry : (Iterable) jf.stream()::iterator) { if (!jarEntry.isDirectory() && jarEntry.getName().endsWith(".jar")) { @@ -411,17 +411,17 @@ public void handleError(final NavigatorFrame frame, final ClientletResponse resp EventQueue.invokeLater(() -> { final Collection ext = extensions; // Call all plugins once to see if they can select the response. - boolean dispatched = false; - for (final Extension ei : ext) { - if (ei.handleError(event)) { - dispatched = true; + boolean dispatched = false; + for (final Extension ei : ext) { + if (ei.handleError(event)) { + dispatched = true; + } } - } - if (!dispatched && logger.isLoggable(Level.INFO)) { - logger.log(Level.WARNING, "No error handlers found for error that occurred while processing response=[" + response + "].", - exception); - } - }); + if (!dispatched && logger.isLoggable(Level.INFO)) { + logger.log(Level.WARNING, "No error handlers found for error that occurred while processing response=[" + response + "].", + exception); + } + }); } public void dispatchBeforeNavigate(final NavigationEvent event) throws NavigationVetoException { diff --git a/src/Platform_Core/org/lobobrowser/main/PlatformInit.java b/src/Platform_Core/org/lobobrowser/main/PlatformInit.java index 4d2b38ca..9d63b9b1 100644 --- a/src/Platform_Core/org/lobobrowser/main/PlatformInit.java +++ b/src/Platform_Core/org/lobobrowser/main/PlatformInit.java @@ -99,7 +99,9 @@ private PlatformInit() { * @see #addPrivilegedPermission(Permission) */ public void initSecurity() { + // Set security policy and manager (essential) + // TODO: WHoa Policy.setPolicy(LocalSecurityPolicy.getInstance()); System.setSecurityManager(new LocalSecurityManager()); } @@ -117,6 +119,7 @@ public void initProtocols(final SSLSocketFactory sslSocketFactory) { // HttpsURLConnection.setDefaultSSLSocketFactory(sslSocketFactory); okHttpClient.setSslSocketFactory(sslSocketFactory); okHttpClient.setFollowRedirects(false); + okHttpClient.setFollowSslRedirects(false); factory.addFactory(new OkUrlFactory(okHttpClient)); factory.addFactory(new LocalStreamHandlerFactory()); } diff --git a/src/Platform_Core/org/lobobrowser/main/ReuseManager.java b/src/Platform_Core/org/lobobrowser/main/ReuseManager.java index d074a393..d8880675 100644 --- a/src/Platform_Core/org/lobobrowser/main/ReuseManager.java +++ b/src/Platform_Core/org/lobobrowser/main/ReuseManager.java @@ -80,8 +80,8 @@ public void launch(final String[] args, final SSLSocketFactory sslSocketFactory) // Look for running VM int port = -1; try ( - final InputStream in = new FileInputStream(portFile); - final DataInputStream din = new DataInputStream(in);) { + final InputStream in = new FileInputStream(portFile); + final DataInputStream din = new DataInputStream(in);) { port = din.readInt(); } catch (final java.io.EOFException eofe) { eofe.printStackTrace(System.err); @@ -91,11 +91,11 @@ public void launch(final String[] args, final SSLSocketFactory sslSocketFactory) } if (port != -1) { try ( - final Socket s = new Socket(bindHost, port);) { + final Socket s = new Socket(bindHost, port);) { s.setTcpNoDelay(true); try ( - final OutputStream out = s.getOutputStream(); - final OutputStreamWriter writer = new OutputStreamWriter(out);) { + final OutputStream out = s.getOutputStream(); + final OutputStreamWriter writer = new OutputStreamWriter(out);) { boolean hadPath = false; for (final String arg : args) { final String url = arg; @@ -136,8 +136,8 @@ public void launch(final String[] args, final SSLSocketFactory sslSocketFactory) } try ( - final OutputStream out = new FileOutputStream(portFile); - final DataOutputStream dout = new DataOutputStream(out);) { + final OutputStream out = new FileOutputStream(portFile); + final DataOutputStream dout = new DataOutputStream(out);) { dout.writeInt(port); dout.flush(); } diff --git a/src/Platform_Core/org/lobobrowser/main/TrustManager.java b/src/Platform_Core/org/lobobrowser/main/TrustManager.java index 45f08850..e79a2dcc 100644 --- a/src/Platform_Core/org/lobobrowser/main/TrustManager.java +++ b/src/Platform_Core/org/lobobrowser/main/TrustManager.java @@ -25,7 +25,7 @@ public static SSLSocketFactory makeSSLSocketFactory(final InputStream extraCerts final String hardDefaultPath = System.getProperty("java.home") + sep + "lib" + sep + "security" + sep + "cacerts"; final String defaultStorePath = System.getProperty("javax.net.ssl.trustStore", hardDefaultPath); try ( - final FileInputStream defaultIS = new FileInputStream(defaultStorePath)) { + final FileInputStream defaultIS = new FileInputStream(defaultStorePath)) { final KeyStore defKeyStore = KeyStore.getInstance(KeyStore.getDefaultType()); defKeyStore.load(defaultIS, "changeit".toCharArray()); diff --git a/src/Platform_Core/org/lobobrowser/request/CacheInfo.java b/src/Platform_Core/org/lobobrowser/request/CacheInfo.java index 8c49205c..a40e5bc4 100644 --- a/src/Platform_Core/org/lobobrowser/request/CacheInfo.java +++ b/src/Platform_Core/org/lobobrowser/request/CacheInfo.java @@ -171,8 +171,8 @@ public Object getPersistentObject(final ClassLoader classLoader) { } try ( - final InputStream in = new ByteArrayInputStream(content); - final ObjectInputStream oin = new ClassLoaderObjectInputStream(in, classLoader);) { + final InputStream in = new ByteArrayInputStream(content); + final ObjectInputStream oin = new ClassLoaderObjectInputStream(in, classLoader);) { return oin.readObject(); } } catch (final IOException ioe) { diff --git a/src/Platform_Core/org/lobobrowser/request/FileWithHeadersURLConnection.java b/src/Platform_Core/org/lobobrowser/request/FileWithHeadersURLConnection.java index e7b85b7a..dd7bbfa0 100644 --- a/src/Platform_Core/org/lobobrowser/request/FileWithHeadersURLConnection.java +++ b/src/Platform_Core/org/lobobrowser/request/FileWithHeadersURLConnection.java @@ -193,4 +193,14 @@ public Map> getHeaderFields() { public boolean usingProxy() { return false; } + + // Response code was not being set + // Not sure if this is the best way to change it + @Override + public int getResponseCode() throws IOException { + if (this.connected) { + return 200; + } + return super.getResponseCode(); + } } diff --git a/src/Platform_Core/org/lobobrowser/request/RequestEngine.java b/src/Platform_Core/org/lobobrowser/request/RequestEngine.java index a70efec4..3e48a563 100644 --- a/src/Platform_Core/org/lobobrowser/request/RequestEngine.java +++ b/src/Platform_Core/org/lobobrowser/request/RequestEngine.java @@ -34,6 +34,7 @@ import java.net.MalformedURLException; import java.net.ProtocolException; import java.net.Proxy; +import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.net.URLConnection; @@ -128,11 +129,14 @@ public String getCookie(final java.net.URL url) { return cookieText.toString(); } - public void setCookie(final URL url, final String cookieSpec) { - try { - this.cookieStore.saveCookie(url.toURI(), cookieSpec); - } catch (final URISyntaxException e) { - throw new RuntimeException(e); + public void setCookie(final URL url, final String cookieSpec, final UserAgentContext uaContext) { + // changed for issue #78 + if (uaContext.isRequestPermitted(new Request(url, RequestKind.Cookie))) { + try { + this.cookieStore.saveCookie(url.toURI(), cookieSpec); + } catch (final URISyntaxException e) { + throw new RuntimeException(e); + } } } @@ -176,7 +180,7 @@ public void scheduleRequest(final RequestHandler handler) { private void postData(final URLConnection connection, final ParameterInfo pinfo, final String altPostData) throws IOException { final BooleanSettings boolSettings = this.booleanSettings; - final String encoding = pinfo.getEncoding(); + final String encoding = pinfo != null ? pinfo.getEncoding() : NORMAL_FORM_ENCODING; if ((encoding == null) || NORMAL_FORM_ENCODING.equalsIgnoreCase(encoding)) { final ByteArrayOutputStream bufOut = new ByteArrayOutputStream(); if (pinfo != null) { @@ -671,9 +675,13 @@ private URLConnection getURLConnection(final URL connectionUrl, final ClientletR } addRequestProperties(connection, request, cacheInfo, method, connectionUrl, rhandler); - // TODO: Consider adding cookies here? addRequestedHeadersToRequest(connection, rhandler); + // Moved add cookies here since connection is initiated in this method for POST requests. + // And we can't add headers after the connection is made. + addCookiesToRequest(connection, rhandler); + // dumpRequestInfo(connection); + // Allow extensions to modify the connection object. // Doing it after addRequestProperties() to allow such // functionality as altering the Accept header. @@ -685,10 +693,16 @@ private URLConnection getURLConnection(final URL connectionUrl, final ClientletR // POST data if we need to. if (isPost) { final ParameterInfo pinfo = rhandler instanceof RedirectRequestHandler ? null : request.getParameterInfo(); - if (pinfo == null) { - throw new IllegalStateException("POST has no parameter information"); + final String altPostData = rhandler instanceof RedirectRequestHandler ? null : request.getAltPostData(); + System.out.println("Posting to: " + connectionUrl); + System.out.println(" pinfo: " + pinfo); + System.out.println(" alt post data: " + altPostData); + if ((pinfo == null) && (altPostData == null)) { + // throw new IllegalStateException("POST has no parameter information"); + logger.warning("POST has no parameter information"); + } else { + this.postData(connection, pinfo, altPostData); } - this.postData(connection, pinfo, request.getAltPostData()); } return connection; } @@ -744,7 +758,8 @@ private void processHandler(final RequestHandler rhandler, final int recursionLe final CacheInfo cacheInfo = getCacheInfo(rhandler, connectionUrl, isGet); try { URLConnection connection = this.getURLConnection(connectionUrl, request, protocol, method, rhandler, cacheInfo); - addCookiesToRequest(connection, rhandler); + // addCookiesToRequest(connection, rhandler); + // This causes exceptions sometimes (when the connection is already open) // dumpRequestInfo(connection); @@ -762,13 +777,18 @@ private void processHandler(final RequestHandler rhandler, final int recursionLe rhandler.handleProgress(ProgressType.CONNECTING, url, method, 0, -1); // Handle response boolean isContentCached = (cacheInfo != null) && cacheInfo.isCacheConnection(connection); + + if (isContentCached) { + System.out.println("URL Cached: " + url); + } + boolean isCacheable = false; if ((connection instanceof HttpURLConnection) && !isContentCached) { final HttpURLConnection hconnection = (HttpURLConnection) connection; hconnection.setInstanceFollowRedirects(false); final int responseCode = hconnection.getResponseCode(); logInfo("run(): ResponseCode=" + responseCode + " for url=" + connectionUrl); - dumpResponseInfo(connection); + // dumpResponseInfo(connection); handleCookies(connectionUrl, hconnection, rhandler); if (responseCode == HttpURLConnection.HTTP_OK) { @@ -897,10 +917,16 @@ private void addCookiesToRequest(final URLConnection connection, final RequestHa final String protocol = connection.getURL().getProtocol(); if ("http".equalsIgnoreCase(protocol) || "https".equalsIgnoreCase(protocol)) { final URL url = connection.getURL(); - if (rhandler.getContext().isRequestPermitted(new Request(url, RequestKind.Cookie))) { - final Map> cookieHeaders = cookieHandler.get(url.toURI(), null); - addCookieHeaderToRequest(connection, cookieHeaders, "Cookie"); - // addCookieHeaderToRequest(connection, cookieHeaders, "Cookie2"); + final URI uri = new URI(url.getProtocol(), url.getUserInfo(), url.getHost(), url.getPort(), url.getPath(), url.getQuery(), + url.getRef()); + // TODO: optimization #1: list is not required if we directly call our CookieHandler implementation + // TODO: optimization #2: even if we use the CookieHandler interface, we can avoid the joining of List entries, since our impl always returns a single element list + final Map> cookieHeaders = cookieHandler.get(uri, null); + if (!cookieHeaders.isEmpty()) { + if (rhandler.getContext().isRequestPermitted(new Request(url, RequestKind.Cookie))) { + addCookieHeaderToRequest(connection, cookieHeaders, "Cookie"); + // addCookieHeaderToRequest(connection, cookieHeaders, "Cookie2"); + } } } } catch (IOException | URISyntaxException e) { diff --git a/src/Platform_Core/org/lobobrowser/request/SilentUserAgentContextImpl.java b/src/Platform_Core/org/lobobrowser/request/SilentUserAgentContextImpl.java index bb8b92cf..eb8ef66b 100644 --- a/src/Platform_Core/org/lobobrowser/request/SilentUserAgentContextImpl.java +++ b/src/Platform_Core/org/lobobrowser/request/SilentUserAgentContextImpl.java @@ -122,7 +122,8 @@ public boolean isScriptingEnabled() { public void setCookie(final java.net.URL url, final String cookieSpec) { // Requires privileges. - RequestEngine.getInstance().setCookie(url, cookieSpec); + // Changed for issue #78 + RequestEngine.getInstance().setCookie(url, cookieSpec, this); } public Policy getSecurityPolicy() { diff --git a/src/Platform_Core/org/lobobrowser/request/UserAgentContextImpl.java b/src/Platform_Core/org/lobobrowser/request/UserAgentContextImpl.java index 08ada09b..ff0c38a2 100644 --- a/src/Platform_Core/org/lobobrowser/request/UserAgentContextImpl.java +++ b/src/Platform_Core/org/lobobrowser/request/UserAgentContextImpl.java @@ -137,7 +137,8 @@ public boolean isScriptingEnabled() { public void setCookie(final java.net.URL url, final String cookieSpec) { // Requires privileges. - RequestEngine.getInstance().setCookie(url, cookieSpec); + // changed for issue #78 + RequestEngine.getInstance().setCookie(url, cookieSpec, this); } public Policy getSecurityPolicy() { diff --git a/src/Platform_Core/org/lobobrowser/request/UserAgentImpl.java b/src/Platform_Core/org/lobobrowser/request/UserAgentImpl.java index 489648ec..1512b0c2 100644 --- a/src/Platform_Core/org/lobobrowser/request/UserAgentImpl.java +++ b/src/Platform_Core/org/lobobrowser/request/UserAgentImpl.java @@ -42,7 +42,8 @@ public static UserAgentImpl getInstance() { } public String getName() { - return "gngr"; + return ""; + // return "gngr"; } public String getMajorVersion() { @@ -64,6 +65,8 @@ public String getJavaVersion() { private volatile String textValue = null; public String getUserAgentString() { + // TODO: Whoa, Change! + // return "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:28.0) Gecko/20100101 Firefox/28.0"; return ""; } diff --git a/src/Platform_Core/org/lobobrowser/security/LocalSecurityPolicy.java b/src/Platform_Core/org/lobobrowser/security/LocalSecurityPolicy.java index f9abd853..c6756ac7 100644 --- a/src/Platform_Core/org/lobobrowser/security/LocalSecurityPolicy.java +++ b/src/Platform_Core/org/lobobrowser/security/LocalSecurityPolicy.java @@ -33,12 +33,14 @@ import java.net.URLPermission; import java.security.AccessControlException; import java.security.AccessController; +import java.security.AllPermission; import java.security.CodeSource; import java.security.Permission; import java.security.PermissionCollection; import java.security.Permissions; import java.security.Policy; import java.security.PrivilegedAction; +import java.security.ProtectionDomain; import java.security.SecurityPermission; import java.util.Arrays; import java.util.Collection; @@ -48,6 +50,8 @@ import javax.net.ssl.SSLPermission; +import jdk.net.NetworkPermission; + import org.lobobrowser.main.ExtensionManager; import org.lobobrowser.main.PlatformInit; import org.lobobrowser.request.DomainValidation; @@ -59,6 +63,9 @@ public class LocalSecurityPolicy extends Policy { private static final String JAVA_CLASS_PATH = System.getProperty("java.class.path"); private static final String PATH_SEPARATOR = System.getProperty("path.separator"); + // For js debugger + private static final String USER_HOME = System.getProperty("user.home"); + /** * Directory where gngr should save files. Any files saved here have * privileges of a remote file. @@ -138,6 +145,13 @@ private static void initCorePermissions() { CORE_PERMISSIONS.add(new java.util.logging.LoggingPermission("control", null)); CORE_PERMISSIONS.add(GenericLocalPermission.EXT_GENERIC); + // For stopping JS Scheduler + CORE_PERMISSIONS.add(new RuntimePermission("stopThread")); + + // For JS Debugger + // CORE_PERMISSIONS.add(new PropertyPermission("user.home", "read")); + // CORE_PERMISSIONS.add(new FilePermission(System.getProperty("user.home") + recursiveSuffix, "read")); + copyPermissions(EXTENSION_PERMISSIONS, CORE_PERMISSIONS); addStoreDirectoryPermissions(CORE_PERMISSIONS); @@ -245,6 +259,9 @@ public Boolean run() { } catch (final java.io.IOException ioe) { ioe.printStackTrace(System.err); return false; + + // Temp for js debugger, but most probably not right and doesn't work + // return true; } } }); @@ -287,6 +304,14 @@ private static boolean unoMatch(final URL url) { } } + @Override + public PermissionCollection getPermissions(final ProtectionDomain domain) { + // System.out.println("Permissions for protection domain: " + domain.getCodeSource()); + // System.out.println(" principals: " + domain.getPrincipals()); + // System.out.println(" loader: " + domain.getClassLoader()); + return super.getPermissions(domain); + } + /* * (non-Javadoc) * @@ -295,14 +320,17 @@ private static boolean unoMatch(final URL url) { @Override public PermissionCollection getPermissions(final CodeSource codesource) { if (codesource == null) { - throw new AccessControlException("codesource was null"); + // throw new AccessControlException("codesource was null"); + final Permissions permissions = new Permissions(); + permissions.add(new AllPermission()); // TODO: Whoa! all permissions? + return permissions; } if (PlatformInit.getInstance().debugOn) { System.out.println("Codesource: " + codesource.getLocation()); - if (codesource.getCodeSigners() != null) { - System.out.println(" signers: " + codesource.getCodeSigners().length); - } + // if (codesource.getCodeSigners() != null) { + // System.out.println(" signers: " + codesource.getCodeSigners().length); + // } } // TODO: Important: This was required after switching to JDK Rhino. This @@ -332,6 +360,14 @@ public PermissionCollection getPermissions(final CodeSource codesource) { final boolean isLocal = isLocal(location); final Permissions permissions = new Permissions(); + + // TODO: for js debugger + permissions.add(new PropertyPermission("user.home", "read")); + permissions.add(new FilePermission(USER_HOME, "read")); + permissions.add(new FilePermission(USER_HOME + recursiveSuffix, "read")); + permissions.add(new NetworkPermission("getProxySelector")); + permissions.add(new AWTPermission("*")); + if (isLocal) { final String path = location.getPath(); // System.out.println("Path: " + path); @@ -390,6 +426,11 @@ public PermissionCollection getPermissions(final CodeSource codesource) { permissions.add(new PropertyPermission("line.separator", "read")); permissions.add(new RuntimePermission("getClassLoader")); + // Extra for debugger: + // permissions.add(new PropertyPermission("user.home", "read")); + // permissions.add(new FilePermission(System.getProperty("user.home"), "read")); + // permissions.add(new SocketPermission("*", "connect,resolve")); + } else if (path.endsWith("okhttp-urlconnection-2.1.1-SNAPSHOT.jar")) { permissions.add(new NetPermission("getProxySelector")); permissions.add(new NetPermission("getCookieHandler")); @@ -431,7 +472,7 @@ public PermissionCollection getPermissions(final CodeSource codesource) { } if (PlatformInit.getInstance().debugOn) { - System.out.println("Returning permissions: " + permissions); + // System.out.println("Returning permissions: " + permissions); } return permissions; diff --git a/src/Platform_Core/org/lobobrowser/security/RequestManager.java b/src/Platform_Core/org/lobobrowser/security/RequestManager.java index e067009b..8eb03c78 100644 --- a/src/Platform_Core/org/lobobrowser/security/RequestManager.java +++ b/src/Platform_Core/org/lobobrowser/security/RequestManager.java @@ -41,6 +41,7 @@ public final class RequestManager { private final NavigatorFrame frame; public RequestManager(final NavigatorFrame frame) { + System.out.println("Creating req mgr for: " + frame); this.frame = frame; } @@ -76,11 +77,15 @@ private void ensureHostInCounter(final String host) { private Optional getFrameNavigationEntry() { final NavigationEntry currentNavigationEntry = frame.getCurrentNavigationEntry(); + // System.out.println(" frame nav entry: " + currentNavigationEntry); return Optional.ofNullable(currentNavigationEntry); } private Optional getFrameHost() { - return getFrameNavigationEntry().map(e -> e.getUrl().getHost().toLowerCase()); + return getFrameNavigationEntry().map(e -> { + final String host = e.getUrl().getHost(); + return host == null ? "" : host.toLowerCase(); + }); } private Optional getFrameURL() { @@ -101,12 +106,14 @@ private Request rewriteRequest(final Request request) { } public boolean isRequestPermitted(final Request request) { + // System.out.println("Checking request: " + request); final Request finalRequest = rewriteRequest(request); if (permissionSystemOpt.isPresent()) { final Boolean permitted = permissionSystemOpt.map(p -> p.isRequestPermitted(finalRequest)).orElse(false); updateCounter(finalRequest); // dumpCounters(); + // System.out.println(" Permitted: " + permitted); return permitted; } else { logger.severe("Unexpected permission system state. Request without context!"); diff --git a/src/Platform_Core/org/lobobrowser/store/RestrictedStore.java b/src/Platform_Core/org/lobobrowser/store/RestrictedStore.java index f61d19d0..90b47999 100644 --- a/src/Platform_Core/org/lobobrowser/store/RestrictedStore.java +++ b/src/Platform_Core/org/lobobrowser/store/RestrictedStore.java @@ -115,8 +115,8 @@ private void updateSizeFileImpl(final long totalSize) throws IOException { final File sizeFile = new File(this.baseDirectory, SIZE_FILE_NAME); try ( - final FileOutputStream out = new FileOutputStream(sizeFile); - final DataOutputStream dout = new DataOutputStream(out);) { + final FileOutputStream out = new FileOutputStream(sizeFile); + final DataOutputStream dout = new DataOutputStream(out);) { dout.writeLong(totalSize); dout.flush(); } @@ -381,8 +381,8 @@ public Object retrieveObject(final String path) throws IOException, ClassNotFoun public Object retrieveObject(final String path, final ClassLoader classLoader) throws IOException, ClassNotFoundException { final ManagedFile file = this.getManagedFile(path); try ( - final InputStream in = file.openInputStream(); - final ObjectInputStream oin = new ClassLoaderObjectInputStream(in, classLoader)) { + final InputStream in = file.openInputStream(); + final ObjectInputStream oin = new ClassLoaderObjectInputStream(in, classLoader)) { return oin.readObject(); } catch (final FileNotFoundException err) { return null; diff --git a/src/Platform_Core/org/lobobrowser/store/StorageManager.java b/src/Platform_Core/org/lobobrowser/store/StorageManager.java index 3c53fada..9b7c1505 100644 --- a/src/Platform_Core/org/lobobrowser/store/StorageManager.java +++ b/src/Platform_Core/org/lobobrowser/store/StorageManager.java @@ -222,9 +222,9 @@ public void saveSettings(final String name, final Serializable data) throws IOEx } final File file = new File(dir, name); try ( - final OutputStream out = new FileOutputStream(file); - final BufferedOutputStream bos = new BufferedOutputStream(out); - final ObjectOutputStream oos = new ObjectOutputStream(bos);) { + final OutputStream out = new FileOutputStream(file); + final BufferedOutputStream bos = new BufferedOutputStream(out); + final ObjectOutputStream oos = new ObjectOutputStream(bos);) { oos.writeObject(data); oos.flush(); } @@ -240,9 +240,9 @@ public Serializable retrieveSettings(final String name, final ClassLoader classL return null; } try ( - final InputStream in = new FileInputStream(file); - final BufferedInputStream bin = new BufferedInputStream(in); - final ObjectInputStream ois = new ClassLoaderObjectInputStream(bin, classLoader);) { + final InputStream in = new FileInputStream(file); + final BufferedInputStream bin = new BufferedInputStream(in); + final ObjectInputStream ois = new ClassLoaderObjectInputStream(bin, classLoader);) { return (Serializable) ois.readObject(); } catch (final InvalidClassException ice) { ice.printStackTrace(); diff --git a/src/Platform_Public_API/org/lobobrowser/ua/NetworkRequest.java b/src/Platform_Public_API/org/lobobrowser/ua/NetworkRequest.java index dfbb3bb8..e79866c4 100644 --- a/src/Platform_Public_API/org/lobobrowser/ua/NetworkRequest.java +++ b/src/Platform_Public_API/org/lobobrowser/ua/NetworkRequest.java @@ -72,6 +72,8 @@ public interface NetworkRequest { */ public static final int STATE_COMPLETE = 4; + public static final int STATE_ABORTED = 5; + /** * Gets the state of the request, a value between 0 and 4. * @@ -222,5 +224,7 @@ public interface NetworkRequest { Optional getURL(); + public boolean isAsnyc(); + public void addRequestedHeader(String header, String value); } diff --git a/src/Platform_Public_API/org/lobobrowser/ua/TargetType.java b/src/Platform_Public_API/org/lobobrowser/ua/TargetType.java index 6521f768..80cb6300 100644 --- a/src/Platform_Public_API/org/lobobrowser/ua/TargetType.java +++ b/src/Platform_Public_API/org/lobobrowser/ua/TargetType.java @@ -36,5 +36,7 @@ public enum TargetType { /** Document should be opened in the parent frame. */ PARENT, /** Document should be opened in the top frame of the current window. */ - TOP + TOP, + /** Document should be opened in the top frame of the current window. */ + NAMED } diff --git a/src/Primary_Extension/org/lobobrowser/primary/clientlets/html/BrowserFrameImpl.java b/src/Primary_Extension/org/lobobrowser/primary/clientlets/html/BrowserFrameImpl.java index 3978c9bd..08b27df2 100644 --- a/src/Primary_Extension/org/lobobrowser/primary/clientlets/html/BrowserFrameImpl.java +++ b/src/Primary_Extension/org/lobobrowser/primary/clientlets/html/BrowserFrameImpl.java @@ -32,6 +32,7 @@ import org.lobobrowser.html.BrowserFrame; import org.lobobrowser.html.HtmlRendererContext; import org.lobobrowser.ua.NavigatorFrame; +import org.lobobrowser.ua.ParameterInfo; import org.lobobrowser.ua.RequestType; import org.lobobrowser.ua.TargetType; @@ -78,4 +79,9 @@ public void setDefaultOverflowX(final int overflowX) { public void setDefaultOverflowY(final int overflowY) { this.frame.setProperty("defaultOverflowY", overflowY); } + + // Trying out a way for a frame's target to be set to an iframe. for issue #96 + public void navigate(final URL url, final String method, final ParameterInfo pinfo, final TargetType targetType, final RequestType form) { + frame.navigate(url, method, pinfo, targetType, form); + } } diff --git a/src/Primary_Extension/org/lobobrowser/primary/clientlets/html/HtmlRendererContextImpl.java b/src/Primary_Extension/org/lobobrowser/primary/clientlets/html/HtmlRendererContextImpl.java index be860b1a..46f15749 100644 --- a/src/Primary_Extension/org/lobobrowser/primary/clientlets/html/HtmlRendererContextImpl.java +++ b/src/Primary_Extension/org/lobobrowser/primary/clientlets/html/HtmlRendererContextImpl.java @@ -32,6 +32,7 @@ import java.lang.ref.WeakReference; import java.net.MalformedURLException; import java.net.URL; +import java.util.Arrays; import java.util.Map; import java.util.Optional; import java.util.Properties; @@ -48,6 +49,7 @@ import org.lobobrowser.html.HtmlRendererContext; import org.lobobrowser.html.domimpl.FrameNode; import org.lobobrowser.html.domimpl.HTMLDocumentImpl; +import org.lobobrowser.html.domimpl.HTMLIFrameElementImpl; import org.lobobrowser.html.domimpl.HTMLImageElementImpl; import org.lobobrowser.html.domimpl.HTMLLinkElementImpl; import org.lobobrowser.html.gui.HtmlPanel; @@ -161,15 +163,35 @@ private static TargetType getTargetType(final String target) { return TargetType.PARENT; } else if ("_top".equalsIgnoreCase(target)) { return TargetType.TOP; + } else if ((target != null) && (target.trim().length() > 0)) { + return TargetType.NAMED; } else { return TargetType.SELF; } } public void submitForm(final String method, final URL url, final String target, final String enctype, final FormInput[] formInputs) { + System.out.println("Submitting form to " + target); final TargetType targetType = HtmlRendererContextImpl.getTargetType(target); - final ParameterInfo pinfo = new LocalParameterInfo(enctype, formInputs); - this.clientletFrame.navigate(url, method, pinfo, targetType, RequestType.FORM); + System.out.println("target type " + targetType); + if (targetType == TargetType.NAMED) { + final HTMLCollection frames = getFrames(); + for (int i = 0; i < frames.getLength(); i++) { + final Node frame = frames.item(i); + if (frame instanceof HTMLIFrameElementImpl) { + final HTMLIFrameElementImpl iframe = (HTMLIFrameElementImpl) frame; + final String name = iframe.getAttribute("name"); + System.out.println("Found iframe with name: " + name); + final ParameterInfo pinfo = new LocalParameterInfo(enctype, formInputs); + iframe.navigate(url, method, pinfo, TargetType.SELF, RequestType.FORM); + return; + } + } + } else { + + final ParameterInfo pinfo = new LocalParameterInfo(enctype, formInputs); + this.clientletFrame.navigate(url, method, pinfo, targetType, RequestType.FORM); + } } public BrowserFrame createBrowserFrame() { @@ -544,6 +566,11 @@ public boolean isText() { } return params; } + + @Override + public String toString() { + return Arrays.toString(formInputs); + } } public void setCursor(final Optional cursorOpt) { diff --git a/src/Primary_Extension/org/lobobrowser/primary/ext/ComponentSource.java b/src/Primary_Extension/org/lobobrowser/primary/ext/ComponentSource.java index 78183dc5..e4344bc5 100644 --- a/src/Primary_Extension/org/lobobrowser/primary/ext/ComponentSource.java +++ b/src/Primary_Extension/org/lobobrowser/primary/ext/ComponentSource.java @@ -362,10 +362,14 @@ private static boolean isHistoryRequest(final RequestType requestType) { public void documentAccessed(final NavigatorWindowEvent event) { final java.net.URL url = event.getUrl(); + final boolean isRootEvent = event.getNavigatorFrame().getParentFrame() == null; + if (isRootEvent) { - // TODO: Have a better condition for isManageable, or change requestManager to deal with other protocols as well - final boolean isManageable = "http".equals(url.getProtocol()) || "https".equals(url.getProtocol()); - reqManagerButton.getAction().setEnabled(isManageable); + // TODO: Have a better condition for isManageable, or change requestManager to deal with other protocols as well + final boolean isManageable = "http".equals(url.getProtocol()) || "https".equals(url.getProtocol()); + reqManagerButton.getAction().setEnabled(isManageable); + // reqManagerButton.getAction().setEnabled(true); + } if ("GET".equals(event.getMethod()) && isHistoryRequest(event.getRequestType())) { NavigationHistory.getInstance().addAsRecent(url, null); diff --git a/src/Primary_Extension/org/lobobrowser/primary/ext/ExtensionImpl.java b/src/Primary_Extension/org/lobobrowser/primary/ext/ExtensionImpl.java index fc9cde1f..09266d9d 100644 --- a/src/Primary_Extension/org/lobobrowser/primary/ext/ExtensionImpl.java +++ b/src/Primary_Extension/org/lobobrowser/primary/ext/ExtensionImpl.java @@ -34,6 +34,7 @@ import org.lobobrowser.clientlet.NavigatorVersionException; import org.lobobrowser.html.HtmlRendererContext; import org.lobobrowser.html.gui.HtmlPanel; +import org.lobobrowser.js.JavaScript; import org.lobobrowser.main.PlatformInit; import org.lobobrowser.primary.clientlets.PrimaryClientletSelector; import org.lobobrowser.primary.clientlets.html.HtmlContent; @@ -64,6 +65,7 @@ public void errorOcurred(final NavigatorExceptionEvent event) { showError(event.getNavigatorFrame(), event.getResponse(), event.getException()); } }); + JavaScript.init(); } public void windowClosing(final NavigatorWindow wcontext) { diff --git a/src/Primary_Extension/org/lobobrowser/primary/gui/prefs/GeneralSettingsUI.java b/src/Primary_Extension/org/lobobrowser/primary/gui/prefs/GeneralSettingsUI.java index 78453751..bd6d25db 100644 --- a/src/Primary_Extension/org/lobobrowser/primary/gui/prefs/GeneralSettingsUI.java +++ b/src/Primary_Extension/org/lobobrowser/primary/gui/prefs/GeneralSettingsUI.java @@ -68,7 +68,7 @@ public GeneralSettingsUI() { "
" + "

The welcome page has important warnings about this release and we would like the user to be aware of them.

" + "
" + - "

We will enable this setting in a future release.

")); + "

We will enable this setting in a future release.

")); this.add(Box.createRigidArea(new Dimension(8, 8))); // this.add(this.getUserAgentGroupBox()); diff --git a/src/Primary_Extension/org/lobobrowser/protocol/data/DataURLConnection.java b/src/Primary_Extension/org/lobobrowser/protocol/data/DataURLConnection.java index 9e1dca7f..36c881a4 100644 --- a/src/Primary_Extension/org/lobobrowser/protocol/data/DataURLConnection.java +++ b/src/Primary_Extension/org/lobobrowser/protocol/data/DataURLConnection.java @@ -52,6 +52,7 @@ private void loadHeaderMap() { } boolean base64 = false; final String[] split = mediatype.split("[;]"); + // TODO: In some cases the next line throws ArrayOutOfBoundsException. Need to investigate. if (split[0].equals("")) { split[0] = "text/plain"; } diff --git a/src/XAMJ_Build/ext/js.jar b/src/XAMJ_Build/ext/js.jar index 6f0dafbb..2bbfd440 100644 Binary files a/src/XAMJ_Build/ext/js.jar and b/src/XAMJ_Build/ext/js.jar differ