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: 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>; 84 ~DevToolsRestorer() {} 85 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. 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. 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 139 AppWindowCreateFunction::AppWindowCreateFunction() 140 : inject_html_titlebar_(false) {} 141 142 void AppWindowCreateFunction::SendDelayedResponse() { 143 SendResponse(true); 144 } 145 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 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 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 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