tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

GeckoViewHistory.cpp (15665B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 #include "GeckoViewHistory.h"
      6 
      7 #include "jsapi.h"
      8 #include "js/Array.h"               // JS::GetArrayLength, JS::IsArrayObject
      9 #include "js/PropertyAndElement.h"  // JS_GetElement
     10 #include "nsIURI.h"
     11 #include "nsXULAppAPI.h"
     12 
     13 #include "mozilla/ClearOnShutdown.h"
     14 #include "mozilla/StaticPrefs_layout.h"
     15 
     16 #include "mozilla/dom/ContentParent.h"
     17 #include "mozilla/dom/Element.h"
     18 #include "mozilla/dom/Link.h"
     19 #include "mozilla/dom/BrowserChild.h"
     20 
     21 #include "mozilla/ipc/URIUtils.h"
     22 
     23 #include "mozilla/widget/EventDispatcher.h"
     24 #include "mozilla/widget/nsWindow.h"
     25 
     26 using namespace mozilla;
     27 using namespace mozilla::dom;
     28 using namespace mozilla::ipc;
     29 using namespace mozilla::widget;
     30 
     31 static const nsLiteralString kOnVisitedMessage = u"GeckoView:OnVisited"_ns;
     32 static const nsLiteralString kGetVisitedMessage = u"GeckoView:GetVisited"_ns;
     33 
     34 // Keep in sync with `GeckoSession.HistoryDelegate.VisitFlags`.
     35 enum class GeckoViewVisitFlags : int32_t {
     36  VISIT_TOP_LEVEL = 1 << 0,
     37  VISIT_REDIRECT_TEMPORARY = 1 << 1,
     38  VISIT_REDIRECT_PERMANENT = 1 << 2,
     39  VISIT_REDIRECT_SOURCE = 1 << 3,
     40  VISIT_REDIRECT_SOURCE_PERMANENT = 1 << 4,
     41  VISIT_UNRECOVERABLE_ERROR = 1 << 5,
     42 };
     43 
     44 GeckoViewHistory::GeckoViewHistory() {}
     45 
     46 GeckoViewHistory::~GeckoViewHistory() {}
     47 
     48 NS_IMPL_ISUPPORTS(GeckoViewHistory, IHistory)
     49 
     50 StaticRefPtr<GeckoViewHistory> GeckoViewHistory::sHistory;
     51 
     52 /* static */
     53 already_AddRefed<GeckoViewHistory> GeckoViewHistory::GetSingleton() {
     54  if (!sHistory) {
     55    sHistory = new GeckoViewHistory();
     56    ClearOnShutdown(&sHistory);
     57  }
     58  RefPtr<GeckoViewHistory> history = sHistory;
     59  return history.forget();
     60 }
     61 
     62 // Handles a request to fetch visited statuses for new tracked URIs in the
     63 // content process (e10s).
     64 void GeckoViewHistory::QueryVisitedStateInContentProcess(
     65    const PendingVisitedQueries& aQueries) {
     66  // Holds an array of new tracked URIs for a tab in the content process.
     67  struct NewURIEntry {
     68    explicit NewURIEntry(BrowserChild* aBrowserChild, nsIURI* aURI)
     69        : mBrowserChild(aBrowserChild) {
     70      AddURI(aURI);
     71    }
     72 
     73    void AddURI(nsIURI* aURI) { mURIs.AppendElement(aURI); }
     74 
     75    BrowserChild* mBrowserChild;
     76    nsTArray<RefPtr<nsIURI>> mURIs;
     77  };
     78 
     79  MOZ_ASSERT(XRE_IsContentProcess());
     80 
     81  // First, serialize all the new URIs that we need to look up. Note that this
     82  // could be written as `nsTHashMap<nsUint64HashKey,
     83  // nsTArray<URIParams>` instead, but, since we don't expect to have many tab
     84  // children, we can avoid the cost of hashing.
     85  AutoTArray<NewURIEntry, 8> newEntries;
     86  for (auto& query : aQueries) {
     87    nsIURI* uri = query.GetKey();
     88    MOZ_ASSERT(query.GetData().IsEmpty(),
     89               "Shouldn't have parents to notify in child processes");
     90    auto entry = mTrackedURIs.Lookup(uri);
     91    if (!entry) {
     92      continue;
     93    }
     94    ObservingLinks& links = entry.Data();
     95    for (Link* link : links.mLinks.BackwardRange()) {
     96      nsIWidget* widget = nsContentUtils::WidgetForContent(link->GetElement());
     97      if (!widget) {
     98        continue;
     99      }
    100      BrowserChild* browserChild = widget->GetOwningBrowserChild();
    101      if (!browserChild) {
    102        continue;
    103      }
    104      // Add to the list of new URIs for this document, or make a new entry.
    105      bool hasEntry = false;
    106      for (NewURIEntry& entry : newEntries) {
    107        if (entry.mBrowserChild == browserChild) {
    108          entry.AddURI(uri);
    109          hasEntry = true;
    110          break;
    111        }
    112      }
    113      if (!hasEntry) {
    114        newEntries.AppendElement(NewURIEntry(browserChild, uri));
    115      }
    116    }
    117  }
    118 
    119  // Send the request to the parent process, one message per tab child.
    120  for (const NewURIEntry& entry : newEntries) {
    121    (void)NS_WARN_IF(!entry.mBrowserChild->SendQueryVisitedState(entry.mURIs));
    122  }
    123 }
    124 
    125 // Handles a request to fetch visited statuses for new tracked URIs in the
    126 // parent process (non-e10s).
    127 void GeckoViewHistory::QueryVisitedStateInParentProcess(
    128    const PendingVisitedQueries& aQueries) {
    129  // Holds an array of new URIs for a window in the parent process. Unlike
    130  // the content process case, we don't need to track tab children, since we
    131  // have the outer window and can send the request directly to Java.
    132  struct NewURIEntry {
    133    explicit NewURIEntry(nsIWidget* aWidget, nsIURI* aURI) : mWidget(aWidget) {
    134      AddURI(aURI);
    135    }
    136 
    137    void AddURI(nsIURI* aURI) { mURIs.AppendElement(aURI); }
    138 
    139    nsCOMPtr<nsIWidget> mWidget;
    140    nsTArray<RefPtr<nsIURI>> mURIs;
    141  };
    142 
    143  MOZ_ASSERT(XRE_IsParentProcess());
    144 
    145  nsTArray<NewURIEntry> newEntries;
    146  for (const auto& query : aQueries) {
    147    nsIURI* uri = query.GetKey();
    148    auto entry = mTrackedURIs.Lookup(uri);
    149    if (!entry) {
    150      continue;  // Nobody cares about this uri anymore.
    151    }
    152 
    153    ObservingLinks& links = entry.Data();
    154    nsTObserverArray<Link*>::BackwardIterator linksIter(links.mLinks);
    155    while (linksIter.HasMore()) {
    156      Link* link = linksIter.GetNext();
    157 
    158      nsIWidget* widget = nsContentUtils::WidgetForContent(link->GetElement());
    159      if (!widget) {
    160        continue;
    161      }
    162 
    163      bool hasEntry = false;
    164      for (NewURIEntry& entry : newEntries) {
    165        if (entry.mWidget != widget) {
    166          continue;
    167        }
    168        entry.AddURI(uri);
    169        hasEntry = true;
    170      }
    171      if (!hasEntry) {
    172        newEntries.AppendElement(NewURIEntry(widget, uri));
    173      }
    174    }
    175  }
    176 
    177  for (NewURIEntry& entry : newEntries) {
    178    QueryVisitedState(entry.mWidget, nullptr, std::move(entry.mURIs));
    179  }
    180 }
    181 
    182 void GeckoViewHistory::StartPendingVisitedQueries(
    183    PendingVisitedQueries&& aQueries) {
    184  if (XRE_IsContentProcess()) {
    185    QueryVisitedStateInContentProcess(aQueries);
    186  } else {
    187    QueryVisitedStateInParentProcess(aQueries);
    188  }
    189 }
    190 
    191 /**
    192 * Called from the session handler for the history delegate, after the new
    193 * visit is recorded.
    194 */
    195 class OnVisitedCallback final : public nsIGeckoViewEventCallback {
    196 public:
    197  explicit OnVisitedCallback(GeckoViewHistory* aHistory, nsIURI* aURI)
    198      : mHistory(aHistory), mURI(aURI) {}
    199 
    200  NS_DECL_ISUPPORTS
    201 
    202  NS_IMETHOD
    203  OnSuccess(JS::Handle<JS::Value> aData, JSContext* aCx) override {
    204    Maybe<bool> visitedState = GetVisitedValue(aCx, aData);
    205    JS_ClearPendingException(aCx);
    206    if (visitedState) {
    207      AutoTArray<VisitedURI, 1> visitedURIs;
    208      visitedURIs.AppendElement(VisitedURI{mURI.get(), *visitedState});
    209      mHistory->HandleVisitedState(visitedURIs, nullptr);
    210    }
    211    return NS_OK;
    212  }
    213 
    214  NS_IMETHOD
    215  OnError(JS::Handle<JS::Value> aData, JSContext* aCx) override {
    216    return NS_OK;
    217  }
    218 
    219 private:
    220  virtual ~OnVisitedCallback() {}
    221 
    222  Maybe<bool> GetVisitedValue(JSContext* aCx, JS::Handle<JS::Value> aData) {
    223    if (NS_WARN_IF(!aData.isBoolean())) {
    224      return Nothing();
    225    }
    226    return Some(aData.toBoolean());
    227  }
    228 
    229  RefPtr<GeckoViewHistory> mHistory;
    230  nsCOMPtr<nsIURI> mURI;
    231 };
    232 
    233 NS_IMPL_ISUPPORTS(OnVisitedCallback, nsIGeckoViewEventCallback)
    234 
    235 NS_IMETHODIMP
    236 GeckoViewHistory::VisitURI(nsIWidget* aWidget, nsIURI* aURI,
    237                           nsIURI* aLastVisitedURI, uint32_t aFlags,
    238                           uint64_t aBrowserId) {
    239  AssertIsOnMainThread();
    240  if (!aURI) {
    241    return NS_OK;
    242  }
    243 
    244  if (XRE_IsContentProcess()) {
    245    // If we're in the content process, send the visit to the parent. The parent
    246    // will find the matching chrome window for the content process and tab,
    247    // then forward the visit to Java.
    248    if (NS_WARN_IF(!aWidget)) {
    249      return NS_OK;
    250    }
    251    BrowserChild* browserChild = aWidget->GetOwningBrowserChild();
    252    if (NS_WARN_IF(!browserChild)) {
    253      return NS_OK;
    254    }
    255    (void)NS_WARN_IF(
    256        !browserChild->SendVisitURI(aURI, aLastVisitedURI, aFlags, aBrowserId));
    257    return NS_OK;
    258  }
    259 
    260  // Otherwise, we're in the parent process. Wrap the URIs up in a bundle, and
    261  // send them to Java.
    262  MOZ_ASSERT(XRE_IsParentProcess());
    263  RefPtr<nsWindow> window = nsWindow::From(aWidget);
    264  if (NS_WARN_IF(!window)) {
    265    return NS_OK;
    266  }
    267  widget::EventDispatcher* dispatcher = window->GetEventDispatcher();
    268  if (NS_WARN_IF(!dispatcher)) {
    269    return NS_OK;
    270  }
    271 
    272  // If nobody is listening for this, we can stop now.
    273  if (!dispatcher->HasEmbedderListener(kOnVisitedMessage)) {
    274    return NS_OK;
    275  }
    276 
    277  dom::AutoJSAPI jsapi;
    278  NS_ENSURE_TRUE(jsapi.Init(xpc::PrivilegedJunkScope()), NS_OK);
    279 
    280  JS::Rooted<JSObject*> bundle(jsapi.cx(), JS_NewPlainObject(jsapi.cx()));
    281  NS_ENSURE_TRUE(bundle, NS_OK);
    282 
    283  JS::Rooted<JS::Value> value(jsapi.cx());
    284 
    285  nsAutoCString uriSpec;
    286  if (NS_WARN_IF(NS_FAILED(aURI->GetSpec(uriSpec)))) {
    287    return NS_OK;
    288  }
    289  NS_ENSURE_TRUE(ToJSValue(jsapi.cx(), uriSpec, &value), NS_OK);
    290  NS_ENSURE_TRUE(JS_SetProperty(jsapi.cx(), bundle, "url", value), NS_OK);
    291 
    292  if (aLastVisitedURI) {
    293    nsAutoCString lastVisitedURISpec;
    294    if (NS_WARN_IF(NS_FAILED(aLastVisitedURI->GetSpec(lastVisitedURISpec)))) {
    295      return NS_OK;
    296    }
    297    NS_ENSURE_TRUE(ToJSValue(jsapi.cx(), lastVisitedURISpec, &value), NS_OK);
    298    NS_ENSURE_TRUE(JS_SetProperty(jsapi.cx(), bundle, "lastVisitedURL", value),
    299                   NS_OK);
    300  }
    301 
    302  int32_t flags = 0;
    303  if (aFlags & TOP_LEVEL) {
    304    flags |= static_cast<int32_t>(GeckoViewVisitFlags::VISIT_TOP_LEVEL);
    305  }
    306  if (aFlags & REDIRECT_TEMPORARY) {
    307    flags |=
    308        static_cast<int32_t>(GeckoViewVisitFlags::VISIT_REDIRECT_TEMPORARY);
    309  }
    310  if (aFlags & REDIRECT_PERMANENT) {
    311    flags |=
    312        static_cast<int32_t>(GeckoViewVisitFlags::VISIT_REDIRECT_PERMANENT);
    313  }
    314  if (aFlags & REDIRECT_SOURCE) {
    315    flags |= static_cast<int32_t>(GeckoViewVisitFlags::VISIT_REDIRECT_SOURCE);
    316  }
    317  if (aFlags & REDIRECT_SOURCE_PERMANENT) {
    318    flags |= static_cast<int32_t>(
    319        GeckoViewVisitFlags::VISIT_REDIRECT_SOURCE_PERMANENT);
    320  }
    321  if (aFlags & UNRECOVERABLE_ERROR) {
    322    flags |=
    323        static_cast<int32_t>(GeckoViewVisitFlags::VISIT_UNRECOVERABLE_ERROR);
    324  }
    325  value = JS::Int32Value(flags);
    326  NS_ENSURE_TRUE(JS_SetProperty(jsapi.cx(), bundle, "flags", value), NS_OK);
    327 
    328  nsCOMPtr<nsIGeckoViewEventCallback> callback =
    329      new OnVisitedCallback(this, aURI);
    330 
    331  (void)NS_WARN_IF(
    332      NS_FAILED(dispatcher->Dispatch(kOnVisitedMessage, bundle, callback)));
    333  return NS_OK;
    334 }
    335 
    336 NS_IMETHODIMP
    337 GeckoViewHistory::SetURITitle(nsIURI* aURI, const nsAString& aTitle) {
    338  return NS_ERROR_NOT_IMPLEMENTED;
    339 }
    340 
    341 /**
    342 * Called from the session handler for the history delegate, with visited
    343 * statuses for all requested URIs.
    344 */
    345 class GetVisitedCallback final : public nsIGeckoViewEventCallback {
    346 public:
    347  explicit GetVisitedCallback(GeckoViewHistory* aHistory,
    348                              ContentParent* aInterestedProcess,
    349                              nsTArray<RefPtr<nsIURI>>&& aURIs)
    350      : mHistory(aHistory),
    351        mInterestedProcess(aInterestedProcess),
    352        mURIs(std::move(aURIs)) {}
    353 
    354  NS_DECL_ISUPPORTS
    355 
    356  NS_IMETHOD
    357  OnSuccess(JS::Handle<JS::Value> aData, JSContext* aCx) override {
    358    nsTArray<VisitedURI> visitedURIs;
    359    if (!ExtractVisitedURIs(aCx, aData, visitedURIs)) {
    360      JS_ClearPendingException(aCx);
    361      return NS_ERROR_FAILURE;
    362    }
    363    IHistory::ContentParentSet interestedProcesses;
    364    if (mInterestedProcess) {
    365      interestedProcesses.Insert(mInterestedProcess);
    366    }
    367    mHistory->HandleVisitedState(visitedURIs, &interestedProcesses);
    368    return NS_OK;
    369  }
    370 
    371  NS_IMETHOD
    372  OnError(JS::Handle<JS::Value> aData, JSContext* aCx) override {
    373    return NS_OK;
    374  }
    375 
    376 private:
    377  virtual ~GetVisitedCallback() {}
    378 
    379  /**
    380   * Unpacks an array of Boolean visited statuses from the session handler into
    381   * an array of `VisitedURI` structs. Each element in the array corresponds to
    382   * a URI in `mURIs`.
    383   *
    384   * Returns `false` on error, `true` if the array is `null` or was successfully
    385   * unpacked.
    386   *
    387   * TODO (bug 1503482): Remove this unboxing.
    388   */
    389  bool ExtractVisitedURIs(JSContext* aCx, JS::Handle<JS::Value> aData,
    390                          nsTArray<VisitedURI>& aVisitedURIs) {
    391    if (aData.isNull()) {
    392      return true;
    393    }
    394    bool isArray = false;
    395    if (NS_WARN_IF(!JS::IsArrayObject(aCx, aData, &isArray))) {
    396      return false;
    397    }
    398    if (NS_WARN_IF(!isArray)) {
    399      return false;
    400    }
    401    JS::Rooted<JSObject*> visited(aCx, &aData.toObject());
    402    uint32_t length = 0;
    403    if (NS_WARN_IF(!JS::GetArrayLength(aCx, visited, &length))) {
    404      return false;
    405    }
    406    if (NS_WARN_IF(length != mURIs.Length())) {
    407      return false;
    408    }
    409    if (!aVisitedURIs.SetCapacity(length, mozilla::fallible)) {
    410      return false;
    411    }
    412    for (uint32_t i = 0; i < length; ++i) {
    413      JS::Rooted<JS::Value> value(aCx);
    414      if (NS_WARN_IF(!JS_GetElement(aCx, visited, i, &value))) {
    415        JS_ClearPendingException(aCx);
    416        aVisitedURIs.AppendElement(VisitedURI{mURIs[i].get(), false});
    417        continue;
    418      }
    419      if (NS_WARN_IF(!value.isBoolean())) {
    420        aVisitedURIs.AppendElement(VisitedURI{mURIs[i].get(), false});
    421        continue;
    422      }
    423      aVisitedURIs.AppendElement(VisitedURI{mURIs[i].get(), value.toBoolean()});
    424    }
    425    return true;
    426  }
    427 
    428  RefPtr<GeckoViewHistory> mHistory;
    429  RefPtr<ContentParent> mInterestedProcess;
    430  nsTArray<RefPtr<nsIURI>> mURIs;
    431 };
    432 
    433 NS_IMPL_ISUPPORTS(GetVisitedCallback, nsIGeckoViewEventCallback)
    434 
    435 /**
    436 * Queries the history delegate to find which URIs have been visited. This
    437 * is always called in the parent process: from `GetVisited` in non-e10s, and
    438 * from `ContentParent::RecvGetVisited` in e10s.
    439 */
    440 void GeckoViewHistory::QueryVisitedState(nsIWidget* aWidget,
    441                                         ContentParent* aInterestedProcess,
    442                                         nsTArray<RefPtr<nsIURI>>&& aURIs) {
    443  MOZ_ASSERT(XRE_IsParentProcess());
    444  AssertIsOnMainThread();
    445 
    446  RefPtr<nsWindow> window = nsWindow::From(aWidget);
    447  if (NS_WARN_IF(!window)) {
    448    return;
    449  }
    450  widget::EventDispatcher* dispatcher = window->GetEventDispatcher();
    451  if (NS_WARN_IF(!dispatcher)) {
    452    return;
    453  }
    454 
    455  // If nobody is listening for this we can stop now
    456  if (!dispatcher->HasEmbedderListener(kGetVisitedMessage)) {
    457    return;
    458  }
    459 
    460  dom::AutoJSAPI jsapi;
    461  NS_ENSURE_TRUE_VOID(jsapi.Init(xpc::PrivilegedJunkScope()));
    462 
    463  nsTArray<nsCString> specs(aURIs.Length());
    464  for (auto& uri : aURIs) {
    465    nsAutoCString uriSpec;
    466    if (NS_WARN_IF(NS_FAILED(uri->GetSpec(uriSpec)))) {
    467      continue;
    468    }
    469    specs.AppendElement(uriSpec);
    470  }
    471 
    472  JS::Rooted<JS::Value> urls(jsapi.cx());
    473  NS_ENSURE_TRUE_VOID(ToJSValue(jsapi.cx(), specs, &urls));
    474 
    475  JS::Rooted<JSObject*> bundle(jsapi.cx(), JS_NewPlainObject(jsapi.cx()));
    476  NS_ENSURE_TRUE_VOID(bundle);
    477  NS_ENSURE_TRUE_VOID(JS_SetProperty(jsapi.cx(), bundle, "urls", urls));
    478 
    479  nsCOMPtr<nsIGeckoViewEventCallback> callback =
    480      new GetVisitedCallback(this, aInterestedProcess, std::move(aURIs));
    481 
    482  (void)NS_WARN_IF(
    483      NS_FAILED(dispatcher->Dispatch(kGetVisitedMessage, bundle, callback)));
    484 }
    485 
    486 /**
    487 * Updates link states for all tracked links, forwarding the visited statuses to
    488 * the content process in e10s. This is always called in the parent process,
    489 * from `VisitedCallback::OnSuccess` and `GetVisitedCallback::OnSuccess`.
    490 */
    491 void GeckoViewHistory::HandleVisitedState(
    492    const nsTArray<VisitedURI>& aVisitedURIs,
    493    ContentParentSet* aInterestedProcesses) {
    494  MOZ_ASSERT(XRE_IsParentProcess());
    495 
    496  for (const VisitedURI& visitedURI : aVisitedURIs) {
    497    auto status =
    498        visitedURI.mVisited ? VisitedStatus::Visited : VisitedStatus::Unvisited;
    499    NotifyVisited(visitedURI.mURI, status, aInterestedProcesses);
    500  }
    501 }