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.awt.GraphicsEnvironment;
035import java.io.BufferedWriter;
036import java.io.File;
037import java.io.IOException;
038import java.io.InputStream;
039import java.io.OutputStream;
040import java.io.OutputStreamWriter;
041import java.io.Writer;
042import java.lang.reflect.InvocationTargetException;
043import java.net.URI;
044import java.net.URISyntaxException;
045import java.nio.charset.StandardCharsets;
046import java.nio.file.DirectoryIteratorException;
047import java.nio.file.DirectoryStream;
048import java.nio.file.FileSystems;
049import java.nio.file.Files;
050import java.nio.file.InvalidPathException;
051import java.nio.file.Path;
052import java.security.InvalidKeyException;
053import java.security.KeyFactory;
054import java.security.KeyStore;
055import java.security.KeyStoreException;
056import java.security.MessageDigest;
057import java.security.NoSuchAlgorithmException;
058import java.security.NoSuchProviderException;
059import java.security.PublicKey;
060import java.security.SignatureException;
061import java.security.cert.Certificate;
062import java.security.cert.CertificateException;
063import java.security.cert.X509Certificate;
064import java.security.spec.InvalidKeySpecException;
065import java.security.spec.X509EncodedKeySpec;
066import java.text.ParseException;
067import java.util.ArrayList;
068import java.util.Arrays;
069import java.util.Collection;
070import java.util.Enumeration;
071import java.util.HashSet;
072import java.util.List;
073import java.util.Locale;
074import java.util.Properties;
075import java.util.Set;
076import java.util.concurrent.ExecutionException;
077import java.util.concurrent.TimeUnit;
078import java.util.regex.Matcher;
079import java.util.regex.Pattern;
080
081import javax.swing.JOptionPane;
082
083import org.openstreetmap.josm.data.Preferences;
084import org.openstreetmap.josm.data.StructUtils;
085import org.openstreetmap.josm.data.StructUtils.StructEntry;
086import org.openstreetmap.josm.data.StructUtils.WriteExplicitly;
087import org.openstreetmap.josm.gui.MainApplication;
088import org.openstreetmap.josm.io.CertificateAmendment.NativeCertAmend;
089import org.openstreetmap.josm.spi.preferences.Config;
090
091/**
092 * {@code PlatformHook} implementation for Microsoft Windows systems.
093 * @since 1023
094 */
095public class PlatformHookWindows implements PlatformHook {
096
097    /**
098     * Pattern of Microsoft .NET and Powershell version numbers in registry.
099     */
100    private static final Pattern MS_VERSION_PATTERN = Pattern.compile("(\\d+)\\.(\\d+)(\\.\\d+.*)?");
101
102    /**
103     * Simple data class to hold information about a font.
104     *
105     * Used for fontconfig.properties files.
106     */
107    public static class FontEntry {
108        /**
109         * The character subset. Basically a free identifier, but should be unique.
110         */
111        @StructEntry
112        public String charset;
113
114        /**
115         * Platform font name.
116         */
117        @StructEntry
118        @WriteExplicitly
119        public String name = "";
120
121        /**
122         * File name.
123         */
124        @StructEntry
125        @WriteExplicitly
126        public String file = "";
127
128        /**
129         * Constructs a new {@code FontEntry}.
130         */
131        public FontEntry() {
132            // Default constructor needed for construction by reflection
133        }
134
135        /**
136         * Constructs a new {@code FontEntry}.
137         * @param charset The character subset. Basically a free identifier, but should be unique
138         * @param name Platform font name
139         * @param file File name
140         */
141        public FontEntry(String charset, String name, String file) {
142            this.charset = charset;
143            this.name = name;
144            this.file = file;
145        }
146    }
147
148    private static final byte[] INSECURE_PUBLIC_KEY = new byte[] {
149        0x30, (byte) 0x82, 0x1, 0x22, 0x30, 0xd, 0x6, 0x9, 0x2a, (byte) 0x86, 0x48,
150        (byte) 0x86, (byte) 0xf7, 0xd, 0x1, 0x1, 0x1, 0x5, 0x0, 0x3, (byte) 0x82, 0x1, 0xf, 0x0,
151        0x30, (byte) 0x82, 0x01, 0x0a, 0x02, (byte) 0x82, 0x01, 0x01, 0x00, (byte) 0x95, (byte) 0x95, (byte) 0x88,
152        (byte) 0x84, (byte) 0xc8, (byte) 0xd9, 0x6b, (byte) 0xc5, (byte) 0xda, 0x0b, 0x69, (byte) 0xbf, (byte) 0xfc,
153        0x7e, (byte) 0xb9, (byte) 0x96, 0x2c, (byte) 0xeb, (byte) 0x8f, (byte) 0xbc, 0x6e, 0x40, (byte) 0xe6, (byte) 0xe2,
154        (byte) 0xfc, (byte) 0xf1, 0x7f, 0x73, (byte) 0xa7, (byte) 0x9d, (byte) 0xde, (byte) 0xc7, (byte) 0x88, 0x57, 0x51,
155        (byte) 0x84, (byte) 0xed, (byte) 0x96, (byte) 0xfb, (byte) 0xe1, 0x38, (byte) 0xef, 0x08, 0x2b, (byte) 0xf3,
156        (byte) 0xc7, (byte) 0xc3, 0x5d, (byte) 0xfe, (byte) 0xf9, 0x51, (byte) 0xe6, 0x29, (byte) 0xfc, (byte) 0xe5, 0x0d,
157        (byte) 0xa1, 0x0d, (byte) 0xa8, (byte) 0xb4, (byte) 0xae, 0x26, 0x18, 0x19, 0x4d, 0x6c, 0x0c, 0x3b, 0x12, (byte) 0xba,
158        (byte) 0xbc, 0x5f, 0x32, (byte) 0xb3, (byte) 0xbe, (byte) 0x9d, 0x17, 0x0d, 0x4d, 0x2f, 0x1a, 0x48, (byte) 0xb7,
159        (byte) 0xac, (byte) 0xf7, 0x1a, 0x43, 0x01, (byte) 0x97, (byte) 0xf4, (byte) 0xf8, 0x4c, (byte) 0xbb, 0x6a, (byte) 0xbc,
160        0x33, (byte) 0xe1, 0x73, 0x1e, (byte) 0x86, (byte) 0xfb, 0x2e, (byte) 0xb1, 0x63, 0x75, (byte) 0x85, (byte) 0xdc,
161        (byte) 0x82, 0x6c, 0x28, (byte) 0xf1, (byte) 0xe3, (byte) 0x90, 0x63, (byte) 0x9d, 0x3d, 0x48, (byte) 0x8a, (byte) 0x8c,
162        0x47, (byte) 0xe2, 0x10, 0x0b, (byte) 0xef, (byte) 0x91, (byte) 0x94, (byte) 0xb0, 0x6c, 0x4c, (byte) 0x80, 0x76, 0x03,
163        (byte) 0xe1, (byte) 0xb6, (byte) 0x90, (byte) 0x87, (byte) 0xd9, (byte) 0xae, (byte) 0xf4, (byte) 0x8e, (byte) 0xe0,
164        (byte) 0x9f, (byte) 0xe7, 0x3a, 0x2c, 0x2f, 0x21, (byte) 0xd4, 0x46, (byte) 0xba, (byte) 0x95, 0x70, (byte) 0xa9, 0x5b,
165        0x20, 0x2a, (byte) 0xfa, 0x52, 0x3e, (byte) 0x9d, (byte) 0xd9, (byte) 0xef, 0x28, (byte) 0xc5, (byte) 0xd1, 0x60,
166        (byte) 0x89, 0x68, 0x6e, 0x7f, (byte) 0xd7, (byte) 0x9e, (byte) 0x89, 0x4c, (byte) 0xeb, 0x4d, (byte) 0xd2, (byte) 0xc6,
167        (byte) 0xf4, 0x2d, 0x02, 0x5d, (byte) 0xda, (byte) 0xde, 0x33, (byte) 0xfe, (byte) 0xc1, 0x7e, (byte) 0xde, 0x4f, 0x1f,
168        (byte) 0x9b, 0x6e, 0x6f, 0x0f, 0x66, 0x71, 0x19, (byte) 0xe9, 0x43, 0x3c, (byte) 0x83, 0x0a, 0x0f, 0x28, 0x21, (byte) 0xc8,
169        0x38, (byte) 0xd3, 0x4e, 0x48, (byte) 0xdf, (byte) 0xd4, (byte) 0x99, (byte) 0xb5, (byte) 0xc6, (byte) 0x8d, (byte) 0xd4,
170        (byte) 0xc1, 0x69, 0x58, 0x79, (byte) 0x82, 0x32, (byte) 0x82, (byte) 0xd4, (byte) 0x86, (byte) 0xe2, 0x04, 0x08, 0x63,
171        (byte) 0x87, (byte) 0xf0, 0x2a, (byte) 0xf6, (byte) 0xec, 0x3e, 0x51, 0x0f, (byte) 0xda, (byte) 0xb4, 0x67, 0x19, 0x5e,
172        0x16, 0x02, (byte) 0x9f, (byte) 0xf1, 0x19, 0x0c, 0x3e, (byte) 0xb8, 0x04, 0x49, 0x07, 0x53, 0x02, 0x03, 0x01, 0x00, 0x01
173    };
174
175    private static final String WINDOWS_ROOT = "Windows-ROOT";
176
177    private static final String CURRENT_VERSION = "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion";
178
179    private String oSBuildNumber;
180
181    @Override
182    public Platform getPlatform() {
183        return Platform.WINDOWS;
184    }
185
186    @Override
187    public void afterPrefStartupHook() {
188        extendFontconfig("fontconfig.properties.src");
189    }
190
191    @Override
192    public void startupHook(JavaExpirationCallback callback) {
193        checkExpiredJava(callback);
194    }
195
196    @Override
197    public void openUrl(String url) throws IOException {
198        if (!url.startsWith("file:/")) {
199            final String customBrowser = Config.getPref().get("browser.windows", "");
200            if (!customBrowser.isEmpty()) {
201                Runtime.getRuntime().exec(new String[]{customBrowser, url});
202                return;
203            }
204        }
205        try {
206            // Desktop API works fine under Windows
207            Desktop.getDesktop().browse(new URI(url));
208        } catch (IOException | URISyntaxException e) {
209            Logging.log(Logging.LEVEL_WARN, "Desktop class failed. Platform dependent fall back for open url in browser.", e);
210            Runtime.getRuntime().exec(new String[]{"rundll32", "url.dll,FileProtocolHandler", url});
211        }
212    }
213
214    @Override
215    public void initSystemShortcuts() {
216        // CHECKSTYLE.OFF: LineLength
217        //Shortcut.registerSystemCut("system:menuexit", tr("reserved"), VK_Q, CTRL_DOWN_MASK);
218        Shortcut.registerSystemShortcut("system:duplicate", tr("reserved"), VK_D, CTRL_DOWN_MASK); // not really system, but to avoid odd results
219
220        // Windows 7 shortcuts: http://windows.microsoft.com/en-US/windows7/Keyboard-shortcuts
221
222        // Shortcuts with setAutomatic(): items with automatic shortcuts will not be added to the menu bar at all
223
224        // Don't know why Ctrl-Alt-Del isn't even listed on official Microsoft support page
225        Shortcut.registerSystemShortcut("system:reset", tr("reserved"), VK_DELETE, CTRL_DOWN_MASK | ALT_DOWN_MASK).setAutomatic();
226
227        // Ease of Access keyboard shortcuts
228        Shortcut.registerSystemShortcut("microsoft-reserved-01", tr("reserved"), VK_PRINTSCREEN, ALT_DOWN_MASK | SHIFT_DOWN_MASK).setAutomatic(); // Turn High Contrast on or off
229        Shortcut.registerSystemShortcut("microsoft-reserved-02", tr("reserved"), VK_NUM_LOCK, ALT_DOWN_MASK | SHIFT_DOWN_MASK).setAutomatic(); // Turn Mouse Keys on or off
230        //Shortcut.registerSystemCut("microsoft-reserved-03", tr("reserved"), VK_U, );// Open the Ease of Access Center (TODO: Windows-U, how to handle it in Java ?)
231
232        // General keyboard shortcuts
233        //Shortcut.registerSystemShortcut("system:help", tr("reserved"), VK_F1, 0);                            // Display Help
234        Shortcut.registerSystemShortcut("system:copy", tr("reserved"), VK_C, CTRL_DOWN_MASK);                // Copy the selected item
235        Shortcut.registerSystemShortcut("system:cut", tr("reserved"), VK_X, CTRL_DOWN_MASK);                 // Cut the selected item
236        Shortcut.registerSystemShortcut("system:paste", tr("reserved"), VK_V, CTRL_DOWN_MASK);               // Paste the selected item
237        Shortcut.registerSystemShortcut("system:undo", tr("reserved"), VK_Z, CTRL_DOWN_MASK);                // Undo an action
238        Shortcut.registerSystemShortcut("system:redo", tr("reserved"), VK_Y, CTRL_DOWN_MASK);                // Redo an action
239        //Shortcut.registerSystemCut("microsoft-reserved-10", tr("reserved"), VK_DELETE, 0);                  // Delete the selected item and move it to the Recycle Bin
240        //Shortcut.registerSystemCut("microsoft-reserved-11", tr("reserved"), VK_DELETE, SHIFT_DOWN_MASK);    // Delete the selected item without moving it to the Recycle Bin first
241        //Shortcut.registerSystemCut("system:rename", tr("reserved"), VK_F2, 0);                          // Rename the selected item
242        Shortcut.registerSystemShortcut("system:movefocusright", tr("reserved"), VK_RIGHT, CTRL_DOWN_MASK);  // Move the cursor to the beginning of the next word
243        Shortcut.registerSystemShortcut("system:movefocusleft", tr("reserved"), VK_LEFT, CTRL_DOWN_MASK);    // Move the cursor to the beginning of the previous word
244        Shortcut.registerSystemShortcut("system:movefocusdown", tr("reserved"), VK_DOWN, CTRL_DOWN_MASK);    // Move the cursor to the beginning of the next paragraph
245        Shortcut.registerSystemShortcut("system:movefocusup", tr("reserved"), VK_UP, CTRL_DOWN_MASK);        // Move the cursor to the beginning of the previous paragraph
246        //Shortcut.registerSystemCut("microsoft-reserved-17", tr("reserved"), VK_RIGHT, CTRL_DOWN_MASK | SHIFT_DOWN_MASK); // Select a block of text
247        //Shortcut.registerSystemCut("microsoft-reserved-18", tr("reserved"), VK_LEFT, CTRL_DOWN_MASK | SHIFT_DOWN_MASK);  // Select a block of text
248        //Shortcut.registerSystemCut("microsoft-reserved-19", tr("reserved"), VK_DOWN, CTRL_DOWN_MASK | SHIFT_DOWN_MASK);  // Select a block of text
249        //Shortcut.registerSystemCut("microsoft-reserved-20", tr("reserved"), VK_UP, CTRL_DOWN_MASK | SHIFT_DOWN_MASK);    // Select a block of text
250        //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
251        //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
252        //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
253        //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
254        //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 ?)
255        //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 ?)
256        //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 ?)
257        //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 ?)
258        Shortcut.registerSystemShortcut("system:selectall", tr("reserved"), VK_A, CTRL_DOWN_MASK);           // Select all items in a document or window
259        //Shortcut.registerSystemCut("system:search", tr("reserved"), VK_F3, 0);                          // Search for a file or folder
260        Shortcut.registerSystemShortcut("microsoft-reserved-31", tr("reserved"), VK_ENTER, ALT_DOWN_MASK).setAutomatic();   // Display properties for the selected item
261        Shortcut.registerSystemShortcut("system:exit", tr("reserved"), VK_F4, ALT_DOWN_MASK).setAutomatic(); // Close the active item, or exit the active program
262        Shortcut.registerSystemShortcut("microsoft-reserved-33", tr("reserved"), VK_SPACE, ALT_DOWN_MASK).setAutomatic();   // Open the shortcut menu for the active window
263        //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)
264        Shortcut.registerSystemShortcut("microsoft-reserved-35", tr("reserved"), VK_TAB, ALT_DOWN_MASK).setAutomatic();     // Switch between open items
265        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
266        //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 ?)
267        //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 ?)
268        Shortcut.registerSystemShortcut("microsoft-reserved-39", tr("reserved"), VK_ESCAPE, ALT_DOWN_MASK).setAutomatic();  // Cycle through items in the order in which they were opened
269        //Shortcut.registerSystemCut("microsoft-reserved-40", tr("reserved"), VK_F6, 0);                  // Cycle through screen elements in a window or on the desktop
270        //Shortcut.registerSystemCut("microsoft-reserved-41", tr("reserved"), VK_F4, 0);                  // Display the address bar list in Windows Explorer
271        Shortcut.registerSystemShortcut("microsoft-reserved-42", tr("reserved"), VK_F10, SHIFT_DOWN_MASK);   // Display the shortcut menu for the selected item
272        Shortcut.registerSystemShortcut("microsoft-reserved-43", tr("reserved"), VK_ESCAPE, CTRL_DOWN_MASK).setAutomatic(); // Open the Start menu
273        //Shortcut.registerSystemShortcut("microsoft-reserved-44", tr("reserved"), VK_F10, 0);                 // Activate the menu bar in the active program
274        //Shortcut.registerSystemCut("microsoft-reserved-45", tr("reserved"), VK_RIGHT, 0);               // Open the next menu to the right, or open a submenu
275        //Shortcut.registerSystemCut("microsoft-reserved-46", tr("reserved"), VK_LEFT, 0);                // Open the next menu to the left, or close a submenu
276        //Shortcut.registerSystemCut("microsoft-reserved-47", tr("reserved"), VK_F5, 0);                  // Refresh the active window
277        //Shortcut.registerSystemCut("microsoft-reserved-48", tr("reserved"), VK_UP, ALT_DOWN_MASK);      // View the folder one level up in Windows Explorer
278        //Shortcut.registerSystemCut("microsoft-reserved-49", tr("reserved"), VK_ESCAPE, 0);              // Cancel the current task
279        Shortcut.registerSystemShortcut("microsoft-reserved-50", tr("reserved"), VK_ESCAPE, CTRL_DOWN_MASK | SHIFT_DOWN_MASK).setAutomatic(); // Open Task Manager
280        Shortcut.registerSystemShortcut("microsoft-reserved-51", tr("reserved"), VK_SHIFT, ALT_DOWN_MASK).setAutomatic();   // Switch the input language when multiple input languages are enabled
281        Shortcut.registerSystemShortcut("microsoft-reserved-52", tr("reserved"), VK_SHIFT, CTRL_DOWN_MASK).setAutomatic();  // Switch the keyboard layout when multiple keyboard layouts are enabled
282        //Shortcut.registerSystemCut("microsoft-reserved-53", tr("reserved"), ); // Change the reading direction of text in right-to-left reading languages (TODO: unclear)
283        // CHECKSTYLE.ON: LineLength
284    }
285
286    @Override
287    public String getDefaultStyle() {
288        return "com.sun.java.swing.plaf.windows.WindowsLookAndFeel";
289    }
290
291    @Override
292    public boolean rename(File from, File to) {
293        if (to.exists())
294            Utils.deleteFile(to);
295        return from.renameTo(to);
296    }
297
298    @Override
299    public String getOSDescription() {
300        return Utils.strip(getSystemProperty("os.name")) + ' ' +
301                ((getSystemEnv("ProgramFiles(x86)") == null) ? "32" : "64") + "-Bit";
302    }
303
304    /**
305     * Returns the Windows product name from registry (example: "Windows 10 Pro")
306     * @return the Windows product name from registry
307     * @throws IllegalAccessException if Java language access control is enforced and the underlying method is inaccessible
308     * @throws InvocationTargetException if the underlying method throws an exception
309     * @since 12744
310     */
311    public static String getProductName() throws IllegalAccessException, InvocationTargetException {
312        return WinRegistry.readString(HKEY_LOCAL_MACHINE, CURRENT_VERSION, "ProductName");
313    }
314
315    /**
316     * Returns the Windows release identifier from registry (example: "1703")
317     * @return the Windows release identifier from registry
318     * @throws IllegalAccessException if Java language access control is enforced and the underlying method is inaccessible
319     * @throws InvocationTargetException if the underlying method throws an exception
320     * @since 12744
321     */
322    public static String getReleaseId() throws IllegalAccessException, InvocationTargetException {
323        return WinRegistry.readString(HKEY_LOCAL_MACHINE, CURRENT_VERSION, "ReleaseId");
324    }
325
326    /**
327     * Returns the Windows current build number from registry (example: "15063")
328     * @return the Windows current build number from registry
329     * @throws IllegalAccessException if Java language access control is enforced and the underlying method is inaccessible
330     * @throws InvocationTargetException if the underlying method throws an exception
331     * @since 12744
332     */
333    public static String getCurrentBuild() throws IllegalAccessException, InvocationTargetException {
334        return WinRegistry.readString(HKEY_LOCAL_MACHINE, CURRENT_VERSION, "CurrentBuild");
335    }
336
337    private static String buildOSBuildNumber() {
338        StringBuilder sb = new StringBuilder();
339        try {
340            sb.append(getProductName());
341            String releaseId = getReleaseId();
342            if (releaseId != null) {
343                sb.append(' ').append(releaseId);
344            }
345            sb.append(" (").append(getCurrentBuild()).append(')');
346        } catch (ReflectiveOperationException | JosmRuntimeException | NoClassDefFoundError e) {
347            Logging.log(Logging.LEVEL_ERROR, "Unable to get Windows build number", e);
348            Logging.debug(e);
349        }
350        return sb.toString();
351    }
352
353    @Override
354    public String getOSBuildNumber() {
355        if (oSBuildNumber == null) {
356            oSBuildNumber = buildOSBuildNumber();
357        }
358        return oSBuildNumber;
359    }
360
361    /**
362     * Loads Windows-ROOT keystore.
363     * @return Windows-ROOT keystore
364     * @throws NoSuchAlgorithmException if the algorithm used to check the integrity of the keystore cannot be found
365     * @throws CertificateException if any of the certificates in the keystore could not be loaded
366     * @throws IOException if there is an I/O or format problem with the keystore data, if a password is required but not given
367     * @throws KeyStoreException if no Provider supports a KeyStore implementation for the type "Windows-ROOT"
368     * @since 7343
369     */
370    public static KeyStore getRootKeystore() throws NoSuchAlgorithmException, CertificateException, IOException, KeyStoreException {
371        KeyStore ks = KeyStore.getInstance(WINDOWS_ROOT);
372        ks.load(null, null);
373        return ks;
374    }
375
376    /**
377     * Removes potential insecure certificates installed with previous versions of JOSM on Windows.
378     * @throws NoSuchAlgorithmException on unsupported signature algorithms
379     * @throws CertificateException if any of the certificates in the Windows keystore could not be loaded
380     * @throws KeyStoreException if no Provider supports a KeyStoreSpi implementation for the type "Windows-ROOT"
381     * @throws IOException if there is an I/O or format problem with the keystore data, if a password is required but not given
382     * @since 7335
383     */
384    public static void removeInsecureCertificates() throws NoSuchAlgorithmException, CertificateException, KeyStoreException, IOException {
385        // We offered before a public private key we need now to remove from Windows PCs as it might be a huge security risk (see #10230)
386        PublicKey insecurePubKey = null;
387        try {
388            insecurePubKey = KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(INSECURE_PUBLIC_KEY));
389        } catch (InvalidKeySpecException | NoSuchAlgorithmException e) {
390            Logging.error(e);
391            return;
392        }
393        KeyStore ks = getRootKeystore();
394        Enumeration<String> en = ks.aliases();
395        Collection<String> insecureCertificates = new ArrayList<>();
396        while (en.hasMoreElements()) {
397            String alias = en.nextElement();
398            // Look for certificates associated with a private key
399            if (ks.isKeyEntry(alias)) {
400                try {
401                    ks.getCertificate(alias).verify(insecurePubKey);
402                    // If no exception, this is a certificate signed with the insecure key -> remove it
403                    insecureCertificates.add(alias);
404                } catch (InvalidKeyException | NoSuchProviderException | SignatureException e) {
405                    // If exception this is not a certificate related to JOSM, just trace it
406                    Logging.trace(alias + " --> " + e.getClass().getName());
407                    Logging.trace(e);
408                }
409            }
410        }
411        // Remove insecure certificates
412        if (!insecureCertificates.isEmpty()) {
413            StringBuilder message = new StringBuilder("<html>");
414            message.append(tr("A previous version of JOSM has installed a custom certificate "+
415                    "in order to provide HTTPS support for Remote Control:"))
416                   .append("<br><ul>");
417            for (String alias : insecureCertificates) {
418                message.append("<li>")
419                       .append(alias)
420                       .append("</li>");
421            }
422            message.append("</ul>")
423                   .append(tr("It appears it could be an important <b>security risk</b>.<br><br>"+
424                    "You are now going to be prompted by Windows to remove this insecure certificate.<br>"+
425                    "For your own safety, <b>please click Yes</b> in next dialog."))
426                   .append("</html>");
427            JOptionPane.showMessageDialog(MainApplication.getMainFrame(), message.toString(), tr("Warning"), JOptionPane.WARNING_MESSAGE);
428            for (String alias : insecureCertificates) {
429                Logging.warn(tr("Removing insecure certificate from {0} keystore: {1}", WINDOWS_ROOT, alias));
430                try {
431                    ks.deleteEntry(alias);
432                } catch (KeyStoreException e) {
433                    Logging.log(Logging.LEVEL_ERROR, tr("Unable to remove insecure certificate from keystore: {0}", e.getMessage()), e);
434                }
435            }
436        }
437    }
438
439    @Override
440    public boolean setupHttpsCertificate(String entryAlias, KeyStore.TrustedCertificateEntry trustedCert)
441            throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException {
442        KeyStore ks = getRootKeystore();
443        // Look for certificate to install
444        try {
445            String alias = ks.getCertificateAlias(trustedCert.getTrustedCertificate());
446            if (alias != null) {
447                // JOSM certificate found, return
448                Logging.debug(tr("JOSM localhost certificate found in {0} keystore: {1}", WINDOWS_ROOT, alias));
449                return false;
450            }
451        } catch (ArrayIndexOutOfBoundsException e) {
452            // catch error of JDK-8172244 as bug seems to not be fixed anytime soon
453            Logging.log(Logging.LEVEL_ERROR, "JDK-8172244 occurred. Abort HTTPS setup", e);
454            return false;
455        }
456        if (!GraphicsEnvironment.isHeadless()) {
457            // JOSM certificate not found, warn user
458            StringBuilder message = new StringBuilder("<html>");
459            message.append(tr("Remote Control is configured to provide HTTPS support.<br>"+
460                    "This requires to add a custom certificate generated by JOSM to the Windows Root CA store.<br><br>"+
461                    "You are now going to be prompted by Windows to confirm this operation.<br>"+
462                    "To enable proper HTTPS support, <b>please click Yes</b> in next dialog.<br><br>"+
463                    "If unsure, you can also click No then disable HTTPS support in Remote Control preferences."))
464                   .append("</html>");
465            JOptionPane.showMessageDialog(MainApplication.getMainFrame(), message.toString(),
466                    tr("HTTPS support in Remote Control"), JOptionPane.INFORMATION_MESSAGE);
467        }
468        // install it to Windows-ROOT keystore, used by IE, Chrome and Safari, but not by Firefox
469        Logging.info(tr("Adding JOSM localhost certificate to {0} keystore", WINDOWS_ROOT));
470        ks.setEntry(entryAlias, trustedCert, null);
471        return true;
472    }
473
474    @Override
475    public X509Certificate getX509Certificate(NativeCertAmend certAmend)
476            throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException {
477        // Get Windows Trust Root Store
478        KeyStore ks = getRootKeystore();
479        // Search by alias (fast)
480        Certificate result = ks.getCertificate(certAmend.getWinAlias());
481        if (result == null) {
482            // Make a web request to target site to force Windows to update if needed its trust root store from its certificate trust list
483            // A better, but a lot more complex method might be to get certificate list from Windows Registry with PowerShell
484            // using (Get-ItemProperty -Path 'HKLM:\\SOFTWARE\\Microsoft\\SystemCertificates\\AuthRoot\\AutoUpdate').EncodedCtl)
485            // then decode it using CertUtil -dump or calling CertCreateCTLContext API using JNI, and finally find and decode the certificate
486            Logging.trace(webRequest(certAmend.getWebSite()));
487            // Reload Windows Trust Root Store and search again by alias (fast)
488            ks = getRootKeystore();
489            result = ks.getCertificate(certAmend.getWinAlias());
490        }
491        if (result instanceof X509Certificate) {
492            return (X509Certificate) result;
493        }
494        // If not found, search by SHA-256 (slower)
495        MessageDigest md = MessageDigest.getInstance("SHA-256");
496        for (Enumeration<String> aliases = ks.aliases(); aliases.hasMoreElements();) {
497            result = ks.getCertificate(aliases.nextElement());
498            if (result instanceof X509Certificate
499                    && certAmend.getSha256().equalsIgnoreCase(Utils.toHexString(md.digest(result.getEncoded())))) {
500                return (X509Certificate) result;
501            }
502        }
503        // Not found
504        return null;
505    }
506
507    @Override
508    public File getDefaultCacheDirectory() {
509        String p = getSystemEnv("LOCALAPPDATA");
510        if (p == null || p.isEmpty()) {
511            // Fallback for Windows OS earlier than Windows Vista, where the variable is not defined
512            p = getSystemEnv("APPDATA");
513        }
514        return new File(new File(p, Preferences.getJOSMDirectoryBaseName()), "cache");
515    }
516
517    @Override
518    public File getDefaultPrefDirectory() {
519        return new File(getSystemEnv("APPDATA"), Preferences.getJOSMDirectoryBaseName());
520    }
521
522    @Override
523    public File getDefaultUserDataDirectory() {
524        // Use preferences directory by default
525        return Config.getDirs().getPreferencesDirectory(false);
526    }
527
528    /**
529     * <p>Add more fallback fonts to the Java runtime, in order to get
530     * support for more scripts.</p>
531     *
532     * <p>The font configuration in Java doesn't include some Indic scripts,
533     * even though MS Windows ships with fonts that cover these unicode ranges.</p>
534     *
535     * <p>To fix this, the fontconfig.properties template is copied to the JOSM
536     * cache folder. Then, the additional entries are added to the font
537     * configuration. Finally the system property "sun.awt.fontconfig" is set
538     * to the customized fontconfig.properties file.</p>
539     *
540     * <p>This is a crude hack, but better than no font display at all for these languages.
541     * There is no guarantee, that the template file
542     * ($JAVA_HOME/lib/fontconfig.properties.src) matches the default
543     * configuration (which is in a binary format).
544     * Furthermore, the system property "sun.awt.fontconfig" is undocumented and
545     * may no longer work in future versions of Java.</p>
546     *
547     * <p>Related Java bug: <a href="https://bugs.openjdk.java.net/browse/JDK-8008572">JDK-8008572</a></p>
548     *
549     * @param templateFileName file name of the fontconfig.properties template file
550     */
551    protected void extendFontconfig(String templateFileName) {
552        String customFontconfigFile = Config.getPref().get("fontconfig.properties", null);
553        if (customFontconfigFile != null) {
554            Utils.updateSystemProperty("sun.awt.fontconfig", customFontconfigFile);
555            return;
556        }
557        if (!Config.getPref().getBoolean("font.extended-unicode", true))
558            return;
559
560        String javaLibPath = getSystemProperty("java.home") + File.separator + "lib";
561        Path templateFile = FileSystems.getDefault().getPath(javaLibPath, templateFileName);
562        String templatePath = templateFile.toString();
563        if (templatePath.startsWith("null") || !Files.isReadable(templateFile)) {
564            Logging.warn("extended font config - unable to find font config template file {0}", templatePath);
565            return;
566        }
567        try (InputStream fis = Files.newInputStream(templateFile)) {
568            Properties props = new Properties();
569            props.load(fis);
570            byte[] content = Files.readAllBytes(templateFile);
571            File cachePath = Config.getDirs().getCacheDirectory(true);
572            Path fontconfigFile = cachePath.toPath().resolve("fontconfig.properties");
573            OutputStream os = Files.newOutputStream(fontconfigFile);
574            os.write(content);
575            try (Writer w = new BufferedWriter(new OutputStreamWriter(os, StandardCharsets.UTF_8))) {
576                Collection<FontEntry> extrasPref = StructUtils.getListOfStructs(Config.getPref(),
577                        "font.extended-unicode.extra-items", getAdditionalFonts(), FontEntry.class);
578                Collection<FontEntry> extras = new ArrayList<>();
579                w.append("\n\n# Added by JOSM to extend unicode coverage of Java font support:\n\n");
580                List<String> allCharSubsets = new ArrayList<>();
581                for (FontEntry entry: extrasPref) {
582                    Collection<String> fontsAvail = getInstalledFonts();
583                    if (fontsAvail != null && fontsAvail.contains(entry.file.toUpperCase(Locale.ENGLISH))) {
584                        if (!allCharSubsets.contains(entry.charset)) {
585                            allCharSubsets.add(entry.charset);
586                            extras.add(entry);
587                        } else {
588                            Logging.trace("extended font config - already registered font for charset ''{0}'' - skipping ''{1}''",
589                                    entry.charset, entry.name);
590                        }
591                    } else {
592                        Logging.trace("extended font config - Font ''{0}'' not found on system - skipping", entry.name);
593                    }
594                }
595                for (FontEntry entry: extras) {
596                    allCharSubsets.add(entry.charset);
597                    if ("".equals(entry.name)) {
598                        continue;
599                    }
600                    String key = "allfonts." + entry.charset;
601                    String value = entry.name;
602                    String prevValue = props.getProperty(key);
603                    if (prevValue != null && !prevValue.equals(value)) {
604                        Logging.warn("extended font config - overriding ''{0}={1}'' with ''{2}''", key, prevValue, value);
605                    }
606                    w.append(key + '=' + value + '\n');
607                }
608                w.append('\n');
609                for (FontEntry entry: extras) {
610                    if ("".equals(entry.name) || "".equals(entry.file)) {
611                        continue;
612                    }
613                    String key = "filename." + entry.name.replace(' ', '_');
614                    String value = entry.file;
615                    String prevValue = props.getProperty(key);
616                    if (prevValue != null && !prevValue.equals(value)) {
617                        Logging.warn("extended font config - overriding ''{0}={1}'' with ''{2}''", key, prevValue, value);
618                    }
619                    w.append(key + '=' + value + '\n');
620                }
621                w.append('\n');
622                String fallback = props.getProperty("sequence.fallback");
623                if (fallback != null) {
624                    w.append("sequence.fallback=" + fallback + ',' + Utils.join(",", allCharSubsets) + '\n');
625                } else {
626                    w.append("sequence.fallback=" + Utils.join(",", allCharSubsets) + '\n');
627                }
628            }
629            Utils.updateSystemProperty("sun.awt.fontconfig", fontconfigFile.toString());
630        } catch (IOException | InvalidPathException ex) {
631            Logging.error(ex);
632        }
633    }
634
635    /**
636     * Get a list of fonts that are installed on the system.
637     *
638     * Must be done without triggering the Java Font initialization.
639     * (See {@link #extendFontconfig(java.lang.String)}, have to set system
640     * property first, which is then read by sun.awt.FontConfiguration upon initialization.)
641     *
642     * @return list of file names
643     */
644    protected Collection<String> getInstalledFonts() {
645        // Cannot use GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames()
646        // because we have to set the system property before Java initializes its fonts.
647        // Use more low-level method to find the installed fonts.
648        List<String> fontsAvail = new ArrayList<>();
649        Path fontPath = FileSystems.getDefault().getPath(getSystemEnv("SYSTEMROOT"), "Fonts");
650        try (DirectoryStream<Path> ds = Files.newDirectoryStream(fontPath)) {
651            for (Path p : ds) {
652                Path filename = p.getFileName();
653                if (filename != null) {
654                    fontsAvail.add(filename.toString().toUpperCase(Locale.ENGLISH));
655                }
656            }
657            fontsAvail.add(""); // for devanagari
658        } catch (IOException | DirectoryIteratorException ex) {
659            Logging.log(Logging.LEVEL_ERROR, ex);
660            Logging.warn("extended font config - failed to load available Fonts");
661            fontsAvail = null;
662        }
663        return fontsAvail;
664    }
665
666    /**
667     * Get default list of additional fonts to add to the configuration.
668     *
669     * Java will choose thee first font in the list that can render a certain character.
670     *
671     * @return list of FontEntry objects
672     */
673    protected Collection<FontEntry> getAdditionalFonts() {
674        Collection<FontEntry> def = new ArrayList<>(33);
675        def.add(new FontEntry("devanagari", "", "")); // just include in fallback list font already defined in template
676
677        // Windows scripts: https://msdn.microsoft.com/en-us/goglobal/bb688099.aspx
678        // IE default fonts: https://msdn.microsoft.com/en-us/library/ie/dn467844(v=vs.85).aspx
679
680        // Windows 10 and later
681        def.add(new FontEntry("historic", "Segoe UI Historic", "SEGUIHIS.TTF"));       // historic charsets
682
683        // Windows 8/8.1 and later
684        def.add(new FontEntry("javanese", "Javanese Text", "JAVATEXT.TTF"));           // ISO 639: jv
685        def.add(new FontEntry("leelawadee", "Leelawadee", "LEELAWAD.TTF"));            // ISO 639: bug
686        def.add(new FontEntry("malgun", "Malgun Gothic", "MALGUN.TTF"));               // ISO 639: ko
687        def.add(new FontEntry("myanmar", "Myanmar Text", "MMRTEXT.TTF"));              // ISO 639: my
688        def.add(new FontEntry("nirmala", "Nirmala UI", "NIRMALA.TTF"));                // ISO 639: sat,srb
689        def.add(new FontEntry("segoeui", "Segoe UI", "SEGOEUI.TTF"));                  // ISO 639: lis
690        def.add(new FontEntry("emoji", "Segoe UI Emoji", "SEGUIEMJ.TTF"));             // emoji symbol characters
691
692        // Windows 7 and later
693        def.add(new FontEntry("nko_tifinagh_vai_osmanya", "Ebrima", "EBRIMA.TTF"));    // ISO 639: ber. Nko only since Win 8
694        def.add(new FontEntry("khmer1", "Khmer UI", "KHMERUI.TTF"));                   // ISO 639: km
695        def.add(new FontEntry("lao1", "Lao UI", "LAOUI.TTF"));                         // ISO 639: lo
696        def.add(new FontEntry("tai_le", "Microsoft Tai Le", "TAILE.TTF"));             // ISO 639: khb
697        def.add(new FontEntry("new_tai_lue", "Microsoft New Tai Lue", "NTHAILU.TTF")); // ISO 639: khb
698
699        // Windows Vista and later:
700        def.add(new FontEntry("ethiopic", "Nyala", "NYALA.TTF"));                   // ISO 639: am,gez,ti
701        def.add(new FontEntry("tibetan", "Microsoft Himalaya", "HIMALAYA.TTF"));    // ISO 639: bo,dz
702        def.add(new FontEntry("cherokee", "Plantagenet Cherokee", "PLANTC.TTF"));   // ISO 639: chr
703        def.add(new FontEntry("unified_canadian", "Euphemia", "EUPHEMIA.TTF"));     // ISO 639: cr,in
704        def.add(new FontEntry("khmer2", "DaunPenh", "DAUNPENH.TTF"));               // ISO 639: km
705        def.add(new FontEntry("khmer3", "MoolBoran", "MOOLBOR.TTF"));               // ISO 639: km
706        def.add(new FontEntry("lao_thai", "DokChampa", "DOKCHAMP.TTF"));            // ISO 639: lo
707        def.add(new FontEntry("mongolian", "Mongolian Baiti", "MONBAITI.TTF"));     // ISO 639: mn
708        def.add(new FontEntry("oriya", "Kalinga", "KALINGA.TTF"));                  // ISO 639: or
709        def.add(new FontEntry("sinhala", "Iskoola Pota", "ISKPOTA.TTF"));           // ISO 639: si
710        def.add(new FontEntry("yi", "Yi Baiti", "MSYI.TTF"));                       // ISO 639: ii
711
712        // Windows XP and later
713        def.add(new FontEntry("gujarati", "Shruti", "SHRUTI.TTF"));
714        def.add(new FontEntry("kannada", "Tunga", "TUNGA.TTF"));
715        def.add(new FontEntry("gurmukhi", "Raavi", "RAAVI.TTF"));
716        def.add(new FontEntry("telugu", "Gautami", "GAUTAMI.TTF"));
717        def.add(new FontEntry("bengali", "Vrinda", "VRINDA.TTF"));                  // since XP SP2
718        def.add(new FontEntry("syriac", "Estrangelo Edessa", "ESTRE.TTF"));         // ISO 639: arc
719        def.add(new FontEntry("thaana", "MV Boli", "MVBOLI.TTF"));                  // ISO 639: dv
720        def.add(new FontEntry("malayalam", "Kartika", "KARTIKA.TTF"));              // ISO 639: ml; since XP SP2
721
722        // Windows 2000 and later
723        def.add(new FontEntry("tamil", "Latha", "LATHA.TTF"));
724
725        // Comes with MS Office & Outlook 2000. Good unicode coverage, so add if available.
726        def.add(new FontEntry("arialuni", "Arial Unicode MS", "ARIALUNI.TTF"));
727
728        return def;
729    }
730
731    /**
732     * Determines if the .NET framework 4.5 (or later) is installed.
733     * Windows 7 ships by default with an older version.
734     * @return {@code true} if the .NET framework 4.5 (or later) is installed.
735     * @since 13463
736     */
737    public static boolean isDotNet45Installed() {
738        try {
739            // https://docs.microsoft.com/en-us/dotnet/framework/migration-guide/how-to-determine-which-versions-are-installed#net_d
740            // "The existence of the Release DWORD indicates that the .NET Framework 4.5 or later has been installed"
741            // Great, but our WinRegistry only handles REG_SZ type, so we have to check the Version key
742            String version = WinRegistry.readString(HKEY_LOCAL_MACHINE, "SOFTWARE\\Microsoft\\NET Framework Setup\\NDP\\v4\\Full", "Version");
743            if (version != null) {
744                Matcher m = MS_VERSION_PATTERN.matcher(version);
745                if (m.matches()) {
746                    int maj = Integer.parseInt(m.group(1));
747                    int min = Integer.parseInt(m.group(2));
748                    return (maj == 4 && min >= 5) || maj > 4;
749                }
750            }
751        } catch (IllegalAccessException | InvocationTargetException | NumberFormatException e) {
752            Logging.error(e);
753        }
754        return false;
755    }
756
757    /**
758     * Returns the major version number of PowerShell.
759     * @return the major version number of PowerShell. -1 in case of error
760     * @since 13465
761     */
762    public static int getPowerShellVersion() {
763        try {
764            String version = WinRegistry.readString(
765                    HKEY_LOCAL_MACHINE, "SOFTWARE\\Microsoft\\Powershell\\3\\PowershellEngine", "PowershellVersion");
766            if (version != null) {
767                Matcher m = MS_VERSION_PATTERN.matcher(version);
768                if (m.matches()) {
769                    return Integer.parseInt(m.group(1));
770                }
771            }
772        } catch (NumberFormatException | IllegalAccessException | InvocationTargetException e) {
773            Logging.error(e);
774        }
775        return -1;
776    }
777
778    /**
779     * Performs a web request using Windows CryptoAPI (through PowerShell).
780     * This is useful to ensure Windows trust store will contain a specific root CA.
781     * @param uri the web URI to request
782     * @return HTTP response from the given URI
783     * @throws IOException if any I/O error occurs
784     * @since 13458
785     */
786    public static String webRequest(String uri) throws IOException {
787        // With PS 6.0 (not yet released in Windows) we could simply use:
788        // Invoke-WebRequest -SSlProtocol Tsl12 $uri
789        // .NET framework < 4.5 does not support TLS 1.2 (https://stackoverflow.com/a/43240673/2257172)
790        if (isDotNet45Installed() && getPowerShellVersion() >= 3) {
791            try {
792                // The following works with PS 3.0 (Windows 8+), https://stackoverflow.com/a/41618979/2257172
793                return Utils.execOutput(Arrays.asList("powershell", "-Command",
794                        "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12;"+
795                        "[System.Net.WebRequest]::Create('"+uri+"').GetResponse()"
796                        ), 5, TimeUnit.SECONDS);
797            } catch (ExecutionException | InterruptedException e) {
798                Logging.warn("Unable to request certificate of " + uri);
799                Logging.debug(e);
800            }
801        }
802        return null;
803    }
804
805    @Override
806    public File resolveFileLink(File file) {
807        if (file.getName().endsWith(".lnk")) {
808            try {
809                return new File(new WindowsShortcut(file).getRealFilename());
810            } catch (IOException | ParseException e) {
811                Logging.error(e);
812            }
813        }
814        return file;
815    }
816
817    @Override
818    public Collection<String> getPossiblePreferenceDirs() {
819        Set<String> locations = new HashSet<>();
820        String appdata = getSystemEnv("APPDATA");
821        if (appdata != null && getSystemEnv("ALLUSERSPROFILE") != null
822                && appdata.lastIndexOf(File.separator) != -1) {
823            appdata = appdata.substring(appdata.lastIndexOf(File.separator));
824            locations.add(new File(new File(getSystemEnv("ALLUSERSPROFILE"), appdata), "JOSM").getPath());
825        }
826        return locations;
827    }
828}