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.
5 #include "base/win/shortcut.h"
12 #include "base/files/file_util.h"
13 #include "base/strings/string_util.h"
14 #include "base/threading/thread_restrictions.h"
15 #include "base/win/scoped_bstr.h"
16 #include "base/win/scoped_comptr.h"
17 #include "base/win/scoped_handle.h"
18 #include "base/win/scoped_propvariant.h"
19 #include "base/win/scoped_variant.h"
20 #include "base/win/win_util.h"
21 #include "base/win/windows_version.h"
28 // String resource IDs in shell32.dll.
29 const uint32_t kPinToTaskbarID
= 5386; // Win7+
30 const uint32_t kUnpinFromTaskbarID
= 5387; // Win7+
31 const uint32_t kPinToStartID
= 51201; // Win8+
32 const uint32_t kUnpinFromStartID
= 51394; // Win10+
34 // Traits for a GenericScopedHandle that will free a module on closure.
36 typedef HMODULE Handle
;
37 static Handle
NullHandle() { return nullptr; }
38 static bool IsHandleValid(Handle module
) { return !!module
; }
39 static bool CloseHandle(Handle module
) { return !!::FreeLibrary(module
); }
42 DISALLOW_IMPLICIT_CONSTRUCTORS(ModuleTraits
);
45 // An object that will free a module when it goes out of scope.
46 using ScopedLibrary
= GenericScopedHandle
<ModuleTraits
, DummyVerifierTraits
>;
48 // Returns the shell resource string identified by |resource_id|, or an empty
50 string16
LoadShellResourceString(uint32_t resource_id
) {
51 ScopedLibrary
shell32(::LoadLibrary(L
"shell32.dll"));
52 if (!shell32
.IsValid())
55 const wchar_t* resource_ptr
= nullptr;
56 int length
= ::LoadStringW(shell32
.Get(), resource_id
,
57 reinterpret_cast<wchar_t*>(&resource_ptr
), 0);
58 if (!length
|| !resource_ptr
)
60 return string16(resource_ptr
, length
);
63 // Uses the shell to perform the verb identified by |resource_id| on |path|.
64 bool DoVerbOnFile(uint32_t resource_id
, const FilePath
& path
) {
65 string16
verb_name(LoadShellResourceString(resource_id
));
66 if (verb_name
.empty())
69 ScopedComPtr
<IShellDispatch
> shell_dispatch
;
71 shell_dispatch
.CreateInstance(CLSID_Shell
, nullptr, CLSCTX_INPROC_SERVER
);
72 if (FAILED(hresult
) || !shell_dispatch
.get())
75 ScopedComPtr
<Folder
> folder
;
76 hresult
= shell_dispatch
->NameSpace(
77 ScopedVariant(path
.DirName().value().c_str()), folder
.Receive());
78 if (FAILED(hresult
) || !folder
.get())
81 ScopedComPtr
<FolderItem
> item
;
82 hresult
= folder
->ParseName(ScopedBstr(path
.BaseName().value().c_str()),
84 if (FAILED(hresult
) || !item
.get())
87 ScopedComPtr
<FolderItemVerbs
> verbs
;
88 hresult
= item
->Verbs(verbs
.Receive());
89 if (FAILED(hresult
) || !verbs
.get())
93 hresult
= verbs
->get_Count(&verb_count
);
97 for (long i
= 0; i
< verb_count
; ++i
) {
98 ScopedComPtr
<FolderItemVerb
> verb
;
99 hresult
= verbs
->Item(ScopedVariant(i
, VT_I4
), verb
.Receive());
100 if (FAILED(hresult
) || !verb
.get())
103 hresult
= verb
->get_Name(name
.Receive());
106 if (StringPiece16(name
, name
.Length()) == verb_name
) {
107 hresult
= verb
->DoIt();
108 return SUCCEEDED(hresult
);
114 // Initializes |i_shell_link| and |i_persist_file| (releasing them first if they
115 // are already initialized).
116 // If |shortcut| is not NULL, loads |shortcut| into |i_persist_file|.
117 // If any of the above steps fail, both |i_shell_link| and |i_persist_file| will
119 void InitializeShortcutInterfaces(
120 const wchar_t* shortcut
,
121 ScopedComPtr
<IShellLink
>* i_shell_link
,
122 ScopedComPtr
<IPersistFile
>* i_persist_file
) {
123 i_shell_link
->Release();
124 i_persist_file
->Release();
125 if (FAILED(i_shell_link
->CreateInstance(CLSID_ShellLink
, NULL
,
126 CLSCTX_INPROC_SERVER
)) ||
127 FAILED(i_persist_file
->QueryFrom(i_shell_link
->get())) ||
128 (shortcut
&& FAILED((*i_persist_file
)->Load(shortcut
, STGM_READWRITE
)))) {
129 i_shell_link
->Release();
130 i_persist_file
->Release();
136 ShortcutProperties::ShortcutProperties()
137 : icon_index(-1), dual_mode(false), options(0U) {
140 ShortcutProperties::~ShortcutProperties() {
143 bool CreateOrUpdateShortcutLink(const FilePath
& shortcut_path
,
144 const ShortcutProperties
& properties
,
145 ShortcutOperation operation
) {
146 base::ThreadRestrictions::AssertIOAllowed();
148 // A target is required unless |operation| is SHORTCUT_UPDATE_EXISTING.
149 if (operation
!= SHORTCUT_UPDATE_EXISTING
&&
150 !(properties
.options
& ShortcutProperties::PROPERTIES_TARGET
)) {
155 bool shortcut_existed
= PathExists(shortcut_path
);
157 // Interfaces to the old shortcut when replacing an existing shortcut.
158 ScopedComPtr
<IShellLink
> old_i_shell_link
;
159 ScopedComPtr
<IPersistFile
> old_i_persist_file
;
161 // Interfaces to the shortcut being created/updated.
162 ScopedComPtr
<IShellLink
> i_shell_link
;
163 ScopedComPtr
<IPersistFile
> i_persist_file
;
165 case SHORTCUT_CREATE_ALWAYS
:
166 InitializeShortcutInterfaces(NULL
, &i_shell_link
, &i_persist_file
);
168 case SHORTCUT_UPDATE_EXISTING
:
169 InitializeShortcutInterfaces(shortcut_path
.value().c_str(), &i_shell_link
,
172 case SHORTCUT_REPLACE_EXISTING
:
173 InitializeShortcutInterfaces(shortcut_path
.value().c_str(),
174 &old_i_shell_link
, &old_i_persist_file
);
175 // Confirm |shortcut_path| exists and is a shortcut by verifying
176 // |old_i_persist_file| was successfully initialized in the call above. If
177 // so, initialize the interfaces to begin writing a new shortcut (to
178 // overwrite the current one if successful).
179 if (old_i_persist_file
.get())
180 InitializeShortcutInterfaces(NULL
, &i_shell_link
, &i_persist_file
);
186 // Return false immediately upon failure to initialize shortcut interfaces.
187 if (!i_persist_file
.get())
190 if ((properties
.options
& ShortcutProperties::PROPERTIES_TARGET
) &&
191 FAILED(i_shell_link
->SetPath(properties
.target
.value().c_str()))) {
195 if ((properties
.options
& ShortcutProperties::PROPERTIES_WORKING_DIR
) &&
196 FAILED(i_shell_link
->SetWorkingDirectory(
197 properties
.working_dir
.value().c_str()))) {
201 if (properties
.options
& ShortcutProperties::PROPERTIES_ARGUMENTS
) {
202 if (FAILED(i_shell_link
->SetArguments(properties
.arguments
.c_str())))
204 } else if (old_i_persist_file
.get()) {
205 wchar_t current_arguments
[MAX_PATH
] = {0};
206 if (SUCCEEDED(old_i_shell_link
->GetArguments(current_arguments
,
208 i_shell_link
->SetArguments(current_arguments
);
212 if ((properties
.options
& ShortcutProperties::PROPERTIES_DESCRIPTION
) &&
213 FAILED(i_shell_link
->SetDescription(properties
.description
.c_str()))) {
217 if ((properties
.options
& ShortcutProperties::PROPERTIES_ICON
) &&
218 FAILED(i_shell_link
->SetIconLocation(properties
.icon
.value().c_str(),
219 properties
.icon_index
))) {
224 (properties
.options
& ShortcutProperties::PROPERTIES_APP_ID
) != 0;
226 (properties
.options
& ShortcutProperties::PROPERTIES_DUAL_MODE
) != 0;
227 if ((has_app_id
|| has_dual_mode
) &&
228 GetVersion() >= VERSION_WIN7
) {
229 ScopedComPtr
<IPropertyStore
> property_store
;
230 if (FAILED(property_store
.QueryFrom(i_shell_link
.get())) ||
231 !property_store
.get())
235 !SetAppIdForPropertyStore(property_store
.get(),
236 properties
.app_id
.c_str())) {
240 !SetBooleanValueForPropertyStore(property_store
.get(),
241 PKEY_AppUserModel_IsDualMode
,
242 properties
.dual_mode
)) {
247 // Release the interfaces to the old shortcut to make sure it doesn't prevent
248 // overwriting it if needed.
249 old_i_persist_file
.Release();
250 old_i_shell_link
.Release();
252 HRESULT result
= i_persist_file
->Save(shortcut_path
.value().c_str(), TRUE
);
254 // Release the interfaces in case the SHChangeNotify call below depends on
255 // the operations above being fully completed.
256 i_persist_file
.Release();
257 i_shell_link
.Release();
259 // If we successfully created/updated the icon, notify the shell that we have
261 const bool succeeded
= SUCCEEDED(result
);
263 if (shortcut_existed
) {
264 // TODO(gab): SHCNE_UPDATEITEM might be sufficient here; further testing
266 SHChangeNotify(SHCNE_ASSOCCHANGED
, SHCNF_IDLIST
, NULL
, NULL
);
268 SHChangeNotify(SHCNE_CREATE
, SHCNF_PATH
, shortcut_path
.value().c_str(),
276 bool ResolveShortcutProperties(const FilePath
& shortcut_path
,
278 ShortcutProperties
* properties
) {
279 DCHECK(options
&& properties
);
280 base::ThreadRestrictions::AssertIOAllowed();
282 if (options
& ~ShortcutProperties::PROPERTIES_ALL
)
283 NOTREACHED() << "Unhandled property is used.";
285 ScopedComPtr
<IShellLink
> i_shell_link
;
287 // Get pointer to the IShellLink interface.
288 if (FAILED(i_shell_link
.CreateInstance(CLSID_ShellLink
, NULL
,
289 CLSCTX_INPROC_SERVER
))) {
293 ScopedComPtr
<IPersistFile
> persist
;
294 // Query IShellLink for the IPersistFile interface.
295 if (FAILED(persist
.QueryFrom(i_shell_link
.get())))
298 // Load the shell link.
299 if (FAILED(persist
->Load(shortcut_path
.value().c_str(), STGM_READ
)))
302 // Reset |properties|.
303 properties
->options
= 0;
305 wchar_t temp
[MAX_PATH
];
306 if (options
& ShortcutProperties::PROPERTIES_TARGET
) {
307 if (FAILED(i_shell_link
->GetPath(temp
, MAX_PATH
, NULL
, SLGP_UNCPRIORITY
)))
309 properties
->set_target(FilePath(temp
));
312 if (options
& ShortcutProperties::PROPERTIES_WORKING_DIR
) {
313 if (FAILED(i_shell_link
->GetWorkingDirectory(temp
, MAX_PATH
)))
315 properties
->set_working_dir(FilePath(temp
));
318 if (options
& ShortcutProperties::PROPERTIES_ARGUMENTS
) {
319 if (FAILED(i_shell_link
->GetArguments(temp
, MAX_PATH
)))
321 properties
->set_arguments(temp
);
324 if (options
& ShortcutProperties::PROPERTIES_DESCRIPTION
) {
325 // Note: description length constrained by MAX_PATH.
326 if (FAILED(i_shell_link
->GetDescription(temp
, MAX_PATH
)))
328 properties
->set_description(temp
);
331 if (options
& ShortcutProperties::PROPERTIES_ICON
) {
333 if (FAILED(i_shell_link
->GetIconLocation(temp
, MAX_PATH
, &temp_index
)))
335 properties
->set_icon(FilePath(temp
), temp_index
);
338 // Windows 7+ options, avoiding unnecessary work.
339 if ((options
& ShortcutProperties::PROPERTIES_WIN7
) &&
340 GetVersion() >= VERSION_WIN7
) {
341 ScopedComPtr
<IPropertyStore
> property_store
;
342 if (FAILED(property_store
.QueryFrom(i_shell_link
.get())))
345 if (options
& ShortcutProperties::PROPERTIES_APP_ID
) {
346 ScopedPropVariant pv_app_id
;
347 if (property_store
->GetValue(PKEY_AppUserModel_ID
,
348 pv_app_id
.Receive()) != S_OK
) {
351 switch (pv_app_id
.get().vt
) {
353 properties
->set_app_id(L
"");
356 properties
->set_app_id(pv_app_id
.get().pwszVal
);
359 NOTREACHED() << "Unexpected variant type: " << pv_app_id
.get().vt
;
364 if (options
& ShortcutProperties::PROPERTIES_DUAL_MODE
) {
365 ScopedPropVariant pv_dual_mode
;
366 if (property_store
->GetValue(PKEY_AppUserModel_IsDualMode
,
367 pv_dual_mode
.Receive()) != S_OK
) {
370 switch (pv_dual_mode
.get().vt
) {
372 properties
->set_dual_mode(false);
375 properties
->set_dual_mode(pv_dual_mode
.get().boolVal
== VARIANT_TRUE
);
378 NOTREACHED() << "Unexpected variant type: " << pv_dual_mode
.get().vt
;
387 bool ResolveShortcut(const FilePath
& shortcut_path
,
388 FilePath
* target_path
,
392 options
|= ShortcutProperties::PROPERTIES_TARGET
;
394 options
|= ShortcutProperties::PROPERTIES_ARGUMENTS
;
397 ShortcutProperties properties
;
398 if (!ResolveShortcutProperties(shortcut_path
, options
, &properties
))
402 *target_path
= properties
.target
;
404 *args
= properties
.arguments
;
408 bool PinShortcutToTaskbar(const FilePath
& shortcut
) {
409 base::ThreadRestrictions::AssertIOAllowed();
411 // "Pin to taskbar" is only supported after Win7.
412 if (GetVersion() < VERSION_WIN7
)
415 return DoVerbOnFile(kPinToTaskbarID
, shortcut
);
418 bool UnpinShortcutFromTaskbar(const FilePath
& shortcut
) {
419 base::ThreadRestrictions::AssertIOAllowed();
421 // "Unpin from taskbar" is only supported after Win7.
422 if (GetVersion() < VERSION_WIN7
)
425 return DoVerbOnFile(kUnpinFromTaskbarID
, shortcut
);
428 bool PinShortcutToStart(const FilePath
& shortcut
) {
429 base::ThreadRestrictions::AssertIOAllowed();
431 // While "Pin to Start" is supported as of Win8, it was never used by Chrome
432 // in Win8. The behaviour on Win8 is different (new shortcut every time
433 // instead of a single pin associated with its app id) and the Start Menu
434 // shortcut itself is visible on the Start Screen whereas it is not on Win10.
435 // For simplicity's sake and per greater necessity on Win10, it is only
436 // supported in Chrome on Win10+.
437 if (GetVersion() < VERSION_WIN10
)
440 return DoVerbOnFile(kPinToStartID
, shortcut
);
443 bool UnpinShortcutFromStart(const FilePath
& shortcut
) {
444 base::ThreadRestrictions::AssertIOAllowed();
446 // "Unpin from Start Menu" is only supported after Win10.
447 if (GetVersion() < VERSION_WIN10
)
450 return DoVerbOnFile(kUnpinFromStartID
, shortcut
);