blob: 1c2de87598a8df1e5a5ae500f7baaee9867b3aea [file] [log] [blame]
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/core/page/scrolling/text_fragment_anchor.h"
#include "third_party/blink/public/platform/web_scroll_into_view_params.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/dom/element.h"
#include "third_party/blink/renderer/core/editing/editor.h"
#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
#include "third_party/blink/renderer/core/editing/markers/document_marker_controller.h"
#include "third_party/blink/renderer/core/editing/visible_units.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/layout/layout_object.h"
#include "third_party/blink/renderer/core/loader/document_loader.h"
#include "third_party/blink/renderer/core/page/chrome_client.h"
#include "third_party/blink/renderer/core/page/page.h"
#include "third_party/blink/renderer/core/page/scrolling/text_fragment_selector.h"
#include "third_party/blink/renderer/core/scroll/scroll_alignment.h"
#include "third_party/blink/renderer/core/scroll/scrollable_area.h"
namespace blink {
namespace {
bool ParseTargetTextIdentifier(const String& fragment,
Vector<TextFragmentSelector>* out_selectors) {
DCHECK(out_selectors);
size_t start_pos = 0;
size_t end_pos = 0;
while (end_pos != kNotFound) {
if (fragment.Find(kTextFragmentIdentifierPrefix, start_pos) != start_pos) {
return false;
}
start_pos += kTextFragmentIdentifierPrefixStringLength;
end_pos = fragment.find('&', start_pos);
String target_text;
if (end_pos == kNotFound) {
target_text = fragment.Substring(start_pos);
} else {
target_text = fragment.Substring(start_pos, end_pos - start_pos);
start_pos = end_pos + 1;
}
out_selectors->push_back(TextFragmentSelector::Create(target_text));
}
return true;
}
bool CheckSecurityRestrictions(LocalFrame& frame,
bool same_document_navigation) {
// For security reasons, we only allow text fragments on the main frame of a
// main window. So no iframes, no window.open. Also only on a full
// navigation.
if (frame.Tree().Parent() || frame.DomWindow()->opener() ||
same_document_navigation) {
return false;
}
// For security reasons, we only allow text fragment anchors for user or
// browser initiated navigations, i.e. no script navigations.
if (!(frame.Loader().GetDocumentLoader()->HadTransientActivation() ||
frame.Loader().GetDocumentLoader()->IsBrowserInitiated())) {
return false;
}
return true;
}
} // namespace
TextFragmentAnchor* TextFragmentAnchor::TryCreate(
const KURL& url,
LocalFrame& frame,
bool same_document_navigation) {
if (!CheckSecurityRestrictions(frame, same_document_navigation))
return nullptr;
Vector<TextFragmentSelector> selectors;
if (!ParseTargetTextIdentifier(url.FragmentIdentifier(), &selectors))
return nullptr;
return MakeGarbageCollected<TextFragmentAnchor>(
selectors, frame, TextFragmentFormat::PlainFragment);
}
TextFragmentAnchor* TextFragmentAnchor::TryCreateFragmentDirective(
const KURL& url,
LocalFrame& frame,
bool same_document_navigation) {
DCHECK(RuntimeEnabledFeatures::TextFragmentIdentifiersEnabled(
frame.GetDocument()));
if (!CheckSecurityRestrictions(frame, same_document_navigation))
return nullptr;
if (!frame.GetDocument()->GetFragmentDirective())
return nullptr;
Vector<TextFragmentSelector> selectors;
if (!ParseTargetTextIdentifier(frame.GetDocument()->GetFragmentDirective(),
&selectors)) {
UseCounter::Count(frame.GetDocument(),
WebFeature::kInvalidFragmentDirective);
return nullptr;
}
return MakeGarbageCollected<TextFragmentAnchor>(
selectors, frame, TextFragmentFormat::FragmentDirective);
}
TextFragmentAnchor::TextFragmentAnchor(
const Vector<TextFragmentSelector>& text_fragment_selectors,
LocalFrame& frame,
const TextFragmentFormat fragment_format)
: frame_(&frame),
fragment_format_(fragment_format),
metrics_(MakeGarbageCollected<TextFragmentAnchorMetrics>(
frame_->GetDocument())) {
DCHECK(!text_fragment_selectors.IsEmpty());
DCHECK(frame_->View());
metrics_->DidCreateAnchor(text_fragment_selectors.size());
text_fragment_finders_.ReserveCapacity(text_fragment_selectors.size());
for (TextFragmentSelector selector : text_fragment_selectors)
text_fragment_finders_.emplace_back(*this, selector);
}
bool TextFragmentAnchor::Invoke() {
if (element_fragment_anchor_) {
DCHECK(search_finished_);
// We need to keep this TextFragmentAnchor alive if we're proxying an
// element fragment anchor.
return true;
}
// If we're done searching, return true if this hasn't been dismissed yet so
// that this is kept alive.
if (search_finished_)
return !dismissed_;
frame_->GetDocument()->Markers().RemoveMarkersOfTypes(
DocumentMarker::MarkerTypes::TextFragment());
if (user_scrolled_ && !did_scroll_into_view_)
metrics_->ScrollCancelled();
first_match_needs_scroll_ = !user_scrolled_;
{
// FindMatch might cause scrolling and set user_scrolled_ so reset it when
// it's done.
base::AutoReset<bool> reset_user_scrolled(&user_scrolled_, user_scrolled_);
metrics_->ResetMatchCount();
for (auto& finder : text_fragment_finders_)
finder.FindMatch(*frame_->GetDocument());
}
if (frame_->GetDocument()->IsLoadCompleted())
DidFinishSearch();
// We return true to keep this anchor alive as long as we need another invoke,
// are waiting to be dismissed, or are proxying an element fragment anchor.
return !search_finished_ || !dismissed_ || element_fragment_anchor_;
}
void TextFragmentAnchor::Installed() {}
void TextFragmentAnchor::DidScroll(ScrollType type) {
if (!IsExplicitScrollType(type))
return;
user_scrolled_ = true;
}
void TextFragmentAnchor::PerformPreRafActions() {
if (element_fragment_anchor_) {
element_fragment_anchor_->Installed();
element_fragment_anchor_->Invoke();
element_fragment_anchor_->PerformPreRafActions();
element_fragment_anchor_ = nullptr;
}
}
void TextFragmentAnchor::DidCompleteLoad() {
if (search_finished_)
return;
// If there is a pending layout we'll finish the search from Invoke.
if (!frame_->View()->NeedsLayout())
DidFinishSearch();
}
void TextFragmentAnchor::Trace(blink::Visitor* visitor) {
visitor->Trace(frame_);
visitor->Trace(element_fragment_anchor_);
visitor->Trace(metrics_);
FragmentAnchor::Trace(visitor);
}
void TextFragmentAnchor::DidFindMatch(const EphemeralRangeInFlatTree& range) {
if (search_finished_)
return;
// TODO(nburris): Determine what we should do with overlapping text matches.
// This implementation drops a match if it overlaps a previous match, since
// overlapping ranges are likely unintentional by the URL creator and could
// therefore indicate that the page text has changed.
if (!frame_->GetDocument()
->Markers()
.MarkersIntersectingRange(
range, DocumentMarker::MarkerTypes::TextFragment())
.IsEmpty()) {
return;
}
metrics_->DidFindMatch(PlainText(range));
did_find_match_ = true;
if (first_match_needs_scroll_) {
first_match_needs_scroll_ = false;
PhysicalRect bounding_box(ComputeTextRect(range));
// Set the bounding box height to zero because we want to center the top of
// the text range.
bounding_box.SetHeight(LayoutUnit());
DCHECK(range.Nodes().begin() != range.Nodes().end());
Node& node = *range.Nodes().begin();
DCHECK(node.GetLayoutObject());
PhysicalRect scrolled_bounding_box =
node.GetLayoutObject()->ScrollRectToVisible(
bounding_box,
WebScrollIntoViewParams(ScrollAlignment::kAlignCenterAlways,
ScrollAlignment::kAlignCenterAlways,
kProgrammaticScroll));
did_scroll_into_view_ = true;
metrics_->DidScroll();
// We scrolled the text into view if the main document scrolled or the text
// bounding box changed, i.e. if it was scrolled in a nested scroller.
// TODO(nburris): The rect returned by ScrollRectToVisible,
// scrolled_bounding_box, should be in frame coordinates in which case
// just checking its location would suffice, but there is a bug where it is
// actually in document coordinates and therefore does not change with a
// main document scroll.
if (!frame_->View()->GetScrollableArea()->GetScrollOffset().IsZero() ||
scrolled_bounding_box.offset != bounding_box.offset) {
metrics_->DidNonZeroScroll();
}
}
EphemeralRange dom_range =
EphemeralRange(ToPositionInDOMTree(range.StartPosition()),
ToPositionInDOMTree(range.EndPosition()));
frame_->GetDocument()->Markers().AddTextFragmentMarker(dom_range);
}
void TextFragmentAnchor::DidFindAmbiguousMatch() {
metrics_->DidFindAmbiguousMatch();
}
void TextFragmentAnchor::DidFinishSearch() {
DCHECK(!search_finished_);
search_finished_ = true;
metrics_->ReportMetrics();
if (!did_find_match_)
dismissed_ = true;
if (!did_find_match_ &&
fragment_format_ == TextFragmentFormat::FragmentDirective) {
DCHECK(!element_fragment_anchor_);
element_fragment_anchor_ =
ElementFragmentAnchor::TryCreate(frame_->GetDocument()->Url(), *frame_);
if (element_fragment_anchor_) {
// Schedule a frame so we can invoke the element anchor in
// PerformPreRafActions.
frame_->GetPage()->GetChromeClient().ScheduleAnimation(frame_->View());
}
}
}
bool TextFragmentAnchor::Dismiss() {
// To decrease the likelihood of the user dismissing the highlight before
// seeing it, we only dismiss the anchor after search_finished_, at which
// point we've scrolled it into view or the user has started scrolling the
// page.
if (!search_finished_)
return false;
if (!did_find_match_ || dismissed_)
return true;
DCHECK(did_scroll_into_view_ || user_scrolled_);
frame_->GetDocument()->Markers().RemoveMarkersOfTypes(
DocumentMarker::MarkerTypes::TextFragment());
dismissed_ = true;
metrics_->Dismissed();
return dismissed_;
}
} // namespace blink