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.Dimension; 008import java.awt.GraphicsEnvironment; 009import java.awt.event.KeyEvent; 010import java.io.BufferedReader; 011import java.io.File; 012import java.io.IOException; 013import java.io.InputStreamReader; 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.KeyStore; 021import java.security.KeyStoreException; 022import java.security.NoSuchAlgorithmException; 023import java.security.cert.CertificateException; 024import java.util.Arrays; 025 026import javax.swing.JOptionPane; 027 028import org.openstreetmap.josm.Main; 029import org.openstreetmap.josm.gui.ExtendedDialog; 030import org.openstreetmap.josm.gui.util.GuiHelper; 031 032/** 033 * {@code PlatformHook} base implementation. 034 * 035 * Don't write (Main.platform instanceof PlatformHookUnixoid) because other platform 036 * hooks are subclasses of this class. 037 */ 038public class PlatformHookUnixoid implements PlatformHook { 039 040 private String osDescription; 041 042 @Override 043 public void preStartupHook() { 044 } 045 046 @Override 047 public void startupHook() { 048 } 049 050 @Override 051 public void openUrl(String url) throws IOException { 052 for (String program : Main.pref.getCollection("browser.unix", 053 Arrays.asList("xdg-open", "#DESKTOP#", "$BROWSER", "gnome-open", "kfmclient openURL", "firefox"))) { 054 try { 055 if ("#DESKTOP#".equals(program)) { 056 Desktop.getDesktop().browse(new URI(url)); 057 } else if (program.startsWith("$")) { 058 program = System.getenv().get(program.substring(1)); 059 Runtime.getRuntime().exec(new String[]{program, url}); 060 } else { 061 Runtime.getRuntime().exec(new String[]{program, url}); 062 } 063 return; 064 } catch (IOException | URISyntaxException e) { 065 Main.warn(e); 066 } 067 } 068 } 069 070 @Override 071 public void initSystemShortcuts() { 072 // TODO: Insert system shortcuts here. See Windows and especially OSX to see how to. 073 for(int i = KeyEvent.VK_F1; i <= KeyEvent.VK_F12; ++i) 074 Shortcut.registerSystemShortcut("screen:toogle"+i, tr("reserved"), i, KeyEvent.CTRL_DOWN_MASK | KeyEvent.ALT_DOWN_MASK).setAutomatic(); 075 Shortcut.registerSystemShortcut("system:reset", tr("reserved"), KeyEvent.VK_DELETE, KeyEvent.CTRL_DOWN_MASK | KeyEvent.ALT_DOWN_MASK).setAutomatic(); 076 Shortcut.registerSystemShortcut("system:resetX", tr("reserved"), KeyEvent.VK_BACK_SPACE, KeyEvent.CTRL_DOWN_MASK | KeyEvent.ALT_DOWN_MASK).setAutomatic(); 077 } 078 079 /** 080 * This should work for all platforms. Yeah, should. 081 * See PlatformHook.java for a list of reasons why 082 * this is implemented here... 083 */ 084 @Override 085 public String makeTooltip(String name, Shortcut sc) { 086 String result = ""; 087 result += "<html>"; 088 result += name; 089 if (sc != null && sc.getKeyText().length() != 0) { 090 result += " "; 091 result += "<font size='-2'>"; 092 result += "("+sc.getKeyText()+")"; 093 result += "</font>"; 094 } 095 result += " </html>"; 096 return result; 097 } 098 099 @Override 100 public String getDefaultStyle() { 101 return "javax.swing.plaf.metal.MetalLookAndFeel"; 102 } 103 104 @Override 105 public boolean canFullscreen() { 106 return !GraphicsEnvironment.isHeadless() && 107 GraphicsEnvironment.getLocalGraphicsEnvironment() 108 .getDefaultScreenDevice().isFullScreenSupported(); 109 } 110 111 @Override 112 public boolean rename(File from, File to) { 113 return from.renameTo(to); 114 } 115 116 /** 117 * Determines if the JVM is OpenJDK-based. 118 * @return {@code true} if {@code java.home} contains "openjdk", {@code false} otherwise 119 * @since 6951 120 */ 121 public static boolean isOpenJDK() { 122 String javaHome = System.getProperty("java.home"); 123 return javaHome != null && javaHome.contains("openjdk"); 124 } 125 126 /** 127 * Get the package name including detailed version. 128 * @param packageNames The possible package names (when a package can have different names on different distributions) 129 * @return The package name and package version if it can be identified, null otherwise 130 * @since 7314 131 */ 132 public static String getPackageDetails(String ... packageNames) { 133 try { 134 boolean dpkg = Files.exists(Paths.get("/usr/bin/dpkg-query")); 135 boolean rpm = Files.exists(Paths.get("/bin/rpm")); 136 if (dpkg || rpm) { 137 for (String packageName : packageNames) { 138 String[] args = null; 139 if (dpkg) { 140 args = new String[] {"dpkg-query", "--show", "--showformat", "${Architecture}-${Version}", packageName}; 141 } else { 142 args = new String[] {"rpm", "-q", "--qf", "%{arch}-%{version}", packageName}; 143 } 144 String version = Utils.execOutput(Arrays.asList(args)); 145 if (version != null && !version.contains("not installed")) { 146 return packageName + ":" + version; 147 } 148 } 149 } 150 } catch (IOException e) { 151 Main.warn(e); 152 } 153 return null; 154 } 155 156 /** 157 * Get the Java package name including detailed version. 158 * 159 * Some Java bugs are specific to a certain security update, so in addition 160 * to the Java version, we also need the exact package version. 161 * 162 * @return The package name and package version if it can be identified, null otherwise 163 */ 164 public String getJavaPackageDetails() { 165 String home = System.getProperty("java.home"); 166 if(home.contains("java-7-openjdk") || home.contains("java-1.7.0-openjdk")) { 167 return getPackageDetails("openjdk-7-jre", "java-1_7_0-openjdk", "java-1.7.0-openjdk"); 168 } 169 return null; 170 } 171 172 /** 173 * Get the Web Start package name including detailed version. 174 * 175 * OpenJDK packages are shipped with icedtea-web package, 176 * but its version generally does not match main java package version. 177 * 178 * Simply return {@code null} if there's no separate package for Java WebStart. 179 * 180 * @return The package name and package version if it can be identified, null otherwise 181 */ 182 public String getWebStartPackageDetails() { 183 if (isOpenJDK()) { 184 return getPackageDetails("icedtea-netx", "icedtea-web"); 185 } 186 return null; 187 } 188 189 protected String buildOSDescription() { 190 String osName = System.getProperty("os.name"); 191 if ("Linux".equalsIgnoreCase(osName)) { 192 try { 193 // Try lsb_release (only available on LSB-compliant Linux systems, see https://www.linuxbase.org/lsb-cert/productdir.php?by_prod ) 194 Process p = Runtime.getRuntime().exec("lsb_release -ds"); 195 try (BufferedReader input = new BufferedReader(new InputStreamReader(p.getInputStream(), StandardCharsets.UTF_8))) { 196 String line = Utils.strip(input.readLine()); 197 if (line != null && !line.isEmpty()) { 198 line = line.replaceAll("\"+",""); 199 line = line.replaceAll("NAME=",""); // strange code for some Gentoo's 200 if(line.startsWith("Linux ")) // e.g. Linux Mint 201 return line; 202 else if(!line.isEmpty()) 203 return "Linux " + line; 204 } 205 } 206 } catch (IOException e) { 207 // Non LSB-compliant Linux system. List of common fallback release files: http://linuxmafia.com/faq/Admin/release-files.html 208 for (LinuxReleaseInfo info : new LinuxReleaseInfo[]{ 209 new LinuxReleaseInfo("/etc/lsb-release", "DISTRIB_DESCRIPTION", "DISTRIB_ID", "DISTRIB_RELEASE"), 210 new LinuxReleaseInfo("/etc/os-release", "PRETTY_NAME", "NAME", "VERSION"), 211 new LinuxReleaseInfo("/etc/arch-release"), 212 new LinuxReleaseInfo("/etc/debian_version", "Debian GNU/Linux "), 213 new LinuxReleaseInfo("/etc/fedora-release"), 214 new LinuxReleaseInfo("/etc/gentoo-release"), 215 new LinuxReleaseInfo("/etc/redhat-release"), 216 new LinuxReleaseInfo("/etc/SuSE-release") 217 }) { 218 String description = info.extractDescription(); 219 if (description != null && !description.isEmpty()) { 220 return "Linux " + description; 221 } 222 } 223 } 224 } 225 return osName; 226 } 227 228 @Override 229 public String getOSDescription() { 230 if (osDescription == null) { 231 osDescription = buildOSDescription(); 232 } 233 return osDescription; 234 } 235 236 protected static class LinuxReleaseInfo { 237 private final String path; 238 private final String descriptionField; 239 private final String idField; 240 private final String releaseField; 241 private final boolean plainText; 242 private final String prefix; 243 244 public LinuxReleaseInfo(String path, String descriptionField, String idField, String releaseField) { 245 this(path, descriptionField, idField, releaseField, false, null); 246 } 247 248 public LinuxReleaseInfo(String path) { 249 this(path, null, null, null, true, null); 250 } 251 252 public LinuxReleaseInfo(String path, String prefix) { 253 this(path, null, null, null, true, prefix); 254 } 255 256 private LinuxReleaseInfo(String path, String descriptionField, String idField, String releaseField, boolean plainText, String prefix) { 257 this.path = path; 258 this.descriptionField = descriptionField; 259 this.idField = idField; 260 this.releaseField = releaseField; 261 this.plainText = plainText; 262 this.prefix = prefix; 263 } 264 265 @Override public String toString() { 266 return "ReleaseInfo [path=" + path + ", descriptionField=" + descriptionField + 267 ", idField=" + idField + ", releaseField=" + releaseField + "]"; 268 } 269 270 /** 271 * Extracts OS detailed information from a Linux release file (/etc/xxx-release) 272 * @return The OS detailed information, or {@code null} 273 */ 274 public String extractDescription() { 275 String result = null; 276 if (path != null) { 277 Path p = Paths.get(path); 278 if (Files.exists(p)) { 279 try (BufferedReader reader = Files.newBufferedReader(p, StandardCharsets.UTF_8)) { 280 String id = null; 281 String release = null; 282 String line; 283 while (result == null && (line = reader.readLine()) != null) { 284 if (line.contains("=")) { 285 String[] tokens = line.split("="); 286 if (tokens.length >= 2) { 287 // Description, if available, contains exactly what we need 288 if (descriptionField != null && descriptionField.equalsIgnoreCase(tokens[0])) { 289 result = Utils.strip(tokens[1]); 290 } else if (idField != null && idField.equalsIgnoreCase(tokens[0])) { 291 id = Utils.strip(tokens[1]); 292 } else if (releaseField != null && releaseField.equalsIgnoreCase(tokens[0])) { 293 release = Utils.strip(tokens[1]); 294 } 295 } 296 } else if (plainText && !line.isEmpty()) { 297 // Files composed of a single line 298 result = Utils.strip(line); 299 } 300 } 301 // If no description has been found, try to rebuild it with "id" + "release" (i.e. "name" + "version") 302 if (result == null && id != null && release != null) { 303 result = id + " " + release; 304 } 305 } catch (IOException e) { 306 // Ignore 307 } 308 } 309 } 310 // Append prefix if any 311 if (result != null && !result.isEmpty() && prefix != null && !prefix.isEmpty()) { 312 result = prefix + result; 313 } 314 if(result != null) 315 result = result.replaceAll("\"+",""); 316 return result; 317 } 318 } 319 320 protected void askUpdateJava(String version) { 321 askUpdateJava(version, "https://www.java.com/download"); 322 } 323 324 // Method kept because strings have already been translated. To enable for Java 8 migration somewhere in 2016 325 protected void askUpdateJava(final String version, final String url) { 326 GuiHelper.runInEDTAndWait(new Runnable() { 327 @Override 328 public void run() { 329 ExtendedDialog ed = new ExtendedDialog( 330 Main.parent, 331 tr("Outdated Java version"), 332 new String[]{tr("Update Java"), tr("Cancel")}); 333 // Check if the dialog has not already been permanently hidden by user 334 if (!ed.toggleEnable("askUpdateJava8").toggleCheckState()) { 335 ed.setButtonIcons(new String[]{"java.png", "cancel.png"}).setCancelButton(2); 336 ed.setMinimumSize(new Dimension(480, 300)); 337 ed.setIcon(JOptionPane.WARNING_MESSAGE); 338 String content = tr("You are running version {0} of Java.", "<b>"+version+"</b>")+"<br><br>"; 339 if ("Sun Microsystems Inc.".equals(System.getProperty("java.vendor")) && !isOpenJDK()) { 340 content += "<b>"+tr("This version is no longer supported by {0} since {1} and is not recommended for use.", 341 "Oracle", tr("April 2015"))+"</b><br><br>"; 342 } 343 content += "<b>"+tr("JOSM will soon stop working with this version; we highly recommend you to update to Java {0}.", "8")+"</b><br><br>"+ 344 tr("Would you like to update now ?"); 345 ed.setContent(content); 346 347 if (ed.showDialog().getValue() == 1) { 348 try { 349 openUrl(url); 350 } catch (IOException e) { 351 Main.warn(e); 352 } 353 } 354 } 355 } 356 }); 357 } 358 359 @Override 360 public boolean setupHttpsCertificate(String entryAlias, KeyStore.TrustedCertificateEntry trustedCert) 361 throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException { 362 // TODO setup HTTPS certificate on Unix systems 363 return false; 364 } 365}