Bug 612128 - Prevent the editor from modifying nodes which are not under an editing host; r=roc,bzbarsky
authorEhsan Akhgari <ehsan@mozilla.com>
Tue, 16 Nov 2010 15:45:49 -0500
changeset 97673 838fb33405ba7f8804a76528aa956b913068015c
parent 97672 e5e8a0a47dc5228722cc46aa87e02a5100467517
child 97674 d59106f03fa00a1e4597a4a76878a6200b7993ca
push id1116
push userlsblakk@mozilla.com
push dateMon, 16 Jul 2012 19:38:18 +0000
treeherdermozilla-beta@95f959a8b4dc [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersroc, bzbarsky
bugs612128
milestone15.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 612128 - Prevent the editor from modifying nodes which are not under an editing host; r=roc,bzbarsky This patch ensures that the NODE_IS_EDITABLE flag is only set on nodes living under an editing host. Things like text controls which used to have that flag previously will not have it any more. The flag would be set on their anonymous div node instead. Note that if text controls actually fall under an editing host, they will get the NODE_IS_EDITABLE flag. This patch also makes nsHTMLEditor::IsEditable return sane results (text nodes are always considered to be editable).
content/base/src/nsContentAreaDragDrop.cpp
content/html/content/public/nsITextControlElement.h
content/html/content/src/nsGenericHTMLElement.cpp
content/html/content/src/nsGenericHTMLElement.h
content/html/content/src/nsHTMLInputElement.cpp
content/html/content/src/nsHTMLInputElement.h
content/html/content/src/nsHTMLTextAreaElement.cpp
content/html/content/src/nsTextEditorState.cpp
editor/libeditor/base/crashtests/382527-1.html
editor/libeditor/base/nsEditor.h
editor/libeditor/base/nsEditorCommands.cpp
editor/libeditor/html/crashtests/716456-1.html
editor/libeditor/html/nsHTMLDataTransfer.cpp
editor/libeditor/html/nsHTMLEditor.cpp
editor/libeditor/html/nsHTMLEditor.h
editor/libeditor/html/tests/Makefile.in
editor/libeditor/html/tests/test_bug612128.html
editor/reftests/readonly-editable-ref.html
editor/reftests/readonly-editable.html
editor/reftests/readonly-non-editable-ref.html
editor/reftests/readonly-non-editable.html
editor/reftests/readwrite-editable-ref.html
editor/reftests/readwrite-editable.html
editor/reftests/readwrite-non-editable-ref.html
editor/reftests/readwrite-non-editable.html
editor/reftests/reftest.list
layout/base/nsCSSFrameConstructor.cpp
layout/generic/nsIAnonymousContentCreator.h
--- a/content/base/src/nsContentAreaDragDrop.cpp
+++ b/content/base/src/nsContentAreaDragDrop.cpp
@@ -399,17 +399,18 @@ DragDataProducer::Produce(nsDOMDataTrans
   *aSelection = nsnull;
 
   // Find the selection to see what we could be dragging and if what we're
   // dragging is in what is selected. If this is an editable textbox, use
   // the textbox's selection, otherwise use the window's selection.
   nsCOMPtr<nsISelection> selection;
   nsIContent* editingElement = mSelectionTargetNode->IsEditable() ?
                                mSelectionTargetNode->GetEditingHost() : nsnull;
-  nsCOMPtr<nsITextControlElement> textControl(do_QueryInterface(editingElement));
+  nsCOMPtr<nsITextControlElement> textControl =
+    nsITextControlElement::GetTextControlElementFromEditingHost(editingElement);
   if (textControl) {
     nsISelectionController* selcon = textControl->GetSelectionController();
     if (selcon) {
       selcon->GetSelection(nsISelectionController::SELECTION_NORMAL, getter_AddRefs(selection));
     }
 
     if (!selection)
       return NS_OK;
--- a/content/html/content/public/nsITextControlElement.h
+++ b/content/html/content/public/nsITextControlElement.h
@@ -35,16 +35,17 @@
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
 #ifndef nsITextControlElement_h___
 #define nsITextControlElement_h___
 
 #include "nsISupports.h"
+#include "nsCOMPtr.h"
 class nsIContent;
 class nsAString;
 class nsIEditor;
 class nsISelectionController;
 class nsFrameSelection;
 class nsTextControlFrame;
 
 // IID for the nsITextControl interface
@@ -221,15 +222,18 @@ public:
 
   /**
    * Does the editor have a selection cache?
    *
    * Note that this function has the side effect of making the editor for input
    * elements be initialized eagerly.
    */
   NS_IMETHOD_(bool) HasCachedSelection() = 0;
+
+  static already_AddRefed<nsITextControlElement>
+  GetTextControlElementFromEditingHost(nsIContent* aHost);
 };
 
 NS_DEFINE_STATIC_IID_ACCESSOR(nsITextControlElement,
                               NS_ITEXTCONTROLELEMENT_IID)
 
 #endif // nsITextControlElement_h___
 
--- a/content/html/content/src/nsGenericHTMLElement.cpp
+++ b/content/html/content/src/nsGenericHTMLElement.cpp
@@ -1871,47 +1871,16 @@ nsGenericHTMLElement::MapCommonAttribute
     if (display->GetUnit() == eCSSUnit_Null) {
       if (aAttributes->IndexOfAttr(nsGkAtoms::hidden, kNameSpaceID_None) >= 0) {
         display->SetIntValue(NS_STYLE_DISPLAY_NONE, eCSSUnit_Enumerated);
       }
     }
   }
 }
 
-void
-nsGenericHTMLFormElement::UpdateEditableFormControlState(bool aNotify)
-{
-  // nsCSSFrameConstructor::MaybeConstructLazily is based on the logic of this
-  // function, so should be kept in sync with that.
-
-  ContentEditableTristate value = GetContentEditableValue();
-  if (value != eInherit) {
-    DoSetEditableFlag(!!value, aNotify);
-    return;
-  }
-
-  nsIContent *parent = GetParent();
-
-  if (parent && parent->HasFlag(NODE_IS_EDITABLE)) {
-    DoSetEditableFlag(true, aNotify);
-    return;
-  }
-
-  if (!IsTextControl(false)) {
-    DoSetEditableFlag(false, aNotify);
-    return;
-  }
-
-  // If not contentEditable we still need to check the readonly attribute.
-  bool roState;
-  GetBoolAttr(nsGkAtoms::readonly, &roState);
-
-  DoSetEditableFlag(!roState, aNotify);
-}
-
 
 /* static */ const nsGenericHTMLElement::MappedAttributeEntry
 nsGenericHTMLElement::sCommonAttributeMap[] = {
   { &nsGkAtoms::contenteditable },
   { &nsGkAtoms::lang },
   { &nsGkAtoms::hidden },
   { nsnull }
 };
@@ -2969,16 +2938,28 @@ nsGenericHTMLFormElement::IntrinsicState
   
   if (mForm && mForm->IsDefaultSubmitElement(this)) {
       NS_ASSERTION(IsSubmitControl(),
                    "Default submit element that isn't a submit control.");
       // We are the default submit element (:default)
       state |= NS_EVENT_STATE_DEFAULT;
   }
 
+  // Make the text controls read-write
+  if (!state.HasState(NS_EVENT_STATE_MOZ_READWRITE) &&
+      IsTextControl(false)) {
+    bool roState;
+    GetBoolAttr(nsGkAtoms::readonly, &roState);
+
+    if (!roState) {
+      state |= NS_EVENT_STATE_MOZ_READWRITE;
+      state &= ~NS_EVENT_STATE_MOZ_READONLY;
+    }
+  }
+
   return state;
 }
 
 nsGenericHTMLFormElement::FocusTristate
 nsGenericHTMLFormElement::FocusState()
 {
   // We can't be focused if we aren't in a document
   nsIDocument* doc = GetCurrentDoc();
--- a/content/html/content/src/nsGenericHTMLElement.h
+++ b/content/html/content/src/nsGenericHTMLElement.h
@@ -905,18 +905,16 @@ public:
 protected:
   virtual nsresult BeforeSetAttr(PRInt32 aNameSpaceID, nsIAtom* aName,
                                  const nsAttrValueOrString* aValue,
                                  bool aNotify);
 
   virtual nsresult AfterSetAttr(PRInt32 aNameSpaceID, nsIAtom* aName,
                                 const nsAttrValue* aValue, bool aNotify);
 
-  void UpdateEditableFormControlState(bool aNotify);
-
   /**
    * This method will update the form owner, using @form or looking to a parent.
    *
    * @param aBindToTree Whether the element is being attached to the tree.
    * @param aFormIdElement The element associated with the id in @form. If
    * aBindToTree is false, aFormIdElement *must* contain the element associated
    * with the id in @form. Otherwise, it *must* be null.
    *
--- a/content/html/content/src/nsHTMLInputElement.cpp
+++ b/content/html/content/src/nsHTMLInputElement.cpp
@@ -855,21 +855,16 @@ nsHTMLInputElement::AfterSetAttr(PRInt32
     } else if (MaxLengthApplies() && aName == nsGkAtoms::maxlength) {
       UpdateTooLongValidityState();
     } else if (aName == nsGkAtoms::pattern) {
       UpdatePatternMismatchValidityState();
     } else if (aName == nsGkAtoms::multiple) {
       UpdateTypeMismatchValidityState();
     }
 
-    UpdateEditableState(aNotify);
-    nsTextEditorState *state = GetEditorState();
-    if (state) {
-      state->UpdateEditableState(aNotify);
-    }
     UpdateState(aNotify);
   }
 
   return nsGenericHTMLFormElement::AfterSetAttr(aNameSpaceID, aName,
                                                 aValue, aNotify);
 }
 
 // nsIDOMHTMLInputElement
--- a/content/html/content/src/nsHTMLInputElement.h
+++ b/content/html/content/src/nsHTMLInputElement.h
@@ -218,21 +218,16 @@ public:
    * @return the selected button (or null).
    */
   already_AddRefed<nsIDOMHTMLInputElement> GetSelectedRadioButton();
 
   virtual nsresult Clone(nsINodeInfo *aNodeInfo, nsINode **aResult) const;
 
   NS_IMETHOD FireAsyncClickHandler();
 
-  virtual void UpdateEditableState(bool aNotify)
-  {
-    return UpdateEditableFormControlState(aNotify);
-  }
-
   NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(nsHTMLInputElement,
                                            nsGenericHTMLFormElement)
 
   static UploadLastDir* gUploadLastDir;
   // create and destroy the static UploadLastDir object for remembering
   // which directory was last used on a site-by-site basis
   static void InitUploadLastDir();
   static void DestroyUploadLastDir();
--- a/content/html/content/src/nsHTMLTextAreaElement.cpp
+++ b/content/html/content/src/nsHTMLTextAreaElement.cpp
@@ -214,21 +214,16 @@ public:
                                  bool aNotify);
 
   // nsIMutationObserver
   NS_DECL_NSIMUTATIONOBSERVER_CHARACTERDATACHANGED
   NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED
   NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED
   NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED
 
-  virtual void UpdateEditableState(bool aNotify)
-  {
-    return UpdateEditableFormControlState(aNotify);
-  }
-
   NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(nsHTMLTextAreaElement,
                                            nsGenericHTMLFormElement)
 
   virtual nsXPCClassInfo* GetClassInfo();
 
   virtual nsIDOMNode* AsDOMNode() { return this; }
 
   // nsIConstraintValidation
@@ -1308,20 +1303,16 @@ nsHTMLTextAreaElement::AfterSetAttr(PRIn
       // This *has* to be called *after* validity has changed.
       if (aName == nsGkAtoms::readonly || aName == nsGkAtoms::disabled) {
         UpdateBarredFromConstraintValidation();
       }
     } else if (aName == nsGkAtoms::maxlength) {
       UpdateTooLongValidityState();
     }
 
-    if (aName == nsGkAtoms::readonly) {
-      UpdateEditableState(aNotify);
-      mState.UpdateEditableState(aNotify);
-    }
     UpdateState(aNotify);
   }
 
   return nsGenericHTMLFormElement::AfterSetAttr(aNameSpaceID, aName, aValue,
                                                 aNotify);
 }
 
 nsresult
--- a/content/html/content/src/nsTextEditorState.cpp
+++ b/content/html/content/src/nsTextEditorState.cpp
@@ -140,16 +140,30 @@ nsITextControlElement::GetWrapPropertyEn
     }
 
     return true;
   }
 
   return false;
 }
 
+/*static*/
+already_AddRefed<nsITextControlElement>
+nsITextControlElement::GetTextControlElementFromEditingHost(nsIContent* aHost)
+{
+  if (!aHost) {
+    return nsnull;
+  }
+
+  nsCOMPtr<nsITextControlElement> parent =
+    do_QueryInterface(aHost->GetParent());
+
+  return parent.forget();
+}
+
 static bool
 SuppressEventHandlers(nsPresContext* aPresContext)
 {
   bool suppressHandlers = false;
 
   if (aPresContext)
   {
     // Right now we only suppress event handlers and controller manipulation
@@ -1561,16 +1575,19 @@ nsTextEditorState::CreateRootNode()
   }
 
   return rv;
 }
 
 nsresult
 nsTextEditorState::InitializeRootNode()
 {
+  // Make our root node editable
+  mRootNode->SetFlags(NODE_IS_EDITABLE);
+
   // Set the necessary classes on the text control. We use class values
   // instead of a 'style' attribute so that the style comes from a user-agent
   // style sheet and is still applied even if author styles are disabled.
   nsAutoString classValue;
   classValue.AppendLiteral("anonymous-div");
   PRInt32 wrapCols = GetWrapCols();
   if (wrapCols >= 0) {
     classValue.AppendLiteral(" wrap");
--- a/editor/libeditor/base/crashtests/382527-1.html
+++ b/editor/libeditor/base/crashtests/382527-1.html
@@ -31,17 +31,19 @@ function init2()
 
 function init3()
 {
   var rng = targetDocument.createRange(); 
   rng.setStart(textNode, 1);
   rng.setEnd(textNode, 1);
   targetWindow.getSelection().addRange(rng);
 
-  targetDocument.execCommand("inserthtml", false, "<p>");
+  try {
+    targetDocument.execCommand("inserthtml", false, "<p>");
+  } catch(e) {}
   
   document.documentElement.removeAttribute("class");
 }
 
 
 </script>
 
 </head>
--- a/editor/libeditor/base/nsEditor.h
+++ b/editor/libeditor/base/nsEditor.h
@@ -576,17 +576,17 @@ public:
   bool IsDescendantOfEditorRoot(nsIDOMNode* aNode);
   bool IsDescendantOfEditorRoot(nsINode* aNode);
 
   /** returns true if aNode is a container */
   virtual bool IsContainer(nsIDOMNode *aNode);
 
   /** returns true if aNode is an editable node */
   bool IsEditable(nsIDOMNode *aNode);
-  bool IsEditable(nsIContent *aNode);
+  virtual bool IsEditable(nsIContent *aNode);
 
   /**
    * aNode must be a non-null text node.
    */
   virtual bool IsTextInDirtyFrameVisible(nsIContent *aNode);
 
   /** returns true if aNode is a MozEditorBogus node */
   bool IsMozEditorBogusNode(nsIContent *aNode);
--- a/editor/libeditor/base/nsEditorCommands.cpp
+++ b/editor/libeditor/base/nsEditorCommands.cpp
@@ -651,26 +651,28 @@ nsDeleteCommand::GetCommandStateParams(c
 NS_IMETHODIMP
 nsSelectAllCommand::IsCommandEnabled(const char * aCommandName,
                                      nsISupports *aCommandRefCon,
                                      bool *outCmdEnabled)
 {
   NS_ENSURE_ARG_POINTER(outCmdEnabled);
 
   nsresult rv = NS_OK;
-  *outCmdEnabled = false;
+  // You can always select all, unless the selection is editable,
+  // and the editable region is empty!
+  *outCmdEnabled = true;
   bool docIsEmpty;
- 
+
   // you can select all if there is an editor which is non-empty
   nsCOMPtr<nsIEditor> editor = do_QueryInterface(aCommandRefCon);
   if (editor) {
     rv = editor->GetDocumentIsEmpty(&docIsEmpty);
     NS_ENSURE_SUCCESS(rv, rv);
     *outCmdEnabled = !docIsEmpty;
-  } 
+  }
 
   return rv;
 }
 
 
 NS_IMETHODIMP
 nsSelectAllCommand::DoCommand(const char *aCommandName,
                               nsISupports *aCommandRefCon)
--- a/editor/libeditor/html/crashtests/716456-1.html
+++ b/editor/libeditor/html/crashtests/716456-1.html
@@ -9,16 +9,17 @@ function boom()
   div.contentEditable = "true";
   div.focus();
 
   var r = document.documentElement;
   document["removeChild"](r);
   document["appendChild"](r);
 
   setTimeout(function() {
+    getSelection().collapse(div, 0);
     document.execCommand("inserthtml", false, "a");
     setTimeout(function() {
       document.documentElement.removeAttribute("class");
     }, 0);
   }, 0);
 }
 
 </script>
--- a/editor/libeditor/html/nsHTMLDataTransfer.cpp
+++ b/editor/libeditor/html/nsHTMLDataTransfer.cpp
@@ -309,17 +309,19 @@ nsHTMLEditor::DoInsertHTMLWithContext(co
   PRInt32 targetOffset=0;
 
   if (!aDestNode)
   {
     // if caller didn't provide the destination/target node,
     // fetch the paste insertion point from our selection
     rv = GetStartNodeAndOffset(selection, getter_AddRefs(targetNode), &targetOffset);
     NS_ENSURE_SUCCESS(rv, rv);
-    NS_ENSURE_TRUE(targetNode, NS_ERROR_FAILURE);
+    if (!targetNode || !IsEditable(targetNode)) {
+      return NS_ERROR_FAILURE;
+    }
   }
   else
   {
     targetNode = aDestNode;
     targetOffset = aDestOffset;
   }
 
   bool doContinue = true;
--- a/editor/libeditor/html/nsHTMLEditor.cpp
+++ b/editor/libeditor/html/nsHTMLEditor.cpp
@@ -412,16 +412,22 @@ nsHTMLEditor::FindSelectionRoot(nsINode 
   // permission script.
   if (IsReadonly()) {
     // We still want to allow selection in a readonly editor.
     content = do_QueryInterface(GetRoot());
     return content.forget();
   }
 
   if (!content->HasFlag(NODE_IS_EDITABLE)) {
+    // If the content is in read-write state but is not editable itself,
+    // return it as the selection root.
+    if (content->IsElement() &&
+        content->AsElement()->State().HasState(NS_EVENT_STATE_MOZ_READWRITE)) {
+      return content.forget();
+    }
     return nsnull;
   }
 
   // For non-readonly editors we want to find the root of the editable subtree
   // containing aContent.
   content = content->GetEditingHost();
   return content.forget();
 }
@@ -5696,14 +5702,28 @@ nsHTMLEditor::GetPreferredIMEState(IMESt
 
 already_AddRefed<nsIContent>
 nsHTMLEditor::GetInputEventTargetContent()
 {
   nsCOMPtr<nsIContent> target = GetActiveEditingHost();
   return target.forget();
 }
 
+bool
+nsHTMLEditor::IsEditable(nsIContent* aNode) {
+  if (!nsPlaintextEditor::IsEditable(aNode)) {
+    return false;
+  }
+  if (aNode->IsElement()) {
+    // If we're dealing with an element, then ask it whether it's editable.
+    return aNode->IsEditable();
+  }
+  // We might be dealing with a text node for example, which we always consider
+  // to be editable.
+  return true;
+}
+
 // virtual MOZ_OVERRIDE
 dom::Element*
 nsHTMLEditor::GetEditorRoot()
 {
   return GetActiveEditingHost();
 }
--- a/editor/libeditor/html/nsHTMLEditor.h
+++ b/editor/libeditor/html/nsHTMLEditor.h
@@ -135,16 +135,18 @@ public:
   virtual nsresult HandleKeyPressEvent(nsIDOMKeyEvent* aKeyEvent);
   virtual already_AddRefed<nsIContent> GetFocusedContent();
   virtual bool IsActiveInDOMWindow();
   virtual already_AddRefed<nsIDOMEventTarget> GetDOMEventTarget();
   virtual mozilla::dom::Element* GetEditorRoot() MOZ_OVERRIDE;
   virtual already_AddRefed<nsIContent> FindSelectionRoot(nsINode *aNode);
   virtual bool IsAcceptableInputEvent(nsIDOMEvent* aEvent);
   virtual already_AddRefed<nsIContent> GetInputEventTargetContent();
+  virtual bool IsEditable(nsIContent *aNode);
+  using nsEditor::IsEditable;
 
   /* ------------ nsStubMutationObserver overrides --------- */
   NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED
   NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED
   NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED
 
   /* ------------ nsIEditorIMESupport overrides ------------ */
   NS_IMETHOD GetPreferredIMEState(mozilla::widget::IMEState *aState);
--- a/editor/libeditor/html/tests/Makefile.in
+++ b/editor/libeditor/html/tests/Makefile.in
@@ -79,16 +79,17 @@ include $(topsrcdir)/config/rules.mk
 		test_bug552782.html \
 		test_bug570144.html \
 		test_bug578771.html \
 		test_bug592592.html \
 		test_bug597784.html \
 		test_bug599322.html \
 		test_bug607584.html \
 		test_bug611182.html \
+		test_bug612128.html \
 		test_bug612447.html \
 		test_bug620906.html \
 		test_bug622371.html \
 		test_bug629845.html \
 		test_bug640321.html \
 		test_bug668599.html \
 		test_bug674770-1.html \
 		file_bug674770-1.html \
new file mode 100644
--- /dev/null
+++ b/editor/libeditor/html/tests/test_bug612128.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=612128
+-->
+<head>
+  <title>Test for Bug 612128</title>
+  <script type="application/javascript" src="/MochiKit/packed.js"></script>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="text/javascript" src="/tests/SimpleTest/EventUtils.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=612128">Mozilla Bug 612128</a>
+<p id="display"></p>
+<div id="content">
+<input>
+<div contenteditable></div>
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 612128 **/
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+  document.querySelector("input").focus();
+  var threw = false;
+  try {
+    document.execCommand("inserthtml", null, "<span>f" + "oo</span>");
+  } catch (e) {
+    threw = true;
+  }
+  ok(threw, "The inserthtml command should fail");
+  is(document.querySelectorAll("span").length, 0, "No span element should be injected inside the page");
+  is(document.body.innerHTML.indexOf("f" + "oo"), -1, "No text should be injected inside the page");
+  SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/editor/reftests/readonly-editable-ref.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+  <body>
+    <input>
+    <input readonly>
+    <input type=password>
+    <input type=password readonly>
+    <input type=email>
+    <input type=email readonly>
+    <textarea></textarea>
+    <textarea readonly></textarea>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/editor/reftests/readonly-editable.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <style>
+      :-moz-read-write + span {
+        display: none;
+      }
+      span {
+        color: transparent; /* workaround for bug 617524 */
+        outline: 1px solid green;
+      }
+    </style>
+  </head>
+  <body contenteditable>
+    <input><span>hide me</span>
+    <input readonly><span>hide me</span>
+    <input type=password><span>hide me</span>
+    <input type=password readonly><span>hide me</span>
+    <input type=email><span>hide me</span>
+    <input type=email readonly><span>hide me</span>
+    <textarea></textarea><span>hide me</span>
+    <textarea readonly></textarea><span>hide me</span>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/editor/reftests/readonly-non-editable-ref.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <style>
+      span {
+        color: transparent; /* workaround for bug 617524 */
+        outline: 1px solid green;
+      }
+    </style>
+  </head>
+  <body>
+    <input><span>hide me</span>
+    <input readonly>
+    <input type=password><span>hide me</span>
+    <input type=password readonly>
+    <input type=email><span>hide me</span>
+    <input type=email readonly>
+    <textarea></textarea><span>hide me</span>
+    <textarea readonly></textarea>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/editor/reftests/readonly-non-editable.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <style>
+      :-moz-read-only + span {
+        display: none;
+      }
+      span {
+        color: transparent; /* workaround for bug 617524 */
+        outline: 1px solid green;
+      }
+    </style>
+  </head>
+  <body>
+    <input><span>hide me</span>
+    <input readonly><span>hide me</span>
+    <input type=password><span>hide me</span>
+    <input type=password readonly><span>hide me</span>
+    <input type=email><span>hide me</span>
+    <input type=email readonly><span>hide me</span>
+    <textarea></textarea><span>hide me</span>
+    <textarea readonly></textarea><span>hide me</span>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/editor/reftests/readwrite-editable-ref.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+  <body>
+    <input>
+    <input readonly>
+    <input type=password>
+    <input type=password readonly>
+    <input type=email>
+    <input type=email readonly>
+    <textarea></textarea>
+    <textarea readonly></textarea>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/editor/reftests/readwrite-editable.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <style>
+      :-moz-read-write + span {
+        display: none;
+      }
+      span {
+        color: transparent; /* workaround for bug 617524 */
+        outline: 1px solid green;
+      }
+    </style>
+  </head>
+  <body contenteditable>
+    <input><span>hide me</span>
+    <input readonly><span>hide me</span>
+    <input type=password><span>hide me</span>
+    <input type=password readonly><span>hide me</span>
+    <input type=email><span>hide me</span>
+    <input type=email readonly><span>hide me</span>
+    <textarea></textarea><span>hide me</span>
+    <textarea readonly></textarea><span>hide me</span>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/editor/reftests/readwrite-non-editable-ref.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <style>
+      span {
+        color: transparent; /* workaround for bug 617524 */
+        outline: 1px solid green;
+      }
+    </style>
+  </head>
+  <body>
+    <input>
+    <input readonly><span>hide me</span>
+    <input type=password>
+    <input type=password readonly><span>hide me</span>
+    <input type=email>
+    <input type=email readonly><span>hide me</span>
+    <textarea></textarea>
+    <textarea readonly></textarea><span>hide me</span>
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/editor/reftests/readwrite-non-editable.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <style>
+      :-moz-read-write + span {
+        display: none;
+      }
+      span {
+        color: transparent; /* workaround for bug 617524 */
+        outline: 1px solid green;
+      }
+    </style>
+  </head>
+  <body>
+    <input><span>hide me</span>
+    <input readonly><span>hide me</span>
+    <input type=password><span>hide me</span>
+    <input type=password readonly><span>hide me</span>
+    <input type=email><span>hide me</span>
+    <input type=email readonly><span>hide me</span>
+    <textarea></textarea><span>hide me</span>
+    <textarea readonly></textarea><span>hide me</span>
+  </body>
+</html>
--- a/editor/reftests/reftest.list
+++ b/editor/reftests/reftest.list
@@ -80,13 +80,17 @@ fails-if(Android) != spellcheck-hyphen-m
 != selection_visibility_after_reframe-2.html selection_visibility_after_reframe-ref.html
 != selection_visibility_after_reframe-3.html selection_visibility_after_reframe-ref.html
 == 672709.html 672709-ref.html
 == 338427-1.html 338427-1-ref.html
 skip-if(Android) == 674212-spellcheck.html 674212-spellcheck-ref.html
 skip-if(Android) == 338427-2.html 338427-2-ref.html
 skip-if(Android) needs-focus == 338427-3.html 338427-3-ref.html
 skip-if(Android) == 462758-grabbers-resizers.html 462758-grabbers-resizers-ref.html
+== readwrite-non-editable.html readwrite-non-editable-ref.html
+== readwrite-editable.html readwrite-editable-ref.html
+== readonly-non-editable.html readonly-non-editable-ref.html
+== readonly-editable.html readonly-editable-ref.html
 == dynamic-overflow-change.html dynamic-overflow-change-ref.html
 == 694880-1.html 694880-ref.html
 == 694880-2.html 694880-ref.html
 == 694880-3.html 694880-ref.html
 == 388980-1.html 388980-1-ref.html
--- a/layout/base/nsCSSFrameConstructor.cpp
+++ b/layout/base/nsCSSFrameConstructor.cpp
@@ -3868,16 +3868,41 @@ nsCSSFrameConstructor::CreateAnonymousFr
       // create the frame and attach it to our frame
       ConstructFrame(aState, content, aParentFrame, aChildItems);
     }
   }
 
   return NS_OK;
 }
 
+static void
+SetFlagsOnSubtree(nsIContent *aNode, PtrBits aFlagsToSet)
+{
+#ifdef DEBUG
+  // Make sure that the node passed to us doesn't have any XBL children
+  {
+    nsIDocument *doc = aNode->OwnerDoc();
+    NS_ASSERTION(doc, "The node must be in a document");
+    NS_ASSERTION(!doc->BindingManager()->GetXBLChildNodesFor(aNode),
+                 "The node should not have any XBL children");
+  }
+#endif
+
+  // Set the flag on the node itself
+  aNode->SetFlags(aFlagsToSet);
+
+  // Set the flag on all of its children recursively
+  PRUint32 count;
+  nsIContent * const *children = aNode->GetChildArray(&count);
+
+  for (PRUint32 index = 0; index < count; ++index) {
+    SetFlagsOnSubtree(children[index], aFlagsToSet);
+  }
+}
+
 nsresult
 nsCSSFrameConstructor::GetAnonymousContent(nsIContent* aParent,
                                            nsIFrame* aParentFrame,
                                            nsTArray<nsIAnonymousContentCreator::ContentInfo>& aContent)
 {
   nsIAnonymousContentCreator* creator = do_QueryFrame(aParentFrame);
   if (!creator)
     return NS_OK;
@@ -3895,17 +3920,27 @@ nsCSSFrameConstructor::GetAnonymousConte
     // cascading rules for <svg:use> - bug 265894
     if (aParent &&
         aParent->NodeInfo()->Equals(nsGkAtoms::use, kNameSpaceID_SVG)) {
       content->SetFlags(NODE_IS_ANONYMOUS);
     } else {
       content->SetNativeAnonymous();
     }
 
+    bool anonContentIsEditable = content->HasFlag(NODE_IS_EDITABLE);
     rv = content->BindToTree(mDocument, aParent, aParent, true);
+    // If the anonymous content creator requested that the content should be
+    // editable, honor its request.
+    // We need to set the flag on the whole subtree, because existing
+    // children's flags have already been set as part of the BindToTree operation.
+    if (anonContentIsEditable) {
+      NS_ASSERTION(aParentFrame->GetType() == nsGkAtoms::textInputFrame,
+                   "We only expect this for anonymous content under a text control frame");
+      SetFlagsOnSubtree(content, NODE_IS_EDITABLE);
+    }
     if (NS_FAILED(rv)) {
       content->UnbindFromTree();
       return rv;
     }
   }
 
   return NS_OK;
 }
@@ -6103,59 +6138,40 @@ nsCSSFrameConstructor::ReframeTextIfNeed
     // NS_CREATE_FRAME_IF_NON_WHITESPACE flag)
     return;
   }
   NS_ASSERTION(!aContent->GetPrimaryFrame(),
                "Text node has a frame and NS_CREATE_FRAME_IF_NON_WHITESPACE");
   ContentInserted(aParentContent, aContent, nsnull, false);
 }
 
-// We want to disable lazy frame construction for nodes that are under an
-// editor. We use nsINode::IsEditable, but that includes inputs with type text
-// and password and textareas, which are common and aren't really editable (the
-// native anonymous content under them is what is actually editable) so we want
-// to construct frames for those lazily.
-// The logic for this check is based on
-// nsGenericHTMLFormElement::UpdateEditableFormControlState and so must be kept
-// in sync with that.  MayHaveContentEditableAttr() being true only indicates
-// a contenteditable attribute, it doesn't indicate whether it is true or false,
-// so we force eager construction in some cases when the node is not editable,
-// but that should be rare.
-static inline bool
-IsActuallyEditable(nsIContent* aContainer, nsIContent* aChild)
-{
-  return (aChild->IsEditable() &&
-          (aContainer->IsEditable() ||
-           aChild->MayHaveContentEditableAttr()));
-}
-
 // For inserts aChild should be valid, for appends it should be null.
 // Returns true if this operation can be lazy, false if not.
 bool
 nsCSSFrameConstructor::MaybeConstructLazily(Operation aOperation,
                                             nsIContent* aContainer,
                                             nsIContent* aChild)
 {
   if (mPresShell->GetPresContext()->IsChrome() || !aContainer ||
       aContainer->IsInNativeAnonymousSubtree() || aContainer->IsXUL()) {
     return false;
   }
 
   if (aOperation == CONTENTINSERT) {
     if (aChild->IsRootOfAnonymousSubtree() ||
-        aChild->IsXUL() || IsActuallyEditable(aContainer, aChild)) {
+        aChild->IsEditable() || aChild->IsXUL()) {
       return false;
     }
   } else { // CONTENTAPPEND
     NS_ASSERTION(aOperation == CONTENTAPPEND,
                  "operation should be either insert or append");
     for (nsIContent* child = aChild; child; child = child->GetNextSibling()) {
       NS_ASSERTION(!child->IsRootOfAnonymousSubtree(),
                    "Should be coming through the CONTENTAPPEND case");
-      if (child->IsXUL() || IsActuallyEditable(aContainer, child)) {
+      if (child->IsXUL() || child->IsEditable()) {
         return false;
       }
     }
   }
 
   // We can construct lazily; just need to set suitable bits in the content
   // tree.
 
--- a/layout/generic/nsIAnonymousContentCreator.h
+++ b/layout/generic/nsIAnonymousContentCreator.h
@@ -74,16 +74,20 @@ public:
     nsIContent* mContent;
     nsRefPtr<nsStyleContext> mStyleContext;
   };
 
   /**
    * Creates "native" anonymous content and adds the created content to
    * the aElements array. None of the returned elements can be nsnull.
    *
+   * If the anonymous content creator sets the editable flag on some
+   * of the elements that it creates, the flag will be applied to the node
+   * upon being bound to the document.
+   *
    * @note The returned elements are owned by this object. This object is
    *       responsible for calling UnbindFromTree on the elements it returned
    *       from CreateAnonymousContent when appropriate (i.e. before releasing
    *       them).
    */
   virtual nsresult CreateAnonymousContent(nsTArray<ContentInfo>& aElements)=0;
 
   /**