1 // Copyright 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "components/plugins/renderer/loadable_plugin_placeholder.h"
8 #include "base/bind_helpers.h"
9 #include "base/command_line.h"
10 #include "base/json/string_escape.h"
11 #include "base/strings/string_piece.h"
12 #include "base/strings/string_util.h"
13 #include "base/strings/utf_string_conversions.h"
14 #include "base/thread_task_runner_handle.h"
15 #include "base/values.h"
16 #include "content/public/child/v8_value_converter.h"
17 #include "content/public/common/content_switches.h"
18 #include "content/public/renderer/render_frame.h"
19 #include "content/public/renderer/render_thread.h"
20 #include "gin/handle.h"
21 #include "gin/object_template_builder.h"
22 #include "third_party/WebKit/public/web/WebDOMMessageEvent.h"
23 #include "third_party/WebKit/public/web/WebDocument.h"
24 #include "third_party/WebKit/public/web/WebElement.h"
25 #include "third_party/WebKit/public/web/WebInputEvent.h"
26 #include "third_party/WebKit/public/web/WebKit.h"
27 #include "third_party/WebKit/public/web/WebLocalFrame.h"
28 #include "third_party/WebKit/public/web/WebPluginContainer.h"
29 #include "third_party/WebKit/public/web/WebScriptSource.h"
30 #include "third_party/WebKit/public/web/WebSerializedScriptValue.h"
31 #include "third_party/WebKit/public/web/WebView.h"
32 #include "third_party/re2/re2/re2.h"
34 using base::UserMetricsAction
;
35 using blink::WebElement
;
36 using blink::WebLocalFrame
;
37 using blink::WebMouseEvent
;
39 using blink::WebPlugin
;
40 using blink::WebPluginContainer
;
41 using blink::WebPluginParams
;
42 using blink::WebScriptSource
;
43 using blink::WebURLRequest
;
44 using content::PluginInstanceThrottler
;
45 using content::RenderThread
;
49 #if defined(ENABLE_PLUGINS)
50 // TODO(tommycli): After an unthrottling size update, re-check the size after
51 // this delay, as Blink can report incorrect sizes to plugins while the
52 // compositing state is dirty. Chosen because it seems to work.
53 const int kSizeChangeRecheckDelayMilliseconds
= 100;
55 void LoadablePluginPlaceholder::BlockForPowerSaverPoster() {
56 DCHECK(!is_blocked_for_power_saver_poster_
);
57 is_blocked_for_power_saver_poster_
= true;
59 render_frame()->RegisterPeripheralPlugin(
60 GURL(GetPluginParams().url
).GetOrigin(),
61 base::Bind(&LoadablePluginPlaceholder::MarkPluginEssential
,
62 weak_factory_
.GetWeakPtr(),
63 PluginInstanceThrottler::UNTHROTTLE_METHOD_BY_WHITELIST
));
66 void LoadablePluginPlaceholder::SetPremadePlugin(
67 content::PluginInstanceThrottler
* throttler
) {
69 DCHECK(!premade_throttler_
);
70 premade_throttler_
= throttler
;
74 LoadablePluginPlaceholder::LoadablePluginPlaceholder(
75 content::RenderFrame
* render_frame
,
77 const WebPluginParams
& params
,
78 const std::string
& html_data
,
79 GURL placeholderDataUrl
)
80 : PluginPlaceholder(render_frame
,
85 is_blocked_for_background_tab_(false),
86 is_blocked_for_prerendering_(false),
87 is_blocked_for_power_saver_poster_(false),
88 power_saver_enabled_(false),
89 premade_throttler_(nullptr),
90 allow_loading_(false),
92 finished_loading_(false),
93 in_size_recheck_(false),
97 LoadablePluginPlaceholder::~LoadablePluginPlaceholder() {
100 #if defined(ENABLE_PLUGINS)
101 void LoadablePluginPlaceholder::MarkPluginEssential(
102 PluginInstanceThrottler::PowerSaverUnthrottleMethod method
) {
103 if (!power_saver_enabled_
)
106 power_saver_enabled_
= false;
108 if (premade_throttler_
)
109 premade_throttler_
->MarkPluginEssential(method
);
111 PluginInstanceThrottler::RecordUnthrottleMethodMetric(method
);
113 if (is_blocked_for_power_saver_poster_
) {
114 is_blocked_for_power_saver_poster_
= false;
115 if (!LoadingBlocked())
121 void LoadablePluginPlaceholder::BindWebFrame(blink::WebFrame
* frame
) {
122 v8::Isolate
* isolate
= blink::mainThreadIsolate();
123 v8::HandleScope
handle_scope(isolate
);
124 v8::Local
<v8::Context
> context
= frame
->mainWorldScriptContext();
125 DCHECK(!context
.IsEmpty());
127 v8::Context::Scope
context_scope(context
);
128 v8::Local
<v8::Object
> global
= context
->Global();
129 global
->Set(gin::StringToV8(isolate
, "plugin"),
130 gin::CreateHandle(isolate
, this).ToV8());
133 gin::ObjectTemplateBuilder
LoadablePluginPlaceholder::GetObjectTemplateBuilder(
134 v8::Isolate
* isolate
) {
135 return gin::Wrappable
<PluginPlaceholder
>::GetObjectTemplateBuilder(isolate
)
136 .SetMethod("load", &LoadablePluginPlaceholder::LoadCallback
)
137 .SetMethod("hide", &LoadablePluginPlaceholder::HideCallback
)
138 .SetMethod("didFinishLoading",
139 &LoadablePluginPlaceholder::DidFinishLoadingCallback
);
142 void LoadablePluginPlaceholder::ReplacePlugin(WebPlugin
* new_plugin
) {
146 WebPluginContainer
* container
= plugin()->container();
147 // Set the new plugin on the container before initializing it.
148 container
->setPlugin(new_plugin
);
149 // Save the element in case the plugin is removed from the page during
151 WebElement element
= container
->element();
152 bool plugin_needs_initialization
=
153 !premade_throttler_
|| new_plugin
!= premade_throttler_
->GetWebPlugin();
154 if (plugin_needs_initialization
&& !new_plugin
->initialize(container
)) {
155 // We couldn't initialize the new plugin. Restore the old one and abort.
156 container
->setPlugin(plugin());
160 // The plugin has been removed from the page. Destroy the old plugin. We
161 // will be destroyed as soon as V8 garbage collects us.
162 if (!element
.pluginContainer()) {
167 // During initialization, the new plugin might have replaced itself in turn
168 // with another plugin. Make sure not to use the passed in |new_plugin| after
170 new_plugin
= container
->plugin();
172 plugin()->RestoreTitleText();
173 container
->invalidate();
174 container
->reportGeometry();
175 plugin()->ReplayReceivedData(new_plugin
);
179 void LoadablePluginPlaceholder::HidePlugin() {
183 WebPluginContainer
* container
= plugin()->container();
184 WebElement element
= container
->element();
185 element
.setAttribute("style", "display: none;");
186 // If we have a width and height, search for a parent (often <div>) with the
187 // same dimensions. If we find such a parent, hide that as well.
188 // This makes much more uncovered page content usable (including clickable)
189 // as opposed to merely visible.
190 // TODO(cevans) -- it's a foul heuristic but we're going to tolerate it for
191 // now for these reasons:
192 // 1) Makes the user experience better.
193 // 2) Foulness is encapsulated within this single function.
194 // 3) Confidence in no fasle positives.
195 // 4) Seems to have a good / low false negative rate at this time.
196 if (element
.hasAttribute("width") && element
.hasAttribute("height")) {
197 std::string
width_str("width:[\\s]*");
198 width_str
+= element
.getAttribute("width").utf8().data();
199 if (EndsWith(width_str
, "px", false)) {
200 width_str
= width_str
.substr(0, width_str
.length() - 2);
202 base::TrimWhitespace(width_str
, base::TRIM_TRAILING
, &width_str
);
203 width_str
+= "[\\s]*px";
204 std::string
height_str("height:[\\s]*");
205 height_str
+= element
.getAttribute("height").utf8().data();
206 if (EndsWith(height_str
, "px", false)) {
207 height_str
= height_str
.substr(0, height_str
.length() - 2);
209 base::TrimWhitespace(height_str
, base::TRIM_TRAILING
, &height_str
);
210 height_str
+= "[\\s]*px";
211 WebNode parent
= element
;
212 while (!parent
.parentNode().isNull()) {
213 parent
= parent
.parentNode();
214 if (!parent
.isElementNode())
216 element
= parent
.toConst
<WebElement
>();
217 if (element
.hasAttribute("style")) {
218 std::string style_str
= element
.getAttribute("style").utf8();
219 if (RE2::PartialMatch(style_str
, width_str
) &&
220 RE2::PartialMatch(style_str
, height_str
))
221 element
.setAttribute("style", "display: none;");
227 void LoadablePluginPlaceholder::SetMessage(const base::string16
& message
) {
229 if (finished_loading_
)
233 void LoadablePluginPlaceholder::UpdateMessage() {
237 "window.setMessage(" + base::GetQuotedJSONString(message_
) + ")";
238 plugin()->web_view()->mainFrame()->executeScript(
239 WebScriptSource(base::UTF8ToUTF16(script
)));
242 void LoadablePluginPlaceholder::PluginDestroyed() {
243 #if defined(ENABLE_PLUGINS)
244 if (power_saver_enabled_
) {
245 if (premade_throttler_
) {
246 // Since the premade plugin has been detached from the container, it will
247 // not be automatically destroyed along with the page.
248 premade_throttler_
->GetWebPlugin()->destroy();
249 premade_throttler_
= nullptr;
250 } else if (is_blocked_for_power_saver_poster_
) {
251 // Record the NEVER unthrottle count only if there is no throttler.
252 PluginInstanceThrottler::RecordUnthrottleMethodMetric(
253 PluginInstanceThrottler::UNTHROTTLE_METHOD_NEVER
);
256 // Prevent processing subsequent calls to MarkPluginEssential.
257 power_saver_enabled_
= false;
261 PluginPlaceholder::PluginDestroyed();
264 v8::Local
<v8::Object
> LoadablePluginPlaceholder::GetV8ScriptableObject(
265 v8::Isolate
* isolate
) const {
266 #if defined(ENABLE_PLUGINS)
267 // Pass through JavaScript access to the underlying throttled plugin.
268 if (premade_throttler_
&& premade_throttler_
->GetWebPlugin()) {
269 return premade_throttler_
->GetWebPlugin()->v8ScriptableObject(isolate
);
272 return v8::Local
<v8::Object
>();
275 #if defined(ENABLE_PLUGINS)
276 void LoadablePluginPlaceholder::OnUnobscuredSizeUpdate(
277 const gfx::Size
& unobscured_size
) {
279 content::RenderThread::Get()->GetTaskRunner()->BelongsToCurrentThread());
280 if (!power_saver_enabled_
|| !premade_throttler_
|| !finished_loading_
)
283 unobscured_size_
= unobscured_size
;
285 // During a size recheck, we will get another notification into this method.
286 // Use this flag to early exit to prevent reentrancy issues.
287 if (in_size_recheck_
)
290 if (PluginInstanceThrottler::IsLargeContent(unobscured_size
.width(),
291 unobscured_size
.height())) {
292 if (!size_update_timer_
.IsRunning()) {
293 // TODO(tommycli): We have to post a delayed task to recheck the size, as
294 // Blink can report wrong sizes for partially obscured plugins while the
295 // compositing state is dirty. https://crbug.com/343769
296 size_update_timer_
.Start(
297 FROM_HERE
, base::TimeDelta::FromMilliseconds(
298 kSizeChangeRecheckDelayMilliseconds
),
299 base::Bind(&LoadablePluginPlaceholder::RecheckSizeAndMaybeUnthrottle
,
300 weak_factory_
.GetWeakPtr()));
303 // Cancel any pending unthrottle due to resize calls.
304 size_update_timer_
.Stop();
307 #endif // defined(ENABLE_PLUGINS)
309 void LoadablePluginPlaceholder::WasShown() {
310 if (is_blocked_for_background_tab_
) {
311 is_blocked_for_background_tab_
= false;
312 if (!LoadingBlocked())
317 void LoadablePluginPlaceholder::OnLoadBlockedPlugins(
318 const std::string
& identifier
) {
319 if (!identifier
.empty() && identifier
!= identifier_
)
322 RenderThread::Get()->RecordAction(UserMetricsAction("Plugin_Load_UI"));
326 void LoadablePluginPlaceholder::OnSetIsPrerendering(bool is_prerendering
) {
327 // Prerendering can only be enabled prior to a RenderView's first navigation,
328 // so no BlockedPlugin should see the notification that enables prerendering.
329 DCHECK(!is_prerendering
);
330 if (is_blocked_for_prerendering_
) {
331 is_blocked_for_prerendering_
= false;
332 if (!LoadingBlocked())
337 void LoadablePluginPlaceholder::LoadPlugin() {
338 // This is not strictly necessary but is an important defense in case the
339 // event propagation changes between "close" vs. "click-to-play".
344 if (!allow_loading_
) {
349 if (premade_throttler_
) {
350 premade_throttler_
->SetHiddenForPlaceholder(false /* hidden */);
351 ReplacePlugin(premade_throttler_
->GetWebPlugin());
352 premade_throttler_
= nullptr;
354 ReplacePlugin(CreatePlugin());
358 void LoadablePluginPlaceholder::LoadCallback() {
359 RenderThread::Get()->RecordAction(UserMetricsAction("Plugin_Load_Click"));
360 #if defined(ENABLE_PLUGINS)
361 // If the user specifically clicks on the plugin content's placeholder,
362 // disable power saver throttling for this instance.
363 MarkPluginEssential(PluginInstanceThrottler::UNTHROTTLE_METHOD_BY_CLICK
);
368 void LoadablePluginPlaceholder::HideCallback() {
369 RenderThread::Get()->RecordAction(UserMetricsAction("Plugin_Hide_Click"));
373 void LoadablePluginPlaceholder::DidFinishLoadingCallback() {
374 finished_loading_
= true;
375 if (message_
.length() > 0)
378 // Wait for the placeholder to finish loading to hide the premade plugin.
379 // This is necessary to prevent a flicker.
380 if (premade_throttler_
&& power_saver_enabled_
)
381 premade_throttler_
->SetHiddenForPlaceholder(true /* hidden */);
383 // Set an attribute and post an event, so browser tests can wait for the
384 // placeholder to be ready to receive simulated user input.
385 if (base::CommandLine::ForCurrentProcess()->HasSwitch(
386 switches::kEnablePluginPlaceholderTesting
)) {
387 WebElement element
= plugin()->container()->element();
388 element
.setAttribute("placeholderLoaded", "true");
390 scoped_ptr
<content::V8ValueConverter
> converter(
391 content::V8ValueConverter::create());
392 base::StringValue
value("placeholderLoaded");
393 blink::WebSerializedScriptValue message_data
=
394 blink::WebSerializedScriptValue::serialize(converter
->ToV8Value(
395 &value
, element
.document().frame()->mainWorldScriptContext()));
397 blink::WebDOMEvent event
= element
.document().createEvent("MessageEvent");
398 blink::WebDOMMessageEvent msg_event
= event
.to
<blink::WebDOMMessageEvent
>();
399 msg_event
.initMessageEvent("message", // type
402 message_data
, // data
406 element
.dispatchEvent(msg_event
);
410 void LoadablePluginPlaceholder::SetPluginInfo(
411 const content::WebPluginInfo
& plugin_info
) {
412 plugin_info_
= plugin_info
;
415 const content::WebPluginInfo
& LoadablePluginPlaceholder::GetPluginInfo() const {
419 void LoadablePluginPlaceholder::SetIdentifier(const std::string
& identifier
) {
420 identifier_
= identifier
;
423 const std::string
& LoadablePluginPlaceholder::GetIdentifier() const {
427 bool LoadablePluginPlaceholder::LoadingBlocked() const {
428 DCHECK(allow_loading_
);
429 return is_blocked_for_background_tab_
|| is_blocked_for_power_saver_poster_
||
430 is_blocked_for_prerendering_
;
433 #if defined(ENABLE_PLUGINS)
434 void LoadablePluginPlaceholder::RecheckSizeAndMaybeUnthrottle() {
436 content::RenderThread::Get()->GetTaskRunner()->BelongsToCurrentThread());
437 DCHECK(!in_size_recheck_
);
438 in_size_recheck_
= true;
440 // Re-check the size in case the reported size was incorrect.
441 plugin()->container()->reportGeometry();
443 if (PluginInstanceThrottler::IsLargeContent(unobscured_size_
.width(),
444 unobscured_size_
.height())) {
446 PluginInstanceThrottler::UNTHROTTLE_METHOD_BY_SIZE_CHANGE
);
449 in_size_recheck_
= false;
451 #endif // defined(ENABLE_PLUGINS)
453 } // namespace plugins