514 lines
16 KiB
C++
514 lines
16 KiB
C++
/* 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"
|
|
|
|
#ifdef MOZ_WIDGET_ANDROID
|
|
# include "JavaBuiltins.h"
|
|
#endif
|
|
#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 nsIGeckoViewEventCallback {
|
|
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, nsIGeckoViewEventCallback)
|
|
|
|
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;
|
|
}
|
|
|
|
#ifdef MOZ_WIDGET_ANDROID
|
|
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<nsIGeckoViewEventCallback> callback =
|
|
new OnVisitedCallback(this, aURI);
|
|
|
|
Unused << NS_WARN_IF(
|
|
NS_FAILED(dispatcher->Dispatch(kOnVisitedMessage, bundle, callback)));
|
|
#endif
|
|
|
|
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 nsIGeckoViewEventCallback {
|
|
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, nsIGeckoViewEventCallback)
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
|
|
#ifdef MOZ_WIDGET_ANDROID
|
|
// 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<nsIGeckoViewEventCallback> callback =
|
|
new GetVisitedCallback(this, aInterestedProcess, std::move(aURIs));
|
|
|
|
Unused << NS_WARN_IF(
|
|
NS_FAILED(dispatcher->Dispatch(kGetVisitedMessage, bundle, callback)));
|
|
#endif
|
|
}
|
|
|
|
/**
|
|
* 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);
|
|
}
|
|
}
|