|
|
@ -6,6 +6,7 @@
|
|
|
|
# Based on: http://powershellblogger.com/2016/01/create-shortcuts-lnk-or-url-files-with-powershell/
|
|
|
|
# Based on: http://powershellblogger.com/2016/01/create-shortcuts-lnk-or-url-files-with-powershell/
|
|
|
|
|
|
|
|
|
|
|
|
#AnsibleRequires -CSharpUtil Ansible.Basic
|
|
|
|
#AnsibleRequires -CSharpUtil Ansible.Basic
|
|
|
|
|
|
|
|
#Requires -Module Ansible.ModuleUtils.AddType
|
|
|
|
|
|
|
|
|
|
|
|
$spec = @{
|
|
|
|
$spec = @{
|
|
|
|
options = @{
|
|
|
|
options = @{
|
|
|
@ -18,6 +19,7 @@ $spec = @{
|
|
|
|
icon = @{ type='path' }
|
|
|
|
icon = @{ type='path' }
|
|
|
|
description = @{ type='str' }
|
|
|
|
description = @{ type='str' }
|
|
|
|
windowstyle = @{ type='str'; choices=@( 'maximized', 'minimized', 'normal' ) }
|
|
|
|
windowstyle = @{ type='str'; choices=@( 'maximized', 'minimized', 'normal' ) }
|
|
|
|
|
|
|
|
run_as_admin = @{ type='bool'; default=$false }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
supports_check_mode = $true
|
|
|
|
supports_check_mode = $true
|
|
|
|
}
|
|
|
|
}
|
|
|
@ -33,6 +35,7 @@ $hotkey = $module.Params.hotkey
|
|
|
|
$icon = $module.Params.icon
|
|
|
|
$icon = $module.Params.icon
|
|
|
|
$description = $module.Params.description
|
|
|
|
$description = $module.Params.description
|
|
|
|
$windowstyle = $module.Params.windowstyle
|
|
|
|
$windowstyle = $module.Params.windowstyle
|
|
|
|
|
|
|
|
$run_as_admin = $module.Params.run_as_admin
|
|
|
|
|
|
|
|
|
|
|
|
# Expand environment variables on non-path types
|
|
|
|
# Expand environment variables on non-path types
|
|
|
|
if ($null -ne $src) {
|
|
|
|
if ($null -ne $src) {
|
|
|
@ -45,9 +48,192 @@ if ($null -ne $description) {
|
|
|
|
$description = [System.Environment]::ExpandEnvironmentVariables($description)
|
|
|
|
$description = [System.Environment]::ExpandEnvironmentVariables($description)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
$module.Result.changed = $false
|
|
|
|
$module.Result.dest = $dest
|
|
|
|
$module.Result.dest = $dest
|
|
|
|
$module.Result.state = $state
|
|
|
|
$module.Result.state = $state
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# TODO: look at consolidating other COM actions into the C# class for future compatibility
|
|
|
|
|
|
|
|
Add-CSharpType -AnsibleModule $module -References @'
|
|
|
|
|
|
|
|
using System;
|
|
|
|
|
|
|
|
using System.Runtime.InteropServices;
|
|
|
|
|
|
|
|
using System.Runtime.InteropServices.ComTypes;
|
|
|
|
|
|
|
|
using System.Text;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
namespace Ansible.Shortcut
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
[ComImport()]
|
|
|
|
|
|
|
|
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
|
|
|
|
|
|
[Guid("000214F9-0000-0000-C000-000000000046")]
|
|
|
|
|
|
|
|
internal interface IShellLinkW
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
// We only care about GetPath and GetIDList, omit the other methods for now
|
|
|
|
|
|
|
|
void GetPath(StringBuilder pszFile, int cch, IntPtr pfd, UInt32 fFlags);
|
|
|
|
|
|
|
|
void GetIDList(out IntPtr ppidl);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
[ComImport()]
|
|
|
|
|
|
|
|
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
|
|
|
|
|
|
|
[Guid("45E2b4AE-B1C3-11D0-B92F-00A0C90312E1")]
|
|
|
|
|
|
|
|
internal interface IShellLinkDataList
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
void AddDataBlock(IntPtr pDataBlock);
|
|
|
|
|
|
|
|
void CopyDataBlock(uint dwSig, out IntPtr ppDataBlock);
|
|
|
|
|
|
|
|
void RemoveDataBlock(uint dwSig);
|
|
|
|
|
|
|
|
void GetFlags(out ShellLinkFlags dwFlags);
|
|
|
|
|
|
|
|
void SetFlags(ShellLinkFlags dwFlags);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
internal class NativeHelpers
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
|
|
|
|
|
|
|
public struct SHFILEINFO
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
public IntPtr hIcon;
|
|
|
|
|
|
|
|
public int iIcon;
|
|
|
|
|
|
|
|
public UInt32 dwAttributes;
|
|
|
|
|
|
|
|
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 260)] public char[] szDisplayName;
|
|
|
|
|
|
|
|
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 80)] public char[] szTypeName;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
internal class NativeMethods
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
[DllImport("shell32.dll")]
|
|
|
|
|
|
|
|
public static extern void ILFree(
|
|
|
|
|
|
|
|
IntPtr pidl);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
[DllImport("shell32.dll")]
|
|
|
|
|
|
|
|
public static extern IntPtr SHGetFileInfoW(
|
|
|
|
|
|
|
|
IntPtr pszPath,
|
|
|
|
|
|
|
|
UInt32 dwFileAttributes,
|
|
|
|
|
|
|
|
ref NativeHelpers.SHFILEINFO psfi,
|
|
|
|
|
|
|
|
int sbFileInfo,
|
|
|
|
|
|
|
|
UInt32 uFlags);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
[DllImport("shell32.dll")]
|
|
|
|
|
|
|
|
public static extern int SHParseDisplayName(
|
|
|
|
|
|
|
|
[MarshalAs(UnmanagedType.LPWStr)] string pszName,
|
|
|
|
|
|
|
|
IntPtr pbc,
|
|
|
|
|
|
|
|
out IntPtr ppidl,
|
|
|
|
|
|
|
|
UInt32 sfagoIn,
|
|
|
|
|
|
|
|
out UInt32 psfgaoOut);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
[System.Flags]
|
|
|
|
|
|
|
|
public enum ShellLinkFlags : uint
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
Default = 0x00000000,
|
|
|
|
|
|
|
|
HasIdList = 0x00000001,
|
|
|
|
|
|
|
|
HasLinkInfo = 0x00000002,
|
|
|
|
|
|
|
|
HasName = 0x00000004,
|
|
|
|
|
|
|
|
HasRelPath = 0x00000008,
|
|
|
|
|
|
|
|
HasWorkingDir = 0x00000010,
|
|
|
|
|
|
|
|
HasArgs = 0x00000020,
|
|
|
|
|
|
|
|
HasIconLocation = 0x00000040,
|
|
|
|
|
|
|
|
Unicode = 0x00000080,
|
|
|
|
|
|
|
|
ForceNoLinkInfo = 0x00000100,
|
|
|
|
|
|
|
|
HasExpSz = 0x00000200,
|
|
|
|
|
|
|
|
RunInSeparate = 0x00000400,
|
|
|
|
|
|
|
|
HasLogo3Id = 0x00000800,
|
|
|
|
|
|
|
|
HasDarwinId = 0x00001000,
|
|
|
|
|
|
|
|
RunAsUser = 0x00002000,
|
|
|
|
|
|
|
|
HasExpIconSz = 0x00004000,
|
|
|
|
|
|
|
|
NoPidlAlias = 0x00008000,
|
|
|
|
|
|
|
|
ForceUncName = 0x00010000,
|
|
|
|
|
|
|
|
RunWithShimLayer = 0x00020000,
|
|
|
|
|
|
|
|
ForceNoLinkTrack = 0x00040000,
|
|
|
|
|
|
|
|
EnableTargetMetadata = 0x00080000,
|
|
|
|
|
|
|
|
DisableLinkPathTracking = 0x00100000,
|
|
|
|
|
|
|
|
DisableKnownFolderRelativeTracking = 0x00200000,
|
|
|
|
|
|
|
|
NoKfAlias = 0x00400000,
|
|
|
|
|
|
|
|
AllowLinkToLink = 0x00800000,
|
|
|
|
|
|
|
|
UnAliasOnSave = 0x01000000,
|
|
|
|
|
|
|
|
PreferEnvironmentPath = 0x02000000,
|
|
|
|
|
|
|
|
KeepLocalIdListForUncTarget = 0x04000000,
|
|
|
|
|
|
|
|
PersistVolumeIdToRelative = 0x08000000,
|
|
|
|
|
|
|
|
Valid = 0x0FFFF7FF,
|
|
|
|
|
|
|
|
Reserved = 0x80000000
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public class ShellLink
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
private static Guid CLSID_ShellLink = new Guid("00021401-0000-0000-C000-000000000046");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public static ShellLinkFlags GetFlags(string path)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
IShellLinkW link = InitialiseObj(path);
|
|
|
|
|
|
|
|
ShellLinkFlags dwFlags;
|
|
|
|
|
|
|
|
((IShellLinkDataList)link).GetFlags(out dwFlags);
|
|
|
|
|
|
|
|
return dwFlags;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public static void SetFlags(string path, ShellLinkFlags flags)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
IShellLinkW link = InitialiseObj(path);
|
|
|
|
|
|
|
|
((IShellLinkDataList)link).SetFlags(flags);
|
|
|
|
|
|
|
|
((IPersistFile)link).Save(null, false);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public static string GetTargetPath(string path)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
IShellLinkW link = InitialiseObj(path);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
StringBuilder pathSb = new StringBuilder(260);
|
|
|
|
|
|
|
|
link.GetPath(pathSb, pathSb.Capacity, IntPtr.Zero, 0);
|
|
|
|
|
|
|
|
string linkPath = pathSb.ToString();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// If the path wasn't set, try and get the path from the ItemIDList
|
|
|
|
|
|
|
|
ShellLinkFlags flags = GetFlags(path);
|
|
|
|
|
|
|
|
if (String.IsNullOrEmpty(linkPath) && ((uint)flags & (uint)ShellLinkFlags.HasIdList) == (uint)ShellLinkFlags.HasIdList)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
IntPtr idList = IntPtr.Zero;
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
link.GetIDList(out idList);
|
|
|
|
|
|
|
|
linkPath = GetDisplayNameFromPidl(idList);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
finally
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
NativeMethods.ILFree(idList);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return linkPath;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public static string GetDisplayNameFromPath(string path)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
UInt32 sfgaoOut;
|
|
|
|
|
|
|
|
IntPtr pidl = IntPtr.Zero;
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
int res = NativeMethods.SHParseDisplayName(path, IntPtr.Zero, out pidl, 0, out sfgaoOut);
|
|
|
|
|
|
|
|
Marshal.ThrowExceptionForHR(res);
|
|
|
|
|
|
|
|
return GetDisplayNameFromPidl(pidl);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
finally
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
NativeMethods.ILFree(pidl);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private static string GetDisplayNameFromPidl(IntPtr pidl)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
NativeHelpers.SHFILEINFO shFileInfo = new NativeHelpers.SHFILEINFO();
|
|
|
|
|
|
|
|
UInt32 uFlags = 0x000000208; // SHGFI_DISPLAYNAME | SHGFI_PIDL
|
|
|
|
|
|
|
|
NativeMethods.SHGetFileInfoW(pidl, 0, ref shFileInfo, Marshal.SizeOf(typeof(NativeHelpers.SHFILEINFO)), uFlags);
|
|
|
|
|
|
|
|
return new string(shFileInfo.szDisplayName).TrimEnd('\0');
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private static IShellLinkW InitialiseObj(string path)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
IShellLinkW link = Activator.CreateInstance(Type.GetTypeFromCLSID(CLSID_ShellLink)) as IShellLinkW;
|
|
|
|
|
|
|
|
((IPersistFile)link).Load(path, 0);
|
|
|
|
|
|
|
|
return link;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
'@
|
|
|
|
|
|
|
|
|
|
|
|
# Convert from window style name to window style id
|
|
|
|
# Convert from window style name to window style id
|
|
|
|
$windowstyles = @{
|
|
|
|
$windowstyles = @{
|
|
|
|
normal = 1
|
|
|
|
normal = 1
|
|
|
@ -91,16 +277,31 @@ If ($state -eq "absent") {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
If (($null -ne $src) -and ($ShortCut.TargetPath -ne $src)) {
|
|
|
|
|
|
|
|
$module.Result.changed = $true
|
|
|
|
|
|
|
|
$ShortCut.TargetPath = $src
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
$module.Result.src = $ShortCut.TargetPath
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Determine if we have a WshShortcut or WshUrlShortcut by checking the Arguments property
|
|
|
|
# Determine if we have a WshShortcut or WshUrlShortcut by checking the Arguments property
|
|
|
|
# A WshUrlShortcut objects only consists of a TargetPath property
|
|
|
|
# A WshUrlShortcut objects only consists of a TargetPath property
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
$file_shortcut = $false
|
|
|
|
If (Get-Member -InputObject $ShortCut -Name Arguments) {
|
|
|
|
If (Get-Member -InputObject $ShortCut -Name Arguments) {
|
|
|
|
|
|
|
|
# File ShortCut, compare multiple properties
|
|
|
|
|
|
|
|
$file_shortcut = $true
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
$target_path = $ShortCut.TargetPath
|
|
|
|
|
|
|
|
If (($null -ne $src) -and ($ShortCut.TargetPath -ne $src)) {
|
|
|
|
|
|
|
|
if ((Test-Path -Path $dest) -and (-not $ShortCut.TargetPath)) {
|
|
|
|
|
|
|
|
# If the shortcut already exists but not on the COM object, we
|
|
|
|
|
|
|
|
# are dealing with a shell path like 'shell:RecycleBinFolder'.
|
|
|
|
|
|
|
|
$expanded_src = [Ansible.Shortcut.ShellLink]::GetDisplayNameFromPath($src)
|
|
|
|
|
|
|
|
$actual_src = [Ansible.Shortcut.ShellLink]::GetTargetPath($dest)
|
|
|
|
|
|
|
|
if ($expanded_src -ne $actual_src) {
|
|
|
|
|
|
|
|
$module.Result.changed = $true
|
|
|
|
|
|
|
|
$ShortCut.TargetPath = $src
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
$module.Result.changed = $true
|
|
|
|
|
|
|
|
$ShortCut.TargetPath = $src
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
$target_path = $src
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# This is a full-featured application shortcut !
|
|
|
|
# This is a full-featured application shortcut !
|
|
|
|
If (($null -ne $arguments) -and ($ShortCut.Arguments -ne $arguments)) {
|
|
|
|
If (($null -ne $arguments) -and ($ShortCut.Arguments -ne $arguments)) {
|
|
|
@ -139,7 +340,15 @@ If ($state -eq "absent") {
|
|
|
|
$ShortCut.WindowStyle = $windowstyles.$windowstyle
|
|
|
|
$ShortCut.WindowStyle = $windowstyles.$windowstyle
|
|
|
|
}
|
|
|
|
}
|
|
|
|
$module.Result.windowstyle = $windowstyleids[$ShortCut.WindowStyle]
|
|
|
|
$module.Result.windowstyle = $windowstyleids[$ShortCut.WindowStyle]
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
# URL Shortcut, just compare the TargetPath
|
|
|
|
|
|
|
|
if (($null -ne $src) -and ($ShortCut.TargetPath -ne $src)) {
|
|
|
|
|
|
|
|
$module.Result.changed = $true
|
|
|
|
|
|
|
|
$ShortCut.TargetPath = $src
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
$target_path = $ShortCut.TargetPath
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$module.Result.src = $target_path
|
|
|
|
|
|
|
|
|
|
|
|
If (($module.Result.changed -eq $true) -and ($module.CheckMode -ne $true)) {
|
|
|
|
If (($module.Result.changed -eq $true) -and ($module.CheckMode -ne $true)) {
|
|
|
|
Try {
|
|
|
|
Try {
|
|
|
@ -148,6 +357,18 @@ If ($state -eq "absent") {
|
|
|
|
$module.FailJson("Failed to create shortcut '$dest'. ($($_.Exception.Message))", $_)
|
|
|
|
$module.FailJson("Failed to create shortcut '$dest'. ($($_.Exception.Message))", $_)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if ((Test-Path -Path $dest) -and $file_shortcut) {
|
|
|
|
|
|
|
|
# Only control the run_as_admin flag if using a File Shortcut
|
|
|
|
|
|
|
|
$flags = [Ansible.Shortcut.ShellLink]::GetFlags($dest)
|
|
|
|
|
|
|
|
if ($run_as_admin -and (-not $flags.HasFlag([Ansible.Shortcut.ShellLinkFlags]::RunAsUser))) {
|
|
|
|
|
|
|
|
[Ansible.Shortcut.ShellLink]::SetFlags($dest, ($flags -bor [Ansible.Shortcut.ShellLinkFlags]::RunAsUser))
|
|
|
|
|
|
|
|
$module.Result.changed = $true
|
|
|
|
|
|
|
|
} elseif (-not $run_as_admin -and ($flags.HasFlag([Ansible.Shortcut.ShellLinkFlags]::RunAsUser))) {
|
|
|
|
|
|
|
|
[Ansible.Shortcut.ShellLink]::SetFlags($dest, ($flags -bxor [Ansible.Shortcut.ShellLinkFlags]::RunAsUser))
|
|
|
|
|
|
|
|
$module.Result.changed = $true
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$module.ExitJson()
|
|
|
|
$module.ExitJson()
|
|
|
|