1 // Copyright (c) 2012 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 "extensions/browser/api/app_window/app_window_api.h"
7 #include "base/command_line.h"
8 #include "base/strings/string_number_conversions.h"
9 #include "base/strings/string_util.h"
10 #include "base/time/time.h"
11 #include "base/values.h"
12 #include "content/public/browser/notification_registrar.h"
13 #include "content/public/browser/notification_types.h"
14 #include "content/public/browser/render_frame_host.h"
15 #include "content/public/browser/render_process_host.h"
16 #include "content/public/browser/web_contents.h"
17 #include "content/public/common/content_switches.h"
18 #include "content/public/common/url_constants.h"
19 #include "extensions/browser/app_window/app_window.h"
20 #include "extensions/browser/app_window/app_window_client.h"
21 #include "extensions/browser/app_window/app_window_contents.h"
22 #include "extensions/browser/app_window/app_window_registry.h"
23 #include "extensions/browser/app_window/native_app_window.h"
24 #include "extensions/browser/extensions_browser_client.h"
25 #include "extensions/common/api/app_window.h"
26 #include "extensions/common/features/simple_feature.h"
27 #include "extensions/common/image_util.h"
28 #include "extensions/common/manifest.h"
29 #include "extensions/common/permissions/permissions_data.h"
30 #include "extensions/common/switches.h"
31 #include "third_party/skia/include/core/SkColor.h"
32 #include "ui/base/ui_base_types.h"
33 #include "ui/gfx/geometry/rect.h"
36 namespace app_window
= extensions::api::app_window
;
37 namespace Create
= app_window::Create
;
39 namespace extensions
{
41 namespace app_window_constants
{
42 const char kInvalidWindowId
[] =
43 "The window id can not be more than 256 characters long.";
44 const char kInvalidColorSpecification
[] =
45 "The color specification could not be parsed.";
46 const char kColorWithFrameNone
[] = "Windows with no frame cannot have a color.";
47 const char kInactiveColorWithoutColor
[] =
48 "frame.inactiveColor must be used with frame.color.";
49 const char kConflictingBoundsOptions
[] =
50 "The $1 property cannot be specified for both inner and outer bounds.";
51 const char kAlwaysOnTopPermission
[] =
52 "The \"app.window.alwaysOnTop\" permission is required.";
53 const char kInvalidUrlParameter
[] =
54 "The URL used for window creation must be local for security reasons.";
55 const char kAlphaEnabledWrongChannel
[] =
56 "The alphaEnabled option requires dev channel or newer.";
57 const char kAlphaEnabledMissingPermission
[] =
58 "The alphaEnabled option requires app.window.alpha permission.";
59 const char kAlphaEnabledNeedsFrameNone
[] =
60 "The alphaEnabled option can only be used with \"frame: 'none'\".";
61 const char kImeWindowMissingPermission
[] =
62 "Extensions require the \"app.window.ime\" permission to create windows.";
63 const char kImeOptionIsNotSupported
[] =
64 "The \"ime\" option is not supported for platform app.";
65 #if !defined(OS_CHROMEOS)
66 const char kImeWindowUnsupportedPlatform
[] =
67 "The \"ime\" option can only be used on ChromeOS.";
69 const char kImeOptionMustBeTrueAndNeedsFrameNone
[] =
70 "IME extensions must create window with \"ime: true\" and "
73 } // namespace app_window_constants
75 const char kNoneFrameOption
[] = "none";
76 // TODO(benwells): Remove HTML titlebar injection.
77 const char kHtmlFrameOption
[] = "experimental-html";
81 // If the same property is specified for the inner and outer bounds, raise an
83 bool CheckBoundsConflict(const scoped_ptr
<int>& inner_property
,
84 const scoped_ptr
<int>& outer_property
,
85 const std::string
& property_name
,
87 if (inner_property
.get() && outer_property
.get()) {
88 std::vector
<std::string
> subst
;
89 subst
.push_back(property_name
);
90 *error
= base::ReplaceStringPlaceholders(
91 app_window_constants::kConflictingBoundsOptions
, subst
, NULL
);
98 // Copy over the bounds specification properties from the API to the
99 // AppWindow::CreateParams.
100 void CopyBoundsSpec(const app_window::BoundsSpecification
* input_spec
,
101 AppWindow::BoundsSpecification
* create_spec
) {
105 if (input_spec
->left
.get())
106 create_spec
->bounds
.set_x(*input_spec
->left
);
107 if (input_spec
->top
.get())
108 create_spec
->bounds
.set_y(*input_spec
->top
);
109 if (input_spec
->width
.get())
110 create_spec
->bounds
.set_width(*input_spec
->width
);
111 if (input_spec
->height
.get())
112 create_spec
->bounds
.set_height(*input_spec
->height
);
113 if (input_spec
->min_width
.get())
114 create_spec
->minimum_size
.set_width(*input_spec
->min_width
);
115 if (input_spec
->min_height
.get())
116 create_spec
->minimum_size
.set_height(*input_spec
->min_height
);
117 if (input_spec
->max_width
.get())
118 create_spec
->maximum_size
.set_width(*input_spec
->max_width
);
119 if (input_spec
->max_height
.get())
120 create_spec
->maximum_size
.set_height(*input_spec
->max_height
);
125 AppWindowCreateFunction::AppWindowCreateFunction()
126 : inject_html_titlebar_(false) {}
128 bool AppWindowCreateFunction::RunAsync() {
129 // Don't create app window if the system is shutting down.
130 if (ExtensionsBrowserClient::Get()->IsShuttingDown())
133 scoped_ptr
<Create::Params
> params(Create::Params::Create(*args_
));
134 EXTENSION_FUNCTION_VALIDATE(params
.get());
136 GURL url
= extension()->GetResourceURL(params
->url
);
137 // Allow absolute URLs for component apps, otherwise prepend the extension
139 // TODO(devlin): Investigate if this is still used. If not, kill it dead!
140 GURL absolute
= GURL(params
->url
);
141 if (absolute
.has_scheme()) {
142 if (extension()->location() == Manifest::COMPONENT
) {
145 // Show error when url passed isn't local.
146 error_
= app_window_constants::kInvalidUrlParameter
;
151 // TODO(jeremya): figure out a way to pass the opening WebContents through to
152 // AppWindow::Create so we can set the opener at create time rather than
153 // with a hack in AppWindowCustomBindings::GetView().
154 AppWindow::CreateParams create_params
;
155 app_window::CreateWindowOptions
* options
= params
->options
.get();
157 if (options
->id
.get()) {
158 // TODO(mek): use URL if no id specified?
159 // Limit length of id to 256 characters.
160 if (options
->id
->length() > 256) {
161 error_
= app_window_constants::kInvalidWindowId
;
165 create_params
.window_key
= *options
->id
;
167 if (options
->singleton
&& *options
->singleton
== false) {
169 content::CONSOLE_MESSAGE_LEVEL_WARNING
,
170 "The 'singleton' option in chrome.apps.window.create() is deprecated!"
171 " Change your code to no longer rely on this.");
174 if (!options
->singleton
|| *options
->singleton
) {
175 AppWindow
* existing_window
=
176 AppWindowRegistry::Get(browser_context())
177 ->GetAppWindowForAppAndKey(extension_id(),
178 create_params
.window_key
);
179 if (existing_window
) {
180 content::RenderFrameHost
* existing_frame
=
181 existing_window
->web_contents()->GetMainFrame();
182 int frame_id
= MSG_ROUTING_NONE
;
183 if (render_frame_host()->GetProcess()->GetID() ==
184 existing_frame
->GetProcess()->GetID()) {
185 frame_id
= existing_frame
->GetRoutingID();
188 if (!options
->hidden
.get() || !*options
->hidden
.get()) {
189 if (options
->focused
.get() && !*options
->focused
.get())
190 existing_window
->Show(AppWindow::SHOW_INACTIVE
);
192 existing_window
->Show(AppWindow::SHOW_ACTIVE
);
195 base::DictionaryValue
* result
= new base::DictionaryValue
;
196 result
->Set("frameId", new base::FundamentalValue(frame_id
));
197 existing_window
->GetSerializedState(result
);
198 result
->SetBoolean("existingWindow", true);
199 // TODO(benwells): Remove HTML titlebar injection.
200 result
->SetBoolean("injectTitlebar", false);
208 if (!GetBoundsSpec(*options
, &create_params
, &error_
))
211 if (options
->type
== app_window::WINDOW_TYPE_PANEL
) {
212 #if defined(OS_CHROMEOS)
213 // Panels for v2 apps are only supported on Chrome OS.
214 create_params
.window_type
= AppWindow::WINDOW_TYPE_PANEL
;
216 WriteToConsole(content::CONSOLE_MESSAGE_LEVEL_WARNING
,
217 "Panels are not supported on this platform");
221 if (!GetFrameOptions(*options
, &create_params
))
224 if (extension()->GetType() == Manifest::TYPE_EXTENSION
) {
225 // Whitelisted IME extensions are allowed to use this API to create IME
226 // specific windows to show accented characters or suggestions.
227 if (!extension()->permissions_data()->HasAPIPermission(
228 APIPermission::kImeWindowEnabled
)) {
229 error_
= app_window_constants::kImeWindowMissingPermission
;
233 #if !defined(OS_CHROMEOS)
234 // IME window is only supported on ChromeOS.
235 error_
= app_window_constants::kImeWindowUnsupportedPlatform
;
238 // IME extensions must create window with "ime: true" and "frame: none".
239 if (!options
->ime
.get() || !*options
->ime
.get() ||
240 create_params
.frame
!= AppWindow::FRAME_NONE
) {
241 error_
= app_window_constants::kImeOptionMustBeTrueAndNeedsFrameNone
;
244 create_params
.is_ime_window
= true;
245 #endif // OS_CHROMEOS
247 if (options
->ime
.get()) {
248 error_
= app_window_constants::kImeOptionIsNotSupported
;
253 if (options
->alpha_enabled
.get()) {
254 const char* const kWhitelist
[] = {
255 #if defined(OS_CHROMEOS)
256 "B58B99751225318C7EB8CF4688B5434661083E07", // http://crbug.com/410550
257 "06BE211D5F014BAB34BC22D9DDA09C63A81D828E", // http://crbug.com/425539
258 "F94EE6AB36D6C6588670B2B01EB65212D9C64E33",
259 "B9EF10DDFEA11EF77873CC5009809E5037FC4C7A", // http://crbug.com/435380
261 "0F42756099D914A026DADFA182871C015735DD95", // http://crbug.com/323773
262 "2D22CDB6583FD0A13758AEBE8B15E45208B4E9A7",
263 "E7E2461CE072DF036CF9592740196159E2D7C089", // http://crbug.com/356200
264 "A74A4D44C7CFCD8844830E6140C8D763E12DD8F3",
265 "312745D9BF916161191143F6490085EEA0434997",
266 "53041A2FA309EECED01FFC751E7399186E860B2C",
267 "A07A5B743CD82A1C2579DB77D353C98A23201EEF", // http://crbug.com/413748
268 "F16F23C83C5F6DAD9B65A120448B34056DD80691",
269 "0F585FB1D0FDFBEBCE1FEB5E9DFFB6DA476B8C9B"
271 if (AppWindowClient::Get()->IsCurrentChannelOlderThanDev() &&
272 !SimpleFeature::IsIdInArray(
273 extension_id(), kWhitelist
, arraysize(kWhitelist
))) {
274 error_
= app_window_constants::kAlphaEnabledWrongChannel
;
277 if (!extension()->permissions_data()->HasAPIPermission(
278 APIPermission::kAlphaEnabled
)) {
279 error_
= app_window_constants::kAlphaEnabledMissingPermission
;
282 if (create_params
.frame
!= AppWindow::FRAME_NONE
) {
283 error_
= app_window_constants::kAlphaEnabledNeedsFrameNone
;
286 #if defined(USE_AURA)
287 create_params
.alpha_enabled
= *options
->alpha_enabled
;
289 // Transparency is only supported on Aura.
290 // Fallback to creating an opaque window (by ignoring alphaEnabled).
294 if (options
->hidden
.get())
295 create_params
.hidden
= *options
->hidden
.get();
297 if (options
->resizable
.get())
298 create_params
.resizable
= *options
->resizable
.get();
300 if (options
->always_on_top
.get()) {
301 create_params
.always_on_top
= *options
->always_on_top
.get();
303 if (create_params
.always_on_top
&&
304 !extension()->permissions_data()->HasAPIPermission(
305 APIPermission::kAlwaysOnTopWindows
)) {
306 error_
= app_window_constants::kAlwaysOnTopPermission
;
311 if (options
->focused
.get())
312 create_params
.focused
= *options
->focused
.get();
314 if (options
->visible_on_all_workspaces
.get()) {
315 create_params
.visible_on_all_workspaces
=
316 *options
->visible_on_all_workspaces
.get();
319 if (options
->type
!= app_window::WINDOW_TYPE_PANEL
) {
320 switch (options
->state
) {
321 case app_window::STATE_NONE
:
322 case app_window::STATE_NORMAL
:
324 case app_window::STATE_FULLSCREEN
:
325 create_params
.state
= ui::SHOW_STATE_FULLSCREEN
;
327 case app_window::STATE_MAXIMIZED
:
328 create_params
.state
= ui::SHOW_STATE_MAXIMIZED
;
330 case app_window::STATE_MINIMIZED
:
331 create_params
.state
= ui::SHOW_STATE_MINIMIZED
;
337 create_params
.creator_process_id
=
338 render_frame_host()->GetProcess()->GetID();
340 AppWindow
* app_window
=
341 AppWindowClient::Get()->CreateAppWindow(browser_context(), extension());
342 app_window
->Init(url
, new AppWindowContentsImpl(app_window
), create_params
);
344 if (ExtensionsBrowserClient::Get()->IsRunningInForcedAppMode() &&
345 !app_window
->is_ime_window()) {
346 app_window
->ForcedFullscreen();
349 content::RenderFrameHost
* created_frame
=
350 app_window
->web_contents()->GetMainFrame();
351 int frame_id
= MSG_ROUTING_NONE
;
352 if (create_params
.creator_process_id
== created_frame
->GetProcess()->GetID())
353 frame_id
= created_frame
->GetRoutingID();
355 base::DictionaryValue
* result
= new base::DictionaryValue
;
356 result
->Set("frameId", new base::FundamentalValue(frame_id
));
357 result
->Set("injectTitlebar",
358 new base::FundamentalValue(inject_html_titlebar_
));
359 result
->Set("id", new base::StringValue(app_window
->window_key()));
360 app_window
->GetSerializedState(result
);
363 if (AppWindowRegistry::Get(browser_context())
364 ->HadDevToolsAttached(app_window
->web_contents())) {
365 AppWindowClient::Get()->OpenDevToolsWindow(
366 app_window
->web_contents(),
367 base::Bind(&AppWindowCreateFunction::SendResponse
, this, true));
371 // PlzNavigate: delay sending the response until the newly created window has
372 // been told to navigate, and blink has been correctly initialized in the
374 if (base::CommandLine::ForCurrentProcess()->HasSwitch(
375 ::switches::kEnableBrowserSideNavigation
)) {
376 app_window
->SetOnFirstCommitCallback(
377 base::Bind(&AppWindowCreateFunction::SendResponse
, this, true));
382 app_window
->WindowEventsReady();
387 bool AppWindowCreateFunction::GetBoundsSpec(
388 const app_window::CreateWindowOptions
& options
,
389 AppWindow::CreateParams
* params
,
390 std::string
* error
) {
394 if (options
.inner_bounds
.get() || options
.outer_bounds
.get()) {
395 // Parse the inner and outer bounds specifications. If developers use the
396 // new API, the deprecated fields will be ignored - do not attempt to merge
399 const app_window::BoundsSpecification
* inner_bounds
=
400 options
.inner_bounds
.get();
401 const app_window::BoundsSpecification
* outer_bounds
=
402 options
.outer_bounds
.get();
403 if (inner_bounds
&& outer_bounds
) {
404 if (!CheckBoundsConflict(
405 inner_bounds
->left
, outer_bounds
->left
, "left", error
)) {
408 if (!CheckBoundsConflict(
409 inner_bounds
->top
, outer_bounds
->top
, "top", error
)) {
412 if (!CheckBoundsConflict(
413 inner_bounds
->width
, outer_bounds
->width
, "width", error
)) {
416 if (!CheckBoundsConflict(
417 inner_bounds
->height
, outer_bounds
->height
, "height", error
)) {
420 if (!CheckBoundsConflict(inner_bounds
->min_width
,
421 outer_bounds
->min_width
,
426 if (!CheckBoundsConflict(inner_bounds
->min_height
,
427 outer_bounds
->min_height
,
432 if (!CheckBoundsConflict(inner_bounds
->max_width
,
433 outer_bounds
->max_width
,
438 if (!CheckBoundsConflict(inner_bounds
->max_height
,
439 outer_bounds
->max_height
,
446 CopyBoundsSpec(inner_bounds
, &(params
->content_spec
));
447 CopyBoundsSpec(outer_bounds
, &(params
->window_spec
));
449 // Parse deprecated fields.
450 // Due to a bug in NativeAppWindow::GetFrameInsets() on Windows and ChromeOS
451 // the bounds set the position of the window and the size of the content.
452 // This will be preserved as apps may be relying on this behavior.
454 if (options
.default_width
.get())
455 params
->content_spec
.bounds
.set_width(*options
.default_width
.get());
456 if (options
.default_height
.get())
457 params
->content_spec
.bounds
.set_height(*options
.default_height
.get());
458 if (options
.default_left
.get())
459 params
->window_spec
.bounds
.set_x(*options
.default_left
.get());
460 if (options
.default_top
.get())
461 params
->window_spec
.bounds
.set_y(*options
.default_top
.get());
463 if (options
.width
.get())
464 params
->content_spec
.bounds
.set_width(*options
.width
.get());
465 if (options
.height
.get())
466 params
->content_spec
.bounds
.set_height(*options
.height
.get());
467 if (options
.left
.get())
468 params
->window_spec
.bounds
.set_x(*options
.left
.get());
469 if (options
.top
.get())
470 params
->window_spec
.bounds
.set_y(*options
.top
.get());
472 if (options
.bounds
.get()) {
473 app_window::ContentBounds
* bounds
= options
.bounds
.get();
474 if (bounds
->width
.get())
475 params
->content_spec
.bounds
.set_width(*bounds
->width
.get());
476 if (bounds
->height
.get())
477 params
->content_spec
.bounds
.set_height(*bounds
->height
.get());
478 if (bounds
->left
.get())
479 params
->window_spec
.bounds
.set_x(*bounds
->left
.get());
480 if (bounds
->top
.get())
481 params
->window_spec
.bounds
.set_y(*bounds
->top
.get());
484 gfx::Size
& minimum_size
= params
->content_spec
.minimum_size
;
485 if (options
.min_width
.get())
486 minimum_size
.set_width(*options
.min_width
);
487 if (options
.min_height
.get())
488 minimum_size
.set_height(*options
.min_height
);
489 gfx::Size
& maximum_size
= params
->content_spec
.maximum_size
;
490 if (options
.max_width
.get())
491 maximum_size
.set_width(*options
.max_width
);
492 if (options
.max_height
.get())
493 maximum_size
.set_height(*options
.max_height
);
499 AppWindow::Frame
AppWindowCreateFunction::GetFrameFromString(
500 const std::string
& frame_string
) {
501 if (frame_string
== kHtmlFrameOption
&&
502 (extension()->permissions_data()->HasAPIPermission(
503 APIPermission::kExperimental
) ||
504 base::CommandLine::ForCurrentProcess()->HasSwitch(
505 switches::kEnableExperimentalExtensionApis
))) {
506 inject_html_titlebar_
= true;
507 return AppWindow::FRAME_NONE
;
510 if (frame_string
== kNoneFrameOption
)
511 return AppWindow::FRAME_NONE
;
513 return AppWindow::FRAME_CHROME
;
516 bool AppWindowCreateFunction::GetFrameOptions(
517 const app_window::CreateWindowOptions
& options
,
518 AppWindow::CreateParams
* create_params
) {
522 DCHECK(options
.frame
->as_string
|| options
.frame
->as_frame_options
);
523 if (options
.frame
->as_string
) {
524 create_params
->frame
= GetFrameFromString(*options
.frame
->as_string
);
528 if (options
.frame
->as_frame_options
->type
)
529 create_params
->frame
=
530 GetFrameFromString(*options
.frame
->as_frame_options
->type
);
532 if (options
.frame
->as_frame_options
->color
.get()) {
533 if (create_params
->frame
!= AppWindow::FRAME_CHROME
) {
534 error_
= app_window_constants::kColorWithFrameNone
;
538 if (!image_util::ParseCSSColorString(
539 *options
.frame
->as_frame_options
->color
,
540 &create_params
->active_frame_color
)) {
541 error_
= app_window_constants::kInvalidColorSpecification
;
545 create_params
->has_frame_color
= true;
546 create_params
->inactive_frame_color
= create_params
->active_frame_color
;
548 if (options
.frame
->as_frame_options
->inactive_color
.get()) {
549 if (!image_util::ParseCSSColorString(
550 *options
.frame
->as_frame_options
->inactive_color
,
551 &create_params
->inactive_frame_color
)) {
552 error_
= app_window_constants::kInvalidColorSpecification
;
560 if (options
.frame
->as_frame_options
->inactive_color
.get()) {
561 error_
= app_window_constants::kInactiveColorWithoutColor
;
568 } // namespace extensions