diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /mobile/android/components/geckoview | |
parent | Initial commit. (diff) | |
download | firefox-esr-upstream.tar.xz firefox-esr-upstream.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
21 files changed, 3464 insertions, 0 deletions
diff --git a/mobile/android/components/geckoview/ColorPickerDelegate.jsm b/mobile/android/components/geckoview/ColorPickerDelegate.jsm new file mode 100644 index 0000000000..b72662a316 --- /dev/null +++ b/mobile/android/components/geckoview/ColorPickerDelegate.jsm @@ -0,0 +1,43 @@ +/* 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/. */ +"use strict"; + +var EXPORTED_SYMBOLS = ["ColorPickerDelegate"]; + +const { GeckoViewUtils } = ChromeUtils.importESModule( + "resource://gre/modules/GeckoViewUtils.sys.mjs" +); + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + GeckoViewPrompter: "resource://gre/modules/GeckoViewPrompter.sys.mjs", +}); + +const { debug, warn } = GeckoViewUtils.initLogging("ColorPickerDelegate"); + +class ColorPickerDelegate { + // TODO(bug 1805397): Implement default colors + init(aParent, aTitle, aInitialColor, aDefaultColors) { + this._prompt = new lazy.GeckoViewPrompter(aParent); + this._msg = { + type: "color", + title: aTitle, + value: aInitialColor, + predefinedValues: aDefaultColors, + }; + } + + open(aColorPickerShownCallback) { + this._prompt.asyncShowPrompt(this._msg, result => { + // OK: result + // Cancel: !result + aColorPickerShownCallback.done((result && result.color) || ""); + }); + } +} + +ColorPickerDelegate.prototype.QueryInterface = ChromeUtils.generateQI([ + "nsIColorPicker", +]); diff --git a/mobile/android/components/geckoview/FilePickerDelegate.jsm b/mobile/android/components/geckoview/FilePickerDelegate.jsm new file mode 100644 index 0000000000..32966b65e6 --- /dev/null +++ b/mobile/android/components/geckoview/FilePickerDelegate.jsm @@ -0,0 +1,194 @@ +/* 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/. */ +"use strict"; + +var EXPORTED_SYMBOLS = ["FilePickerDelegate"]; + +const { GeckoViewUtils } = ChromeUtils.importESModule( + "resource://gre/modules/GeckoViewUtils.sys.mjs" +); + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + FileUtils: "resource://gre/modules/FileUtils.sys.mjs", + GeckoViewPrompter: "resource://gre/modules/GeckoViewPrompter.sys.mjs", +}); + +const { debug, warn } = GeckoViewUtils.initLogging("FilePickerDelegate"); + +class FilePickerDelegate { + /* ---------- nsIFilePicker ---------- */ + init(aParent, aTitle, aMode) { + if ( + aMode === Ci.nsIFilePicker.modeGetFolder || + aMode === Ci.nsIFilePicker.modeSave + ) { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + } + this._prompt = new lazy.GeckoViewPrompter(aParent); + this._msg = { + type: "file", + title: aTitle, + mode: aMode === Ci.nsIFilePicker.modeOpenMultiple ? "multiple" : "single", + }; + this._mode = aMode; + this._mimeTypes = []; + this._capture = 0; + } + + get mode() { + return this._mode; + } + + appendRawFilter(aFilter) { + this._mimeTypes.push(aFilter); + } + + show() { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + } + + open(aFilePickerShownCallback) { + this._msg.mimeTypes = this._mimeTypes; + this._msg.capture = this._capture; + this._prompt.asyncShowPrompt(this._msg, result => { + // OK: result + // Cancel: !result + if (!result || !result.files || !result.files.length) { + aFilePickerShownCallback.done(Ci.nsIFilePicker.returnCancel); + } else { + this._resolveFiles(result.files, aFilePickerShownCallback); + } + }); + } + + async _resolveFiles(aFiles, aCallback) { + const fileData = []; + + try { + for (const file of aFiles) { + const domFile = await this._getDOMFile(file); + fileData.push({ + file, + domFile, + }); + } + } catch (ex) { + warn`Error resolving files from file picker: ${ex}`; + aCallback.done(Ci.nsIFilePicker.returnCancel); + return; + } + + this._fileData = fileData; + aCallback.done(Ci.nsIFilePicker.returnOK); + } + + get file() { + if (!this._fileData) { + throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE); + } + const fileData = this._fileData[0]; + if (!fileData) { + return null; + } + return new lazy.FileUtils.File(fileData.file); + } + + get fileURL() { + return Services.io.newFileURI(this.file); + } + + *_getEnumerator(aDOMFile) { + if (!this._fileData) { + throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE); + } + + for (const fileData of this._fileData) { + if (aDOMFile) { + yield fileData.domFile; + } + yield new lazy.FileUtils.File(fileData.file); + } + } + + get files() { + return this._getEnumerator(/* aDOMFile */ false); + } + + _getDOMFile(aPath) { + if (this._prompt.domWin) { + return this._prompt.domWin.File.createFromFileName(aPath); + } + return File.createFromFileName(aPath); + } + + get domFileOrDirectory() { + if (!this._fileData) { + throw Components.Exception("", Cr.NS_ERROR_NOT_AVAILABLE); + } + return this._fileData[0] ? this._fileData[0].domFile : null; + } + + get domFileOrDirectoryEnumerator() { + return this._getEnumerator(/* aDOMFile */ true); + } + + get defaultString() { + return ""; + } + + set defaultString(aValue) {} + + get defaultExtension() { + return ""; + } + + set defaultExtension(aValue) {} + + get filterIndex() { + return 0; + } + + set filterIndex(aValue) {} + + get displayDirectory() { + return null; + } + + set displayDirectory(aValue) {} + + get displaySpecialDirectory() { + return ""; + } + + set displaySpecialDirectory(aValue) {} + + get addToRecentDocs() { + return false; + } + + set addToRecentDocs(aValue) {} + + get okButtonLabel() { + return ""; + } + + set okButtonLabel(aValue) {} + + get capture() { + return this._capture; + } + + set capture(aValue) { + this._capture = aValue; + } +} + +FilePickerDelegate.prototype.classID = Components.ID( + "{e4565e36-f101-4bf5-950b-4be0887785a9}" +); +FilePickerDelegate.prototype.QueryInterface = ChromeUtils.generateQI([ + "nsIFilePicker", +]); diff --git a/mobile/android/components/geckoview/GeckoView.manifest b/mobile/android/components/geckoview/GeckoView.manifest new file mode 100644 index 0000000000..472c4d7298 --- /dev/null +++ b/mobile/android/components/geckoview/GeckoView.manifest @@ -0,0 +1,4 @@ +# GeckoViewStartup.js +category app-startup GeckoViewStartup @mozilla.org/geckoview/startup;1 process=main +category content-process-ready-for-script GeckoViewStartup @mozilla.org/geckoview/startup;1 process=content +category profile-after-change GeckoViewStartup @mozilla.org/geckoview/startup;1 process=main diff --git a/mobile/android/components/geckoview/GeckoViewExternalAppService.cpp b/mobile/android/components/geckoview/GeckoViewExternalAppService.cpp new file mode 100644 index 0000000000..30dedc9251 --- /dev/null +++ b/mobile/android/components/geckoview/GeckoViewExternalAppService.cpp @@ -0,0 +1,100 @@ +/* -*- 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 "GeckoViewExternalAppService.h" + +#include "mozilla/dom/BrowsingContext.h" +#include "mozilla/dom/CanonicalBrowsingContext.h" +#include "mozilla/dom/WindowGlobalParent.h" +#include "nsIChannel.h" + +#include "mozilla/widget/EventDispatcher.h" +#include "mozilla/widget/nsWindow.h" +#include "GeckoViewStreamListener.h" + +#include "JavaBuiltins.h" + +class StreamListener final : public mozilla::GeckoViewStreamListener { + public: + explicit StreamListener(nsWindow* aWindow) + : GeckoViewStreamListener(), mWindow(aWindow) {} + + void SendWebResponse(mozilla::java::WebResponse::Param aResponse) { + mWindow->PassExternalResponse(aResponse); + } + + void CompleteWithError(nsresult aStatus, nsIChannel* aChannel) { + // Currently we don't do anything about errors here + } + + virtual ~StreamListener() {} + + private: + RefPtr<nsWindow> mWindow; +}; + +mozilla::StaticRefPtr<GeckoViewExternalAppService> + GeckoViewExternalAppService::sService; + +/* static */ +already_AddRefed<GeckoViewExternalAppService> +GeckoViewExternalAppService::GetSingleton() { + if (!sService) { + sService = new GeckoViewExternalAppService(); + } + RefPtr<GeckoViewExternalAppService> service = sService; + return service.forget(); +} + +GeckoViewExternalAppService::GeckoViewExternalAppService() {} + +NS_IMPL_ISUPPORTS(GeckoViewExternalAppService, nsIExternalHelperAppService); + +NS_IMETHODIMP GeckoViewExternalAppService::DoContent( + const nsACString& aMimeContentType, nsIRequest* aRequest, + nsIInterfaceRequestor* aContentContext, bool aForceSave, + nsIInterfaceRequestor* aWindowContext, + nsIStreamListener** aStreamListener) { + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP GeckoViewExternalAppService::CreateListener( + const nsACString& aMimeContentType, nsIRequest* aRequest, + mozilla::dom::BrowsingContext* aContentContext, bool aForceSave, + nsIInterfaceRequestor* aWindowContext, + nsIStreamListener** aStreamListener) { + using namespace mozilla; + using namespace mozilla::dom; + MOZ_ASSERT(XRE_IsParentProcess()); + + nsresult rv; + nsCOMPtr<nsIChannel> channel(do_QueryInterface(aRequest, &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIWidget> widget = + aContentContext->Canonical()->GetParentProcessWidgetContaining(); + if (!widget) { + return NS_ERROR_ABORT; + } + + RefPtr<nsWindow> window = nsWindow::From(widget); + MOZ_ASSERT(window); + + RefPtr<StreamListener> listener = new StreamListener(window); + + rv = channel->SetNotificationCallbacks(listener); + NS_ENSURE_SUCCESS(rv, rv); + + listener.forget(aStreamListener); + return NS_OK; +} + +NS_IMETHODIMP GeckoViewExternalAppService::ApplyDecodingForExtension( + const nsACString& aExtension, const nsACString& aEncodingType, + bool* aApplyDecoding) { + // This currently doesn't matter, because we never read the stream. + *aApplyDecoding = true; + return NS_OK; +} diff --git a/mobile/android/components/geckoview/GeckoViewExternalAppService.h b/mobile/android/components/geckoview/GeckoViewExternalAppService.h new file mode 100644 index 0000000000..1dfb7c9491 --- /dev/null +++ b/mobile/android/components/geckoview/GeckoViewExternalAppService.h @@ -0,0 +1,26 @@ +/* -*- 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 GeckoViewExternalAppService_h__ +#define GeckoViewExternalAppService_h__ + +#include "nsIExternalHelperAppService.h" +#include "mozilla/StaticPtr.h" + +class GeckoViewExternalAppService : public nsIExternalHelperAppService { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIEXTERNALHELPERAPPSERVICE + + GeckoViewExternalAppService(); + + static already_AddRefed<GeckoViewExternalAppService> GetSingleton(); + + private: + virtual ~GeckoViewExternalAppService() {} + static mozilla::StaticRefPtr<GeckoViewExternalAppService> sService; +}; + +#endif diff --git a/mobile/android/components/geckoview/GeckoViewHistory.cpp b/mobile/android/components/geckoview/GeckoViewHistory.cpp new file mode 100644 index 0000000000..c69e419b18 --- /dev/null +++ b/mobile/android/components/geckoview/GeckoViewHistory.cpp @@ -0,0 +1,508 @@ +/* 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 "GeckoViewHistory.h" + +#include "JavaBuiltins.h" +#include "jsapi.h" +#include "js/Array.h" // JS::GetArrayLength, JS::IsArrayObject +#include "js/PropertyAndElement.h" // JS_GetElement +#include "nsIURI.h" +#include "nsXULAppAPI.h" + +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/ResultExtensions.h" +#include "mozilla/StaticPrefs_layout.h" + +#include "mozilla/dom/ContentParent.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/Link.h" +#include "mozilla/dom/BrowserChild.h" + +#include "mozilla/ipc/URIUtils.h" + +#include "mozilla/widget/EventDispatcher.h" +#include "mozilla/widget/nsWindow.h" + +using namespace mozilla; +using namespace mozilla::dom; +using namespace mozilla::ipc; +using namespace mozilla::widget; + +static const char16_t kOnVisitedMessage[] = u"GeckoView:OnVisited"; +static const char16_t kGetVisitedMessage[] = u"GeckoView:GetVisited"; + +// Keep in sync with `GeckoSession.HistoryDelegate.VisitFlags`. +enum class GeckoViewVisitFlags : int32_t { + VISIT_TOP_LEVEL = 1 << 0, + VISIT_REDIRECT_TEMPORARY = 1 << 1, + VISIT_REDIRECT_PERMANENT = 1 << 2, + VISIT_REDIRECT_SOURCE = 1 << 3, + VISIT_REDIRECT_SOURCE_PERMANENT = 1 << 4, + VISIT_UNRECOVERABLE_ERROR = 1 << 5, +}; + +GeckoViewHistory::GeckoViewHistory() {} + +GeckoViewHistory::~GeckoViewHistory() {} + +NS_IMPL_ISUPPORTS(GeckoViewHistory, IHistory) + +StaticRefPtr<GeckoViewHistory> GeckoViewHistory::sHistory; + +/* static */ +already_AddRefed<GeckoViewHistory> GeckoViewHistory::GetSingleton() { + if (!sHistory) { + sHistory = new GeckoViewHistory(); + ClearOnShutdown(&sHistory); + } + RefPtr<GeckoViewHistory> history = sHistory; + return history.forget(); +} + +// Handles a request to fetch visited statuses for new tracked URIs in the +// content process (e10s). +void GeckoViewHistory::QueryVisitedStateInContentProcess( + const PendingVisitedQueries& aQueries) { + // Holds an array of new tracked URIs for a tab in the content process. + struct NewURIEntry { + explicit NewURIEntry(BrowserChild* aBrowserChild, nsIURI* aURI) + : mBrowserChild(aBrowserChild) { + AddURI(aURI); + } + + void AddURI(nsIURI* aURI) { mURIs.AppendElement(aURI); } + + BrowserChild* mBrowserChild; + nsTArray<RefPtr<nsIURI>> mURIs; + }; + + MOZ_ASSERT(XRE_IsContentProcess()); + + // First, serialize all the new URIs that we need to look up. Note that this + // could be written as `nsTHashMap<nsUint64HashKey, + // nsTArray<URIParams>` instead, but, since we don't expect to have many tab + // children, we can avoid the cost of hashing. + AutoTArray<NewURIEntry, 8> newEntries; + for (auto& query : aQueries) { + nsIURI* uri = query.GetKey(); + MOZ_ASSERT(query.GetData().IsEmpty(), + "Shouldn't have parents to notify in child processes"); + auto entry = mTrackedURIs.Lookup(uri); + if (!entry) { + continue; + } + ObservingLinks& links = entry.Data(); + for (Link* link : links.mLinks.BackwardRange()) { + nsIWidget* widget = nsContentUtils::WidgetForContent(link->GetElement()); + if (!widget) { + continue; + } + BrowserChild* browserChild = widget->GetOwningBrowserChild(); + if (!browserChild) { + continue; + } + // Add to the list of new URIs for this document, or make a new entry. + bool hasEntry = false; + for (NewURIEntry& entry : newEntries) { + if (entry.mBrowserChild == browserChild) { + entry.AddURI(uri); + hasEntry = true; + break; + } + } + if (!hasEntry) { + newEntries.AppendElement(NewURIEntry(browserChild, uri)); + } + } + } + + // Send the request to the parent process, one message per tab child. + for (const NewURIEntry& entry : newEntries) { + Unused << NS_WARN_IF( + !entry.mBrowserChild->SendQueryVisitedState(entry.mURIs)); + } +} + +// Handles a request to fetch visited statuses for new tracked URIs in the +// parent process (non-e10s). +void GeckoViewHistory::QueryVisitedStateInParentProcess( + const PendingVisitedQueries& aQueries) { + // Holds an array of new URIs for a window in the parent process. Unlike + // the content process case, we don't need to track tab children, since we + // have the outer window and can send the request directly to Java. + struct NewURIEntry { + explicit NewURIEntry(nsIWidget* aWidget, nsIURI* aURI) : mWidget(aWidget) { + AddURI(aURI); + } + + void AddURI(nsIURI* aURI) { mURIs.AppendElement(aURI); } + + nsCOMPtr<nsIWidget> mWidget; + nsTArray<RefPtr<nsIURI>> mURIs; + }; + + MOZ_ASSERT(XRE_IsParentProcess()); + + nsTArray<NewURIEntry> newEntries; + for (const auto& query : aQueries) { + nsIURI* uri = query.GetKey(); + auto entry = mTrackedURIs.Lookup(uri); + if (!entry) { + continue; // Nobody cares about this uri anymore. + } + + ObservingLinks& links = entry.Data(); + nsTObserverArray<Link*>::BackwardIterator linksIter(links.mLinks); + while (linksIter.HasMore()) { + Link* link = linksIter.GetNext(); + + nsIWidget* widget = nsContentUtils::WidgetForContent(link->GetElement()); + if (!widget) { + continue; + } + + bool hasEntry = false; + for (NewURIEntry& entry : newEntries) { + if (entry.mWidget != widget) { + continue; + } + entry.AddURI(uri); + hasEntry = true; + } + if (!hasEntry) { + newEntries.AppendElement(NewURIEntry(widget, uri)); + } + } + } + + for (NewURIEntry& entry : newEntries) { + QueryVisitedState(entry.mWidget, nullptr, std::move(entry.mURIs)); + } +} + +void GeckoViewHistory::StartPendingVisitedQueries( + PendingVisitedQueries&& aQueries) { + if (XRE_IsContentProcess()) { + QueryVisitedStateInContentProcess(aQueries); + } else { + QueryVisitedStateInParentProcess(aQueries); + } +} + +/** + * Called from the session handler for the history delegate, after the new + * visit is recorded. + */ +class OnVisitedCallback final : public nsIAndroidEventCallback { + public: + explicit OnVisitedCallback(GeckoViewHistory* aHistory, nsIURI* aURI) + : mHistory(aHistory), mURI(aURI) {} + + NS_DECL_ISUPPORTS + + NS_IMETHOD + OnSuccess(JS::Handle<JS::Value> aData, JSContext* aCx) override { + Maybe<bool> visitedState = GetVisitedValue(aCx, aData); + JS_ClearPendingException(aCx); + if (visitedState) { + AutoTArray<VisitedURI, 1> visitedURIs; + visitedURIs.AppendElement(VisitedURI{mURI.get(), *visitedState}); + mHistory->HandleVisitedState(visitedURIs, nullptr); + } + return NS_OK; + } + + NS_IMETHOD + OnError(JS::Handle<JS::Value> aData, JSContext* aCx) override { + return NS_OK; + } + + private: + virtual ~OnVisitedCallback() {} + + Maybe<bool> GetVisitedValue(JSContext* aCx, JS::Handle<JS::Value> aData) { + if (NS_WARN_IF(!aData.isBoolean())) { + return Nothing(); + } + return Some(aData.toBoolean()); + } + + RefPtr<GeckoViewHistory> mHistory; + nsCOMPtr<nsIURI> mURI; +}; + +NS_IMPL_ISUPPORTS(OnVisitedCallback, nsIAndroidEventCallback) + +NS_IMETHODIMP +GeckoViewHistory::VisitURI(nsIWidget* aWidget, nsIURI* aURI, + nsIURI* aLastVisitedURI, uint32_t aFlags, + uint64_t aBrowserId) { + if (!aURI) { + return NS_OK; + } + + if (XRE_IsContentProcess()) { + // If we're in the content process, send the visit to the parent. The parent + // will find the matching chrome window for the content process and tab, + // then forward the visit to Java. + if (NS_WARN_IF(!aWidget)) { + return NS_OK; + } + BrowserChild* browserChild = aWidget->GetOwningBrowserChild(); + if (NS_WARN_IF(!browserChild)) { + return NS_OK; + } + Unused << NS_WARN_IF( + !browserChild->SendVisitURI(aURI, aLastVisitedURI, aFlags, aBrowserId)); + return NS_OK; + } + + // Otherwise, we're in the parent process. Wrap the URIs up in a bundle, and + // send them to Java. + MOZ_ASSERT(XRE_IsParentProcess()); + RefPtr<nsWindow> window = nsWindow::From(aWidget); + if (NS_WARN_IF(!window)) { + return NS_OK; + } + widget::EventDispatcher* dispatcher = window->GetEventDispatcher(); + if (NS_WARN_IF(!dispatcher)) { + return NS_OK; + } + + // If nobody is listening for this, we can stop now. + if (!dispatcher->HasListener(kOnVisitedMessage)) { + return NS_OK; + } + + AutoTArray<jni::String::LocalRef, 3> keys; + AutoTArray<jni::Object::LocalRef, 3> values; + + nsAutoCString uriSpec; + if (NS_WARN_IF(NS_FAILED(aURI->GetSpec(uriSpec)))) { + return NS_OK; + } + keys.AppendElement(jni::StringParam(u"url"_ns)); + values.AppendElement(jni::StringParam(uriSpec)); + + if (aLastVisitedURI) { + nsAutoCString lastVisitedURISpec; + if (NS_WARN_IF(NS_FAILED(aLastVisitedURI->GetSpec(lastVisitedURISpec)))) { + return NS_OK; + } + keys.AppendElement(jni::StringParam(u"lastVisitedURL"_ns)); + values.AppendElement(jni::StringParam(lastVisitedURISpec)); + } + + int32_t flags = 0; + if (aFlags & TOP_LEVEL) { + flags |= static_cast<int32_t>(GeckoViewVisitFlags::VISIT_TOP_LEVEL); + } + if (aFlags & REDIRECT_TEMPORARY) { + flags |= + static_cast<int32_t>(GeckoViewVisitFlags::VISIT_REDIRECT_TEMPORARY); + } + if (aFlags & REDIRECT_PERMANENT) { + flags |= + static_cast<int32_t>(GeckoViewVisitFlags::VISIT_REDIRECT_PERMANENT); + } + if (aFlags & REDIRECT_SOURCE) { + flags |= static_cast<int32_t>(GeckoViewVisitFlags::VISIT_REDIRECT_SOURCE); + } + if (aFlags & REDIRECT_SOURCE_PERMANENT) { + flags |= static_cast<int32_t>( + GeckoViewVisitFlags::VISIT_REDIRECT_SOURCE_PERMANENT); + } + if (aFlags & UNRECOVERABLE_ERROR) { + flags |= + static_cast<int32_t>(GeckoViewVisitFlags::VISIT_UNRECOVERABLE_ERROR); + } + keys.AppendElement(jni::StringParam(u"flags"_ns)); + values.AppendElement(java::sdk::Integer::ValueOf(flags)); + + MOZ_ASSERT(keys.Length() == values.Length()); + + auto bundleKeys = jni::ObjectArray::New<jni::String>(keys.Length()); + auto bundleValues = jni::ObjectArray::New<jni::Object>(values.Length()); + for (size_t i = 0; i < keys.Length(); ++i) { + bundleKeys->SetElement(i, keys[i]); + bundleValues->SetElement(i, values[i]); + } + auto bundle = java::GeckoBundle::New(bundleKeys, bundleValues); + + nsCOMPtr<nsIAndroidEventCallback> callback = + new OnVisitedCallback(this, aURI); + + Unused << NS_WARN_IF( + NS_FAILED(dispatcher->Dispatch(kOnVisitedMessage, bundle, callback))); + + return NS_OK; +} + +NS_IMETHODIMP +GeckoViewHistory::SetURITitle(nsIURI* aURI, const nsAString& aTitle) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +/** + * Called from the session handler for the history delegate, with visited + * statuses for all requested URIs. + */ +class GetVisitedCallback final : public nsIAndroidEventCallback { + public: + explicit GetVisitedCallback(GeckoViewHistory* aHistory, + ContentParent* aInterestedProcess, + nsTArray<RefPtr<nsIURI>>&& aURIs) + : mHistory(aHistory), + mInterestedProcess(aInterestedProcess), + mURIs(std::move(aURIs)) {} + + NS_DECL_ISUPPORTS + + NS_IMETHOD + OnSuccess(JS::Handle<JS::Value> aData, JSContext* aCx) override { + nsTArray<VisitedURI> visitedURIs; + if (!ExtractVisitedURIs(aCx, aData, visitedURIs)) { + JS_ClearPendingException(aCx); + return NS_ERROR_FAILURE; + } + IHistory::ContentParentSet interestedProcesses; + if (mInterestedProcess) { + interestedProcesses.Insert(mInterestedProcess); + } + mHistory->HandleVisitedState(visitedURIs, &interestedProcesses); + return NS_OK; + } + + NS_IMETHOD + OnError(JS::Handle<JS::Value> aData, JSContext* aCx) override { + return NS_OK; + } + + private: + virtual ~GetVisitedCallback() {} + + /** + * Unpacks an array of Boolean visited statuses from the session handler into + * an array of `VisitedURI` structs. Each element in the array corresponds to + * a URI in `mURIs`. + * + * Returns `false` on error, `true` if the array is `null` or was successfully + * unpacked. + * + * TODO (bug 1503482): Remove this unboxing. + */ + bool ExtractVisitedURIs(JSContext* aCx, JS::Handle<JS::Value> aData, + nsTArray<VisitedURI>& aVisitedURIs) { + if (aData.isNull()) { + return true; + } + bool isArray = false; + if (NS_WARN_IF(!JS::IsArrayObject(aCx, aData, &isArray))) { + return false; + } + if (NS_WARN_IF(!isArray)) { + return false; + } + JS::Rooted<JSObject*> visited(aCx, &aData.toObject()); + uint32_t length = 0; + if (NS_WARN_IF(!JS::GetArrayLength(aCx, visited, &length))) { + return false; + } + if (NS_WARN_IF(length != mURIs.Length())) { + return false; + } + if (!aVisitedURIs.SetCapacity(length, mozilla::fallible)) { + return false; + } + for (uint32_t i = 0; i < length; ++i) { + JS::Rooted<JS::Value> value(aCx); + if (NS_WARN_IF(!JS_GetElement(aCx, visited, i, &value))) { + JS_ClearPendingException(aCx); + aVisitedURIs.AppendElement(VisitedURI{mURIs[i].get(), false}); + continue; + } + if (NS_WARN_IF(!value.isBoolean())) { + aVisitedURIs.AppendElement(VisitedURI{mURIs[i].get(), false}); + continue; + } + aVisitedURIs.AppendElement(VisitedURI{mURIs[i].get(), value.toBoolean()}); + } + return true; + } + + RefPtr<GeckoViewHistory> mHistory; + RefPtr<ContentParent> mInterestedProcess; + nsTArray<RefPtr<nsIURI>> mURIs; +}; + +NS_IMPL_ISUPPORTS(GetVisitedCallback, nsIAndroidEventCallback) + +/** + * Queries the history delegate to find which URIs have been visited. This + * is always called in the parent process: from `GetVisited` in non-e10s, and + * from `ContentParent::RecvGetVisited` in e10s. + */ +void GeckoViewHistory::QueryVisitedState(nsIWidget* aWidget, + ContentParent* aInterestedProcess, + nsTArray<RefPtr<nsIURI>>&& aURIs) { + MOZ_ASSERT(XRE_IsParentProcess()); + RefPtr<nsWindow> window = nsWindow::From(aWidget); + if (NS_WARN_IF(!window)) { + return; + } + widget::EventDispatcher* dispatcher = window->GetEventDispatcher(); + if (NS_WARN_IF(!dispatcher)) { + return; + } + + // If nobody is listening for this we can stop now + if (!dispatcher->HasListener(kGetVisitedMessage)) { + return; + } + + // Assemble a bundle like `{ urls: ["http://example.com/1", ...] }`. + auto uris = jni::ObjectArray::New<jni::String>(aURIs.Length()); + for (size_t i = 0; i < aURIs.Length(); ++i) { + nsAutoCString uriSpec; + if (NS_WARN_IF(NS_FAILED(aURIs[i]->GetSpec(uriSpec)))) { + continue; + } + jni::String::LocalRef value{jni::StringParam(uriSpec)}; + uris->SetElement(i, value); + } + + auto bundleKeys = jni::ObjectArray::New<jni::String>(1); + jni::String::LocalRef key(jni::StringParam(u"urls"_ns)); + bundleKeys->SetElement(0, key); + + auto bundleValues = jni::ObjectArray::New<jni::Object>(1); + jni::Object::LocalRef value(uris); + bundleValues->SetElement(0, value); + + auto bundle = java::GeckoBundle::New(bundleKeys, bundleValues); + + nsCOMPtr<nsIAndroidEventCallback> callback = + new GetVisitedCallback(this, aInterestedProcess, std::move(aURIs)); + + Unused << NS_WARN_IF( + NS_FAILED(dispatcher->Dispatch(kGetVisitedMessage, bundle, callback))); +} + +/** + * Updates link states for all tracked links, forwarding the visited statuses to + * the content process in e10s. This is always called in the parent process, + * from `VisitedCallback::OnSuccess` and `GetVisitedCallback::OnSuccess`. + */ +void GeckoViewHistory::HandleVisitedState( + const nsTArray<VisitedURI>& aVisitedURIs, + ContentParentSet* aInterestedProcesses) { + MOZ_ASSERT(XRE_IsParentProcess()); + + for (const VisitedURI& visitedURI : aVisitedURIs) { + auto status = + visitedURI.mVisited ? VisitedStatus::Visited : VisitedStatus::Unvisited; + NotifyVisited(visitedURI.mURI, status, aInterestedProcesses); + } +} diff --git a/mobile/android/components/geckoview/GeckoViewHistory.h b/mobile/android/components/geckoview/GeckoViewHistory.h new file mode 100644 index 0000000000..a3b96ba58f --- /dev/null +++ b/mobile/android/components/geckoview/GeckoViewHistory.h @@ -0,0 +1,60 @@ +/* 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 GECKOVIEWHISTORY_H +#define GECKOVIEWHISTORY_H + +#include "mozilla/BaseHistory.h" +#include "nsTObserverArray.h" +#include "nsURIHashKey.h" +#include "nsINamed.h" +#include "nsITimer.h" +#include "nsIURI.h" + +#include "mozilla/StaticPtr.h" + +class nsIWidget; + +namespace mozilla { +namespace dom { +class Document; +} +} // namespace mozilla + +struct VisitedURI { + nsCOMPtr<nsIURI> mURI; + bool mVisited = false; +}; + +class GeckoViewHistory final : public mozilla::BaseHistory { + public: + NS_DECL_ISUPPORTS + + // IHistory + NS_IMETHOD VisitURI(nsIWidget*, nsIURI*, nsIURI* aLastVisitedURI, + uint32_t aFlags, uint64_t aBrowserId) final; + NS_IMETHOD SetURITitle(nsIURI*, const nsAString&) final; + + static already_AddRefed<GeckoViewHistory> GetSingleton(); + + void StartPendingVisitedQueries(PendingVisitedQueries&&) final; + + GeckoViewHistory(); + + void QueryVisitedState(nsIWidget* aWidget, + mozilla::dom::ContentParent* aInterestedProcess, + nsTArray<RefPtr<nsIURI>>&& aURIs); + void HandleVisitedState(const nsTArray<VisitedURI>& aVisitedURIs, + ContentParentSet* aInterestedProcesses); + + private: + virtual ~GeckoViewHistory(); + + void QueryVisitedStateInContentProcess(const PendingVisitedQueries&); + void QueryVisitedStateInParentProcess(const PendingVisitedQueries&); + + static mozilla::StaticRefPtr<GeckoViewHistory> sHistory; +}; + +#endif diff --git a/mobile/android/components/geckoview/GeckoViewOutputStream.cpp b/mobile/android/components/geckoview/GeckoViewOutputStream.cpp new file mode 100644 index 0000000000..6368363f59 --- /dev/null +++ b/mobile/android/components/geckoview/GeckoViewOutputStream.cpp @@ -0,0 +1,61 @@ +/* -*- Mode: c++; c-basic-offset: 2; tab-width: 2; indent-tabs-mode: nil; -*- + * 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 "GeckoViewOutputStream.h" +#include "mozilla/fallible.h" + +using namespace mozilla; + +NS_IMPL_ISUPPORTS(GeckoViewOutputStream, nsIOutputStream); + +NS_IMETHODIMP +GeckoViewOutputStream::Close() { + mStream->SendEof(); + return NS_OK; +} + +NS_IMETHODIMP +GeckoViewOutputStream::Flush() { return NS_ERROR_NOT_IMPLEMENTED; } + +NS_IMETHODIMP +GeckoViewOutputStream::StreamStatus() { + return mStream->IsStreamClosed() ? NS_BASE_STREAM_CLOSED : NS_OK; +} + +NS_IMETHODIMP +GeckoViewOutputStream::Write(const char* buf, uint32_t count, + uint32_t* retval) { + jni::ByteArray::LocalRef buffer = jni::ByteArray::New( + reinterpret_cast<const int8_t*>(buf), count, fallible); + if (!buffer) { + return NS_ERROR_OUT_OF_MEMORY; + } + if (NS_FAILED(mStream->AppendBuffer(buffer))) { + // The stream was closed, abort reading this channel. + return NS_BASE_STREAM_CLOSED; + } + // Return amount of bytes written + *retval = count; + + return NS_OK; +} + +NS_IMETHODIMP +GeckoViewOutputStream::WriteFrom(nsIInputStream* fromStream, uint32_t count, + uint32_t* retval) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +GeckoViewOutputStream::WriteSegments(nsReadSegmentFun reader, void* closure, + uint32_t count, uint32_t* retval) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +GeckoViewOutputStream::IsNonBlocking(bool* retval) { + *retval = true; + return NS_OK; +} diff --git a/mobile/android/components/geckoview/GeckoViewOutputStream.h b/mobile/android/components/geckoview/GeckoViewOutputStream.h new file mode 100644 index 0000000000..70ab8a9198 --- /dev/null +++ b/mobile/android/components/geckoview/GeckoViewOutputStream.h @@ -0,0 +1,29 @@ + +/* -*- Mode: c++; c-basic-offset: 2; tab-width: 2; indent-tabs-mode: nil; -*- + * 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 GeckoViewOutputStream_h__ +#define GeckoViewOutputStream_h__ + +#include "mozilla/java/GeckoInputStreamNatives.h" +#include "mozilla/java/GeckoInputStreamWrappers.h" + +#include "nsIOutputStream.h" +#include "nsIRequest.h" + +class GeckoViewOutputStream : public nsIOutputStream { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIOUTPUTSTREAM + explicit GeckoViewOutputStream( + mozilla::java::GeckoInputStream::GlobalRef aStream) + : mStream(aStream) {} + + private: + const mozilla::java::GeckoInputStream::GlobalRef mStream; + virtual ~GeckoViewOutputStream() = default; +}; + +#endif // GeckoViewOutputStream_h__ diff --git a/mobile/android/components/geckoview/GeckoViewPermission.jsm b/mobile/android/components/geckoview/GeckoViewPermission.jsm new file mode 100644 index 0000000000..88ab64ab9d --- /dev/null +++ b/mobile/android/components/geckoview/GeckoViewPermission.jsm @@ -0,0 +1,41 @@ +/* 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/. */ +"use strict"; + +var EXPORTED_SYMBOLS = ["GeckoViewPermission"]; + +const { GeckoViewUtils } = ChromeUtils.importESModule( + "resource://gre/modules/GeckoViewUtils.sys.mjs" +); + +class GeckoViewPermission { + constructor() { + this.wrappedJSObject = this; + } + + async prompt(aRequest) { + const window = aRequest.window + ? aRequest.window + : aRequest.element.ownerGlobal; + + const actor = window.windowGlobalChild.getActor("GeckoViewPermission"); + const result = await actor.promptPermission(aRequest); + if (!result.allow) { + aRequest.cancel(); + } else { + // Note: permission could be undefined, that's what aRequest expects. + const { permission } = result; + aRequest.allow(permission); + } + } +} + +GeckoViewPermission.prototype.classID = Components.ID( + "{42f3c238-e8e8-4015-9ca2-148723a8afcf}" +); +GeckoViewPermission.prototype.QueryInterface = ChromeUtils.generateQI([ + "nsIContentPermissionPrompt", +]); + +const { debug, warn } = GeckoViewUtils.initLogging("GeckoViewPermission"); diff --git a/mobile/android/components/geckoview/GeckoViewPrompt.jsm b/mobile/android/components/geckoview/GeckoViewPrompt.jsm new file mode 100644 index 0000000000..af488279c9 --- /dev/null +++ b/mobile/android/components/geckoview/GeckoViewPrompt.jsm @@ -0,0 +1,826 @@ +/* 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/. */ +"use strict"; + +var EXPORTED_SYMBOLS = ["PromptFactory"]; + +const { GeckoViewUtils } = ChromeUtils.importESModule( + "resource://gre/modules/GeckoViewUtils.sys.mjs" +); + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs", + GeckoViewPrompter: "resource://gre/modules/GeckoViewPrompter.sys.mjs", +}); + +const { debug, warn } = GeckoViewUtils.initLogging("GeckoViewPrompt"); + +class PromptFactory { + constructor() { + this.wrappedJSObject = this; + } + + handleEvent(aEvent) { + switch (aEvent.type) { + case "mozshowdropdown": + case "mozshowdropdown-sourcetouch": + this._handleSelect(aEvent.composedTarget, /* aIsDropDown = */ true); + break; + case "MozOpenDateTimePicker": + this._handleDateTime(aEvent.composedTarget); + break; + case "click": + this._handleClick(aEvent); + break; + case "DOMPopupBlocked": + this._handlePopupBlocked(aEvent); + break; + } + } + + _handleClick(aEvent) { + const target = aEvent.composedTarget; + const className = ChromeUtils.getClassName(target); + if (className !== "HTMLInputElement" && className !== "HTMLSelectElement") { + return; + } + + if ( + target.isContentEditable || + target.disabled || + target.readOnly || + !target.willValidate + ) { + // target.willValidate is false when any associated fieldset is disabled, + // in which case this element is treated as disabled per spec. + return; + } + + if (className === "HTMLSelectElement") { + if (!target.isCombobox) { + this._handleSelect(target, /* aIsDropDown = */ false); + return; + } + // combobox select is handled by mozshowdropdown. + return; + } + + const type = target.type; + if (type === "month" || type === "week") { + // If there's a shadow root, the MozOpenDateTimePicker event takes care + // of this. Right now for these input types there's never a shadow root. + // Once we support UA widgets for month/week inputs (see bug 888320), we + // can remove this. + if (!target.openOrClosedShadowRoot) { + this._handleDateTime(target); + aEvent.preventDefault(); + } + } + } + + _generateSelectItems(aElement) { + const win = aElement.ownerGlobal; + let id = 0; + const map = {}; + + const items = (function enumList(elem, disabled) { + const items = []; + const children = elem.children; + for (let i = 0; i < children.length; i++) { + const child = children[i]; + if (win.getComputedStyle(child).display === "none") { + continue; + } + const item = { + id: String(id), + disabled: disabled || child.disabled, + }; + if (win.HTMLOptGroupElement.isInstance(child)) { + item.label = child.label; + item.items = enumList(child, item.disabled); + } else if (win.HTMLOptionElement.isInstance(child)) { + item.label = child.label || child.text; + item.selected = child.selected; + } else { + continue; + } + items.push(item); + map[id++] = child; + } + return items; + })(aElement); + + return [items, map, id]; + } + + _handleSelect(aElement, aIsDropDown) { + const win = aElement.ownerGlobal; + const [items] = this._generateSelectItems(aElement); + + if (aIsDropDown) { + aElement.openInParentProcess = true; + } + + const prompt = new lazy.GeckoViewPrompter(win); + + // Something changed the <select> while it was open. + const deferredUpdate = new lazy.DeferredTask(() => { + // Inner contents in choice prompt are updated. + const [newItems] = this._generateSelectItems(aElement); + prompt.update({ + type: "choice", + mode: aElement.multiple ? "multiple" : "single", + choices: newItems, + }); + }, 0); + const mut = new win.MutationObserver(() => { + deferredUpdate.arm(); + }); + mut.observe(aElement, { + childList: true, + subtree: true, + attributes: true, + }); + + const dismissPrompt = () => prompt.dismiss(); + aElement.addEventListener("blur", dismissPrompt, { mozSystemGroup: true }); + const hidedropdown = event => { + if (aElement === event.target) { + prompt.dismiss(); + } + }; + const chromeEventHandler = aElement.ownerGlobal.docShell.chromeEventHandler; + chromeEventHandler.addEventListener("mozhidedropdown", hidedropdown, { + mozSystemGroup: true, + }); + + prompt.asyncShowPrompt( + { + type: "choice", + mode: aElement.multiple ? "multiple" : "single", + choices: items, + }, + result => { + deferredUpdate.disarm(); + mut.disconnect(); + aElement.removeEventListener("blur", dismissPrompt, { + mozSystemGroup: true, + }); + chromeEventHandler.removeEventListener( + "mozhidedropdown", + hidedropdown, + { mozSystemGroup: true } + ); + + if (aIsDropDown) { + aElement.openInParentProcess = false; + } + // OK: result + // Cancel: !result + if (!result || result.choices === undefined) { + return; + } + + const [, map, id] = this._generateSelectItems(aElement); + let dispatchEvents = false; + if (!aElement.multiple) { + const elem = map[result.choices[0]]; + if (elem && win.HTMLOptionElement.isInstance(elem)) { + dispatchEvents = !elem.selected; + elem.selected = true; + } else { + console.error("Invalid id for select result: " + result.choices[0]); + } + } else { + for (let i = 0; i < id; i++) { + const elem = map[i]; + const index = result.choices.indexOf(String(i)); + if ( + win.HTMLOptionElement.isInstance(elem) && + elem.selected !== index >= 0 + ) { + // Current selected is not the same as the new selected state. + dispatchEvents = true; + elem.selected = !elem.selected; + } + result.choices[index] = undefined; + } + for (let i = 0; i < result.choices.length; i++) { + if (result.choices[i] !== undefined && result.choices[i] !== null) { + console.error( + "Invalid id for select result: " + result.choices[i] + ); + break; + } + } + } + + if (dispatchEvents) { + this._dispatchEvents(aElement); + } + } + ); + } + + _handleDateTime(aElement) { + const win = aElement.ownerGlobal; + const prompt = new lazy.GeckoViewPrompter(win); + + const chromeEventHandler = aElement.ownerGlobal.docShell.chromeEventHandler; + const dismissPrompt = () => prompt.dismiss(); + // Some controls don't have UA widget (bug 888320) + { + const dateTimeBoxElement = aElement.dateTimeBoxElement; + if (["month", "week"].includes(aElement.type) && !dateTimeBoxElement) { + aElement.addEventListener("blur", dismissPrompt, { + mozSystemGroup: true, + }); + } else { + chromeEventHandler.addEventListener( + "MozCloseDateTimePicker", + dismissPrompt + ); + + dateTimeBoxElement.dispatchEvent( + new win.CustomEvent("MozSetDateTimePickerState", { detail: true }) + ); + } + } + + prompt.asyncShowPrompt( + { + type: "datetime", + mode: aElement.type, + value: aElement.value, + min: aElement.min, + max: aElement.max, + step: aElement.step, + }, + result => { + // Some controls don't have UA widget (bug 888320) + const dateTimeBoxElement = aElement.dateTimeBoxElement; + if (["month", "week"].includes(aElement.type) && !dateTimeBoxElement) { + aElement.removeEventListener("blur", dismissPrompt, { + mozSystemGroup: true, + }); + } else { + chromeEventHandler.removeEventListener( + "MozCloseDateTimePicker", + dismissPrompt + ); + dateTimeBoxElement.dispatchEvent( + new win.CustomEvent("MozSetDateTimePickerState", { detail: false }) + ); + } + + // OK: result + // Cancel: !result + if ( + !result || + result.datetime === undefined || + result.datetime === aElement.value + ) { + return; + } + aElement.value = result.datetime; + this._dispatchEvents(aElement); + } + ); + } + + _dispatchEvents(aElement) { + // Fire both "input" and "change" events for <select> and <input> for + // date/time. + aElement.dispatchEvent( + new aElement.ownerGlobal.Event("input", { bubbles: true, composed: true }) + ); + aElement.dispatchEvent( + new aElement.ownerGlobal.Event("change", { bubbles: true }) + ); + } + + _handlePopupBlocked(aEvent) { + const dwi = aEvent.requestingWindow; + const popupWindowURISpec = aEvent.popupWindowURI + ? aEvent.popupWindowURI.displaySpec + : "about:blank"; + + const prompt = new lazy.GeckoViewPrompter(aEvent.requestingWindow); + prompt.asyncShowPrompt( + { + type: "popup", + targetUri: popupWindowURISpec, + }, + ({ response }) => { + if (response && dwi) { + dwi.open( + popupWindowURISpec, + aEvent.popupWindowName, + aEvent.popupWindowFeatures + ); + } + } + ); + } + + /* ---------- nsIPromptFactory ---------- */ + getPrompt(aDOMWin, aIID) { + // Delegated to login manager here, which in turn calls back into us via nsIPromptService. + if (aIID.equals(Ci.nsIAuthPrompt2) || aIID.equals(Ci.nsIAuthPrompt)) { + try { + const pwmgr = Cc[ + "@mozilla.org/passwordmanager/authpromptfactory;1" + ].getService(Ci.nsIPromptFactory); + return pwmgr.getPrompt(aDOMWin, aIID); + } catch (e) { + console.error("Delegation to password manager failed: " + e); + } + } + + const p = new PromptDelegate(aDOMWin); + p.QueryInterface(aIID); + return p; + } + + /* ---------- private memebers ---------- */ + + // nsIPromptService methods proxy to our Prompt class + callProxy(aMethod, aArguments) { + const prompt = new PromptDelegate(aArguments[0]); + let promptArgs; + if (BrowsingContext.isInstance(aArguments[0])) { + // Called by BrowsingContext prompt method, strip modalType. + [, , /*browsingContext*/ /*modalType*/ ...promptArgs] = aArguments; + } else { + [, /*domWindow*/ ...promptArgs] = aArguments; + } + return prompt[aMethod].apply(prompt, promptArgs); + } + + /* ---------- nsIPromptService ---------- */ + + alert() { + return this.callProxy("alert", arguments); + } + alertBC() { + return this.callProxy("alert", arguments); + } + alertCheck() { + return this.callProxy("alertCheck", arguments); + } + alertCheckBC() { + return this.callProxy("alertCheck", arguments); + } + confirm() { + return this.callProxy("confirm", arguments); + } + confirmBC() { + return this.callProxy("confirm", arguments); + } + confirmCheck() { + return this.callProxy("confirmCheck", arguments); + } + confirmCheckBC() { + return this.callProxy("confirmCheck", arguments); + } + confirmEx() { + return this.callProxy("confirmEx", arguments); + } + confirmExBC() { + return this.callProxy("confirmEx", arguments); + } + prompt() { + return this.callProxy("prompt", arguments); + } + promptBC() { + return this.callProxy("prompt", arguments); + } + promptUsernameAndPassword() { + return this.callProxy("promptUsernameAndPassword", arguments); + } + promptUsernameAndPasswordBC() { + return this.callProxy("promptUsernameAndPassword", arguments); + } + promptPassword() { + return this.callProxy("promptPassword", arguments); + } + promptPasswordBC() { + return this.callProxy("promptPassword", arguments); + } + select() { + return this.callProxy("select", arguments); + } + selectBC() { + return this.callProxy("select", arguments); + } + promptAuth() { + return this.callProxy("promptAuth", arguments); + } + promptAuthBC() { + return this.callProxy("promptAuth", arguments); + } + asyncPromptAuth() { + return this.callProxy("asyncPromptAuth", arguments); + } +} + +PromptFactory.prototype.classID = Components.ID( + "{076ac188-23c1-4390-aa08-7ef1f78ca5d9}" +); +PromptFactory.prototype.QueryInterface = ChromeUtils.generateQI([ + "nsIPromptFactory", + "nsIPromptService", +]); + +class PromptDelegate { + constructor(aParent) { + this._prompter = new lazy.GeckoViewPrompter(aParent); + } + + BUTTON_TYPE_POSITIVE = 0; + BUTTON_TYPE_NEUTRAL = 1; + BUTTON_TYPE_NEGATIVE = 2; + + /* ---------- internal methods ---------- */ + + _addText(aTitle, aText, aMsg) { + return Object.assign(aMsg, { + title: aTitle, + msg: aText, + }); + } + + _addCheck(aCheckMsg, aCheckState, aMsg) { + return Object.assign(aMsg, { + hasCheck: !!aCheckMsg, + checkMsg: aCheckMsg, + checkValue: aCheckState && aCheckState.value, + }); + } + + /* ---------- nsIPrompt ---------- */ + + alert(aTitle, aText) { + this.alertCheck(aTitle, aText); + } + + alertCheck(aTitle, aText, aCheckMsg, aCheckState) { + const result = this._prompter.showPrompt( + this._addText( + aTitle, + aText, + this._addCheck(aCheckMsg, aCheckState, { + type: "alert", + }) + ) + ); + if (result && aCheckState) { + aCheckState.value = !!result.checkValue; + } + } + + confirm(aTitle, aText) { + // Button 0 is OK. + return this.confirmCheck(aTitle, aText); + } + + confirmCheck(aTitle, aText, aCheckMsg, aCheckState) { + // Button 0 is OK. + return ( + this.confirmEx( + aTitle, + aText, + Ci.nsIPrompt.STD_OK_CANCEL_BUTTONS, + /* aButton0 */ null, + /* aButton1 */ null, + /* aButton2 */ null, + aCheckMsg, + aCheckState + ) == 0 + ); + } + + confirmEx( + aTitle, + aText, + aButtonFlags, + aButton0, + aButton1, + aButton2, + aCheckMsg, + aCheckState + ) { + const btnMap = Array(3).fill(null); + const btnTitle = Array(3).fill(null); + const btnCustomTitle = Array(3).fill(null); + const savedButtonId = []; + for (let i = 0; i < 3; i++) { + const btnFlags = aButtonFlags >> (i * 8); + switch (btnFlags & 0xff) { + case Ci.nsIPrompt.BUTTON_TITLE_OK: + btnMap[this.BUTTON_TYPE_POSITIVE] = i; + btnTitle[this.BUTTON_TYPE_POSITIVE] = "ok"; + break; + case Ci.nsIPrompt.BUTTON_TITLE_CANCEL: + btnMap[this.BUTTON_TYPE_NEGATIVE] = i; + btnTitle[this.BUTTON_TYPE_NEGATIVE] = "cancel"; + break; + case Ci.nsIPrompt.BUTTON_TITLE_YES: + btnMap[this.BUTTON_TYPE_POSITIVE] = i; + btnTitle[this.BUTTON_TYPE_POSITIVE] = "yes"; + break; + case Ci.nsIPrompt.BUTTON_TITLE_NO: + btnMap[this.BUTTON_TYPE_NEGATIVE] = i; + btnTitle[this.BUTTON_TYPE_NEGATIVE] = "no"; + break; + case Ci.nsIPrompt.BUTTON_TITLE_IS_STRING: + // We don't know if this is positive/negative/neutral, so save for later. + savedButtonId.push(i); + break; + case Ci.nsIPrompt.BUTTON_TITLE_SAVE: + case Ci.nsIPrompt.BUTTON_TITLE_DONT_SAVE: + case Ci.nsIPrompt.BUTTON_TITLE_REVERT: + // Not supported; fall-through. + default: + break; + } + } + + // Put saved buttons into available slots. + for (let i = 0; i < 3 && savedButtonId.length; i++) { + if (btnMap[i] === null) { + btnMap[i] = savedButtonId.shift(); + btnTitle[i] = "custom"; + btnCustomTitle[i] = [aButton0, aButton1, aButton2][btnMap[i]]; + } + } + + const result = this._prompter.showPrompt( + this._addText( + aTitle, + aText, + this._addCheck(aCheckMsg, aCheckState, { + type: "button", + btnTitle, + btnCustomTitle, + }) + ) + ); + if (result && aCheckState) { + aCheckState.value = !!result.checkValue; + } + return result && result.button in btnMap ? btnMap[result.button] : -1; + } + + prompt(aTitle, aText, aValue, aCheckMsg, aCheckState) { + const result = this._prompter.showPrompt( + this._addText( + aTitle, + aText, + this._addCheck(aCheckMsg, aCheckState, { + type: "text", + value: aValue.value, + }) + ) + ); + // OK: result && result.text !== undefined + // Cancel: result && result.text === undefined + // Error: !result + if (result && aCheckState) { + aCheckState.value = !!result.checkValue; + } + if (!result || result.text === undefined) { + return false; + } + aValue.value = result.text || ""; + return true; + } + + promptPassword(aTitle, aText, aPassword) { + return this._promptUsernameAndPassword( + aTitle, + aText, + /* aUsername */ undefined, + aPassword + ); + } + + promptUsernameAndPassword(aTitle, aText, aUsername, aPassword) { + const msg = { + type: "auth", + mode: aUsername ? "auth" : "password", + options: { + flags: aUsername ? 0 : Ci.nsIAuthInformation.ONLY_PASSWORD, + username: aUsername ? aUsername.value : undefined, + password: aPassword.value, + }, + }; + const result = this._prompter.showPrompt(this._addText(aTitle, aText, msg)); + // OK: result && result.password !== undefined + // Cancel: result && result.password === undefined + // Error: !result + if (!result || result.password === undefined) { + return false; + } + if (aUsername) { + aUsername.value = result.username || ""; + } + aPassword.value = result.password || ""; + return true; + } + + select(aTitle, aText, aSelectList, aOutSelection) { + const choices = Array.prototype.map.call(aSelectList, (item, index) => ({ + id: String(index), + label: item, + disabled: false, + selected: false, + })); + const result = this._prompter.showPrompt( + this._addText(aTitle, aText, { + type: "choice", + mode: "single", + choices, + }) + ); + // OK: result + // Cancel: !result + if (!result || result.choices === undefined) { + return false; + } + aOutSelection.value = Number(result.choices[0]); + return true; + } + + _getAuthMsg(aChannel, aLevel, aAuthInfo) { + let username; + if ( + aAuthInfo.flags & Ci.nsIAuthInformation.NEED_DOMAIN && + aAuthInfo.domain + ) { + username = aAuthInfo.domain + "\\" + aAuthInfo.username; + } else { + username = aAuthInfo.username; + } + return this._addText( + /* title */ null, + this._getAuthText(aChannel, aAuthInfo), + { + type: "auth", + mode: + aAuthInfo.flags & Ci.nsIAuthInformation.ONLY_PASSWORD + ? "password" + : "auth", + options: { + flags: aAuthInfo.flags, + uri: aChannel && aChannel.URI.displaySpec, + level: aLevel, + username, + password: aAuthInfo.password, + }, + } + ); + } + + _fillAuthInfo(aAuthInfo, aResult) { + if (!aResult || aResult.password === undefined) { + return false; + } + + aAuthInfo.password = aResult.password || ""; + if (aAuthInfo.flags & Ci.nsIAuthInformation.ONLY_PASSWORD) { + return true; + } + + const username = aResult.username || ""; + if (aAuthInfo.flags & Ci.nsIAuthInformation.NEED_DOMAIN) { + // Domain is separated from username by a backslash + var idx = username.indexOf("\\"); + if (idx >= 0) { + aAuthInfo.domain = username.substring(0, idx); + aAuthInfo.username = username.substring(idx + 1); + return true; + } + } + aAuthInfo.username = username; + return true; + } + + promptAuth(aChannel, aLevel, aAuthInfo) { + const result = this._prompter.showPrompt( + this._getAuthMsg(aChannel, aLevel, aAuthInfo) + ); + // OK: result && result.password !== undefined + // Cancel: result && result.password === undefined + // Error: !result + return this._fillAuthInfo(aAuthInfo, result); + } + + async asyncPromptAuth(aChannel, aLevel, aAuthInfo) { + const result = await this._prompter.asyncShowPromptPromise( + this._getAuthMsg(aChannel, aLevel, aAuthInfo) + ); + // OK: result && result.password !== undefined + // Cancel: result && result.password === undefined + // Error: !result + return this._fillAuthInfo(aAuthInfo, result); + } + + _getAuthText(aChannel, aAuthInfo) { + const isProxy = aAuthInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY; + const isPassOnly = aAuthInfo.flags & Ci.nsIAuthInformation.ONLY_PASSWORD; + const isCrossOrig = + aAuthInfo.flags & Ci.nsIAuthInformation.CROSS_ORIGIN_SUB_RESOURCE; + + const username = aAuthInfo.username; + const authTarget = this._getAuthTarget(aChannel, aAuthInfo); + const { displayHost } = authTarget; + let { realm } = authTarget; + + // Suppress "the site says: $realm" when we synthesized a missing realm. + if (!aAuthInfo.realm && !isProxy) { + realm = ""; + } + + // Trim obnoxiously long realms. + if (realm.length > 50) { + realm = realm.substring(0, 50) + "\u2026"; + } + + const bundle = Services.strings.createBundle( + "chrome://global/locale/commonDialogs.properties" + ); + let text; + if (isProxy) { + text = bundle.formatStringFromName("EnterLoginForProxy3", [ + realm, + displayHost, + ]); + } else if (isPassOnly) { + text = bundle.formatStringFromName("EnterPasswordFor", [ + username, + displayHost, + ]); + } else if (isCrossOrig) { + text = bundle.formatStringFromName("EnterUserPasswordForCrossOrigin2", [ + displayHost, + ]); + } else if (!realm) { + text = bundle.formatStringFromName("EnterUserPasswordFor2", [ + displayHost, + ]); + } else { + text = bundle.formatStringFromName("EnterLoginForRealm3", [ + realm, + displayHost, + ]); + } + + return text; + } + + _getAuthTarget(aChannel, aAuthInfo) { + // If our proxy is demanding authentication, don't use the + // channel's actual destination. + if (aAuthInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY) { + if (!(aChannel instanceof Ci.nsIProxiedChannel)) { + throw new Error("proxy auth needs nsIProxiedChannel"); + } + const info = aChannel.proxyInfo; + if (!info) { + throw new Error("proxy auth needs nsIProxyInfo"); + } + // Proxies don't have a scheme, but we'll use "moz-proxy://" + // so that it's more obvious what the login is for. + const idnService = Cc["@mozilla.org/network/idn-service;1"].getService( + Ci.nsIIDNService + ); + const displayHost = + "moz-proxy://" + + idnService.convertUTF8toACE(info.host) + + ":" + + info.port; + let realm = aAuthInfo.realm; + if (!realm) { + realm = displayHost; + } + return { displayHost, realm }; + } + + const displayHost = + aChannel.URI.scheme + "://" + aChannel.URI.displayHostPort; + // If a HTTP WWW-Authenticate header specified a realm, that value + // will be available here. If it wasn't set or wasn't HTTP, we'll use + // the formatted hostname instead. + let realm = aAuthInfo.realm; + if (!realm) { + realm = displayHost; + } + return { displayHost, realm }; + } +} + +PromptDelegate.prototype.QueryInterface = ChromeUtils.generateQI(["nsIPrompt"]); diff --git a/mobile/android/components/geckoview/GeckoViewPrompter.sys.mjs b/mobile/android/components/geckoview/GeckoViewPrompter.sys.mjs new file mode 100644 index 0000000000..a36ae0abec --- /dev/null +++ b/mobile/android/components/geckoview/GeckoViewPrompter.sys.mjs @@ -0,0 +1,206 @@ +/* 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/. */ + +import { GeckoViewUtils } from "resource://gre/modules/GeckoViewUtils.sys.mjs"; + +const { debug, warn } = GeckoViewUtils.initLogging("GeckoViewPrompter"); + +export class GeckoViewPrompter { + constructor(aParent) { + this.id = Services.uuid.generateUUID().toString().slice(1, -1); // Discard surrounding braces + + if (aParent) { + if (Window.isInstance(aParent)) { + this._domWin = aParent; + } else if (aParent.window) { + this._domWin = aParent.window; + } else { + this._domWin = + aParent.embedderElement && aParent.embedderElement.ownerGlobal; + } + } + + if (!this._domWin) { + this._domWin = Services.wm.getMostRecentWindow("navigator:geckoview"); + } + + this._innerWindowId = + this._domWin?.browsingContext.currentWindowContext.innerWindowId; + } + + get domWin() { + return this._domWin; + } + + get prompterActor() { + const actor = this.domWin?.windowGlobalChild.getActor("GeckoViewPrompter"); + return actor; + } + + _changeModalState(aEntering) { + if (!this._domWin) { + // Allow not having a DOM window. + return true; + } + // Accessing the document object can throw if this window no longer exists. See bug 789888. + try { + const winUtils = this._domWin.windowUtils; + if (!aEntering) { + winUtils.leaveModalState(); + } + + const event = this._domWin.document.createEvent("Events"); + event.initEvent( + aEntering ? "DOMWillOpenModalDialog" : "DOMModalDialogClosed", + true, + true + ); + winUtils.dispatchEventToChromeOnly(this._domWin, event); + + if (aEntering) { + winUtils.enterModalState(); + } + return true; + } catch (ex) { + console.error("Failed to change modal state: " + ex); + } + return false; + } + + _dismissUi() { + this.prompterActor?.dismissPrompt(this); + } + + accept(aInputText = this.inputText) { + if (this.callback) { + let acceptMsg = {}; + switch (this.message.type) { + case "alert": + acceptMsg = null; + break; + case "button": + acceptMsg.button = 0; + break; + case "text": + acceptMsg.text = aInputText; + break; + default: + acceptMsg = null; + break; + } + this.callback(acceptMsg); + // Notify the UI that this prompt should be hidden. + this._dismissUi(); + } + } + + dismiss() { + this.callback(null); + // Notify the UI that this prompt should be hidden. + this._dismissUi(); + } + + getPromptType() { + switch (this.message.type) { + case "alert": + return this.message.checkValue ? "alertCheck" : "alert"; + case "button": + return this.message.checkValue ? "confirmCheck" : "confirm"; + case "text": + return this.message.checkValue ? "promptCheck" : "prompt"; + default: + return this.message.type; + } + } + + getPromptText() { + return this.message.msg; + } + + getInputText() { + return this.inputText; + } + + setInputText(aInput) { + this.inputText = aInput; + } + + /** + * Shows a native prompt, and then spins the event loop for this thread while we wait + * for a response + */ + showPrompt(aMsg) { + let result = undefined; + if (!this._domWin || !this._changeModalState(/* aEntering */ true)) { + return result; + } + try { + this.asyncShowPrompt(aMsg, res => (result = res)); + + // Spin this thread while we wait for a result + Services.tm.spinEventLoopUntil( + "GeckoViewPrompter.jsm:showPrompt", + () => this._domWin.closed || result !== undefined + ); + } finally { + this._changeModalState(/* aEntering */ false); + } + return result; + } + + checkInnerWindow() { + // Checks that the innerWindow where this prompt was created still matches + // the current innerWindow. + // This checks will fail if the page navigates away, making this prompt + // obsolete. + return ( + this._innerWindowId === + this._domWin.browsingContext.currentWindowContext.innerWindowId + ); + } + + asyncShowPromptPromise(aMsg) { + return new Promise(resolve => { + this.asyncShowPrompt(aMsg, resolve); + }); + } + + async asyncShowPrompt(aMsg, aCallback) { + this.message = aMsg; + this.inputText = aMsg.value; + this.callback = aCallback; + + aMsg.id = this.id; + + let response = null; + try { + if (this.checkInnerWindow()) { + response = await this.prompterActor.prompt(this, aMsg); + } + } catch (error) { + // Nothing we can do really, we will treat this as a dismiss. + warn`Error while prompting: ${error}`; + } + + if (!this.checkInnerWindow()) { + // Page has navigated away, let's dismiss the prompt + aCallback(null); + } else { + aCallback(response); + } + // This callback object is tied to the Java garbage collector because + // it is invoked from Java. Manually release the target callback + // here; otherwise we may hold onto resources for too long, because + // we would be relying on both the Java and the JS garbage collectors + // to run. + aMsg = undefined; + aCallback = undefined; + } + + update(aMsg) { + this.message = aMsg; + aMsg.id = this.id; + this.prompterActor?.updatePrompt(aMsg); + } +} diff --git a/mobile/android/components/geckoview/GeckoViewPush.jsm b/mobile/android/components/geckoview/GeckoViewPush.jsm new file mode 100644 index 0000000000..5899bfd3d8 --- /dev/null +++ b/mobile/android/components/geckoview/GeckoViewPush.jsm @@ -0,0 +1,257 @@ +/* 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/. */ +"use strict"; + +var EXPORTED_SYMBOLS = ["PushService"]; + +const { GeckoViewUtils } = ChromeUtils.importESModule( + "resource://gre/modules/GeckoViewUtils.sys.mjs" +); + +const { debug, warn } = GeckoViewUtils.initLogging("GeckoViewPush"); + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + EventDispatcher: "resource://gre/modules/Messaging.sys.mjs", +}); + +// Observer notification topics for push messages and subscription status +// changes. These are duplicated and used in `nsIPushNotifier`. They're exposed +// on `nsIPushService` so that JS callers only need to import this service. +const OBSERVER_TOPIC_PUSH = "push-message"; +const OBSERVER_TOPIC_SUBSCRIPTION_CHANGE = "push-subscription-change"; +const OBSERVER_TOPIC_SUBSCRIPTION_MODIFIED = "push-subscription-modified"; + +function createSubscription({ + scope, + principal, + browserPublicKey, + authSecret, + endpoint, + appServerKey, +}) { + const decodedBrowserKey = ChromeUtils.base64URLDecode(browserPublicKey, { + padding: "ignore", + }); + const decodedAuthSecret = ChromeUtils.base64URLDecode(authSecret, { + padding: "ignore", + }); + + return new PushSubscription({ + endpoint, + scope, + p256dhKey: decodedBrowserKey, + authenticationSecret: decodedAuthSecret, + appServerKey, + }); +} + +function scopeWithAttrs(scope, attrs) { + return scope + ChromeUtils.originAttributesToSuffix(attrs); +} + +class PushService { + constructor() { + this.wrappedJSObject = this; + } + + pushTopic = OBSERVER_TOPIC_PUSH; + subscriptionChangeTopic = OBSERVER_TOPIC_SUBSCRIPTION_CHANGE; + subscriptionModifiedTopic = OBSERVER_TOPIC_SUBSCRIPTION_MODIFIED; + + // nsIObserver methods + + observe(subject, topic, data) {} + + // nsIPushService methods + + subscribe(scope, principal, callback) { + this.subscribeWithKey(scope, principal, null, callback); + } + + async subscribeWithKey(scope, principal, appServerKey, callback) { + try { + const response = await lazy.EventDispatcher.instance.sendRequestForResult( + { + type: "GeckoView:PushSubscribe", + scope: scopeWithAttrs(scope, principal.originAttributes), + appServerKey: appServerKey + ? ChromeUtils.base64URLEncode(new Uint8Array(appServerKey), { + pad: true, + }) + : null, + } + ); + + let subscription = null; + if (response) { + subscription = createSubscription({ + ...response, + scope, + principal, + appServerKey, + }); + } + + callback.onPushSubscription(Cr.NS_OK, subscription); + } catch (e) { + callback.onPushSubscription(Cr.NS_ERROR_FAILURE, null); + } + } + + async unsubscribe(scope, principal, callback) { + try { + await lazy.EventDispatcher.instance.sendRequestForResult({ + type: "GeckoView:PushUnsubscribe", + scope: scopeWithAttrs(scope, principal.originAttributes), + }); + + callback.onUnsubscribe(Cr.NS_OK, true); + } catch (e) { + callback.onUnsubscribe(Cr.NS_ERROR_FAILURE, false); + } + } + + async getSubscription(scope, principal, callback) { + try { + const response = await lazy.EventDispatcher.instance.sendRequestForResult( + { + type: "GeckoView:PushGetSubscription", + scope: scopeWithAttrs(scope, principal.originAttributes), + } + ); + + let subscription = null; + if (response) { + subscription = createSubscription({ + ...response, + scope, + principal, + }); + } + + callback.onPushSubscription(Cr.NS_OK, subscription); + } catch (e) { + callback.onPushSubscription(Cr.NS_ERROR_FAILURE, null); + } + } + + clearForDomain(domain, callback) { + callback.onClear(Cr.NS_OK); + } + + // nsIPushQuotaManager methods + + notificationForOriginShown(origin) {} + + notificationForOriginClosed(origin) {} + + // nsIPushErrorReporter methods + + reportDeliveryError(messageId, reason) {} +} + +PushService.prototype.classID = Components.ID( + "{a54d84d7-98a4-4fec-b664-e42e512ae9cc}" +); +PushService.prototype.contractID = "@mozilla.org/push/Service;1"; +PushService.prototype.QueryInterface = ChromeUtils.generateQI([ + "nsIObserver", + "nsISupportsWeakReference", + "nsIPushService", + "nsIPushQuotaManager", + "nsIPushErrorReporter", +]); + +/** `PushSubscription` instances are passed to all subscription callbacks. */ +class PushSubscription { + constructor(props) { + this._props = props; + } + + /** The URL for sending messages to this subscription. */ + get endpoint() { + return this._props.endpoint; + } + + /** The last time a message was sent to this subscription. */ + get lastPush() { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + } + + /** The total number of messages sent to this subscription. */ + get pushCount() { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + } + + /** + * The app will take care of throttling, so we don't + * care about the quota stuff here. + */ + get quota() { + return -1; + } + + /** + * Indicates whether this subscription was created with the system principal. + * System subscriptions are exempt from the background message quota and + * permission checks. + */ + get isSystemSubscription() { + return false; + } + + /** The private key used to decrypt incoming push messages, in JWK format */ + get p256dhPrivateKey() { + throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); + } + + /** + * Indicates whether this subscription is subject to the background message + * quota. + */ + quotaApplies() { + return false; + } + + /** + * Indicates whether this subscription exceeded the background message quota, + * or the user revoked the notification permission. The caller must request a + * new subscription to continue receiving push messages. + */ + isExpired() { + return false; + } + + /** + * Returns a key for encrypting messages sent to this subscription. JS + * callers receive the key buffer as a return value, while C++ callers + * receive the key size and buffer as out parameters. + */ + getKey(name) { + switch (name) { + case "p256dh": + return this._getRawKey(this._props.p256dhKey); + + case "auth": + return this._getRawKey(this._props.authenticationSecret); + + case "appServer": + return this._getRawKey(this._props.appServerKey); + } + return []; + } + + _getRawKey(key) { + if (!key) { + return []; + } + return new Uint8Array(key); + } +} + +PushSubscription.prototype.QueryInterface = ChromeUtils.generateQI([ + "nsIPushSubscription", +]); diff --git a/mobile/android/components/geckoview/GeckoViewStartup.jsm b/mobile/android/components/geckoview/GeckoViewStartup.jsm new file mode 100644 index 0000000000..07924b6e07 --- /dev/null +++ b/mobile/android/components/geckoview/GeckoViewStartup.jsm @@ -0,0 +1,344 @@ +/* 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/. */ +"use strict"; + +var EXPORTED_SYMBOLS = ["GeckoViewStartup"]; + +const { GeckoViewUtils } = ChromeUtils.importESModule( + "resource://gre/modules/GeckoViewUtils.sys.mjs" +); + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + ActorManagerParent: "resource://gre/modules/ActorManagerParent.sys.mjs", + EventDispatcher: "resource://gre/modules/Messaging.sys.mjs", + PdfJs: "resource://pdf.js/PdfJs.sys.mjs", + Preferences: "resource://gre/modules/Preferences.sys.mjs", +}); + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const { debug, warn } = GeckoViewUtils.initLogging("Startup"); + +var { DelayedInit } = ChromeUtils.import( + "resource://gre/modules/DelayedInit.jsm" +); + +function InitLater(fn, object, name) { + return DelayedInit.schedule(fn, object, name, 15000 /* 15s max wait */); +} + +const JSPROCESSACTORS = { + GeckoViewPermissionProcess: { + parent: { + esModuleURI: + "resource:///actors/GeckoViewPermissionProcessParent.sys.mjs", + }, + child: { + esModuleURI: "resource:///actors/GeckoViewPermissionProcessChild.sys.mjs", + observers: [ + "getUserMedia:ask-device-permission", + "getUserMedia:request", + "recording-device-events", + "PeerConnection:request", + ], + }, + }, +}; + +const JSWINDOWACTORS = { + LoadURIDelegate: { + parent: { + esModuleURI: "resource:///actors/LoadURIDelegateParent.sys.mjs", + }, + child: { + esModuleURI: "resource:///actors/LoadURIDelegateChild.sys.mjs", + }, + messageManagerGroups: ["browsers"], + }, + GeckoViewPermission: { + parent: { + esModuleURI: "resource:///actors/GeckoViewPermissionParent.sys.mjs", + }, + child: { + esModuleURI: "resource:///actors/GeckoViewPermissionChild.sys.mjs", + }, + allFrames: true, + includeChrome: true, + }, + GeckoViewPrompt: { + child: { + esModuleURI: "resource:///actors/GeckoViewPromptChild.sys.mjs", + events: { + click: { capture: false, mozSystemGroup: true }, + contextmenu: { capture: false, mozSystemGroup: true }, + mozshowdropdown: {}, + "mozshowdropdown-sourcetouch": {}, + MozOpenDateTimePicker: {}, + DOMPopupBlocked: { capture: false, mozSystemGroup: true }, + }, + }, + allFrames: true, + messageManagerGroups: ["browsers"], + }, + GeckoViewFormValidation: { + child: { + esModuleURI: "resource:///actors/GeckoViewFormValidationChild.sys.mjs", + events: { + MozInvalidForm: {}, + }, + }, + allFrames: true, + messageManagerGroups: ["browsers"], + }, + GeckoViewClipboardPermission: { + parent: { + esModuleURI: + "resource:///actors/GeckoViewClipboardPermissionParent.sys.mjs", + }, + child: { + esModuleURI: + "resource:///actors/GeckoViewClipboardPermissionChild.sys.mjs", + events: { + MozClipboardReadPaste: {}, + deactivate: { mozSystemGroup: true }, + mousedown: { capture: true, mozSystemGroup: true }, + mozvisualscroll: { mozSystemGroup: true }, + pagehide: { capture: true, mozSystemGroup: true }, + }, + }, + allFrames: true, + }, + GeckoViewPdfjs: { + parent: { + esModuleURI: "resource://pdf.js/GeckoViewPdfjsParent.sys.mjs", + }, + child: { + esModuleURI: "resource://pdf.js/GeckoViewPdfjsChild.sys.mjs", + }, + allFrames: true, + }, +}; + +class GeckoViewStartup { + /* ---------- nsIObserver ---------- */ + observe(aSubject, aTopic, aData) { + debug`observe: ${aTopic}`; + switch (aTopic) { + case "content-process-ready-for-script": + case "app-startup": { + GeckoViewUtils.addLazyGetter(this, "GeckoViewConsole", { + module: "resource://gre/modules/GeckoViewConsole.sys.mjs", + }); + + GeckoViewUtils.addLazyGetter(this, "GeckoViewStorageController", { + module: "resource://gre/modules/GeckoViewStorageController.sys.mjs", + ged: [ + "GeckoView:ClearData", + "GeckoView:ClearSessionContextData", + "GeckoView:ClearHostData", + "GeckoView:ClearBaseDomainData", + "GeckoView:GetAllPermissions", + "GeckoView:GetPermissionsByURI", + "GeckoView:SetPermission", + "GeckoView:SetPermissionByURI", + "GeckoView:GetCookieBannerModeForDomain", + "GeckoView:SetCookieBannerModeForDomain", + "GeckoView:RemoveCookieBannerModeForDomain", + ], + }); + + GeckoViewUtils.addLazyGetter(this, "GeckoViewPushController", { + module: "resource://gre/modules/GeckoViewPushController.sys.mjs", + ged: ["GeckoView:PushEvent", "GeckoView:PushSubscriptionChanged"], + }); + + GeckoViewUtils.addLazyPrefObserver( + { + name: "geckoview.console.enabled", + default: false, + }, + { + handler: _ => this.GeckoViewConsole, + } + ); + + // Parent process only + if ( + Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT + ) { + lazy.ActorManagerParent.addJSWindowActors(JSWINDOWACTORS); + lazy.ActorManagerParent.addJSProcessActors(JSPROCESSACTORS); + + if (Services.appinfo.sessionHistoryInParent) { + GeckoViewUtils.addLazyGetter(this, "GeckoViewSessionStore", { + module: "resource://gre/modules/GeckoViewSessionStore.sys.mjs", + observers: [ + "browsing-context-did-set-embedder", + "browsing-context-discarded", + ], + }); + } + + GeckoViewUtils.addLazyGetter(this, "GeckoViewWebExtension", { + module: "resource://gre/modules/GeckoViewWebExtension.sys.mjs", + ged: [ + "GeckoView:ActionDelegate:Attached", + "GeckoView:BrowserAction:Click", + "GeckoView:PageAction:Click", + "GeckoView:RegisterWebExtension", + "GeckoView:UnregisterWebExtension", + "GeckoView:WebExtension:CancelInstall", + "GeckoView:WebExtension:Disable", + "GeckoView:WebExtension:Enable", + "GeckoView:WebExtension:EnsureBuiltIn", + "GeckoView:WebExtension:Get", + "GeckoView:WebExtension:Install", + "GeckoView:WebExtension:InstallBuiltIn", + "GeckoView:WebExtension:List", + "GeckoView:WebExtension:PortDisconnect", + "GeckoView:WebExtension:PortMessageFromApp", + "GeckoView:WebExtension:SetPBAllowed", + "GeckoView:WebExtension:Uninstall", + "GeckoView:WebExtension:Update", + ], + observers: [ + "devtools-installed-addon", + "testing-installed-addon", + "testing-uninstalled-addon", + ], + }); + + GeckoViewUtils.addLazyGetter(this, "ChildCrashHandler", { + module: "resource://gre/modules/ChildCrashHandler.sys.mjs", + observers: ["ipc:content-shutdown", "compositor:process-aborted"], + }); + + lazy.EventDispatcher.instance.registerListener(this, [ + "GeckoView:StorageDelegate:Attached", + ]); + } + break; + } + + case "profile-after-change": { + GeckoViewUtils.addLazyGetter(this, "GeckoViewRemoteDebugger", { + module: "resource://gre/modules/GeckoViewRemoteDebugger.sys.mjs", + init: gvrd => gvrd.onInit(), + }); + + GeckoViewUtils.addLazyPrefObserver( + { + name: "devtools.debugger.remote-enabled", + default: false, + }, + { + handler: _ => this.GeckoViewRemoteDebugger, + } + ); + + GeckoViewUtils.addLazyGetter(this, "DownloadTracker", { + module: "resource://gre/modules/GeckoViewWebExtension.sys.mjs", + ged: ["GeckoView:WebExtension:DownloadChanged"], + }); + + ChromeUtils.importESModule( + "resource://gre/modules/NotificationDB.sys.mjs" + ); + + // Listen for global EventDispatcher messages + lazy.EventDispatcher.instance.registerListener(this, [ + "GeckoView:ResetUserPrefs", + "GeckoView:SetDefaultPrefs", + "GeckoView:SetLocale", + ]); + + Services.obs.addObserver(this, "browser-idle-startup-tasks-finished"); + Services.obs.addObserver(this, "handlersvc-store-initialized"); + + Services.obs.notifyObservers(null, "geckoview-startup-complete"); + break; + } + case "browser-idle-startup-tasks-finished": { + // TODO bug 1730026: when an alternative is introduced that runs once, + // replace this observer topic with that alternative. + // This only needs to happen once during startup. + Services.obs.removeObserver(this, aTopic); + // Notify the start up crash tracker that the browser has successfully + // started up so the startup cache isn't rebuilt on next startup. + Services.startup.trackStartupCrashEnd(); + break; + } + case "handlersvc-store-initialized": { + // Initialize PdfJs when running in-process and remote. This only + // happens once since PdfJs registers global hooks. If the PdfJs + // extension is installed the init method below will be overridden + // leaving initialization to the extension. + // parent only: configure default prefs, set up pref observers, register + // pdf content handler, and initializes parent side message manager + // shim for privileged api access. + try { + lazy.PdfJs.init(this._isNewProfile); + } catch {} + break; + } + } + } + + onEvent(aEvent, aData, aCallback) { + debug`onEvent ${aEvent}`; + + switch (aEvent) { + case "GeckoView:ResetUserPrefs": { + const prefs = new lazy.Preferences(); + prefs.reset(aData.names); + break; + } + case "GeckoView:SetDefaultPrefs": { + const prefs = new lazy.Preferences({ defaultBranch: true }); + for (const name of Object.keys(aData)) { + try { + prefs.set(name, aData[name]); + } catch (e) { + warn`Failed to set preference ${name}: ${e}`; + } + } + break; + } + case "GeckoView:SetLocale": + if (aData.requestedLocales) { + Services.locale.requestedLocales = aData.requestedLocales; + } + const pls = Cc["@mozilla.org/pref-localizedstring;1"].createInstance( + Ci.nsIPrefLocalizedString + ); + pls.data = aData.acceptLanguages; + Services.prefs.setComplexValue( + "intl.accept_languages", + Ci.nsIPrefLocalizedString, + pls + ); + break; + + case "GeckoView:StorageDelegate:Attached": + InitLater(() => { + const loginDetection = Cc[ + "@mozilla.org/login-detection-service;1" + ].createInstance(Ci.nsILoginDetectionService); + loginDetection.init(); + }); + break; + } + } +} + +GeckoViewStartup.prototype.classID = Components.ID( + "{8e993c34-fdd6-432c-967e-f995d888777f}" +); +GeckoViewStartup.prototype.QueryInterface = ChromeUtils.generateQI([ + "nsIObserver", +]); diff --git a/mobile/android/components/geckoview/GeckoViewStreamListener.cpp b/mobile/android/components/geckoview/GeckoViewStreamListener.cpp new file mode 100644 index 0000000000..71b9fadeb5 --- /dev/null +++ b/mobile/android/components/geckoview/GeckoViewStreamListener.cpp @@ -0,0 +1,298 @@ +/* -*- Mode: c++; c-basic-offset: 2; tab-width: 2; indent-tabs-mode: nil; -*- + * 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 "GeckoViewStreamListener.h" + +#include "mozilla/fallible.h" +#include "nsIAsyncVerifyRedirectCallback.h" +#include "nsIChannelEventSink.h" +#include "nsIHttpChannel.h" +#include "nsIHttpHeaderVisitor.h" +#include "nsIInputStream.h" +#include "nsINSSErrorsService.h" +#include "nsITransportSecurityInfo.h" +#include "nsIWebProgressListener.h" +#include "nsIX509Cert.h" +#include "nsPrintfCString.h" + +#include "nsNetUtil.h" + +#include "JavaBuiltins.h" + +using namespace mozilla; + +NS_IMPL_ISUPPORTS(GeckoViewStreamListener, nsIStreamListener, + nsIInterfaceRequestor, nsIChannelEventSink) + +class HeaderVisitor final : public nsIHttpHeaderVisitor { + public: + NS_DECL_THREADSAFE_ISUPPORTS + + explicit HeaderVisitor(java::WebResponse::Builder::Param aBuilder) + : mBuilder(aBuilder) {} + + NS_IMETHOD + VisitHeader(const nsACString& aHeader, const nsACString& aValue) override { + mBuilder->Header(aHeader, aValue); + return NS_OK; + } + + private: + virtual ~HeaderVisitor() {} + + const java::WebResponse::Builder::GlobalRef mBuilder; +}; + +NS_IMPL_ISUPPORTS(HeaderVisitor, nsIHttpHeaderVisitor) + +class StreamSupport final + : public java::GeckoInputStream::Support::Natives<StreamSupport> { + public: + typedef java::GeckoInputStream::Support::Natives<StreamSupport> Base; + using Base::AttachNative; + using Base::GetNative; + + explicit StreamSupport(java::GeckoInputStream::Support::Param aInstance, + nsIRequest* aRequest) + : mInstance(aInstance), mRequest(aRequest) {} + + void Close() { + mRequest->Cancel(NS_ERROR_ABORT); + mRequest->Resume(); + + // This is basically `delete this`, so don't run anything else! + Base::DisposeNative(mInstance); + } + + void Resume() { mRequest->Resume(); } + + private: + java::GeckoInputStream::Support::GlobalRef mInstance; + nsCOMPtr<nsIRequest> mRequest; +}; + +NS_IMETHODIMP +GeckoViewStreamListener::OnStartRequest(nsIRequest* aRequest) { + MOZ_ASSERT(!mStream); + + nsresult status; + aRequest->GetStatus(&status); + if (NS_FAILED(status)) { + nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest); + CompleteWithError(status, channel); + return NS_OK; + } + + // We're expecting data later via OnDataAvailable, so create the stream now. + InitializeStreamSupport(aRequest); + + mStream = java::GeckoInputStream::New(mSupport); + + // Suspend the request immediately. It will be resumed when (if) someone + // tries to read the Java stream. + aRequest->Suspend(); + + nsresult rv = HandleWebResponse(aRequest); + if (NS_FAILED(rv)) { + nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest); + CompleteWithError(rv, channel); + return NS_OK; + } + + return NS_OK; +} + +NS_IMETHODIMP +GeckoViewStreamListener::OnStopRequest(nsIRequest* aRequest, + nsresult aStatusCode) { + if (mStream) { + if (NS_FAILED(aStatusCode)) { + mStream->SendError(); + } else { + mStream->SendEof(); + } + } + return NS_OK; +} + +NS_IMETHODIMP GeckoViewStreamListener::OnDataAvailable( + nsIRequest* aRequest, nsIInputStream* aInputStream, uint64_t aOffset, + uint32_t aCount) { + MOZ_ASSERT(mStream); + + // We only need this for the ReadSegments call, the value is unused. + uint32_t countRead; + nsresult rv = + aInputStream->ReadSegments(WriteSegment, this, aCount, &countRead); + NS_ENSURE_SUCCESS(rv, rv); + return rv; +} + +NS_IMETHODIMP +GeckoViewStreamListener::GetInterface(const nsIID& aIID, void** aResultOut) { + if (aIID.Equals(NS_GET_IID(nsIChannelEventSink))) { + *aResultOut = static_cast<nsIChannelEventSink*>(this); + NS_ADDREF_THIS(); + return NS_OK; + } + + return NS_ERROR_NO_INTERFACE; +} + +NS_IMETHODIMP +GeckoViewStreamListener::AsyncOnChannelRedirect( + nsIChannel* aOldChannel, nsIChannel* aNewChannel, uint32_t flags, + nsIAsyncVerifyRedirectCallback* callback) { + callback->OnRedirectVerifyCallback(NS_OK); + return NS_OK; +} + +/* static */ +nsresult GeckoViewStreamListener::WriteSegment( + nsIInputStream* aInputStream, void* aClosure, const char* aFromSegment, + uint32_t aToOffset, uint32_t aCount, uint32_t* aWriteCount) { + GeckoViewStreamListener* self = + static_cast<GeckoViewStreamListener*>(aClosure); + MOZ_ASSERT(self); + MOZ_ASSERT(self->mStream); + + *aWriteCount = aCount; + + jni::ByteArray::LocalRef buffer = jni::ByteArray::New( + reinterpret_cast<signed char*>(const_cast<char*>(aFromSegment)), + *aWriteCount, fallible); + if (!buffer) { + return NS_ERROR_OUT_OF_MEMORY; + } + + if (NS_FAILED(self->mStream->AppendBuffer(buffer))) { + // The stream was closed or something, abort reading this channel. + return NS_ERROR_ABORT; + } + + return NS_OK; +} + +nsresult GeckoViewStreamListener::HandleWebResponse(nsIRequest* aRequest) { + nsresult rv; + + nsCOMPtr<nsIChannel> channel = do_QueryInterface(aRequest, &rv); + NS_ENSURE_SUCCESS(rv, rv); + + // URI + nsCOMPtr<nsIURI> uri; + rv = channel->GetURI(getter_AddRefs(uri)); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString uriSpec; + rv = uri->GetSpec(uriSpec); + NS_ENSURE_SUCCESS(rv, rv); + + java::WebResponse::Builder::LocalRef builder = + java::WebResponse::Builder::New(uriSpec); + + // Body stream + if (mStream) { + builder->Body(mStream); + } + + // Redirected + nsCOMPtr<nsILoadInfo> loadInfo = channel->LoadInfo(); + builder->Redirected(!loadInfo->RedirectChain().IsEmpty()); + + // Secure status + auto [certBytes, isSecure] = CertificateFromChannel(channel); + builder->IsSecure(isSecure); + if (certBytes) { + rv = builder->CertificateBytes(certBytes); + NS_ENSURE_SUCCESS(rv, rv); + } + + // We might need some additional info for response to http/https request + nsCOMPtr<nsIHttpChannel> httpChannel(do_QueryInterface(channel, &rv)); + if (httpChannel) { + // Status code + uint32_t statusCode; + rv = httpChannel->GetResponseStatus(&statusCode); + NS_ENSURE_SUCCESS(rv, rv); + builder->StatusCode(statusCode); + + // Headers + RefPtr<HeaderVisitor> visitor = new HeaderVisitor(builder); + rv = httpChannel->VisitResponseHeaders(visitor); + NS_ENSURE_SUCCESS(rv, rv); + } else { + // Headers for other responses + // try to provide some basic metadata about the response + nsString filename; + if (NS_SUCCEEDED(channel->GetContentDispositionFilename(filename))) { + builder->Header(jni::StringParam(u"content-disposition"_ns), + nsPrintfCString("attachment; filename=\"%s\"", + NS_ConvertUTF16toUTF8(filename).get())); + } + + nsCString contentType; + if (NS_SUCCEEDED(channel->GetContentType(contentType))) { + builder->Header(jni::StringParam(u"content-type"_ns), contentType); + } + + int64_t contentLength = 0; + if (NS_SUCCEEDED(channel->GetContentLength(&contentLength))) { + nsString contentLengthString; + contentLengthString.AppendInt(contentLength); + builder->Header(jni::StringParam(u"content-length"_ns), + contentLengthString); + } + } + + java::WebResponse::GlobalRef response = builder->Build(); + + SendWebResponse(response); + return NS_OK; +} + +void GeckoViewStreamListener::InitializeStreamSupport(nsIRequest* aRequest) { + StreamSupport::Init(); + + mSupport = java::GeckoInputStream::Support::New(); + StreamSupport::AttachNative( + mSupport, mozilla::MakeUnique<StreamSupport>(mSupport, aRequest)); +} + +std::tuple<jni::ByteArray::LocalRef, java::sdk::Boolean::LocalRef> +GeckoViewStreamListener::CertificateFromChannel(nsIChannel* aChannel) { + MOZ_ASSERT(aChannel); + + nsCOMPtr<nsITransportSecurityInfo> securityInfo; + aChannel->GetSecurityInfo(getter_AddRefs(securityInfo)); + if (!securityInfo) { + return std::make_tuple((jni::ByteArray::LocalRef) nullptr, + (java::sdk::Boolean::LocalRef) nullptr); + } + + uint32_t securityState = 0; + securityInfo->GetSecurityState(&securityState); + auto isSecure = securityState == nsIWebProgressListener::STATE_IS_SECURE + ? java::sdk::Boolean::TRUE() + : java::sdk::Boolean::FALSE(); + + nsCOMPtr<nsIX509Cert> cert; + securityInfo->GetServerCert(getter_AddRefs(cert)); + if (!cert) { + return std::make_tuple((jni::ByteArray::LocalRef) nullptr, + (java::sdk::Boolean::LocalRef) nullptr); + } + + nsTArray<uint8_t> derBytes; + nsresult rv = cert->GetRawDER(derBytes); + NS_ENSURE_SUCCESS(rv, + std::make_tuple((jni::ByteArray::LocalRef) nullptr, + (java::sdk::Boolean::LocalRef) nullptr)); + + auto certBytes = jni::ByteArray::New( + reinterpret_cast<const int8_t*>(derBytes.Elements()), derBytes.Length()); + + return std::make_tuple(certBytes, isSecure); +} diff --git a/mobile/android/components/geckoview/GeckoViewStreamListener.h b/mobile/android/components/geckoview/GeckoViewStreamListener.h new file mode 100644 index 0000000000..b42249f458 --- /dev/null +++ b/mobile/android/components/geckoview/GeckoViewStreamListener.h @@ -0,0 +1,57 @@ +/* -*- Mode: c++; c-basic-offset: 2; tab-width: 2; indent-tabs-mode: nil; -*- + * 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 GeckoViewStreamListener_h__ +#define GeckoViewStreamListener_h__ + +#include "nsIStreamListener.h" +#include "nsIInterfaceRequestor.h" +#include "nsIChannelEventSink.h" + +#include "mozilla/widget/EventDispatcher.h" +#include "mozilla/java/GeckoInputStreamNatives.h" +#include "mozilla/java/WebResponseWrappers.h" + +#include "JavaBuiltins.h" + +namespace mozilla { + +class GeckoViewStreamListener : public nsIStreamListener, + public nsIInterfaceRequestor, + public nsIChannelEventSink { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIREQUESTOBSERVER + NS_DECL_NSISTREAMLISTENER + NS_DECL_NSIINTERFACEREQUESTOR + NS_DECL_NSICHANNELEVENTSINK + + explicit GeckoViewStreamListener() {} + + static std::tuple<jni::ByteArray::LocalRef, java::sdk::Boolean::LocalRef> + CertificateFromChannel(nsIChannel* aChannel); + + protected: + virtual ~GeckoViewStreamListener() {} + + java::GeckoInputStream::GlobalRef mStream; + java::GeckoInputStream::Support::GlobalRef mSupport; + + void InitializeStreamSupport(nsIRequest* aRequest); + + static nsresult WriteSegment(nsIInputStream* aInputStream, void* aClosure, + const char* aFromSegment, uint32_t aToOffset, + uint32_t aCount, uint32_t* aWriteCount); + + virtual nsresult HandleWebResponse(nsIRequest* aRequest); + + virtual void SendWebResponse(java::WebResponse::Param aResponse) = 0; + + virtual void CompleteWithError(nsresult aStatus, nsIChannel* aChannel) = 0; +}; + +} // namespace mozilla + +#endif // GeckoViewStreamListener_h__ diff --git a/mobile/android/components/geckoview/LoginStorageDelegate.jsm b/mobile/android/components/geckoview/LoginStorageDelegate.jsm new file mode 100644 index 0000000000..c5d3b118f4 --- /dev/null +++ b/mobile/android/components/geckoview/LoginStorageDelegate.jsm @@ -0,0 +1,138 @@ +/* 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/. */ +"use strict"; + +var EXPORTED_SYMBOLS = ["LoginStorageDelegate"]; + +const { GeckoViewUtils } = ChromeUtils.importESModule( + "resource://gre/modules/GeckoViewUtils.sys.mjs" +); + +const { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + GeckoViewPrompter: "resource://gre/modules/GeckoViewPrompter.sys.mjs", +}); + +XPCOMUtils.defineLazyModuleGetters(lazy, { + GeckoViewAutocomplete: "resource://gre/modules/GeckoViewAutocomplete.jsm", + LoginEntry: "resource://gre/modules/GeckoViewAutocomplete.jsm", +}); + +const { debug, warn } = GeckoViewUtils.initLogging("LoginStorageDelegate"); + +// Sync with LoginSaveOption.Hint in Autocomplete.java. +const LoginStorageHint = { + NONE: 0, + GENERATED: 1 << 0, + LOW_CONFIDENCE: 1 << 1, +}; + +class LoginStorageDelegate { + _createMessage({ dismissed, autoSavedLoginGuid }, aLogins) { + let hint = LoginStorageHint.NONE; + if (dismissed) { + hint |= LoginStorageHint.LOW_CONFIDENCE; + } + if (autoSavedLoginGuid) { + hint |= LoginStorageHint.GENERATED; + } + return { + // Sync with PromptController + type: "Autocomplete:Save:Login", + hint, + logins: aLogins, + }; + } + + promptToSavePassword( + aBrowser, + aLogin, + dismissed = false, + notifySaved = false + ) { + const prompt = new lazy.GeckoViewPrompter(aBrowser.ownerGlobal); + prompt.asyncShowPrompt( + this._createMessage({ dismissed }, [ + lazy.LoginEntry.fromLoginInfo(aLogin), + ]), + result => { + const selectedLogin = result?.selection?.value; + + if (!selectedLogin) { + return; + } + + const loginInfo = lazy.LoginEntry.parse(selectedLogin).toLoginInfo(); + Services.obs.notifyObservers(loginInfo, "passwordmgr-prompt-save"); + + lazy.GeckoViewAutocomplete.onLoginSave(selectedLogin); + } + ); + + return { + dismiss() { + prompt.dismiss(); + }, + }; + } + + promptToChangePassword( + aBrowser, + aOldLogin, + aNewLogin, + dismissed = false, + notifySaved = false, + autoSavedLoginGuid = "" + ) { + const newLogin = lazy.LoginEntry.fromLoginInfo(aOldLogin || aNewLogin); + const oldGuid = (aOldLogin && newLogin.guid) || null; + newLogin.origin = aNewLogin.origin; + newLogin.formActionOrigin = aNewLogin.formActionOrigin; + newLogin.password = aNewLogin.password; + newLogin.username = aNewLogin.username; + + const prompt = new lazy.GeckoViewPrompter(aBrowser.ownerGlobal); + prompt.asyncShowPrompt( + this._createMessage({ dismissed, autoSavedLoginGuid }, [newLogin]), + result => { + const selectedLogin = result?.selection?.value; + + if (!selectedLogin) { + return; + } + + lazy.GeckoViewAutocomplete.onLoginSave(selectedLogin); + + const loginInfo = lazy.LoginEntry.parse(selectedLogin).toLoginInfo(); + Services.obs.notifyObservers( + loginInfo, + "passwordmgr-prompt-change", + oldGuid + ); + } + ); + + return { + dismiss() { + prompt.dismiss(); + }, + }; + } + + promptToChangePasswordWithUsernames(aBrowser, aLogins, aNewLogin) { + this.promptToChangePassword(aBrowser, null /* oldLogin */, aNewLogin); + } +} + +LoginStorageDelegate.prototype.classID = Components.ID( + "{3d765750-1c3d-11ea-aaef-0800200c9a66}" +); +LoginStorageDelegate.prototype.QueryInterface = ChromeUtils.generateQI([ + "nsILoginManagerPrompter", +]); diff --git a/mobile/android/components/geckoview/PromptCollection.jsm b/mobile/android/components/geckoview/PromptCollection.jsm new file mode 100644 index 0000000000..16ec60fd2e --- /dev/null +++ b/mobile/android/components/geckoview/PromptCollection.jsm @@ -0,0 +1,48 @@ +/* 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/. */ +"use strict"; + +var EXPORTED_SYMBOLS = ["PromptCollection"]; + +const { GeckoViewUtils } = ChromeUtils.importESModule( + "resource://gre/modules/GeckoViewUtils.sys.mjs" +); + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + GeckoViewPrompter: "resource://gre/modules/GeckoViewPrompter.sys.mjs", +}); + +const { debug, warn } = GeckoViewUtils.initLogging("PromptCollection"); + +class PromptCollection { + confirmRepost(browsingContext) { + const msg = { + type: "repost", + }; + const prompter = new lazy.GeckoViewPrompter(browsingContext); + const result = prompter.showPrompt(msg); + return !!result?.allow; + } + + asyncBeforeUnloadCheck(browsingContext) { + return new Promise(resolve => { + const msg = { + type: "beforeUnload", + }; + const prompter = new lazy.GeckoViewPrompter(browsingContext); + prompter.asyncShowPrompt(msg, resolve); + }).then(result => !!result?.allow); + } + + confirmFolderUpload() { + // Folder upload is not supported by GeckoView yet, see Bug 1674428. + return false; + } +} + +PromptCollection.prototype.QueryInterface = ChromeUtils.generateQI([ + "nsIPromptCollection", +]); diff --git a/mobile/android/components/geckoview/ShareDelegate.jsm b/mobile/android/components/geckoview/ShareDelegate.jsm new file mode 100644 index 0000000000..1e3a133953 --- /dev/null +++ b/mobile/android/components/geckoview/ShareDelegate.jsm @@ -0,0 +1,82 @@ +/* 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/. */ +"use strict"; + +var EXPORTED_SYMBOLS = ["ShareDelegate"]; + +const { GeckoViewUtils } = ChromeUtils.importESModule( + "resource://gre/modules/GeckoViewUtils.sys.mjs" +); + +const lazy = {}; + +ChromeUtils.defineESModuleGetters(lazy, { + GeckoViewPrompter: "resource://gre/modules/GeckoViewPrompter.sys.mjs", +}); + +const domBundle = Services.strings.createBundle( + "chrome://global/locale/dom/dom.properties" +); + +const { debug, warn } = GeckoViewUtils.initLogging("ShareDelegate"); + +class ShareDelegate { + init(aParent) { + this._openerWindow = aParent; + } + + get openerWindow() { + return this._openerWindow; + } + + async share(aTitle, aText, aUri) { + const ABORT = 2; + const FAILURE = 1; + const SUCCESS = 0; + + const msg = { + type: "share", + title: aTitle, + text: aText, + uri: aUri ? aUri.displaySpec : null, + }; + const prompt = new lazy.GeckoViewPrompter(this._openerWindow); + const result = await new Promise(resolve => { + prompt.asyncShowPrompt(msg, resolve); + }); + + if (!result) { + // A null result is treated as a dismissal in GeckoViewPrompter. + throw new DOMException( + domBundle.GetStringFromName("WebShareAPI_Aborted"), + "AbortError" + ); + } + + const res = result && result.response; + switch (res) { + case FAILURE: + throw new DOMException( + domBundle.GetStringFromName("WebShareAPI_Failed"), + "DataError" + ); + case ABORT: // Handle aborted attempt and invalid responses the same. + throw new DOMException( + domBundle.GetStringFromName("WebShareAPI_Aborted"), + "AbortError" + ); + case SUCCESS: + return; + default: + throw new DOMException("Unknown error.", "UnknownError"); + } + } +} + +ShareDelegate.prototype.classID = Components.ID( + "{1201d357-8417-4926-a694-e6408fbedcf8}" +); +ShareDelegate.prototype.QueryInterface = ChromeUtils.generateQI([ + "nsISharePicker", +]); diff --git a/mobile/android/components/geckoview/components.conf b/mobile/android/components/geckoview/components.conf new file mode 100644 index 0000000000..d33567bb74 --- /dev/null +++ b/mobile/android/components/geckoview/components.conf @@ -0,0 +1,93 @@ +# -*- 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/. + +Classes = [ + { + 'cid': '{3e30d2a0-9934-11ea-bb37-0242ac130002}', + 'contract_ids': ['@mozilla.org/embedcomp/prompt-collection;1'], + 'jsm': 'resource://gre/modules/PromptCollection.jsm', + 'constructor': 'PromptCollection', + }, + { + 'js_name': 'prompt', + 'cid': '{076ac188-23c1-4390-aa08-7ef1f78ca5d9}', + 'contract_ids': [ + '@mozilla.org/prompter;1', + ], + 'interfaces': ['nsIPromptService'], + 'jsm': 'resource://gre/modules/GeckoViewPrompt.jsm', + 'constructor': 'PromptFactory', + }, + { + 'cid': '{8e993c34-fdd6-432c-967e-f995d888777f}', + 'contract_ids': ['@mozilla.org/geckoview/startup;1'], + 'jsm': 'resource://gre/modules/GeckoViewStartup.jsm', + 'constructor': 'GeckoViewStartup', + }, + { + 'cid': '{42f3c238-e8e8-4015-9ca2-148723a8afcf}', + 'contract_ids': ['@mozilla.org/content-permission/prompt;1'], + 'jsm': 'resource://gre/modules/GeckoViewPermission.jsm', + 'constructor': 'GeckoViewPermission', + }, + { + 'cid': '{a54d84d7-98a4-4fec-b664-e42e512ae9cc}', + 'contract_ids': ['@mozilla.org/push/Service;1'], + 'jsm': 'resource://gre/modules/GeckoViewPush.jsm', + 'constructor': 'PushService', + }, + { + 'cid': '{fc4bec74-ddd0-4ea8-9a66-9a5081258e32}', + 'contract_ids': ['@mozilla.org/parent/colorpicker;1'], + 'jsm': 'resource://gre/modules/ColorPickerDelegate.jsm', + 'constructor': 'ColorPickerDelegate', + 'processes': ProcessSelector.MAIN_PROCESS_ONLY, + }, + { + 'cid': '{25fdbae6-f684-4bf0-b773-ff2b7a6273c8}', + 'contract_ids': ['@mozilla.org/parent/filepicker;1'], + 'jsm': 'resource://gre/modules/FilePickerDelegate.jsm', + 'constructor': 'FilePickerDelegate', + 'processes': ProcessSelector.MAIN_PROCESS_ONLY, + }, + { + 'cid': '{1201d357-8417-4926-a694-e6408fbedcf8}', + 'contract_ids': ['@mozilla.org/sharepicker;1'], + 'jsm': 'resource://gre/modules/ShareDelegate.jsm', + 'constructor': 'ShareDelegate', + 'processes': ProcessSelector.MAIN_PROCESS_ONLY, + }, + { + 'cid': '{3d765750-1c3d-11ea-aaef-0800200c9a66}', + 'contract_ids': ['@mozilla.org/login-manager/prompter;1'], + 'jsm': 'resource://gre/modules/LoginStorageDelegate.jsm', + 'constructor': 'LoginStorageDelegate', + 'processes': ProcessSelector.MAIN_PROCESS_ONLY, + }, + { + 'cid': '{91455c77-64a1-4c37-be00-f94eb9c7b8e1}', + 'contract_ids': [ + '@mozilla.org/uriloader/external-helper-app-service;1', + ], + 'type': 'GeckoViewExternalAppService', + 'constructor': 'GeckoViewExternalAppService::GetSingleton', + 'headers': ['GeckoViewExternalAppService.h'], + 'processes': ProcessSelector.ALLOW_IN_SOCKET_PROCESS, + }, +] + +if defined('MOZ_ANDROID_HISTORY'): + Classes += [ + { + 'name': 'History', + 'cid': '{0937a705-91a6-417a-8292-b22eb10da86c}', + 'contract_ids': ['@mozilla.org/browser/history;1'], + 'singleton': True, + 'type': 'GeckoViewHistory', + 'headers': ['GeckoViewHistory.h'], + 'constructor': 'GeckoViewHistory::GetSingleton', + }, + ] diff --git a/mobile/android/components/geckoview/moz.build b/mobile/android/components/geckoview/moz.build new file mode 100644 index 0000000000..607b02f367 --- /dev/null +++ b/mobile/android/components/geckoview/moz.build @@ -0,0 +1,49 @@ +# -*- 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/. + +SOURCES += [ + "GeckoViewExternalAppService.cpp", + "GeckoViewOutputStream.cpp", + "GeckoViewStreamListener.cpp", +] + +EXPORTS += [ + "GeckoViewExternalAppService.h", + "GeckoViewOutputStream.h", + "GeckoViewStreamListener.h", +] + +if CONFIG["MOZ_ANDROID_HISTORY"]: + EXPORTS += [ + "GeckoViewHistory.h", + ] + SOURCES += [ + "GeckoViewHistory.cpp", + ] + include("/ipc/chromium/chromium-config.mozbuild") + +XPCOM_MANIFESTS += [ + "components.conf", +] + +EXTRA_COMPONENTS += [ + "GeckoView.manifest", +] + +EXTRA_JS_MODULES += [ + "ColorPickerDelegate.jsm", + "FilePickerDelegate.jsm", + "GeckoViewPermission.jsm", + "GeckoViewPrompt.jsm", + "GeckoViewPrompter.sys.mjs", + "GeckoViewPush.jsm", + "GeckoViewStartup.jsm", + "LoginStorageDelegate.jsm", + "PromptCollection.jsm", + "ShareDelegate.jsm", +] + +FINAL_LIBRARY = "xul" |