blob: 85fdfff613fe54f53db16b312b2a9b7915c3b4c0 [file] [log] [blame]
// Copyright 2020 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.
#import "ios/chrome/browser/link_to_text/link_to_text_tab_helper.h"
#import "base/bind.h"
#import "base/metrics/histogram_functions.h"
#import "base/optional.h"
#import "base/timer/elapsed_timer.h"
#import "base/values.h"
#import "components/shared_highlighting/core/common/disabled_sites.h"
#import "ios/chrome/browser/link_to_text/link_to_text_constants.h"
#import "ios/web/public/js_messaging/web_frame.h"
#import "ios/web/public/js_messaging/web_frames_manager.h"
#import "ios/web/public/ui/crw_web_view_proxy.h"
#import "ios/web/public/ui/crw_web_view_scroll_view_proxy.h"
#import "url/gurl.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
// Interface encapsulating the properties needed to check the contents of the
// selection and whether or not it is editable.
@protocol EditableTextInput <UITextInput>
- (BOOL)isEditable;
@end
namespace {
const char kGetLinkToTextJavaScript[] = "linkToText.getLinkToText";
// Pattern to identify non-whitespace/punctuation characters. Mirrors the regex
// used in the JS lib to identify non-boundary characters.
NSString* const kNotBoundaryCharPattern = @"[^\\p{P}\\s]";
// Limit the search for a non-boundary char to the first 200 characters,
// to ensure this regex does not have performance impact.
const int kBoundaryCharSearchLimit = 200;
// Corresponds to LinkToTextShouldOfferResult in enums.xml; used to log
// fine-grained behavior of ShouldOffer.
enum class ShouldOfferResult {
kSuccess = 0,
kBlockListed = 2,
kSelectionEmpty = 6,
kUserEditing = 7,
kTextInputNotFound = 8,
kPartialSuccess = 9,
// Deprecated. Do not reuse, change, or remove these values.
kRejectedInJavaScript = 1,
kUnableToInvokeJavaScript = 3,
kWebLayerTaskTimeout = 4,
kDispatchedTimeout = 5,
kMaxValue = kPartialSuccess
};
void LogShouldOfferResult(ShouldOfferResult result) {
base::UmaHistogramEnumeration("IOS.LinkToText.ShouldOfferResult", result);
}
// Traverse subviews to find the one responsible for the text selection
// behavior (UITextInput).
UIView<EditableTextInput>* FindInput(UIView* root) {
if ([root conformsToProtocol:@protocol(UITextInput)] &&
[root respondsToSelector:@selector(isEditable)]) {
return (UIView<EditableTextInput>*)root;
}
for (UIView* view in [root subviews]) {
auto* maybe_input = FindInput(view);
if (maybe_input) {
return maybe_input;
}
}
return nil;
}
} // namespace
LinkToTextTabHelper::LinkToTextTabHelper(web::WebState* web_state)
: web_state_(web_state), weak_ptr_factory_(this) {
web_state_->AddObserver(this);
}
LinkToTextTabHelper::~LinkToTextTabHelper() {}
// static
void LinkToTextTabHelper::CreateForWebState(web::WebState* web_state) {
DCHECK(web_state);
if (!FromWebState(web_state)) {
web_state->SetUserData(
UserDataKey(), base::WrapUnique(new LinkToTextTabHelper(web_state)));
}
}
bool LinkToTextTabHelper::ShouldOffer() {
if (!shared_highlighting::ShouldOfferLinkToText(
web_state_->GetLastCommittedURL())) {
LogShouldOfferResult(ShouldOfferResult::kBlockListed);
return false;
}
web::WebFrame* main_frame =
web_state_->GetWebFramesManager()->GetMainWebFrame();
if (!web_state_->ContentIsHTML() || !main_frame ||
!main_frame->CanCallJavaScriptFunction()) {
LogShouldOfferResult(ShouldOfferResult::kUnableToInvokeJavaScript);
return false;
}
UIView<EditableTextInput>* textInputView = FindInput(web_state_->GetView());
if (!textInputView) {
LogShouldOfferResult(ShouldOfferResult::kTextInputNotFound);
NOTREACHED();
return false;
}
if ([textInputView isEditable]) {
LogShouldOfferResult(ShouldOfferResult::kUserEditing);
return false;
}
NSString* selection =
[textInputView textInRange:[textInputView selectedTextRange]];
if (!selection) {
// A bug on older versions can cause selection to be nil. In this case, we
// offer the feature even though it might just be whitespace.
LogShouldOfferResult(ShouldOfferResult::kPartialSuccess);
return true;
}
if (IsOnlyBoundaryChars(selection)) {
LogShouldOfferResult(ShouldOfferResult::kSelectionEmpty);
return false;
}
LogShouldOfferResult(ShouldOfferResult::kSuccess);
return true;
}
void LinkToTextTabHelper::GetLinkToText(LinkToTextCallback callback) {
link_generation_timer_ = std::make_unique<base::ElapsedTimer>();
base::WeakPtr<LinkToTextTabHelper> weak_ptr = weak_ptr_factory_.GetWeakPtr();
web_state_->GetWebFramesManager()->GetMainWebFrame()->CallJavaScriptFunction(
kGetLinkToTextJavaScript, {},
base::BindOnce(^(const base::Value* response) {
if (weak_ptr) {
weak_ptr->OnJavaScriptResponseReceived(callback, response);
}
}),
base::TimeDelta::FromMilliseconds(
link_to_text::kLinkGenerationTimeoutInMs));
}
void LinkToTextTabHelper::OnJavaScriptResponseReceived(
LinkToTextCallback callback,
const base::Value* response) {
if (callback) {
base::TimeDelta latency;
if (link_generation_timer_) {
// Compute latency.
latency = link_generation_timer_->Elapsed();
// Reset variable.
link_generation_timer_.reset();
}
callback([LinkToTextResponse linkToTextResponseWithValue:response
webState:web_state_
latency:latency]);
}
}
bool LinkToTextTabHelper::IsOnlyBoundaryChars(NSString* str) {
if (!not_boundary_char_regex_) {
NSError* error = nil;
not_boundary_char_regex_ = [NSRegularExpression
regularExpressionWithPattern:kNotBoundaryCharPattern
options:0
error:&error];
if (error) {
// We should never get an error from compiling the regex, since it's a
// literal.
NOTREACHED();
return true;
}
}
int max_len = MIN(kBoundaryCharSearchLimit, [str length]);
auto range = [not_boundary_char_regex_
rangeOfFirstMatchInString:str
options:0
range:NSMakeRange(0, max_len)];
return range.location == NSNotFound;
}
void LinkToTextTabHelper::WebStateDestroyed(web::WebState* web_state) {
DCHECK_EQ(web_state_, web_state);
web_state_->RemoveObserver(this);
web_state_ = nil;
// The call to RemoveUserData cause the destruction of the current instance,
// so nothing should be done after that point (this is like "delete this;").
web_state->RemoveUserData(UserDataKey());
}
WEB_STATE_USER_DATA_KEY_IMPL(LinkToTextTabHelper)