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.
4
5 #include "chrome/browser/extensions/api/app_window/app_window_api.h"
6
7 #include "apps/app_window.h"
8 #include "apps/app_window_contents.h"
9 #include "apps/app_window_registry.h"
10 #include "apps/apps_client.h"
11 #include "apps/ui/native_app_window.h"
12 #include "base/command_line.h"
13 #include "base/strings/string_number_conversions.h"
14 #include "base/strings/string_util.h"
15 #include "base/time/time.h"
16 #include "base/values.h"
17 #include "chrome/browser/devtools/devtools_window.h"
18 #include "chrome/common/chrome_switches.h"
19 #include "chrome/common/extensions/api/app_window.h"
20 #include "chrome/common/extensions/features/feature_channel.h"
21 #include "content/public/browser/notification_registrar.h"
22 #include "content/public/browser/notification_types.h"
23 #include "content/public/browser/render_process_host.h"
24 #include "content/public/browser/render_view_host.h"
25 #include "content/public/browser/web_contents.h"
26 #include "content/public/common/url_constants.h"
27 #include "extensions/browser/extensions_browser_client.h"
28 #include "extensions/browser/image_util.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/rect.h"
34 #include "url/gurl.h"
35
36 using apps::AppWindow;
37
38 namespace app_window = extensions::api::app_window;
39 namespace Create = app_window::Create;
40
41 namespace extensions {
42
43 namespace app_window_constants {
44 const char kInvalidWindowId[] =
45 "The window id can not be more than 256 characters long.";
46 const char kInvalidColorSpecification[] =
47 "The color specification could not be parsed.";
48 const char kColorWithFrameNone[] = "Windows with no frame cannot have a color.";
49 const char kInactiveColorWithoutColor[] =
50 "frame.inactiveColor must be used with frame.color.";
51 const char kConflictingBoundsOptions[] =
52 "The $1 property cannot be specified for both inner and outer bounds.";
53 const char kAlwaysOnTopPermission[] =
54 "The \"app.window.alwaysOnTop\" permission is required.";
55 const char kInvalidUrlParameter[] =
56 "The URL used for window creation must be local for security reasons.";
57 } // namespace app_window_constants
58
59 const char kNoneFrameOption[] = "none";
60 // TODO(benwells): Remove HTML titlebar injection.
61 const char kHtmlFrameOption[] = "experimental-html";
62
63 namespace {
64
65 // Opens an inspector window and delays the response to the
66 // AppWindowCreateFunction until the DevToolsWindow has finished loading, and is
67 // ready to stop on breakpoints in the callback.
68 class DevToolsRestorer : public base::RefCounted<DevToolsRestorer> {
69 public:
DevToolsRestorer(AppWindowCreateFunction * delayed_create_function,content::RenderViewHost * created_view)70 DevToolsRestorer(AppWindowCreateFunction* delayed_create_function,
71 content::RenderViewHost* created_view)
72 : delayed_create_function_(delayed_create_function) {
73 AddRef(); // Balanced in LoadCompleted.
74 DevToolsWindow* devtools_window =
75 DevToolsWindow::OpenDevToolsWindow(
76 created_view,
77 DevToolsToggleAction::ShowConsole());
78 devtools_window->SetLoadCompletedCallback(
79 base::Bind(&DevToolsRestorer::LoadCompleted, this));
80 }
81
82 private:
83 friend class base::RefCounted<DevToolsRestorer>;
~DevToolsRestorer()84 ~DevToolsRestorer() {}
85
LoadCompleted()86 void LoadCompleted() {
87 delayed_create_function_->SendDelayedResponse();
88 Release();
89 }
90
91 scoped_refptr<AppWindowCreateFunction> delayed_create_function_;
92 };
93
94 // If the same property is specified for the inner and outer bounds, raise an
95 // error.
CheckBoundsConflict(const scoped_ptr<int> & inner_property,const scoped_ptr<int> & outer_property,const std::string & property_name,std::string * error)96 bool CheckBoundsConflict(const scoped_ptr<int>& inner_property,
97 const scoped_ptr<int>& outer_property,
98 const std::string& property_name,
99 std::string* error) {
100 if (inner_property.get() && outer_property.get()) {
101 std::vector<std::string> subst;
102 subst.push_back(property_name);
103 *error = ReplaceStringPlaceholders(
104 app_window_constants::kConflictingBoundsOptions, subst, NULL);
105 return false;
106 }
107
108 return true;
109 }
110
111 // Copy over the bounds specification properties from the API to the
112 // AppWindow::CreateParams.
CopyBoundsSpec(const extensions::api::app_window::BoundsSpecification * input_spec,apps::AppWindow::BoundsSpecification * create_spec)113 void CopyBoundsSpec(
114 const extensions::api::app_window::BoundsSpecification* input_spec,
115 apps::AppWindow::BoundsSpecification* create_spec) {
116 if (!input_spec)
117 return;
118
119 if (input_spec->left.get())
120 create_spec->bounds.set_x(*input_spec->left);
121 if (input_spec->top.get())
122 create_spec->bounds.set_y(*input_spec->top);
123 if (input_spec->width.get())
124 create_spec->bounds.set_width(*input_spec->width);
125 if (input_spec->height.get())
126 create_spec->bounds.set_height(*input_spec->height);
127 if (input_spec->min_width.get())
128 create_spec->minimum_size.set_width(*input_spec->min_width);
129 if (input_spec->min_height.get())
130 create_spec->minimum_size.set_height(*input_spec->min_height);
131 if (input_spec->max_width.get())
132 create_spec->maximum_size.set_width(*input_spec->max_width);
133 if (input_spec->max_height.get())
134 create_spec->maximum_size.set_height(*input_spec->max_height);
135 }
136
137 } // namespace
138
AppWindowCreateFunction()139 AppWindowCreateFunction::AppWindowCreateFunction()
140 : inject_html_titlebar_(false) {}
141
SendDelayedResponse()142 void AppWindowCreateFunction::SendDelayedResponse() {
143 SendResponse(true);
144 }
145
RunAsync()146 bool AppWindowCreateFunction::RunAsync() {
147 // Don't create app window if the system is shutting down.
148 if (extensions::ExtensionsBrowserClient::Get()->IsShuttingDown())
149 return false;
150
151 scoped_ptr<Create::Params> params(Create::Params::Create(*args_));
152 EXTENSION_FUNCTION_VALIDATE(params.get());
153
154 GURL url = GetExtension()->GetResourceURL(params->url);
155 // Allow absolute URLs for component apps, otherwise prepend the extension
156 // path.
157 GURL absolute = GURL(params->url);
158 if (absolute.has_scheme()) {
159 if (GetExtension()->location() == extensions::Manifest::COMPONENT) {
160 url = absolute;
161 } else {
162 // Show error when url passed isn't local.
163 error_ = app_window_constants::kInvalidUrlParameter;
164 return false;
165 }
166 }
167
168 // TODO(jeremya): figure out a way to pass the opening WebContents through to
169 // AppWindow::Create so we can set the opener at create time rather than
170 // with a hack in AppWindowCustomBindings::GetView().
171 AppWindow::CreateParams create_params;
172 app_window::CreateWindowOptions* options = params->options.get();
173 if (options) {
174 if (options->id.get()) {
175 // TODO(mek): use URL if no id specified?
176 // Limit length of id to 256 characters.
177 if (options->id->length() > 256) {
178 error_ = app_window_constants::kInvalidWindowId;
179 return false;
180 }
181
182 create_params.window_key = *options->id;
183
184 if (options->singleton && *options->singleton == false) {
185 WriteToConsole(
186 content::CONSOLE_MESSAGE_LEVEL_WARNING,
187 "The 'singleton' option in chrome.apps.window.create() is deprecated!"
188 " Change your code to no longer rely on this.");
189 }
190
191 if (!options->singleton || *options->singleton) {
192 AppWindow* window = apps::AppWindowRegistry::Get(browser_context())
193 ->GetAppWindowForAppAndKey(
194 extension_id(), create_params.window_key);
195 if (window) {
196 content::RenderViewHost* created_view =
197 window->web_contents()->GetRenderViewHost();
198 int view_id = MSG_ROUTING_NONE;
199 if (render_view_host_->GetProcess()->GetID() ==
200 created_view->GetProcess()->GetID()) {
201 view_id = created_view->GetRoutingID();
202 }
203
204 if (options->focused.get() && !*options->focused.get())
205 window->Show(AppWindow::SHOW_INACTIVE);
206 else
207 window->Show(AppWindow::SHOW_ACTIVE);
208
209 base::DictionaryValue* result = new base::DictionaryValue;
210 result->Set("viewId", new base::FundamentalValue(view_id));
211 window->GetSerializedState(result);
212 result->SetBoolean("existingWindow", true);
213 // TODO(benwells): Remove HTML titlebar injection.
214 result->SetBoolean("injectTitlebar", false);
215 SetResult(result);
216 SendResponse(true);
217 return true;
218 }
219 }
220 }
221
222 if (!GetBoundsSpec(*options, &create_params, &error_))
223 return false;
224
225 if (GetCurrentChannel() <= chrome::VersionInfo::CHANNEL_DEV ||
226 GetExtension()->location() == extensions::Manifest::COMPONENT) {
227 if (options->type == extensions::api::app_window::WINDOW_TYPE_PANEL) {
228 create_params.window_type = AppWindow::WINDOW_TYPE_PANEL;
229 }
230 }
231
232 if (!GetFrameOptions(*options, &create_params))
233 return false;
234
235 if (options->transparent_background.get() &&
236 (GetExtension()->permissions_data()->HasAPIPermission(
237 APIPermission::kExperimental) ||
238 CommandLine::ForCurrentProcess()->HasSwitch(
239 switches::kEnableExperimentalExtensionApis))) {
240 create_params.transparent_background = *options->transparent_background;
241 }
242
243 if (options->hidden.get())
244 create_params.hidden = *options->hidden.get();
245
246 if (options->resizable.get())
247 create_params.resizable = *options->resizable.get();
248
249 if (options->always_on_top.get()) {
250 create_params.always_on_top = *options->always_on_top.get();
251
252 if (create_params.always_on_top &&
253 !GetExtension()->permissions_data()->HasAPIPermission(
254 APIPermission::kAlwaysOnTopWindows)) {
255 error_ = app_window_constants::kAlwaysOnTopPermission;
256 return false;
257 }
258 }
259
260 if (options->focused.get())
261 create_params.focused = *options->focused.get();
262
263 if (options->type != extensions::api::app_window::WINDOW_TYPE_PANEL) {
264 switch (options->state) {
265 case extensions::api::app_window::STATE_NONE:
266 case extensions::api::app_window::STATE_NORMAL:
267 break;
268 case extensions::api::app_window::STATE_FULLSCREEN:
269 create_params.state = ui::SHOW_STATE_FULLSCREEN;
270 break;
271 case extensions::api::app_window::STATE_MAXIMIZED:
272 create_params.state = ui::SHOW_STATE_MAXIMIZED;
273 break;
274 case extensions::api::app_window::STATE_MINIMIZED:
275 create_params.state = ui::SHOW_STATE_MINIMIZED;
276 break;
277 }
278 }
279 }
280
281 create_params.creator_process_id =
282 render_view_host_->GetProcess()->GetID();
283
284 AppWindow* app_window = apps::AppsClient::Get()->CreateAppWindow(
285 browser_context(), GetExtension());
286 app_window->Init(
287 url, new apps::AppWindowContentsImpl(app_window), create_params);
288
289 if (ExtensionsBrowserClient::Get()->IsRunningInForcedAppMode())
290 app_window->ForcedFullscreen();
291
292 content::RenderViewHost* created_view =
293 app_window->web_contents()->GetRenderViewHost();
294 int view_id = MSG_ROUTING_NONE;
295 if (create_params.creator_process_id == created_view->GetProcess()->GetID())
296 view_id = created_view->GetRoutingID();
297
298 base::DictionaryValue* result = new base::DictionaryValue;
299 result->Set("viewId", new base::FundamentalValue(view_id));
300 result->Set("injectTitlebar",
301 new base::FundamentalValue(inject_html_titlebar_));
302 result->Set("id", new base::StringValue(app_window->window_key()));
303 app_window->GetSerializedState(result);
304 SetResult(result);
305
306 if (apps::AppWindowRegistry::Get(browser_context())
307 ->HadDevToolsAttached(created_view)) {
308 new DevToolsRestorer(this, created_view);
309 return true;
310 }
311
312 SendResponse(true);
313 app_window->WindowEventsReady();
314
315 return true;
316 }
317
GetBoundsSpec(const extensions::api::app_window::CreateWindowOptions & options,apps::AppWindow::CreateParams * params,std::string * error)318 bool AppWindowCreateFunction::GetBoundsSpec(
319 const extensions::api::app_window::CreateWindowOptions& options,
320 apps::AppWindow::CreateParams* params,
321 std::string* error) {
322 DCHECK(params);
323 DCHECK(error);
324
325 if (options.inner_bounds.get() || options.outer_bounds.get()) {
326 // Parse the inner and outer bounds specifications. If developers use the
327 // new API, the deprecated fields will be ignored - do not attempt to merge
328 // them.
329
330 const extensions::api::app_window::BoundsSpecification* inner_bounds =
331 options.inner_bounds.get();
332 const extensions::api::app_window::BoundsSpecification* outer_bounds =
333 options.outer_bounds.get();
334 if (inner_bounds && outer_bounds) {
335 if (!CheckBoundsConflict(
336 inner_bounds->left, outer_bounds->left, "left", error)) {
337 return false;
338 }
339 if (!CheckBoundsConflict(
340 inner_bounds->top, outer_bounds->top, "top", error)) {
341 return false;
342 }
343 if (!CheckBoundsConflict(
344 inner_bounds->width, outer_bounds->width, "width", error)) {
345 return false;
346 }
347 if (!CheckBoundsConflict(
348 inner_bounds->height, outer_bounds->height, "height", error)) {
349 return false;
350 }
351 if (!CheckBoundsConflict(inner_bounds->min_width,
352 outer_bounds->min_width,
353 "minWidth",
354 error)) {
355 return false;
356 }
357 if (!CheckBoundsConflict(inner_bounds->min_height,
358 outer_bounds->min_height,
359 "minHeight",
360 error)) {
361 return false;
362 }
363 if (!CheckBoundsConflict(inner_bounds->max_width,
364 outer_bounds->max_width,
365 "maxWidth",
366 error)) {
367 return false;
368 }
369 if (!CheckBoundsConflict(inner_bounds->max_height,
370 outer_bounds->max_height,
371 "maxHeight",
372 error)) {
373 return false;
374 }
375 }
376
377 CopyBoundsSpec(inner_bounds, &(params->content_spec));
378 CopyBoundsSpec(outer_bounds, &(params->window_spec));
379 } else {
380 // Parse deprecated fields.
381 // Due to a bug in NativeAppWindow::GetFrameInsets() on Windows and ChromeOS
382 // the bounds set the position of the window and the size of the content.
383 // This will be preserved as apps may be relying on this behavior.
384
385 if (options.default_width.get())
386 params->content_spec.bounds.set_width(*options.default_width.get());
387 if (options.default_height.get())
388 params->content_spec.bounds.set_height(*options.default_height.get());
389 if (options.default_left.get())
390 params->window_spec.bounds.set_x(*options.default_left.get());
391 if (options.default_top.get())
392 params->window_spec.bounds.set_y(*options.default_top.get());
393
394 if (options.width.get())
395 params->content_spec.bounds.set_width(*options.width.get());
396 if (options.height.get())
397 params->content_spec.bounds.set_height(*options.height.get());
398 if (options.left.get())
399 params->window_spec.bounds.set_x(*options.left.get());
400 if (options.top.get())
401 params->window_spec.bounds.set_y(*options.top.get());
402
403 if (options.bounds.get()) {
404 app_window::ContentBounds* bounds = options.bounds.get();
405 if (bounds->width.get())
406 params->content_spec.bounds.set_width(*bounds->width.get());
407 if (bounds->height.get())
408 params->content_spec.bounds.set_height(*bounds->height.get());
409 if (bounds->left.get())
410 params->window_spec.bounds.set_x(*bounds->left.get());
411 if (bounds->top.get())
412 params->window_spec.bounds.set_y(*bounds->top.get());
413 }
414
415 gfx::Size& minimum_size = params->content_spec.minimum_size;
416 if (options.min_width.get())
417 minimum_size.set_width(*options.min_width);
418 if (options.min_height.get())
419 minimum_size.set_height(*options.min_height);
420 gfx::Size& maximum_size = params->content_spec.maximum_size;
421 if (options.max_width.get())
422 maximum_size.set_width(*options.max_width);
423 if (options.max_height.get())
424 maximum_size.set_height(*options.max_height);
425 }
426
427 return true;
428 }
429
GetFrameFromString(const std::string & frame_string)430 AppWindow::Frame AppWindowCreateFunction::GetFrameFromString(
431 const std::string& frame_string) {
432 if (frame_string == kHtmlFrameOption &&
433 (GetExtension()->permissions_data()->HasAPIPermission(
434 APIPermission::kExperimental) ||
435 CommandLine::ForCurrentProcess()->HasSwitch(
436 switches::kEnableExperimentalExtensionApis))) {
437 inject_html_titlebar_ = true;
438 return AppWindow::FRAME_NONE;
439 }
440
441 if (frame_string == kNoneFrameOption)
442 return AppWindow::FRAME_NONE;
443
444 return AppWindow::FRAME_CHROME;
445 }
446
GetFrameOptions(const app_window::CreateWindowOptions & options,AppWindow::CreateParams * create_params)447 bool AppWindowCreateFunction::GetFrameOptions(
448 const app_window::CreateWindowOptions& options,
449 AppWindow::CreateParams* create_params) {
450 if (!options.frame)
451 return true;
452
453 DCHECK(options.frame->as_string || options.frame->as_frame_options);
454 if (options.frame->as_string) {
455 create_params->frame = GetFrameFromString(*options.frame->as_string);
456 return true;
457 }
458
459 if (options.frame->as_frame_options->type)
460 create_params->frame =
461 GetFrameFromString(*options.frame->as_frame_options->type);
462
463 if (options.frame->as_frame_options->color.get()) {
464 if (create_params->frame != AppWindow::FRAME_CHROME) {
465 error_ = app_window_constants::kColorWithFrameNone;
466 return false;
467 }
468
469 if (!image_util::ParseCSSColorString(
470 *options.frame->as_frame_options->color,
471 &create_params->active_frame_color)) {
472 error_ = app_window_constants::kInvalidColorSpecification;
473 return false;
474 }
475
476 create_params->has_frame_color = true;
477 create_params->inactive_frame_color = create_params->active_frame_color;
478
479 if (options.frame->as_frame_options->inactive_color.get()) {
480 if (!image_util::ParseCSSColorString(
481 *options.frame->as_frame_options->inactive_color,
482 &create_params->inactive_frame_color)) {
483 error_ = app_window_constants::kInvalidColorSpecification;
484 return false;
485 }
486 }
487
488 return true;
489 }
490
491 if (options.frame->as_frame_options->inactive_color.get()) {
492 error_ = app_window_constants::kInactiveColorWithoutColor;
493 return false;
494 }
495
496 return true;
497 }
498
499 } // namespace extensions
500