diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /editor/composer | |
parent | Initial commit. (diff) | |
download | thunderbird-upstream.tar.xz thunderbird-upstream.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
46 files changed, 3113 insertions, 0 deletions
diff --git a/editor/composer/ComposerCommandsUpdater.cpp b/editor/composer/ComposerCommandsUpdater.cpp new file mode 100644 index 0000000000..efeb9d9544 --- /dev/null +++ b/editor/composer/ComposerCommandsUpdater.cpp @@ -0,0 +1,199 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * 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 "mozilla/ComposerCommandsUpdater.h" + +#include "mozilla/mozalloc.h" // for operator new +#include "mozilla/TransactionManager.h" // for TransactionManager +#include "mozilla/dom/Selection.h" +#include "nsCommandManager.h" // for nsCommandManager +#include "nsComponentManagerUtils.h" // for do_CreateInstance +#include "nsDebug.h" // for NS_ENSURE_TRUE, etc +#include "nsDocShell.h" // for nsIDocShell +#include "nsError.h" // for NS_OK, NS_ERROR_FAILURE, etc +#include "nsID.h" // for NS_GET_IID, etc +#include "nsIInterfaceRequestorUtils.h" // for do_GetInterface +#include "nsLiteralString.h" // for NS_LITERAL_STRING +#include "nsPIDOMWindow.h" // for nsPIDOMWindow + +class nsITransaction; + +namespace mozilla { + +ComposerCommandsUpdater::ComposerCommandsUpdater() + : mDirtyState(eStateUninitialized), + mSelectionCollapsed(eStateUninitialized), + mFirstDoOfFirstUndo(true) {} + +ComposerCommandsUpdater::~ComposerCommandsUpdater() { + // cancel any outstanding update timer + if (mUpdateTimer) { + mUpdateTimer->Cancel(); + } +} + +NS_IMPL_CYCLE_COLLECTING_ADDREF(ComposerCommandsUpdater) +NS_IMPL_CYCLE_COLLECTING_RELEASE(ComposerCommandsUpdater) + +NS_INTERFACE_MAP_BEGIN(ComposerCommandsUpdater) + NS_INTERFACE_MAP_ENTRY(nsITimerCallback) + NS_INTERFACE_MAP_ENTRY(nsINamed) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsITimerCallback) + NS_INTERFACE_MAP_ENTRIES_CYCLE_COLLECTION(ComposerCommandsUpdater) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTION(ComposerCommandsUpdater, mUpdateTimer, mDOMWindow, + mDocShell) + +#if 0 +# pragma mark - +#endif + +void ComposerCommandsUpdater::DidDoTransaction( + TransactionManager& aTransactionManager) { + // only need to update if the status of the Undo menu item changes. + if (aTransactionManager.NumberOfUndoItems() == 1) { + if (mFirstDoOfFirstUndo) { + UpdateCommandGroup(CommandGroup::Undo); + } + mFirstDoOfFirstUndo = false; + } +} + +void ComposerCommandsUpdater::DidUndoTransaction( + TransactionManager& aTransactionManager) { + if (!aTransactionManager.NumberOfUndoItems()) { + mFirstDoOfFirstUndo = true; // reset the state for the next do + } + UpdateCommandGroup(CommandGroup::Undo); +} + +void ComposerCommandsUpdater::DidRedoTransaction( + TransactionManager& aTransactionManager) { + UpdateCommandGroup(CommandGroup::Undo); +} + +#if 0 +# pragma mark - +#endif + +void ComposerCommandsUpdater::Init(nsPIDOMWindowOuter& aDOMWindow) { + mDOMWindow = &aDOMWindow; + mDocShell = aDOMWindow.GetDocShell(); +} + +nsresult ComposerCommandsUpdater::PrimeUpdateTimer() { + if (!mUpdateTimer) { + mUpdateTimer = NS_NewTimer(); + } + const uint32_t kUpdateTimerDelay = 150; + return mUpdateTimer->InitWithCallback(static_cast<nsITimerCallback*>(this), + kUpdateTimerDelay, + nsITimer::TYPE_ONE_SHOT); +} + +MOZ_CAN_RUN_SCRIPT_BOUNDARY +void ComposerCommandsUpdater::TimerCallback() { + mSelectionCollapsed = SelectionIsCollapsed(); + UpdateCommandGroup(CommandGroup::Style); +} + +void ComposerCommandsUpdater::UpdateCommandGroup(CommandGroup aCommandGroup) { + RefPtr<nsCommandManager> commandManager = GetCommandManager(); + if (NS_WARN_IF(!commandManager)) { + return; + } + + switch (aCommandGroup) { + case CommandGroup::Undo: + commandManager->CommandStatusChanged("cmd_undo"); + commandManager->CommandStatusChanged("cmd_redo"); + return; + case CommandGroup::Style: + commandManager->CommandStatusChanged("cmd_bold"); + commandManager->CommandStatusChanged("cmd_italic"); + commandManager->CommandStatusChanged("cmd_underline"); + commandManager->CommandStatusChanged("cmd_tt"); + + commandManager->CommandStatusChanged("cmd_strikethrough"); + commandManager->CommandStatusChanged("cmd_superscript"); + commandManager->CommandStatusChanged("cmd_subscript"); + commandManager->CommandStatusChanged("cmd_nobreak"); + + commandManager->CommandStatusChanged("cmd_em"); + commandManager->CommandStatusChanged("cmd_strong"); + commandManager->CommandStatusChanged("cmd_cite"); + commandManager->CommandStatusChanged("cmd_abbr"); + commandManager->CommandStatusChanged("cmd_acronym"); + commandManager->CommandStatusChanged("cmd_code"); + commandManager->CommandStatusChanged("cmd_samp"); + commandManager->CommandStatusChanged("cmd_var"); + + commandManager->CommandStatusChanged("cmd_increaseFont"); + commandManager->CommandStatusChanged("cmd_decreaseFont"); + + commandManager->CommandStatusChanged("cmd_paragraphState"); + commandManager->CommandStatusChanged("cmd_fontFace"); + commandManager->CommandStatusChanged("cmd_fontColor"); + commandManager->CommandStatusChanged("cmd_backgroundColor"); + commandManager->CommandStatusChanged("cmd_highlight"); + return; + case CommandGroup::Save: + commandManager->CommandStatusChanged("cmd_setDocumentModified"); + commandManager->CommandStatusChanged("cmd_save"); + return; + default: + MOZ_ASSERT_UNREACHABLE("New command group hasn't been implemented yet"); + } +} + +nsresult ComposerCommandsUpdater::UpdateOneCommand(const char* aCommand) { + RefPtr<nsCommandManager> commandManager = GetCommandManager(); + NS_ENSURE_TRUE(commandManager, NS_ERROR_FAILURE); + commandManager->CommandStatusChanged(aCommand); + return NS_OK; +} + +bool ComposerCommandsUpdater::SelectionIsCollapsed() { + if (NS_WARN_IF(!mDOMWindow)) { + return true; + } + + RefPtr<dom::Selection> domSelection = mDOMWindow->GetSelection(); + if (NS_WARN_IF(!domSelection)) { + return false; + } + + return domSelection->IsCollapsed(); +} + +nsCommandManager* ComposerCommandsUpdater::GetCommandManager() { + if (NS_WARN_IF(!mDocShell)) { + return nullptr; + } + return mDocShell->GetCommandManager(); +} + +NS_IMETHODIMP ComposerCommandsUpdater::GetName(nsACString& aName) { + aName.AssignLiteral("ComposerCommandsUpdater"); + return NS_OK; +} + +#if 0 +# pragma mark - +#endif + +nsresult ComposerCommandsUpdater::Notify(nsITimer* aTimer) { + NS_ASSERTION(aTimer == mUpdateTimer.get(), "Hey, this ain't my timer!"); + TimerCallback(); + return NS_OK; +} + +#if 0 +# pragma mark - +#endif + +} // namespace mozilla diff --git a/editor/composer/ComposerCommandsUpdater.h b/editor/composer/ComposerCommandsUpdater.h new file mode 100644 index 0000000000..6cf0886c57 --- /dev/null +++ b/editor/composer/ComposerCommandsUpdater.h @@ -0,0 +1,132 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * + * This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +#ifndef mozilla_ComposerCommandsUpdater_h +#define mozilla_ComposerCommandsUpdater_h + +#include "nsCOMPtr.h" // for already_AddRefed, nsCOMPtr +#include "nsCycleCollectionParticipant.h" +#include "nsINamed.h" +#include "nsISupportsImpl.h" // for NS_DECL_ISUPPORTS +#include "nsITimer.h" // for NS_DECL_NSITIMERCALLBACK, etc +#include "nscore.h" // for NS_IMETHOD, nsresult, etc + +class nsCommandManager; +class nsIDocShell; +class nsITransaction; +class nsITransactionManager; +class nsPIDOMWindowOuter; + +namespace mozilla { + +class TransactionManager; + +class ComposerCommandsUpdater final : public nsITimerCallback, public nsINamed { + public: + ComposerCommandsUpdater(); + + // nsISupports + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(ComposerCommandsUpdater, + nsITimerCallback) + + // nsITimerCallback + NS_DECL_NSITIMERCALLBACK + + // nsINamed + NS_DECL_NSINAMED + + void Init(nsPIDOMWindowOuter& aDOMWindow); + + /** + * OnSelectionChange() is called when selection is changed in the editor. + */ + void OnSelectionChange() { PrimeUpdateTimer(); } + + /** + * OnHTMLEditorCreated() is called when `HTMLEditor` is created and + * initialized. + */ + MOZ_CAN_RUN_SCRIPT void OnHTMLEditorCreated() { + UpdateOneCommand("obs_documentCreated"); + } + + /** + * OnBeforeHTMLEditorDestroyed() is called when `HTMLEditor` is being + * destroyed. + */ + MOZ_CAN_RUN_SCRIPT void OnBeforeHTMLEditorDestroyed() { + // cancel any outstanding update timer + if (mUpdateTimer) { + mUpdateTimer->Cancel(); + mUpdateTimer = nullptr; + } + + // We can't notify the command manager of this right now; it is too late in + // some cases and the window is already partially destructed (e.g. JS + // objects may be gone). + } + + /** + * OnHTMLEditorDirtyStateChanged() is called when dirty state of `HTMLEditor` + * is changed form or to "dirty". + */ + MOZ_CAN_RUN_SCRIPT void OnHTMLEditorDirtyStateChanged(bool aNowDirty) { + if (mDirtyState == static_cast<int8_t>(aNowDirty)) { + return; + } + UpdateCommandGroup(CommandGroup::Save); + UpdateCommandGroup(CommandGroup::Undo); + mDirtyState = aNowDirty; + } + + /** + * The following methods are called when aTransactionManager did + * `DoTransaction`, `UndoTransaction` or `RedoTransaction` of a transaction + * instance. + */ + MOZ_CAN_RUN_SCRIPT void DidDoTransaction( + TransactionManager& aTransactionManager); + MOZ_CAN_RUN_SCRIPT void DidUndoTransaction( + TransactionManager& aTransactionManager); + MOZ_CAN_RUN_SCRIPT void DidRedoTransaction( + TransactionManager& aTransactionManager); + + protected: + virtual ~ComposerCommandsUpdater(); + + enum { + eStateUninitialized = -1, + eStateOff = 0, + eStateOn = 1, + }; + + bool SelectionIsCollapsed(); + MOZ_CAN_RUN_SCRIPT nsresult UpdateOneCommand(const char* aCommand); + enum class CommandGroup { + Save, + Style, + Undo, + }; + MOZ_CAN_RUN_SCRIPT void UpdateCommandGroup(CommandGroup aCommandGroup); + + nsCommandManager* GetCommandManager(); + + nsresult PrimeUpdateTimer(); + void TimerCallback(); + + nsCOMPtr<nsITimer> mUpdateTimer; + nsCOMPtr<nsPIDOMWindowOuter> mDOMWindow; + nsCOMPtr<nsIDocShell> mDocShell; + + int8_t mDirtyState; + int8_t mSelectionCollapsed; + bool mFirstDoOfFirstUndo; +}; + +} // namespace mozilla + +#endif // #ifndef mozilla_ComposerCommandsUpdater_h diff --git a/editor/composer/crashtests/351236-1.html b/editor/composer/crashtests/351236-1.html new file mode 100644 index 0000000000..99674f1814 --- /dev/null +++ b/editor/composer/crashtests/351236-1.html @@ -0,0 +1,37 @@ +<html><head> +<title>Testcase bug 351236 - Crash [@ nsGetInterface::operator()] with designMode iframes, removing styles, removing iframes, reloading, etc</title> +<script> +function designmodes(i){ +try { +window.frames[0].document.designMode='on'; +window.frames[0].focus(); +window.frames[0].getSelection().collapse(window.frames[0].document.body.childNodes[0],window.frames[0].document.body.childNodes[0].length-2) +window.frames[0].document.execCommand('inserthtml', false, 'tesxt '); +} catch(e) {} + +setTimeout(designmodes,50); +} + +function removestyles(){ +document.getElementsByTagName('iframe')[0].removeAttribute('style'); +document.getElementsByTagName('q')[0].removeAttribute('style'); +} + +function doe() { +setTimeout(designmodes,200); +setTimeout(removestyles,500); +setTimeout(function() {document.removeChild(document.documentElement);}, 1000); +setTimeout(function() {window.location.reload();}, 1500); +} +window.onload=doe; +</script> + +</head> +<body> +This page should not crash Mozilla within 2 seconds<br> +<q style="display: table-row;"> +<iframe style="display: table-row;"></iframe> +<iframe></iframe> +</q> +</body> +</html> diff --git a/editor/composer/crashtests/407062-1.html b/editor/composer/crashtests/407062-1.html new file mode 100644 index 0000000000..9bd5d02e59 --- /dev/null +++ b/editor/composer/crashtests/407062-1.html @@ -0,0 +1,20 @@ +<html contentEditable="true"> +<head> + +<script type="text/javascript"> + +function boom() +{ + var r = document.documentElement; + while(r.firstChild) + r.firstChild.remove(); + + document.execCommand("contentReadOnly", false, ""); + document.documentElement.focus(); +} + +</script> +</head> + +<body onload="boom();"></body> +</html> diff --git a/editor/composer/crashtests/419563-1.xhtml b/editor/composer/crashtests/419563-1.xhtml new file mode 100644 index 0000000000..417530c13a --- /dev/null +++ b/editor/composer/crashtests/419563-1.xhtml @@ -0,0 +1,20 @@ +<html xmlns="http://www.w3.org/1999/xhtml" class="reftest-wait"> +<head> +<script type="text/javascript"> + +function boom() +{ + document.documentElement.appendChild(document.body); + document.getElementById("s").contentEditable = "true"; + document.getElementById("v").focus(); + document.body.focus(); + document.execCommand("delete", false, null); + document.documentElement.removeAttribute("class"); +} + +</script> +</head> + +<span id="s">thesewords arenot realwords</span><body contenteditable="true" onload="setTimeout(boom, 0);"><span contenteditable="false"><div id="v" contenteditable="true"></div>Five</span></body> + +</html> diff --git a/editor/composer/crashtests/428844-1-inner.xhtml b/editor/composer/crashtests/428844-1-inner.xhtml new file mode 100644 index 0000000000..1cc72d0856 --- /dev/null +++ b/editor/composer/crashtests/428844-1-inner.xhtml @@ -0,0 +1,4 @@ +<?xml-stylesheet type="text/xsl" href="#a"?> +<html xmlns="http://www.w3.org/1999/xhtml" onload="dump('Inner onload\n'); window.location.reload()" contenteditable="true"> +<xsl:stylesheet version="1.0" id="a" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"/> +</html> diff --git a/editor/composer/crashtests/428844-1.html b/editor/composer/crashtests/428844-1.html new file mode 100644 index 0000000000..793aababd4 --- /dev/null +++ b/editor/composer/crashtests/428844-1.html @@ -0,0 +1,17 @@ +<html class="reftest-wait"> +<head> +<script> +function boom() { + var iframe = document.getElementById('inner'); + iframe.addEventListener("load", function() { + document.documentElement.removeAttribute("class"); + }); + iframe.src = "data:text/html,"; + dump("Outer onload\n"); +} +</script> +</head> +<body onload="boom()"> +<iframe src="428844-1-inner.xhtml" id="inner"></iframe> +</body> +</html> diff --git a/editor/composer/crashtests/461049-1.html b/editor/composer/crashtests/461049-1.html new file mode 100644 index 0000000000..fea188e646 --- /dev/null +++ b/editor/composer/crashtests/461049-1.html @@ -0,0 +1,25 @@ +<!DOCTYPE html> +<html> +<head> +<script type="text/javascript"> + +function uu() +{ + document.removeEventListener("DOMSubtreeModified", uu); + document.execCommand("undo", false, null); +} + +function boom() +{ + document.execCommand("selectAll", false, null); + document.execCommand("strikethrough", false, null); + document.addEventListener("DOMSubtreeModified", uu); + document.execCommand("undo", false, null); +} + +</script> +</head> + +<body contenteditable="true" onload="boom();"><div></div></body> + +</html> diff --git a/editor/composer/crashtests/crashtests.list b/editor/composer/crashtests/crashtests.list new file mode 100644 index 0000000000..db84e0e5b1 --- /dev/null +++ b/editor/composer/crashtests/crashtests.list @@ -0,0 +1,6 @@ +load 351236-1.html +load 407062-1.html +load 419563-1.xhtml +load 428844-1.html +load 461049-1.html +load removing-editable-xslt.html diff --git a/editor/composer/crashtests/removing-editable-xslt-inner.xhtml b/editor/composer/crashtests/removing-editable-xslt-inner.xhtml new file mode 100644 index 0000000000..cbf206d7ed --- /dev/null +++ b/editor/composer/crashtests/removing-editable-xslt-inner.xhtml @@ -0,0 +1,4 @@ +<?xml-stylesheet type="text/xsl" href="#a"?> +<html xmlns="http://www.w3.org/1999/xhtml" contenteditable="true"> +<xsl:stylesheet version="1.0" id="a" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"/> +</html> diff --git a/editor/composer/crashtests/removing-editable-xslt.html b/editor/composer/crashtests/removing-editable-xslt.html new file mode 100644 index 0000000000..cbf104ac99 --- /dev/null +++ b/editor/composer/crashtests/removing-editable-xslt.html @@ -0,0 +1,17 @@ +<html class="reftest-wait"> +<head> +<script> +function boom() +{ + document.getElementById("i").src = "removing-editable-xslt-inner.xhtml"; + setTimeout(function() { + document.body.removeChild(document.getElementById("i")); + document.documentElement.removeAttribute("class"); + }, 0); +} +</script> +</head> +<body onload="boom();"> +<iframe id="i"></iframe> +</body> +</html> diff --git a/editor/composer/moz.build b/editor/composer/moz.build new file mode 100644 index 0000000000..38f0506665 --- /dev/null +++ b/editor/composer/moz.build @@ -0,0 +1,60 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# 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/. + +MOCHITEST_MANIFESTS += ["test/mochitest.ini"] + +MOCHITEST_CHROME_MANIFESTS += ["test/chrome.ini"] + +XPIDL_SOURCES += [ + "nsIEditingSession.idl", +] + +XPIDL_MODULE = "composer" + +UNIFIED_SOURCES += [ + "ComposerCommandsUpdater.cpp", + "nsEditingSession.cpp", +] + +EXPORTS += [ + "nsEditingSession.h", +] + +EXPORTS.mozilla += [ + "ComposerCommandsUpdater.h", +] + +# Needed because we include HTMLEditor.h which indirectly includes Document.h +LOCAL_INCLUDES += [ + "/dom/base", + "/dom/html", # For nsHTMLDocument + "/editor/spellchecker", # nsComposeTxtSrvFilter.h + "/layout/style", # For things nsHTMLDocument includes. +] + +FINAL_LIBRARY = "xul" +RESOURCE_FILES += [ + "res/EditorOverride.css", + "res/grabber.gif", + "res/table-add-column-after-active.gif", + "res/table-add-column-after-hover.gif", + "res/table-add-column-after.gif", + "res/table-add-column-before-active.gif", + "res/table-add-column-before-hover.gif", + "res/table-add-column-before.gif", + "res/table-add-row-after-active.gif", + "res/table-add-row-after-hover.gif", + "res/table-add-row-after.gif", + "res/table-add-row-before-active.gif", + "res/table-add-row-before-hover.gif", + "res/table-add-row-before.gif", + "res/table-remove-column-active.gif", + "res/table-remove-column-hover.gif", + "res/table-remove-column.gif", + "res/table-remove-row-active.gif", + "res/table-remove-row-hover.gif", + "res/table-remove-row.gif", +] diff --git a/editor/composer/nsEditingSession.cpp b/editor/composer/nsEditingSession.cpp new file mode 100644 index 0000000000..72435ded45 --- /dev/null +++ b/editor/composer/nsEditingSession.cpp @@ -0,0 +1,1301 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 sw=2 et tw=78: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * 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 <string.h> // for nullptr, strcmp + +#include "imgIContainer.h" // for imgIContainer, etc +#include "mozilla/ComposerCommandsUpdater.h" // for ComposerCommandsUpdater +#include "mozilla/FlushType.h" // for FlushType::Frames +#include "mozilla/HTMLEditor.h" // for HTMLEditor +#include "mozilla/mozalloc.h" // for operator new +#include "mozilla/PresShell.h" // for PresShell +#include "nsAString.h" +#include "nsBaseCommandController.h" // for nsBaseCommandController +#include "nsCommandManager.h" // for nsCommandManager +#include "nsComponentManagerUtils.h" // for do_CreateInstance +#include "nsContentUtils.h" +#include "nsDebug.h" // for NS_ENSURE_SUCCESS, etc +#include "nsDocShell.h" // for nsDocShell +#include "nsEditingSession.h" +#include "nsError.h" // for NS_ERROR_FAILURE, NS_OK, etc +#include "nsIChannel.h" // for nsIChannel +#include "nsIContentViewer.h" // for nsIContentViewer +#include "nsIControllers.h" // for nsIControllers +#include "nsID.h" // for NS_GET_IID, etc +#include "nsHTMLDocument.h" // for nsHTMLDocument +#include "nsIDocShell.h" // for nsIDocShell +#include "mozilla/dom/Document.h" // for Document +#include "nsIEditor.h" // for nsIEditor +#include "nsIInterfaceRequestorUtils.h" // for do_GetInterface +#include "nsIRefreshURI.h" // for nsIRefreshURI +#include "nsIRequest.h" // for nsIRequest +#include "nsITimer.h" // for nsITimer, etc +#include "nsIWeakReference.h" // for nsISupportsWeakReference, etc +#include "nsIWebNavigation.h" // for nsIWebNavigation +#include "nsIWebProgress.h" // for nsIWebProgress, etc +#include "nsLiteralString.h" // for NS_LITERAL_STRING +#include "nsPIDOMWindow.h" // for nsPIDOMWindow +#include "nsPresContext.h" // for nsPresContext +#include "nsReadableUtils.h" // for AppendUTF16toUTF8 +#include "nsStringFwd.h" // for nsString +#include "mozilla/dom/BrowsingContext.h" // for BrowsingContext +#include "mozilla/dom/Selection.h" // for AutoHideSelectionChanges, etc +#include "mozilla/dom/WindowContext.h" // for WindowContext +#include "nsFrameSelection.h" // for nsFrameSelection +#include "nsBaseCommandController.h" // for nsBaseCommandController +#include "mozilla/dom/LoadURIOptionsBinding.h" + +class nsISupports; +class nsIURI; + +using namespace mozilla; +using namespace mozilla::dom; + +/*--------------------------------------------------------------------------- + + nsEditingSession + +----------------------------------------------------------------------------*/ +nsEditingSession::nsEditingSession() + : mDoneSetup(false), + mCanCreateEditor(false), + mInteractive(false), + mMakeWholeDocumentEditable(true), + mDisabledJSAndPlugins(false), + mScriptsEnabled(true), + mPluginsEnabled(true), + mProgressListenerRegistered(false), + mImageAnimationMode(0), + mEditorFlags(0), + mEditorStatus(eEditorOK), + mBaseCommandControllerId(0), + mDocStateControllerId(0), + mHTMLCommandControllerId(0) {} + +/*--------------------------------------------------------------------------- + + ~nsEditingSession + +----------------------------------------------------------------------------*/ +nsEditingSession::~nsEditingSession() { + // Must cancel previous timer? + if (mLoadBlankDocTimer) mLoadBlankDocTimer->Cancel(); +} + +NS_IMPL_ISUPPORTS(nsEditingSession, nsIEditingSession, nsIWebProgressListener, + nsISupportsWeakReference) + +/*--------------------------------------------------------------------------- + + MakeWindowEditable + + aEditorType string, "html" "htmlsimple" "text" "textsimple" + void makeWindowEditable(in nsIDOMWindow aWindow, in string aEditorType, + in boolean aDoAfterUriLoad, + in boolean aMakeWholeDocumentEditable, + in boolean aInteractive); +----------------------------------------------------------------------------*/ +#define DEFAULT_EDITOR_TYPE "html" + +NS_IMETHODIMP +nsEditingSession::MakeWindowEditable(mozIDOMWindowProxy* aWindow, + const char* aEditorType, + bool aDoAfterUriLoad, + bool aMakeWholeDocumentEditable, + bool aInteractive) { + mEditorType.Truncate(); + mEditorFlags = 0; + + NS_ENSURE_TRUE(aWindow, NS_ERROR_FAILURE); + auto* window = nsPIDOMWindowOuter::From(aWindow); + + // disable plugins + nsCOMPtr<nsIDocShell> docShell = window->GetDocShell(); + NS_ENSURE_TRUE(docShell, NS_ERROR_FAILURE); + mDocShell = do_GetWeakReference(docShell); + + mInteractive = aInteractive; + mMakeWholeDocumentEditable = aMakeWholeDocumentEditable; + + nsresult rv; + if (!mInteractive) { + rv = DisableJSAndPlugins(window->GetCurrentInnerWindow()); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Always remove existing editor + TearDownEditorOnWindow(aWindow); + + // Tells embedder that startup is in progress + mEditorStatus = eEditorCreationInProgress; + + // temporary to set editor type here. we will need different classes soon. + if (!aEditorType) aEditorType = DEFAULT_EDITOR_TYPE; + mEditorType = aEditorType; + + // if all this does is setup listeners and I don't need listeners, + // can't this step be ignored?? (based on aDoAfterURILoad) + rv = PrepareForEditing(window); + NS_ENSURE_SUCCESS(rv, rv); + + // set the flag on the docShell to say that it's editable + rv = docShell->MakeEditable(aDoAfterUriLoad); + NS_ENSURE_SUCCESS(rv, rv); + + // Setup commands common to plaintext and html editors, + // including the document creation observers + // the first is an editing controller + rv = SetupEditorCommandController( + nsBaseCommandController::CreateEditingController, aWindow, + static_cast<nsIEditingSession*>(this), &mBaseCommandControllerId); + NS_ENSURE_SUCCESS(rv, rv); + + // The second is a controller to monitor doc state, + // such as creation and "dirty flag" + rv = SetupEditorCommandController( + nsBaseCommandController::CreateHTMLEditorDocStateController, aWindow, + static_cast<nsIEditingSession*>(this), &mDocStateControllerId); + NS_ENSURE_SUCCESS(rv, rv); + + // aDoAfterUriLoad can be false only when making an existing window editable + if (!aDoAfterUriLoad) { + rv = SetupEditorOnWindow(MOZ_KnownLive(*window)); + + // mEditorStatus is set to the error reason + // Since this is used only when editing an existing page, + // it IS ok to destroy current editor + if (NS_FAILED(rv)) { + TearDownEditorOnWindow(aWindow); + } + } + return rv; +} + +nsresult nsEditingSession::DisableJSAndPlugins(nsPIDOMWindowInner* aWindow) { + WindowContext* wc = aWindow->GetWindowContext(); + BrowsingContext* bc = wc->GetBrowsingContext(); + + mScriptsEnabled = wc->GetAllowJavascript(); + + MOZ_TRY(wc->SetAllowJavascript(false)); + + // Disable plugins in this document: + mPluginsEnabled = bc->GetAllowPlugins(); + + MOZ_TRY(bc->SetAllowPlugins(false)); + + mDisabledJSAndPlugins = true; + + return NS_OK; +} + +nsresult nsEditingSession::RestoreJSAndPlugins(nsPIDOMWindowInner* aWindow) { + if (!mDisabledJSAndPlugins) { + return NS_OK; + } + + mDisabledJSAndPlugins = false; + + if (NS_WARN_IF(!aWindow)) { + // DetachFromWindow may call this method with nullptr. + return NS_ERROR_FAILURE; + } + + WindowContext* wc = aWindow->GetWindowContext(); + BrowsingContext* bc = wc->GetBrowsingContext(); + + MOZ_TRY(wc->SetAllowJavascript(mScriptsEnabled)); + + // Disable plugins in this document: + + return bc->SetAllowPlugins(mPluginsEnabled); +} + +/*--------------------------------------------------------------------------- + + WindowIsEditable + + boolean windowIsEditable (in nsIDOMWindow aWindow); +----------------------------------------------------------------------------*/ +NS_IMETHODIMP +nsEditingSession::WindowIsEditable(mozIDOMWindowProxy* aWindow, + bool* outIsEditable) { + NS_ENSURE_STATE(aWindow); + nsCOMPtr<nsIDocShell> docShell = + nsPIDOMWindowOuter::From(aWindow)->GetDocShell(); + NS_ENSURE_STATE(docShell); + + return docShell->GetEditable(outIsEditable); +} + +bool IsSupportedTextType(const nsAString& aMIMEType) { + // These are MIME types that are automatically parsed as "text/plain" + // and thus we can edit them as plaintext + // Note: in older versions, we attempted to convert the mimetype of + // the network channel for these and "text/xml" to "text/plain", + // but further investigation reveals that strategy doesn't work + static constexpr nsLiteralString sSupportedTextTypes[] = { + u"text/plain"_ns, + u"text/css"_ns, + u"text/rdf"_ns, + u"text/xsl"_ns, + u"text/javascript"_ns, // obsolete type + u"text/ecmascript"_ns, // obsolete type + u"application/javascript"_ns, + u"application/ecmascript"_ns, + u"application/x-javascript"_ns, // obsolete type + u"text/xul"_ns // obsolete type + }; + + for (const nsLiteralString& supportedTextType : sSupportedTextTypes) { + if (aMIMEType.Equals(supportedTextType)) { + return true; + } + } + + return false; +} + +nsresult nsEditingSession::SetupEditorOnWindow(nsPIDOMWindowOuter& aWindow) { + mDoneSetup = true; + + // MIME CHECKING + // must get the content type + // Note: the doc gets this from the network channel during StartPageLoad, + // so we don't have to get it from there ourselves + nsAutoString mimeType; + + // then lets check the mime type + if (RefPtr<Document> doc = aWindow.GetDoc()) { + doc->GetContentType(mimeType); + + if (IsSupportedTextType(mimeType)) { + mEditorType.AssignLiteral("text"); + mimeType.AssignLiteral("text/plain"); + } else if (!doc->IsHTMLOrXHTML()) { + // Neither an acceptable text or html type. + mEditorStatus = eEditorErrorCantEditMimeType; + + // Turn editor into HTML -- we will load blank page later + mEditorType.AssignLiteral("html"); + mimeType.AssignLiteral("text/html"); + } + + // Flush out frame construction to make sure that the subframe's + // presshell is set up if it needs to be. + doc->FlushPendingNotifications(mozilla::FlushType::Frames); + if (mMakeWholeDocumentEditable) { + doc->SetEditableFlag(true); + // Enable usage of the execCommand API + doc->SetEditingState(Document::EditingState::eDesignMode); + } + } + bool needHTMLController = false; + + if (mEditorType.EqualsLiteral("textmail")) { + mEditorFlags = nsIEditor::eEditorPlaintextMask | + nsIEditor::eEditorEnableWrapHackMask | + nsIEditor::eEditorMailMask; + } else if (mEditorType.EqualsLiteral("text")) { + mEditorFlags = + nsIEditor::eEditorPlaintextMask | nsIEditor::eEditorEnableWrapHackMask; + } else if (mEditorType.EqualsLiteral("htmlmail")) { + if (mimeType.EqualsLiteral("text/html")) { + needHTMLController = true; + mEditorFlags = nsIEditor::eEditorMailMask; + } else { + // Set the flags back to textplain. + mEditorFlags = nsIEditor::eEditorPlaintextMask | + nsIEditor::eEditorEnableWrapHackMask; + } + } else { + // Defaulted to html + needHTMLController = true; + } + + if (mInteractive) { + mEditorFlags |= nsIEditor::eEditorAllowInteraction; + } + + // make the UI state maintainer + RefPtr<ComposerCommandsUpdater> commandsUpdater = + new ComposerCommandsUpdater(); + mComposerCommandsUpdater = commandsUpdater; + + // now init the state maintainer + // This allows notification of error state + // even if we don't create an editor + commandsUpdater->Init(aWindow); + + if (mEditorStatus != eEditorCreationInProgress) { + commandsUpdater->OnHTMLEditorCreated(); + + // At this point we have made a final decision that we don't support + // editing the current document. This is an internal failure state, but + // we return NS_OK to avoid throwing an exception from the designMode + // setter for web compatibility. The document editing APIs will tell the + // developer if editing has been disabled because we're in a document type + // that doesn't support editing. + return NS_OK; + } + + // Create editor and do other things + // only if we haven't found some error above, + const RefPtr<nsDocShell> docShell = nsDocShell::Cast(aWindow.GetDocShell()); + if (NS_WARN_IF(!docShell)) { + return NS_ERROR_FAILURE; + } + const RefPtr<PresShell> presShell = docShell->GetPresShell(); + if (NS_WARN_IF(!presShell)) { + return NS_ERROR_FAILURE; + } + + if (!mInteractive) { + // Disable animation of images in this document: + nsPresContext* presContext = presShell->GetPresContext(); + NS_ENSURE_TRUE(presContext, NS_ERROR_FAILURE); + + mImageAnimationMode = presContext->ImageAnimationMode(); + presContext->SetImageAnimationMode(imgIContainer::kDontAnimMode); + } + + // Hide selection changes during initialization, in order to hide this + // from web pages. + RefPtr<nsFrameSelection> fs = presShell->FrameSelection(); + NS_ENSURE_TRUE(fs, NS_ERROR_FAILURE); + AutoHideSelectionChanges hideSelectionChanges(fs); + + nsCOMPtr<nsIContentViewer> contentViewer; + nsresult rv = docShell->GetContentViewer(getter_AddRefs(contentViewer)); + if (NS_FAILED(rv) || NS_WARN_IF(!contentViewer)) { + NS_WARNING("nsDocShell::GetContentViewer() failed"); + return rv; + } + + const RefPtr<Document> doc = contentViewer->GetDocument(); + if (NS_WARN_IF(!doc)) { + return NS_ERROR_FAILURE; + } + + // create and set editor + // Try to reuse an existing editor + nsCOMPtr<nsIEditor> editor = do_QueryReferent(mExistingEditor); + RefPtr<HTMLEditor> htmlEditor = HTMLEditor::GetFrom(editor); + MOZ_ASSERT_IF(editor, htmlEditor); + if (htmlEditor) { + htmlEditor->PreDestroy(); + } else { + htmlEditor = new HTMLEditor(*doc); + mExistingEditor = + do_GetWeakReference(static_cast<nsIEditor*>(htmlEditor.get())); + } + // set the editor on the docShell. The docShell now owns it. + rv = docShell->SetHTMLEditor(htmlEditor); + NS_ENSURE_SUCCESS(rv, rv); + + // setup the HTML editor command controller + if (needHTMLController) { + // The third controller takes an nsIEditor as the context + rv = SetupEditorCommandController( + nsBaseCommandController::CreateHTMLEditorController, &aWindow, + static_cast<nsIEditor*>(htmlEditor), &mHTMLCommandControllerId); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Set mimetype on editor + rv = htmlEditor->SetContentsMIMEType(mimeType); + NS_ENSURE_SUCCESS(rv, rv); + + MOZ_ASSERT(docShell->HasContentViewer()); + MOZ_ASSERT(contentViewer->GetDocument()); + + MOZ_DIAGNOSTIC_ASSERT(commandsUpdater == mComposerCommandsUpdater); + if (MOZ_UNLIKELY(commandsUpdater != mComposerCommandsUpdater)) { + commandsUpdater = mComposerCommandsUpdater; + } + rv = htmlEditor->Init(*doc, *commandsUpdater, mEditorFlags); + NS_ENSURE_SUCCESS(rv, rv); + + RefPtr<Selection> selection = htmlEditor->GetSelection(); + if (NS_WARN_IF(!selection)) { + return NS_ERROR_FAILURE; + } + + // Set context on all controllers to be the editor + rv = SetEditorOnControllers(aWindow, htmlEditor); + NS_ENSURE_SUCCESS(rv, rv); + + // Everything went fine! + mEditorStatus = eEditorOK; + + // This will trigger documentCreation notification + return htmlEditor->PostCreate(); +} + +// Removes all listeners and controllers from aWindow and aEditor. +void nsEditingSession::RemoveListenersAndControllers( + nsPIDOMWindowOuter* aWindow, HTMLEditor* aHTMLEditor) { + if (!mComposerCommandsUpdater || !aHTMLEditor) { + return; + } + + // Remove all the listeners + RefPtr<ComposerCommandsUpdater> composertCommandsUpdater = + std::move(mComposerCommandsUpdater); + MOZ_ASSERT(!mComposerCommandsUpdater); + aHTMLEditor->Detach(*composertCommandsUpdater); + + // Remove editor controllers from the window now that we're not + // editing in that window any more. + RemoveEditorControllers(aWindow); +} + +/*--------------------------------------------------------------------------- + + TearDownEditorOnWindow + + void tearDownEditorOnWindow (in nsIDOMWindow aWindow); +----------------------------------------------------------------------------*/ +NS_IMETHODIMP +nsEditingSession::TearDownEditorOnWindow(mozIDOMWindowProxy* aWindow) { + if (!mDoneSetup) { + return NS_OK; + } + + NS_ENSURE_TRUE(aWindow, NS_ERROR_NULL_POINTER); + + // Kill any existing reload timer + if (mLoadBlankDocTimer) { + mLoadBlankDocTimer->Cancel(); + mLoadBlankDocTimer = nullptr; + } + + mDoneSetup = false; + + // Check if we're turning off editing (from contentEditable or designMode). + auto* window = nsPIDOMWindowOuter::From(aWindow); + + RefPtr<Document> doc = window->GetDoc(); + bool stopEditing = doc && doc->IsEditingOn(); + if (stopEditing) { + RemoveWebProgressListener(window); + } + + nsCOMPtr<nsIDocShell> docShell = window->GetDocShell(); + NS_ENSURE_STATE(docShell); + + RefPtr<HTMLEditor> htmlEditor = docShell->GetHTMLEditor(); + if (stopEditing) { + doc->TearingDownEditor(); + } + + if (mComposerCommandsUpdater && htmlEditor) { + // Null out the editor on the controllers first to prevent their weak + // references from pointing to a destroyed editor. + SetEditorOnControllers(*window, nullptr); + } + + // Null out the editor on the docShell to trigger PreDestroy which + // needs to happen before document state listeners are removed below. + docShell->SetEditor(nullptr); + + RemoveListenersAndControllers(window, htmlEditor); + + if (stopEditing) { + // Make things the way they were before we started editing. + RestoreJSAndPlugins(window->GetCurrentInnerWindow()); + RestoreAnimationMode(window); + + if (mMakeWholeDocumentEditable) { + doc->SetEditableFlag(false); + doc->SetEditingState(Document::EditingState::eOff); + } + } + + return NS_OK; +} + +/*--------------------------------------------------------------------------- + + GetEditorForFrame + + nsIEditor getEditorForFrame (in nsIDOMWindow aWindow); +----------------------------------------------------------------------------*/ +NS_IMETHODIMP +nsEditingSession::GetEditorForWindow(mozIDOMWindowProxy* aWindow, + nsIEditor** outEditor) { + if (NS_WARN_IF(!aWindow)) { + return NS_ERROR_INVALID_ARG; + } + nsCOMPtr<nsIEditor> editor = GetHTMLEditorForWindow(aWindow); + editor.forget(outEditor); + return NS_OK; +} + +/*--------------------------------------------------------------------------- + + OnStateChange + +----------------------------------------------------------------------------*/ +NS_IMETHODIMP +nsEditingSession::OnStateChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, uint32_t aStateFlags, + nsresult aStatus) { +#ifdef NOISY_DOC_LOADING + nsCOMPtr<nsIChannel> channel(do_QueryInterface(aRequest)); + if (channel) { + nsAutoCString contentType; + channel->GetContentType(contentType); + if (!contentType.IsEmpty()) { + printf(" ++++++ MIMETYPE = %s\n", contentType.get()); + } + } +#endif + + // + // A Request has started... + // + if (aStateFlags & nsIWebProgressListener::STATE_START) { +#ifdef NOISY_DOC_LOADING + { + nsCOMPtr<nsIChannel> channel(do_QueryInterface(aRequest)); + if (channel) { + nsCOMPtr<nsIURI> uri; + channel->GetURI(getter_AddRefs(uri)); + if (uri) { + nsCString spec; + uri->GetSpec(spec); + printf(" **** STATE_START: CHANNEL URI=%s, flags=%x\n", spec.get(), + aStateFlags); + } + } else { + printf(" STATE_START: NO CHANNEL flags=%x\n", aStateFlags); + } + } +#endif + // Page level notification... + if (aStateFlags & nsIWebProgressListener::STATE_IS_NETWORK) { + nsCOMPtr<nsIChannel> channel(do_QueryInterface(aRequest)); + StartPageLoad(channel); +#ifdef NOISY_DOC_LOADING + printf("STATE_START & STATE_IS_NETWORK flags=%x\n", aStateFlags); +#endif + } + + // Document level notification... + if (aStateFlags & nsIWebProgressListener::STATE_IS_DOCUMENT && + !(aStateFlags & nsIWebProgressListener::STATE_RESTORING)) { +#ifdef NOISY_DOC_LOADING + printf("STATE_START & STATE_IS_DOCUMENT flags=%x\n", aStateFlags); +#endif + + bool progressIsForTargetDocument = + IsProgressForTargetDocument(aWebProgress); + + if (progressIsForTargetDocument) { + nsCOMPtr<mozIDOMWindowProxy> window; + aWebProgress->GetDOMWindow(getter_AddRefs(window)); + + auto* piWindow = nsPIDOMWindowOuter::From(window); + RefPtr<Document> doc = piWindow->GetDoc(); + nsHTMLDocument* htmlDoc = + doc && doc->IsHTMLOrXHTML() ? doc->AsHTMLDocument() : nullptr; + if (htmlDoc && doc->IsWriting()) { + nsAutoString designMode; + htmlDoc->GetDesignMode(designMode); + + if (designMode.EqualsLiteral("on")) { + // This notification is for data coming in through + // document.open/write/close(), ignore it. + + return NS_OK; + } + } + + mCanCreateEditor = true; + StartDocumentLoad(aWebProgress, progressIsForTargetDocument); + } + } + } + // + // A Request is being processed + // + else if (aStateFlags & nsIWebProgressListener::STATE_TRANSFERRING) { + if (aStateFlags & nsIWebProgressListener::STATE_IS_DOCUMENT) { + // document transfer started + } + } + // + // Got a redirection + // + else if (aStateFlags & nsIWebProgressListener::STATE_REDIRECTING) { + if (aStateFlags & nsIWebProgressListener::STATE_IS_DOCUMENT) { + // got a redirect + } + } + // + // A network or document Request has finished... + // + else if (aStateFlags & nsIWebProgressListener::STATE_STOP) { +#ifdef NOISY_DOC_LOADING + { + nsCOMPtr<nsIChannel> channel(do_QueryInterface(aRequest)); + if (channel) { + nsCOMPtr<nsIURI> uri; + channel->GetURI(getter_AddRefs(uri)); + if (uri) { + nsCString spec; + uri->GetSpec(spec); + printf(" **** STATE_STOP: CHANNEL URI=%s, flags=%x\n", spec.get(), + aStateFlags); + } + } else { + printf(" STATE_STOP: NO CHANNEL flags=%x\n", aStateFlags); + } + } +#endif + + // Document level notification... + if (aStateFlags & nsIWebProgressListener::STATE_IS_DOCUMENT) { + nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest); + EndDocumentLoad(aWebProgress, channel, aStatus, + IsProgressForTargetDocument(aWebProgress)); +#ifdef NOISY_DOC_LOADING + printf("STATE_STOP & STATE_IS_DOCUMENT flags=%x\n", aStateFlags); +#endif + } + + // Page level notification... + if (aStateFlags & nsIWebProgressListener::STATE_IS_NETWORK) { + nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest); + (void)EndPageLoad(aWebProgress, channel, aStatus); +#ifdef NOISY_DOC_LOADING + printf("STATE_STOP & STATE_IS_NETWORK flags=%x\n", aStateFlags); +#endif + } + } + + return NS_OK; +} + +/*--------------------------------------------------------------------------- + + OnProgressChange + +----------------------------------------------------------------------------*/ +NS_IMETHODIMP +nsEditingSession::OnProgressChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + int32_t aCurSelfProgress, + int32_t aMaxSelfProgress, + int32_t aCurTotalProgress, + int32_t aMaxTotalProgress) { + MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)"); + return NS_OK; +} + +/*--------------------------------------------------------------------------- + + OnLocationChange + +----------------------------------------------------------------------------*/ +NS_IMETHODIMP +nsEditingSession::OnLocationChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, nsIURI* aURI, + uint32_t aFlags) { + nsCOMPtr<mozIDOMWindowProxy> domWindow; + nsresult rv = aWebProgress->GetDOMWindow(getter_AddRefs(domWindow)); + NS_ENSURE_SUCCESS(rv, rv); + + auto* piWindow = nsPIDOMWindowOuter::From(domWindow); + + RefPtr<Document> doc = piWindow->GetDoc(); + NS_ENSURE_TRUE(doc, NS_ERROR_FAILURE); + + doc->SetDocumentURI(aURI); + + // Notify the location-changed observer that + // the document URL has changed + nsIDocShell* docShell = piWindow->GetDocShell(); + NS_ENSURE_TRUE(docShell, NS_ERROR_FAILURE); + + RefPtr<nsCommandManager> commandManager = docShell->GetCommandManager(); + commandManager->CommandStatusChanged("obs_documentLocationChanged"); + return NS_OK; +} + +/*--------------------------------------------------------------------------- + + OnStatusChange + +----------------------------------------------------------------------------*/ +NS_IMETHODIMP +nsEditingSession::OnStatusChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, nsresult aStatus, + const char16_t* aMessage) { + MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)"); + return NS_OK; +} + +/*--------------------------------------------------------------------------- + + OnSecurityChange + +----------------------------------------------------------------------------*/ +NS_IMETHODIMP +nsEditingSession::OnSecurityChange(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, uint32_t aState) { + MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)"); + return NS_OK; +} + +/*--------------------------------------------------------------------------- + + OnContentBlockingEvent + +----------------------------------------------------------------------------*/ +NS_IMETHODIMP +nsEditingSession::OnContentBlockingEvent(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, + uint32_t aEvent) { + MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)"); + return NS_OK; +} + +/*--------------------------------------------------------------------------- + + IsProgressForTargetDocument + + Check that this notification is for our document. +----------------------------------------------------------------------------*/ + +bool nsEditingSession::IsProgressForTargetDocument( + nsIWebProgress* aWebProgress) { + nsCOMPtr<nsIWebProgress> editedWebProgress = do_QueryReferent(mDocShell); + return editedWebProgress == aWebProgress; +} + +/*--------------------------------------------------------------------------- + + GetEditorStatus + + Called during GetCommandStateParams("obs_documentCreated"...) + to determine if editor was created and document + was loaded successfully +----------------------------------------------------------------------------*/ +NS_IMETHODIMP +nsEditingSession::GetEditorStatus(uint32_t* aStatus) { + NS_ENSURE_ARG_POINTER(aStatus); + *aStatus = mEditorStatus; + return NS_OK; +} + +/*--------------------------------------------------------------------------- + + StartDocumentLoad + + Called on start of load in a single frame +----------------------------------------------------------------------------*/ +nsresult nsEditingSession::StartDocumentLoad(nsIWebProgress* aWebProgress, + bool aIsToBeMadeEditable) { +#ifdef NOISY_DOC_LOADING + printf("======= StartDocumentLoad ========\n"); +#endif + + NS_ENSURE_ARG_POINTER(aWebProgress); + + if (aIsToBeMadeEditable) { + mEditorStatus = eEditorCreationInProgress; + } + + return NS_OK; +} + +/*--------------------------------------------------------------------------- + + EndDocumentLoad + + Called on end of load in a single frame +----------------------------------------------------------------------------*/ +nsresult nsEditingSession::EndDocumentLoad(nsIWebProgress* aWebProgress, + nsIChannel* aChannel, + nsresult aStatus, + bool aIsToBeMadeEditable) { + NS_ENSURE_ARG_POINTER(aWebProgress); + +#ifdef NOISY_DOC_LOADING + printf("======= EndDocumentLoad ========\n"); + printf("with status %d, ", aStatus); + nsCOMPtr<nsIURI> uri; + nsCString spec; + if (NS_SUCCEEDED(aChannel->GetURI(getter_AddRefs(uri)))) { + uri->GetSpec(spec); + printf(" uri %s\n", spec.get()); + } +#endif + + // We want to call the base class EndDocumentLoad, + // but avoid some of the stuff + // that nsDocShell does (need to refactor). + + // OK, time to make an editor on this document + nsCOMPtr<mozIDOMWindowProxy> domWindow; + aWebProgress->GetDOMWindow(getter_AddRefs(domWindow)); + NS_ENSURE_TRUE(domWindow, NS_ERROR_FAILURE); + + // Set the error state -- we will create an editor + // anyway and load empty doc later + if (aIsToBeMadeEditable && aStatus == NS_ERROR_FILE_NOT_FOUND) { + mEditorStatus = eEditorErrorFileNotFound; + } + + auto* window = nsPIDOMWindowOuter::From(domWindow); + nsIDocShell* docShell = window->GetDocShell(); + NS_ENSURE_TRUE(docShell, NS_ERROR_FAILURE); // better error handling? + + // cancel refresh from meta tags + // we need to make sure that all pages in editor (whether editable or not) + // can't refresh contents being edited + nsCOMPtr<nsIRefreshURI> refreshURI = do_QueryInterface(docShell); + if (refreshURI) { + refreshURI->CancelRefreshURITimers(); + } + + nsresult rv = NS_OK; + + // did someone set the flag to make this shell editable? + if (aIsToBeMadeEditable && mCanCreateEditor) { + bool makeEditable; + docShell->GetEditable(&makeEditable); + + if (makeEditable) { + // To keep pre Gecko 1.9 behavior, setup editor always when + // mMakeWholeDocumentEditable. + bool needsSetup = false; + if (mMakeWholeDocumentEditable) { + needsSetup = true; + } else { + // do we already have an editor here? + needsSetup = !docShell->GetHTMLEditor(); + } + + if (needsSetup) { + mCanCreateEditor = false; + rv = SetupEditorOnWindow(MOZ_KnownLive(*window)); + if (NS_FAILED(rv)) { + // If we had an error, setup timer to load a blank page later + if (mLoadBlankDocTimer) { + // Must cancel previous timer? + mLoadBlankDocTimer->Cancel(); + mLoadBlankDocTimer = nullptr; + } + + rv = NS_NewTimerWithFuncCallback(getter_AddRefs(mLoadBlankDocTimer), + nsEditingSession::TimerCallback, + static_cast<void*>(mDocShell.get()), + 10, nsITimer::TYPE_ONE_SHOT, + "nsEditingSession::EndDocumentLoad"); + NS_ENSURE_SUCCESS(rv, rv); + + mEditorStatus = eEditorCreationInProgress; + } + } + } + } + return rv; +} + +void nsEditingSession::TimerCallback(nsITimer* aTimer, void* aClosure) { + nsCOMPtr<nsIDocShell> docShell = + do_QueryReferent(static_cast<nsIWeakReference*>(aClosure)); + if (docShell) { + nsCOMPtr<nsIWebNavigation> webNav(do_QueryInterface(docShell)); + if (webNav) { + LoadURIOptions loadURIOptions; + loadURIOptions.mTriggeringPrincipal = + nsContentUtils::GetSystemPrincipal(); + nsCOMPtr<nsIURI> uri; + MOZ_ALWAYS_SUCCEEDS(NS_NewURI(getter_AddRefs(uri), "about:blank"_ns)); + webNav->LoadURI(uri, loadURIOptions); + } + } +} + +/*--------------------------------------------------------------------------- + + StartPageLoad + + Called on start load of the entire page (incl. subframes) +----------------------------------------------------------------------------*/ +nsresult nsEditingSession::StartPageLoad(nsIChannel* aChannel) { +#ifdef NOISY_DOC_LOADING + printf("======= StartPageLoad ========\n"); +#endif + return NS_OK; +} + +/*--------------------------------------------------------------------------- + + EndPageLoad + + Called on end load of the entire page (incl. subframes) +----------------------------------------------------------------------------*/ +nsresult nsEditingSession::EndPageLoad(nsIWebProgress* aWebProgress, + nsIChannel* aChannel, nsresult aStatus) { +#ifdef NOISY_DOC_LOADING + printf("======= EndPageLoad ========\n"); + printf(" with status %d, ", aStatus); + nsCOMPtr<nsIURI> uri; + nsCString spec; + if (NS_SUCCEEDED(aChannel->GetURI(getter_AddRefs(uri)))) { + uri->GetSpec(spec); + printf("uri %s\n", spec.get()); + } + + nsAutoCString contentType; + aChannel->GetContentType(contentType); + if (!contentType.IsEmpty()) { + printf(" flags = %d, status = %d, MIMETYPE = %s\n", mEditorFlags, + mEditorStatus, contentType.get()); + } +#endif + + // Set the error state -- we will create an editor anyway + // and load empty doc later + if (aStatus == NS_ERROR_FILE_NOT_FOUND) { + mEditorStatus = eEditorErrorFileNotFound; + } + + nsCOMPtr<mozIDOMWindowProxy> domWindow; + aWebProgress->GetDOMWindow(getter_AddRefs(domWindow)); + + nsIDocShell* docShell = + domWindow ? nsPIDOMWindowOuter::From(domWindow)->GetDocShell() : nullptr; + NS_ENSURE_TRUE(docShell, NS_ERROR_FAILURE); + + // cancel refresh from meta tags + // we need to make sure that all pages in editor (whether editable or not) + // can't refresh contents being edited + nsCOMPtr<nsIRefreshURI> refreshURI = do_QueryInterface(docShell); + if (refreshURI) { + refreshURI->CancelRefreshURITimers(); + } + +#if 0 + // Shouldn't we do this when we want to edit sub-frames? + return MakeWindowEditable(domWindow, "html", false, mInteractive); +#else + return NS_OK; +#endif +} + +/*--------------------------------------------------------------------------- + + PrepareForEditing + + Set up this editing session for one or more editors +----------------------------------------------------------------------------*/ +nsresult nsEditingSession::PrepareForEditing(nsPIDOMWindowOuter* aWindow) { + if (mProgressListenerRegistered) { + return NS_OK; + } + + nsIDocShell* docShell = aWindow ? aWindow->GetDocShell() : nullptr; + + // register callback + nsCOMPtr<nsIWebProgress> webProgress = do_GetInterface(docShell); + NS_ENSURE_TRUE(webProgress, NS_ERROR_FAILURE); + + nsresult rv = webProgress->AddProgressListener( + this, (nsIWebProgress::NOTIFY_STATE_NETWORK | + nsIWebProgress::NOTIFY_STATE_DOCUMENT | + nsIWebProgress::NOTIFY_LOCATION)); + + mProgressListenerRegistered = NS_SUCCEEDED(rv); + + return rv; +} + +/*--------------------------------------------------------------------------- + + SetupEditorCommandController + + Create a command controller, append to controllers, + get and return the controller ID, and set the context +----------------------------------------------------------------------------*/ +nsresult nsEditingSession::SetupEditorCommandController( + nsEditingSession::ControllerCreatorFn aControllerCreatorFn, + mozIDOMWindowProxy* aWindow, nsISupports* aContext, + uint32_t* aControllerId) { + NS_ENSURE_ARG_POINTER(aControllerCreatorFn); + NS_ENSURE_ARG_POINTER(aWindow); + NS_ENSURE_ARG_POINTER(aContext); + NS_ENSURE_ARG_POINTER(aControllerId); + + auto* piWindow = nsPIDOMWindowOuter::From(aWindow); + MOZ_ASSERT(piWindow); + + nsCOMPtr<nsIControllers> controllers; + nsresult rv = piWindow->GetControllers(getter_AddRefs(controllers)); + NS_ENSURE_SUCCESS(rv, rv); + + // We only have to create each singleton controller once + // We know this has happened once we have a controllerId value + if (!*aControllerId) { + RefPtr<nsBaseCommandController> commandController = aControllerCreatorFn(); + NS_ENSURE_TRUE(commandController, NS_ERROR_FAILURE); + + // We must insert at head of the list to be sure our + // controller is found before other implementations + // (e.g., not-implemented versions by browser) + rv = controllers->InsertControllerAt(0, commandController); + NS_ENSURE_SUCCESS(rv, rv); + + // Remember the ID for the controller + rv = controllers->GetControllerId(commandController, aControllerId); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Set the context + return SetContextOnControllerById(controllers, aContext, *aControllerId); +} + +nsresult nsEditingSession::SetEditorOnControllers(nsPIDOMWindowOuter& aWindow, + HTMLEditor* aEditor) { + nsCOMPtr<nsIControllers> controllers; + nsresult rv = aWindow.GetControllers(getter_AddRefs(controllers)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsISupports> editorAsISupports = static_cast<nsIEditor*>(aEditor); + if (mBaseCommandControllerId) { + rv = SetContextOnControllerById(controllers, editorAsISupports, + mBaseCommandControllerId); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (mDocStateControllerId) { + rv = SetContextOnControllerById(controllers, editorAsISupports, + mDocStateControllerId); + NS_ENSURE_SUCCESS(rv, rv); + } + + if (mHTMLCommandControllerId) { + rv = SetContextOnControllerById(controllers, editorAsISupports, + mHTMLCommandControllerId); + } + + return rv; +} + +nsresult nsEditingSession::SetContextOnControllerById( + nsIControllers* aControllers, nsISupports* aContext, uint32_t aID) { + NS_ENSURE_ARG_POINTER(aControllers); + + // aContext can be null (when destroying editor) + nsCOMPtr<nsIController> controller; + aControllers->GetControllerById(aID, getter_AddRefs(controller)); + + // ok with nil controller + nsCOMPtr<nsIControllerContext> editorController = + do_QueryInterface(controller); + NS_ENSURE_TRUE(editorController, NS_ERROR_FAILURE); + + return editorController->SetCommandContext(aContext); +} + +void nsEditingSession::RemoveEditorControllers(nsPIDOMWindowOuter* aWindow) { + // Remove editor controllers from the aWindow, call when we're + // tearing down/detaching editor. + + nsCOMPtr<nsIControllers> controllers; + if (aWindow) { + aWindow->GetControllers(getter_AddRefs(controllers)); + } + + if (controllers) { + nsCOMPtr<nsIController> controller; + if (mBaseCommandControllerId) { + controllers->GetControllerById(mBaseCommandControllerId, + getter_AddRefs(controller)); + if (controller) { + controllers->RemoveController(controller); + } + } + + if (mDocStateControllerId) { + controllers->GetControllerById(mDocStateControllerId, + getter_AddRefs(controller)); + if (controller) { + controllers->RemoveController(controller); + } + } + + if (mHTMLCommandControllerId) { + controllers->GetControllerById(mHTMLCommandControllerId, + getter_AddRefs(controller)); + if (controller) { + controllers->RemoveController(controller); + } + } + } + + // Clear IDs to trigger creation of new controllers. + mBaseCommandControllerId = 0; + mDocStateControllerId = 0; + mHTMLCommandControllerId = 0; +} + +void nsEditingSession::RemoveWebProgressListener(nsPIDOMWindowOuter* aWindow) { + nsIDocShell* docShell = aWindow ? aWindow->GetDocShell() : nullptr; + nsCOMPtr<nsIWebProgress> webProgress = do_GetInterface(docShell); + if (webProgress) { + webProgress->RemoveProgressListener(this); + mProgressListenerRegistered = false; + } +} + +void nsEditingSession::RestoreAnimationMode(nsPIDOMWindowOuter* aWindow) { + if (mInteractive) { + return; + } + + nsCOMPtr<nsIDocShell> docShell = aWindow ? aWindow->GetDocShell() : nullptr; + NS_ENSURE_TRUE_VOID(docShell); + RefPtr<PresShell> presShell = docShell->GetPresShell(); + if (NS_WARN_IF(!presShell)) { + return; + } + nsPresContext* presContext = presShell->GetPresContext(); + NS_ENSURE_TRUE_VOID(presContext); + + presContext->SetImageAnimationMode(mImageAnimationMode); +} + +nsresult nsEditingSession::DetachFromWindow(nsPIDOMWindowOuter* aWindow) { + NS_ENSURE_TRUE(mDoneSetup, NS_OK); + + NS_ASSERTION(mComposerCommandsUpdater, + "mComposerCommandsUpdater should exist."); + + // Kill any existing reload timer + if (mLoadBlankDocTimer) { + mLoadBlankDocTimer->Cancel(); + mLoadBlankDocTimer = nullptr; + } + + // Remove controllers, webprogress listener, and otherwise + // make things the way they were before we started editing. + RemoveEditorControllers(aWindow); + RemoveWebProgressListener(aWindow); + RestoreJSAndPlugins(aWindow->GetCurrentInnerWindow()); + RestoreAnimationMode(aWindow); + + // Kill our weak reference to our original window, in case + // it changes on restore, or otherwise dies. + mDocShell = nullptr; + + return NS_OK; +} + +nsresult nsEditingSession::ReattachToWindow(nsPIDOMWindowOuter* aWindow) { + NS_ENSURE_TRUE(mDoneSetup, NS_OK); + NS_ENSURE_TRUE(aWindow, NS_ERROR_FAILURE); + + NS_ASSERTION(mComposerCommandsUpdater, + "mComposerCommandsUpdater should exist."); + + // Imitate nsEditorDocShell::MakeEditable() to reattach the + // old editor to the window. + nsresult rv; + + nsIDocShell* docShell = aWindow->GetDocShell(); + NS_ENSURE_TRUE(docShell, NS_ERROR_FAILURE); + mDocShell = do_GetWeakReference(docShell); + + // Disable plugins. + if (!mInteractive) { + rv = DisableJSAndPlugins(aWindow->GetCurrentInnerWindow()); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Tells embedder that startup is in progress. + mEditorStatus = eEditorCreationInProgress; + + // Adds back web progress listener. + rv = PrepareForEditing(aWindow); + NS_ENSURE_SUCCESS(rv, rv); + + // Setup the command controllers again. + rv = SetupEditorCommandController( + nsBaseCommandController::CreateEditingController, aWindow, + static_cast<nsIEditingSession*>(this), &mBaseCommandControllerId); + NS_ENSURE_SUCCESS(rv, rv); + + rv = SetupEditorCommandController( + nsBaseCommandController::CreateHTMLEditorDocStateController, aWindow, + static_cast<nsIEditingSession*>(this), &mDocStateControllerId); + NS_ENSURE_SUCCESS(rv, rv); + + if (mComposerCommandsUpdater) { + mComposerCommandsUpdater->Init(*aWindow); + } + + // Get editor + RefPtr<HTMLEditor> htmlEditor = GetHTMLEditorForWindow(aWindow); + if (NS_WARN_IF(!htmlEditor)) { + return NS_ERROR_FAILURE; + } + + if (!mInteractive) { + // Disable animation of images in this document: + RefPtr<PresShell> presShell = docShell->GetPresShell(); + if (NS_WARN_IF(!presShell)) { + return NS_ERROR_FAILURE; + } + nsPresContext* presContext = presShell->GetPresContext(); + NS_ENSURE_TRUE(presContext, NS_ERROR_FAILURE); + + mImageAnimationMode = presContext->ImageAnimationMode(); + presContext->SetImageAnimationMode(imgIContainer::kDontAnimMode); + } + + // The third controller takes an nsIEditor as the context + rv = SetupEditorCommandController( + nsBaseCommandController::CreateHTMLEditorController, aWindow, + static_cast<nsIEditor*>(htmlEditor.get()), &mHTMLCommandControllerId); + NS_ENSURE_SUCCESS(rv, rv); + + // Set context on all controllers to be the editor + rv = SetEditorOnControllers(*aWindow, htmlEditor); + NS_ENSURE_SUCCESS(rv, rv); + +#ifdef DEBUG + { + bool isEditable; + rv = WindowIsEditable(aWindow, &isEditable); + NS_ENSURE_SUCCESS(rv, rv); + NS_ASSERTION(isEditable, + "Window is not editable after reattaching editor."); + } +#endif // DEBUG + + return NS_OK; +} + +HTMLEditor* nsIEditingSession::GetHTMLEditorForWindow( + mozIDOMWindowProxy* aWindow) { + if (NS_WARN_IF(!aWindow)) { + return nullptr; + } + + nsCOMPtr<nsIDocShell> docShell = + nsPIDOMWindowOuter::From(aWindow)->GetDocShell(); + if (NS_WARN_IF(!docShell)) { + return nullptr; + } + + return docShell->GetHTMLEditor(); +} diff --git a/editor/composer/nsEditingSession.h b/editor/composer/nsEditingSession.h new file mode 100644 index 0000000000..c808c71c79 --- /dev/null +++ b/editor/composer/nsEditingSession.h @@ -0,0 +1,172 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +#ifndef nsEditingSession_h__ +#define nsEditingSession_h__ + +#include "nsCOMPtr.h" // for nsCOMPtr +#include "nsISupportsImpl.h" // for NS_DECL_ISUPPORTS +#include "nsIWeakReferenceUtils.h" // for nsWeakPtr +#include "nsWeakReference.h" // for nsSupportsWeakReference, etc +#include "nscore.h" // for nsresult + +#ifndef __gen_nsIWebProgressListener_h__ +# include "nsIWebProgressListener.h" +#endif + +#ifndef __gen_nsIEditingSession_h__ +# include "nsIEditingSession.h" // for NS_DECL_NSIEDITINGSESSION, etc +#endif + +#include "nsString.h" // for nsCString + +class mozIDOMWindowProxy; +class nsBaseCommandController; +class nsIDOMWindow; +class nsISupports; +class nsITimer; +class nsIChannel; +class nsIControllers; +class nsIDocShell; +class nsIWebProgress; +class nsIPIDOMWindowOuter; +class nsIPIDOMWindowInner; + +namespace mozilla { +class ComposerCommandsUpdater; +class HTMLEditor; +} // namespace mozilla + +class nsEditingSession final : public nsIEditingSession, + public nsIWebProgressListener, + public nsSupportsWeakReference { + public: + nsEditingSession(); + + // nsISupports + NS_DECL_ISUPPORTS + + // nsIWebProgressListener + NS_DECL_NSIWEBPROGRESSLISTENER + + // nsIEditingSession + NS_DECL_NSIEDITINGSESSION + + /** + * Removes all the editor's controllers/listeners etc and makes the window + * uneditable. + */ + nsresult DetachFromWindow(nsPIDOMWindowOuter* aWindow); + + /** + * Undos DetachFromWindow(), reattaches this editing session/editor + * to the window. + */ + nsresult ReattachToWindow(nsPIDOMWindowOuter* aWindow); + + protected: + virtual ~nsEditingSession(); + + typedef already_AddRefed<nsBaseCommandController> (*ControllerCreatorFn)(); + + nsresult SetupEditorCommandController( + ControllerCreatorFn aControllerCreatorFn, mozIDOMWindowProxy* aWindow, + nsISupports* aContext, uint32_t* aControllerId); + + nsresult SetContextOnControllerById(nsIControllers* aControllers, + nsISupports* aContext, uint32_t aID); + + /** + * Set the editor on the controller(s) for this window + */ + nsresult SetEditorOnControllers(nsPIDOMWindowOuter& aWindow, + mozilla::HTMLEditor* aEditor); + + /** + * Setup editor and related support objects + */ + MOZ_CAN_RUN_SCRIPT nsresult SetupEditorOnWindow(nsPIDOMWindowOuter& aWindow); + + nsresult PrepareForEditing(nsPIDOMWindowOuter* aWindow); + + static void TimerCallback(nsITimer* aTimer, void* aClosure); + nsCOMPtr<nsITimer> mLoadBlankDocTimer; + + // progress load stuff + nsresult StartDocumentLoad(nsIWebProgress* aWebProgress, + bool isToBeMadeEditable); + MOZ_CAN_RUN_SCRIPT_BOUNDARY + nsresult EndDocumentLoad(nsIWebProgress* aWebProgress, nsIChannel* aChannel, + nsresult aStatus, bool isToBeMadeEditable); + nsresult StartPageLoad(nsIChannel* aChannel); + nsresult EndPageLoad(nsIWebProgress* aWebProgress, nsIChannel* aChannel, + nsresult aStatus); + + bool IsProgressForTargetDocument(nsIWebProgress* aWebProgress); + + void RemoveEditorControllers(nsPIDOMWindowOuter* aWindow); + void RemoveWebProgressListener(nsPIDOMWindowOuter* aWindow); + void RestoreAnimationMode(nsPIDOMWindowOuter* aWindow); + void RemoveListenersAndControllers(nsPIDOMWindowOuter* aWindow, + mozilla::HTMLEditor* aHTMLEditor); + + /** + * Disable scripts and plugins in aDocShell. + */ + nsresult DisableJSAndPlugins(nsPIDOMWindowInner* aWindow); + + /** + * Restore JS and plugins (enable/disable them) according to the state they + * were before the last call to disableJSAndPlugins. + */ + nsresult RestoreJSAndPlugins(nsPIDOMWindowInner* aWindow); + + protected: + bool mDoneSetup; // have we prepared for editing yet? + + // Used to prevent double creation of editor because nsIWebProgressListener + // receives a STATE_STOP notification before the STATE_START + // for our document, so we wait for the STATE_START, then STATE_STOP + // before creating an editor + bool mCanCreateEditor; + + bool mInteractive; + bool mMakeWholeDocumentEditable; + + bool mDisabledJSAndPlugins; + + // True if scripts were enabled before the editor turned scripts + // off, otherwise false. + bool mScriptsEnabled; + + // True if plugins were enabled before the editor turned plugins + // off, otherwise false. + bool mPluginsEnabled; + + bool mProgressListenerRegistered; + + // The image animation mode before it was turned off. + uint16_t mImageAnimationMode; + + // THE REMAINING MEMBER VARIABLES WILL BECOME A SET WHEN WE EDIT + // MORE THAN ONE EDITOR PER EDITING SESSION + RefPtr<mozilla::ComposerCommandsUpdater> mComposerCommandsUpdater; + + // Save the editor type so we can create the editor after loading uri + nsCString mEditorType; + uint32_t mEditorFlags; + uint32_t mEditorStatus; + uint32_t mBaseCommandControllerId; + uint32_t mDocStateControllerId; + uint32_t mHTMLCommandControllerId; + + // Make sure the docshell we use is safe + nsWeakPtr mDocShell; + + // See if we can reuse an existing editor + nsWeakPtr mExistingEditor; +}; + +#endif // nsEditingSession_h__ diff --git a/editor/composer/nsIEditingSession.idl b/editor/composer/nsIEditingSession.idl new file mode 100644 index 0000000000..9691092ce8 --- /dev/null +++ b/editor/composer/nsIEditingSession.idl @@ -0,0 +1,84 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * 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 "nsISupports.idl" +#include "domstubs.idl" + +interface mozIDOMWindowProxy; +interface nsIEditor; + +%{ C++ +class mozIDOMWindowProxy; +namespace mozilla { +class HTMLEditor; +} // namespace mozilla +%} + +[scriptable, builtinclass, uuid(24f963d1-e6fc-43ea-a206-99ac5fcc5265)] +interface nsIEditingSession : nsISupports +{ + /** + * Error codes when we fail to create an editor + * is placed in attribute editorStatus + */ + const long eEditorOK = 0; + const long eEditorCreationInProgress = 1; + const long eEditorErrorCantEditMimeType = 2; + const long eEditorErrorFileNotFound = 3; + const long eEditorErrorCantEditFramesets = 8; + const long eEditorErrorUnknown = 9; + + /** + * Status after editor creation and document loading + * Value is one of the above error codes + */ + readonly attribute unsigned long editorStatus; + + /** + * Make this window editable + * @param aWindow nsIDOMWindow, the window the embedder needs to make editable + * @param aEditorType string, "html" "htmlsimple" "text" "textsimple" + * @param aMakeWholeDocumentEditable if PR_TRUE make the whole document in + * aWindow editable, otherwise it's the + * embedder who should make the document + * (or part of it) editable. + * @param aInteractive if PR_FALSE turn off scripting and plugins + */ + [can_run_script] + void makeWindowEditable(in mozIDOMWindowProxy window, + in string aEditorType, + in boolean doAfterUriLoad, + in boolean aMakeWholeDocumentEditable, + in boolean aInteractive); + + /** + * Test whether a specific window has had its editable flag set; it may have an editor + * now, or will get one after the uri load. + * + * Use this, passing the content root window, to test if we've set up editing + * for this content. + */ + boolean windowIsEditable(in mozIDOMWindowProxy window); + + /** + * Get the editor for this window. May return null + */ + nsIEditor getEditorForWindow(in mozIDOMWindowProxy window); + + /** + * Destroy editor and related support objects + */ + [noscript] void tearDownEditorOnWindow(in mozIDOMWindowProxy window); + +%{C++ + /** + * This method is implemented with nsIDocShell::GetHTMLEditor(). I.e., + * This method doesn't depend on nsEditingSession. Therefore, even if + * there were some implementation of nsIEditingSession interface, this + * would be safe to use. + */ + mozilla::HTMLEditor* GetHTMLEditorForWindow(mozIDOMWindowProxy* aWindow); +%} +}; diff --git a/editor/composer/res/EditorOverride.css b/editor/composer/res/EditorOverride.css new file mode 100644 index 0000000000..373ed869a1 --- /dev/null +++ b/editor/composer/res/EditorOverride.css @@ -0,0 +1,322 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +*|* { + -moz-user-modify: read-write; +} + +/* Styles to alter look of things in the Editor content window + * that should NOT be removed when we display in completely WYSIWYG + * "Browser Preview" mode. + * Anything that should change, like appearance of table borders + * and Named Anchors, should be placed in EditorContent.css instead of here. +*/ + +/* Primary cursor is text I-beam */ + +::-moz-canvas, a:link { + cursor: text; +} + +/* Use default arrow over objects with size that + are selected when clicked on. + Override the browser's pointer cursor over links +*/ + +img, img[usemap], area, +object, object[usemap], +applet, hr, button, input, textarea, select, +a:link img, a:visited img, a:active img, +a[name]:-moz-only-whitespace { + cursor: default; +} + +a:visited, a:active { + cursor: text; +} + +/* Prevent clicking on links from going to link */ +a:link img, a:visited img { + -moz-user-input: none; +} + +/* We suppress user/author's prefs for link underline, + so we must set explicitly. This isn't good! +*/ +a:link { + color: -moz-hyperlinktext; +} + +/* Allow double-clicks on these widgets to open properties dialogs + XXX except when the widget has disabled attribute */ +input, button, textarea { + user-select: all !important; + -moz-user-input: auto !important; + -moz-user-focus: none !important; +} + +/* XXX Still need a better way of blocking other events to these widgets */ +select, input[disabled], input[type="checkbox"], input[type="radio"], input[type="file"] { + user-select: all !important; + -moz-user-input: none !important; + -moz-user-focus: none !important; +} + +input[type="hidden"] { + border: 1px solid black !important; + visibility: visible !important; +} + +label { + user-select: all !important; +} + +::-moz-display-comboboxcontrol-frame { + user-select: text !important; +} + +option { + user-select: text !important; +} + +#mozToc.readonly { + user-select: all !important; + -moz-user-input: none !important; +} + +/* the following rules are for Image Resizing */ + +span[\_moz_anonclass="mozResizer"] { + width: 5px; + height: 5px; + position: absolute; + border: 1px black solid; + background-color: white; + user-select: none; + z-index: 2147483646; /* max value -1 for this property */ +} + +/* we can't use :active below */ +span[\_moz_anonclass="mozResizer"][\_moz_activated], +span[\_moz_anonclass="mozResizer"]:hover { + background-color: black; +} + +span[\_moz_anonclass="mozResizer"].hidden, +span[\_moz_anonclass="mozResizingShadow"].hidden, +img[\_moz_anonclass="mozResizingShadow"].hidden, +span[\_moz_anonclass="mozGrabber"].hidden, +span[\_moz_anonclass="mozResizingInfo"].hidden, +a[\_moz_anonclass="mozTableRemoveRow"].hidden, +a[\_moz_anonclass="mozTableRemoveColumn"].hidden { + display: none !important; +} + +span[\_moz_anonclass="mozResizer"][anonlocation="nw"] { + cursor: nw-resize; +} +span[\_moz_anonclass="mozResizer"][anonlocation="n"] { + cursor: n-resize; +} +span[\_moz_anonclass="mozResizer"][anonlocation="ne"] { + cursor: ne-resize; +} +span[\_moz_anonclass="mozResizer"][anonlocation="w"] { + cursor: w-resize; +} +span[\_moz_anonclass="mozResizer"][anonlocation="e"] { + cursor: e-resize; +} +span[\_moz_anonclass="mozResizer"][anonlocation="sw"] { + cursor: sw-resize; +} +span[\_moz_anonclass="mozResizer"][anonlocation="s"] { + cursor: s-resize; +} +span[\_moz_anonclass="mozResizer"][anonlocation="se"] { + cursor: se-resize; +} + +span[\_moz_anonclass="mozResizingShadow"], +img[\_moz_anonclass="mozResizingShadow"] { + outline: thin dashed black; + user-select: none; + opacity: 0.5; + position: absolute; + z-index: 2147483647; /* max value for this property */ +} + +span[\_moz_anonclass="mozResizingInfo"] { + font-family: sans-serif; + font-size: x-small; + color: black; + background-color: #d0d0d0; + border: ridge 2px #d0d0d0; + padding: 2px; + position: absolute; + z-index: 2147483647; /* max value for this property */ +} + +img[\_moz_resizing] { + outline: thin solid black; +} + +*[\_moz_abspos] { + outline: silver ridge 2px; + z-index: 2147483645 !important; /* max value -2 for this property */ +} +*[\_moz_abspos="white"] { + background-color: white !important; +} +*[\_moz_abspos="black"] { + background-color: black !important; +} + +span[\_moz_anonclass="mozGrabber"] { + outline: ridge 2px silver; + padding: 2px; + position: absolute; + width: 12px; + height: 12px; + background-image: url("resource://gre/res/grabber.gif"); + background-repeat: no-repeat; + background-position: center center; + user-select: none; + cursor: move; + z-index: 2147483647; /* max value for this property */ +} + +/* INLINE TABLE EDITING */ + +a[\_moz_anonclass="mozTableAddColumnBefore"] { + position: absolute; + z-index: 2147483647; /* max value for this property */ + text-decoration: none !important; + border: none 0px !important; + width: 4px; + height: 8px; + background-image: url("resource://gre/res/table-add-column-before.gif"); + background-repeat: no-repeat; + background-position: center center; + user-select: none !important; + -moz-user-focus: none !important; +} + +a[\_moz_anonclass="mozTableAddColumnBefore"]:hover { + background-image: url("resource://gre/res/table-add-column-before-hover.gif"); +} + +a[\_moz_anonclass="mozTableAddColumnBefore"]:active { + background-image: url("resource://gre/res/table-add-column-before-active.gif"); +} + +a[\_moz_anonclass="mozTableAddColumnAfter"] { + position: absolute; + z-index: 2147483647; /* max value for this property */ + text-decoration: none !important; + border: none 0px !important; + width: 4px; + height: 8px; + background-image: url("resource://gre/res/table-add-column-after.gif"); + background-repeat: no-repeat; + background-position: center center; + user-select: none !important; + -moz-user-focus: none !important; +} + +a[\_moz_anonclass="mozTableAddColumnAfter"]:hover { + background-image: url("resource://gre/res/table-add-column-after-hover.gif"); +} + +a[\_moz_anonclass="mozTableAddColumnAfter"]:active { + background-image: url("resource://gre/res/table-add-column-after-active.gif"); +} + +a[\_moz_anonclass="mozTableRemoveColumn"] { + position: absolute; + z-index: 2147483647; /* max value for this property */ + text-decoration: none !important; + border: none 0px !important; + width: 8px; + height: 8px; + background-image: url("resource://gre/res/table-remove-column.gif"); + background-repeat: no-repeat; + background-position: center center; + user-select: none !important; + -moz-user-focus: none !important; +} + +a[\_moz_anonclass="mozTableRemoveColumn"]:hover { + background-image: url("resource://gre/res/table-remove-column-hover.gif"); +} + +a[\_moz_anonclass="mozTableRemoveColumn"]:active { + background-image: url("resource://gre/res/table-remove-column-active.gif"); +} + +a[\_moz_anonclass="mozTableAddRowBefore"] { + position: absolute; + z-index: 2147483647; /* max value for this property */ + text-decoration: none !important; + border: none 0px !important; + width: 8px; + height: 4px; + background-image: url("resource://gre/res/table-add-row-before.gif"); + background-repeat: no-repeat; + background-position: center center; + user-select: none !important; + -moz-user-focus: none !important; +} + +a[\_moz_anonclass="mozTableAddRowBefore"]:hover { + background-image: url("resource://gre/res/table-add-row-before-hover.gif"); +} + +a[\_moz_anonclass="mozTableAddRowBefore"]:active { + background-image: url("resource://gre/res/table-add-row-before-active.gif"); +} + +a[\_moz_anonclass="mozTableAddRowAfter"] { + position: absolute; + z-index: 2147483647; /* max value for this property */ + text-decoration: none !important; + border: none 0px !important; + width: 8px; + height: 4px; + background-image: url("resource://gre/res/table-add-row-after.gif"); + background-repeat: no-repeat; + background-position: center center; + user-select: none !important; + -moz-user-focus: none !important; +} + +a[\_moz_anonclass="mozTableAddRowAfter"]:hover { + background-image: url("resource://gre/res/table-add-row-after-hover.gif"); +} + +a[\_moz_anonclass="mozTableAddRowAfter"]:active { + background-image: url("resource://gre/res/table-add-row-after-active.gif"); +} + +a[\_moz_anonclass="mozTableRemoveRow"] { + position: absolute; + z-index: 2147483647; /* max value for this property */ + text-decoration: none !important; + border: none 0px !important; + width: 8px; + height: 8px; + background-image: url("resource://gre/res/table-remove-row.gif"); + background-repeat: no-repeat; + background-position: center center; + user-select: none !important; + -moz-user-focus: none !important; +} + +a[\_moz_anonclass="mozTableRemoveRow"]:hover { + background-image: url("resource://gre/res/table-remove-row-hover.gif"); +} + +a[\_moz_anonclass="mozTableRemoveRow"]:active { + background-image: url("resource://gre/res/table-remove-row-active.gif"); +} diff --git a/editor/composer/res/grabber.gif b/editor/composer/res/grabber.gif Binary files differnew file mode 100644 index 0000000000..06749a64fc --- /dev/null +++ b/editor/composer/res/grabber.gif diff --git a/editor/composer/res/table-add-column-after-active.gif b/editor/composer/res/table-add-column-after-active.gif Binary files differnew file mode 100644 index 0000000000..3ec50b82ee --- /dev/null +++ b/editor/composer/res/table-add-column-after-active.gif diff --git a/editor/composer/res/table-add-column-after-hover.gif b/editor/composer/res/table-add-column-after-hover.gif Binary files differnew file mode 100644 index 0000000000..29679f9812 --- /dev/null +++ b/editor/composer/res/table-add-column-after-hover.gif diff --git a/editor/composer/res/table-add-column-after.gif b/editor/composer/res/table-add-column-after.gif Binary files differnew file mode 100644 index 0000000000..8891be969c --- /dev/null +++ b/editor/composer/res/table-add-column-after.gif diff --git a/editor/composer/res/table-add-column-before-active.gif b/editor/composer/res/table-add-column-before-active.gif Binary files differnew file mode 100644 index 0000000000..1e205291e1 --- /dev/null +++ b/editor/composer/res/table-add-column-before-active.gif diff --git a/editor/composer/res/table-add-column-before-hover.gif b/editor/composer/res/table-add-column-before-hover.gif Binary files differnew file mode 100644 index 0000000000..7b54537e40 --- /dev/null +++ b/editor/composer/res/table-add-column-before-hover.gif diff --git a/editor/composer/res/table-add-column-before.gif b/editor/composer/res/table-add-column-before.gif Binary files differnew file mode 100644 index 0000000000..d4a3ffe5e9 --- /dev/null +++ b/editor/composer/res/table-add-column-before.gif diff --git a/editor/composer/res/table-add-row-after-active.gif b/editor/composer/res/table-add-row-after-active.gif Binary files differnew file mode 100644 index 0000000000..cc01da2c9e --- /dev/null +++ b/editor/composer/res/table-add-row-after-active.gif diff --git a/editor/composer/res/table-add-row-after-hover.gif b/editor/composer/res/table-add-row-after-hover.gif Binary files differnew file mode 100644 index 0000000000..a829351b64 --- /dev/null +++ b/editor/composer/res/table-add-row-after-hover.gif diff --git a/editor/composer/res/table-add-row-after.gif b/editor/composer/res/table-add-row-after.gif Binary files differnew file mode 100644 index 0000000000..3f1a39d981 --- /dev/null +++ b/editor/composer/res/table-add-row-after.gif diff --git a/editor/composer/res/table-add-row-before-active.gif b/editor/composer/res/table-add-row-before-active.gif Binary files differnew file mode 100644 index 0000000000..34f1e0adec --- /dev/null +++ b/editor/composer/res/table-add-row-before-active.gif diff --git a/editor/composer/res/table-add-row-before-hover.gif b/editor/composer/res/table-add-row-before-hover.gif Binary files differnew file mode 100644 index 0000000000..e8f1d10b0c --- /dev/null +++ b/editor/composer/res/table-add-row-before-hover.gif diff --git a/editor/composer/res/table-add-row-before.gif b/editor/composer/res/table-add-row-before.gif Binary files differnew file mode 100644 index 0000000000..1682170cb3 --- /dev/null +++ b/editor/composer/res/table-add-row-before.gif diff --git a/editor/composer/res/table-remove-column-active.gif b/editor/composer/res/table-remove-column-active.gif Binary files differnew file mode 100644 index 0000000000..4dfbde4ce2 --- /dev/null +++ b/editor/composer/res/table-remove-column-active.gif diff --git a/editor/composer/res/table-remove-column-hover.gif b/editor/composer/res/table-remove-column-hover.gif Binary files differnew file mode 100644 index 0000000000..fd11bb52c3 --- /dev/null +++ b/editor/composer/res/table-remove-column-hover.gif diff --git a/editor/composer/res/table-remove-column.gif b/editor/composer/res/table-remove-column.gif Binary files differnew file mode 100644 index 0000000000..d8071da0a9 --- /dev/null +++ b/editor/composer/res/table-remove-column.gif diff --git a/editor/composer/res/table-remove-row-active.gif b/editor/composer/res/table-remove-row-active.gif Binary files differnew file mode 100644 index 0000000000..4dfbde4ce2 --- /dev/null +++ b/editor/composer/res/table-remove-row-active.gif diff --git a/editor/composer/res/table-remove-row-hover.gif b/editor/composer/res/table-remove-row-hover.gif Binary files differnew file mode 100644 index 0000000000..fd11bb52c3 --- /dev/null +++ b/editor/composer/res/table-remove-row-hover.gif diff --git a/editor/composer/res/table-remove-row.gif b/editor/composer/res/table-remove-row.gif Binary files differnew file mode 100644 index 0000000000..d8071da0a9 --- /dev/null +++ b/editor/composer/res/table-remove-row.gif diff --git a/editor/composer/test/chrome.ini b/editor/composer/test/chrome.ini new file mode 100644 index 0000000000..8b4d9c3333 --- /dev/null +++ b/editor/composer/test/chrome.ini @@ -0,0 +1,5 @@ +[DEFAULT] +skip-if = os == 'android' + +[test_bug1266815.html] +[test_bug434998.xhtml] diff --git a/editor/composer/test/file_bug1453190.html b/editor/composer/test/file_bug1453190.html new file mode 100644 index 0000000000..74b13bb0ec --- /dev/null +++ b/editor/composer/test/file_bug1453190.html @@ -0,0 +1,12 @@ +<script> +window.onload = function () { + document.createElement("frameset").setAttribute("onunload", "go()") +} +function go() { + let a = document.getElementById("a"); + let b = document.getElementById("b"); + a.appendChild(b); +} +</script> +<div id="a"> +<li id="b" contenteditable="true"> diff --git a/editor/composer/test/mochitest.ini b/editor/composer/test/mochitest.ini new file mode 100644 index 0000000000..eb364a4a50 --- /dev/null +++ b/editor/composer/test/mochitest.ini @@ -0,0 +1,9 @@ +[test_bug1453190.html] +skip-if = os == "android" +support-files = + file_bug1453190.html +[test_bug348497.html] +[test_bug384147.html] +[test_bug389350.html] +[test_bug519928.html] +[test_bug738440.html] diff --git a/editor/composer/test/test_bug1266815.html b/editor/composer/test/test_bug1266815.html new file mode 100644 index 0000000000..1c565fb2c1 --- /dev/null +++ b/editor/composer/test/test_bug1266815.html @@ -0,0 +1,97 @@ +<!DOCTYPE html> +<html> +<head> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<script type="text/javascript"> +// XXX(nika): Why are we using SpecialPowers here? If we're a chrome mochitest +// can't we avoid using the SpecialPowers wrappers? +const Cc = SpecialPowers.Cc; +const Ci = SpecialPowers.Ci; +const Cu = SpecialPowers.Cu; + +const { ComponentUtils } = ChromeUtils.importESModule( + "resource://gre/modules/ComponentUtils.sys.mjs" +); + +const HELPERAPP_DIALOG_CID = + SpecialPowers.wrap(SpecialPowers.Components) + .ID(Cc["@mozilla.org/helperapplauncherdialog;1"].number); +const HELPERAPP_DIALOG_CONTRACT_ID = "@mozilla.org/helperapplauncherdialog;1"; +const MOCK_HELPERAPP_DIALOG_CID = + SpecialPowers.wrap(SpecialPowers.Components) + .ID("{391832c8-5232-4676-b838-cc8ad373f3d8}"); + +var registrar = SpecialPowers.wrap(Components).manager + .QueryInterface(Ci.nsIComponentRegistrar); + +const HandlerService = Cc[ + "@mozilla.org/uriloader/handler-service;1" +].getService(Ci.nsIHandlerService); + +const MIMEService = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService); + +var helperAppDlgPromise = new Promise(function(resolve) { + var mockHelperAppService; + + function HelperAppLauncherDialog() { + } + + HelperAppLauncherDialog.prototype = { + show(aLauncher, aWindowContext, aReason) { + ok(true, "Whether showing Dialog"); + resolve(); + registrar.unregisterFactory(MOCK_HELPERAPP_DIALOG_CID, + mockHelperAppService); + }, + QueryInterface: ChromeUtils.generateQI(["nsIHelperAppLauncherDialog"]), + }; + + mockHelperAppService = ComponentUtils.generateSingletonFactory(HelperAppLauncherDialog); + registrar.registerFactory(MOCK_HELPERAPP_DIALOG_CID, "", + HELPERAPP_DIALOG_CONTRACT_ID, + mockHelperAppService); +}); + +add_task(async function() { + // ensure the download triggers the external app dialog + const mimeInfo = MIMEService.getFromTypeAndExtension("application/octet-stream", ""); + mimeInfo.alwaysAskBeforeHandling = true; + HandlerService.store(mimeInfo); + + SimpleTest.registerCleanupFunction(() => { + HandlerService.remove(mimeInfo); + }); + + let promise = new Promise(function(resolve) { + let iframe = document.createElement("iframe"); + iframe.onload = function() { + is(iframe.contentDocument.getElementById("edit").innerText, "abc", + "load iframe source"); + resolve(); + }; + iframe.id = "testframe"; + iframe.src = "data:text/html,<div id=edit contenteditable=true>abc</div>"; + document.body.appendChild(iframe); + }); + + await promise; + + let iframe = document.getElementById("testframe"); + let docShell = SpecialPowers.wrap(iframe.contentWindow).docShell; + + ok(docShell.hasEditingSession, "Should have editing session"); + + document.getElementById("testframe").src = + "data:application/octet-stream,TESTCONTENT"; + + await helperAppDlgPromise; + + ok(docShell.hasEditingSession, "Should have editing session"); +}); +</script> +</body> +</html> diff --git a/editor/composer/test/test_bug1453190.html b/editor/composer/test/test_bug1453190.html new file mode 100644 index 0000000000..cf6b0d888a --- /dev/null +++ b/editor/composer/test/test_bug1453190.html @@ -0,0 +1,36 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1453190 +--> +<head> + <title>Test for Bug 1453190</title> + <script 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=1453190">Mozilla Bug 1453190</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +SimpleTest.waitForExplicitFinish(); + +let testWindow = window.open("file_bug1453190.html"); +testWindow.addEventListener("load", () => { + SimpleTest.executeSoon(() => { + runTest(testWindow); + }); +}, {once: true}); + +function runTest(win) { + ok(!win.closed, "test window is opened"); + win.close(); + ok(win.closed, "test window is closed"); + SimpleTest.finish(); +} +</script> +</pre> +</body> +</html> diff --git a/editor/composer/test/test_bug348497.html b/editor/composer/test/test_bug348497.html new file mode 100644 index 0000000000..898fb49ae1 --- /dev/null +++ b/editor/composer/test/test_bug348497.html @@ -0,0 +1,36 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=348497 +--> +<head> + <title>Test for Bug 348497</title> + <script 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=348497">Mozilla Bug 348497</a> +<p id="display"></p> +<div id="content"> + This page should not crash Mozilla<br> + <iframe id="testIframe"></iframe> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 348497 **/ +function doe() { + document.getElementById("testIframe").style.display = "block"; + document.getElementById("testIframe").contentDocument.designMode = "on"; +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(doe); +addLoadEvent(function() { ok(true, "enabling designmode on an iframe onload does not crash Mozilla"); }); +addLoadEvent(SimpleTest.finish); + +</script> +</pre> +</body> +</html> + diff --git a/editor/composer/test/test_bug384147.html b/editor/composer/test/test_bug384147.html new file mode 100644 index 0000000000..6feb8b4ad4 --- /dev/null +++ b/editor/composer/test/test_bug384147.html @@ -0,0 +1,203 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=384147 +--> +<head> + <title>Test for Bug 384147</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script 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=384147">Mozilla Bug 384147</a> +<p id="display"></p> +<div id="content" style="display: block"> +<div contentEditable id="editor"></div> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +/** Test for Bug 384147 **/ + +SimpleTest.waitForExplicitFinish(); + +var editor = document.getElementById("editor"); + +editor.innerHTML = "<ol><li>Item 1</li><li>Item 2</li><ol><li>Item 3</li></ol></ol><ul><li>Item 4</li><li>Item 5</li></ul>"; +editor.focus(); + +// If executed directly, a race condition exists that will cause execCommand +// to fail occasionally (but often). Defer test execution to page load. +addLoadEvent(function() { + var sel = window.getSelection(); + + // Test the effect that the tab key has on list items. Each test is + // documented with the initial state of the list on the left, and the + // expected state of the list on the right. {\t} indicates the list item + // that will be indented. {\st} indicates that a shift-tab will be simulated + // on that list item, outdenting it. + // + // Note: any test failing will likely result in all following tests failing + // as well, since each test depends on the document being in a given state. + // Unfortunately, due to the problems getting document focus and key events + // to fire consistently, it's difficult to reset state between tests. + // If there are test failures here, only debug the first test failure. + + // *** test 1 *** + // 1. Item 1 1. Item 1 + // 2. {\t}Item 2 1. Item 2 + // 1. Item 3 2. Item 3 + // * Item 4 * Item 4 + // * Item 5 * Item 5 + sel.removeAllRanges(); + sel.selectAllChildren(editor.getElementsByTagName("li")[1]); + document.execCommand("indent", false, null); + ok(editor.innerHTML == "<ol><li>Item 1</li><ol><li>Item 2</li><li>Item 3</li></ol></ol><ul><li>Item 4</li><li>Item 5</li></ul>", + "html output doesn't match expected value in test 1"); + + // *** test 2 *** + // 1. Item 1 1. Item 1 + // 1. Item 2 1. Item 2 + // 2. {\t}Item 3 1. Item 3 + // * Item 4 * Item 4 + // * Item 5 * Item 5 + sel.removeAllRanges(); + sel.selectAllChildren(editor.getElementsByTagName("li")[2]); + document.execCommand("indent", false, null); + ok(editor.innerHTML == "<ol><li>Item 1</li><ol><li>Item 2</li><ol><li>Item 3</li></ol></ol></ol><ul><li>Item 4</li><li>Item 5</li></ul>", + "html output doesn't match expected value in test 2"); + + // *** test 3 *** + // 1. Item 1 1. Item 1 + // 1. Item 2 1. Item 2 + // 1. {\st}Item 3 2. Item 3 + // * Item 4 * Item 4 + // * Item 5 * Item 5 + document.execCommand("outdent", false, null); + ok(editor.innerHTML == "<ol><li>Item 1</li><ol><li>Item 2</li><li>Item 3</li></ol></ol><ul><li>Item 4</li><li>Item 5</li></ul>", + "html output doesn't match expected value in test 3"); + + // *** test 4 *** + // 1. Item 1 1. Item 1 + // 1. Item 2 1. Item 2 + // 2. {\st}Item 3 2. Item 3 + // * Item 4 * Item 4 + // * Item 5 * Item 5 + document.execCommand("outdent", false, null); + ok(editor.innerHTML == "<ol><li>Item 1</li><ol><li>Item 2</li></ol><li>Item 3</li></ol><ul><li>Item 4</li><li>Item 5</li></ul>", + "html output doesn't match expected value in test 4"); + + // *** test 5 *** + // 1. Item 1 1. Item 1 + // 1. {\st}Item 2 2. Item 2 + // 2. Item 3 3. Item 3 + // * Item 4 * Item 4 + // * Item 5 * Item 5 + sel.removeAllRanges(); + sel.selectAllChildren(editor.getElementsByTagName("li")[1]); + document.execCommand("outdent", false, null); + ok(editor.innerHTML == "<ol><li>Item 1</li><li>Item 2</li><li>Item 3</li></ol><ul><li>Item 4</li><li>Item 5</li></ul>", + "html output doesn't match expected value in test 5"); + + // *** test 6 *** + // 1. Item 1 1. Item 1 + // 2. {\t}Item 2 1. Item 2 + // 3. Item 3 2. Item 3 + // * Item 4 * Item 4 + // * Item 5 * Item 5 + document.execCommand("indent", false, null); + ok(editor.innerHTML == "<ol><li>Item 1</li><ol><li>Item 2</li></ol><li>Item 3</li></ol><ul><li>Item 4</li><li>Item 5</li></ul>", + "html output doesn't match expected value in test 6"); + + // *** test 7 *** + // 1. Item 1 1. Item 1 + // 1. Item 2 1. Item 2 + // 2. {\t}Item 3 2. Item 3 + // * Item 4 * Item 4 + // * Item 5 * Item 5 + sel.removeAllRanges(); + sel.selectAllChildren(editor.getElementsByTagName("li")[2]); + document.execCommand("indent", false, null); + ok(editor.innerHTML == "<ol><li>Item 1</li><ol><li>Item 2</li><li>Item 3</li></ol></ol><ul><li>Item 4</li><li>Item 5</li></ul>", + "html output doesn't match expected value in test 7"); + + // That covers the basics of merging lists on indent and outdent. + // We also want to check that ul / ol lists won't be merged together, + // since they're different types of lists. + // *** test 8 *** + // 1. Item 1 1. Item 1 + // 1. Item 2 1. Item 2 + // 2. Item 3 2. Item 3 + // * {\t}Item 4 * Item 4 + // * Item 5 * Item 5 + sel.removeAllRanges(); + sel.selectAllChildren(editor.getElementsByTagName("li")[3]); + document.execCommand("indent", false, null); + ok(editor.innerHTML == "<ol><li>Item 1</li><ol><li>Item 2</li><li>Item 3</li></ol></ol><ul><ul><li>Item 4</li></ul><li>Item 5</li></ul>", + "html output doesn't match expected value in test 8"); + + // Better test merging with <ul> rather than <ol> too. + // *** test 9 *** + // 1. Item 1 1. Item 1 + // 1. Item 2 1. Item 2 + // 2. Item 3 2. Item 3 + // * Item 4 * Item 4 + // * {\t}Item 5 * Item 5 + sel.removeAllRanges(); + sel.selectAllChildren(editor.getElementsByTagName("li")[4]); + document.execCommand("indent", false, null); + ok(editor.innerHTML == "<ol><li>Item 1</li><ol><li>Item 2</li><li>Item 3</li></ol></ol><ul><ul><li>Item 4</li><li>Item 5</li></ul></ul>", + "html output doesn't match expected value in test 9"); + + // Same test as test 8, but with outdent rather than indent. + // *** test 10 *** + // 1. Item 1 1. Item 1 + // 1. Item 2 1. Item 2 + // 2. Item 3 2. Item 3 + // * {\st}Item 4 * Item 4 + // * Item 5 * Item 5 + sel.removeAllRanges(); + sel.selectAllChildren(editor.getElementsByTagName("li")[3]); + document.execCommand("outdent", false, null); + ok(editor.innerHTML == "<ol><li>Item 1</li><ol><li>Item 2</li><li>Item 3</li></ol></ol><ul><li>Item 4</li><ul><li>Item 5</li></ul></ul>", + "html output doesn't match expected value in test 10"); + + // Test indenting multiple items at once. Hold down "shift" and select + // upwards to get all the <ol> items and the first <ul> item. + // *** test 11 *** + // 1. Item 1 1. Item 1 + // 1. {\t}Item 2 1. Item 2 + // 2. {\t}Item 3 2. Item 3 + // * {\t}Item 4 * Item 4 + // * Item 5 * Item 5 + sel.removeAllRanges(); + var range = document.createRange(); + range.setStart(editor.getElementsByTagName("li")[1], 0); + range.setEnd(editor.getElementsByTagName("li")[3], editor.getElementsByTagName("li")[3].childNodes.length); + sel.addRange(range); + document.execCommand("indent", false, null); + ok(editor.innerHTML == "<ol><li>Item 1</li><ol><ol><li>Item 2</li><li>Item 3</li></ol></ol></ol><ul><ul><li>Item 4</li><li>Item 5</li></ul></ul>", + "html output doesn't match expected value in test 11"); + + // Test outdenting multiple items at once. Selection is already ready... + // *** test 12 *** + // 1. Item 1 1. Item 1 + // 1. {\st}Item 2 1. Item 2 + // 2. {\st}Item 3 2. Item 3 + // * {\st}Item 4 * Item 4 + // * Item 5 * Item 5 + document.execCommand("outdent", false, null); + ok(editor.innerHTML == "<ol><li>Item 1</li><ol><li>Item 2</li><li>Item 3</li></ol></ol><ul><li>Item 4</li><ul><li>Item 5</li></ul></ul>", + "html output doesn't match expected value in test 12"); + + SimpleTest.finish(); +}); + + + +</script> +</pre> +</body> +</html> + diff --git a/editor/composer/test/test_bug389350.html b/editor/composer/test/test_bug389350.html new file mode 100644 index 0000000000..9b9aecd1e7 --- /dev/null +++ b/editor/composer/test/test_bug389350.html @@ -0,0 +1,33 @@ +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=389350 +--> +<head> +<title>Test for Bug 389350</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<script src="/tests/SimpleTest/EventUtils.js"></script> +<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + +<script type="text/javascript"> + +function runTest() { + var e = document.getElementById("edit"); + e.contentDocument.designMode = "on"; + e.style.display = "block"; + e.focus(); + sendString("abc"); + var expected = "<head></head><body>abc</body>"; + var result = e.contentDocument.documentElement.innerHTML; + is(result, expected, "iframe with designmode on had incorrect content"); + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +addLoadEvent(runTest); +</script> + +</head> +<body id="body"> +<iframe id="edit" width="200" height="100" style="display: none;" src=""> +</body> +</html> diff --git a/editor/composer/test/test_bug434998.xhtml b/editor/composer/test/test_bug434998.xhtml new file mode 100644 index 0000000000..db2261e3a5 --- /dev/null +++ b/editor/composer/test/test_bug434998.xhtml @@ -0,0 +1,106 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" + type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=434998 +--> +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Mozilla Bug 434998" onload="runTest();"> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=434998" + target="_blank">Mozilla Bug 434998</a> + <p/> + <editor xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + id="editor" + type="content" + primary="true" + editortype="html" + style="width: 400px; height: 100px; border: thin solid black"/> + <p/> + <pre id="test"> + </pre> + </body> + <script class="testbody" type="application/javascript"> + <![CDATA[ + + SimpleTest.waitForExplicitFinish(); + + function EditorContentListener(aEditor) + { + this.init(aEditor); + } + + EditorContentListener.prototype = { + init(aEditor) + { + this.mEditor = aEditor; + }, + + QueryInterface: ChromeUtils.generateQI(["nsIWebProgressListener", + "nsISupportsWeakReference"]), + + onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) + { + if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) + { + var editor = this.mEditor.getEditor(this.mEditor.contentWindow); + if (editor) { + // Should not throw + var threw = false; + try { + this.mEditor.contentDocument.execCommand("bold", false, null); + } catch (e) { + threw = true; + } + ok(!threw, "The execCommand API should work on <xul:editor>"); + progress.removeProgressListener(progressListener, Ci.nsIWebProgress.NOTIFY_ALL); + SimpleTest.finish(); + } + } + }, + + + onProgressChange(aWebProgress, aRequest, + aCurSelfProgress, aMaxSelfProgress, + aCurTotalProgress, aMaxTotalProgress) + { + }, + + onLocationChange(aWebProgress, aRequest, aLocation, aFlags) + { + }, + + onStatusChange(aWebProgress, aRequest, aStatus, aMessage) + { + }, + + onSecurityChange(aWebProgress, aRequest, aState) + { + }, + + onContentBlockingEvent(aWebProgress, aRequest, aEvent) + { + }, + + mEditor: null + }; + + var progress, progressListener; + + function runTest() { + var newEditorElement = document.getElementById("editor"); + newEditorElement.makeEditable("html", true); + var docShell = newEditorElement.docShell; + progress = docShell.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebProgress); + progressListener = new EditorContentListener(newEditorElement); + progress.addProgressListener(progressListener, Ci.nsIWebProgress.NOTIFY_ALL); + newEditorElement.setAttribute("src", "data:text/html,"); + } +]]> +</script> +</window> diff --git a/editor/composer/test/test_bug519928.html b/editor/composer/test/test_bug519928.html new file mode 100644 index 0000000000..4ccb1aaba5 --- /dev/null +++ b/editor/composer/test/test_bug519928.html @@ -0,0 +1,119 @@ +<!DOCTYPE HTML> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=519928 +--> +<head> + <title>Test for Bug 519928</title> + <script 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=519928">Mozilla Bug 519928</a> +<p id="display"></p> +<div id="content"> +<iframe id="load-frame"></iframe> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +var iframe = document.getElementById("load-frame"); + +function enableJS() { allowJS(true, iframe); } +function disableJS() { allowJS(false, iframe); } +function allowJS(allow, frame) { + SpecialPowers.wrap(frame.contentWindow).windowGlobalChild.windowContext.allowJavascript = allow; +} + +function expectJSAllowed(allowed, testCondition, callback) { + window.ICanRunMyJS = false; + var self_ = window; + testCondition(); + + var doc = iframe.contentDocument; + doc.body.innerHTML = "<iframe></iframe>"; + var innerFrame = doc.querySelector("iframe"); + innerFrame.addEventListener("load", function() { + var msg = "The inner iframe should" + (allowed ? "" : " not") + " be able to run Javascript"; + is(self_.ICanRunMyJS, allowed, msg); + callback(); + }, {once: true}); + // eslint-disable-next-line no-useless-concat + var iframeSrc = "<script>parent.parent.ICanRunMyJS = true;</scr" + "ipt>"; + innerFrame.srcdoc = iframeSrc; +} + +SimpleTest.waitForExplicitFinish(); +/* eslint-disable max-nested-callbacks */ +addLoadEvent(function() { + var enterDesignMode = function() { document.designMode = "on"; }; + var leaveDesignMode = function() { document.designMode = "off"; }; + expectJSAllowed(false, disableJS, function() { + expectJSAllowed(true, enableJS, function() { + expectJSAllowed(true, enterDesignMode, function() { + expectJSAllowed(true, leaveDesignMode, function() { + expectJSAllowed(false, disableJS, function() { + expectJSAllowed(false, enterDesignMode, function() { + expectJSAllowed(false, leaveDesignMode, function() { + expectJSAllowed(true, enableJS, function() { + enterDesignMode = function() { iframe.contentDocument.designMode = "on"; }; + leaveDesignMode = function() { iframe.contentDocument.designMode = "off"; }; + expectJSAllowed(false, disableJS, function() { + expectJSAllowed(true, enableJS, function() { + expectJSAllowed(true, enterDesignMode, function() { + expectJSAllowed(true, leaveDesignMode, function() { + expectJSAllowed(false, disableJS, function() { + expectJSAllowed(false, enterDesignMode, function() { + expectJSAllowed(false, leaveDesignMode, function() { + expectJSAllowed(true, enableJS, function() { + testDocumentDisabledJS(); + }); + }); + }); + }); + }); + }); + }); + }); + }); + }); + }); + }); + }); + }); + }); + }); +}); +/* eslint-enable max-nested-callbacks */ + +function testDocumentDisabledJS() { + window.ICanRunMyJS = false; + var self_ = window; + // Ensure design modes are disabled + document.designMode = "off"; + iframe.contentDocument.designMode = "off"; + + // Javascript enabled on the main iframe + enableJS(); + + var doc = iframe.contentDocument; + doc.body.innerHTML = "<iframe></iframe>"; + var innerFrame = doc.querySelector("iframe"); + + // Javascript disabled on the innerFrame. + allowJS(false, innerFrame); + + innerFrame.addEventListener("load", function() { + var msg = "The inner iframe should not be able to run Javascript"; + is(self_.ICanRunMyJS, false, msg); + SimpleTest.finish(); + }, {once: true}); + // eslint-disable-next-line no-useless-concat + var iframeSrc = "<script>parent.parent.ICanRunMyJS = true;</scr" + "ipt>"; + innerFrame.srcdoc = iframeSrc; +} + +</script> +</pre> +</body> +</html> diff --git a/editor/composer/test/test_bug738440.html b/editor/composer/test/test_bug738440.html new file mode 100644 index 0000000000..a021906cfc --- /dev/null +++ b/editor/composer/test/test_bug738440.html @@ -0,0 +1,37 @@ +<!doctype html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=738440 +--> +<title>Test for Bug 738440</title> +<script src="/tests/SimpleTest/SimpleTest.js"></script> +<link rel="stylesheet" href="/tests/SimpleTest/test.css" /> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=738440">Mozilla Bug 738440</a> +<div contenteditable></div> +<script> + +/** Test for Bug 738440 **/ +document.execCommand("stylewithcss", false, "true"); +is(document.queryCommandState("stylewithcss"), true, + "setting stylewithcss to true should cause its state to be true"); +is(document.queryCommandState("usecss"), false, + "usecss state should always be false"); + +document.execCommand("stylewithcss", false, "false"); +is(document.queryCommandState("stylewithcss"), false, + "setting stylewithcss to false should cause its state to be false"); +is(document.queryCommandState("usecss"), false, + "usecss state should always be false"); + +document.execCommand("usecss", false, "true"); +is(document.queryCommandState("stylewithcss"), false, + "setting usecss to true should cause stylewithcss state to be false"); +is(document.queryCommandState("usecss"), false, + "usecss state should always be false"); + +document.execCommand("usecss", false, "false"); +is(document.queryCommandState("stylewithcss"), true, + "setting usecss to false should cause stylewithcss state to be true"); +is(document.queryCommandState("usecss"), false, + "usecss state should always be false"); + +</script> |