1 #!powershell
2 
3 # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
4 
5 #Requires -Module Ansible.ModuleUtils.Legacy
6 
7 $params = Parse-Args -arguments $args -supports_check_mode $true
8 $check_mode = Get-AnsibleParam -obj $params "_ansible_check_mode" -type 'bool' -default $false
9 $_remote_tmp = Get-AnsibleParam $params "_ansible_remote_tmp" -type "path" -default $env:TMP
10 
11 $location = Get-AnsibleParam -obj $params -name 'location' -type 'str'
12 $format = Get-AnsibleParam -obj $params -name 'format' -type 'str'
13 $unicode_language = Get-AnsibleParam -obj $params -name 'unicode_language' -type 'str'
14 $copy_settings = Get-AnsibleParam -obj $params -name 'copy_settings' -type 'bool' -default $false
15 
16 $result = @{
17     changed = $false
18     restart_required = $false
19 }
20 
21 # This is used to get the format values based on the LCType enum based through. When running Vista/7/2008/200R2
22 $lctype_util = @"
23 using System;
24 using System.Text;
25 using System.Runtime.InteropServices;
26 using System.ComponentModel;
27 
28 namespace Ansible.WinRegion {
29 
30     public class NativeMethods
31     {
32         [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
33         public static extern int GetLocaleInfoEx(
34             String lpLocaleName,
35             UInt32 LCType,
36             StringBuilder lpLCData,
37             int cchData);
38 
39         [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
40         public static extern int GetSystemDefaultLocaleName(
41             IntPtr lpLocaleName,
42             int cchLocaleName);
43 
44         [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
45         public static extern int GetUserDefaultLocaleName(
46             IntPtr lpLocaleName,
47             int cchLocaleName);
48 
49         [DllImport("advapi32.dll", CharSet = CharSet.Unicode)]
50         public static extern int RegLoadKeyW(
51             UInt32 hKey,
52             string lpSubKey,
53             string lpFile);
54 
55         [DllImport("advapi32.dll", CharSet = CharSet.Unicode)]
56         public static extern int RegUnLoadKeyW(
57             UInt32 hKey,
58             string lpSubKey);
59     }
60 
61     public class Win32Exception : System.ComponentModel.Win32Exception
62     {
63         private string _msg;
64         public Win32Exception(string message) : this(Marshal.GetLastWin32Error(), message) { }
65         public Win32Exception(int errorCode, string message) : base(errorCode)
66         {
67             _msg = String.Format("{0} ({1}, Win32ErrorCode {2})", message, base.Message, errorCode);
68         }
69         public override string Message { get { return _msg; } }
70         public static explicit operator Win32Exception(string message) { return new Win32Exception(message); }
71     }
72 
73     public class Hive : IDisposable
74     {
75         private const UInt32 SCOPE = 0x80000003;  // HKU
76         private string hiveKey;
77         private bool loaded = false;
78 
79         public Hive(string hiveKey, string hivePath)
80         {
81             this.hiveKey = hiveKey;
82             int ret = NativeMethods.RegLoadKeyW(SCOPE, hiveKey, hivePath);
83             if (ret != 0)
84                 throw new Win32Exception(ret, String.Format("Failed to load registry hive at {0}", hivePath));
85             loaded = true;
86         }
87 
88         public static void UnloadHive(string hiveKey)
89         {
90             int ret = NativeMethods.RegUnLoadKeyW(SCOPE, hiveKey);
91             if (ret != 0)
92                 throw new Win32Exception(ret, String.Format("Failed to unload registry hive at {0}", hiveKey));
93         }
94 
95         public void Dispose()
96         {
97             if (loaded)
98             {
99                 // Make sure the garbage collector disposes all unused handles and waits until it is complete
100                 GC.Collect();
101                 GC.WaitForPendingFinalizers();
102 
103                 UnloadHive(hiveKey);
104                 loaded = false;
105             }
106             GC.SuppressFinalize(this);
107         }
108         ~Hive() { this.Dispose(); }
109     }
110 
111     public class LocaleHelper {
112         private String Locale;
113 
114         public LocaleHelper(String locale) {
115             Locale = locale;
116         }
117 
118         public String GetValueFromType(UInt32 LCType) {
119             StringBuilder data = new StringBuilder(500);
120             int result = NativeMethods.GetLocaleInfoEx(Locale, LCType, data, 500);
121             if (result == 0)
122                 throw new Win32Exception("Error getting locale info with legacy method");
123 
124             return data.ToString();
125         }
126     }
127 }
128 "@
129 $original_tmp = $env:TMP
130 $env:TMP = $_remote_tmp
131 Add-Type -TypeDefinition $lctype_util
132 $env:TMP = $original_tmp
133 
134 
Get-LastWin32ExceptionMessagenull135 Function Get-LastWin32ExceptionMessage {
136     param([int]$ErrorCode)
137     $exp = New-Object -TypeName System.ComponentModel.Win32Exception -ArgumentList $ErrorCode
138     $exp_msg = "{0} (Win32 ErrorCode {1} - 0x{1:X8})" -f $exp.Message, $ErrorCode
139     return $exp_msg
140 }
141 
Get-SystemLocaleName()142 Function Get-SystemLocaleName {
143     $max_length = 85  # LOCALE_NAME_MAX_LENGTH
144     $ptr = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($max_length)
145 
146     try {
147         $res = [Ansible.WinRegion.NativeMethods]::GetSystemDefaultLocaleName($ptr, $max_length)
148 
149         if ($res -eq 0) {
150             $err_code = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error()
151             $msg = Get-LastWin32ExceptionMessage -Error $err_code
152             Fail-Json -obj $result -message "Failed to get system locale: $msg"
153         }
154 
155         $system_locale = [System.Runtime.InteropServices.Marshal]::PtrToStringUni($ptr)
156     } finally {
157         [System.Runtime.InteropServices.Marshal]::FreeHGlobal($ptr)
158     }
159 
160     return $system_locale
161 }
162 
Get-UserLocaleNamenull163 Function Get-UserLocaleName {
164     $max_length = 85  # LOCALE_NAME_MAX_LENGTH
165     $ptr = [System.Runtime.InteropServices.Marshal]::AllocHGlobal($max_length)
166 
167     try {
168         $res = [Ansible.WinRegion.NativeMethods]::GetUserDefaultLocaleName($ptr, $max_length)
169 
170         if ($res -eq 0) {
171             $err_code = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error()
172             $msg = Get-LastWin32ExceptionMessage -Error $err_code
173             Fail-Json -obj $result -message "Failed to get user locale: $msg"
174         }
175 
176         $user_locale = [System.Runtime.InteropServices.Marshal]::PtrToStringUni($ptr)
177     } finally {
178         [System.Runtime.InteropServices.Marshal]::FreeHGlobal($ptr)
179     }
180 
181     return $user_locale
182 }
183 
Get-ValidGeoIds($cultures)184 Function Get-ValidGeoIds($cultures) {
185    $geo_ids = @()
186    foreach($culture in $cultures) {
187        try {
188            $geo_id = [System.Globalization.RegionInfo]$culture.Name
189            $geo_ids += $geo_id.GeoId
190        } catch {}
191    }
192    $geo_ids
193 }
194 
Test-RegistryProperty($reg_key, $property)195 Function Test-RegistryProperty($reg_key, $property) {
196     $type = Get-ItemProperty -LiteralPath $reg_key -Name $property -ErrorAction SilentlyContinue
197     if ($null -eq $type) {
198         $false
199     } else {
200         $true
201     }
202 }
203 
Copy-RegistryKey($source, $target)204 Function Copy-RegistryKey($source, $target) {
205     # Using Copy-Item -Recurse is giving me weird results, doing it recursively
206     Copy-Item -LiteralPath $source -Destination $target -WhatIf:$check_mode
207 
208     foreach($key in Get-ChildItem -LiteralPath $source) {
209         $sourceKey = "$source\$($key.PSChildName)"
210         $targetKey = (Get-Item -LiteralPath $source).PSChildName
211         Copy-RegistryKey -source "$sourceKey" -target "$target\$targetKey"
212     }
213 }
214 
Set-UserLocale($culture)215 Function Set-UserLocale($culture) {
216     $reg_key = 'HKCU:\Control Panel\International'
217 
218     $lookup = New-Object Ansible.WinRegion.LocaleHelper($culture)
219     # hex values are from http://www.pinvoke.net/default.aspx/kernel32/GetLocaleInfoEx.html
220     $wanted_values = @{
221         Locale = '{0:x8}' -f ([System.Globalization.CultureInfo]$culture).LCID
222         LocaleName = $culture
223         s1159 = $lookup.GetValueFromType(0x00000028)
224         s2359 = $lookup.GetValueFromType(0x00000029)
225         sCountry = $lookup.GetValueFromType(0x00000006)
226         sCurrency = $lookup.GetValueFromType(0x00000014)
227         sDate = $lookup.GetValueFromType(0x0000001D)
228         sDecimal = $lookup.GetValueFromType(0x0000000E)
229         sGrouping = $lookup.GetValueFromType(0x00000010)
230         sLanguage = $lookup.GetValueFromType(0x00000003) # LOCALE_ABBREVLANGNAME
231         sList = $lookup.GetValueFromType(0x0000000C)
232         sLongDate = $lookup.GetValueFromType(0x00000020)
233         sMonDecimalSep = $lookup.GetValueFromType(0x00000016)
234         sMonGrouping = $lookup.GetValueFromType(0x00000018)
235         sMonThousandSep = $lookup.GetValueFromType(0x00000017)
236         sNativeDigits = $lookup.GetValueFromType(0x00000013)
237         sNegativeSign = $lookup.GetValueFromType(0x00000051)
238         sPositiveSign = $lookup.GetValueFromType(0x00000050)
239         sShortDate = $lookup.GetValueFromType(0x0000001F)
240         sThousand = $lookup.GetValueFromType(0x0000000F)
241         sTime = $lookup.GetValueFromType(0x0000001E)
242         sTimeFormat = $lookup.GetValueFromType(0x00001003)
243         sYearMonth = $lookup.GetValueFromType(0x00001006)
244         iCalendarType = $lookup.GetValueFromType(0x00001009)
245         iCountry = $lookup.GetValueFromType(0x00000005)
246         iCurrDigits = $lookup.GetValueFromType(0x00000019)
247         iCurrency = $lookup.GetValueFromType(0x0000001B)
248         iDate = $lookup.GetValueFromType(0x00000021)
249         iDigits = $lookup.GetValueFromType(0x00000011)
250         NumShape = $lookup.GetValueFromType(0x00001014) # LOCALE_IDIGITSUBSTITUTION
251         iFirstDayOfWeek = $lookup.GetValueFromType(0x0000100C)
252         iFirstWeekOfYear = $lookup.GetValueFromType(0x0000100D)
253         iLZero = $lookup.GetValueFromType(0x00000012)
254         iMeasure = $lookup.GetValueFromType(0x0000000D)
255         iNegCurr = $lookup.GetValueFromType(0x0000001C)
256         iNegNumber = $lookup.GetValueFromType(0x00001010)
257         iPaperSize = $lookup.GetValueFromType(0x0000100A)
258         iTime = $lookup.GetValueFromType(0x00000023)
259         iTimePrefix = $lookup.GetValueFromType(0x00001005)
260         iTLZero = $lookup.GetValueFromType(0x00000025)
261     }
262 
263     if (Test-RegistryProperty -reg_key $reg_key -property 'sShortTime') {
264         # sShortTime was added after Vista, will check anyway and add in the value if it exists
265         $wanted_values.sShortTime = $lookup.GetValueFromType(0x00000079)
266     }
267 
268     $properties = Get-ItemProperty -LiteralPath $reg_key
269     foreach($property in $properties.PSObject.Properties) {
270         if (Test-RegistryProperty -reg_key $reg_key -property $property.Name) {
271             $name = $property.Name
272             $old_value = $property.Value
273             $new_value = $wanted_values.$name
274 
275             if ($new_value -ne $old_value) {
276                 Set-ItemProperty -LiteralPath $reg_key -Name $name -Value $new_value -WhatIf:$check_mode
277                 $result.changed = $true
278             }
279         }
280     }
281 }
282 
Set-SystemLocaleLegacy($unicode_language)283 Function Set-SystemLocaleLegacy($unicode_language) {
284     # For when Get/Set-WinSystemLocale is not available (Pre Windows 8 and Server 2012)
285     $current_language_value = (Get-ItemProperty -LiteralPath 'HKLM:\SYSTEM\CurrentControlSet\Control\Nls\Language').Default
286     $wanted_language_value = '{0:x4}' -f ([System.Globalization.CultureInfo]$unicode_language).LCID
287     if ($current_language_value -ne $wanted_language_value) {
288         Set-ItemProperty -LiteralPath 'HKLM:\SYSTEM\CurrentControlSet\Control\Nls\Language' -Name 'Default' -Value $wanted_language_value -WhatIf:$check_mode
289         $result.changed = $true
290         $result.restart_required = $true
291     }
292 
293     # This reads from the non registry (Default) key, the extra prop called (Default) see below for more details
294     $current_locale_value = (Get-ItemProperty -LiteralPath 'HKLM:\SYSTEM\CurrentControlSet\Control\Nls\Locale')."(Default)"
295     $wanted_locale_value = '{0:x8}' -f ([System.Globalization.CultureInfo]$unicode_language).LCID
296     if ($current_locale_value -ne $wanted_locale_value) {
297         # Need to use .net to write property value, Locale has 2 (Default) properties
298         # 1: The actual (Default) property, we don't want to change Set-ItemProperty writes to this value when using (Default)
299         # 2: A property called (Default), this is what we want to change and only .net SetValue can do this one
300         if (-not $check_mode) {
301             $hive = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey("LocalMachine", $env:COMPUTERNAME)
302             $key = $hive.OpenSubKey("SYSTEM\CurrentControlSet\Control\Nls\Locale", $true)
303             $key.SetValue("(Default)", $wanted_locale_value, [Microsoft.Win32.RegistryValueKind]::String)
304         }
305         $result.changed = $true
306         $result.restart_required = $true
307     }
308 
309     $codepage_path = 'HKLM:\SYSTEM\CurrentControlSet\Control\Nls\CodePage'
310     $current_codepage_info = Get-ItemProperty -LiteralPath $codepage_path
311     $wanted_codepage_info = ([System.Globalization.CultureInfo]::GetCultureInfo($unicode_language)).TextInfo
312 
313     $current_a_cp = $current_codepage_info.ACP
314     $current_oem_cp = $current_codepage_info.OEMCP
315     $current_mac_cp = $current_codepage_info.MACCP
316     $wanted_a_cp = $wanted_codepage_info.ANSICodePage
317     $wanted_oem_cp = $wanted_codepage_info.OEMCodePage
318     $wanted_mac_cp = $wanted_codepage_info.MacCodePage
319 
320     if ($current_a_cp -ne $wanted_a_cp) {
321         Set-ItemProperty -LiteralPath $codepage_path -Name 'ACP' -Value $wanted_a_cp -WhatIf:$check_mode
322         $result.changed = $true
323         $result.restart_required = $true
324     }
325     if ($current_oem_cp -ne $wanted_oem_cp) {
326         Set-ItemProperty -LiteralPath $codepage_path -Name 'OEMCP' -Value $wanted_oem_cp -WhatIf:$check_mode
327         $result.changed = $true
328         $result.restart_required = $true
329     }
330     if ($current_mac_cp -ne $wanted_mac_cp) {
331         Set-ItemProperty -LiteralPath $codepage_path -Name 'MACCP' -Value $wanted_mac_cp -WhatIf:$check_mode
332         $result.changed = $true
333         $result.restart_required = $true
334     }
335 }
336 
337 if ($null -eq $format -and $null -eq $location -and $null -eq $unicode_language) {
338     Fail-Json $result "An argument for 'format', 'location' or 'unicode_language' needs to be supplied"
339 } else {
340     $valid_cultures = [System.Globalization.CultureInfo]::GetCultures('InstalledWin32Cultures')
341     $valid_geoids = Get-ValidGeoIds -cultures $valid_cultures
342 
343     if ($null -ne $location) {
344         if ($valid_geoids -notcontains $location) {
345             Fail-Json $result "The argument location '$location' does not contain a valid Geo ID"
346         }
347     }
348 
349     if ($null -ne $format) {
350         if ($valid_cultures.Name -notcontains $format) {
351             Fail-Json $result "The argument format '$format' does not contain a valid Culture Name"
352         }
353     }
354 
355     if ($null -ne $unicode_language) {
356         if ($valid_cultures.Name -notcontains $unicode_language) {
357             Fail-Json $result "The argument unicode_language '$unicode_language' does not contain a valid Culture Name"
358         }
359     }
360 }
361 
362 if ($null -ne $location) {
363     # Get-WinHomeLocation was only added in Server 2012 and above
364     # Use legacy option if older
365     if (Get-Command 'Get-WinHomeLocation' -ErrorAction SilentlyContinue) {
366         $current_location = (Get-WinHomeLocation).GeoId
367         if ($current_location -ne $location) {
368             if (-not $check_mode) {
369                 Set-WinHomeLocation -GeoId $location
370             }
371             $result.changed = $true
372         }
373     } else {
374         $current_location = (Get-ItemProperty -LiteralPath 'HKCU:\Control Panel\International\Geo').Nation
375         if ($current_location -ne $location) {
376             Set-ItemProperty -LiteralPath 'HKCU:\Control Panel\International\Geo' -Name 'Nation' -Value $location -WhatIf:$check_mode
377             $result.changed = $true
378         }
379     }
380 }
381 
382 if ($null -ne $format) {
383     # Cannot use Get/Set-Culture as that fails to get and set the culture when running in the PSRP runspace.
384     $current_format = Get-UserLocaleName
385     if ($current_format -ne $format) {
386         Set-UserLocale -culture $format
387         $result.changed = $true
388     }
389 }
390 
391 if ($null -ne $unicode_language) {
392     # Get/Set-WinSystemLocale was only added in Server 2012 and above, use legacy option if older
393     if (Get-Command 'Get-WinSystemLocale' -ErrorAction SilentlyContinue) {
394         $current_unicode_language = Get-SystemLocaleName
395         if ($current_unicode_language -ne $unicode_language) {
396             if (-not $check_mode) {
397                 Set-WinSystemLocale -SystemLocale $unicode_language
398             }
399             $result.changed = $true
400             $result.restart_required = $true
401         }
402     } else {
403         Set-SystemLocaleLegacy -unicode_language $unicode_language
404     }
405 }
406 
407 if ($copy_settings -eq $true -and $result.changed -eq $true) {
408     if (-not $check_mode) {
409         New-PSDrive -Name HKU -PSProvider Registry -Root Registry::HKEY_USERS
410 
411         if (Test-Path -LiteralPath HKU:\ANSIBLE) {
412             $module.Warn("hive already loaded at HKU:\ANSIBLE, had to unload hive for win_region to continue")
413             [Ansible.WinRegion.Hive]::UnloadHive("ANSIBLE")
414         }
415 
416         $loaded_hive = New-Object -TypeName Ansible.WinRegion.Hive -ArgumentList "ANSIBLE", 'C:\Users\Default\NTUSER.DAT'
417         try {
418             $sids = 'ANSIBLE', '.DEFAULT', 'S-1-5-19', 'S-1-5-20'
419             foreach ($sid in $sids) {
420                 Copy-RegistryKey -source "HKCU:\Keyboard Layout" -target "HKU:\$sid"
421                 Copy-RegistryKey -source "HKCU:\Control Panel\International" -target "HKU:\$sid\Control Panel"
422                 Copy-RegistryKey -source "HKCU:\Control Panel\Input Method" -target "HKU:\$sid\Control Panel"
423             }
424         }
425         finally {
426             $loaded_hive.Dispose()
427         }
428 
429         Remove-PSDrive HKU
430     }
431     $result.changed = $true
432 }
433 
434 Exit-Json $result
435