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;
30 const uint32_t kUnpinFromTaskbarID
= 5387;
32 // Traits for a GenericScopedHandle that will free a module on closure.
34 typedef HMODULE Handle
;
35 static Handle
NullHandle() { return nullptr; }
36 static bool IsHandleValid(Handle module
) { return !!module
; }
37 static bool CloseHandle(Handle module
) { return !!::FreeLibrary(module
); }
40 DISALLOW_IMPLICIT_CONSTRUCTORS(ModuleTraits
);
43 // An object that will free a module when it goes out of scope.
44 using ScopedLibrary
= GenericScopedHandle
<ModuleTraits
, DummyVerifierTraits
>;
46 // Returns the shell resource string identified by |resource_id|, or an empty
48 string16
LoadShellResourceString(uint32_t resource_id
) {
49 ScopedLibrary
shell32(::LoadLibrary(L
"shell32.dll"));
50 if (!shell32
.IsValid())
53 const wchar_t* resource_ptr
= nullptr;
54 int length
= ::LoadStringW(shell32
.Get(), resource_id
,
55 reinterpret_cast<wchar_t*>(&resource_ptr
), 0);
56 if (!length
|| !resource_ptr
)
58 return string16(resource_ptr
, length
);
61 // Uses the shell to perform the verb identified by |resource_id| on |path|.
62 bool DoVerbOnFile(uint32_t resource_id
, const FilePath
& path
) {
63 string16
verb_name(LoadShellResourceString(resource_id
));
64 if (verb_name
.empty())
67 ScopedComPtr
<IShellDispatch
> shell_dispatch
;
69 shell_dispatch
.CreateInstance(CLSID_Shell
, nullptr, CLSCTX_INPROC_SERVER
);
70 if (FAILED(hresult
) || !shell_dispatch
.get())
73 ScopedComPtr
<Folder
> folder
;
74 hresult
= shell_dispatch
->NameSpace(
75 ScopedVariant(path
.DirName().value().c_str()), folder
.Receive());
76 if (FAILED(hresult
) || !folder
.get())
79 ScopedComPtr
<FolderItem
> item
;
80 hresult
= folder
->ParseName(ScopedBstr(path
.BaseName().value().c_str()),
82 if (FAILED(hresult
) || !item
.get())
85 ScopedComPtr
<FolderItemVerbs
> verbs
;
86 hresult
= item
->Verbs(verbs
.Receive());
87 if (FAILED(hresult
) || !verbs
.get())
91 hresult
= verbs
->get_Count(&verb_count
);
95 for (long i
= 0; i
< verb_count
; ++i
) {
96 ScopedComPtr
<FolderItemVerb
> verb
;
97 hresult
= verbs
->Item(ScopedVariant(i
, VT_I4
), verb
.Receive());
98 if (FAILED(hresult
) || !verb
.get())
101 hresult
= verb
->get_Name(name
.Receive());
104 if (StringPiece16(name
, name
.Length()) == verb_name
) {
105 hresult
= verb
->DoIt();
106 return SUCCEEDED(hresult
);
112 // Initializes |i_shell_link| and |i_persist_file| (releasing them first if they
113 // are already initialized).
114 // If |shortcut| is not NULL, loads |shortcut| into |i_persist_file|.
115 // If any of the above steps fail, both |i_shell_link| and |i_persist_file| will
117 void InitializeShortcutInterfaces(
118 const wchar_t* shortcut
,
119 ScopedComPtr
<IShellLink
>* i_shell_link
,
120 ScopedComPtr
<IPersistFile
>* i_persist_file
) {
121 i_shell_link
->Release();
122 i_persist_file
->Release();
123 if (FAILED(i_shell_link
->CreateInstance(CLSID_ShellLink
, NULL
,
124 CLSCTX_INPROC_SERVER
)) ||
125 FAILED(i_persist_file
->QueryFrom(i_shell_link
->get())) ||
126 (shortcut
&& FAILED((*i_persist_file
)->Load(shortcut
, STGM_READWRITE
)))) {
127 i_shell_link
->Release();
128 i_persist_file
->Release();
134 ShortcutProperties::ShortcutProperties()
135 : icon_index(-1), dual_mode(false), options(0U) {
138 ShortcutProperties::~ShortcutProperties() {
141 bool CreateOrUpdateShortcutLink(const FilePath
& shortcut_path
,
142 const ShortcutProperties
& properties
,
143 ShortcutOperation operation
) {
144 base::ThreadRestrictions::AssertIOAllowed();
146 // A target is required unless |operation| is SHORTCUT_UPDATE_EXISTING.
147 if (operation
!= SHORTCUT_UPDATE_EXISTING
&&
148 !(properties
.options
& ShortcutProperties::PROPERTIES_TARGET
)) {
153 bool shortcut_existed
= PathExists(shortcut_path
);
155 // Interfaces to the old shortcut when replacing an existing shortcut.
156 ScopedComPtr
<IShellLink
> old_i_shell_link
;
157 ScopedComPtr
<IPersistFile
> old_i_persist_file
;
159 // Interfaces to the shortcut being created/updated.
160 ScopedComPtr
<IShellLink
> i_shell_link
;
161 ScopedComPtr
<IPersistFile
> i_persist_file
;
163 case SHORTCUT_CREATE_ALWAYS
:
164 InitializeShortcutInterfaces(NULL
, &i_shell_link
, &i_persist_file
);
166 case SHORTCUT_UPDATE_EXISTING
:
167 InitializeShortcutInterfaces(shortcut_path
.value().c_str(), &i_shell_link
,
170 case SHORTCUT_REPLACE_EXISTING
:
171 InitializeShortcutInterfaces(shortcut_path
.value().c_str(),
172 &old_i_shell_link
, &old_i_persist_file
);
173 // Confirm |shortcut_path| exists and is a shortcut by verifying
174 // |old_i_persist_file| was successfully initialized in the call above. If
175 // so, initialize the interfaces to begin writing a new shortcut (to
176 // overwrite the current one if successful).
177 if (old_i_persist_file
.get())
178 InitializeShortcutInterfaces(NULL
, &i_shell_link
, &i_persist_file
);
184 // Return false immediately upon failure to initialize shortcut interfaces.
185 if (!i_persist_file
.get())
188 if ((properties
.options
& ShortcutProperties::PROPERTIES_TARGET
) &&
189 FAILED(i_shell_link
->SetPath(properties
.target
.value().c_str()))) {
193 if ((properties
.options
& ShortcutProperties::PROPERTIES_WORKING_DIR
) &&
194 FAILED(i_shell_link
->SetWorkingDirectory(
195 properties
.working_dir
.value().c_str()))) {
199 if (properties
.options
& ShortcutProperties::PROPERTIES_ARGUMENTS
) {
200 if (FAILED(i_shell_link
->SetArguments(properties
.arguments
.c_str())))
202 } else if (old_i_persist_file
.get()) {
203 wchar_t current_arguments
[MAX_PATH
] = {0};
204 if (SUCCEEDED(old_i_shell_link
->GetArguments(current_arguments
,
206 i_shell_link
->SetArguments(current_arguments
);
210 if ((properties
.options
& ShortcutProperties::PROPERTIES_DESCRIPTION
) &&
211 FAILED(i_shell_link
->SetDescription(properties
.description
.c_str()))) {
215 if ((properties
.options
& ShortcutProperties::PROPERTIES_ICON
) &&
216 FAILED(i_shell_link
->SetIconLocation(properties
.icon
.value().c_str(),
217 properties
.icon_index
))) {
222 (properties
.options
& ShortcutProperties::PROPERTIES_APP_ID
) != 0;
224 (properties
.options
& ShortcutProperties::PROPERTIES_DUAL_MODE
) != 0;
225 if ((has_app_id
|| has_dual_mode
) &&
226 GetVersion() >= VERSION_WIN7
) {
227 ScopedComPtr
<IPropertyStore
> property_store
;
228 if (FAILED(property_store
.QueryFrom(i_shell_link
.get())) ||
229 !property_store
.get())
233 !SetAppIdForPropertyStore(property_store
.get(),
234 properties
.app_id
.c_str())) {
238 !SetBooleanValueForPropertyStore(property_store
.get(),
239 PKEY_AppUserModel_IsDualMode
,
240 properties
.dual_mode
)) {
245 // Release the interfaces to the old shortcut to make sure it doesn't prevent
246 // overwriting it if needed.
247 old_i_persist_file
.Release();
248 old_i_shell_link
.Release();
250 HRESULT result
= i_persist_file
->Save(shortcut_path
.value().c_str(), TRUE
);
252 // Release the interfaces in case the SHChangeNotify call below depends on
253 // the operations above being fully completed.
254 i_persist_file
.Release();
255 i_shell_link
.Release();
257 // If we successfully created/updated the icon, notify the shell that we have
259 const bool succeeded
= SUCCEEDED(result
);
261 if (shortcut_existed
) {
262 // TODO(gab): SHCNE_UPDATEITEM might be sufficient here; further testing
264 SHChangeNotify(SHCNE_ASSOCCHANGED
, SHCNF_IDLIST
, NULL
, NULL
);
266 SHChangeNotify(SHCNE_CREATE
, SHCNF_PATH
, shortcut_path
.value().c_str(),
274 bool ResolveShortcutProperties(const FilePath
& shortcut_path
,
276 ShortcutProperties
* properties
) {
277 DCHECK(options
&& properties
);
278 base::ThreadRestrictions::AssertIOAllowed();
280 if (options
& ~ShortcutProperties::PROPERTIES_ALL
)
281 NOTREACHED() << "Unhandled property is used.";
283 ScopedComPtr
<IShellLink
> i_shell_link
;
285 // Get pointer to the IShellLink interface.
286 if (FAILED(i_shell_link
.CreateInstance(CLSID_ShellLink
, NULL
,
287 CLSCTX_INPROC_SERVER
))) {
291 ScopedComPtr
<IPersistFile
> persist
;
292 // Query IShellLink for the IPersistFile interface.
293 if (FAILED(persist
.QueryFrom(i_shell_link
.get())))
296 // Load the shell link.
297 if (FAILED(persist
->Load(shortcut_path
.value().c_str(), STGM_READ
)))
300 // Reset |properties|.
301 properties
->options
= 0;
303 wchar_t temp
[MAX_PATH
];
304 if (options
& ShortcutProperties::PROPERTIES_TARGET
) {
305 if (FAILED(i_shell_link
->GetPath(temp
, MAX_PATH
, NULL
, SLGP_UNCPRIORITY
)))
307 properties
->set_target(FilePath(temp
));
310 if (options
& ShortcutProperties::PROPERTIES_WORKING_DIR
) {
311 if (FAILED(i_shell_link
->GetWorkingDirectory(temp
, MAX_PATH
)))
313 properties
->set_working_dir(FilePath(temp
));
316 if (options
& ShortcutProperties::PROPERTIES_ARGUMENTS
) {
317 if (FAILED(i_shell_link
->GetArguments(temp
, MAX_PATH
)))
319 properties
->set_arguments(temp
);
322 if (options
& ShortcutProperties::PROPERTIES_DESCRIPTION
) {
323 // Note: description length constrained by MAX_PATH.
324 if (FAILED(i_shell_link
->GetDescription(temp
, MAX_PATH
)))
326 properties
->set_description(temp
);
329 if (options
& ShortcutProperties::PROPERTIES_ICON
) {
331 if (FAILED(i_shell_link
->GetIconLocation(temp
, MAX_PATH
, &temp_index
)))
333 properties
->set_icon(FilePath(temp
), temp_index
);
336 // Windows 7+ options, avoiding unnecessary work.
337 if ((options
& ShortcutProperties::PROPERTIES_WIN7
) &&
338 GetVersion() >= VERSION_WIN7
) {
339 ScopedComPtr
<IPropertyStore
> property_store
;
340 if (FAILED(property_store
.QueryFrom(i_shell_link
.get())))
343 if (options
& ShortcutProperties::PROPERTIES_APP_ID
) {
344 ScopedPropVariant pv_app_id
;
345 if (property_store
->GetValue(PKEY_AppUserModel_ID
,
346 pv_app_id
.Receive()) != S_OK
) {
349 switch (pv_app_id
.get().vt
) {
351 properties
->set_app_id(L
"");
354 properties
->set_app_id(pv_app_id
.get().pwszVal
);
357 NOTREACHED() << "Unexpected variant type: " << pv_app_id
.get().vt
;
362 if (options
& ShortcutProperties::PROPERTIES_DUAL_MODE
) {
363 ScopedPropVariant pv_dual_mode
;
364 if (property_store
->GetValue(PKEY_AppUserModel_IsDualMode
,
365 pv_dual_mode
.Receive()) != S_OK
) {
368 switch (pv_dual_mode
.get().vt
) {
370 properties
->set_dual_mode(false);
373 properties
->set_dual_mode(pv_dual_mode
.get().boolVal
== VARIANT_TRUE
);
376 NOTREACHED() << "Unexpected variant type: " << pv_dual_mode
.get().vt
;
385 bool ResolveShortcut(const FilePath
& shortcut_path
,
386 FilePath
* target_path
,
390 options
|= ShortcutProperties::PROPERTIES_TARGET
;
392 options
|= ShortcutProperties::PROPERTIES_ARGUMENTS
;
395 ShortcutProperties properties
;
396 if (!ResolveShortcutProperties(shortcut_path
, options
, &properties
))
400 *target_path
= properties
.target
;
402 *args
= properties
.arguments
;
406 bool TaskbarPinShortcutLink(const FilePath
& shortcut
) {
407 base::ThreadRestrictions::AssertIOAllowed();
409 // "Pin to taskbar" is only supported after Win7.
410 if (GetVersion() < VERSION_WIN7
)
413 return DoVerbOnFile(kPinToTaskbarID
, shortcut
);
416 bool TaskbarUnpinShortcutLink(const FilePath
& shortcut
) {
417 base::ThreadRestrictions::AssertIOAllowed();
419 // "Unpin from taskbar" is only supported after Win7.
420 if (GetVersion() < VERSION_WIN7
)
423 return DoVerbOnFile(kUnpinFromTaskbarID
, shortcut
);