diff options
Diffstat (limited to '')
-rw-r--r-- | accessible/base/DocManager.cpp | 562 |
1 files changed, 562 insertions, 0 deletions
diff --git a/accessible/base/DocManager.cpp b/accessible/base/DocManager.cpp new file mode 100644 index 0000000000..b7a5203e40 --- /dev/null +++ b/accessible/base/DocManager.cpp @@ -0,0 +1,562 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "DocManager.h" + +#include "ApplicationAccessible.h" +#include "DocAccessible-inl.h" +#include "DocAccessibleParent.h" +#include "nsAccessibilityService.h" +#include "Platform.h" +#include "RootAccessibleWrap.h" + +#ifdef A11Y_LOG +# include "Logging.h" +#endif + +#include "mozilla/Components.h" +#include "mozilla/EventListenerManager.h" +#include "mozilla/PresShell.h" +#include "mozilla/dom/Event.h" // for Event +#include "nsContentUtils.h" +#include "nsDocShellLoadTypes.h" +#include "nsIChannel.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIWebNavigation.h" +#include "nsIWebProgress.h" +#include "nsCoreUtils.h" +#include "xpcAccessibleDocument.h" + +using namespace mozilla; +using namespace mozilla::a11y; +using namespace mozilla::dom; + +StaticAutoPtr<nsTArray<DocAccessibleParent*>> DocManager::sRemoteDocuments; +StaticAutoPtr<nsRefPtrHashtable<nsPtrHashKey<const DocAccessibleParent>, + xpcAccessibleDocument>> + DocManager::sRemoteXPCDocumentCache; + +//////////////////////////////////////////////////////////////////////////////// +// DocManager +//////////////////////////////////////////////////////////////////////////////// + +DocManager::DocManager() : mDocAccessibleCache(2), mXPCDocumentCache(0) {} + +//////////////////////////////////////////////////////////////////////////////// +// DocManager public + +DocAccessible* DocManager::GetDocAccessible(Document* aDocument) { + if (!aDocument) return nullptr; + + DocAccessible* docAcc = GetExistingDocAccessible(aDocument); + if (docAcc) return docAcc; + + return CreateDocOrRootAccessible(aDocument); +} + +DocAccessible* DocManager::GetDocAccessible(const PresShell* aPresShell) { + if (!aPresShell) { + return nullptr; + } + + DocAccessible* doc = aPresShell->GetDocAccessible(); + if (doc) { + return doc; + } + + return GetDocAccessible(aPresShell->GetDocument()); +} + +LocalAccessible* DocManager::FindAccessibleInCache(nsINode* aNode) const { + for (const auto& docAccessible : mDocAccessibleCache.Values()) { + NS_ASSERTION(docAccessible, + "No doc accessible for the object in doc accessible cache!"); + + if (docAccessible) { + LocalAccessible* accessible = docAccessible->GetAccessible(aNode); + if (accessible) { + return accessible; + } + } + } + return nullptr; +} + +void DocManager::RemoveFromXPCDocumentCache(DocAccessible* aDocument, + bool aAllowServiceShutdown) { + xpcAccessibleDocument* xpcDoc = mXPCDocumentCache.GetWeak(aDocument); + if (xpcDoc) { + xpcDoc->Shutdown(); + mXPCDocumentCache.Remove(aDocument); + + if (aAllowServiceShutdown && !HasXPCDocuments()) { + MaybeShutdownAccService(nsAccessibilityService::eXPCOM); + } + } +} + +void DocManager::NotifyOfDocumentShutdown(DocAccessible* aDocument, + Document* aDOMDocument, + bool aAllowServiceShutdown) { + // We need to remove listeners in both cases, when document is being shutdown + // or when accessibility service is being shut down as well. + RemoveListeners(aDOMDocument); + + // Document will already be removed when accessibility service is shutting + // down so we do not need to remove it twice. + if (nsAccessibilityService::IsShutdown()) { + return; + } + + RemoveFromXPCDocumentCache(aDocument, aAllowServiceShutdown); + mDocAccessibleCache.Remove(aDOMDocument); +} + +void DocManager::RemoveFromRemoteXPCDocumentCache(DocAccessibleParent* aDoc) { + xpcAccessibleDocument* doc = GetCachedXPCDocument(aDoc); + if (doc) { + doc->Shutdown(); + sRemoteXPCDocumentCache->Remove(aDoc); + } + + if (sRemoteXPCDocumentCache && sRemoteXPCDocumentCache->Count() == 0) { + MaybeShutdownAccService(nsAccessibilityService::eXPCOM); + } +} + +void DocManager::NotifyOfRemoteDocShutdown(DocAccessibleParent* aDoc) { + RemoveFromRemoteXPCDocumentCache(aDoc); +} + +xpcAccessibleDocument* DocManager::GetXPCDocument(DocAccessible* aDocument) { + if (!aDocument) return nullptr; + + return mXPCDocumentCache.GetOrInsertNew(aDocument, aDocument); +} + +xpcAccessibleDocument* DocManager::GetXPCDocument(DocAccessibleParent* aDoc) { + xpcAccessibleDocument* doc = GetCachedXPCDocument(aDoc); + if (doc) { + return doc; + } + + if (!sRemoteXPCDocumentCache) { + sRemoteXPCDocumentCache = + new nsRefPtrHashtable<nsPtrHashKey<const DocAccessibleParent>, + xpcAccessibleDocument>; + ClearOnShutdown(&sRemoteXPCDocumentCache); + } + + MOZ_ASSERT(!aDoc->IsShutdown(), "Adding a shutdown doc to remote XPC cache"); + doc = new xpcAccessibleDocument(aDoc); + sRemoteXPCDocumentCache->InsertOrUpdate(aDoc, RefPtr{doc}); + + return doc; +} + +#ifdef DEBUG +bool DocManager::IsProcessingRefreshDriverNotification() const { + for (const auto& entry : mDocAccessibleCache) { + DocAccessible* docAccessible = entry.GetWeak(); + NS_ASSERTION(docAccessible, + "No doc accessible for the object in doc accessible cache!"); + + if (docAccessible && docAccessible->mNotificationController && + docAccessible->mNotificationController->IsUpdating()) { + return true; + } + } + return false; +} +#endif + +//////////////////////////////////////////////////////////////////////////////// +// DocManager protected + +bool DocManager::Init() { + nsCOMPtr<nsIWebProgress> progress = components::DocLoader::Service(); + + if (!progress) return false; + + progress->AddProgressListener(static_cast<nsIWebProgressListener*>(this), + nsIWebProgress::NOTIFY_STATE_DOCUMENT); + + return true; +} + +void DocManager::Shutdown() { + nsCOMPtr<nsIWebProgress> progress = components::DocLoader::Service(); + + if (progress) { + progress->RemoveProgressListener( + static_cast<nsIWebProgressListener*>(this)); + } + + ClearDocCache(); +} + +//////////////////////////////////////////////////////////////////////////////// +// nsISupports + +NS_IMPL_ISUPPORTS(DocManager, nsIWebProgressListener, nsIDOMEventListener, + nsISupportsWeakReference) + +//////////////////////////////////////////////////////////////////////////////// +// nsIWebProgressListener + +NS_IMETHODIMP +DocManager::OnStateChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest, + uint32_t aStateFlags, nsresult aStatus) { + NS_ASSERTION(aStateFlags & STATE_IS_DOCUMENT, "Other notifications excluded"); + + if (nsAccessibilityService::IsShutdown() || !aWebProgress || + (aStateFlags & (STATE_START | STATE_STOP)) == 0) { + return NS_OK; + } + + nsCOMPtr<mozIDOMWindowProxy> DOMWindow; + aWebProgress->GetDOMWindow(getter_AddRefs(DOMWindow)); + NS_ENSURE_STATE(DOMWindow); + + nsPIDOMWindowOuter* piWindow = nsPIDOMWindowOuter::From(DOMWindow); + MOZ_ASSERT(piWindow); + + nsCOMPtr<Document> document = piWindow->GetDoc(); + NS_ENSURE_STATE(document); + + // Document was loaded. + if (aStateFlags & STATE_STOP) { +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eDocLoad)) { + logging::DocLoad("document loaded", aWebProgress, aRequest, aStateFlags); + } +#endif + + // Figure out an event type to notify the document has been loaded. + uint32_t eventType = nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_STOPPED; + + // Some XUL documents get start state and then stop state with failure + // status when everything is ok. Fire document load complete event in this + // case. + if (NS_SUCCEEDED(aStatus) || !document->IsContentDocument()) { + eventType = nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_COMPLETE; + } + + // If end consumer has been retargeted for loaded content then do not fire + // any event because it means no new document has been loaded, for example, + // it happens when user clicks on file link. + if (aRequest) { + uint32_t loadFlags = 0; + aRequest->GetLoadFlags(&loadFlags); + if (loadFlags & nsIChannel::LOAD_RETARGETED_DOCUMENT_URI) eventType = 0; + } + + HandleDOMDocumentLoad(document, eventType); + return NS_OK; + } + + // Document loading was started. +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eDocLoad)) { + logging::DocLoad("start document loading", aWebProgress, aRequest, + aStateFlags); + } +#endif + + DocAccessible* docAcc = GetExistingDocAccessible(document); + if (!docAcc) return NS_OK; + + nsCOMPtr<nsIWebNavigation> webNav(do_GetInterface(DOMWindow)); + nsCOMPtr<nsIDocShell> docShell(do_QueryInterface(webNav)); + NS_ENSURE_STATE(docShell); + + bool isReloading = false; + uint32_t loadType; + docShell->GetLoadType(&loadType); + if (loadType == LOAD_RELOAD_NORMAL || loadType == LOAD_RELOAD_BYPASS_CACHE || + loadType == LOAD_RELOAD_BYPASS_PROXY || + loadType == LOAD_RELOAD_BYPASS_PROXY_AND_CACHE) { + isReloading = true; + } + + docAcc->NotifyOfLoading(isReloading); + return NS_OK; +} + +NS_IMETHODIMP +DocManager::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; +} + +NS_IMETHODIMP +DocManager::OnLocationChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest, + nsIURI* aLocation, uint32_t aFlags) { + MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)"); + return NS_OK; +} + +NS_IMETHODIMP +DocManager::OnStatusChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest, + nsresult aStatus, const char16_t* aMessage) { + MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)"); + return NS_OK; +} + +NS_IMETHODIMP +DocManager::OnSecurityChange(nsIWebProgress* aWebProgress, nsIRequest* aRequest, + uint32_t aState) { + MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)"); + return NS_OK; +} + +NS_IMETHODIMP +DocManager::OnContentBlockingEvent(nsIWebProgress* aWebProgress, + nsIRequest* aRequest, uint32_t aEvent) { + MOZ_ASSERT_UNREACHABLE("notification excluded in AddProgressListener(...)"); + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// +// nsIDOMEventListener + +NS_IMETHODIMP +DocManager::HandleEvent(Event* aEvent) { + nsAutoString type; + aEvent->GetType(type); + + nsCOMPtr<Document> document = do_QueryInterface(aEvent->GetTarget()); + NS_ASSERTION(document, "pagehide or DOMContentLoaded for non document!"); + if (!document) return NS_OK; + + if (type.EqualsLiteral("pagehide")) { + // 'pagehide' event is registered on every DOM document we create an + // accessible for, process the event for the target. This document + // accessible and all its sub document accessible are shutdown as result of + // processing. + +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eDocDestroy)) { + logging::DocDestroy("received 'pagehide' event", document); + } +#endif + + // Shutdown this one and sub document accessibles. + + // We're allowed to not remove listeners when accessible document is + // shutdown since we don't keep strong reference on chrome event target and + // listeners are removed automatically when chrome event target goes away. + DocAccessible* docAccessible = GetExistingDocAccessible(document); + if (docAccessible) docAccessible->Shutdown(); + + return NS_OK; + } + + // XXX: handle error pages loading separately since they get neither + // webprogress notifications nor 'pageshow' event. + if (type.EqualsLiteral("DOMContentLoaded") && + nsCoreUtils::IsErrorPage(document)) { +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eDocLoad)) { + logging::DocLoad("handled 'DOMContentLoaded' event", document); + } +#endif + + HandleDOMDocumentLoad(document, + nsIAccessibleEvent::EVENT_DOCUMENT_LOAD_COMPLETE); + } + + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// +// DocManager private + +void DocManager::HandleDOMDocumentLoad(Document* aDocument, + uint32_t aLoadEventType) { + // Document accessible can be created before we were notified the DOM document + // was loaded completely. However if it's not created yet then create it. + DocAccessible* docAcc = GetExistingDocAccessible(aDocument); + if (!docAcc) { + docAcc = CreateDocOrRootAccessible(aDocument); + if (!docAcc) return; + } + + docAcc->NotifyOfLoad(aLoadEventType); +} + +void DocManager::AddListeners(Document* aDocument, + bool aAddDOMContentLoadedListener) { + nsPIDOMWindowOuter* window = aDocument->GetWindow(); + EventTarget* target = window->GetChromeEventHandler(); + EventListenerManager* elm = target->GetOrCreateListenerManager(); + elm->AddEventListenerByType(this, u"pagehide"_ns, TrustedEventsAtCapture()); + +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eDocCreate)) { + logging::Text("added 'pagehide' listener"); + } +#endif + + if (aAddDOMContentLoadedListener) { + elm->AddEventListenerByType(this, u"DOMContentLoaded"_ns, + TrustedEventsAtCapture()); +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eDocCreate)) { + logging::Text("added 'DOMContentLoaded' listener"); + } +#endif + } +} + +void DocManager::RemoveListeners(Document* aDocument) { + nsPIDOMWindowOuter* window = aDocument->GetWindow(); + if (!window) return; + + EventTarget* target = window->GetChromeEventHandler(); + if (!target) return; + + EventListenerManager* elm = target->GetOrCreateListenerManager(); + elm->RemoveEventListenerByType(this, u"pagehide"_ns, + TrustedEventsAtCapture()); + + elm->RemoveEventListenerByType(this, u"DOMContentLoaded"_ns, + TrustedEventsAtCapture()); +} + +DocAccessible* DocManager::CreateDocOrRootAccessible(Document* aDocument) { + // Ignore hidden documents, resource documents, static clone + // (printing) documents and documents without a docshell. + if (!nsCoreUtils::IsDocumentVisibleConsideringInProcessAncestors(aDocument) || + aDocument->IsResourceDoc() || aDocument->IsStaticDocument() || + !aDocument->IsActive()) { + return nullptr; + } + + nsIDocShell* docShell = aDocument->GetDocShell(); + if (!docShell || docShell->IsInvisible()) { + return nullptr; + } + + nsIWidget* widget = nsContentUtils::WidgetForDocument(aDocument); + if (!widget || widget->GetWindowType() == widget::WindowType::Invisible) { + return nullptr; + } + + // Ignore documents without presshell. We must not ignore documents with no + // root frame because DOM focus can hit such documents and ignoring them would + // prevent a11y focus. + PresShell* presShell = aDocument->GetPresShell(); + if (!presShell || presShell->IsDestroying()) { + return nullptr; + } + + bool isRootDoc = nsCoreUtils::IsRootDocument(aDocument); + + DocAccessible* parentDocAcc = nullptr; + if (!isRootDoc) { + // XXXaaronl: ideally we would traverse the presshell chain. Since there's + // no easy way to do that, we cheat and use the document hierarchy. + parentDocAcc = GetDocAccessible(aDocument->GetInProcessParentDocument()); + NS_ASSERTION(parentDocAcc, "Can't create an accessible for the document!"); + if (!parentDocAcc) return nullptr; + } + + // We only create root accessibles for the true root, otherwise create a + // doc accessible. + RefPtr<DocAccessible> docAcc = + isRootDoc ? new RootAccessibleWrap(aDocument, presShell) + : new DocAccessibleWrap(aDocument, presShell); + + // Cache the document accessible into document cache. + mDocAccessibleCache.InsertOrUpdate(aDocument, RefPtr{docAcc}); + + // Initialize the document accessible. + docAcc->Init(); + + // Bind the document to the tree. + if (isRootDoc) { + if (!ApplicationAcc()->AppendChild(docAcc)) { + docAcc->Shutdown(); + return nullptr; + } + + // Fire reorder event to notify new accessible document has been attached to + // the tree. The reorder event is delivered after the document tree is + // constructed because event processing and tree construction are done by + // the same document. + // Note: don't use AccReorderEvent to avoid coalsecense and special reorder + // events processing. + docAcc->FireDelayedEvent(nsIAccessibleEvent::EVENT_REORDER, + ApplicationAcc()); + + } else { + parentDocAcc->BindChildDocument(docAcc); + } + +#ifdef A11Y_LOG + if (logging::IsEnabled(logging::eDocCreate)) { + logging::DocCreate("document creation finished", aDocument); + logging::Stack(); + } +#endif + + AddListeners(aDocument, isRootDoc); + return docAcc; +} + +//////////////////////////////////////////////////////////////////////////////// +// DocManager static + +void DocManager::ClearDocCache() { + while (mDocAccessibleCache.Count() > 0) { + auto iter = mDocAccessibleCache.Iter(); + MOZ_ASSERT(!iter.Done()); + DocAccessible* docAcc = iter.UserData(); + NS_ASSERTION(docAcc, + "No doc accessible for the object in doc accessible cache!"); + if (docAcc) { + docAcc->Shutdown(); + } + + iter.Remove(); + } + + // Ensure that all xpcom accessible documents are shut down as well. + while (mXPCDocumentCache.Count() > 0) { + auto iter = mXPCDocumentCache.Iter(); + MOZ_ASSERT(!iter.Done()); + xpcAccessibleDocument* xpcDoc = iter.UserData(); + NS_ASSERTION(xpcDoc, "No xpc doc for the object in xpc doc cache!"); + + if (xpcDoc) { + xpcDoc->Shutdown(); + } + + iter.Remove(); + } +} + +void DocManager::RemoteDocAdded(DocAccessibleParent* aDoc) { + if (!sRemoteDocuments) { + sRemoteDocuments = new nsTArray<DocAccessibleParent*>; + ClearOnShutdown(&sRemoteDocuments); + } + + MOZ_ASSERT(!sRemoteDocuments->Contains(aDoc), + "How did we already have the doc!"); + sRemoteDocuments->AppendElement(aDoc); + ProxyCreated(aDoc); +} + +DocAccessible* mozilla::a11y::GetExistingDocAccessible( + const dom::Document* aDocument) { + PresShell* presShell = aDocument->GetPresShell(); + return presShell ? presShell->GetDocAccessible() : nullptr; +} |