web_app_win.cc revision a02191e04bc25c4935f804f2c080ae28663d096d
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/web_applications/web_app_win.h" 6 7#include <shlobj.h> 8 9#include "base/command_line.h" 10#include "base/file_util.h" 11#include "base/files/file_enumerator.h" 12#include "base/logging.h" 13#include "base/md5.h" 14#include "base/path_service.h" 15#include "base/strings/string_piece.h" 16#include "base/strings/stringprintf.h" 17#include "base/strings/utf_string_conversions.h" 18#include "base/win/shortcut.h" 19#include "base/win/windows_version.h" 20#include "chrome/browser/web_applications/update_shortcut_worker_win.h" 21#include "chrome/common/chrome_switches.h" 22#include "chrome/installer/util/browser_distribution.h" 23#include "chrome/installer/util/shell_util.h" 24#include "chrome/installer/util/util_constants.h" 25#include "content/public/browser/browser_thread.h" 26#include "ui/gfx/icon_util.h" 27#include "ui/gfx/image/image.h" 28#include "ui/gfx/image/image_family.h" 29 30namespace { 31 32const base::FilePath::CharType kIconChecksumFileExt[] = 33 FILE_PATH_LITERAL(".ico.md5"); 34 35// Calculates checksum of an icon family using MD5. 36// The checksum is derived from all of the icons in the family. 37void GetImageCheckSum(const gfx::ImageFamily& image, base::MD5Digest* digest) { 38 DCHECK(digest); 39 base::MD5Context md5_context; 40 base::MD5Init(&md5_context); 41 42 for (gfx::ImageFamily::const_iterator it = image.begin(); it != image.end(); 43 ++it) { 44 SkBitmap bitmap = it->AsBitmap(); 45 46 SkAutoLockPixels image_lock(bitmap); 47 base::StringPiece image_data( 48 reinterpret_cast<const char*>(bitmap.getPixels()), bitmap.getSize()); 49 base::MD5Update(&md5_context, image_data); 50 } 51 52 base::MD5Final(digest, &md5_context); 53} 54 55// Saves |image| as an |icon_file| with the checksum. 56bool SaveIconWithCheckSum(const base::FilePath& icon_file, 57 const gfx::ImageFamily& image) { 58 if (!IconUtil::CreateIconFileFromImageFamily(image, icon_file)) 59 return false; 60 61 base::MD5Digest digest; 62 GetImageCheckSum(image, &digest); 63 64 base::FilePath cheksum_file(icon_file.ReplaceExtension(kIconChecksumFileExt)); 65 return base::WriteFile(cheksum_file, 66 reinterpret_cast<const char*>(&digest), 67 sizeof(digest)) == sizeof(digest); 68} 69 70// Returns true if |icon_file| is missing or different from |image|. 71bool ShouldUpdateIcon(const base::FilePath& icon_file, 72 const gfx::ImageFamily& image) { 73 base::FilePath checksum_file( 74 icon_file.ReplaceExtension(kIconChecksumFileExt)); 75 76 // Returns true if icon_file or checksum file is missing. 77 if (!base::PathExists(icon_file) || 78 !base::PathExists(checksum_file)) 79 return true; 80 81 base::MD5Digest persisted_image_checksum; 82 if (sizeof(persisted_image_checksum) != base::ReadFile(checksum_file, 83 reinterpret_cast<char*>(&persisted_image_checksum), 84 sizeof(persisted_image_checksum))) 85 return true; 86 87 base::MD5Digest downloaded_image_checksum; 88 GetImageCheckSum(image, &downloaded_image_checksum); 89 90 // Update icon if checksums are not equal. 91 return memcmp(&persisted_image_checksum, &downloaded_image_checksum, 92 sizeof(base::MD5Digest)) != 0; 93} 94 95// Returns true if |shortcut_file_name| matches profile |profile_path|, and has 96// an --app-id flag. 97bool IsAppShortcutForProfile(const base::FilePath& shortcut_file_name, 98 const base::FilePath& profile_path) { 99 base::string16 cmd_line_string; 100 if (base::win::ResolveShortcut(shortcut_file_name, NULL, &cmd_line_string)) { 101 cmd_line_string = L"program " + cmd_line_string; 102 CommandLine shortcut_cmd_line = CommandLine::FromString(cmd_line_string); 103 return shortcut_cmd_line.HasSwitch(switches::kProfileDirectory) && 104 shortcut_cmd_line.GetSwitchValuePath(switches::kProfileDirectory) == 105 profile_path.BaseName() && 106 shortcut_cmd_line.HasSwitch(switches::kAppId); 107 } 108 109 return false; 110} 111 112// Finds shortcuts in |shortcut_path| that match profile for |profile_path| and 113// extension with title |shortcut_name|. 114// If |shortcut_name| is empty, finds all shortcuts matching |profile_path|. 115std::vector<base::FilePath> FindAppShortcutsByProfileAndTitle( 116 const base::FilePath& shortcut_path, 117 const base::FilePath& profile_path, 118 const base::string16& shortcut_name) { 119 std::vector<base::FilePath> shortcut_paths; 120 121 if (shortcut_name.empty()) { 122 // Find all shortcuts for this profile. 123 base::FileEnumerator files(shortcut_path, false, 124 base::FileEnumerator::FILES, 125 FILE_PATH_LITERAL("*.lnk")); 126 base::FilePath shortcut_file = files.Next(); 127 while (!shortcut_file.empty()) { 128 if (IsAppShortcutForProfile(shortcut_file, profile_path)) 129 shortcut_paths.push_back(shortcut_file); 130 shortcut_file = files.Next(); 131 } 132 } else { 133 // Find all shortcuts matching |shortcut_name|. 134 base::FilePath base_path = shortcut_path. 135 Append(web_app::internals::GetSanitizedFileName(shortcut_name)). 136 AddExtension(FILE_PATH_LITERAL(".lnk")); 137 138 const int fileNamesToCheck = 10; 139 for (int i = 0; i < fileNamesToCheck; ++i) { 140 base::FilePath shortcut_file = base_path; 141 if (i > 0) { 142 shortcut_file = shortcut_file.InsertBeforeExtensionASCII( 143 base::StringPrintf(" (%d)", i)); 144 } 145 if (base::PathExists(shortcut_file) && 146 IsAppShortcutForProfile(shortcut_file, profile_path)) { 147 shortcut_paths.push_back(shortcut_file); 148 } 149 } 150 } 151 152 return shortcut_paths; 153} 154 155// Creates application shortcuts in a given set of paths. 156// |shortcut_paths| is a list of directories in which shortcuts should be 157// created. If |creation_reason| is SHORTCUT_CREATION_AUTOMATED and there is an 158// existing shortcut to this app for this profile, does nothing (succeeding). 159// Returns true on success, false on failure. 160// Must be called on the FILE thread. 161bool CreateShortcutsInPaths( 162 const base::FilePath& web_app_path, 163 const web_app::ShortcutInfo& shortcut_info, 164 const std::vector<base::FilePath>& shortcut_paths, 165 web_app::ShortcutCreationReason creation_reason, 166 std::vector<base::FilePath>* out_filenames) { 167 // Ensure web_app_path exists. 168 if (!base::PathExists(web_app_path) && 169 !base::CreateDirectory(web_app_path)) { 170 return false; 171 } 172 173 // Generates file name to use with persisted ico and shortcut file. 174 base::FilePath icon_file = 175 web_app::internals::GetIconFilePath(web_app_path, shortcut_info.title); 176 if (!web_app::internals::CheckAndSaveIcon(icon_file, shortcut_info.favicon)) { 177 return false; 178 } 179 180 base::FilePath chrome_exe; 181 if (!PathService::Get(base::FILE_EXE, &chrome_exe)) { 182 NOTREACHED(); 183 return false; 184 } 185 186 // Working directory. 187 base::FilePath working_dir(chrome_exe.DirName()); 188 189 CommandLine cmd_line(CommandLine::NO_PROGRAM); 190 cmd_line = ShellIntegration::CommandLineArgsForLauncher(shortcut_info.url, 191 shortcut_info.extension_id, shortcut_info.profile_path); 192 193 // TODO(evan): we rely on the fact that command_line_string() is 194 // properly quoted for a Windows command line. The method on 195 // CommandLine should probably be renamed to better reflect that 196 // fact. 197 base::string16 wide_switches(cmd_line.GetCommandLineString()); 198 199 // Sanitize description 200 base::string16 description = shortcut_info.description; 201 if (description.length() >= MAX_PATH) 202 description.resize(MAX_PATH - 1); 203 204 // Generates app id from web app url and profile path. 205 std::string app_name(web_app::GenerateApplicationNameFromInfo(shortcut_info)); 206 base::string16 app_id(ShellIntegration::GetAppModelIdForProfile( 207 base::UTF8ToUTF16(app_name), shortcut_info.profile_path)); 208 209 bool success = true; 210 for (size_t i = 0; i < shortcut_paths.size(); ++i) { 211 base::FilePath shortcut_file = 212 shortcut_paths[i] 213 .Append( 214 web_app::internals::GetSanitizedFileName(shortcut_info.title)) 215 .AddExtension(installer::kLnkExt); 216 if (creation_reason == web_app::SHORTCUT_CREATION_AUTOMATED) { 217 // Check whether there is an existing shortcut to this app. 218 std::vector<base::FilePath> shortcut_files = 219 FindAppShortcutsByProfileAndTitle(shortcut_paths[i], 220 shortcut_info.profile_path, 221 shortcut_info.title); 222 if (!shortcut_files.empty()) 223 continue; 224 } 225 if (shortcut_paths[i] != web_app_path) { 226 int unique_number = 227 base::GetUniquePathNumber(shortcut_file, 228 base::FilePath::StringType()); 229 if (unique_number == -1) { 230 success = false; 231 continue; 232 } else if (unique_number > 0) { 233 shortcut_file = shortcut_file.InsertBeforeExtensionASCII( 234 base::StringPrintf(" (%d)", unique_number)); 235 } 236 } 237 base::win::ShortcutProperties shortcut_properties; 238 shortcut_properties.set_target(chrome_exe); 239 shortcut_properties.set_working_dir(working_dir); 240 shortcut_properties.set_arguments(wide_switches); 241 shortcut_properties.set_description(description); 242 shortcut_properties.set_icon(icon_file, 0); 243 shortcut_properties.set_app_id(app_id); 244 shortcut_properties.set_dual_mode(false); 245 if (!base::PathExists(shortcut_file.DirName()) && 246 !base::CreateDirectory(shortcut_file.DirName())) { 247 NOTREACHED(); 248 return false; 249 } 250 success = base::win::CreateOrUpdateShortcutLink( 251 shortcut_file, shortcut_properties, 252 base::win::SHORTCUT_CREATE_ALWAYS) && success; 253 if (out_filenames) 254 out_filenames->push_back(shortcut_file); 255 } 256 257 return success; 258} 259 260// Gets the directories with shortcuts for an app, and deletes the shortcuts. 261// This will search the standard locations for shortcuts named |title| that open 262// in the profile with |profile_path|. 263// |was_pinned_to_taskbar| will be set to true if there was previously a 264// shortcut pinned to the taskbar for this app; false otherwise. 265// If |web_app_path| is empty, this will not delete shortcuts from the web app 266// directory. If |title| is empty, all shortcuts for this profile will be 267// deleted. 268// |shortcut_paths| will be populated with a list of directories where shortcuts 269// for this app were found (and deleted). This will delete duplicate shortcuts, 270// but only return each path once, even if it contained multiple deleted 271// shortcuts. Both of these may be NULL. 272void GetShortcutLocationsAndDeleteShortcuts( 273 const base::FilePath& web_app_path, 274 const base::FilePath& profile_path, 275 const base::string16& title, 276 bool* was_pinned_to_taskbar, 277 std::vector<base::FilePath>* shortcut_paths) { 278 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::FILE)); 279 280 // Get all possible locations for shortcuts. 281 web_app::ShortcutLocations all_shortcut_locations; 282 all_shortcut_locations.in_quick_launch_bar = true; 283 all_shortcut_locations.on_desktop = true; 284 // Delete shortcuts from the Chrome Apps subdirectory. 285 // This matches the subdir name set by CreateApplicationShortcutView::Accept 286 // for Chrome apps (not URL apps, but this function does not apply for them). 287 all_shortcut_locations.applications_menu_location = 288 web_app::APP_MENU_LOCATION_SUBDIR_CHROMEAPPS; 289 std::vector<base::FilePath> all_paths = web_app::internals::GetShortcutPaths( 290 all_shortcut_locations); 291 if (base::win::GetVersion() >= base::win::VERSION_WIN7 && 292 !web_app_path.empty()) { 293 all_paths.push_back(web_app_path); 294 } 295 296 if (was_pinned_to_taskbar) { 297 // Determine if there is a link to this app in the TaskBar pin directory. 298 base::FilePath taskbar_pin_path; 299 if (PathService::Get(base::DIR_TASKBAR_PINS, &taskbar_pin_path)) { 300 std::vector<base::FilePath> taskbar_pin_files = 301 FindAppShortcutsByProfileAndTitle(taskbar_pin_path, profile_path, 302 title); 303 *was_pinned_to_taskbar = !taskbar_pin_files.empty(); 304 } else { 305 *was_pinned_to_taskbar = false; 306 } 307 } 308 309 for (std::vector<base::FilePath>::const_iterator i = all_paths.begin(); 310 i != all_paths.end(); ++i) { 311 std::vector<base::FilePath> shortcut_files = 312 FindAppShortcutsByProfileAndTitle(*i, profile_path, title); 313 if (shortcut_paths && !shortcut_files.empty()) { 314 shortcut_paths->push_back(*i); 315 } 316 for (std::vector<base::FilePath>::const_iterator j = shortcut_files.begin(); 317 j != shortcut_files.end(); ++j) { 318 // Any shortcut could have been pinned, either by chrome or the user, so 319 // they are all unpinned. 320 base::win::TaskbarUnpinShortcutLink(j->value().c_str()); 321 base::DeleteFile(*j, false); 322 } 323 } 324} 325 326} // namespace 327 328namespace web_app { 329 330base::FilePath CreateShortcutInWebAppDir( 331 const base::FilePath& web_app_dir, 332 const web_app::ShortcutInfo& shortcut_info) { 333 std::vector<base::FilePath> paths; 334 paths.push_back(web_app_dir); 335 std::vector<base::FilePath> out_filenames; 336 base::FilePath web_app_dir_shortcut = 337 web_app_dir.Append(internals::GetSanitizedFileName(shortcut_info.title)) 338 .AddExtension(installer::kLnkExt); 339 if (!PathExists(web_app_dir_shortcut)) { 340 CreateShortcutsInPaths(web_app_dir, 341 shortcut_info, 342 paths, 343 SHORTCUT_CREATION_BY_USER, 344 &out_filenames); 345 DCHECK_EQ(out_filenames.size(), 1u); 346 DCHECK_EQ(out_filenames[0].value(), web_app_dir_shortcut.value()); 347 } else { 348 internals::CheckAndSaveIcon( 349 internals::GetIconFilePath(web_app_dir, shortcut_info.title), 350 shortcut_info.favicon); 351 } 352 return web_app_dir_shortcut; 353} 354 355namespace internals { 356 357// Saves |image| to |icon_file| if the file is outdated and refresh shell's 358// icon cache to ensure correct icon is displayed. Returns true if icon_file 359// is up to date or successfully updated. 360bool CheckAndSaveIcon(const base::FilePath& icon_file, 361 const gfx::ImageFamily& image) { 362 if (ShouldUpdateIcon(icon_file, image)) { 363 if (SaveIconWithCheckSum(icon_file, image)) { 364 // Refresh shell's icon cache. This call is quite disruptive as user would 365 // see explorer rebuilding the icon cache. It would be great that we find 366 // a better way to achieve this. 367 SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST | SHCNF_FLUSHNOWAIT, 368 NULL, NULL); 369 } else { 370 return false; 371 } 372 } 373 374 return true; 375} 376 377bool CreatePlatformShortcuts( 378 const base::FilePath& web_app_path, 379 const web_app::ShortcutInfo& shortcut_info, 380 const extensions::FileHandlersInfo& file_handlers_info, 381 const web_app::ShortcutLocations& creation_locations, 382 ShortcutCreationReason creation_reason) { 383 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::FILE)); 384 385 // Shortcut paths under which to create shortcuts. 386 std::vector<base::FilePath> shortcut_paths = 387 GetShortcutPaths(creation_locations); 388 389 bool pin_to_taskbar = creation_locations.in_quick_launch_bar && 390 (base::win::GetVersion() >= base::win::VERSION_WIN7); 391 392 // Create/update the shortcut in the web app path for the "Pin To Taskbar" 393 // option in Win7. We use the web app path shortcut because we will overwrite 394 // it rather than appending unique numbers if the shortcut already exists. 395 // This prevents pinned apps from having unique numbers in their names. 396 if (pin_to_taskbar) 397 shortcut_paths.push_back(web_app_path); 398 399 if (shortcut_paths.empty()) 400 return false; 401 402 if (!CreateShortcutsInPaths(web_app_path, shortcut_info, shortcut_paths, 403 creation_reason, NULL)) 404 return false; 405 406 if (pin_to_taskbar) { 407 base::FilePath file_name = GetSanitizedFileName(shortcut_info.title); 408 // Use the web app path shortcut for pinning to avoid having unique numbers 409 // in the application name. 410 base::FilePath shortcut_to_pin = web_app_path.Append(file_name). 411 AddExtension(installer::kLnkExt); 412 if (!base::win::TaskbarPinShortcutLink(shortcut_to_pin.value().c_str())) 413 return false; 414 } 415 416 return true; 417} 418 419void UpdatePlatformShortcuts( 420 const base::FilePath& web_app_path, 421 const base::string16& old_app_title, 422 const web_app::ShortcutInfo& shortcut_info, 423 const extensions::FileHandlersInfo& file_handlers_info) { 424 DCHECK(content::BrowserThread::CurrentlyOn(content::BrowserThread::FILE)); 425 426 // Generates file name to use with persisted ico and shortcut file. 427 base::FilePath file_name = 428 web_app::internals::GetSanitizedFileName(shortcut_info.title); 429 430 if (old_app_title != shortcut_info.title) { 431 // The app's title has changed. Delete all existing app shortcuts and 432 // recreate them in any locations they already existed (but do not add them 433 // to locations where they do not currently exist). 434 bool was_pinned_to_taskbar; 435 std::vector<base::FilePath> shortcut_paths; 436 GetShortcutLocationsAndDeleteShortcuts( 437 web_app_path, shortcut_info.profile_path, old_app_title, 438 &was_pinned_to_taskbar, &shortcut_paths); 439 CreateShortcutsInPaths(web_app_path, shortcut_info, shortcut_paths, 440 SHORTCUT_CREATION_BY_USER, NULL); 441 // If the shortcut was pinned to the taskbar, 442 // GetShortcutLocationsAndDeleteShortcuts will have deleted it. In that 443 // case, re-pin it. 444 if (was_pinned_to_taskbar) { 445 base::FilePath file_name = GetSanitizedFileName(shortcut_info.title); 446 // Use the web app path shortcut for pinning to avoid having unique 447 // numbers in the application name. 448 base::FilePath shortcut_to_pin = web_app_path.Append(file_name). 449 AddExtension(installer::kLnkExt); 450 base::win::TaskbarPinShortcutLink(shortcut_to_pin.value().c_str()); 451 } 452 } 453 454 // Update the icon if necessary. 455 base::FilePath icon_file = GetIconFilePath(web_app_path, shortcut_info.title); 456 CheckAndSaveIcon(icon_file, shortcut_info.favicon); 457} 458 459void DeletePlatformShortcuts( 460 const base::FilePath& web_app_path, 461 const web_app::ShortcutInfo& shortcut_info) { 462 GetShortcutLocationsAndDeleteShortcuts( 463 web_app_path, shortcut_info.profile_path, shortcut_info.title, NULL, 464 NULL); 465 466 // If there are no more shortcuts in the Chrome Apps subdirectory, remove it. 467 base::FilePath chrome_apps_dir; 468 if (ShellUtil::GetShortcutPath( 469 ShellUtil::SHORTCUT_LOCATION_START_MENU_CHROME_APPS_DIR, 470 BrowserDistribution::GetDistribution(), 471 ShellUtil::CURRENT_USER, 472 &chrome_apps_dir)) { 473 if (base::IsDirectoryEmpty(chrome_apps_dir)) 474 base::DeleteFile(chrome_apps_dir, false); 475 } 476} 477 478void DeleteAllShortcutsForProfile(const base::FilePath& profile_path) { 479 GetShortcutLocationsAndDeleteShortcuts(base::FilePath(), profile_path, L"", 480 NULL, NULL); 481 482 // If there are no more shortcuts in the Chrome Apps subdirectory, remove it. 483 base::FilePath chrome_apps_dir; 484 if (ShellUtil::GetShortcutPath( 485 ShellUtil::SHORTCUT_LOCATION_START_MENU_CHROME_APPS_DIR, 486 BrowserDistribution::GetDistribution(), 487 ShellUtil::CURRENT_USER, 488 &chrome_apps_dir)) { 489 if (base::IsDirectoryEmpty(chrome_apps_dir)) 490 base::DeleteFile(chrome_apps_dir, false); 491 } 492} 493 494std::vector<base::FilePath> GetShortcutPaths( 495 const web_app::ShortcutLocations& creation_locations) { 496 // Shortcut paths under which to create shortcuts. 497 std::vector<base::FilePath> shortcut_paths; 498 // Locations to add to shortcut_paths. 499 struct { 500 bool use_this_location; 501 ShellUtil::ShortcutLocation location_id; 502 } locations[] = { 503 { 504 creation_locations.on_desktop, 505 ShellUtil::SHORTCUT_LOCATION_DESKTOP 506 }, { 507 creation_locations.applications_menu_location == 508 web_app::APP_MENU_LOCATION_ROOT, 509 ShellUtil::SHORTCUT_LOCATION_START_MENU_ROOT 510 }, { 511 creation_locations.applications_menu_location == 512 web_app::APP_MENU_LOCATION_SUBDIR_CHROME, 513 ShellUtil::SHORTCUT_LOCATION_START_MENU_CHROME_DIR 514 }, { 515 creation_locations.applications_menu_location == 516 web_app::APP_MENU_LOCATION_SUBDIR_CHROMEAPPS, 517 ShellUtil::SHORTCUT_LOCATION_START_MENU_CHROME_APPS_DIR 518 }, { 519 // For Win7+, |in_quick_launch_bar| indicates that we are pinning to 520 // taskbar. This needs to be handled by callers. 521 creation_locations.in_quick_launch_bar && 522 base::win::GetVersion() < base::win::VERSION_WIN7, 523 ShellUtil::SHORTCUT_LOCATION_QUICK_LAUNCH 524 } 525 }; 526 527 BrowserDistribution* dist = BrowserDistribution::GetDistribution(); 528 // Populate shortcut_paths. 529 for (int i = 0; i < arraysize(locations); ++i) { 530 if (locations[i].use_this_location) { 531 base::FilePath path; 532 if (!ShellUtil::GetShortcutPath(locations[i].location_id, 533 dist, 534 ShellUtil::CURRENT_USER, 535 &path)) { 536 NOTREACHED(); 537 continue; 538 } 539 shortcut_paths.push_back(path); 540 } 541 } 542 return shortcut_paths; 543} 544 545base::FilePath GetIconFilePath(const base::FilePath& web_app_path, 546 const base::string16& title) { 547 return web_app_path.Append(GetSanitizedFileName(title)) 548 .AddExtension(FILE_PATH_LITERAL(".ico")); 549} 550 551} // namespace internals 552 553void UpdateShortcutForTabContents(content::WebContents* web_contents) { 554 // UpdateShortcutWorker will delete itself when it's done. 555 UpdateShortcutWorker* worker = new UpdateShortcutWorker(web_contents); 556 worker->Run(); 557} 558 559} // namespace web_app 560