From 677e04a03f7d631e4ea283a83f41101e4cc90db7 Mon Sep 17 00:00:00 2001 From: hrj Date: Sat, 28 Feb 2015 16:56:29 +0530 Subject: [PATCH] Squashed commit of jsUgly2, misc2 and optim branches --- src/Common/org/lobobrowser/util/Urls.java | 6 +- .../org/lobobrowser/html/BrowserFrame.java | 6 + .../domimpl/DescendentHTMLCollection.java | 27 + .../lobobrowser/html/domimpl/ElementImpl.java | 18 +- .../html/domimpl/HTMLAbstractUIElement.java | 9 +- .../html/domimpl/HTMLBaseInputElement.java | 18 +- .../html/domimpl/HTMLButtonElementImpl.java | 6 + .../html/domimpl/HTMLDocumentImpl.java | 229 ++++++- .../html/domimpl/HTMLElementImpl.java | 34 +- .../html/domimpl/HTMLFormElementImpl.java | 21 +- .../html/domimpl/HTMLIFrameElementImpl.java | 119 +++- .../html/domimpl/HTMLImageElementImpl.java | 24 +- .../html/domimpl/HTMLLinkElementImpl.java | 159 ++++- .../html/domimpl/HTMLScriptElementImpl.java | 52 +- .../html/domimpl/ImageListener.java | 2 + .../lobobrowser/html/domimpl/NodeImpl.java | 307 ++++++++-- .../html/domimpl/NodeListImpl.java | 44 +- .../lobobrowser/html/gui/HtmlBlockPanel.java | 5 +- .../org/lobobrowser/html/js/Event.java | 92 ++- .../html/js/EventTargetManager.java | 245 ++++++++ .../org/lobobrowser/html/js/Executor.java | 73 ++- .../org/lobobrowser/html/js/Window.java | 566 ++++++++++++++++-- .../lobobrowser/html/js/XMLHttpRequest.java | 72 ++- .../html/renderer/HtmlController.java | 140 ++++- .../lobobrowser/html/renderer/ImgControl.java | 4 + .../html/renderer/InputImageControl.java | 4 + .../html/renderer/InputTextAreaControl.java | 32 +- .../html/renderer/InputTextControl.java | 18 + .../org/lobobrowser/html/renderer/RBlock.java | 6 +- .../html/renderer/RBlockViewport.java | 35 +- .../html/style/ComputedJStyleProperties.java | 3 +- .../html/style/JStyleProperties.java | 3 +- .../html/style/LocalJStyleProperties.java | 3 +- .../org/lobobrowser/js/JavaClassWrapper.java | 34 ++ .../lobobrowser/js/JavaConstructorObject.java | 12 +- .../lobobrowser/js/JavaFunctionObject.java | 15 + .../org/lobobrowser/js/JavaObjectWrapper.java | 112 +++- .../org/lobobrowser/js/JavaScript.java | 24 +- .../context/NetworkRequestImpl.java | 137 +++-- .../gui/AbstractBrowserWindow.java | 5 - .../org/lobobrowser/gui/FramePanel.java | 11 +- .../lobobrowser/gui/NavigatorWindowImpl.java | 3 +- .../org/lobobrowser/main/Extension.java | 4 +- .../request/FileWithHeadersURLConnection.java | 10 + .../lobobrowser/request/RequestEngine.java | 18 +- .../request/SilentUserAgentContextImpl.java | 3 +- .../request/UserAgentContextImpl.java | 3 +- .../security/LocalSecurityPolicy.java | 31 +- .../lobobrowser/security/RequestManager.java | 5 +- .../org/lobobrowser/ua/NetworkRequest.java | 4 + .../org/lobobrowser/ua/TargetType.java | 4 +- .../clientlets/html/BrowserFrameImpl.java | 6 + .../html/HtmlRendererContextImpl.java | 31 +- .../primary/ext/ComponentSource.java | 10 +- .../primary/ext/ExtensionImpl.java | 2 + 55 files changed, 2503 insertions(+), 363 deletions(-) create mode 100644 src/HTML_Renderer/org/lobobrowser/html/js/EventTargetManager.java diff --git a/src/Common/org/lobobrowser/util/Urls.java b/src/Common/org/lobobrowser/util/Urls.java index 4612f016..deda7ce7 100644 --- a/src/Common/org/lobobrowser/util/Urls.java +++ b/src/Common/org/lobobrowser/util/Urls.java @@ -133,7 +133,11 @@ public static Long getExpiration(final URLConnection connection, final long base } } } - return null; + + // TODO: For issue #99 + // When there is no cache setting; assume a 60 second cache expiry time for now. + // return null; + return baseTime + (60 * 1000); } public static List getHeaders(final URLConnection connection) { 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/ElementImpl.java b/src/HTML_Renderer/org/lobobrowser/html/domimpl/ElementImpl.java index 3a2d89f5..6a5d7857 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<>(); @@ -213,10 +214,11 @@ public String getTagName() { 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 { 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 5c251b85..e130a2cd 100644 --- a/src/HTML_Renderer/org/lobobrowser/html/domimpl/HTMLElementImpl.java +++ b/src/HTML_Renderer/org/lobobrowser/html/domimpl/HTMLElementImpl.java @@ -52,6 +52,10 @@ 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; @@ -66,7 +70,7 @@ 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); @@ -95,12 +99,13 @@ protected final void forgetLocalStyle() { 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; @@ -295,6 +300,9 @@ protected void assignAttributeField(final String normalName, final String value) } else { if ("style".equals(normalName)) { this.forgetLocalStyle(); + // informDocumentInvalid(); + // informLayoutInvalid(); + // invalidateDescendentsForHover(); } } super.assignAttributeField(normalName, value); @@ -317,6 +325,10 @@ public void setMouseOver(final boolean mouseOver) { } // Change isMouseOver field before checking to invalidate. this.isMouseOver = mouseOver; + + // TODO: descendant invalidation can be moved to the else part of this.hasHoverStyle() ? + // assuming that if this is invalidated, descendants are also invalidated. Need to verify the assumption. + // Check if descendents are affected (e.g. div:hover a { ... } ) this.invalidateDescendentsForHover(); if (this.hasHoverStyle()) { @@ -837,4 +849,22 @@ public boolean toggle(final String token, final boolean 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..016b3cb6 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; @@ -20,9 +26,38 @@ public HTMLIFrameElementImpl(final String name) { public void setBrowserFrame(final BrowserFrame 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 +177,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 +212,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 +246,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..4b191f31 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; @@ -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/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..0a2fd158 100644 --- a/src/HTML_Renderer/org/lobobrowser/html/gui/HtmlBlockPanel.java +++ b/src/HTML_Renderer/org/lobobrowser/html/gui/HtmlBlockPanel.java @@ -588,9 +588,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..86bd7faa 100644 --- a/src/HTML_Renderer/org/lobobrowser/html/js/Event.java +++ b/src/HTML_Renderer/org/lobobrowser/html/js/Event.java @@ -25,19 +25,23 @@ import java.awt.event.MouseEvent; import org.lobobrowser.js.AbstractScriptableDelegate; +import org.lobobrowser.js.HideFromJS; +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 +49,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 +79,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 +92,7 @@ public boolean isCancelBubble() { } public void setCancelBubble(final boolean cancelBubble) { + System.out.println("Event.setCancelBubble()"); this.cancelBubble = cancelBubble; } @@ -146,7 +153,7 @@ public void setReturnValue(final boolean returnValue) { this.returnValue = returnValue; } - public HTMLElement getSrcElement() { + public Node getSrcElement() { return srcElement; } @@ -185,4 +192,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..be8f0ffa 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,17 +81,19 @@ 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()); + 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); @@ -87,23 +103,31 @@ public static boolean executeFunction(final NodeImpl element, final Object thisO } 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) { + final Context ctx = createContext(codeSource, ucontext, contextFactory); try { try { final Object result = f.call(ctx, thisScope, thisScope, new Object[0]); @@ -112,7 +136,7 @@ public static boolean executeFunction(final Scriptable thisScope, final Function } 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 +144,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/Window.java b/src/HTML_Renderer/org/lobobrowser/html/js/Window.java index 462a5732..1e9f9029 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; @@ -43,14 +53,19 @@ import org.lobobrowser.html.domimpl.HTMLScriptElementImpl; import org.lobobrowser.html.domimpl.HTMLSelectElementImpl; import org.lobobrowser.js.AbstractScriptableDelegate; +import org.lobobrowser.js.HideFromJS; import org.lobobrowser.js.JavaClassWrapper; import org.lobobrowser.js.JavaClassWrapperFactory; import org.lobobrowser.js.JavaInstantiator; 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 +74,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 +89,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 +127,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 +151,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; + } + + // 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); - // Set up Javascript scope - document.setUserData(Executor.SCOPE_KEY, getWindowScope(), null); + jsScheduler.start(); - this.document = document; + this.document = document; + // eventTargetManager.setNode(document); + } } } @@ -164,6 +195,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 +490,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 +504,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 +558,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 +591,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 +622,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,6 +681,55 @@ 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); + + return cx; + } + + @Override + protected void observeInstructionCount(final Context cx, final int instructionCount) { + System.out.println("Context: " + cx + " Instruction count: " + instructionCount); + } + + } + + // TODO: make private + public MyContextFactory windowFactory = new MyContextFactory(); + private void initWindowScope(final Document doc) { // Special Javascript class: XMLHttpRequest final Scriptable ws = this.getWindowScope(); @@ -390,30 +745,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 +920,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 +938,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 +957,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 +1030,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 +1164,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 +1228,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 +1249,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 +1292,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 +1313,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 +1343,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..dd670945 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,21 +186,58 @@ 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); + } } } 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 a3100daf..4a4d28b5 100644 --- a/src/HTML_Renderer/org/lobobrowser/html/renderer/ImgControl.java +++ b/src/HTML_Renderer/org/lobobrowser/html/renderer/ImgControl.java @@ -220,6 +220,10 @@ public void imageLoaded(final ImageEvent event) { } } + 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..4497e27b 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,6 +37,22 @@ 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) { 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 410dd861..beed7f72 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 727f4116..d6b8f125 100644 --- a/src/HTML_Renderer/org/lobobrowser/html/renderer/RBlockViewport.java +++ b/src/HTML_Renderer/org/lobobrowser/html/renderer/RBlockViewport.java @@ -685,7 +685,11 @@ private final void positionRElement(final HTMLElementImpl markupElement, final R } private final void layoutRBlock(final HTMLElementImpl markupElement) { - RBlock renderable = (RBlock) markupElement.getUINode(); + UINode uiNode = markupElement.getUINode(); + RBlock renderable = null; + if (uiNode instanceof RBlock) { + renderable = (RBlock) markupElement.getUINode(); + } if (renderable == null) { renderable = new RBlock(markupElement, this.listNesting, this.userAgentContext, this.rendererContext, this.frameContext, this.container); @@ -2177,7 +2181,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)) { @@ -2245,10 +2250,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. @@ -2257,16 +2262,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; @@ -2286,7 +2299,15 @@ public boolean isContainedByNode() { } private void layoutRInlineBlock(final HTMLElementImpl markupElement) { - final RInlineBlock inlineBlock = new RInlineBlock(container, markupElement, userAgentContext, rendererContext, frameContext); + final UINode uINode = markupElement.getUINode(); + RInlineBlock inlineBlock = null; + if (uINode instanceof RInlineBlock) { + inlineBlock = (RInlineBlock) uINode; + } else { + final RInlineBlock newInlineBlock = new RInlineBlock(container, markupElement, userAgentContext, rendererContext, frameContext); + markupElement.setUINode(newInlineBlock); + inlineBlock = newInlineBlock; + } inlineBlock.doLayout(availContentWidth, availContentHeight, sizeOnly); addRenderableToLine(inlineBlock); } 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/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/js/JavaClassWrapper.java b/src/HTML_Renderer/org/lobobrowser/js/JavaClassWrapper.java index 4c228013..5787b329 100644 --- a/src/HTML_Renderer/org/lobobrowser/js/JavaClassWrapper.java +++ b/src/HTML_Renderer/org/lobobrowser/js/JavaClassWrapper.java @@ -20,18 +20,24 @@ */ 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.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 +57,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,6 +70,14 @@ 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++) { @@ -197,4 +215,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 810aeb53..0138d966 100644 --- a/src/HTML_Renderer/org/lobobrowser/js/JavaFunctionObject.java +++ b/src/HTML_Renderer/org/lobobrowser/js/JavaFunctionObject.java @@ -29,6 +29,7 @@ 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; @@ -47,6 +48,20 @@ public JavaFunctionObject(final String name, final String className) { super(); 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) { + 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) { diff --git a/src/HTML_Renderer/org/lobobrowser/js/JavaObjectWrapper.java b/src/HTML_Renderer/org/lobobrowser/js/JavaObjectWrapper.java index cb69d317..a2d63e15 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,68 @@ 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) -> { + // 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) -> { + try { + defineProperty(name, field.get(null), READONLY); + } catch (final Exception e) { + throw new RuntimeException(e); + } + }); } /** @@ -81,6 +154,7 @@ public String getClassName() { return this.classWrapper.getClassName(); } + /* @Override public Object get(final int index, final Scriptable start) { final PropertyInfo pinfo = this.classWrapper.getIntegerIndexer(); @@ -107,7 +181,7 @@ public Object get(final int index, final Scriptable start) { throw new WrappedException(err); } } - } + }*/ @Override public Object get(final String name, final Scriptable start) { @@ -117,17 +191,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 +367,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/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 6a0c5892..08d021f3 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. @@ -715,6 +716,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; @@ -1223,13 +1229,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 2534e19a..505c1e0d 100644 --- a/src/Platform_Core/org/lobobrowser/gui/NavigatorWindowImpl.java +++ b/src/Platform_Core/org/lobobrowser/gui/NavigatorWindowImpl.java @@ -382,7 +382,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..3ec2fb22 100644 --- a/src/Platform_Core/org/lobobrowser/main/Extension.java +++ b/src/Platform_Core/org/lobobrowser/main/Extension.java @@ -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/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 3d9ad16c..c8cb9bf0 100644 --- a/src/Platform_Core/org/lobobrowser/request/RequestEngine.java +++ b/src/Platform_Core/org/lobobrowser/request/RequestEngine.java @@ -129,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); + } } } @@ -776,7 +779,7 @@ private void processHandler(final RequestHandler rhandler, final int recursionLe 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) { @@ -905,7 +908,8 @@ 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(); - final URI uri = url.toURI(); + 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); 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/security/LocalSecurityPolicy.java b/src/Platform_Core/org/lobobrowser/security/LocalSecurityPolicy.java index 1c8c783f..c79f7c50 100644 --- a/src/Platform_Core/org/lobobrowser/security/LocalSecurityPolicy.java +++ b/src/Platform_Core/org/lobobrowser/security/LocalSecurityPolicy.java @@ -34,12 +34,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; @@ -147,6 +149,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); @@ -296,6 +305,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) * @@ -304,14 +321,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 @@ -341,6 +361,7 @@ public PermissionCollection getPermissions(final CodeSource codesource) { final boolean isLocal = isLocal(location); final Permissions permissions = new Permissions(); + if (isLocal) { final String path = location.toExternalForm(); @@ -444,7 +465,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..87cadf5d 100644 --- a/src/Platform_Core/org/lobobrowser/security/RequestManager.java +++ b/src/Platform_Core/org/lobobrowser/security/RequestManager.java @@ -80,7 +80,10 @@ private Optional getFrameNavigationEntry() { } 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() { 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..1abfa146 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 named frame. */ + 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 22357039..683224ad 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() { @@ -553,6 +575,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 f6fdfa1b..3bee7400 100644 --- a/src/Primary_Extension/org/lobobrowser/primary/ext/ComponentSource.java +++ b/src/Primary_Extension/org/lobobrowser/primary/ext/ComponentSource.java @@ -363,10 +363,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) {