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