/* GNU LESSER GENERAL PUBLIC LICENSE Copyright (C) 2006 The Lobo Project This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA Contact info: lobochief@users.sourceforge.net */ /* * Created on Nov 12, 2005 */ package org.lobobrowser.html.js; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.lang.ref.WeakReference; import java.net.MalformedURLException; 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.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; 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.CanvasPath2D; 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; import org.lobobrowser.html.domimpl.HTMLImageElementImpl; import org.lobobrowser.html.domimpl.HTMLOptionElementImpl; import org.lobobrowser.html.domimpl.HTMLScriptElementImpl; import org.lobobrowser.html.domimpl.HTMLSelectElementImpl; import org.lobobrowser.html.domimpl.NodeImpl; import org.lobobrowser.html.domimpl.TextImpl; 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.ScriptRuntime; import org.mozilla.javascript.Scriptable; import org.mozilla.javascript.ScriptableObject; import org.w3c.dom.Document; import org.w3c.dom.Element; 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, EventTarget { private static final Storage STORAGE = new Storage(); private static final Logger logger = Logger.getLogger(Window.class.getName()); private static final Map<HtmlRendererContext, WeakReference<Window>> CONTEXT_WINDOWS = new WeakHashMap<>(); // private static final JavaClassWrapper IMAGE_WRAPPER = // JavaClassWrapperFactory.getInstance().getClassWrapper(Image.class); private static final JavaClassWrapper XMLHTTPREQUEST_WRAPPER = JavaClassWrapperFactory.getInstance() .getClassWrapper(XMLHttpRequest.class); private static final JavaClassWrapper PATH2D_WRAPPER = JavaClassWrapperFactory.getInstance() .getClassWrapper(CanvasPath2D.class); private static final JavaClassWrapper EVENT_WRAPPER = JavaClassWrapperFactory.getInstance() .getClassWrapper(Event.class); // 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; private Navigator navigator; private Screen screen; private Location location; private Map<Integer, TaskWrapper> taskMap; private volatile Document document; // private volatile HTMLDocumentImpl document; public Window(final HtmlRendererContext rcontext, final UserAgentContext uaContext) { // TODO: Probably need to create a new Window instance // for every document. Sharing of Window state between // different documents is not correct. this.rcontext = rcontext; this.uaContext = uaContext; } private static int generateTimerID() { synchronized (logger) { return timerIdCounter++; } } public HtmlRendererContext getHtmlRendererContext() { return this.rcontext; } public UserAgentContext getUserAgentContext() { return this.uaContext; } private void clearState() { synchronized (this) { // windowClosing = true; if (document instanceof HTMLDocumentImpl) { ((HTMLDocumentImpl) document).stopEverything(); } jsScheduler.stopAndWindUp(true); 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(); final Scriptable s = this.windowScope; if (s != null) { final Object[] ids = s.getIds(); for (final Object id : ids) { if (id instanceof String) { s.delete((String) id); } else if (id instanceof Integer) { s.delete(((Integer) id).intValue()); } } } // This will ensure that a fresh scope will be created by getWindowScope() on the next call this.windowScope = null; } } @HideFromJS public void setDocument(final Document 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, windowContextFactory); 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); jobsOver.set(false); jsScheduler.start(); this.document = document; // eventTargetManager.setNode(document); } } } public DocumentView getDocument() { return (DocumentView) this.document; } public Document getDocumentNode() { return this.document; } private abstract static class JSTask implements Comparable<JSTask> { 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<T> extends JSTask { private final Supplier<T> supplier; private final Consumer<T> consumer; public JSSupplierTask(final int priority, final Supplier<T> supplier, final Consumer<T> 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 int JS_SCHED_POLL_INTERVAL_MILLIS = 100; private static final int JS_SCHED_JOIN_INTERVAL_MILLIS = JS_SCHED_POLL_INTERVAL_MILLIS * 2; private static final class JSScheduler extends Thread { private static final class ScheduledTask implements Comparable<ScheduledTask> { 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<ScheduledTask> jsQueue = new PriorityBlockingQueue<>(); private final AtomicBoolean running = new AtomicBoolean(false); 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); private String name; public JSScheduler(final Window window) { super("JS Scheduler"); this.name = "JS Sched " + (window.document == null ? "" : "" + window.document.getBaseURI()); } @Override public void run() { setName(name); while (!windowClosing) { try { ScheduledTask scheduledTask; // TODO: uncomment if synchronization is necessary with the add methods // synchronized (this) { scheduledTask = jsQueue.poll(JS_SCHED_POLL_INTERVAL_MILLIS, TimeUnit.MILLISECONDS); if (scheduledTask != null) { final PrivilegedAction<Object> action = new PrivilegedAction<Object>() { public Object run() { // System.out.println("In " + window.document.getBaseURI() + "\n Running task: " + scheduledTask); // System.out.println("In " + name + "\n Running task: " + scheduledTask); running.set(true); 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(); } catch (final WindowClosingError wce) { // Javascript context detected a request for closing and bailed out. assert(windowClosing); } finally { running.set(false); } } // System.out.println("Exiting loop\n\n"); } public void stopAndWindUp(final boolean blocking) { // System.out.println("Going to stop JS scheduler"); windowClosing = true; /* TODO: If the thread refuses to join(), perhaps the thread could be interrupted and stopped in * the catch block of join() below. This could be done immediately, or scheduled for a stopping * in a separate collector Thread * */ // this.interrupt(); if (blocking) { try { this.join(JS_SCHED_JOIN_INTERVAL_MILLIS); } catch (final InterruptedException e) { e.printStackTrace(); } } /* this.stop(); */ } public boolean isWindowClosing() { return windowClosing; } 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; // } } public boolean hasPendingTasks() { return (!jsQueue.isEmpty()) || running.get(); } } private URL getCurrURL() { try { return new URL(rcontext.getCurrentURL()); } catch (MalformedURLException e) { return null; } } private volatile JSScheduler jsScheduler = new JSScheduler(this); @HideFromJS public void addJSTask(final JSTask task) { /* final URL urlContext = new URL(rcontext.getCurrentURL()); if (document != null) { final URL urlDoc = document.getDocumentURL(); if (!urlDoc.equals(urlContext)) { throw new RuntimeException(String.format("doc url(%s) is different from context url (%s)", urlDoc, urlContext)); } }*/ final URL urlContext = getCurrURL(); if (urlContext != null) { if (uaContext.isRequestPermitted(new Request(urlContext, RequestKind.JavaScript))) { // System.out.println("Adding task: " + task); synchronized (this) { jsScheduler.addJSTask(task); } } } else { // TODO: This happens when the URL is not accepted by okhttp System.out.println("Not adding task because url context is null"); } } // TODO: Also look at GH #149 // 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) { Map<Integer, TaskWrapper> taskMap = this.taskMap; if (taskMap == null) { taskMap = new HashMap<>(4); this.taskMap = taskMap; } else { oldTaskWrapper = taskMap.get(timeoutID); } taskMap.put(timeoutID, new TaskWrapper(timer, retained)); } // Do this outside synchronized block, just in case. if (oldTaskWrapper != null) { oldTaskWrapper.timer.stop(); } timer.start(); } private void forgetTask(final Integer timeoutID, final boolean cancel) { TaskWrapper oldTimer = null; synchronized (this) { final Map<Integer, TaskWrapper> taskMap = this.taskMap; if (taskMap != null) { oldTimer = taskMap.remove(timeoutID); } } if ((oldTimer != null) && cancel) { oldTimer.timer.stop(); } } private void forgetAllTasks() { TaskWrapper[] oldTaskWrappers = null; synchronized (this) { final Map<Integer, TaskWrapper> taskMap = this.taskMap; if (taskMap != null) { oldTaskWrappers = taskMap.values().toArray(new TaskWrapper[0]); this.taskMap = null; } } if (oldTaskWrappers != null) { for (final TaskWrapper taskWrapper : oldTaskWrappers) { taskWrapper.timer.stop(); } } } // private Timer getTask(Long timeoutID) { // synchronized(this) { // Map taskMap = this.taskMap; // if(taskMap != null) { // return (Timer) taskMap.get(timeoutID); // } // } // return null; // } /** * @param aFunction * Javascript function to invoke on each loop. * @param aTimeInMs * Time in millisecund between each loop. TODO: Can this be converted * to long type? * @return Return the timer ID to use as reference * @see <a * href="http://developer.mozilla.org/en/docs/DOM:window.setInterval">Window.setInterval * interface definition</a> * @todo Make proper and refactore with * {@link Window#setTimeout(Function, double)}. */ public int setInterval(final Function aFunction, final double aTimeInMs) { if ((aTimeInMs > Integer.MAX_VALUE) || (aTimeInMs < 0)) { 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; if (t < 1) { t = 1; } final Timer timer = new Timer(t, task); timer.setRepeats(true); // The only difference with setTimeout this.putAndStartTask(timeIDInt, timer, aFunction); return timeID; } /** * @param aExpression * Javascript expression to invoke on each loop. * @param aTimeInMs * Time in millisecund between each loop. * @return Return the timer ID to use as reference * @see <a * href="http://developer.mozilla.org/en/docs/DOM:window.setInterval">Window.setInterval * interface definition</a> * @todo Make proper and refactore with * {@link Window#setTimeout(String, double)}. */ public int setInterval(final String aExpression, final double aTimeInMs) { if ((aTimeInMs > Integer.MAX_VALUE) || (aTimeInMs < 0)) { throw new IllegalArgumentException("Timeout value " + aTimeInMs + " is not supported."); } final int timeID = generateTimerID(); final Integer timeIDInt = new Integer(timeID); final ActionListener task = new ExpressionTimerTask(this, timeIDInt, aExpression, false); int t = (int) aTimeInMs; if (t < 1) { t = 1; } final Timer timer = new Timer(t, task); timer.setRepeats(false); // The only difference with setTimeout this.putAndStartTask(timeIDInt, timer, null); return timeID; } /** * @param aTimerID * Timer ID to stop. * @see <a * href="http://developer.mozilla.org/en/docs/DOM:window.clearInterval">Window.clearInterval * interface Definition</a> */ public void clearInterval(final int aTimerID) { final Integer key = new Integer(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) { final HtmlRendererContext rc = this.rcontext; if (rc != null) { rc.alert(message); } } public void back() { final HtmlRendererContext rc = this.rcontext; if (rc != null) { rc.back(); } } public void blur() { final HtmlRendererContext rc = this.rcontext; if (rc != null) { rc.blur(); } } 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); } public void close() { final HtmlRendererContext rc = this.rcontext; if (rc != null) { rc.close(); } } public boolean confirm(final String message) { final HtmlRendererContext rc = this.rcontext; if (rc != null) { return rc.confirm(message); } else { return false; } } // Making public for link element @HideFromJS 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(getCurrURL(), Window.this.uaContext, windowContextFactory); 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 { final String scriptURI = "window.eval"; return ctx.evaluateString(getWindowScope(), javascript, scriptURI, 1, null); } finally { Context.exit(); } } /* Removing because this eval method interferes with the default eval() method. * The context of the JS eval() call is not preserved by this method. public Object eval(final String javascript) { final HTMLDocumentImpl document = (HTMLDocumentImpl) this.document; if (document == null) { throw new IllegalStateException("Cannot evaluate if document is not set."); } final Context ctx = Executor.createContext(document.getDocumentURL(), this.uaContext); try { final Scriptable scope = this.getWindowScope(); if (scope == null) { throw new IllegalStateException("Scriptable (scope) instance was expected to be keyed as UserData to document using " + Executor.SCOPE_KEY); } final String scriptURI = "window.eval"; if (logger.isLoggable(Level.INFO)) { logger.info("eval(): javascript follows...\r\n" + javascript); } return ctx.evaluateString(scope, javascript, scriptURI, 1, null); } finally { Context.exit(); } } */ public void focus() { final HtmlRendererContext rc = this.rcontext; if (rc != null) { rc.focus(); } } private class MyContextFactory extends ContextFactory { 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); cx.setLanguageVersion(Context.VERSION_1_8); // Make Rhino runtime to call observeInstructionCount each 100_000 bytecode instructions cx.setInstructionObserverThreshold(100_000); // cx.setMaximumInterpreterStackDepth(100); // cx.seal(null); return cx; } @Override protected void observeInstructionCount(final Context cx, final int instructionCount) { final JSScheduler jsSchedulerLocal = jsScheduler; if (jsSchedulerLocal != null) { if (jsSchedulerLocal.isWindowClosing()) { throw new WindowClosingError(); } } } @Override protected boolean hasFeature(Context cx, int featureIndex) { if (featureIndex == Context.FEATURE_V8_EXTENSIONS) { return true; } return super.hasFeature(cx, featureIndex); } } static final class WindowClosingError extends Error { private static final long serialVersionUID = 5375592396498284425L; } private final MyContextFactory windowContextFactory = new MyContextFactory(); @HideFromJS public ContextFactory getContextFactory() { return windowContextFactory; } private void initWindowScope(final Document doc) { // Special Javascript class: XMLHttpRequest final Scriptable ws = this.getWindowScope(); final JavaInstantiator xi = new JavaInstantiator() { public Object newInstance(final Object[] args) { final Document d = doc; if (d == null) { throw new IllegalStateException("Cannot perform operation when document is unset."); } HTMLDocumentImpl hd; try { hd = (HTMLDocumentImpl) d; } catch (final ClassCastException err) { throw new IllegalStateException("Cannot perform operation with documents of type " + d.getClass().getName() + "."); } return new XMLHttpRequest(uaContext, hd.getDocumentURL(), ws, Window.this); } }; defineInstantiator(ws, "XMLHttpRequest", XMLHTTPREQUEST_WRAPPER, xi); final JavaInstantiator pi = new JavaInstantiator() { public Object newInstance(final Object[] args) { return new CanvasPath2D(); } }; defineInstantiator(ws, "Path2D", PATH2D_WRAPPER, pi); final JavaInstantiator ei = new JavaInstantiator() { public Object newInstance(final Object[] args) { if (args.length > 0) { return new Event(args[0].toString(), doc); } throw ScriptRuntime.constructError("TypeError", "An event name must be provided"); } }; defineInstantiator(ws, "Event", EVENT_WRAPPER, ei); // We can use a single shared instance since it is dummy for now ScriptableObject.putProperty(ws, "localStorage", STORAGE); ScriptableObject.putProperty(ws, "sessionStorage", STORAGE); // 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); defineInstantiator(ws, "Text", JavaClassWrapperFactory.getInstance().getClassWrapper(TextImpl.class), new JavaInstantiator() { public Object newInstance(final Object[] args) { final String data = (args != null && args.length > 0 && args[0] != null) ? args[0].toString() : ""; return document.createTextNode(data); } }); } private static void defineInstantiator( final Scriptable ws, final String name, final JavaClassWrapper wrapper, final JavaInstantiator ji) { final Function constructor = JavaObjectWrapper.getConstructor(name, wrapper, ws, ji); ScriptableObject.defineProperty(ws, name, constructor, ScriptableObject.READONLY); } private Scriptable windowScope; @HideFromJS public Scriptable getWindowScope() { synchronized (this) { Scriptable ws = this.windowScope; if (ws != null) { return ws; } // Context.enter() OK in this particular case. // final Context ctx = Context.enter(); final Context ctx = windowContextFactory.enterContext(); try { // Window scope needs to be top-most scope. ws = (Scriptable) JavaScript.getInstance().getJavascriptObject(this, null); ws = ctx.initSafeStandardObjects((ScriptableObject) ws); final Object consoleJSObj = JavaScript.getInstance().getJavascriptObject(new Console(), ws); ScriptableObject.putProperty(ws, "console", consoleJSObj); this.windowScope = ws; return ws; } finally { Context.exit(); } } } static public class Console { public static void log(final Object obj) { System.out.println("> " + obj); } } private final static void defineElementClass(final Scriptable scope, final Document document, final String jsClassName, final String elementName, final Class<?> javaClass) { final JavaInstantiator ji = new JavaInstantiator() { public Object newInstance(final Object[] args) { final Document d = document; if (d == null) { throw new IllegalStateException("Document not set in current context."); } return d.createElement(elementName); } }; final JavaClassWrapper classWrapper = JavaClassWrapperFactory.getInstance().getClassWrapper(javaClass); final Function constructorFunction = JavaObjectWrapper.getConstructor(jsClassName, classWrapper, scope, ji); ScriptableObject.defineProperty(scope, jsClassName, constructorFunction, ScriptableObject.READONLY); } @HideFromJS public static Window getWindow(final HtmlRendererContext rcontext) { if (rcontext == null) { return null; } synchronized (CONTEXT_WINDOWS) { final WeakReference<Window> wref = CONTEXT_WINDOWS.get(rcontext); if (wref != null) { final Window window = wref.get(); if (window != null) { return window; } } final Window window = new Window(rcontext, rcontext.getUserAgentContext()); CONTEXT_WINDOWS.put(rcontext, new WeakReference<>(window)); return window; } } public Window open(final String relativeUrl, final String windowName, final String windowFeatures, final boolean replace) { final HtmlRendererContext rc = this.rcontext; if (rc != null) { java.net.URL url; final Document doc = this.document; try { if (doc instanceof HTMLDocumentImpl) { url = ((HTMLDocumentImpl) doc).getFullURL(relativeUrl); } else { url = new java.net.URL(relativeUrl); } } catch (final java.net.MalformedURLException mfu) { throw new IllegalArgumentException("Malformed URI: " + relativeUrl); } if (replace) { this.document = null; rc.navigate(url, null); return this; } else { final HtmlRendererContext newContext = rc.open(url, windowName, windowFeatures, replace); return getWindow(newContext); } } else { return null; } } public Window open(final String url) { return this.open(url, "window:" + String.valueOf(ID.generateLong())); } public Window open(final String url, final String windowName) { return this.open(url, windowName, "", false); } public Window open(final String url, final String windowName, final String windowFeatures) { return this.open(url, windowName, windowFeatures, false); } public String prompt(final String message) { return this.prompt(message, ""); } public String prompt(final String message, final int inputDefault) { return this.prompt(message, String.valueOf(inputDefault)); } public String prompt(final String message, final String inputDefault) { final HtmlRendererContext rcontext = this.rcontext; if (rcontext != null) { return rcontext.prompt(message, inputDefault); } else { return null; } } public void scrollTo(final int x, final int y) { final HtmlRendererContext rc = this.rcontext; if (rc != null) { rc.scroll(x, y); } } public void scrollBy(final int x, final int y) { final HtmlRendererContext rc = this.rcontext; if (rc != null) { rc.scrollBy(x, y); } } public void resizeTo(final int width, final int height) { final HtmlRendererContext rc = this.rcontext; if (rc != null) { rc.resizeTo(width, height); } } public void resizeBy(final int byWidth, final int byHeight) { final HtmlRendererContext rc = this.rcontext; if (rc != null) { rc.resizeBy(byWidth, 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."); } final int timeID = generateTimerID(); final Integer timeIDInt = new Integer(timeID); final ActionListener task = new ExpressionTimerTask(this, timeIDInt, expr, true); int t = (int) millis; if (t < 1) { t = 1; } final Timer timer = new Timer(t, task); timer.setRepeats(false); this.putAndStartTask(timeIDInt, timer, null); 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; if (t < 1) { t = 1; } final Timer timer = new Timer(t, task); timer.setRepeats(false); this.putAndStartTask(timeIDInt, timer, function); return timeID; } @NotGetterSetter public int setTimeout(final Function function) { return setTimeout(function, 0); } @NotGetterSetter public int setTimeout(final String expr) { return setTimeout(expr, 0); } public boolean isClosed() { final HtmlRendererContext rc = this.rcontext; if (rc != null) { return rc.isClosed(); } else { return false; } } public String getDefaultStatus() { final HtmlRendererContext rc = this.rcontext; if (rc != null) { return rc.getDefaultStatus(); } else { return null; } } public HTMLCollection getFrames() { final Document doc = this.document; if (doc instanceof HTMLDocumentImpl) { return ((HTMLDocumentImpl) doc).getFrames(); } return null; } private int length; private boolean lengthSet = false; /** * Gets the number of frames. */ public int getLength() { if (this.lengthSet) { return this.length; } else { final HTMLCollection frames = this.getFrames(); return frames == null ? 0 : frames.getLength(); } } public void setLength(final int length) { this.lengthSet = true; this.length = length; } public String getName() { final HtmlRendererContext rc = this.rcontext; if (rc != null) { return rc.getName(); } else { return null; } } public void setName(final String newName) { // TODO System.out.println("TODO: window.setName"); } public Window getParent() { final HtmlRendererContext rc = this.rcontext; if (rc != null) { final HtmlRendererContext rcontextParent = rc.getParent(); if (rcontextParent == null) { return this; } else { return Window.getWindow(rcontextParent); } } else { return null; } } public Window getOpener() { final HtmlRendererContext rc = this.rcontext; if (rc != null) { return Window.getWindow(rc.getOpener()); } else { return null; } } public void setOpener(final Window opener) { final HtmlRendererContext rc = this.rcontext; if (rc != null) { if (opener == null) { rc.setOpener(null); } else { rc.setOpener(opener.rcontext); } } } public Window getSelf() { return this; } public String getStatus() { final HtmlRendererContext rc = this.rcontext; if (rc != null) { return rc.getStatus(); } else { return null; } } public void setStatus(final String message) { final HtmlRendererContext rc = this.rcontext; if (rc != null) { rc.setStatus(message); } } public Window getTop() { final HtmlRendererContext rc = this.rcontext; if (rc != null) { return Window.getWindow(rc.getTop()); } else { return null; } } public Window getWindow() { return this; } public Navigator getNavigator() { synchronized (this) { Navigator nav = this.navigator; if (nav == null) { nav = new Navigator(this.uaContext); this.navigator = nav; } return nav; } } public Screen getScreen() { synchronized (this) { Screen nav = this.screen; if (nav == null) { nav = new Screen(); this.screen = nav; } return nav; } } public Location getLocation() { synchronized (this) { Location loc = this.location; if (loc == null) { loc = new Location(this); this.location = loc; } return loc; } } public void setLocation(final String location) { this.getLocation().setHref(location); } private History history; public History getHistory() { synchronized (this) { History hist = this.history; if (hist == null) { hist = new History(this); this.history = hist; } return hist; } } public CSS2Properties getComputedStyle(final HTMLElement element, final String pseudoElement) { if (element instanceof HTMLElementImpl) { return ((HTMLElementImpl) element).getComputedStyle(pseudoElement); } else { throw new java.lang.IllegalArgumentException("Element implementation unknown: " + element); } } public Function getOnload() { final Document doc = this.document; if (doc instanceof HTMLDocumentImpl) { return ((HTMLDocumentImpl) doc).getOnloadHandler(); } else { return null; } } public void setOnload(final Function onload) { // Note that body.onload overrides // window.onload. /* final Document doc = this.document; if (doc instanceof HTMLDocumentImpl) { ((HTMLDocumentImpl) doc).setWindowOnloadHandler(onload); }*/ onWindowLoadHandler = onload; } private Function onunload; public Function getOnunload() { return onunload; } public void setOnunload(final Function onunload) { this.onunload = onunload; } public org.w3c.dom.Node namedItem(final String name) { // Bug 1928758: Element IDs are named objects in context. final Document doc = this.document; if (doc == null) { return null; } final org.w3c.dom.Node node = doc.getElementById(name); if (node != null) { return node; } return null; } private static abstract class WeakWindowTask implements ActionListener { private final WeakReference<Window> windowRef; public WeakWindowTask(final Window window) { this.windowRef = new WeakReference<>(window); } protected Window getWindow() { final WeakReference<Window> ref = this.windowRef; return ref == null ? null : ref.get(); } } private static class FunctionTimerTask extends WeakWindowTask { // Implemented as a static WeakWindowTask to allow the Window // to get garbage collected, especially in infinite loop // scenarios. private final Integer timeIDInt; private final WeakReference<Function> functionRef; private final boolean removeTask; public FunctionTimerTask(final Window window, final Integer timeIDInt, final Function function, final boolean removeTask) { super(window); this.timeIDInt = timeIDInt; this.functionRef = new WeakReference<>(function); this.removeTask = removeTask; } 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(); if (window == null) { if (logger.isLoggable(Level.INFO)) { logger.info("actionPerformed(): Window is no longer available."); } return; } if (this.removeTask) { window.forgetTask(this.timeIDInt, false); } // final HTMLDocumentImpl doc = (HTMLDocumentImpl) window.getDocument(); if (window.getDocument() == null) { throw new IllegalStateException("Cannot perform operation when document is unset."); } final Function function = this.functionRef.get(); if (function == null) { throw new IllegalStateException("Cannot perform operation. Function is no longer available."); } window.addJSTaskUnchecked(new JSRunnableTask(0, "timer task for id: " + timeIDInt + ", oneshot: " + removeTask, () -> { Executor.executeFunction(window.getWindowScope(), function, window.getCurrURL(), window.getUserAgentContext(), window.windowContextFactory); })); // Executor.executeFunction(window.getWindowScope(), function, doc.getDocumentURL(), window.getUserAgentContext(), window.windowFactory); } catch (final Exception err) { logger.log(Level.WARNING, "actionPerformed()", err); } } } private static class ExpressionTimerTask extends WeakWindowTask { // Implemented as a static WeakWindowTask to allow the Window // to get garbage collected, especially in infinite loop // scenarios. private final Integer timeIDInt; private final String expression; private final boolean removeTask; public ExpressionTimerTask(final Window window, final Integer timeIDInt, final String expression, final boolean removeTask) { super(window); this.timeIDInt = timeIDInt; this.expression = expression; this.removeTask = removeTask; } public void actionPerformed(final ActionEvent e) { // This executes in the GUI thread and that's good. try { final Window window = this.getWindow(); if (window == null) { if (logger.isLoggable(Level.INFO)) { logger.info("actionPerformed(): Window is no longer available."); } return; } if (this.removeTask) { window.forgetTask(this.timeIDInt, false); } // final HTMLDocumentImpl doc = (HTMLDocumentImpl) window.getDocument(); if (window.getDocument() == null) { throw new IllegalStateException("Cannot perform operation when document is unset."); } window.addJSTaskUnchecked(new JSRunnableTask(0, "timer task for id: " + timeIDInt, () -> { window.evalInScope(this.expression); })); // window.evalInScope(this.expression); } catch (final Exception err) { logger.log(Level.WARNING, "actionPerformed()", err); } } } private static class TaskWrapper { public final Timer timer; // TODO: The retained object seems to be required to keep timer callback functions from being garbage collected. // The FunctionTimerTask only keeps a weak reference. Need to review this design. @SuppressWarnings("unused") private final Object retained; public TaskWrapper(final Timer timer, final Object retained) { super(); this.timer = timer; this.retained = 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((NodeImpl) document, type, listener); } public void removeEventListener(final String type, final Function listener, final boolean useCapture) { // TODO: Should this delegate completely to document if ("load".equals(type)) { ((HTMLDocumentImpl) document).removeLoadHandler(listener); } eventTargetManager.removeEventListener((NodeImpl) document, type, listener, useCapture); } public boolean dispatchEvent(final Event evt) throws EventException { // TODO System.out.println("TODO: window dispatch event"); eventTargetManager.dispatchEvent((NodeImpl) document, evt); return false; } // TODO: Hide from JS public void domContentLoaded(final Event domContentLoadedEvent) { eventTargetManager.dispatchEvent((NodeImpl) document, domContentLoadedEvent); } private Function onWindowLoadHandler; // private Function windowLoadListeners; // TODO: Move job scheduling logic into Window class private AtomicBoolean jobsOver = new AtomicBoolean(false); @HideFromJS public void jobsFinished() { final Event windowLoadEvent = new Event("load", document); eventTargetManager.dispatchEvent((NodeImpl) document, windowLoadEvent); final Function handler = this.onWindowLoadHandler; if (handler != null) { addJSTask(new JSRunnableTask(0, new Runnable() { public void run() { Executor.executeFunction((NodeImpl) document, handler, windowLoadEvent, windowContextFactory); } })); // Executor.executeFunction(document, handler, windowLoadEvent); } jobsOver.set(true); } @PropertyName("Element") public Class<Element> getElement() { return Element.class; } @PropertyName("Node") public Class<Node> 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((NodeImpl) 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(); } private void shutdown() { // TODO: Add the sync below, when/if the scheduleLock is added // synchronized (scheduleLock) { forgetAllTasks(); if (jsScheduler != null) { AccessController.doPrivileged((PrivilegedAction<Object>) () -> { jsScheduler.stopAndWindUp(false); jsScheduler = null; return null; }); } // } } // TODO: More thorough research and design needs to be done here. GH-127 @Override protected void finalize() throws Throwable { shutdown(); super.finalize(); } @HideFromJS public boolean hasPendingTasks() { return (!jobsOver.get()) || jsScheduler.hasPendingTasks(); } }