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 "base/win/shortcut.h"
6
7#include <shellapi.h>
8#include <shlobj.h>
9#include <propkey.h>
10
11#include "base/files/file_util.h"
12#include "base/threading/thread_restrictions.h"
13#include "base/win/scoped_comptr.h"
14#include "base/win/scoped_propvariant.h"
15#include "base/win/win_util.h"
16#include "base/win/windows_version.h"
17
18namespace base {
19namespace win {
20
21namespace {
22
23// Initializes |i_shell_link| and |i_persist_file| (releasing them first if they
24// are already initialized).
25// If |shortcut| is not NULL, loads |shortcut| into |i_persist_file|.
26// If any of the above steps fail, both |i_shell_link| and |i_persist_file| will
27// be released.
28void InitializeShortcutInterfaces(
29    const wchar_t* shortcut,
30    ScopedComPtr<IShellLink>* i_shell_link,
31    ScopedComPtr<IPersistFile>* i_persist_file) {
32  i_shell_link->Release();
33  i_persist_file->Release();
34  if (FAILED(i_shell_link->CreateInstance(CLSID_ShellLink, NULL,
35                                          CLSCTX_INPROC_SERVER)) ||
36      FAILED(i_persist_file->QueryFrom(*i_shell_link)) ||
37      (shortcut && FAILED((*i_persist_file)->Load(shortcut, STGM_READWRITE)))) {
38    i_shell_link->Release();
39    i_persist_file->Release();
40  }
41}
42
43}  // namespace
44
45bool CreateOrUpdateShortcutLink(const FilePath& shortcut_path,
46                                const ShortcutProperties& properties,
47                                ShortcutOperation operation) {
48  base::ThreadRestrictions::AssertIOAllowed();
49
50  // A target is required unless |operation| is SHORTCUT_UPDATE_EXISTING.
51  if (operation != SHORTCUT_UPDATE_EXISTING &&
52      !(properties.options & ShortcutProperties::PROPERTIES_TARGET)) {
53    NOTREACHED();
54    return false;
55  }
56
57  bool shortcut_existed = PathExists(shortcut_path);
58
59  // Interfaces to the old shortcut when replacing an existing shortcut.
60  ScopedComPtr<IShellLink> old_i_shell_link;
61  ScopedComPtr<IPersistFile> old_i_persist_file;
62
63  // Interfaces to the shortcut being created/updated.
64  ScopedComPtr<IShellLink> i_shell_link;
65  ScopedComPtr<IPersistFile> i_persist_file;
66  switch (operation) {
67    case SHORTCUT_CREATE_ALWAYS:
68      InitializeShortcutInterfaces(NULL, &i_shell_link, &i_persist_file);
69      break;
70    case SHORTCUT_UPDATE_EXISTING:
71      InitializeShortcutInterfaces(shortcut_path.value().c_str(), &i_shell_link,
72                                   &i_persist_file);
73      break;
74    case SHORTCUT_REPLACE_EXISTING:
75      InitializeShortcutInterfaces(shortcut_path.value().c_str(),
76                                   &old_i_shell_link, &old_i_persist_file);
77      // Confirm |shortcut_path| exists and is a shortcut by verifying
78      // |old_i_persist_file| was successfully initialized in the call above. If
79      // so, initialize the interfaces to begin writing a new shortcut (to
80      // overwrite the current one if successful).
81      if (old_i_persist_file.get())
82        InitializeShortcutInterfaces(NULL, &i_shell_link, &i_persist_file);
83      break;
84    default:
85      NOTREACHED();
86  }
87
88  // Return false immediately upon failure to initialize shortcut interfaces.
89  if (!i_persist_file.get())
90    return false;
91
92  if ((properties.options & ShortcutProperties::PROPERTIES_TARGET) &&
93      FAILED(i_shell_link->SetPath(properties.target.value().c_str()))) {
94    return false;
95  }
96
97  if ((properties.options & ShortcutProperties::PROPERTIES_WORKING_DIR) &&
98      FAILED(i_shell_link->SetWorkingDirectory(
99          properties.working_dir.value().c_str()))) {
100    return false;
101  }
102
103  if (properties.options & ShortcutProperties::PROPERTIES_ARGUMENTS) {
104    if (FAILED(i_shell_link->SetArguments(properties.arguments.c_str())))
105      return false;
106  } else if (old_i_persist_file.get()) {
107    wchar_t current_arguments[MAX_PATH] = {0};
108    if (SUCCEEDED(old_i_shell_link->GetArguments(current_arguments,
109                                                 MAX_PATH))) {
110      i_shell_link->SetArguments(current_arguments);
111    }
112  }
113
114  if ((properties.options & ShortcutProperties::PROPERTIES_DESCRIPTION) &&
115      FAILED(i_shell_link->SetDescription(properties.description.c_str()))) {
116    return false;
117  }
118
119  if ((properties.options & ShortcutProperties::PROPERTIES_ICON) &&
120      FAILED(i_shell_link->SetIconLocation(properties.icon.value().c_str(),
121                                           properties.icon_index))) {
122    return false;
123  }
124
125  bool has_app_id =
126      (properties.options & ShortcutProperties::PROPERTIES_APP_ID) != 0;
127  bool has_dual_mode =
128      (properties.options & ShortcutProperties::PROPERTIES_DUAL_MODE) != 0;
129  if ((has_app_id || has_dual_mode) &&
130      GetVersion() >= VERSION_WIN7) {
131    ScopedComPtr<IPropertyStore> property_store;
132    if (FAILED(property_store.QueryFrom(i_shell_link)) || !property_store.get())
133      return false;
134
135    if (has_app_id &&
136        !SetAppIdForPropertyStore(property_store, properties.app_id.c_str())) {
137      return false;
138    }
139    if (has_dual_mode &&
140        !SetBooleanValueForPropertyStore(property_store,
141                                         PKEY_AppUserModel_IsDualMode,
142                                         properties.dual_mode)) {
143      return false;
144    }
145  }
146
147  // Release the interfaces to the old shortcut to make sure it doesn't prevent
148  // overwriting it if needed.
149  old_i_persist_file.Release();
150  old_i_shell_link.Release();
151
152  HRESULT result = i_persist_file->Save(shortcut_path.value().c_str(), TRUE);
153
154  // Release the interfaces in case the SHChangeNotify call below depends on
155  // the operations above being fully completed.
156  i_persist_file.Release();
157  i_shell_link.Release();
158
159  // If we successfully created/updated the icon, notify the shell that we have
160  // done so.
161  const bool succeeded = SUCCEEDED(result);
162  if (succeeded) {
163    if (shortcut_existed) {
164      // TODO(gab): SHCNE_UPDATEITEM might be sufficient here; further testing
165      // required.
166      SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, NULL, NULL);
167    } else {
168      SHChangeNotify(SHCNE_CREATE, SHCNF_PATH, shortcut_path.value().c_str(),
169                     NULL);
170    }
171  }
172
173  return succeeded;
174}
175
176bool ResolveShortcutProperties(const FilePath& shortcut_path,
177                               uint32 options,
178                               ShortcutProperties* properties) {
179  DCHECK(options && properties);
180  base::ThreadRestrictions::AssertIOAllowed();
181
182  if (options & ~ShortcutProperties::PROPERTIES_ALL)
183    NOTREACHED() << "Unhandled property is used.";
184
185  ScopedComPtr<IShellLink> i_shell_link;
186
187  // Get pointer to the IShellLink interface.
188  if (FAILED(i_shell_link.CreateInstance(CLSID_ShellLink, NULL,
189                                         CLSCTX_INPROC_SERVER))) {
190    return false;
191  }
192
193  ScopedComPtr<IPersistFile> persist;
194  // Query IShellLink for the IPersistFile interface.
195  if (FAILED(persist.QueryFrom(i_shell_link)))
196    return false;
197
198  // Load the shell link.
199  if (FAILED(persist->Load(shortcut_path.value().c_str(), STGM_READ)))
200    return false;
201
202  // Reset |properties|.
203  properties->options = 0;
204
205  wchar_t temp[MAX_PATH];
206  if (options & ShortcutProperties::PROPERTIES_TARGET) {
207    if (FAILED(i_shell_link->GetPath(temp, MAX_PATH, NULL, SLGP_UNCPRIORITY)))
208      return false;
209    properties->set_target(FilePath(temp));
210  }
211
212  if (options & ShortcutProperties::PROPERTIES_WORKING_DIR) {
213    if (FAILED(i_shell_link->GetWorkingDirectory(temp, MAX_PATH)))
214      return false;
215    properties->set_working_dir(FilePath(temp));
216  }
217
218  if (options & ShortcutProperties::PROPERTIES_ARGUMENTS) {
219    if (FAILED(i_shell_link->GetArguments(temp, MAX_PATH)))
220      return false;
221    properties->set_arguments(temp);
222  }
223
224  if (options & ShortcutProperties::PROPERTIES_DESCRIPTION) {
225    // Note: description length constrained by MAX_PATH.
226    if (FAILED(i_shell_link->GetDescription(temp, MAX_PATH)))
227      return false;
228    properties->set_description(temp);
229  }
230
231  if (options & ShortcutProperties::PROPERTIES_ICON) {
232    int temp_index;
233    if (FAILED(i_shell_link->GetIconLocation(temp, MAX_PATH, &temp_index)))
234      return false;
235    properties->set_icon(FilePath(temp), temp_index);
236  }
237
238  // Windows 7+ options, avoiding unnecessary work.
239  if ((options & ShortcutProperties::PROPERTIES_WIN7) &&
240      GetVersion() >= VERSION_WIN7) {
241    ScopedComPtr<IPropertyStore> property_store;
242    if (FAILED(property_store.QueryFrom(i_shell_link)))
243      return false;
244
245    if (options & ShortcutProperties::PROPERTIES_APP_ID) {
246      ScopedPropVariant pv_app_id;
247      if (property_store->GetValue(PKEY_AppUserModel_ID,
248                                   pv_app_id.Receive()) != S_OK) {
249        return false;
250      }
251      switch (pv_app_id.get().vt) {
252        case VT_EMPTY:
253          properties->set_app_id(L"");
254          break;
255        case VT_LPWSTR:
256          properties->set_app_id(pv_app_id.get().pwszVal);
257          break;
258        default:
259          NOTREACHED() << "Unexpected variant type: " << pv_app_id.get().vt;
260          return false;
261      }
262    }
263
264    if (options & ShortcutProperties::PROPERTIES_DUAL_MODE) {
265      ScopedPropVariant pv_dual_mode;
266      if (property_store->GetValue(PKEY_AppUserModel_IsDualMode,
267                                   pv_dual_mode.Receive()) != S_OK) {
268        return false;
269      }
270      switch (pv_dual_mode.get().vt) {
271        case VT_EMPTY:
272          properties->set_dual_mode(false);
273          break;
274        case VT_BOOL:
275          properties->set_dual_mode(pv_dual_mode.get().boolVal == VARIANT_TRUE);
276          break;
277        default:
278          NOTREACHED() << "Unexpected variant type: " << pv_dual_mode.get().vt;
279          return false;
280      }
281    }
282  }
283
284  return true;
285}
286
287bool ResolveShortcut(const FilePath& shortcut_path,
288                     FilePath* target_path,
289                     string16* args) {
290  uint32 options = 0;
291  if (target_path)
292    options |= ShortcutProperties::PROPERTIES_TARGET;
293  if (args)
294    options |= ShortcutProperties::PROPERTIES_ARGUMENTS;
295  DCHECK(options);
296
297  ShortcutProperties properties;
298  if (!ResolveShortcutProperties(shortcut_path, options, &properties))
299    return false;
300
301  if (target_path)
302    *target_path = properties.target;
303  if (args)
304    *args = properties.arguments;
305  return true;
306}
307
308bool TaskbarPinShortcutLink(const wchar_t* shortcut) {
309  base::ThreadRestrictions::AssertIOAllowed();
310
311  // "Pin to taskbar" is only supported after Win7.
312  if (GetVersion() < VERSION_WIN7)
313    return false;
314
315  int result = reinterpret_cast<int>(ShellExecute(NULL, L"taskbarpin", shortcut,
316      NULL, NULL, 0));
317  return result > 32;
318}
319
320bool TaskbarUnpinShortcutLink(const wchar_t* shortcut) {
321  base::ThreadRestrictions::AssertIOAllowed();
322
323  // "Unpin from taskbar" is only supported after Win7.
324  if (GetVersion() < VERSION_WIN7)
325    return false;
326
327  int result = reinterpret_cast<int>(ShellExecute(NULL, L"taskbarunpin",
328      shortcut, NULL, NULL, 0));
329  return result > 32;
330}
331
332}  // namespace win
333}  // namespace base
334