001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.tools;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.Desktop;
007import java.awt.event.KeyEvent;
008import java.io.BufferedReader;
009import java.io.File;
010import java.io.IOException;
011import java.io.InputStream;
012import java.net.URI;
013import java.net.URISyntaxException;
014import java.nio.charset.StandardCharsets;
015import java.nio.file.Files;
016import java.nio.file.Path;
017import java.nio.file.Paths;
018import java.security.KeyStoreException;
019import java.security.NoSuchAlgorithmException;
020import java.security.cert.CertificateException;
021import java.security.cert.CertificateFactory;
022import java.security.cert.X509Certificate;
023import java.util.Arrays;
024import java.util.Locale;
025import java.util.concurrent.ExecutionException;
026
027import org.openstreetmap.josm.Main;
028import org.openstreetmap.josm.io.CertificateAmendment.NativeCertAmend;
029import org.openstreetmap.josm.spi.preferences.Config;
030
031/**
032 * {@code PlatformHook} implementation for Unix systems.
033 * @since 1023
034 */
035public class PlatformHookUnixoid implements PlatformHook {
036
037    private String osDescription;
038
039    @Override
040    public Platform getPlatform() {
041        return Platform.UNIXOID;
042    }
043
044    @Override
045    public void preStartupHook() {
046        // See #12022 - Disable GNOME ATK Java wrapper as it causes a lot of serious trouble
047        if ("org.GNOME.Accessibility.AtkWrapper".equals(System.getProperty("assistive_technologies"))) {
048            System.clearProperty("assistive_technologies");
049        }
050    }
051
052    @Override
053    public void openUrl(String url) throws IOException {
054        for (String program : Config.getPref().getList("browser.unix",
055                Arrays.asList("xdg-open", "#DESKTOP#", "$BROWSER", "gnome-open", "kfmclient openURL", "firefox"))) {
056            try {
057                if ("#DESKTOP#".equals(program)) {
058                    Desktop.getDesktop().browse(new URI(url));
059                } else if (program.startsWith("$")) {
060                    program = System.getenv().get(program.substring(1));
061                    Runtime.getRuntime().exec(new String[]{program, url});
062                } else {
063                    Runtime.getRuntime().exec(new String[]{program, url});
064                }
065                return;
066            } catch (IOException | URISyntaxException e) {
067                Logging.warn(e);
068            }
069        }
070    }
071
072    @Override
073    public void initSystemShortcuts() {
074        // CHECKSTYLE.OFF: LineLength
075        // TODO: Insert system shortcuts here. See Windows and especially OSX to see how to.
076        for (int i = KeyEvent.VK_F1; i <= KeyEvent.VK_F12; ++i) {
077            Shortcut.registerSystemShortcut("screen:toogle"+i, tr("reserved"), i, KeyEvent.CTRL_DOWN_MASK | KeyEvent.ALT_DOWN_MASK)
078                .setAutomatic();
079        }
080        Shortcut.registerSystemShortcut("system:reset", tr("reserved"), KeyEvent.VK_DELETE, KeyEvent.CTRL_DOWN_MASK | KeyEvent.ALT_DOWN_MASK)
081            .setAutomatic();
082        Shortcut.registerSystemShortcut("system:resetX", tr("reserved"), KeyEvent.VK_BACK_SPACE, KeyEvent.CTRL_DOWN_MASK | KeyEvent.ALT_DOWN_MASK)
083            .setAutomatic();
084        // CHECKSTYLE.ON: LineLength
085    }
086
087    @Override
088    public String getDefaultStyle() {
089        return "javax.swing.plaf.metal.MetalLookAndFeel";
090    }
091
092    /**
093     * Determines if the distribution is Debian or Ubuntu, or a derivative.
094     * @return {@code true} if the distribution is Debian, Ubuntu or Mint, {@code false} otherwise
095     */
096    public static boolean isDebianOrUbuntu() {
097        try {
098            String dist = Utils.execOutput(Arrays.asList("lsb_release", "-i", "-s"));
099            return "Debian".equalsIgnoreCase(dist) || "Ubuntu".equalsIgnoreCase(dist) || "Mint".equalsIgnoreCase(dist);
100        } catch (IOException | ExecutionException | InterruptedException e) {
101            // lsb_release is not available on all Linux systems, so don't log at warning level
102            Logging.debug(e);
103            return false;
104        }
105    }
106
107    /**
108     * Get the package name including detailed version.
109     * @param packageNames The possible package names (when a package can have different names on different distributions)
110     * @return The package name and package version if it can be identified, null otherwise
111     * @since 7314
112     */
113    public static String getPackageDetails(String... packageNames) {
114        try {
115            // CHECKSTYLE.OFF: SingleSpaceSeparator
116            boolean dpkg = Paths.get("/usr/bin/dpkg-query").toFile().exists();
117            boolean eque = Paths.get("/usr/bin/equery").toFile().exists();
118            boolean rpm  = Paths.get("/bin/rpm").toFile().exists();
119            // CHECKSTYLE.ON: SingleSpaceSeparator
120            if (dpkg || rpm || eque) {
121                for (String packageName : packageNames) {
122                    String[] args;
123                    if (dpkg) {
124                        args = new String[] {"dpkg-query", "--show", "--showformat", "${Architecture}-${Version}", packageName};
125                    } else if (eque) {
126                        args = new String[] {"equery", "-q", "list", "-e", "--format=$fullversion", packageName};
127                    } else {
128                        args = new String[] {"rpm", "-q", "--qf", "%{arch}-%{version}", packageName};
129                    }
130                    try {
131                        String version = Utils.execOutput(Arrays.asList(args));
132                        if (version != null && !version.isEmpty()) {
133                            return packageName + ':' + version;
134                        }
135                    } catch (ExecutionException e) {
136                        // Package does not exist, continue
137                        Logging.trace(e);
138                    }
139                }
140            }
141        } catch (IOException | InterruptedException e) {
142            Logging.warn(e);
143        }
144        return null;
145    }
146
147    /**
148     * Get the Java package name including detailed version.
149     *
150     * Some Java bugs are specific to a certain security update, so in addition
151     * to the Java version, we also need the exact package version.
152     *
153     * @return The package name and package version if it can be identified, null otherwise
154     */
155    public String getJavaPackageDetails() {
156        String home = System.getProperty("java.home");
157        if (home.contains("java-8-openjdk") || home.contains("java-1.8.0-openjdk")) {
158            return getPackageDetails("openjdk-8-jre", "java-1_8_0-openjdk", "java-1.8.0-openjdk");
159        } else if (home.contains("java-9-openjdk") || home.contains("java-1.9.0-openjdk")) {
160            return getPackageDetails("openjdk-9-jre", "java-1_9_0-openjdk", "java-1.9.0-openjdk", "java-9-openjdk");
161        } else if (home.contains("icedtea")) {
162            return getPackageDetails("icedtea-bin");
163        } else if (home.contains("oracle")) {
164            return getPackageDetails("oracle-jdk-bin", "oracle-jre-bin");
165        }
166        return null;
167    }
168
169    /**
170     * Get the Web Start package name including detailed version.
171     *
172     * OpenJDK packages are shipped with icedtea-web package,
173     * but its version generally does not match main java package version.
174     *
175     * Simply return {@code null} if there's no separate package for Java WebStart.
176     *
177     * @return The package name and package version if it can be identified, null otherwise
178     */
179    public String getWebStartPackageDetails() {
180        if (isOpenJDK()) {
181            return getPackageDetails("icedtea-netx", "icedtea-web");
182        }
183        return null;
184    }
185
186    /**
187     * Get the Gnome ATK wrapper package name including detailed version.
188     *
189     * Debian and Ubuntu derivatives come with a pre-enabled accessibility software
190     * completely buggy that makes Swing crash in a lot of different ways.
191     *
192     * Simply return {@code null} if it's not found.
193     *
194     * @return The package name and package version if it can be identified, null otherwise
195     */
196    public String getAtkWrapperPackageDetails() {
197        if (isOpenJDK() && isDebianOrUbuntu()) {
198            return getPackageDetails("libatk-wrapper-java");
199        }
200        return null;
201    }
202
203    private String buildOSDescription() {
204        String osName = System.getProperty("os.name");
205        if ("Linux".equalsIgnoreCase(osName)) {
206            try {
207                // Try lsb_release (only available on LSB-compliant Linux systems,
208                // see https://www.linuxbase.org/lsb-cert/productdir.php?by_prod )
209                String line = exec("lsb_release", "-ds");
210                if (line != null && !line.isEmpty()) {
211                    line = line.replaceAll("\"+", "");
212                    line = line.replaceAll("NAME=", ""); // strange code for some Gentoo's
213                    if (line.startsWith("Linux ")) // e.g. Linux Mint
214                        return line;
215                    else if (!line.isEmpty())
216                        return "Linux " + line;
217                }
218            } catch (IOException e) {
219                Logging.debug(e);
220                // Non LSB-compliant Linux system. List of common fallback release files: http://linuxmafia.com/faq/Admin/release-files.html
221                for (LinuxReleaseInfo info : new LinuxReleaseInfo[]{
222                        new LinuxReleaseInfo("/etc/lsb-release", "DISTRIB_DESCRIPTION", "DISTRIB_ID", "DISTRIB_RELEASE"),
223                        new LinuxReleaseInfo("/etc/os-release", "PRETTY_NAME", "NAME", "VERSION"),
224                        new LinuxReleaseInfo("/etc/arch-release"),
225                        new LinuxReleaseInfo("/etc/debian_version", "Debian GNU/Linux "),
226                        new LinuxReleaseInfo("/etc/fedora-release"),
227                        new LinuxReleaseInfo("/etc/gentoo-release"),
228                        new LinuxReleaseInfo("/etc/redhat-release"),
229                        new LinuxReleaseInfo("/etc/SuSE-release")
230                }) {
231                    String description = info.extractDescription();
232                    if (description != null && !description.isEmpty()) {
233                        return "Linux " + description;
234                    }
235                }
236            }
237        }
238        return osName;
239    }
240
241    @Override
242    public String getOSDescription() {
243        if (osDescription == null) {
244            osDescription = buildOSDescription();
245        }
246        return osDescription;
247    }
248
249    private static class LinuxReleaseInfo {
250        private final String path;
251        private final String descriptionField;
252        private final String idField;
253        private final String releaseField;
254        private final boolean plainText;
255        private final String prefix;
256
257        LinuxReleaseInfo(String path, String descriptionField, String idField, String releaseField) {
258            this(path, descriptionField, idField, releaseField, false, null);
259        }
260
261        LinuxReleaseInfo(String path) {
262            this(path, null, null, null, true, null);
263        }
264
265        LinuxReleaseInfo(String path, String prefix) {
266            this(path, null, null, null, true, prefix);
267        }
268
269        private LinuxReleaseInfo(String path, String descriptionField, String idField, String releaseField, boolean plainText, String prefix) {
270            this.path = path;
271            this.descriptionField = descriptionField;
272            this.idField = idField;
273            this.releaseField = releaseField;
274            this.plainText = plainText;
275            this.prefix = prefix;
276        }
277
278        @Override
279        public String toString() {
280            return "ReleaseInfo [path=" + path + ", descriptionField=" + descriptionField +
281                    ", idField=" + idField + ", releaseField=" + releaseField + ']';
282        }
283
284        /**
285         * Extracts OS detailed information from a Linux release file (/etc/xxx-release)
286         * @return The OS detailed information, or {@code null}
287         */
288        public String extractDescription() {
289            String result = null;
290            if (path != null) {
291                Path p = Paths.get(path);
292                if (p.toFile().exists()) {
293                    try (BufferedReader reader = Files.newBufferedReader(p, StandardCharsets.UTF_8)) {
294                        String id = null;
295                        String release = null;
296                        String line;
297                        while (result == null && (line = reader.readLine()) != null) {
298                            if (line.contains("=")) {
299                                String[] tokens = line.split("=");
300                                if (tokens.length >= 2) {
301                                    // Description, if available, contains exactly what we need
302                                    if (descriptionField != null && descriptionField.equalsIgnoreCase(tokens[0])) {
303                                        result = Utils.strip(tokens[1]);
304                                    } else if (idField != null && idField.equalsIgnoreCase(tokens[0])) {
305                                        id = Utils.strip(tokens[1]);
306                                    } else if (releaseField != null && releaseField.equalsIgnoreCase(tokens[0])) {
307                                        release = Utils.strip(tokens[1]);
308                                    }
309                                }
310                            } else if (plainText && !line.isEmpty()) {
311                                // Files composed of a single line
312                                result = Utils.strip(line);
313                            }
314                        }
315                        // If no description has been found, try to rebuild it with "id" + "release" (i.e. "name" + "version")
316                        if (result == null && id != null && release != null) {
317                            result = id + ' ' + release;
318                        }
319                    } catch (IOException e) {
320                        // Ignore
321                        Logging.trace(e);
322                    }
323                }
324            }
325            // Append prefix if any
326            if (result != null && !result.isEmpty() && prefix != null && !prefix.isEmpty()) {
327                result = prefix + result;
328            }
329            if (result != null)
330                result = result.replaceAll("\"+", "");
331            return result;
332        }
333    }
334
335    /**
336     * Get the dot directory <code>~/.josm</code>.
337     * @return the dot directory
338     */
339    private static File getDotDirectory() {
340        String dirName = "." + Main.pref.getJOSMDirectoryBaseName().toLowerCase(Locale.ENGLISH);
341        return new File(System.getProperty("user.home"), dirName);
342    }
343
344    /**
345     * Returns true if the dot directory should be used for storing preferences,
346     * cache and user data.
347     * Currently this is the case, if the dot directory already exists.
348     * @return true if the dot directory should be used
349     */
350    private static boolean useDotDirectory() {
351        return getDotDirectory().exists();
352    }
353
354    @Override
355    public File getDefaultCacheDirectory() {
356        if (useDotDirectory()) {
357            return new File(getDotDirectory(), "cache");
358        } else {
359            String xdgCacheDir = System.getenv("XDG_CACHE_HOME");
360            if (xdgCacheDir != null && !xdgCacheDir.isEmpty()) {
361                return new File(xdgCacheDir, Main.pref.getJOSMDirectoryBaseName());
362            } else {
363                return new File(System.getProperty("user.home") + File.separator +
364                        ".cache" + File.separator + Main.pref.getJOSMDirectoryBaseName());
365            }
366        }
367    }
368
369    @Override
370    public File getDefaultPrefDirectory() {
371        if (useDotDirectory()) {
372            return getDotDirectory();
373        } else {
374            String xdgConfigDir = System.getenv("XDG_CONFIG_HOME");
375            if (xdgConfigDir != null && !xdgConfigDir.isEmpty()) {
376                return new File(xdgConfigDir, Main.pref.getJOSMDirectoryBaseName());
377            } else {
378                return new File(System.getProperty("user.home") + File.separator +
379                        ".config" + File.separator + Main.pref.getJOSMDirectoryBaseName());
380            }
381        }
382    }
383
384    @Override
385    public File getDefaultUserDataDirectory() {
386        if (useDotDirectory()) {
387            return getDotDirectory();
388        } else {
389            String xdgDataDir = System.getenv("XDG_DATA_HOME");
390            if (xdgDataDir != null && !xdgDataDir.isEmpty()) {
391                return new File(xdgDataDir, Main.pref.getJOSMDirectoryBaseName());
392            } else {
393                return new File(System.getProperty("user.home") + File.separator +
394                        ".local" + File.separator + "share" + File.separator + Main.pref.getJOSMDirectoryBaseName());
395            }
396        }
397    }
398
399    @Override
400    public X509Certificate getX509Certificate(NativeCertAmend certAmend)
401            throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException {
402        File f = new File("/usr/share/ca-certificates/mozilla", certAmend.getFilename());
403        if (f.exists()) {
404            CertificateFactory fact = CertificateFactory.getInstance("X.509");
405            try (InputStream is = Files.newInputStream(f.toPath())) {
406                return (X509Certificate) fact.generateCertificate(is);
407            }
408        }
409        return null;
410    }
411}