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