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