Bug 477700. Push a null JSContext around editor init, so it'll work no matter what JS is on the stack. r+sr=smaug
authorBoris Zbarsky <bzbarsky@mit.edu>
Mon, 16 Feb 2009 09:11:41 -0500
changeset 25038 742c2f73fe4478baa487f45fe39a650e7a2a71ba
parent 25037 f39d15c0b15e7c9799b399734a9546e41f2eebc9
child 25039 9537d42f8308cb3df0cd328ec307a44c7a04774d
push idunknown
push userunknown
push dateunknown
bugs477700
milestone1.9.2a1pre
Bug 477700. Push a null JSContext around editor init, so it'll work no matter what JS is on the stack. r+sr=smaug
content/base/public/nsContentUtils.h
content/base/src/nsContentUtils.cpp
layout/forms/nsTextControlFrame.cpp
layout/forms/test/Makefile.in
layout/forms/test/bug287446_subframe.html
layout/forms/test/bug477700_subframe.html
layout/forms/test/test_bug287446.html
layout/forms/test/test_bug477700.html
--- a/content/base/public/nsContentUtils.h
+++ b/content/base/public/nsContentUtils.h
@@ -1471,22 +1471,35 @@ private:
 class NS_STACK_CLASS nsCxPusher
 {
 public:
   nsCxPusher();
   ~nsCxPusher(); // Calls Pop();
 
   // Returns PR_FALSE if something erroneous happened.
   PRBool Push(nsPIDOMEventTarget *aCurrentTarget);
+  // If a null JSContext is passed to Push(), that will cause no
+  // Push() to happen and an error to be returned.
   PRBool Push(JSContext *cx);
+  // Explicitly push a null JSContext on the the stack
+  PRBool PushNull();
+
+  // Pop() will be a no-op if Push() or PushNull() fail
   void Pop();
 
 private:
+  // Combined code for PushNull() and Push(JSContext*)
+  PRBool DoPush(JSContext* cx);
+
   nsCOMPtr<nsIScriptContext> mScx;
   PRBool mScriptIsRunning;
+  PRBool mPushedSomething;
+#ifdef DEBUG
+  JSContext* mPushedContext;
+#endif
 };
 
 class nsAutoGCRoot {
 public:
   // aPtr should be the pointer to the jsval we want to protect
   nsAutoGCRoot(jsval* aPtr, nsresult* aResult) :
     mPtr(aPtr)
   {
--- a/content/base/src/nsContentUtils.cpp
+++ b/content/base/src/nsContentUtils.cpp
@@ -2651,17 +2651,18 @@ nsContentUtils::GetEventArgNames(PRInt32
   } else if (aNameSpaceID == kNameSpaceID_SVG) {
     SET_EVENT_ARG_NAMES(gSVGEventNames);
   } else {
     SET_EVENT_ARG_NAMES(gEventNames);
   }
 }
 
 nsCxPusher::nsCxPusher()
-    : mScriptIsRunning(PR_FALSE)
+    : mScriptIsRunning(PR_FALSE),
+      mPushedSomething(PR_FALSE)
 {
 }
 
 nsCxPusher::~nsCxPusher()
 {
   Pop();
 }
 
@@ -2696,17 +2697,17 @@ IsContextOnStack(nsIJSContextStack *aSta
   }
 
   return PR_FALSE;
 }
 
 PRBool
 nsCxPusher::Push(nsPIDOMEventTarget *aCurrentTarget)
 {
-  if (mScx) {
+  if (mPushedSomething) {
     NS_ERROR("Whaaa! No double pushing with nsCxPusher::Push()!");
 
     return PR_FALSE;
   }
 
   NS_ENSURE_TRUE(aCurrentTarget, PR_FALSE);
   nsCOMPtr<nsIScriptContext> scx;
   nsresult rv = aCurrentTarget->GetContextForEventHandlers(getter_AddRefs(scx));
@@ -2724,68 +2725,100 @@ nsCxPusher::Push(nsPIDOMEventTarget *aCu
   // script context about scripts having been evaluated in such a
   // case, calling with a null cx is fine in that case.
   return Push(cx);
 }
 
 PRBool
 nsCxPusher::Push(JSContext *cx)
 {
-  if (mScx) {
+  if (mPushedSomething) {
     NS_ERROR("Whaaa! No double pushing with nsCxPusher::Push()!");
 
     return PR_FALSE;
   }
 
-  if (cx) {
-    mScx = GetScriptContextFromJSContext(cx);
-    if (!mScx) {
-      // Should probably return PR_FALSE. See bug 416916.
-      return PR_TRUE;
-    }
-
-    nsIThreadJSContextStack* stack = nsContentUtils::ThreadJSContextStack();
-    if (stack) {
-      if (IsContextOnStack(stack, cx)) {
-        // If the context is on the stack, that means that a script
-        // is running at the moment in the context.
-        mScriptIsRunning = PR_TRUE;
-      }
-
-      stack->Push(cx);
-    }
-  }
+  if (!cx) {
+    return PR_FALSE;
+  }
+
+  // Hold a strong ref to the nsIScriptContext, just in case
+  // XXXbz do we really need to?  If we don't get one of these in Pop(), is
+  // that really a problem?  Or do we need to do this to effectively root |cx|?
+  mScx = GetScriptContextFromJSContext(cx);
+  if (!mScx) {
+    // Should probably return PR_FALSE. See bug 416916.
+    return PR_TRUE;
+  }
+
+  return DoPush(cx);
+}
+
+PRBool
+nsCxPusher::DoPush(JSContext* cx)
+{
+  nsIThreadJSContextStack* stack = nsContentUtils::ThreadJSContextStack();
+  if (!stack) {
+    return PR_TRUE;
+  }
+
+  if (cx && IsContextOnStack(stack, cx)) {
+    // If the context is on the stack, that means that a script
+    // is running at the moment in the context.
+    mScriptIsRunning = PR_TRUE;
+  }
+
+  if (NS_FAILED(stack->Push(cx))) {
+    mScriptIsRunning = PR_FALSE;
+    mScx = nsnull;
+    return PR_FALSE;
+  }
+
+  mPushedSomething = PR_TRUE;
+#ifdef DEBUG
+  mPushedContext = cx;
+#endif
   return PR_TRUE;
 }
 
+PRBool
+nsCxPusher::PushNull()
+{
+  return DoPush(nsnull);
+}
+
 void
 nsCxPusher::Pop()
 {
   nsIThreadJSContextStack* stack = nsContentUtils::ThreadJSContextStack();
-  if (!mScx || !stack) {
+  if (!mPushedSomething || !stack) {
     mScx = nsnull;
+    mPushedSomething = PR_FALSE;
 
     NS_ASSERTION(!mScriptIsRunning, "Huh, this can't be happening, "
                  "mScriptIsRunning can't be set here!");
 
     return;
   }
 
   JSContext *unused;
   stack->Pop(&unused);
 
-  if (!mScriptIsRunning) {
+  NS_ASSERTION(unused == mPushedContext, "Unexpected context popped");
+
+  if (!mScriptIsRunning && mScx) {
     // No JS is running in the context, but executing the event handler might have
     // caused some JS to run. Tell the script context that it's done.
 
     mScx->ScriptEvaluated(PR_TRUE);
   }
 
   mScx = nsnull;
   mScriptIsRunning = PR_FALSE;
+  mPushedSomething = PR_FALSE;
 }
 
 static const char gPropertiesFiles[nsContentUtils::PropertiesFile_COUNT][56] = {
   // Must line up with the enum values in |PropertiesFile| enum.
   "chrome://global/locale/css.properties",
   "chrome://global/locale/xbl.properties",
   "chrome://global/locale/xul.properties",
   "chrome://global/locale/layout_errors.properties",
--- a/layout/forms/nsTextControlFrame.cpp
+++ b/layout/forms/nsTextControlFrame.cpp
@@ -1381,16 +1381,21 @@ nsTextControlFrame::CalcIntrinsicSize(ns
   }
 
   return NS_OK;
 }
 
 void
 nsTextControlFrame::DelayedEditorInit()
 {
+  // Time to mess with our security context... See comments in GetValue()
+  // for why this is needed.
+  nsCxPusher pusher;
+  pusher.PushNull();
+
   InitEditor();
   // Notify the text listener we have focus and setup the caret etc (bug 446663).
   if (IsFocusedContent(PresContext(), GetContent())) {
     mTextListener->Focus(nsnull);
     SetFocus(PR_TRUE, PR_FALSE);
   }
 }
 
@@ -2564,27 +2569,22 @@ nsTextControlFrame::GetValue(nsAString& 
     // access its own DOM nodes!  Let's try to deal with that by pushing a null
     // JSContext on the JSContext stack to make it clear that we're native
     // code.  Note that any script that's directly trying to access our value
     // has to be going through some scriptable object to do that and that
     // already does the relevant security checks.
     // XXXbz if we could just get the textContent of our anonymous content (eg
     // if plaintext editor didn't create <br> nodes all over), we wouldn't need
     // this.
-    nsCOMPtr<nsIJSContextStack> stack =
-      do_GetService("@mozilla.org/js/xpc/ContextStack;1");
-    PRBool pushed = stack && NS_SUCCEEDED(stack->Push(nsnull));
+    { /* Scope for context pusher */
+      nsCxPusher pusher;
+      pusher.PushNull();
       
-    rv = mEditor->OutputToString(NS_LITERAL_STRING("text/plain"), flags,
-                                 aValue);
-
-    if (pushed) {
-      JSContext* cx;
-      stack->Pop(&cx);
-      NS_ASSERTION(!cx, "Unexpected JSContext popped!");
+      rv = mEditor->OutputToString(NS_LITERAL_STRING("text/plain"), flags,
+                                   aValue);
     }
   }
   else
   {
     // Otherwise get the value from content.
     nsCOMPtr<nsIDOMHTMLInputElement> inputControl = do_QueryInterface(mContent);
     if (inputControl)
     {
@@ -2642,87 +2642,79 @@ nsTextControlFrame::SetValue(const nsASt
       currentValue.Assign(aValue);
       ::PlatformToDOMLineBreaks(currentValue);
 
       nsCOMPtr<nsIDOMDocument>domDoc;
       nsresult rv = editor->GetDocument(getter_AddRefs(domDoc));
       NS_ENSURE_SUCCESS(rv, rv);
       NS_ENSURE_STATE(domDoc);
 
+      PRBool outerTransaction;
       // Time to mess with our security context... See comments in GetValue()
       // for why this is needed.  Note that we have to do this up here, because
       // otherwise SelectAll() will fail.
-      nsCOMPtr<nsIJSContextStack> stack =
-        do_GetService("@mozilla.org/js/xpc/ContextStack;1");
-      PRBool pushed = stack && NS_SUCCEEDED(stack->Push(nsnull));
-
-      nsCOMPtr<nsISelection> domSel;
-      nsCOMPtr<nsISelectionPrivate> selPriv;
-      mSelCon->GetSelection(nsISelectionController::SELECTION_NORMAL, getter_AddRefs(domSel));
-      if (domSel)
-      {
-        selPriv = do_QueryInterface(domSel);
-        if (selPriv)
-          selPriv->StartBatchChanges();
-      }
-
-      nsCOMPtr<nsISelectionController> kungFuDeathGrip = mSelCon;
-      mSelCon->SelectAll();
-      nsCOMPtr<nsIPlaintextEditor> plaintextEditor = do_QueryInterface(editor);
-      if (!plaintextEditor || !weakFrame.IsAlive()) {
-        NS_WARNING("Somehow not a plaintext editor?");
-        if (pushed) {
-          JSContext* cx;
-          stack->Pop(&cx);
-          NS_ASSERTION(!cx, "Unexpected JSContext popped!");
+      { /* Scope for context pusher */
+        nsCxPusher pusher;
+        pusher.PushNull();
+
+        nsCOMPtr<nsISelection> domSel;
+        nsCOMPtr<nsISelectionPrivate> selPriv;
+        mSelCon->GetSelection(nsISelectionController::SELECTION_NORMAL,
+                              getter_AddRefs(domSel));
+        if (domSel)
+        {
+          selPriv = do_QueryInterface(domSel);
+          if (selPriv)
+            selPriv->StartBatchChanges();
+        }
+
+        nsCOMPtr<nsISelectionController> kungFuDeathGrip = mSelCon;
+        mSelCon->SelectAll();
+        nsCOMPtr<nsIPlaintextEditor> plaintextEditor = do_QueryInterface(editor);
+        if (!plaintextEditor || !weakFrame.IsAlive()) {
+          NS_WARNING("Somehow not a plaintext editor?");
+          return NS_ERROR_FAILURE;
         }
-        return NS_ERROR_FAILURE;
-      }
-
-      // Since this code does not handle user-generated changes to the text,
-      // make sure we don't fire oninput when the editor notifies us.
-      // (mNotifyOnInput must be reset before we return).
-
-      // To protect against a reentrant call to SetValue, we check whether
-      // another SetValue is already happening for this frame.  If it is,
-      // we must wait until we unwind to re-enable oninput events.
-      PRBool outerTransaction = mNotifyOnInput;
-      if (outerTransaction)
-        mNotifyOnInput = PR_FALSE;
-
-      // get the flags, remove readonly and disabled, set the value,
-      // restore flags
-      PRUint32 flags, savedFlags;
-      editor->GetFlags(&savedFlags);
-      flags = savedFlags;
-      flags &= ~(nsIPlaintextEditor::eEditorDisabledMask);
-      flags &= ~(nsIPlaintextEditor::eEditorReadonlyMask);
-      editor->SetFlags(flags);
-
-      // Also don't enforce max-length here
-      PRInt32 savedMaxLength;
-      plaintextEditor->GetMaxTextLength(&savedMaxLength);
-      plaintextEditor->SetMaxTextLength(-1);
-
-      if (currentValue.Length() < 1)
-        editor->DeleteSelection(nsIEditor::eNone);
-      else {
-        if (plaintextEditor)
-          plaintextEditor->InsertText(currentValue);
-      }
-
-      plaintextEditor->SetMaxTextLength(savedMaxLength);
-      editor->SetFlags(savedFlags);
-      if (selPriv)
-        selPriv->EndBatchChanges();
-
-      if (pushed) {
-        JSContext* cx;
-        stack->Pop(&cx);
-        NS_ASSERTION(!cx, "Unexpected JSContext popped!");
+
+        // Since this code does not handle user-generated changes to the text,
+        // make sure we don't fire oninput when the editor notifies us.
+        // (mNotifyOnInput must be reset before we return).
+
+        // To protect against a reentrant call to SetValue, we check whether
+        // another SetValue is already happening for this frame.  If it is,
+        // we must wait until we unwind to re-enable oninput events.
+        outerTransaction = mNotifyOnInput;
+        if (outerTransaction)
+          mNotifyOnInput = PR_FALSE;
+
+        // get the flags, remove readonly and disabled, set the value,
+        // restore flags
+        PRUint32 flags, savedFlags;
+        editor->GetFlags(&savedFlags);
+        flags = savedFlags;
+        flags &= ~(nsIPlaintextEditor::eEditorDisabledMask);
+        flags &= ~(nsIPlaintextEditor::eEditorReadonlyMask);
+        editor->SetFlags(flags);
+
+        // Also don't enforce max-length here
+        PRInt32 savedMaxLength;
+        plaintextEditor->GetMaxTextLength(&savedMaxLength);
+        plaintextEditor->SetMaxTextLength(-1);
+
+        if (currentValue.Length() < 1)
+          editor->DeleteSelection(nsIEditor::eNone);
+        else {
+          if (plaintextEditor)
+            plaintextEditor->InsertText(currentValue);
+        }
+
+        plaintextEditor->SetMaxTextLength(savedMaxLength);
+        editor->SetFlags(savedFlags);
+        if (selPriv)
+          selPriv->EndBatchChanges();
       }
 
       NS_ENSURE_STATE(weakFrame.IsAlive());
       if (outerTransaction)
         mNotifyOnInput = PR_TRUE;
 
       if (focusValueInit) {
         // Reset mFocusedValue so the onchange event doesn't fire incorrectly.
--- a/layout/forms/test/Makefile.in
+++ b/layout/forms/test/Makefile.in
@@ -39,19 +39,23 @@ DEPTH		= ../../..
 topsrcdir	= @top_srcdir@
 srcdir		= @srcdir@
 VPATH		= @srcdir@
 relativesrcdir  = layout/forms/test
 
 include $(DEPTH)/config/autoconf.mk
 include $(topsrcdir)/config/rules.mk
 
-_TEST_FILES =	test_bug345267.html \
+_TEST_FILES =	test_bug287446.html \
+		bug287446_subframe.html \
+		test_bug345267.html \
 		test_bug348236.html \
 		test_bug402198.html \
 		test_bug411236.html \
 		test_bug446663.html \
 		test_bug476308.html \
 		test_bug477531.html \
+		test_bug477700.html \
+		bug477700_subframe.html \
 		$(NULL)
 
 libs:: $(_TEST_FILES)
 	$(INSTALL) $^ $(DEPTH)/_tests/testing/mochitest/tests/$(relativesrcdir)
new file mode 100644
--- /dev/null
+++ b/layout/forms/test/bug287446_subframe.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+    <script>
+      function doIs(arg1, arg2, arg3) {
+        window.parent.postMessage("t " + encodeURIComponent(arg1) + " " +
+                                  encodeURIComponent(arg2) + " " +
+                                  encodeURIComponent(arg3), "*");
+      }
+
+      function $(arg) { return document.getElementById(arg); }
+
+      window.addEventListener("message",
+        function(evt) {
+          var t = $("target");
+          if (evt.data == "start") {
+            doIs(t.value, "Test", "Shouldn't have lost our initial value");
+            t.focus();
+            sendString("Foo");
+            doIs(t.value, "TestFoo", "Typing should work");
+            window.parent.postMessage("c", "*");
+          } else {
+            doIs(evt.data, "continue", "Unexpected message");
+            doIs(t.value, "TestFoo", "Shouldn't have lost our typed value");
+            sendString("Bar");
+            doIs(t.value, "TestFooBar", "Typing should still work");
+            window.parent.postMessage("f", "*");
+          }
+        },
+        "false");
+      
+    </script>
+  </head>
+  <body>
+    <input id="target" value="Test">
+  </body>
+</html>
+           
new file mode 100644
--- /dev/null
+++ b/layout/forms/test/bug477700_subframe.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+    <script>
+      function doIs(arg1, arg2, arg3) {
+        window.parent.postMessage("t " + encodeURIComponent(arg1) + " " +
+                                  encodeURIComponent(arg2) + " " +
+                                  encodeURIComponent(arg3), "*");
+      }
+
+      function $(arg) { return document.getElementById(arg); }
+
+      window.addEventListener("message",
+        function(evt) {
+          doIs(evt.data, "start", "Unexpected message");
+          sendString("Test");
+          var t = $("target");
+          doIs(t.value, "Test", "Typing should work");
+          (function() {
+            netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
+            t.QueryInterface(Components.interfaces.nsIDOMNSEditableElement).editor.undo(1);
+          })()
+          doIs(t.value, "", "Undo should work");
+          (function() {
+            netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
+            t.QueryInterface(Components.interfaces.nsIDOMNSEditableElement).editor.redo(1);
+          })()
+          doIs(t.value, "Test", "Redo should work");
+          window.parent.postMessage("f", "*");
+        },
+        "false");
+      
+    </script>
+  </head>
+  <body>
+    <input id="target">
+  </body>
+</html>
+           
new file mode 100644
--- /dev/null
+++ b/layout/forms/test/test_bug287446.html
@@ -0,0 +1,77 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=287446
+-->
+<head>
+  <title>Test for Bug 287446</title>
+  <script type="application/javascript" src="/MochiKit/MochiKit.js"></script>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=287446">Mozilla Bug 287446</a>
+<p id="display">
+  <iframe id="i"
+          src="http://example.com/tests/layout/forms/test/bug287446_subframe.html"></iframe>
+</p>
+<div id="content" style="display: none">
+  
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 287446 **/
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+  isnot(window.location.host, "example.com", "test is not testing cross-site");
+  var accessed = false;
+  try {
+    $("i").contentDocument.documentElement;
+    accessed = true;
+  } catch(e) {}
+  is(accessed, false, "Shouldn't be able to access cross-site");
+
+  $("i").style.display = "none";
+  document.body.offsetWidth;
+  is(document.defaultView.getComputedStyle($("i"), "").display, "none",
+     "toggling display failed");
+  $("i").style.display = "";
+  document.body.offsetWidth;
+  is(document.defaultView.getComputedStyle($("i"), "").display, "inline",
+     "toggling display back failed");
+
+  $("i").contentWindow.postMessage("start", "*");
+});
+
+function continueTest() {
+  dump('aaa');
+  $("i").style.display = "none";
+  document.body.offsetWidth;
+  is(document.defaultView.getComputedStyle($("i"), "").display, "none",
+     "toggling display second time failed");
+  $("i").style.display = "";
+  document.body.offsetWidth;
+  is(document.defaultView.getComputedStyle($("i"), "").display, "inline",
+     "toggling display back second time failed");
+
+$("i").contentWindow.postMessage("continue", "*");
+}
+
+window.addEventListener("message",
+  function(evt) {
+    var arr = evt.data.split(/ /).map(decodeURIComponent);
+    if (arr[0] == 't') {
+      is(arr[1], arr[2], arr[3]);
+    } else if (arr[0] == 'c') {
+      continueTest();
+    } else if (arr[0] == 'f') {
+      SimpleTest.finish();
+    }
+  },
+  false);
+
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/forms/test/test_bug477700.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=477700
+-->
+<head>
+  <title>Test for Bug 477700</title>
+  <script type="application/javascript" src="/MochiKit/MochiKit.js"></script>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=477700">Mozilla Bug 477700</a>
+<p id="display">
+  <iframe id="i"
+          src="http://example.com/tests/layout/forms/test/bug477700_subframe.html"></iframe>
+</p>
+<div id="content" style="display: none">
+  
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 477700 **/
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+  isnot(window.location.host, "example.com", "test is not testing cross-site");
+  var accessed = false;
+  try {
+    $("i").contentDocument.documentElement;
+    accessed = true;
+  } catch(e) {}
+  is(accessed, false, "Shouldn't be able to access cross-site");
+
+  $("i").style.display = "none";
+  document.body.offsetWidth;
+  is(document.defaultView.getComputedStyle($("i"), "").display, "none",
+     "toggling display failed");
+  $("i").style.display = "";
+  document.body.offsetWidth;
+  is(document.defaultView.getComputedStyle($("i"), "").display, "inline",
+     "toggling display back failed");
+
+  $("i").contentWindow.postMessage("start", "*");
+});
+
+window.addEventListener("message",
+  function(evt) {
+    var arr = evt.data.split(/ /).map(decodeURIComponent);
+    if (arr[0] == 't') {
+      is(arr[1], arr[2], arr[3]);
+    } else if (arr[0] == 'f') {
+      SimpleTest.finish();
+    }
+  },
+  false);
+
+</script>
+</pre>
+</body>
+</html>