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}