Commit d023fe30 authored by Gauthier Ambard's avatar Gauthier Ambard Committed by Chromium LUCI CQ

[iOS] Extract JavaScript logic from ContextMenu

This CL extracts the JavaScript logic to find the DOM element on which
the user is long pressing and creating the ContextMenuParams out of it.

It is also renaming HTMLElementFetchRequest in
CRWHTMLElementFetchRequest to respect ios/web standard.

Bug: 1140387
Change-Id: I62e6e2df9be54f67706ff440354912b00d77337c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2580073
Commit-Queue: Gauthier Ambard <gambard@chromium.org>
Reviewed-by: default avatarMike Dougherty <michaeldo@chromium.org>
Cr-Commit-Position: refs/heads/master@{#841459}
parent c29e41c1
......@@ -478,11 +478,11 @@ source_set("ios_web_web_state_ui_unittests") {
]
sources = [
"web_state/ui/crw_html_element_fetch_request_unittest.mm",
"web_state/ui/crw_web_controller_unittest.mm",
"web_state/ui/crw_web_view_content_view_unittest.mm",
"web_state/ui/crw_web_view_proxy_impl_unittest.mm",
"web_state/ui/crw_web_view_scroll_view_proxy_unittest.mm",
"web_state/ui/html_element_fetch_request_unittest.mm",
"web_state/ui/web_view_js_utils_unittest.mm",
"web_state/ui/wk_content_rule_list_util_unittest.mm",
"web_state/ui/wk_web_view_configuration_provider_unittest.mm",
......@@ -527,6 +527,7 @@ test("ios_web_inttests") {
":web",
"//base/test:test_support",
"//ios/net",
"//ios/testing:block_swizzler",
"//ios/testing:embedded_test_server_support",
"//ios/testing:http_server_bundle_data",
"//ios/web:resources_grit",
......@@ -554,6 +555,8 @@ test("ios_web_inttests") {
"//ios/web/test:test_constants",
"//ios/web/test:test_support",
"//ios/web/web_state",
"//ios/web/web_state:context_menu",
"//ios/web/web_state/ui:crw_context_menu_controller",
"//mojo/core/embedder",
"//net:test_support",
"//services/network/public/cpp",
......@@ -578,6 +581,7 @@ test("ios_web_inttests") {
"web_state/error_page_inttest.mm",
"web_state/http_auth_inttest.mm",
"web_state/keep_render_process_alive_inttest.mm",
"web_state/ui/crw_context_menu_element_fetcher_inttest.mm",
"web_state/web_state_observer_inttest.mm",
"webui/web_ui_inttest.mm",
"webui/web_ui_mojo_inttest.mm",
......
......@@ -11,10 +11,9 @@
namespace web {
// Returns true if the |element| dictionary contains enough information to
// present a context menu. (A valid url for either kContextMenuElementHyperlink
// or kContextMenuElementSource must exist in the dicitionary.)
BOOL CanShowContextMenuForElementDictionary(NSDictionary* element);
// Returns true if the |params| contain enough information to present a context
// menu. (A valid url for either link_url or src_url must exist in the params.)
BOOL CanShowContextMenuForParams(const ContextMenuParams& params);
// creates a ContextMenuParams from a NSDictionary representing an HTML element.
// The fields "href", "src", "title", "referrerPolicy" and "innerText" will
......
......@@ -68,13 +68,11 @@ TitleAndOrigin GetContextMenuTitleAndOrigin(NSDictionary* element) {
namespace web {
BOOL CanShowContextMenuForElementDictionary(NSDictionary* element) {
NSString* href = element[kContextMenuElementHyperlink];
if (GURL(base::SysNSStringToUTF8(href)).is_valid()) {
BOOL CanShowContextMenuForParams(const ContextMenuParams& params) {
if (params.link_url.is_valid()) {
return YES;
}
NSString* src = element[kContextMenuElementSource];
if (GURL(base::SysNSStringToUTF8(src)).is_valid()) {
if (params.src_url.is_valid()) {
return YES;
}
return NO;
......
......@@ -113,55 +113,49 @@ TEST_F(ContextMenuParamsUtilsTest, DictionaryConstructorTestDataTitle) {
EXPECT_EQ(params.menu_title_origin, ContextMenuTitleOrigin::kURL);
}
// Tests that a context menu will not be shown for an empty element dictionary.
// Tests that a context menu will not be shown for empty params.
TEST_F(ContextMenuParamsUtilsTest, CanShowContextMenuTestEmptyDictionary) {
EXPECT_FALSE(CanShowContextMenuForElementDictionary(@{}));
}
// Tests that a context menu will not be shown for an element dictionary with
// only a request id.
TEST_F(ContextMenuParamsUtilsTest, CanShowContextMenuTestRequestIdOnly) {
EXPECT_FALSE(CanShowContextMenuForElementDictionary(
@{kContextMenuElementRequestId : @"kContextMenuElementRequestId"}));
EXPECT_FALSE(CanShowContextMenuForParams(ContextMenuParams()));
}
// Tests that a context menu will be shown for a link.
TEST_F(ContextMenuParamsUtilsTest, CanShowContextMenuTestHyperlink) {
EXPECT_TRUE(CanShowContextMenuForElementDictionary(@{
kContextMenuElementHyperlink : @"http://example.com",
kContextMenuElementInnerText : @"Click me."
}));
ContextMenuParams params;
params.link_url = GURL("http://example.com");
params.link_text = @"Click me.";
EXPECT_TRUE(CanShowContextMenuForParams(params));
}
// Tests that a context menu will not be shown for an invalid link.
TEST_F(ContextMenuParamsUtilsTest, CanShowContextMenuTestInvalidHyperlink) {
EXPECT_FALSE(CanShowContextMenuForElementDictionary(
@{kContextMenuElementHyperlink : @"invalid_url"}));
ContextMenuParams params;
params.link_url = GURL("invalid_url");
EXPECT_FALSE(CanShowContextMenuForParams(params));
}
// Tests that a context menu will be shown for an image.
TEST_F(ContextMenuParamsUtilsTest, CanShowContextMenuTestImageWithTitle) {
EXPECT_TRUE(CanShowContextMenuForElementDictionary(@{
kContextMenuElementSource : @"http://example.com/image.jpeg",
kContextMenuElementTitle : @"Image"
}));
ContextMenuParams params;
params.src_url = GURL("http://example.com/image.jpeg");
params.menu_title = @"Image";
EXPECT_TRUE(CanShowContextMenuForParams(params));
}
// Tests that a context menu will not be shown for an image with an invalid
// source url.
TEST_F(ContextMenuParamsUtilsTest,
CanShowContextMenuTestImageWithInvalidSource) {
EXPECT_FALSE(CanShowContextMenuForElementDictionary(@{
kContextMenuElementSource : @"invalid_url",
}));
ContextMenuParams params;
params.src_url = GURL("invalid_url");
EXPECT_FALSE(CanShowContextMenuForParams(params));
}
// Tests that a context menu will be shown for a linked image.
TEST_F(ContextMenuParamsUtilsTest, CanShowContextMenuTestLinkedImage) {
EXPECT_TRUE(CanShowContextMenuForElementDictionary(@{
kContextMenuElementHyperlink : @"http://example.com",
kContextMenuElementSource : @"http://example.com/image.jpeg"
}));
ContextMenuParams params;
params.link_url = GURL("http://example.com");
params.src_url = GURL("http://example.com/image.jpeg");
EXPECT_TRUE(CanShowContextMenuForParams(params));
}
// Tests that the menu title prepends the element's alt text if it is an image
......
......@@ -104,10 +104,12 @@ source_set("crw_context_menu_controller") {
]
sources = [
"crw_context_menu_element_fetcher.h",
"crw_context_menu_element_fetcher.mm",
"crw_html_element_fetch_request.h",
"crw_html_element_fetch_request.mm",
"crw_legacy_context_menu_controller.h",
"crw_legacy_context_menu_controller.mm",
"html_element_fetch_request.h",
"html_element_fetch_request.mm",
]
configs += [ "//build/config/compiler:enable_arc" ]
......
// 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.
#ifndef IOS_WEB_WEB_STATE_UI_CRW_CONTEXT_MENU_ELEMENT_FETCHER_H_
#define IOS_WEB_WEB_STATE_UI_CRW_CONTEXT_MENU_ELEMENT_FETCHER_H_
#import <Foundation/Foundation.h>
#import <WebKit/WebKit.h>
namespace web {
struct ContextMenuParams;
class WebState;
}
// Class handling the fetching information about DOM element in a specific
// position.
@interface CRWContextMenuElementFetcher : NSObject
- (instancetype)initWithWebView:(WKWebView*)webView
webState:(web::WebState*)webState;
// Asynchronously fetches information about DOM element for the given |point|
// (in the scroll view coordinates). |handler| can not be nil.
- (void)fetchDOMElementAtPoint:(CGPoint)point
completionHandler:(void (^)(const web::ContextMenuParams&))handler;
// Cancels all the fetches current in progress.
- (void)cancelFetches;
@end
#endif // IOS_WEB_WEB_STATE_UI_CRW_CONTEXT_MENU_ELEMENT_FETCHER_H_
// 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/web/web_state/ui/crw_context_menu_element_fetcher.h"
#include "base/strings/sys_string_conversions.h"
#include "base/unguessable_token.h"
#include "base/values.h"
#import "ios/web/js_messaging/crw_wk_script_message_router.h"
#import "ios/web/public/js_messaging/web_frame.h"
#import "ios/web/public/js_messaging/web_frame_util.h"
#import "ios/web/public/ui/context_menu_params.h"
#import "ios/web/public/web_state.h"
#import "ios/web/public/web_state_observer_bridge.h"
#import "ios/web/web_state/context_menu_constants.h"
#import "ios/web/web_state/context_menu_params_utils.h"
#import "ios/web/web_state/ui/crw_html_element_fetch_request.h"
#import "ios/web/web_state/ui/wk_web_view_configuration_provider.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
namespace {
// Javascript function name to obtain element details at a point.
const char kFindElementAtPointFunctionName[] = "findElementAtPoint";
// JavaScript message handler name installed in WKWebView for found element
// response.
NSString* const kFindElementResultHandlerName = @"FindElementResultHandler";
} // namespace
@interface CRWContextMenuElementFetcher () <CRWWebStateObserver> {
std::unique_ptr<web::WebStateObserverBridge> _observer;
}
@property(nonatomic, readonly, weak) WKWebView* webView;
@property(nonatomic, assign) web::WebState* webState;
// Details for currently in progress element fetches. The objects are
// instances of CRWHTMLElementFetchRequest and are keyed by a unique requestId
// string.
@property(nonatomic, strong) NSMutableDictionary* pendingElementFetchRequests;
@end
@implementation CRWContextMenuElementFetcher
- (instancetype)initWithWebView:(WKWebView*)webView
webState:(web::WebState*)webState {
self = [super init];
if (self) {
_pendingElementFetchRequests = [[NSMutableDictionary alloc] init];
_webView = webView;
_webState = webState;
_observer = std::make_unique<web::WebStateObserverBridge>(self);
webState->AddObserver(_observer.get());
// Listen for fetched element response.
web::WKWebViewConfigurationProvider& configurationProvider =
web::WKWebViewConfigurationProvider::FromBrowserState(
webState->GetBrowserState());
CRWWKScriptMessageRouter* messageRouter =
configurationProvider.GetScriptMessageRouter();
__weak __typeof(self) weakSelf = self;
[messageRouter
setScriptMessageHandler:^(WKScriptMessage* message) {
[weakSelf didReceiveScriptMessage:message];
}
name:kFindElementResultHandlerName
webView:webView];
}
return self;
}
- (void)dealloc {
if (self.webState)
self.webState->RemoveObserver(_observer.get());
}
- (void)fetchDOMElementAtPoint:(CGPoint)point
completionHandler:
(void (^)(const web::ContextMenuParams&))handler {
if (!self.webState) {
return;
}
web::WebFrame* frame = GetMainFrame(self.webState);
if (!frame) {
// A WebFrame may not exist for certain types of content, like PDFs.
return;
}
DCHECK(handler);
std::string requestID = base::UnguessableToken::Create().ToString();
CRWHTMLElementFetchRequest* fetchRequest =
[[CRWHTMLElementFetchRequest alloc] initWithFoundElementHandler:handler];
_pendingElementFetchRequests[base::SysUTF8ToNSString(requestID)] =
fetchRequest;
CGSize webViewContentSize = self.webView.scrollView.contentSize;
std::vector<base::Value> args;
args.push_back(base::Value(requestID));
args.push_back(base::Value(point.x));
args.push_back(base::Value(point.y));
args.push_back(base::Value(webViewContentSize.width));
args.push_back(base::Value(webViewContentSize.height));
frame->CallJavaScriptFunction(std::string(kFindElementAtPointFunctionName),
args);
}
- (void)cancelFetches {
for (CRWHTMLElementFetchRequest* fetchRequest in _pendingElementFetchRequests
.allValues) {
[fetchRequest invalidate];
}
}
#pragma mark - Private
// Called when web controller receives a new message from the web page.
- (void)didReceiveScriptMessage:(WKScriptMessage*)message {
NSMutableDictionary* response =
[[NSMutableDictionary alloc] initWithDictionary:message.body];
NSString* requestID = response[web::kContextMenuElementRequestId];
CRWHTMLElementFetchRequest* fetchRequest =
_pendingElementFetchRequests[requestID];
if (!fetchRequest) {
// Do not process the message if a fetch request with a matching |requestID|
// was not found. This ensures that the response matches a request made by
// this instance.
return;
}
web::ContextMenuParams params =
web::ContextMenuParamsFromElementDictionary(response);
params.is_main_frame = message.frameInfo.mainFrame;
params.view = self.webView;
[_pendingElementFetchRequests removeObjectForKey:requestID];
[fetchRequest runHandlerWithResponse:params];
}
#pragma mark - CRWWebStateObserver
- (void)webStateDestroyed:(web::WebState*)webState {
if (self.webState)
self.webState->RemoveObserver(_observer.get());
self.webState = nullptr;
}
@end
// 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/web/web_state/ui/crw_context_menu_element_fetcher.h"
#import <WebKit/WebKit.h>
#include "base/macros.h"
#import "base/test/ios/wait_util.h"
#include "ios/testing/scoped_block_swizzler.h"
#import "ios/web/public/test/web_view_content_test_util.h"
#import "ios/web/test/web_test_with_web_controller.h"
#import "ios/web/web_state/context_menu_constants.h"
#import "ios/web/web_state/ui/crw_legacy_context_menu_controller.h"
#import "ios/web/web_state/ui/crw_web_controller.h"
#import "ios/web/web_state/web_state_impl.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "testing/gtest_mac.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
namespace {
// This is the timeout used while waiting for the JavaScript to complete. The
// general kWaitForJSCompletionTimeout isn't used because one of the test is
// supposed to not complete and so will wait for the whole duration of the
// timeout. This constant is smaller to speed tests up. This constant is used in
// both the "successful" JavaScript calls and the "failing" JavaScript calls. It
// ensures that in the context of this test, the JavaScript completes in the
// given timespan (and so it ensures that if the "failing" JavaScript tests
// pass, it is because the JavaScript isn't called and not because it didn't
// have time to complete).
const CGFloat kFetcherJSTimeout = 1.0;
} // namespace
namespace web {
class CRWContextMenuElementFetcherTest : public WebTestWithWebController {
public:
CRWContextMenuElementFetcherTest() {
// Disable the existing long press handling to avoid duplicating message
// handlers.
swizzler_ = std::make_unique<ScopedBlockSwizzler>(
[CRWLegacyContextMenuController class],
@selector(initWithWebView:webState:), ^id(id self) {
return nil;
});
}
void SetUp() override {
WebTestWithWebState::SetUp();
WKWebView* web_view = [web_controller() ensureWebViewCreated];
fetcher_ =
[[CRWContextMenuElementFetcher alloc] initWithWebView:web_view
webState:web_state()];
}
// Loads a page containing a link and waits until the link is present on the
// page, making sure that the HTML is correctly injected.
bool LoadHtmlPage() WARN_UNUSED_RESULT {
NSString* html =
@"<html><head>"
"<style>body { font-size:14em; }</style>"
"<meta name=\"viewport\" content=\"user-scalable=no, width=100\">"
"</head><body><p><a id=\"linkID\" "
"href=\"http://destination/\">link</a></p></body></html>";
LoadHtml(html);
bool element_present = test::WaitForWebViewContainingElement(
web_state(), [ElementSelector selectorWithElementID:"linkID"]);
if (element_present) {
// If the element is present, we still need a small delay to let all the
// scripts be injected in the page.
base::test::ios::SpinRunLoopWithMinDelay(
base::TimeDelta::FromSecondsD(0.5));
}
return element_present;
}
CRWContextMenuElementFetcher* GetFetcher() { return fetcher_; }
private:
std::unique_ptr<ScopedBlockSwizzler> swizzler_;
CRWContextMenuElementFetcher* fetcher_;
};
// Tests that the fetcher is triggering a callback for one element.
TEST_F(CRWContextMenuElementFetcherTest, FetchOneElement) {
EXPECT_TRUE(LoadHtmlPage());
CRWContextMenuElementFetcher* fetcher = GetFetcher();
__block bool callback_called = false;
[fetcher fetchDOMElementAtPoint:CGPointMake(10, 10)
completionHandler:^(const web::ContextMenuParams&) {
callback_called = true;
}];
EXPECT_TRUE(base::test::ios::WaitUntilConditionOrTimeout(kFetcherJSTimeout, ^{
return callback_called;
}));
}
// Tests that cancelled fetches don't trigger callback.
TEST_F(CRWContextMenuElementFetcherTest, CancelFetch) {
EXPECT_TRUE(LoadHtmlPage());
CRWContextMenuElementFetcher* fetcher = GetFetcher();
__block bool callback_called = false;
[fetcher fetchDOMElementAtPoint:CGPointMake(10, 10)
completionHandler:^(const web::ContextMenuParams&) {
callback_called = true;
}];
[fetcher cancelFetches];
// The callback should never be called.
EXPECT_FALSE(
base::test::ios::WaitUntilConditionOrTimeout(kFetcherJSTimeout, ^{
return callback_called;
}));
}
} // namespace web
......@@ -2,8 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef IOS_WEB_WEB_STATE_UI_HTML_ELEMENT_FETCH_REQUEST_H_
#define IOS_WEB_WEB_STATE_UI_HTML_ELEMENT_FETCH_REQUEST_H_
#ifndef IOS_WEB_WEB_STATE_UI_CRW_HTML_ELEMENT_FETCH_REQUEST_H_
#define IOS_WEB_WEB_STATE_UI_CRW_HTML_ELEMENT_FETCH_REQUEST_H_
#import <Foundation/Foundation.h>
......@@ -11,8 +11,12 @@ namespace base {
class TimeTicks;
} // namespace base
namespace web {
struct ContextMenuParams;
}
// Tracks request details for fetching attributes of an element.
@interface HTMLElementFetchRequest : NSObject
@interface CRWHTMLElementFetchRequest : NSObject
// The time this object was created.
@property(nonatomic, readonly) base::TimeTicks creationTime;
......@@ -21,16 +25,17 @@ class TimeTicks;
// Designated initializer to create a new object with the given completion
// handler |foundElementHandler|.
- (instancetype)initWithFoundElementHandler:
(void (^)(NSDictionary*))foundElementHandler NS_DESIGNATED_INITIALIZER;
(void (^)(const web::ContextMenuParams&))foundElementHandler
NS_DESIGNATED_INITIALIZER;
// Calls the |foundElementHandler| from the receiver's initializer with
// |response| as the parameter. This method has no effect if |invalidate| has
// been called.
- (void)runHandlerWithResponse:(NSDictionary*)response;
- (void)runHandlerWithResponse:(const web::ContextMenuParams&)response;
// Removes the stored |foundElementHandler| from the receiver's initializer.
// |runHandlerWithResponse:| will have no effect if called after |invalidate|.
- (void)invalidate;
@end
#endif // IOS_WEB_WEB_STATE_UI_HTML_ELEMENT_FETCH_REQUEST_H_
#endif // IOS_WEB_WEB_STATE_UI_CRW_HTML_ELEMENT_FETCH_REQUEST_H_
......@@ -2,26 +2,28 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/web/web_state/ui/html_element_fetch_request.h"
#import "ios/web/web_state/ui/crw_html_element_fetch_request.h"
#include "base/time/time.h"
#import "ios/web/public/ui/context_menu_params.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
@interface HTMLElementFetchRequest ()
@interface CRWHTMLElementFetchRequest ()
// Completion handler to call with found DOM element.
@property(nonatomic, copy) void (^foundElementHandler)(NSDictionary*);
@property(nonatomic, copy) void (^foundElementHandler)
(const web::ContextMenuParams&);
@end
@implementation HTMLElementFetchRequest
@implementation CRWHTMLElementFetchRequest
@synthesize creationTime = _creationTime;
@synthesize foundElementHandler = _foundElementHandler;
- (instancetype)initWithFoundElementHandler:
(void (^)(NSDictionary*))foundElementHandler {
(void (^)(const web::ContextMenuParams&))foundElementHandler {
self = [super init];
if (self) {
_creationTime = base::TimeTicks::Now();
......@@ -30,7 +32,7 @@
return self;
}
- (void)runHandlerWithResponse:(NSDictionary*)response {
- (void)runHandlerWithResponse:(const web::ContextMenuParams&)response {
if (_foundElementHandler) {
_foundElementHandler(response);
}
......
......@@ -2,9 +2,10 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/web/web_state/ui/html_element_fetch_request.h"
#import "ios/web/web_state/ui/crw_html_element_fetch_request.h"
#include "base/time/time.h"
#import "ios/web/public/ui/context_menu_params.h"
#import "ios/web/web_state/context_menu_constants.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "testing/gtest_mac.h"
......@@ -16,12 +17,13 @@
namespace web {
using HtmlElementFetchRequestTest = PlatformTest;
using CRWHTMLElementFetchRequestTest = PlatformTest;
// Tests that |creationTime| is set at HtmlElementFetchRequest object creation.
TEST_F(HtmlElementFetchRequestTest, CreationTime) {
HTMLElementFetchRequest* request =
[[HTMLElementFetchRequest alloc] initWithFoundElementHandler:nil];
// Tests that |creationTime| is set at CRWHTMLElementFetchRequest object
// creation.
TEST_F(CRWHTMLElementFetchRequestTest, CreationTime) {
CRWHTMLElementFetchRequest* request =
[[CRWHTMLElementFetchRequest alloc] initWithFoundElementHandler:nil];
base::TimeDelta delta = base::TimeTicks::Now() - request.creationTime;
// Validate that |request.creationTime| is "now", but only use second
// precision to avoid performance induced test flake.
......@@ -30,32 +32,35 @@ TEST_F(HtmlElementFetchRequestTest, CreationTime) {
// Tests that |runHandlerWithResponse:| runs the handler from the object's
// initializer with the expected |response|.
TEST_F(HtmlElementFetchRequestTest, RunHandler) {
TEST_F(CRWHTMLElementFetchRequestTest, RunHandler) {
__block bool handler_called = false;
__block NSDictionary* received_response = nil;
void (^handler)(NSDictionary*) = ^(NSDictionary* response) {
handler_called = true;
received_response = response;
};
HTMLElementFetchRequest* request =
[[HTMLElementFetchRequest alloc] initWithFoundElementHandler:handler];
NSDictionary* response = @{kContextMenuElementInnerText : @"text"};
[request runHandlerWithResponse:response];
__block web::ContextMenuParams received_params;
void (^handler)(const web::ContextMenuParams&) =
^(const web::ContextMenuParams& params) {
handler_called = true;
received_params = params;
};
CRWHTMLElementFetchRequest* request =
[[CRWHTMLElementFetchRequest alloc] initWithFoundElementHandler:handler];
web::ContextMenuParams params = web::ContextMenuParams();
params.link_text = @"text";
[request runHandlerWithResponse:params];
EXPECT_TRUE(handler_called);
EXPECT_NSEQ(response, received_response);
EXPECT_NSEQ(params.link_text, received_params.link_text);
}
// Tests that |runHandlerWithResponse:| does not run the handler from the
// object's initializer if |invalidate| has been called.
TEST_F(HtmlElementFetchRequestTest, Invalidate) {
TEST_F(CRWHTMLElementFetchRequestTest, Invalidate) {
__block bool handler_called = false;
void (^handler)(NSDictionary*) = ^(NSDictionary* response) {
handler_called = true;
};
HTMLElementFetchRequest* request =
[[HTMLElementFetchRequest alloc] initWithFoundElementHandler:handler];
void (^handler)(const web::ContextMenuParams&) =
^(const web::ContextMenuParams& params) {
handler_called = true;
};
CRWHTMLElementFetchRequest* request =
[[CRWHTMLElementFetchRequest alloc] initWithFoundElementHandler:handler];
[request invalidate];
[request runHandlerWithResponse:nil];
[request runHandlerWithResponse:web::ContextMenuParams()];
EXPECT_FALSE(handler_called);
}
......
......@@ -11,21 +11,14 @@
#include "base/ios/ios_util.h"
#include "base/mac/foundation_util.h"
#include "base/metrics/histogram_macros.h"
#include "base/strings/sys_string_conversions.h"
#include "base/unguessable_token.h"
#include "base/values.h"
#import "ios/web/js_messaging/crw_wk_script_message_router.h"
#import "ios/web/public/js_messaging/web_frame.h"
#import "ios/web/public/js_messaging/web_frame_util.h"
#import "ios/web/public/navigation/navigation_context.h"
#import "ios/web/public/ui/context_menu_params.h"
#include "ios/web/public/web_client.h"
#import "ios/web/public/web_state.h"
#import "ios/web/public/web_state_observer_bridge.h"
#import "ios/web/web_state/context_menu_constants.h"
#import "ios/web/web_state/context_menu_params_utils.h"
#import "ios/web/web_state/ui/html_element_fetch_request.h"
#import "ios/web/web_state/ui/wk_web_view_configuration_provider.h"
#import "ios/web/web_state/ui/crw_context_menu_element_fetcher.h"
#import "ios/web/web_state/web_state_impl.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
......@@ -54,13 +47,6 @@ void CancelTouches(UIGestureRecognizer* gesture_recognizer) {
}
}
// Javascript function name to obtain element details at a point.
const char kFindElementAtPointFunctionName[] = "findElementAtPoint";
// JavaScript message handler name installed in WKWebView for found element
// response.
NSString* const kFindElementResultHandlerName = @"FindElementResultHandler";
// Enum used to record element details fetched for the context menu.
enum class ContextMenuElementFrame {
// Recorded when the element was found in the main frame.
......@@ -89,17 +75,6 @@ enum class DelayedElementDetailsState {
kMaxValue = Cancel
};
// Struct to track the details of the element at |location| in |webView|.
struct ContextMenuInfo {
// The location of the long press.
CGPoint location;
// True if the element is in the page's main frame, false if in an iframe.
BOOL is_main_frame;
// DOM element information. May contain the keys defined in
// ios/web/web_state/context_menu_constants.h. All values are strings.
NSDictionary* dom_element;
};
// Returns an array of gesture recognizers with |fragment| in it's description
// and attached to a subview of |webView|.
NSArray<UIGestureRecognizer*>* GestureRecognizersWithDescriptionFragment(
......@@ -182,6 +157,8 @@ void OverrideGestureRecognizers(UIGestureRecognizer* contextMenuRecognizer,
// WebState associated with this controller.
@property(nonatomic, assign) web::WebStateImpl* webState;
@property(nonatomic, strong) CRWContextMenuElementFetcher* elementFetcher;
// Called when the |_contextMenuRecognizer| finishes recognizing a long press.
- (void)longPressDetectedByGestureRecognizer:
(UIGestureRecognizer*)gestureRecognizer;
......@@ -190,21 +167,12 @@ void OverrideGestureRecognizers(UIGestureRecognizer* contextMenuRecognizer,
// Called when the |_contextMenuRecognizer| changes.
- (void)longPressGestureRecognizerChanged;
// Show the context menu or allow the system default behavior based on the DOM
// element details in |_contextMenuInfoForLastTouch.dom_element|.
// element details in |contextMenuParams|.
- (void)processReceivedDOMElement;
// Called when the context menu must be shown.
- (void)showContextMenu;
// Cancels all touch events in the web view (long presses, tapping, scrolling).
- (void)cancelAllTouches;
// Asynchronously fetches information about DOM element for the given point (in
// UIView coordinates). |handler| can not be nil. See
// |_contextMenuInfoForLastTouch.dom_element| for element format description.
- (void)fetchDOMElementAtPoint:(CGPoint)point
completionHandler:(void (^)(NSDictionary*))handler;
// Sets the value of |_contextMenuInfoForLastTouch.dom_element|.
- (void)setDOMElementForLastTouch:(NSDictionary*)element;
// Called to process a message received from JavaScript.
- (void)didReceiveScriptMessage:(WKScriptMessage*)message;
// Cancels the display of the context menu and clears associated element fetch
// request state.
- (void)cancelContextMenuDisplay;
......@@ -214,10 +182,8 @@ void OverrideGestureRecognizers(UIGestureRecognizer* contextMenuRecognizer,
std::unique_ptr<web::WebStateObserverBridge> _observer;
// Long press recognizer that allows showing context menus.
UILongPressGestureRecognizer* _contextMenuRecognizer;
// DOM element information for the point where the user made the last touch.
// Precalculation is necessary because retreiving DOM element relies on async
// API so element info can not be built on demand.
ContextMenuInfo _contextMenuInfoForLastTouch;
// Location of the last touch on the screen.
CGPoint _lastTouchLocation;
// Whether or not the system cotext menu should be displayed. If not, custom
// context menu should be displayed.
BOOL _systemContextMenuEnabled;
......@@ -229,10 +195,8 @@ void OverrideGestureRecognizers(UIGestureRecognizer* contextMenuRecognizer,
// |_contextMenuRecognizer| finished, but couldn't yet show the context menu
// becuase the DOM element details were not yet available.
BOOL _contextMenuNeedsDisplay;
// Details for currently in progress element fetches. The objects are
// instances of HTMLElementFetchRequest and are keyed by a unique requestId
// string.
NSMutableDictionary* _pendingElementFetchRequests;
// Parameters for the context menu, populated by the element fetcher.
base::Optional<web::ContextMenuParams> _contextMenuParams;
}
@synthesize webView = _webView;
......@@ -243,7 +207,10 @@ void OverrideGestureRecognizers(UIGestureRecognizer* contextMenuRecognizer,
self = [super init];
if (self) {
_webView = webView;
_pendingElementFetchRequests = [[NSMutableDictionary alloc] init];
_elementFetcher =
[[CRWContextMenuElementFetcher alloc] initWithWebView:webView
webState:webState];
_webState = webState;
_observer = std::make_unique<web::WebStateObserverBridge>(self);
......@@ -273,20 +240,6 @@ void OverrideGestureRecognizers(UIGestureRecognizer* contextMenuRecognizer,
[_webView addGestureRecognizer:_contextMenuRecognizer];
OverrideGestureRecognizers(_contextMenuRecognizer, _webView);
// Listen for fetched element response.
web::WKWebViewConfigurationProvider& configurationProvider =
web::WKWebViewConfigurationProvider::FromBrowserState(
webState->GetBrowserState());
CRWWKScriptMessageRouter* messageRouter =
configurationProvider.GetScriptMessageRouter();
__weak CRWLegacyContextMenuController* weakSelf = self;
[messageRouter
setScriptMessageHandler:^(WKScriptMessage* message) {
[weakSelf didReceiveScriptMessage:message];
}
name:kFindElementResultHandlerName
webView:webView];
}
return self;
}
......@@ -329,7 +282,7 @@ void OverrideGestureRecognizers(UIGestureRecognizer* contextMenuRecognizer,
}
- (void)longPressGestureRecognizerBegan {
if (_contextMenuInfoForLastTouch.dom_element) {
if (_contextMenuParams.has_value()) {
[self processReceivedDOMElement];
} else {
// Shows the context menu once the DOM element information is set.
......@@ -340,7 +293,7 @@ void OverrideGestureRecognizers(UIGestureRecognizer* contextMenuRecognizer,
- (void)longPressGestureRecognizerChanged {
if (!_contextMenuNeedsDisplay ||
CGPointEqualToPoint(_contextMenuInfoForLastTouch.location, CGPointZero)) {
CGPointEqualToPoint(_lastTouchLocation, CGPointZero)) {
return;
}
......@@ -350,10 +303,8 @@ void OverrideGestureRecognizers(UIGestureRecognizer* contextMenuRecognizer,
// |_contextMenuNeedsDisplay| has already been set to True.
CGPoint currentTouchLocation =
[_contextMenuRecognizer locationInView:_webView];
float deltaX = std::abs(_contextMenuInfoForLastTouch.location.x -
currentTouchLocation.x);
float deltaY = std::abs(_contextMenuInfoForLastTouch.location.y -
currentTouchLocation.y);
float deltaX = std::abs(_lastTouchLocation.x - currentTouchLocation.x);
float deltaY = std::abs(_lastTouchLocation.y - currentTouchLocation.y);
if (deltaX > kLongPressMoveDeltaPixels ||
deltaY > kLongPressMoveDeltaPixels) {
[self cancelContextMenuDisplay];
......@@ -361,8 +312,9 @@ void OverrideGestureRecognizers(UIGestureRecognizer* contextMenuRecognizer,
}
- (void)processReceivedDOMElement {
BOOL canShowContextMenu = web::CanShowContextMenuForElementDictionary(
_contextMenuInfoForLastTouch.dom_element);
BOOL canShowContextMenu =
_contextMenuParams.has_value() &&
web::CanShowContextMenuForParams(_contextMenuParams.value());
if (!canShowContextMenu) {
// There is no link or image under user's gesture. Do not cancel all touches
// to allow system text selection UI.
......@@ -374,30 +326,25 @@ void OverrideGestureRecognizers(UIGestureRecognizer* contextMenuRecognizer,
// intentionally suppress system context menu UI.
[self cancelAllTouches];
_contextMenuInfoForLastTouch.location =
[_contextMenuRecognizer locationInView:_webView];
_lastTouchLocation = [_contextMenuRecognizer locationInView:_webView];
[self showContextMenu];
}
- (void)showContextMenu {
if (!self.webState) {
if (!self.webState || !_contextMenuParams.has_value()) {
return;
}
// Log if the element is in the main frame or a child frame.
UMA_HISTOGRAM_ENUMERATION("ContextMenu.DOMElementFrame",
(_contextMenuInfoForLastTouch.is_main_frame
(_contextMenuParams.value().is_main_frame
? ContextMenuElementFrame::MainFrame
: ContextMenuElementFrame::Iframe),
ContextMenuElementFrame::Count);
web::ContextMenuParams params = web::ContextMenuParamsFromElementDictionary(
_contextMenuInfoForLastTouch.dom_element);
params.view = _webView;
params.location = _contextMenuInfoForLastTouch.location;
params.is_main_frame = _contextMenuInfoForLastTouch.is_main_frame;
_contextMenuParams.value().location = _lastTouchLocation;
self.webState->HandleContextMenu(params);
self.webState->HandleContextMenu(_contextMenuParams.value());
}
- (void)cancelAllTouches {
......@@ -415,8 +362,9 @@ void OverrideGestureRecognizers(UIGestureRecognizer* contextMenuRecognizer,
}
}
- (void)setDOMElementForLastTouch:(NSDictionary*)element {
_contextMenuInfoForLastTouch.dom_element = [element copy];
// Sets the value of |params|.
- (void)setParamsForLastTouch:(const web::ContextMenuParams&)params {
_contextMenuParams = params;
if (_contextMenuNeedsDisplay) {
_contextMenuNeedsDisplay = NO;
UMA_HISTOGRAM_ENUMERATION(kContextMenuDelayedElementDetailsHistogram,
......@@ -425,33 +373,14 @@ void OverrideGestureRecognizers(UIGestureRecognizer* contextMenuRecognizer,
}
}
- (void)didReceiveScriptMessage:(WKScriptMessage*)message {
NSMutableDictionary* response =
[[NSMutableDictionary alloc] initWithDictionary:message.body];
_contextMenuInfoForLastTouch.is_main_frame = message.frameInfo.mainFrame;
NSString* requestID = response[web::kContextMenuElementRequestId];
HTMLElementFetchRequest* fetchRequest =
_pendingElementFetchRequests[requestID];
// Do not process the message if a fetch request with a matching |requestID|
// was not found. This ensures that the response matches a request made by
// this CRWLegacyContextMenuController instance.
if (fetchRequest) {
[_pendingElementFetchRequests removeObjectForKey:requestID];
[fetchRequest runHandlerWithResponse:response];
}
}
- (void)cancelContextMenuDisplay {
if (_contextMenuNeedsDisplay) {
UMA_HISTOGRAM_ENUMERATION(kContextMenuDelayedElementDetailsHistogram,
DelayedElementDetailsState::Cancel);
}
_contextMenuNeedsDisplay = NO;
_contextMenuInfoForLastTouch.location = CGPointZero;
for (HTMLElementFetchRequest* fetchRequest in _pendingElementFetchRequests
.allValues) {
[fetchRequest invalidate];
}
_lastTouchLocation = CGPointZero;
[self.elementFetcher cancelFetches];
}
#pragma mark -
......@@ -481,14 +410,15 @@ void OverrideGestureRecognizers(UIGestureRecognizer* contextMenuRecognizer,
// touch. If there a link, the web controller will reject system's context
// menu and show another one. If for some reason context menu info is not
// fetched - system context menu will be shown.
[self setDOMElementForLastTouch:nil];
_contextMenuParams.reset();
[self cancelContextMenuDisplay];
__weak CRWLegacyContextMenuController* weakSelf = self;
[self fetchDOMElementAtPoint:[touch locationInView:_webView]
completionHandler:^(NSDictionary* element) {
[weakSelf setDOMElementForLastTouch:element];
}];
[self.elementFetcher
fetchDOMElementAtPoint:[touch locationInView:_webView.scrollView]
completionHandler:^(const web::ContextMenuParams& params) {
[weakSelf setParamsForLastTouch:params];
}];
return YES;
}
......@@ -512,39 +442,6 @@ void OverrideGestureRecognizers(UIGestureRecognizer* contextMenuRecognizer,
return YES;
}
#pragma mark -
#pragma mark Web Page Features
- (void)fetchDOMElementAtPoint:(CGPoint)point
completionHandler:(void (^)(NSDictionary*))handler {
if (!self.webState) {
return;
}
web::WebFrame* frame = GetMainFrame(self.webState);
if (!frame) {
// A WebFrame may not exist for certain types of content, like PDFs.
return;
}
DCHECK(handler);
std::string requestID = base::UnguessableToken::Create().ToString();
HTMLElementFetchRequest* fetchRequest =
[[HTMLElementFetchRequest alloc] initWithFoundElementHandler:handler];
_pendingElementFetchRequests[base::SysUTF8ToNSString(requestID)] =
fetchRequest;
CGSize webViewContentSize = self.webScrollView.contentSize;
std::vector<base::Value> args;
args.push_back(base::Value(requestID));
args.push_back(base::Value(point.x + self.scrollPosition.x));
args.push_back(base::Value(point.y + self.scrollPosition.y));
args.push_back(base::Value(webViewContentSize.width));
args.push_back(base::Value(webViewContentSize.height));
frame->CallJavaScriptFunction(std::string(kFindElementAtPointFunctionName),
args);
}
#pragma mark - CRWWebStateObserver
- (void)webStateDestroyed:(web::WebState*)webState {
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment