Bug 1158456 - Remove control characters from composition string, and add dom.compositionevent.allow_control_characters pref to control it. r=masayuki
authorTooru Fujisawa <arai_a@mac.com>
Fri, 01 May 2015 13:49:29 +0900
changeset 271805 2e212218ba40e9d842fcee6b64364b4ddde2b6b2
parent 271804 924a8ed0b89cf23e388e2a673866354ff67f7d80
child 271806 5d7b3b8cce5ab70e7f6280e9b46ffd2e413994ab
push id4830
push userjlund@mozilla.com
push dateMon, 29 Jun 2015 20:18:48 +0000
treeherdermozilla-beta@4c2175bb0420 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmasayuki
bugs1158456
milestone40.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 1158456 - Remove control characters from composition string, and add dom.compositionevent.allow_control_characters pref to control it. r=masayuki
dom/events/TextComposition.cpp
dom/events/TextComposition.h
modules/libpref/init/all.js
widget/TextRange.h
widget/tests/window_composition_text_querycontent.xul
--- a/dom/events/TextComposition.cpp
+++ b/dom/events/TextComposition.cpp
@@ -9,16 +9,17 @@
 #include "nsIContent.h"
 #include "nsIEditor.h"
 #include "nsIPresShell.h"
 #include "nsPresContext.h"
 #include "mozilla/AutoRestore.h"
 #include "mozilla/EventDispatcher.h"
 #include "mozilla/IMEStateManager.h"
 #include "mozilla/MiscEvents.h"
+#include "mozilla/Preferences.h"
 #include "mozilla/TextComposition.h"
 #include "mozilla/TextEvents.h"
 
 using namespace mozilla::widget;
 
 namespace mozilla {
 
 #define IDEOGRAPHIC_SPACE (NS_LITERAL_STRING("\x3000"))
@@ -38,16 +39,19 @@ TextComposition::TextComposition(nsPresC
   , mCompositionTargetOffset(0)
   , mIsSynthesizedForTests(aCompositionEvent->mFlags.mIsSynthesizedForTests)
   , mIsComposing(false)
   , mIsEditorHandlingEvent(false)
   , mIsRequestingCommit(false)
   , mIsRequestingCancel(false)
   , mRequestedToCommitOrCancel(false)
   , mWasNativeCompositionEndEventDiscarded(false)
+  , mAllowControlCharacters(
+      Preferences::GetBool("dom.compositionevent.allow_control_characters",
+                           false))
 {
 }
 
 void
 TextComposition::Destroy()
 {
   mPresContext = nullptr;
   mNode = nullptr;
@@ -128,23 +132,81 @@ TextComposition::OnCompositionEventDisca
   //     TSF.
   if (!aCompositionEvent->CausesDOMCompositionEndEvent()) {
     return;
   }
 
   mWasNativeCompositionEndEventDiscarded = true;
 }
 
+static inline bool
+IsControlChar(uint32_t aCharCode)
+{
+  return aCharCode < ' ' || aCharCode == 0x7F;
+}
+
+static size_t
+FindFirstControlCharacter(const nsAString& aStr)
+{
+  const char16_t* sourceBegin = aStr.BeginReading();
+  const char16_t* sourceEnd = aStr.EndReading();
+
+  for (const char16_t* source = sourceBegin; source < sourceEnd; ++source) {
+    if (*source != '\t' && IsControlChar(*source)) {
+      return source - sourceBegin;
+    }
+  }
+
+  return -1;
+}
+
+static void
+RemoveControlCharactersFrom(nsAString& aStr, TextRangeArray* aRanges)
+{
+  size_t firstControlCharOffset = FindFirstControlCharacter(aStr);
+  if (firstControlCharOffset == (size_t)-1) {
+    return;
+  }
+
+  nsAutoString copy(aStr);
+  const char16_t* sourceBegin = copy.BeginReading();
+  const char16_t* sourceEnd = copy.EndReading();
+
+  char16_t* dest = aStr.BeginWriting();
+  if (NS_WARN_IF(!dest)) {
+    return;
+  }
+
+  char16_t* curDest = dest + firstControlCharOffset;
+  size_t i = firstControlCharOffset;
+  for (const char16_t* source = sourceBegin + firstControlCharOffset;
+       source < sourceEnd; ++source) {
+    if (*source == '\t' || !IsControlChar(*source)) {
+      *curDest = *source;
+      ++curDest;
+      ++i;
+    } else if (aRanges) {
+      aRanges->RemoveCharacter(i);
+    }
+  }
+
+  aStr.SetLength(curDest - dest);
+}
+
 void
 TextComposition::DispatchCompositionEvent(
                    WidgetCompositionEvent* aCompositionEvent,
                    nsEventStatus* aStatus,
                    EventDispatchingCallback* aCallBack,
                    bool aIsSynthesized)
 {
+  if (!mAllowControlCharacters) {
+    RemoveControlCharactersFrom(aCompositionEvent->mData,
+                                aCompositionEvent->mRanges);
+  }
   if (aCompositionEvent->message == NS_COMPOSITION_COMMIT_AS_IS) {
     NS_ASSERTION(!aCompositionEvent->mRanges,
                  "mRanges of NS_COMPOSITION_COMMIT_AS_IS should be null");
     aCompositionEvent->mRanges = nullptr;
     NS_ASSERTION(aCompositionEvent->mData.IsEmpty(),
                  "mData of NS_COMPOSITION_COMMIT_AS_IS should be empty string");
     if (mLastData == IDEOGRAPHIC_SPACE) {
       // If the last data is an ideographic space (FullWidth space), it must be
--- a/dom/events/TextComposition.h
+++ b/dom/events/TextComposition.h
@@ -216,16 +216,23 @@ private:
   //       mIsRequestingCancel are set false.
   bool mRequestedToCommitOrCancel;
 
   // mWasNativeCompositionEndEventDiscarded is true if this composition was
   // requested commit or cancel itself but native compositionend event is
   // discarded by PresShell due to not safe to dispatch events.
   bool mWasNativeCompositionEndEventDiscarded;
 
+  // Allow control characters appear in composition string.
+  // When this is false, control characters except
+  // CHARACTER TABULATION (horizontal tab) are removed from
+  // both composition string and data attribute of compositionupdate
+  // and compositionend events.
+  bool mAllowControlCharacters;
+
   // Hide the default constructor and copy constructor.
   TextComposition() {}
   TextComposition(const TextComposition& aOther);
 
   /**
    * GetEditor() returns nsIEditor pointer of mEditorWeak.
    */
   already_AddRefed<nsIEditor> GetEditor() const;
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -4708,8 +4708,15 @@ pref("media.gmp.insecure.allow", false);
 pref("gfx.vsync.hw-vsync.enabled", true);
 pref("gfx.vsync.compositor", true);
 pref("gfx.vsync.refreshdriver", true);
 
 // Secure Element API
 #ifdef MOZ_SECUREELEMENT
 pref("dom.secureelement.enabled", false);
 #endif
+
+// Allow control characters appear in composition string.
+// When this is false, control characters except
+// CHARACTER TABULATION (horizontal tab) are removed from
+// both composition string and data attribute of compositionupdate
+// and compositionend events.
+pref("dom.compositionevent.allow_control_characters", false);
--- a/widget/TextRange.h
+++ b/widget/TextRange.h
@@ -169,16 +169,26 @@ struct TextRange
 
   bool Equals(const TextRange& aOther) const
   {
     return mStartOffset == aOther.mStartOffset &&
            mEndOffset == aOther.mEndOffset &&
            mRangeType == aOther.mRangeType &&
            mRangeStyle == aOther.mRangeStyle;
   }
+
+  void RemoveCharacter(uint32_t aOffset)
+  {
+    if (mStartOffset > aOffset) {
+      --mStartOffset;
+      --mEndOffset;
+    } else if (mEndOffset > aOffset) {
+      --mEndOffset;
+    }
+  }
 };
 
 /******************************************************************************
  * mozilla::TextRangeArray
  ******************************************************************************/
 class TextRangeArray final : public nsAutoTArray<TextRange, 10>
 {
   ~TextRangeArray() {}
@@ -218,13 +228,20 @@ public:
     }
     for (size_t i = 0; i < len; i++) {
       if (!ElementAt(i).Equals(aOther.ElementAt(i))) {
         return false;
       }
     }
     return true;
   }
+
+  void RemoveCharacter(uint32_t aOffset)
+  {
+    for (size_t i = 0, len = Length(); i < len; i++) {
+      ElementAt(i).RemoveCharacter(aOffset);
+    }
+  }
 };
 
 } // namespace mozilla
 
 #endif // mozilla_TextRage_h_
--- a/widget/tests/window_composition_text_querycontent.xul
+++ b/widget/tests/window_composition_text_querycontent.xul
@@ -3181,16 +3181,109 @@ function runNotRedundantChangeTest()
   synthesizeComposition({ type: "compositioncommit", data: "" });
 
   textarea.removeEventListener("compositionupdate", handler, true);
   textarea.removeEventListener("compositionend", handler, true);
   textarea.removeEventListener("input", handler, true);
   textarea.removeEventListener("text", handler, true);
 }
 
+function runControlCharTest()
+{
+  textarea.focus();
+
+  var result = {};
+  function clearResult()
+  {
+    result = { compositionupdate: null, compositionend: null };
+  }
+
+  function handler(aEvent)
+  {
+    result[aEvent.type] = aEvent.data;
+  }
+
+  textarea.addEventListener("compositionupdate", handler, true);
+  textarea.addEventListener("compositionend", handler, true);
+
+  textarea.value = "";
+
+  var controlChars = String.fromCharCode.apply(null, Object.keys(Array.from({length:0x20}))) + "\x7F";
+  var allowedChars = "\t";
+
+  var data = "AB" + controlChars + "CD" + controlChars + "EF";
+  var removedData = "AB" + allowedChars + "CD" + allowedChars + "EF";
+
+  var DIndex = data.indexOf("D");
+  var removedDIndex = removedData.indexOf("D");
+
+  // input string contains control characters
+  clearResult();
+  synthesizeCompositionChange(
+    { "composition":
+      { "string": data,
+        "clauses":
+        [
+          { "length": DIndex,
+            "attr": COMPOSITION_ATTR_SELECTED_CLAUSE },
+          { "length": data.length - DIndex,
+            "attr": COMPOSITION_ATTR_CONVERTED_CLAUSE }
+        ]
+      },
+      "caret": { "start": DIndex, "length": 0 }
+    });
+
+  checkSelection(removedDIndex, "", "runControlCharTest", "#1")
+
+  is(result.compositionupdate, removedData, "runControlCharTest: control characters in event.data should be removed in compositionupdate event #1");
+  is(textarea.value, removedData, "runControlCharTest: control characters should not appear in textarea #1");
+
+  synthesizeComposition({ type: "compositioncommit", data: data });
+
+  is(result.compositionend, removedData, "runControlCharTest: control characters in event.data should be removed in compositionend event #2");
+  is(textarea.value, removedData, "runControlCharTest: control characters should not appear in textarea #2");
+
+  textarea.value = "";
+
+  clearResult();
+
+  SpecialPowers.setBoolPref("dom.compositionevent.allow_control_characters", true);
+
+  // input string contains control characters, allowing control characters
+  clearResult();
+  synthesizeCompositionChange(
+    { "composition":
+      { "string": data,
+        "clauses":
+        [
+          { "length": DIndex,
+            "attr": COMPOSITION_ATTR_SELECTED_CLAUSE },
+          { "length": data.length - DIndex,
+            "attr": COMPOSITION_ATTR_CONVERTED_CLAUSE }
+        ]
+      },
+      "caret": { "start": DIndex, "length": 0 }
+    });
+
+  checkSelection(DIndex - 1 + kLFLen, "", "runControlCharTest", "#3")
+
+  is(result.compositionupdate, data, "runControlCharTest: control characters in event.data should not be removed in compositionupdate event #3");
+  is(textarea.value, data.replace(/\r/g, "\n"), "runControlCharTest: control characters should appear in textarea #3");
+
+  synthesizeComposition({ type: "compositioncommit", data: data });
+
+  is(result.compositionend, data, "runControlCharTest: control characters in event.data should not be removed in compositionend event #4");
+  is(textarea.value, data.replace(/\r/g, "\n"), "runControlCharTest: control characters should appear in textarea #4");
+
+  SpecialPowers.clearUserPref("dom.compositionevent.allow_control_characters");
+
+  textarea.removeEventListener("compositionupdate", handler, true);
+  textarea.removeEventListener("compositionend", handler, true);
+}
+
 function runRemoveContentTest(aCallback)
 {
   var events = [];
   function eventHandler(aEvent)
   {
     events.push(aEvent);
   }
   textarea.addEventListener("compositionstart", eventHandler, true);
@@ -3755,16 +3848,17 @@ function runTest()
   runCharAtPointTest(textarea, "textarea in the document");
   runCharAtPointAtOutsideTest();
   runBug722639Test();
   runForceCommitTest();
   runBug811755Test();
   runIsComposingTest();
   runRedundantChangeTest();
   runNotRedundantChangeTest();
+  runControlCharTest();
   runAsyncForceCommitTest(function () {
     runRemoveContentTest(function () {
       runFrameTest();
       runPanelTest();
       runMaxLengthTest();
     });
   });
 }