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