Commit 294c5068 authored by Masayuki Nakano's avatar Masayuki Nakano
Browse files

Bug 1815969 - part 1: Redesign `EditorBase::FireClipboardEvent` r=m_kato

It does not handle critical cases properly, and it uses an out-param.
We can rewrite it with `Result`.  However, unfortunately,
`nsCopySupport::FireClipboardEvent` does not return error.  Therefore,
the root callers still need to treat the error cases as "canceled".

Differential Revision: https://phabricator.services.mozilla.com/D176737
parent 32c74afb
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -243,7 +243,7 @@ class HyperTextAccessible : public AccessibleWrap,
  MOZ_CAN_RUN_SCRIPT_BOUNDARY void ReplaceText(const nsAString& aText);
  MOZ_CAN_RUN_SCRIPT_BOUNDARY void InsertText(const nsAString& aText,
                                              int32_t aPosition);
  void CopyText(int32_t aStartPos, int32_t aEndPos);
  MOZ_CAN_RUN_SCRIPT_BOUNDARY void CopyText(int32_t aStartPos, int32_t aEndPos);
  MOZ_CAN_RUN_SCRIPT_BOUNDARY void CutText(int32_t aStartPos, int32_t aEndPos);
  MOZ_CAN_RUN_SCRIPT_BOUNDARY void DeleteText(int32_t aStartPos,
                                              int32_t aEndPos);
+85 −25
Original line number Diff line number Diff line
@@ -33,6 +33,7 @@

#include "ErrorList.h"
#include "gfxFontUtils.h"  // for gfxFontUtils
#include "mozilla/Assertions.h"
#include "mozilla/intl/BidiEmbeddingLevel.h"
#include "mozilla/BasePrincipal.h"            // for BasePrincipal
#include "mozilla/CheckedInt.h"               // for CheckedInt
@@ -1578,35 +1579,64 @@ bool EditorBase::CheckForClipboardCommandListener(
  return false;
}

bool EditorBase::FireClipboardEvent(EventMessage aEventMessage,
                                    int32_t aClipboardType,
                                    bool* aActionTaken) {
Result<EditorBase::ClipboardEventResult, nsresult>
EditorBase::DispatchClipboardEventAndUpdateClipboard(EventMessage aEventMessage,
                                                     int32_t aClipboardType) {
  MOZ_ASSERT(IsEditActionDataAvailable());

  if (aEventMessage == ePaste) {
    CommitComposition();
    if (NS_WARN_IF(Destroyed())) {
      return Err(NS_ERROR_EDITOR_DESTROYED);
    }
  }

  RefPtr<PresShell> presShell = GetPresShell();
  if (NS_WARN_IF(!presShell)) {
    return false;
    return Err(NS_ERROR_NOT_AVAILABLE);
  }

  RefPtr<Selection> sel = &SelectionRef();
  if (IsHTMLEditor() && aEventMessage == eCopy && sel->IsCollapsed()) {
    // If we don't have a usable selection for copy and we're an HTML editor
    // (which is global for the document) try to use the last focused selection
    // instead.
    sel = nsCopySupport::GetSelectionForCopy(GetDocument());
  const RefPtr<Selection> sel = [&]() {
    if (IsHTMLEditor() && aEventMessage == eCopy &&
        SelectionRef().IsCollapsed()) {
      // If we don't have a usable selection for copy and we're an HTML
      // editor (which is global for the document) try to use the last
      // focused selection instead.
      return nsCopySupport::GetSelectionForCopy(GetDocument());
    }
    return do_AddRef(&SelectionRef());
  }();

  const bool clipboardEventCanceled = !nsCopySupport::FireClipboardEvent(
      aEventMessage, aClipboardType, presShell, sel, aActionTaken);
  bool actionTaken = false;
  const bool doDefault = nsCopySupport::FireClipboardEvent(
      aEventMessage, aClipboardType, presShell, sel, &actionTaken);
  NotifyOfDispatchingClipboardEvent();

  // If the event handler caused the editor to be destroyed, return false.
  // Otherwise return true if the event was not cancelled.
  return !clipboardEventCanceled && !mDidPreDestroy;
  if (NS_WARN_IF(Destroyed())) {
    return Err(NS_ERROR_EDITOR_DESTROYED);
  }

  if (doDefault) {
    MOZ_ASSERT(actionTaken);
    return ClipboardEventResult::DoDefault;
  }
  // If we handle a "paste" and nsCopySupport::FireClipboardEvent sets
  // actionTaken to "false" means that it's an error.  Otherwise, the "paste"
  // event is just canceled.
  if (aEventMessage == ePaste) {
    return actionTaken ? ClipboardEventResult::DefaultPreventedOfPaste
                       : ClipboardEventResult::IgnoredOrError;
  }
  // If we handle a "copy", actionTaken is set to true only when
  // nsCopySupport::FireClipboardEvent does not meet an error.
  // If we handle a "cut", actionTaken is set to true only when
  // nsCopySupport::FireClipboardEvent does not meet an error and
  // - the selection is collapsed in editable elements when the event is not
  //   canceled.
  // - the event is canceled but update the clipboard with the dataTransfer
  //   of the event.
  return actionTaken ? ClipboardEventResult::CopyOrCutHandled
                     : ClipboardEventResult::IgnoredOrError;
}

NS_IMETHODIMP EditorBase::Cut() {
@@ -1621,10 +1651,26 @@ nsresult EditorBase::CutAsAction(nsIPrincipal* aPrincipal) {
    return NS_ERROR_NOT_INITIALIZED;
  }

  bool actionTaken = false;
  if (!FireClipboardEvent(eCut, nsIClipboard::kGlobalClipboard, &actionTaken)) {
    return EditorBase::ToGenericNSResult(
        actionTaken ? NS_OK : NS_ERROR_EDITOR_ACTION_CANCELED);
  {
    Result<ClipboardEventResult, nsresult> ret =
        DispatchClipboardEventAndUpdateClipboard(
            eCut, nsIClipboard::kGlobalClipboard);
    if (MOZ_UNLIKELY(ret.isErr())) {
      NS_WARNING(
          "EditorBase::DispatchClipboardEventAndUpdateClipboard(eCut, "
          "nsIClipboard::kGlobalClipboard) failed");
      return EditorBase::ToGenericNSResult(ret.unwrapErr());
    }
    switch (ret.unwrap()) {
      case ClipboardEventResult::DoDefault:
        break;
      case ClipboardEventResult::CopyOrCutHandled:
        return NS_OK;
      case ClipboardEventResult::IgnoredOrError:
        return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_ACTION_CANCELED);
      case ClipboardEventResult::DefaultPreventedOfPaste:
        MOZ_ASSERT_UNREACHABLE("Invalid result for eCut");
    }
  }

  // Dispatch "beforeinput" event after dispatching "cut" event.
@@ -1678,11 +1724,25 @@ NS_IMETHODIMP EditorBase::Copy() {
    return NS_ERROR_NOT_INITIALIZED;
  }

  bool actionTaken = false;
  FireClipboardEvent(eCopy, nsIClipboard::kGlobalClipboard, &actionTaken);

  return EditorBase::ToGenericNSResult(
      actionTaken ? NS_OK : NS_ERROR_EDITOR_ACTION_CANCELED);
  Result<ClipboardEventResult, nsresult> ret =
      DispatchClipboardEventAndUpdateClipboard(eCopy,
                                               nsIClipboard::kGlobalClipboard);
  if (MOZ_UNLIKELY(ret.isErr())) {
    NS_WARNING(
        "EditorBase::DispatchClipboardEventAndUpdateClipboard(eCopy, "
        "nsIClipboard::kGlobalClipboard) failed");
    return EditorBase::ToGenericNSResult(ret.unwrapErr());
  }
  switch (ret.unwrap()) {
    case ClipboardEventResult::DoDefault:
    case ClipboardEventResult::CopyOrCutHandled:
      return NS_OK;
    case ClipboardEventResult::IgnoredOrError:
      return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_ACTION_CANCELED);
    case ClipboardEventResult::DefaultPreventedOfPaste:
      MOZ_ASSERT_UNREACHABLE("Invalid result for eCopy");
  }
  return NS_ERROR_UNEXPECTED;
}

NS_IMETHODIMP EditorBase::CanCopy(bool* aCanCopy) {
+22 −7
Original line number Diff line number Diff line
@@ -2619,18 +2619,33 @@ class EditorBase : public nsIEditor,
      nsAtom* aCommand, EventMessage aEventMessage) const;

  /**
   * FireClipboardEvent() may dispatch a clipboard event.
   * DispatchClipboardEventAndUpdateClipboard() may dispatch a clipboard event
   * and update clipboard if aEventMessage is eCopy or eCut.
   *
   * @param aEventMessage       The event message which may be set to the
   *                            dispatching event.
   * @param aClipboardType      Working with global clipboard or selection.
   * @param aActionTaken        [optional][out] If set to non-nullptr, will be
   *                            set to true if the action for the event is
   *                            handled or prevented default.
   * @return                    false if dispatching event is canceled.
   */
  bool FireClipboardEvent(EventMessage aEventMessage, int32_t aClipboardType,
                          bool* aActionTaken = nullptr);
  enum class ClipboardEventResult {
    // We have met an error in nsCopySupport::FireClipboardEvent,
    // or, default of dispatched event is NOT prevented, the event is "cut"
    // and the event target is not editable.
    IgnoredOrError,
    // A "paste" event is dispatched and prevented its default.
    DefaultPreventedOfPaste,
    // Default of a "copy" or "cut" event is prevented but the clipboard is
    // updated unless the dataTransfer of the event is cleared by the listener.
    // Or, default of the event is NOT prevented but selection is collapsed
    // when the event target is editable or the event is "copy".
    CopyOrCutHandled,
    // A clipboard event is maybe dispatched and not canceled by the web app.
    // In this case, the clipboard has been updated if aEventMessage is eCopy
    // or eCut.
    DoDefault,
  };
  [[nodiscard]] MOZ_CAN_RUN_SCRIPT Result<ClipboardEventResult, nsresult>
  DispatchClipboardEventAndUpdateClipboard(EventMessage aEventMessage,
                                           int32_t aClipboardType);

 private:
  nsCOMPtr<nsISelectionController> mSelectionController;
+71 −9
Original line number Diff line number Diff line
@@ -4,7 +4,6 @@
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "ErrorList.h"
#include "HTMLEditor.h"

#include <string.h>
@@ -19,6 +18,7 @@
#include "SelectionState.h"
#include "WSRunObject.h"

#include "ErrorList.h"
#include "mozilla/dom/Comment.h"
#include "mozilla/dom/DataTransfer.h"
#include "mozilla/dom/Document.h"
@@ -2275,8 +2275,22 @@ nsresult HTMLEditor::PasteAsAction(int32_t aClipboardType,
  }

  if (aDispatchPasteEvent) {
    if (!FireClipboardEvent(ePaste, aClipboardType)) {
    Result<ClipboardEventResult, nsresult> ret =
        DispatchClipboardEventAndUpdateClipboard(ePaste, aClipboardType);
    if (MOZ_UNLIKELY(ret.isErr())) {
      NS_WARNING(
          "EditorBase::DispatchClipboardEventAndUpdateClipboard(ePaste) "
          "failed");
      return EditorBase::ToGenericNSResult(ret.unwrapErr());
    }
    switch (ret.inspect()) {
      case ClipboardEventResult::DoDefault:
        break;
      case ClipboardEventResult::DefaultPreventedOfPaste:
      case ClipboardEventResult::IgnoredOrError:
        return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_ACTION_CANCELED);
      case ClipboardEventResult::CopyOrCutHandled:
        MOZ_ASSERT_UNREACHABLE("Invalid result for ePaste");
    }
  } else {
    // The caller must already have dispatched a "paste" event.
@@ -2428,8 +2442,25 @@ nsresult HTMLEditor::PasteTransferableAsAction(nsITransferable* aTransferable,
  // Use an invalid value for the clipboard type as data comes from
  // aTransferable and we don't currently implement a way to put that in the
  // data transfer yet.
  if (!FireClipboardEvent(ePaste, nsIClipboard::kGlobalClipboard)) {
  {
    Result<ClipboardEventResult, nsresult> ret =
        DispatchClipboardEventAndUpdateClipboard(
            ePaste, nsIClipboard::kGlobalClipboard);
    if (MOZ_UNLIKELY(ret.isErr())) {
      NS_WARNING(
          "EditorBase::DispatchClipboardEventAndUpdateClipboard(ePaste, "
          "nsIClipboard::kGlobalClipboard) failed");
      return EditorBase::ToGenericNSResult(ret.unwrapErr());
    }
    switch (ret.inspect()) {
      case ClipboardEventResult::DoDefault:
        break;
      case ClipboardEventResult::DefaultPreventedOfPaste:
      case ClipboardEventResult::IgnoredOrError:
        return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_ACTION_CANCELED);
      case ClipboardEventResult::CopyOrCutHandled:
        MOZ_ASSERT_UNREACHABLE("Invalid result for ePaste");
    }
  }

  // Dispatch "beforeinput" event after "paste" event.
@@ -2476,8 +2507,25 @@ nsresult HTMLEditor::PasteNoFormattingAsAction(int32_t aSelectionType,
  editActionData.InitializeDataTransferWithClipboard(
      SettingDataTransfer::eWithoutFormat, aSelectionType);

  if (!FireClipboardEvent(ePasteNoFormatting, aSelectionType)) {
  {
    Result<ClipboardEventResult, nsresult> ret =
        DispatchClipboardEventAndUpdateClipboard(ePasteNoFormatting,
                                                 aSelectionType);
    if (MOZ_UNLIKELY(ret.isErr())) {
      NS_WARNING(
          "EditorBase::DispatchClipboardEventAndUpdateClipboard("
          "ePasteNoFormatting) failed");
      return EditorBase::ToGenericNSResult(ret.unwrapErr());
    }
    switch (ret.inspect()) {
      case ClipboardEventResult::DoDefault:
        break;
      case ClipboardEventResult::DefaultPreventedOfPaste:
      case ClipboardEventResult::IgnoredOrError:
        return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_ACTION_CANCELED);
      case ClipboardEventResult::CopyOrCutHandled:
        MOZ_ASSERT_UNREACHABLE("Invalid result for ePaste");
    }
  }

  // Dispatch "beforeinput" event after "paste" event.  And perhaps, before
@@ -2645,8 +2693,22 @@ nsresult HTMLEditor::PasteAsQuotationAsAction(int32_t aClipboardType,
  }

  if (aDispatchPasteEvent) {
    if (!FireClipboardEvent(ePaste, aClipboardType)) {
    Result<ClipboardEventResult, nsresult> ret =
        DispatchClipboardEventAndUpdateClipboard(ePaste, aClipboardType);
    if (MOZ_UNLIKELY(ret.isErr())) {
      NS_WARNING(
          "EditorBase::DispatchClipboardEventAndUpdateClipboard(ePaste) "
          "failed");
      return EditorBase::ToGenericNSResult(ret.unwrapErr());
    }
    switch (ret.inspect()) {
      case ClipboardEventResult::DoDefault:
        break;
      case ClipboardEventResult::DefaultPreventedOfPaste:
      case ClipboardEventResult::IgnoredOrError:
        return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_ACTION_CANCELED);
      case ClipboardEventResult::CopyOrCutHandled:
        MOZ_ASSERT_UNREACHABLE("Invalid result for ePaste");
    }
  } else {
    // The caller must already have dispatched a "paste" event.
+37 −7
Original line number Diff line number Diff line
@@ -170,8 +170,22 @@ nsresult TextEditor::PasteAsAction(int32_t aClipboardType,
  }

  if (aDispatchPasteEvent) {
    if (!FireClipboardEvent(ePaste, aClipboardType)) {
    Result<ClipboardEventResult, nsresult> ret =
        DispatchClipboardEventAndUpdateClipboard(ePaste, aClipboardType);
    if (MOZ_UNLIKELY(ret.isErr())) {
      NS_WARNING(
          "EditorBase::DispatchClipboardEventAndUpdateClipboard(ePaste) "
          "failed");
      return EditorBase::ToGenericNSResult(ret.unwrapErr());
    }
    switch (ret.inspect()) {
      case ClipboardEventResult::DoDefault:
        break;
      case ClipboardEventResult::DefaultPreventedOfPaste:
      case ClipboardEventResult::IgnoredOrError:
        return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_ACTION_CANCELED);
      case ClipboardEventResult::CopyOrCutHandled:
        MOZ_ASSERT_UNREACHABLE("Invalid result for ePaste");
    }
  } else {
    // The caller must already have dispatched a "paste" event.
@@ -235,11 +249,27 @@ nsresult TextEditor::PasteTransferableAsAction(nsITransferable* aTransferable,
    return NS_ERROR_NOT_INITIALIZED;
  }

  {
    // Use an invalid value for the clipboard type as data comes from
    // aTransferable and we don't currently implement a way to put that in the
    // data transfer yet.
  if (!FireClipboardEvent(ePaste, -1)) {
    Result<ClipboardEventResult, nsresult> ret =
        DispatchClipboardEventAndUpdateClipboard(ePaste, -1);
    if (MOZ_UNLIKELY(ret.isErr())) {
      NS_WARNING(
          "EditorBase::DispatchClipboardEventAndUpdateClipboard(ePaste, -1) "
          "failed");
      return EditorBase::ToGenericNSResult(ret.unwrapErr());
    }
    switch (ret.inspect()) {
      case ClipboardEventResult::DoDefault:
        break;
      case ClipboardEventResult::DefaultPreventedOfPaste:
      case ClipboardEventResult::IgnoredOrError:
        return EditorBase::ToGenericNSResult(NS_ERROR_EDITOR_ACTION_CANCELED);
      case ClipboardEventResult::CopyOrCutHandled:
        MOZ_ASSERT_UNREACHABLE("Invalid result for ePaste");
    }
  }

  if (!IsModifiable()) {
Loading