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