Skip to content

Commit 5d88094

Browse files
committed
Mirror the ShellLinkFile setup for InternetShortcutFile.
1 parent cd38e56 commit 5d88094

5 files changed

Lines changed: 367 additions & 0 deletions

File tree

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
using System.Runtime.InteropServices;
3+
using Windows.Win32.UI.Shell;
4+
5+
namespace PSADT.Interop.ComTypes
6+
{
7+
/// <summary>
8+
/// Provides Unicode methods for setting and retrieving URLs in an Internet shortcut.
9+
/// </summary>
10+
[ComImport, Guid("CABB0DA1-DA57-11CF-9974-0020AFD79762")]
11+
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
12+
[CoClass(typeof(InternetShortcut))]
13+
[SuppressMessage("Interoperability", "SYSLIB1096:Convert to 'GeneratedComInterface'", Justification = "GeneratedComInterfaceAttribute is not available in .NET Framework 4.7.2.")]
14+
internal interface IUniformResourceLocatorW
15+
{
16+
/// <summary>
17+
/// Sets the URL of the Internet shortcut.
18+
/// </summary>
19+
/// <param name="pcszURL">The URL string to set.</param>
20+
/// <param name="dwInFlags">Flags that control URL validation and canonicalization.</param>
21+
void SetURL([MarshalAs(UnmanagedType.LPWStr)] string pcszURL, IURL_SETURL_FLAGS dwInFlags);
22+
23+
/// <summary>
24+
/// Gets the URL of the Internet shortcut.
25+
/// </summary>
26+
/// <param name="ppszURL">Receives the URL string. The caller must free this memory using CoTaskMemFree.</param>
27+
void GetURL([MarshalAs(UnmanagedType.LPWStr)] out string? ppszURL);
28+
29+
/// <summary>
30+
/// Invokes a command on the URL (e.g., opens the URL in a browser).
31+
/// </summary>
32+
/// <param name="purlici">A reference to a <see cref="URLINVOKECOMMANDINFOW"/> structure specifying the command to invoke.</param>
33+
void InvokeCommand(in URLINVOKECOMMANDINFOW purlici);
34+
}
35+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using System.Runtime.InteropServices;
2+
3+
namespace PSADT.Interop.ComTypes
4+
{
5+
/// <summary>
6+
/// The InternetShortcut coclass for creating and manipulating .url files.
7+
/// </summary>
8+
[ComImport, Guid("FBF23B40-E3F0-101B-8488-00AA003E56F8")]
9+
[ClassInterface(ClassInterfaceType.None)]
10+
internal class InternetShortcut
11+
{
12+
}
13+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
using System;
2+
3+
namespace PSADT.Interop
4+
{
5+
/// <summary>
6+
/// Defines flags that specify how a command should be invoked on a URL.
7+
/// </summary>
8+
/// <remarks>These flags are used with the IURL_INVOKECOMMAND method to control the behavior of the
9+
/// command invocation, such as allowing user interface interaction, using the default verb, waiting for DDE
10+
/// conversations, enabling asynchronous execution, and logging usage for telemetry purposes.</remarks>
11+
[System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1707:Identifiers should not contain underscores", Justification = "This is how they're named in the Win32 API.")]
12+
[Flags]
13+
public enum IURL_INVOKECOMMAND_FLAGS
14+
{
15+
/// <summary>
16+
/// Specifies that the command can display a user interface when invoked.
17+
/// </summary>
18+
/// <remarks>This flag is used in conjunction with the IURL_INVOKECOMMAND method to indicate that
19+
/// the command may require user interaction.</remarks>
20+
IURL_INVOKECOMMAND_FL_ALLOW_UI = Windows.Win32.UI.Shell.IURL_INVOKECOMMAND_FLAGS.IURL_INVOKECOMMAND_FL_ALLOW_UI,
21+
22+
/// <summary>
23+
/// Specifies that the default verb should be used when invoking a command on a URL.
24+
/// </summary>
25+
/// <remarks>This value is part of the IURL_INVOKECOMMAND_FLAGS enumeration and indicates that the
26+
/// default action associated with the URL will be executed. Use this flag when you want to perform the standard
27+
/// operation defined for the URL, such as opening it in a browser or launching the associated
28+
/// application.</remarks>
29+
IURL_INVOKECOMMAND_FL_USE_DEFAULT_VERB = Windows.Win32.UI.Shell.IURL_INVOKECOMMAND_FLAGS.IURL_INVOKECOMMAND_FL_USE_DEFAULT_VERB,
30+
31+
/// <summary>
32+
/// Specifies that the command should wait for a Dynamic Data Exchange (DDE) conversation to complete before
33+
/// returning control to the caller.
34+
/// </summary>
35+
/// <remarks>Use this flag with the IURL_INVOKECOMMAND method to ensure synchronous execution.
36+
/// When set, the caller will not proceed until the DDE conversation has finished, which may be necessary when
37+
/// interacting with applications that require DDE communication.</remarks>
38+
IURL_INVOKECOMMAND_FL_DDEWAIT = Windows.Win32.UI.Shell.IURL_INVOKECOMMAND_FLAGS.IURL_INVOKECOMMAND_FL_DDEWAIT,
39+
40+
/// <summary>
41+
/// Specifies that the command can be invoked asynchronously without blocking the calling thread.
42+
/// </summary>
43+
/// <remarks>Use this flag to indicate that the operation may be performed in a non-blocking
44+
/// manner, allowing for asynchronous execution. This is useful when the command might take a significant amount
45+
/// of time to complete and you want to maintain responsiveness in the calling application.</remarks>
46+
IURL_INVOKECOMMAND_FL_ASYNCOK = Windows.Win32.UI.Shell.IURL_INVOKECOMMAND_FLAGS.IURL_INVOKECOMMAND_FL_ASYNCOK,
47+
48+
/// <summary>
49+
/// Records the usage of the command for telemetry purposes, allowing for analysis of how often and in what contexts the command is invoked.
50+
/// </summary>
51+
IURL_INVOKECOMMAND_FL_LOG_USAGE = Windows.Win32.UI.Shell.IURL_INVOKECOMMAND_FLAGS.IURL_INVOKECOMMAND_FL_LOG_USAGE,
52+
}
53+
}

src/PSADT/PSADT.Interop/NativeMethods.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,8 @@ IsProcessInJob
131131
IsWindowEnabled
132132
IsWindowVisible
133133
ITaskService
134+
IURL_INVOKECOMMAND_FLAGS
135+
IURL_SETURL_FLAGS
134136
JOB_OBJECT_MSG_ABNORMAL_EXIT_PROCESS
135137
JOB_OBJECT_MSG_ACTIVE_PROCESS_LIMIT
136138
JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO
@@ -2107,6 +2109,7 @@ TOKEN_USER
21072109
TreeResetNamedSecurityInfo
21082110
UNICODE_STRING
21092111
UpdateProcThreadAttribute
2112+
URLINVOKECOMMANDINFOW
21102113
VARENUM
21112114
VER_NT_DOMAIN_CONTROLLER
21122115
VER_NT_SERVER
Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
using System;
2+
using System.IO;
3+
using System.Runtime.CompilerServices;
4+
using System.Runtime.InteropServices;
5+
using PSADT.Interop.ComTypes;
6+
using PSADT.Interop.Extensions;
7+
using PSADT.Interop.SafeHandles;
8+
using Windows.Win32;
9+
using Windows.Win32.Foundation;
10+
using Windows.Win32.System.Com;
11+
using Windows.Win32.UI.Shell;
12+
13+
namespace PSADT.ShortcutManagement
14+
{
15+
/// <summary>
16+
/// Provides a managed wrapper around the Windows InternetShortcut COM interface.
17+
/// This class enables creating, loading, modifying, and saving Internet shortcut (.url) files.
18+
/// </summary>
19+
/// <remarks>
20+
/// This class wraps the <c>IUniformResourceLocatorW</c> and <c>IPersistFile</c> COM interfaces
21+
/// to provide access to URL shortcut properties.
22+
/// </remarks>
23+
public sealed class InternetShortcutFile : IDisposable
24+
{
25+
/// <summary>
26+
/// Creates a new, empty Internet shortcut.
27+
/// </summary>
28+
/// <returns>A new <see cref="InternetShortcutFile"/> instance.</returns>
29+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
30+
public static InternetShortcutFile New()
31+
{
32+
return new();
33+
}
34+
35+
/// <summary>
36+
/// Creates a new Internet shortcut with the specified URL.
37+
/// </summary>
38+
/// <param name="url">The URL for the shortcut.</param>
39+
/// <returns>A new <see cref="InternetShortcutFile"/> instance with the URL set.</returns>
40+
public static InternetShortcutFile New(Uri url)
41+
{
42+
ArgumentNullException.ThrowIfNull(url);
43+
return new() { Url = url };
44+
}
45+
46+
/// <summary>
47+
/// Loads an existing Internet shortcut file in read-only mode.
48+
/// </summary>
49+
/// <param name="filePath">The path to the shortcut file to load.</param>
50+
/// <returns>A new <see cref="InternetShortcutFile"/> instance loaded from the specified file.</returns>
51+
/// <exception cref="ArgumentNullException">Thrown when <paramref name="filePath"/> is null.</exception>
52+
/// <exception cref="ArgumentException">Thrown when <paramref name="filePath"/> is empty or whitespace.</exception>
53+
/// <exception cref="FileNotFoundException">Thrown when the specified file does not exist.</exception>
54+
/// <exception cref="COMException">Thrown when the COM operation fails.</exception>
55+
/// <remarks>Use <see cref="Load(string, Interop.STGM)"/> with <see cref="Interop.STGM.STGM_READWRITE"/> if you need to modify the shortcut.</remarks>
56+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
57+
public static InternetShortcutFile Load(string filePath)
58+
{
59+
return Load(filePath, Interop.STGM.STGM_READ);
60+
}
61+
62+
/// <summary>
63+
/// Loads an existing Internet shortcut file with the specified storage mode.
64+
/// </summary>
65+
/// <param name="filePath">The path to the shortcut file to load.</param>
66+
/// <param name="storageMode">The storage mode flags.</param>
67+
/// <returns>A new <see cref="InternetShortcutFile"/> instance loaded from the specified file.</returns>
68+
/// <exception cref="ArgumentNullException">Thrown when <paramref name="filePath"/> is null.</exception>
69+
/// <exception cref="ArgumentException">Thrown when <paramref name="filePath"/> is empty or whitespace.</exception>
70+
/// <exception cref="FileNotFoundException">Thrown when the specified file does not exist.</exception>
71+
/// <exception cref="COMException">Thrown when the COM operation fails.</exception>
72+
public static InternetShortcutFile Load(string filePath, Interop.STGM storageMode)
73+
{
74+
ArgumentException.ThrowIfNullOrWhiteSpace(filePath);
75+
if (!File.Exists(filePath))
76+
{
77+
throw new FileNotFoundException("The specified shortcut file does not exist.", filePath);
78+
}
79+
InternetShortcutFile internetShortcut = new();
80+
try
81+
{
82+
((IPersistFile)internetShortcut._internetShortcut).Load(filePath, (STGM)storageMode);
83+
return internetShortcut;
84+
}
85+
catch
86+
{
87+
internetShortcut.Dispose();
88+
throw;
89+
}
90+
}
91+
92+
/// <summary>
93+
/// Initializes a new instance of the <see cref="InternetShortcutFile"/> class.
94+
/// </summary>
95+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
96+
private InternetShortcutFile()
97+
{
98+
_internetShortcut = new IUniformResourceLocatorW();
99+
}
100+
101+
/// <summary>
102+
/// Finalizes an instance of the <see cref="InternetShortcutFile"/> class.
103+
/// </summary>
104+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
105+
~InternetShortcutFile()
106+
{
107+
Dispose(false);
108+
}
109+
110+
/// <summary>
111+
/// Saves the Internet shortcut to the currently loaded file path.
112+
/// </summary>
113+
/// <exception cref="InvalidOperationException">Thrown when no file path has been set.</exception>
114+
/// <exception cref="COMException">Thrown when the COM operation fails.</exception>
115+
public void Save()
116+
{
117+
ObjectDisposedException.ThrowIf(_disposed, this);
118+
string? currentFile = FilePath;
119+
if (string.IsNullOrWhiteSpace(currentFile))
120+
{
121+
throw new InvalidOperationException("No file path has been set. Use Save(string) to specify a path.");
122+
}
123+
Save(currentFile!);
124+
}
125+
126+
/// <summary>
127+
/// Saves the Internet shortcut to the specified file path.
128+
/// </summary>
129+
/// <param name="filePath">The path where the shortcut file should be saved.</param>
130+
/// <exception cref="ArgumentNullException">Thrown when <paramref name="filePath"/> is null.</exception>
131+
/// <exception cref="ArgumentException">Thrown when <paramref name="filePath"/> is empty or whitespace.</exception>
132+
/// <exception cref="COMException">Thrown when the COM operation fails.</exception>
133+
public void Save(string filePath)
134+
{
135+
ObjectDisposedException.ThrowIf(_disposed, this);
136+
ArgumentException.ThrowIfNullOrWhiteSpace(filePath);
137+
((IPersistFile)_internetShortcut).Save(filePath, true);
138+
}
139+
140+
/// <summary>
141+
/// Gets the path of the currently loaded shortcut file.
142+
/// </summary>
143+
/// <value>The full path to the shortcut file, or <see langword="null"/> if no file has been loaded or saved.</value>
144+
public string? FilePath
145+
{
146+
get
147+
{
148+
ObjectDisposedException.ThrowIf(_disposed, this);
149+
((IPersistFile)_internetShortcut).GetCurFile(out SafeCoTaskMemHandle? ppszFileName);
150+
using (ppszFileName)
151+
{
152+
return ppszFileName?.ToStringUni();
153+
}
154+
}
155+
}
156+
157+
/// <summary>
158+
/// Gets or sets the URL of the Internet shortcut.
159+
/// </summary>
160+
/// <value>The URL that the shortcut points to.</value>
161+
/// <exception cref="COMException">Thrown when the COM operation fails.</exception>
162+
public Uri? Url
163+
{
164+
get
165+
{
166+
ObjectDisposedException.ThrowIf(_disposed, this);
167+
_internetShortcut.GetURL(out string? url);
168+
return url is not null ? new(url) : null;
169+
}
170+
set
171+
{
172+
ObjectDisposedException.ThrowIf(_disposed, this);
173+
ArgumentNullException.ThrowIfNull(value);
174+
_internetShortcut.SetURL(value.AbsoluteUri, 0);
175+
}
176+
}
177+
178+
/// <summary>
179+
/// Opens the URL using the default handler.
180+
/// </summary>
181+
/// <exception cref="COMException">Thrown when the COM operation fails.</exception>
182+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
183+
public void Invoke()
184+
{
185+
Invoke(null, default, Interop.IURL_INVOKECOMMAND_FLAGS.IURL_INVOKECOMMAND_FL_USE_DEFAULT_VERB);
186+
}
187+
188+
/// <summary>
189+
/// Opens the URL using the specified verb.
190+
/// </summary>
191+
/// <param name="verb">The verb to invoke (e.g., "open"). Pass <see langword="null"/> for the default verb.</param>
192+
/// <exception cref="COMException">Thrown when the COM operation fails.</exception>
193+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
194+
public void Invoke(string? verb)
195+
{
196+
Invoke(verb, default, verb is null ? Interop.IURL_INVOKECOMMAND_FLAGS.IURL_INVOKECOMMAND_FL_USE_DEFAULT_VERB : 0);
197+
}
198+
199+
/// <summary>
200+
/// Opens the URL using the specified verb and options.
201+
/// </summary>
202+
/// <param name="verb">The verb to invoke (e.g., "open"). Pass <see langword="null"/> for the default verb.</param>
203+
/// <param name="hwndParent">A handle to the parent window for any UI that may be displayed.</param>
204+
/// <param name="flags">Flags that control the invocation behavior.</param>
205+
/// <exception cref="COMException">Thrown when the COM operation fails.</exception>
206+
public void Invoke(string? verb, nint hwndParent, Interop.IURL_INVOKECOMMAND_FLAGS flags)
207+
{
208+
ObjectDisposedException.ThrowIf(_disposed, this);
209+
unsafe
210+
{
211+
fixed (char* pVerb = verb)
212+
{
213+
URLINVOKECOMMANDINFOW commandInfo = new()
214+
{
215+
dwcbSize = (uint)sizeof(URLINVOKECOMMANDINFOW),
216+
dwFlags = (uint)flags,
217+
hwndParent = (HWND)hwndParent,
218+
pcszVerb = pVerb
219+
};
220+
_internetShortcut.InvokeCommand(in commandInfo);
221+
}
222+
}
223+
}
224+
225+
/// <summary>
226+
/// Releases all resources used by the <see cref="InternetShortcutFile"/>.
227+
/// </summary>
228+
public void Dispose()
229+
{
230+
Dispose(true);
231+
GC.SuppressFinalize(this);
232+
}
233+
234+
/// <summary>
235+
/// Releases the unmanaged resources used by the <see cref="InternetShortcutFile"/> and optionally releases the managed resources.
236+
/// </summary>
237+
/// <param name="disposing"><see langword="true"/> to release both managed and unmanaged resources; <see langword="false"/> to release only unmanaged resources.</param>
238+
private void Dispose(bool disposing)
239+
{
240+
if (!_disposed)
241+
{
242+
if (disposing)
243+
{
244+
if (_internetShortcut != null)
245+
{
246+
_ = Marshal.FinalReleaseComObject(_internetShortcut);
247+
}
248+
}
249+
_disposed = true;
250+
}
251+
}
252+
253+
/// <summary>
254+
/// Indicates whether the object has been disposed.
255+
/// </summary>
256+
private bool _disposed;
257+
258+
/// <summary>
259+
/// The underlying IUniformResourceLocatorW COM object.
260+
/// </summary>
261+
private readonly IUniformResourceLocatorW _internetShortcut;
262+
}
263+
}

0 commit comments

Comments
 (0)