001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.tools;
003
004import static java.awt.event.InputEvent.ALT_DOWN_MASK;
005import static java.awt.event.InputEvent.CTRL_DOWN_MASK;
006import static java.awt.event.InputEvent.SHIFT_DOWN_MASK;
007import static java.awt.event.KeyEvent.VK_A;
008import static java.awt.event.KeyEvent.VK_C;
009import static java.awt.event.KeyEvent.VK_D;
010import static java.awt.event.KeyEvent.VK_DELETE;
011import static java.awt.event.KeyEvent.VK_DOWN;
012import static java.awt.event.KeyEvent.VK_ENTER;
013import static java.awt.event.KeyEvent.VK_ESCAPE;
014import static java.awt.event.KeyEvent.VK_F10;
015import static java.awt.event.KeyEvent.VK_F4;
016import static java.awt.event.KeyEvent.VK_LEFT;
017import static java.awt.event.KeyEvent.VK_NUM_LOCK;
018import static java.awt.event.KeyEvent.VK_PRINTSCREEN;
019import static java.awt.event.KeyEvent.VK_RIGHT;
020import static java.awt.event.KeyEvent.VK_SHIFT;
021import static java.awt.event.KeyEvent.VK_SPACE;
022import static java.awt.event.KeyEvent.VK_TAB;
023import static java.awt.event.KeyEvent.VK_UP;
024import static java.awt.event.KeyEvent.VK_V;
025import static java.awt.event.KeyEvent.VK_X;
026import static java.awt.event.KeyEvent.VK_Y;
027import static java.awt.event.KeyEvent.VK_Z;
028import static org.openstreetmap.josm.tools.I18n.tr;
029import static org.openstreetmap.josm.tools.Utils.getSystemEnv;
030import static org.openstreetmap.josm.tools.Utils.getSystemProperty;
031import static org.openstreetmap.josm.tools.WinRegistry.HKEY_LOCAL_MACHINE;
032
033import java.awt.Desktop;
034import java.io.BufferedWriter;
035import java.io.File;
036import java.io.IOException;
037import java.io.InputStream;
038import java.io.OutputStream;
039import java.io.OutputStreamWriter;
040import java.io.Writer;
041import java.lang.reflect.InvocationTargetException;
042import java.net.URISyntaxException;
043import java.nio.charset.StandardCharsets;
044import java.nio.file.DirectoryIteratorException;
045import java.nio.file.DirectoryStream;
046import java.nio.file.FileSystems;
047import java.nio.file.Files;
048import java.nio.file.InvalidPathException;
049import java.nio.file.Path;
050import java.security.KeyStore;
051import java.security.KeyStoreException;
052import java.security.MessageDigest;
053import java.security.NoSuchAlgorithmException;
054import java.security.cert.Certificate;
055import java.security.cert.CertificateEncodingException;
056import java.security.cert.CertificateException;
057import java.security.cert.X509Certificate;
058import java.text.ParseException;
059import java.util.ArrayList;
060import java.util.Arrays;
061import java.util.Collection;
062import java.util.Enumeration;
063import java.util.HashSet;
064import java.util.List;
065import java.util.Locale;
066import java.util.Properties;
067import java.util.Set;
068import java.util.concurrent.ExecutionException;
069import java.util.concurrent.TimeUnit;
070import java.util.regex.Matcher;
071import java.util.regex.Pattern;
072
073import org.openstreetmap.josm.data.Preferences;
074import org.openstreetmap.josm.data.StructUtils;
075import org.openstreetmap.josm.data.StructUtils.StructEntry;
076import org.openstreetmap.josm.data.StructUtils.WriteExplicitly;
077import org.openstreetmap.josm.io.CertificateAmendment.NativeCertAmend;
078import org.openstreetmap.josm.io.NetworkManager;
079import org.openstreetmap.josm.io.OnlineResource;
080import org.openstreetmap.josm.spi.preferences.Config;
081
082/**
083 * {@code PlatformHook} implementation for Microsoft Windows systems.
084 * @since 1023
085 */
086public class PlatformHookWindows implements PlatformHook {
087
088    /**
089     * Pattern of Microsoft .NET and Powershell version numbers in registry.
090     */
091    private static final Pattern MS_VERSION_PATTERN = Pattern.compile("(\\d+)\\.(\\d+)(\\.\\d+.*)?");
092
093    /**
094     * Simple data class to hold information about a font.
095     *
096     * Used for fontconfig.properties files.
097     */
098    public static class FontEntry {
099        /**
100         * The character subset. Basically a free identifier, but should be unique.
101         */
102        @StructEntry
103        public String charset;
104
105        /**
106         * Platform font name.
107         */
108        @StructEntry
109        @WriteExplicitly
110        public String name = "";
111
112        /**
113         * File name.
114         */
115        @StructEntry
116        @WriteExplicitly
117        public String file = "";
118
119        /**
120         * Constructs a new {@code FontEntry}.
121         */
122        public FontEntry() {
123            // Default constructor needed for construction by reflection
124        }
125
126        /**
127         * Constructs a new {@code FontEntry}.
128         * @param charset The character subset. Basically a free identifier, but should be unique
129         * @param name Platform font name
130         * @param file File name
131         */
132        public FontEntry(String charset, String name, String file) {
133            this.charset = charset;
134            this.name = name;
135            this.file = file;
136        }
137    }
138
139    private static final String WINDOWS_ROOT = "Windows-ROOT";
140
141    private static final String CURRENT_VERSION = "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion";
142
143    private String oSBuildNumber;
144
145    @Override
146    public Platform getPlatform() {
147        return Platform.WINDOWS;
148    }
149
150    @Override
151    public void afterPrefStartupHook() {
152        extendFontconfig("fontconfig.properties.src");
153    }
154
155    @Override
156    public void startupHook(JavaExpirationCallback javaCallback, WebStartMigrationCallback webStartCallback) {
157        checkExpiredJava(javaCallback);
158        checkWebStartMigration(webStartCallback);
159    }
160
161    @Override
162    public void openUrl(String url) throws IOException {
163        if (!url.startsWith("file:/")) {
164            final String customBrowser = Config.getPref().get("browser.windows", "");
165            if (!customBrowser.isEmpty()) {
166                Runtime.getRuntime().exec(new String[]{customBrowser, url});
167                return;
168            }
169        }
170        try {
171            // Desktop API works fine under Windows
172            Desktop.getDesktop().browse(Utils.urlToURI(url));
173        } catch (IOException | URISyntaxException e) {
174            Logging.log(Logging.LEVEL_WARN, "Desktop class failed. Platform dependent fall back for open url in browser.", e);
175            Runtime.getRuntime().exec(new String[]{"rundll32", "url.dll,FileProtocolHandler", url});
176        }
177    }
178
179    @Override
180    public void initSystemShortcuts() {
181        // CHECKSTYLE.OFF: LineLength
182        //Shortcut.registerSystemCut("system:menuexit", tr("reserved"), VK_Q, CTRL_DOWN_MASK);
183        Shortcut.registerSystemShortcut("system:duplicate", tr("reserved"), VK_D, CTRL_DOWN_MASK); // not really system, but to avoid odd results
184
185        // Windows 7 shortcuts: http://windows.microsoft.com/en-US/windows7/Keyboard-shortcuts
186
187        // Shortcuts with setAutomatic(): items with automatic shortcuts will not be added to the menu bar at all
188
189        // Don't know why Ctrl-Alt-Del isn't even listed on official Microsoft support page
190        Shortcut.registerSystemShortcut("system:reset", tr("reserved"), VK_DELETE, CTRL_DOWN_MASK | ALT_DOWN_MASK).setAutomatic();
191
192        // Ease of Access keyboard shortcuts
193        Shortcut.registerSystemShortcut("microsoft-reserved-01", tr("reserved"), VK_PRINTSCREEN, ALT_DOWN_MASK | SHIFT_DOWN_MASK).setAutomatic(); // Turn High Contrast on or off
194        Shortcut.registerSystemShortcut("microsoft-reserved-02", tr("reserved"), VK_NUM_LOCK, ALT_DOWN_MASK | SHIFT_DOWN_MASK).setAutomatic(); // Turn Mouse Keys on or off
195        //Shortcut.registerSystemCut("microsoft-reserved-03", tr("reserved"), VK_U, );// Open the Ease of Access Center (TODO: Windows-U, how to handle it in Java ?)
196
197        // General keyboard shortcuts
198        //Shortcut.registerSystemShortcut("system:help", tr("reserved"), VK_F1, 0);                            // Display Help
199        Shortcut.registerSystemShortcut("system:copy", tr("reserved"), VK_C, CTRL_DOWN_MASK);                // Copy the selected item
200        Shortcut.registerSystemShortcut("system:cut", tr("reserved"), VK_X, CTRL_DOWN_MASK);                 // Cut the selected item
201        Shortcut.registerSystemShortcut("system:paste", tr("reserved"), VK_V, CTRL_DOWN_MASK);               // Paste the selected item
202        Shortcut.registerSystemShortcut("system:undo", tr("reserved"), VK_Z, CTRL_DOWN_MASK);                // Undo an action
203        Shortcut.registerSystemShortcut("system:redo", tr("reserved"), VK_Y, CTRL_DOWN_MASK);                // Redo an action
204        //Shortcut.registerSystemCut("microsoft-reserved-10", tr("reserved"), VK_DELETE, 0);                  // Delete the selected item and move it to the Recycle Bin
205        //Shortcut.registerSystemCut("microsoft-reserved-11", tr("reserved"), VK_DELETE, SHIFT_DOWN_MASK);    // Delete the selected item without moving it to the Recycle Bin first
206        //Shortcut.registerSystemCut("system:rename", tr("reserved"), VK_F2, 0);                          // Rename the selected item
207        Shortcut.registerSystemShortcut("system:movefocusright", tr("reserved"), VK_RIGHT, CTRL_DOWN_MASK);  // Move the cursor to the beginning of the next word
208        Shortcut.registerSystemShortcut("system:movefocusleft", tr("reserved"), VK_LEFT, CTRL_DOWN_MASK);    // Move the cursor to the beginning of the previous word
209        Shortcut.registerSystemShortcut("system:movefocusdown", tr("reserved"), VK_DOWN, CTRL_DOWN_MASK);    // Move the cursor to the beginning of the next paragraph
210        Shortcut.registerSystemShortcut("system:movefocusup", tr("reserved"), VK_UP, CTRL_DOWN_MASK);        // Move the cursor to the beginning of the previous paragraph
211        //Shortcut.registerSystemCut("microsoft-reserved-17", tr("reserved"), VK_RIGHT, CTRL_DOWN_MASK | SHIFT_DOWN_MASK); // Select a block of text
212        //Shortcut.registerSystemCut("microsoft-reserved-18", tr("reserved"), VK_LEFT, CTRL_DOWN_MASK | SHIFT_DOWN_MASK);  // Select a block of text
213        //Shortcut.registerSystemCut("microsoft-reserved-19", tr("reserved"), VK_DOWN, CTRL_DOWN_MASK | SHIFT_DOWN_MASK);  // Select a block of text
214        //Shortcut.registerSystemCut("microsoft-reserved-20", tr("reserved"), VK_UP, CTRL_DOWN_MASK | SHIFT_DOWN_MASK);    // Select a block of text
215        //Shortcut.registerSystemCut("microsoft-reserved-21", tr("reserved"), VK_RIGHT, SHIFT_DOWN_MASK); // Select more than one item in a window or on the desktop, or select text within a document
216        //Shortcut.registerSystemCut("microsoft-reserved-22", tr("reserved"), VK_LEFT, SHIFT_DOWN_MASK);  // Select more than one item in a window or on the desktop, or select text within a document
217        //Shortcut.registerSystemCut("microsoft-reserved-23", tr("reserved"), VK_DOWN, SHIFT_DOWN_MASK);  // Select more than one item in a window or on the desktop, or select text within a document
218        //Shortcut.registerSystemCut("microsoft-reserved-24", tr("reserved"), VK_UP, SHIFT_DOWN_MASK);    // Select more than one item in a window or on the desktop, or select text within a document
219        //Shortcut.registerSystemCut("microsoft-reserved-25", tr("reserved"), VK_RIGHT+, CTRL_DOWN_MASK); // Select multiple individual items in a window or on the desktop (TODO: ctrl+arrow+spacebar, how to handle it in Java ?)
220        //Shortcut.registerSystemCut("microsoft-reserved-26", tr("reserved"), VK_LEFT+, CTRL_DOWN_MASK);  // Select multiple individual items in a window or on the desktop (TODO: ctrl+arrow+spacebar, how to handle it in Java ?)
221        //Shortcut.registerSystemCut("microsoft-reserved-27", tr("reserved"), VK_DOWN+, CTRL_DOWN_MASK);  // Select multiple individual items in a window or on the desktop (TODO: ctrl+arrow+spacebar, how to handle it in Java ?)
222        //Shortcut.registerSystemCut("microsoft-reserved-28", tr("reserved"), VK_UP+, CTRL_DOWN_MASK);    // Select multiple individual items in a window or on the desktop (TODO: ctrl+arrow+spacebar, how to handle it in Java ?)
223        Shortcut.registerSystemShortcut("system:selectall", tr("reserved"), VK_A, CTRL_DOWN_MASK);           // Select all items in a document or window
224        //Shortcut.registerSystemCut("system:search", tr("reserved"), VK_F3, 0);                          // Search for a file or folder
225        Shortcut.registerSystemShortcut("microsoft-reserved-31", tr("reserved"), VK_ENTER, ALT_DOWN_MASK).setAutomatic();   // Display properties for the selected item
226        Shortcut.registerSystemShortcut("system:exit", tr("reserved"), VK_F4, ALT_DOWN_MASK).setAutomatic(); // Close the active item, or exit the active program
227        Shortcut.registerSystemShortcut("microsoft-reserved-33", tr("reserved"), VK_SPACE, ALT_DOWN_MASK).setAutomatic();   // Open the shortcut menu for the active window
228        //Shortcut.registerSystemCut("microsoft-reserved-34", tr("reserved"), VK_F4, CTRL_DOWN_MASK);     // Close the active document (in programs that allow you to have multiple documents open simultaneously)
229        Shortcut.registerSystemShortcut("microsoft-reserved-35", tr("reserved"), VK_TAB, ALT_DOWN_MASK).setAutomatic();     // Switch between open items
230        Shortcut.registerSystemShortcut("microsoft-reserved-36", tr("reserved"), VK_TAB, CTRL_DOWN_MASK | ALT_DOWN_MASK).setAutomatic(); // Use the arrow keys to switch between open items
231        //Shortcut.registerSystemCut("microsoft-reserved-37", tr("reserved"), VK_TAB, ); // Cycle through programs on the taskbar by using Aero Flip 3-D (TODO: Windows-Tab, how to handle it in Java ?)
232        //Shortcut.registerSystemCut("microsoft-reserved-38", tr("reserved"), VK_TAB, CTRL_DOWN_MASK | ); // Use the arrow keys to cycle through programs on the taskbar by using Aero Flip 3-D (TODO: Ctrl-Windows-Tab, how to handle it in Java ?)
233        Shortcut.registerSystemShortcut("microsoft-reserved-39", tr("reserved"), VK_ESCAPE, ALT_DOWN_MASK).setAutomatic();  // Cycle through items in the order in which they were opened
234        //Shortcut.registerSystemCut("microsoft-reserved-40", tr("reserved"), VK_F6, 0);                  // Cycle through screen elements in a window or on the desktop
235        //Shortcut.registerSystemCut("microsoft-reserved-41", tr("reserved"), VK_F4, 0);                  // Display the address bar list in Windows Explorer
236        Shortcut.registerSystemShortcut("microsoft-reserved-42", tr("reserved"), VK_F10, SHIFT_DOWN_MASK);   // Display the shortcut menu for the selected item
237        Shortcut.registerSystemShortcut("microsoft-reserved-43", tr("reserved"), VK_ESCAPE, CTRL_DOWN_MASK).setAutomatic(); // Open the Start menu
238        //Shortcut.registerSystemShortcut("microsoft-reserved-44", tr("reserved"), VK_F10, 0);                 // Activate the menu bar in the active program
239        //Shortcut.registerSystemCut("microsoft-reserved-45", tr("reserved"), VK_RIGHT, 0);               // Open the next menu to the right, or open a submenu
240        //Shortcut.registerSystemCut("microsoft-reserved-46", tr("reserved"), VK_LEFT, 0);                // Open the next menu to the left, or close a submenu
241        //Shortcut.registerSystemCut("microsoft-reserved-47", tr("reserved"), VK_F5, 0);                  // Refresh the active window
242        //Shortcut.registerSystemCut("microsoft-reserved-48", tr("reserved"), VK_UP, ALT_DOWN_MASK);      // View the folder one level up in Windows Explorer
243        //Shortcut.registerSystemCut("microsoft-reserved-49", tr("reserved"), VK_ESCAPE, 0);              // Cancel the current task
244        Shortcut.registerSystemShortcut("microsoft-reserved-50", tr("reserved"), VK_ESCAPE, CTRL_DOWN_MASK | SHIFT_DOWN_MASK).setAutomatic(); // Open Task Manager
245        Shortcut.registerSystemShortcut("microsoft-reserved-51", tr("reserved"), VK_SHIFT, ALT_DOWN_MASK).setAutomatic();   // Switch the input language when multiple input languages are enabled
246        Shortcut.registerSystemShortcut("microsoft-reserved-52", tr("reserved"), VK_SHIFT, CTRL_DOWN_MASK).setAutomatic();  // Switch the keyboard layout when multiple keyboard layouts are enabled
247        //Shortcut.registerSystemCut("microsoft-reserved-53", tr("reserved"), ); // Change the reading direction of text in right-to-left reading languages (TODO: unclear)
248        // CHECKSTYLE.ON: LineLength
249    }
250
251    @Override
252    public String getDefaultStyle() {
253        return "com.sun.java.swing.plaf.windows.WindowsLookAndFeel";
254    }
255
256    @Override
257    public boolean rename(File from, File to) {
258        if (to.exists())
259            Utils.deleteFile(to);
260        return from.renameTo(to);
261    }
262
263    @Override
264    public String getOSDescription() {
265        return Utils.strip(getSystemProperty("os.name")) + ' ' +
266                ((getSystemEnv("ProgramFiles(x86)") == null) ? "32" : "64") + "-Bit";
267    }
268
269    /**
270     * Returns the Windows product name from registry (example: "Windows 10 Pro")
271     * @return the Windows product name from registry
272     * @throws IllegalAccessException if Java language access control is enforced and the underlying method is inaccessible
273     * @throws InvocationTargetException if the underlying method throws an exception
274     * @since 12744
275     */
276    public static String getProductName() throws IllegalAccessException, InvocationTargetException {
277        return WinRegistry.readString(HKEY_LOCAL_MACHINE, CURRENT_VERSION, "ProductName");
278    }
279
280    /**
281     * Returns the Windows release identifier from registry (example: "1703")
282     * @return the Windows release identifier from registry
283     * @throws IllegalAccessException if Java language access control is enforced and the underlying method is inaccessible
284     * @throws InvocationTargetException if the underlying method throws an exception
285     * @since 12744
286     */
287    public static String getReleaseId() throws IllegalAccessException, InvocationTargetException {
288        return WinRegistry.readString(HKEY_LOCAL_MACHINE, CURRENT_VERSION, "ReleaseId");
289    }
290
291    /**
292     * Returns the Windows current build number from registry (example: "15063")
293     * @return the Windows current build number from registry
294     * @throws IllegalAccessException if Java language access control is enforced and the underlying method is inaccessible
295     * @throws InvocationTargetException if the underlying method throws an exception
296     * @since 12744
297     */
298    public static String getCurrentBuild() throws IllegalAccessException, InvocationTargetException {
299        return WinRegistry.readString(HKEY_LOCAL_MACHINE, CURRENT_VERSION, "CurrentBuild");
300    }
301
302    private static String buildOSBuildNumber() {
303        StringBuilder sb = new StringBuilder();
304        try {
305            sb.append(getProductName());
306            String releaseId = getReleaseId();
307            if (releaseId != null) {
308                sb.append(' ').append(releaseId);
309            }
310            sb.append(" (").append(getCurrentBuild()).append(')');
311        } catch (ReflectiveOperationException | JosmRuntimeException | NoClassDefFoundError e) {
312            Logging.log(Logging.LEVEL_ERROR, "Unable to get Windows build number", e);
313            Logging.debug(e);
314        }
315        return sb.toString();
316    }
317
318    @Override
319    public String getOSBuildNumber() {
320        if (oSBuildNumber == null) {
321            oSBuildNumber = buildOSBuildNumber();
322        }
323        return oSBuildNumber;
324    }
325
326    /**
327     * Loads Windows-ROOT keystore.
328     * @return Windows-ROOT keystore
329     * @throws NoSuchAlgorithmException if the algorithm used to check the integrity of the keystore cannot be found
330     * @throws CertificateException if any of the certificates in the keystore could not be loaded
331     * @throws IOException if there is an I/O or format problem with the keystore data, if a password is required but not given
332     * @throws KeyStoreException if no Provider supports a KeyStore implementation for the type "Windows-ROOT"
333     * @since 7343
334     */
335    public static KeyStore getRootKeystore() throws NoSuchAlgorithmException, CertificateException, IOException, KeyStoreException {
336        KeyStore ks = KeyStore.getInstance(WINDOWS_ROOT);
337        ks.load(null, null);
338        return ks;
339    }
340
341    @Override
342    public X509Certificate getX509Certificate(NativeCertAmend certAmend)
343            throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException {
344        MessageDigest md = MessageDigest.getInstance("SHA-256");
345        // Get Windows Trust Root Store
346        KeyStore ks = getRootKeystore();
347        // Search by alias (fast)
348        for (String winAlias : certAmend.getNativeAliases()) {
349            Certificate result = ks.getCertificate(winAlias);
350            // Check for SHA-256 signature, as sometimes Microsoft can ship several certificates with the same alias, for example:
351            // AC RAIZ FNMT-RCM: EBC5570C29018C4D67B1AA127BAF12F703B4611EBC17B7DAB5573894179B93FA (SHA256)
352            // AC RAIZ FNMT-RCM: 4D9EBB28825C9643AB15D54E5F9614F13CB3E95DE3CF4EAC971301F320F9226E (SHA1)
353            if (!sha256matches(result, certAmend, md)) {
354                Logging.trace("Ignoring {0} as SHA-256 signature does not match", result);
355                result = null;
356            }
357            if (result == null && !NetworkManager.isOffline(OnlineResource.CERTIFICATES)) {
358                // Make a web request to target site to force Windows to update if needed its trust root store from its certificate trust list
359                // A better, but a lot more complex method might be to get certificate list from Windows Registry with PowerShell
360                // using (Get-ItemProperty -Path 'HKLM:\\SOFTWARE\\Microsoft\\SystemCertificates\\AuthRoot\\AutoUpdate').EncodedCtl)
361                // then decode it using CertUtil -dump or calling CertCreateCTLContext API using JNI, and finally find and decode the certificate
362                Logging.trace(webRequest(certAmend.getWebSite()));
363                // Reload Windows Trust Root Store and search again by alias (fast)
364                ks = getRootKeystore();
365                result = ks.getCertificate(winAlias);
366            }
367            if (result instanceof X509Certificate) {
368                return (X509Certificate) result;
369            }
370        }
371        // If not found, search by SHA-256 (slower)
372        for (Enumeration<String> aliases = ks.aliases(); aliases.hasMoreElements();) {
373            String alias = aliases.nextElement();
374            Certificate result = ks.getCertificate(alias);
375            if (sha256matches(result, certAmend, md)) {
376                Logging.warn("Certificate not found for alias ''{0}'' but found for alias ''{1}''", certAmend.getNativeAliases(), alias);
377                return (X509Certificate) result;
378            }
379        }
380        // Not found
381        return null;
382    }
383
384    private static boolean sha256matches(Certificate result, NativeCertAmend certAmend, MessageDigest md) throws CertificateEncodingException {
385        return result instanceof X509Certificate
386                && certAmend.getSha256().equalsIgnoreCase(Utils.toHexString(md.digest(result.getEncoded())));
387    }
388
389    @Override
390    public File getDefaultCacheDirectory() {
391        String p = getSystemEnv("LOCALAPPDATA");
392        if (Utils.isEmpty(p)) {
393            // Fallback for Windows OS earlier than Windows Vista, where the variable is not defined
394            p = getSystemEnv("APPDATA");
395        }
396        return new File(new File(p, Preferences.getJOSMDirectoryBaseName()), "cache");
397    }
398
399    @Override
400    public File getDefaultPrefDirectory() {
401        return new File(getSystemEnv("APPDATA"), Preferences.getJOSMDirectoryBaseName());
402    }
403
404    @Override
405    public File getDefaultUserDataDirectory() {
406        // Use preferences directory by default
407        return Config.getDirs().getPreferencesDirectory(false);
408    }
409
410    /**
411     * <p>Add more fallback fonts to the Java runtime, in order to get
412     * support for more scripts.</p>
413     *
414     * <p>The font configuration in Java doesn't include some Indic scripts,
415     * even though MS Windows ships with fonts that cover these unicode ranges.</p>
416     *
417     * <p>To fix this, the fontconfig.properties template is copied to the JOSM
418     * cache folder. Then, the additional entries are added to the font
419     * configuration. Finally the system property "sun.awt.fontconfig" is set
420     * to the customized fontconfig.properties file.</p>
421     *
422     * <p>This is a crude hack, but better than no font display at all for these languages.
423     * There is no guarantee, that the template file
424     * ($JAVA_HOME/lib/fontconfig.properties.src) matches the default
425     * configuration (which is in a binary format).
426     * Furthermore, the system property "sun.awt.fontconfig" is undocumented and
427     * may no longer work in future versions of Java.</p>
428     *
429     * <p>Related Java bug: <a href="https://bugs.openjdk.java.net/browse/JDK-8008572">JDK-8008572</a></p>
430     *
431     * @param templateFileName file name of the fontconfig.properties template file
432     */
433    protected void extendFontconfig(String templateFileName) {
434        String customFontconfigFile = Config.getPref().get("fontconfig.properties", null);
435        if (customFontconfigFile != null) {
436            Utils.updateSystemProperty("sun.awt.fontconfig", customFontconfigFile);
437            return;
438        }
439        if (!Config.getPref().getBoolean("font.extended-unicode", true))
440            return;
441
442        String javaLibPath = getSystemProperty("java.home") + File.separator + "lib";
443        Path templateFile = FileSystems.getDefault().getPath(javaLibPath, templateFileName);
444        String templatePath = templateFile.toString();
445        if (templatePath.startsWith("null") || !Files.isReadable(templateFile)) {
446            Logging.warn("extended font config - unable to find font config template file {0}", templatePath);
447            return;
448        }
449        try (InputStream fis = Files.newInputStream(templateFile)) {
450            Properties props = new Properties();
451            props.load(fis);
452            byte[] content = Files.readAllBytes(templateFile);
453            File cachePath = Config.getDirs().getCacheDirectory(true);
454            Path fontconfigFile = cachePath.toPath().resolve("fontconfig.properties");
455            OutputStream os = Files.newOutputStream(fontconfigFile); // NOPMD
456            os.write(content);
457            try (Writer w = new BufferedWriter(new OutputStreamWriter(os, StandardCharsets.UTF_8))) {
458                Collection<FontEntry> extrasPref = StructUtils.getListOfStructs(Config.getPref(),
459                        "font.extended-unicode.extra-items", getAdditionalFonts(), FontEntry.class);
460                Collection<FontEntry> extras = new ArrayList<>();
461                w.append("\n\n# Added by JOSM to extend unicode coverage of Java font support:\n\n");
462                List<String> allCharSubsets = new ArrayList<>();
463                for (FontEntry entry: extrasPref) {
464                    Collection<String> fontsAvail = getInstalledFonts();
465                    if (fontsAvail != null && fontsAvail.contains(entry.file.toUpperCase(Locale.ENGLISH))) {
466                        if (!allCharSubsets.contains(entry.charset)) {
467                            allCharSubsets.add(entry.charset);
468                            extras.add(entry);
469                        } else {
470                            Logging.trace("extended font config - already registered font for charset ''{0}'' - skipping ''{1}''",
471                                    entry.charset, entry.name);
472                        }
473                    } else {
474                        Logging.trace("extended font config - Font ''{0}'' not found on system - skipping", entry.name);
475                    }
476                }
477                for (FontEntry entry: extras) {
478                    allCharSubsets.add(entry.charset);
479                    if ("".equals(entry.name)) {
480                        continue;
481                    }
482                    String key = "allfonts." + entry.charset;
483                    String value = entry.name;
484                    String prevValue = props.getProperty(key);
485                    if (prevValue != null && !prevValue.equals(value)) {
486                        Logging.warn("extended font config - overriding ''{0}={1}'' with ''{2}''", key, prevValue, value);
487                    }
488                    w.append(key + '=' + value + '\n');
489                }
490                w.append('\n');
491                for (FontEntry entry: extras) {
492                    if ("".equals(entry.name) || "".equals(entry.file)) {
493                        continue;
494                    }
495                    String key = "filename." + entry.name.replace(' ', '_');
496                    String value = entry.file;
497                    String prevValue = props.getProperty(key);
498                    if (prevValue != null && !prevValue.equals(value)) {
499                        Logging.warn("extended font config - overriding ''{0}={1}'' with ''{2}''", key, prevValue, value);
500                    }
501                    w.append(key + '=' + value + '\n');
502                }
503                w.append('\n');
504                w.append("sequence.fallback=");
505                String fallback = props.getProperty("sequence.fallback");
506                if (fallback != null) {
507                    w.append(fallback).append(",");
508                }
509                w.append(String.join(",", allCharSubsets)).append("\n");
510            }
511            Utils.updateSystemProperty("sun.awt.fontconfig", fontconfigFile.toString());
512        } catch (IOException | InvalidPathException ex) {
513            Logging.error(ex);
514        }
515    }
516
517    /**
518     * Get a list of fonts that are installed on the system.
519     *
520     * Must be done without triggering the Java Font initialization.
521     * (See {@link #extendFontconfig(java.lang.String)}, have to set system
522     * property first, which is then read by sun.awt.FontConfiguration upon initialization.)
523     *
524     * @return list of file names
525     */
526    protected Collection<String> getInstalledFonts() {
527        // Cannot use GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames()
528        // because we have to set the system property before Java initializes its fonts.
529        // Use more low-level method to find the installed fonts.
530        List<String> fontsAvail = new ArrayList<>();
531        String systemRoot = getSystemEnv("SYSTEMROOT");
532        if (systemRoot == null) {
533            return fontsAvail;
534        }
535        Path fontPath = FileSystems.getDefault().getPath(systemRoot, "Fonts");
536        try (DirectoryStream<Path> ds = Files.newDirectoryStream(fontPath)) {
537            for (Path p : ds) {
538                Path filename = p.getFileName();
539                if (filename != null) {
540                    fontsAvail.add(filename.toString().toUpperCase(Locale.ENGLISH));
541                }
542            }
543            fontsAvail.add(""); // for devanagari
544        } catch (IOException | DirectoryIteratorException ex) {
545            Logging.log(Logging.LEVEL_ERROR, ex);
546            Logging.warn("extended font config - failed to load available Fonts");
547            fontsAvail = null;
548        }
549        return fontsAvail;
550    }
551
552    /**
553     * Get default list of additional fonts to add to the configuration.
554     *
555     * Java will choose thee first font in the list that can render a certain character.
556     *
557     * @return list of FontEntry objects
558     */
559    protected Collection<FontEntry> getAdditionalFonts() {
560        Collection<FontEntry> def = new ArrayList<>(33);
561        def.add(new FontEntry("devanagari", "", "")); // just include in fallback list font already defined in template
562
563        // Windows scripts: https://msdn.microsoft.com/en-us/goglobal/bb688099.aspx
564        // IE default fonts: https://msdn.microsoft.com/en-us/library/ie/dn467844(v=vs.85).aspx
565
566        // Windows 10 and later
567        def.add(new FontEntry("historic", "Segoe UI Historic", "SEGUIHIS.TTF"));       // historic charsets
568
569        // Windows 8/8.1 and later
570        def.add(new FontEntry("javanese", "Javanese Text", "JAVATEXT.TTF"));           // ISO 639: jv
571        def.add(new FontEntry("leelawadee", "Leelawadee", "LEELAWAD.TTF"));            // ISO 639: bug
572        def.add(new FontEntry("malgun", "Malgun Gothic", "MALGUN.TTF"));               // ISO 639: ko
573        def.add(new FontEntry("myanmar", "Myanmar Text", "MMRTEXT.TTF"));              // ISO 639: my
574        def.add(new FontEntry("nirmala", "Nirmala UI", "NIRMALA.TTF"));                // ISO 639: sat,srb
575        def.add(new FontEntry("segoeui", "Segoe UI", "SEGOEUI.TTF"));                  // ISO 639: lis
576        def.add(new FontEntry("emoji", "Segoe UI Emoji", "SEGUIEMJ.TTF"));             // emoji symbol characters
577
578        // Windows 7 and later
579        def.add(new FontEntry("nko_tifinagh_vai_osmanya", "Ebrima", "EBRIMA.TTF"));    // ISO 639: ber. Nko only since Win 8
580        def.add(new FontEntry("khmer1", "Khmer UI", "KHMERUI.TTF"));                   // ISO 639: km
581        def.add(new FontEntry("lao1", "Lao UI", "LAOUI.TTF"));                         // ISO 639: lo
582        def.add(new FontEntry("tai_le", "Microsoft Tai Le", "TAILE.TTF"));             // ISO 639: khb
583        def.add(new FontEntry("new_tai_lue", "Microsoft New Tai Lue", "NTHAILU.TTF")); // ISO 639: khb
584
585        // Windows Vista and later:
586        def.add(new FontEntry("ethiopic", "Nyala", "NYALA.TTF"));                   // ISO 639: am,gez,ti
587        def.add(new FontEntry("tibetan", "Microsoft Himalaya", "HIMALAYA.TTF"));    // ISO 639: bo,dz
588        def.add(new FontEntry("cherokee", "Plantagenet Cherokee", "PLANTC.TTF"));   // ISO 639: chr
589        def.add(new FontEntry("unified_canadian", "Euphemia", "EUPHEMIA.TTF"));     // ISO 639: cr,in
590        def.add(new FontEntry("khmer2", "DaunPenh", "DAUNPENH.TTF"));               // ISO 639: km
591        def.add(new FontEntry("khmer3", "MoolBoran", "MOOLBOR.TTF"));               // ISO 639: km
592        def.add(new FontEntry("lao_thai", "DokChampa", "DOKCHAMP.TTF"));            // ISO 639: lo
593        def.add(new FontEntry("mongolian", "Mongolian Baiti", "MONBAITI.TTF"));     // ISO 639: mn
594        def.add(new FontEntry("oriya", "Kalinga", "KALINGA.TTF"));                  // ISO 639: or
595        def.add(new FontEntry("sinhala", "Iskoola Pota", "ISKPOTA.TTF"));           // ISO 639: si
596        def.add(new FontEntry("yi", "Yi Baiti", "MSYI.TTF"));                       // ISO 639: ii
597
598        // Windows XP and later
599        def.add(new FontEntry("gujarati", "Shruti", "SHRUTI.TTF"));
600        def.add(new FontEntry("kannada", "Tunga", "TUNGA.TTF"));
601        def.add(new FontEntry("gurmukhi", "Raavi", "RAAVI.TTF"));
602        def.add(new FontEntry("telugu", "Gautami", "GAUTAMI.TTF"));
603        def.add(new FontEntry("bengali", "Vrinda", "VRINDA.TTF"));                  // since XP SP2
604        def.add(new FontEntry("syriac", "Estrangelo Edessa", "ESTRE.TTF"));         // ISO 639: arc
605        def.add(new FontEntry("thaana", "MV Boli", "MVBOLI.TTF"));                  // ISO 639: dv
606        def.add(new FontEntry("malayalam", "Kartika", "KARTIKA.TTF"));              // ISO 639: ml; since XP SP2
607
608        // Windows 2000 and later
609        def.add(new FontEntry("tamil", "Latha", "LATHA.TTF"));
610
611        // Comes with MS Office & Outlook 2000. Good unicode coverage, so add if available.
612        def.add(new FontEntry("arialuni", "Arial Unicode MS", "ARIALUNI.TTF"));
613
614        return def;
615    }
616
617    /**
618     * Determines if the .NET framework 4.5 (or later) is installed.
619     * Windows 7 ships by default with an older version.
620     * @return {@code true} if the .NET framework 4.5 (or later) is installed.
621     * @since 13463
622     */
623    public static boolean isDotNet45Installed() {
624        try {
625            // https://docs.microsoft.com/en-us/dotnet/framework/migration-guide/how-to-determine-which-versions-are-installed#net_d
626            // "The existence of the Release DWORD indicates that the .NET Framework 4.5 or later has been installed"
627            // Great, but our WinRegistry only handles REG_SZ type, so we have to check the Version key
628            String version = WinRegistry.readString(HKEY_LOCAL_MACHINE, "SOFTWARE\\Microsoft\\NET Framework Setup\\NDP\\v4\\Full", "Version");
629            if (version != null) {
630                Matcher m = MS_VERSION_PATTERN.matcher(version);
631                if (m.matches()) {
632                    int maj = Integer.parseInt(m.group(1));
633                    int min = Integer.parseInt(m.group(2));
634                    return (maj == 4 && min >= 5) || maj > 4;
635                }
636            }
637        } catch (IllegalAccessException | InvocationTargetException | NumberFormatException e) {
638            Logging.error(e);
639        }
640        return false;
641    }
642
643    /**
644     * Returns the major version number of PowerShell.
645     * @return the major version number of PowerShell. -1 in case of error
646     * @since 13465
647     */
648    public static int getPowerShellVersion() {
649        try {
650            String version = WinRegistry.readString(
651                    HKEY_LOCAL_MACHINE, "SOFTWARE\\Microsoft\\Powershell\\3\\PowershellEngine", "PowershellVersion");
652            if (version != null) {
653                Matcher m = MS_VERSION_PATTERN.matcher(version);
654                if (m.matches()) {
655                    return Integer.parseInt(m.group(1));
656                }
657            }
658        } catch (NumberFormatException | IllegalAccessException | InvocationTargetException e) {
659            Logging.error(e);
660        }
661        return -1;
662    }
663
664    /**
665     * Performs a web request using Windows CryptoAPI (through PowerShell).
666     * This is useful to ensure Windows trust store will contain a specific root CA.
667     * @param uri the web URI to request
668     * @return HTTP response from the given URI
669     * @throws IOException if any I/O error occurs
670     * @since 13458
671     */
672    public static String webRequest(String uri) throws IOException {
673        // With PS 6.0 (not yet released in Windows) we could simply use:
674        // Invoke-WebRequest -SSlProtocol Tsl12 $uri
675        // .NET framework < 4.5 does not support TLS 1.2 (https://stackoverflow.com/a/43240673/2257172)
676        if (isDotNet45Installed() && getPowerShellVersion() >= 3) {
677            try {
678                // The following works with PS 3.0 (Windows 8+), https://stackoverflow.com/a/41618979/2257172
679                return Utils.execOutput(Arrays.asList("powershell", "-Command",
680                        "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12;"+
681                        "[System.Net.WebRequest]::Create('"+uri+"').GetResponse()"
682                        ), 5, TimeUnit.SECONDS);
683            } catch (ExecutionException | InterruptedException e) {
684                Logging.warn("Unable to request certificate of " + uri);
685                Logging.debug(e);
686            }
687        }
688        return null;
689    }
690
691    @Override
692    public File resolveFileLink(File file) {
693        if (file.getName().endsWith(".lnk")) {
694            try {
695                return new File(new WindowsShortcut(file).getRealFilename());
696            } catch (IOException | ParseException e) {
697                Logging.error(e);
698            }
699        }
700        return file;
701    }
702
703    @Override
704    public Collection<String> getPossiblePreferenceDirs() {
705        Set<String> locations = new HashSet<>();
706        String appdata = getSystemEnv("APPDATA");
707        if (appdata != null && getSystemEnv("ALLUSERSPROFILE") != null
708                && appdata.lastIndexOf(File.separator) != -1) {
709            appdata = appdata.substring(appdata.lastIndexOf(File.separator));
710            locations.add(new File(new File(getSystemEnv("ALLUSERSPROFILE"), appdata), "JOSM").getPath());
711        }
712        return locations;
713    }
714}