1 /* 2 * Copyright (C) 2005-2008 Jive Software. All rights reserved. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package org.jivesoftware.openfire.update; 18 19 import org.apache.commons.lang3.StringUtils; 20 import org.apache.http.HttpHost; 21 import org.apache.http.HttpStatus; 22 import org.apache.http.client.methods.CloseableHttpResponse; 23 import org.apache.http.client.methods.HttpGet; 24 import org.apache.http.client.methods.HttpUriRequest; 25 import org.apache.http.client.methods.RequestBuilder; 26 import org.apache.http.conn.routing.HttpRoutePlanner; 27 import org.apache.http.impl.client.CloseableHttpClient; 28 import org.apache.http.impl.client.HttpClients; 29 import org.apache.http.impl.conn.DefaultProxyRoutePlanner; 30 import org.apache.http.impl.conn.DefaultRoutePlanner; 31 import org.apache.http.util.EntityUtils; 32 import org.dom4j.Document; 33 import org.dom4j.DocumentFactory; 34 import org.dom4j.Element; 35 import org.dom4j.io.OutputFormat; 36 import org.jivesoftware.openfire.XMPPServer; 37 import org.jivesoftware.openfire.cluster.ClusterManager; 38 import org.jivesoftware.openfire.container.BasicModule; 39 import org.jivesoftware.openfire.container.PluginManager; 40 import org.jivesoftware.openfire.container.PluginMetadata; 41 import org.jivesoftware.util.*; 42 import org.slf4j.Logger; 43 import org.slf4j.LoggerFactory; 44 45 import java.io.*; 46 import java.net.MalformedURLException; 47 import java.net.URL; 48 import java.nio.charset.StandardCharsets; 49 import java.time.Duration; 50 import java.time.Instant; 51 import java.time.temporal.ChronoUnit; 52 import java.util.*; 53 import java.util.concurrent.ExecutionException; 54 55 /** 56 * Service that frequently checks for new server or plugins releases. By default the service 57 * will check every 48 hours for updates. Use the system property {@code update.frequency} 58 * to set new values. 59 * <p> 60 * New versions of plugins can be downloaded and installed. However, new server releases 61 * should be manually installed.</p> 62 * 63 * @author Gaston Dombiak 64 */ 65 public class UpdateManager extends BasicModule { 66 67 private static final SystemProperty<Boolean> ENABLED = SystemProperty.Builder.ofType(Boolean.class) 68 .setKey("update.service-enabled") 69 .setDynamic(true) 70 .setDefaultValue(true) 71 .build(); 72 private static final SystemProperty<Boolean> NOTIFY_ADMINS = SystemProperty.Builder.ofType(Boolean.class) 73 .setKey("update.notify-admins") 74 .setDynamic(true) 75 .setDefaultValue(true) 76 .build(); 77 static final SystemProperty<Instant> LAST_UPDATE_CHECK = SystemProperty.Builder.ofType(Instant.class) 78 .setKey("update.lastCheck") 79 .setDynamic(true) 80 .build(); 81 private static final SystemProperty<Duration> UPDATE_FREQUENCY = SystemProperty.Builder.ofType(Duration.class) 82 .setKey("update.frequency") 83 .setDynamic(false) 84 .setChronoUnit(ChronoUnit.HOURS) 85 .setDefaultValue(Duration.ofHours(48)) 86 .setMinValue(Duration.ofHours(12)) 87 .build(); 88 private static final SystemProperty<String> PROXY_HOST = SystemProperty.Builder.ofType(String.class) 89 .setKey("update.proxy.host") 90 .setDynamic(true) 91 .build(); 92 private static final SystemProperty<Integer> PROXY_PORT = SystemProperty.Builder.ofType(Integer.class) 93 .setKey("update.proxy.port") 94 .setDynamic(true) 95 .setDefaultValue(-1) 96 .setMinValue(-1) 97 .setMaxValue(65535) 98 .build(); 99 100 private static final Logger Log = LoggerFactory.getLogger(UpdateManager.class); 101 102 private static final DocumentFactory docFactory = DocumentFactory.getInstance(); 103 104 /** 105 * URL of the servlet (JSP) that provides the "check for update" service. 106 */ 107 private static final String updateServiceURL = "https://www.igniterealtime.org/projects/openfire/versions.jsp"; 108 109 /** 110 * Information about the available server update. 111 */ 112 private Update serverUpdate; 113 114 /** 115 * List of plugins that need to be updated. 116 */ 117 private Collection<Update> pluginUpdates = new ArrayList<>(); 118 119 /** 120 * List of plugins available at igniterealtime.org. 121 */ 122 private Map<String, AvailablePlugin> availablePlugins = new HashMap<>(); 123 124 /** 125 * Thread that performs the periodic checks for updates. 126 */ 127 private Thread thread; 128 UpdateManager()129 public UpdateManager() { 130 super("Update manager"); 131 ENABLED.addListener(this::enableService); 132 } 133 134 @Override start()135 public void start() throws IllegalStateException { 136 super.start(); 137 startService(); 138 } 139 140 /** 141 * Starts sevice that checks for new updates. 142 */ startService()143 private void startService() { 144 // Thread that performs the periodic checks for updates 145 thread = new Thread("Update Manager") { 146 @Override 147 public void run() { 148 try { 149 // Sleep for 5 seconds before starting to work. This is required because 150 // this module has a dependency on the PluginManager, which is loaded 151 // after all other modules. 152 Thread.sleep(5000); 153 // Load last saved information (if any) 154 loadSavedInfo(); 155 while (isServiceEnabled()) { 156 waitForNextCheck(); 157 // Check if the service is still enabled 158 if (isServiceEnabled()) { 159 try { 160 // Check for server updates 161 checkForServerUpdate(true); 162 // Refresh list of available plugins and check for plugin updates 163 checkForPluginsUpdates(true); 164 } 165 catch (Exception e) { 166 Log.error("Error checking for updates", e); 167 if (e instanceof InterruptedException) { 168 Thread.currentThread().interrupt(); 169 } 170 } 171 // Keep track of the last time we checked for updates. 172 final Instant lastUpdate = Instant.now(); 173 LAST_UPDATE_CHECK.setValue(lastUpdate); 174 // As an extra precaution, make sure that that the value 175 // we just set is saved. If not, return to make sure that 176 // no additional update checks are performed until Openfire 177 // is restarted. 178 if(!lastUpdate.equals(LAST_UPDATE_CHECK.getValue())) { 179 Log.error("Error: update service check did not save correctly. " + 180 "Stopping update service."); 181 return; 182 } 183 } 184 } 185 } 186 catch (InterruptedException e) { 187 Log.error(e.getMessage(), e); 188 Thread.currentThread().interrupt(); 189 } 190 finally { 191 // Clean up reference to this thread 192 thread = null; 193 } 194 } 195 196 private void waitForNextCheck() throws InterruptedException { 197 final Instant lastCheck = LAST_UPDATE_CHECK.getValue(); 198 if (lastCheck == null) { 199 // This is the first time the server is used (since we added this feature) 200 Thread.sleep(30000); 201 } 202 else { 203 final Duration updateFrequency = UPDATE_FREQUENCY.getValue(); 204 // This check is necessary just in case the thread woke up early. 205 while (lastCheck.plus(updateFrequency).isAfter(Instant.now())) { 206 Thread.sleep(Duration.between(Instant.now(), lastCheck.plus(updateFrequency)).toMillis()); 207 } 208 } 209 } 210 }; 211 thread.setDaemon(true); 212 thread.start(); 213 } 214 stopService()215 private void stopService() { 216 if (thread != null) { 217 thread.interrupt(); 218 thread = null; 219 } 220 } 221 222 @Override initialize(XMPPServer server)223 public void initialize(XMPPServer server) { 224 super.initialize(server); 225 226 JiveGlobals.migrateProperty(ENABLED.getKey()); 227 JiveGlobals.migrateProperty(NOTIFY_ADMINS.getKey()); 228 } 229 230 /** 231 * Queries the igniterealtime.org server with a request that contains the currently installed 232 * server version. It's response indicates if a server update (a newer version of Openfire) is 233 * available. 234 * 235 * @param notificationsEnabled true if admins will be notified when new updates are found. 236 * @throws Exception if some error happens during the query. 237 */ checkForServerUpdate(boolean notificationsEnabled)238 public synchronized void checkForServerUpdate(boolean notificationsEnabled) throws Exception { 239 final Optional<String> response = getResponse("update", getServerUpdateRequest()); 240 if (response.isPresent()) { 241 processServerUpdateResponse(response.get(), notificationsEnabled); 242 } 243 } 244 245 /** 246 * Queries the igniterealtime.org server. It's response is expected to include a list of 247 * plugins that are available on the server / for download. 248 * 249 * @param notificationsEnabled true if admins will be notified when new updates are found. 250 * @throws Exception if some error happens during the query. 251 */ checkForPluginsUpdates(boolean notificationsEnabled)252 public synchronized void checkForPluginsUpdates(boolean notificationsEnabled) throws Exception { 253 final Optional<String> response = getResponse("available", getAvailablePluginsUpdateRequest()); 254 if (response.isPresent()) { 255 processAvailablePluginsResponse(response.get(), notificationsEnabled); 256 } 257 } 258 getResponse(final String requestType, final String requestXML)259 private Optional<String> getResponse(final String requestType, final String requestXML) throws IOException { 260 final HttpUriRequest postRequest = RequestBuilder.post(updateServiceURL) 261 .addParameter("type", requestType) 262 .addParameter("query", requestXML) 263 .build(); 264 265 try (final CloseableHttpClient httpClient = HttpClients.custom().setRoutePlanner(getRoutePlanner()).build(); 266 final CloseableHttpResponse response = httpClient.execute(postRequest)) { 267 final int statusCode = response.getStatusLine().getStatusCode(); 268 if (statusCode == HttpStatus.SC_OK) { 269 return Optional.of(EntityUtils.toString(response.getEntity())); 270 } else { 271 return Optional.empty(); 272 } 273 } 274 } 275 getRoutePlanner()276 private HttpRoutePlanner getRoutePlanner() { 277 if (isUsingProxy()) { 278 return new DefaultProxyRoutePlanner(new HttpHost(getProxyHost(), getProxyPort())); 279 } else { 280 return new DefaultRoutePlanner(null); 281 } 282 } 283 284 /** 285 * Download and install latest version of plugin. 286 * 287 * @param url the URL of the latest version of the plugin. 288 * @return true if the plugin was successfully downloaded and installed. 289 */ downloadPlugin(String url)290 public boolean downloadPlugin(String url) { 291 boolean installed = false; 292 // Download and install new version of plugin 293 if (isKnownPlugin(url)) { 294 final HttpGet httpGet = new HttpGet(url); 295 296 try (final CloseableHttpClient httpClient = HttpClients.custom().setRoutePlanner(getRoutePlanner()).build(); 297 final CloseableHttpResponse response = httpClient.execute(httpGet)) { 298 final int statusCode = response.getStatusLine().getStatusCode(); 299 if (statusCode == HttpStatus.SC_OK) { 300 String pluginFilename = url.substring(url.lastIndexOf("/") + 1); 301 installed = XMPPServer.getInstance().getPluginManager() 302 .installPlugin(response.getEntity().getContent(), pluginFilename); 303 if (installed) { 304 // Remove the plugin from the list of plugins to update 305 for (Update update : pluginUpdates) { 306 if (update.getURL().equals(url)) { 307 update.setDownloaded(true); 308 } 309 } 310 // Save response in a file for later retrieval 311 saveLatestServerInfo(); 312 } 313 } 314 } catch (IOException e) { 315 Log.warn("Error downloading new plugin version", e); 316 } 317 } else { 318 Log.error("Invalid plugin download URL: " +url); 319 } 320 return installed; 321 } 322 323 /** 324 * Check if the plugin URL is in the known list of available plugins. 325 * 326 * i.e. that it's an approved download source. 327 * 328 * @param url The URL of the plugin to download. 329 * @return true if the URL is in the list. Otherwise false. 330 */ isKnownPlugin(String url)331 private boolean isKnownPlugin(String url) { 332 for (String pluginName : availablePlugins.keySet()) { 333 if (availablePlugins.get(pluginName).getDownloadURL().toString().equals(url)) { 334 return true; 335 } 336 } 337 338 return false; 339 } 340 341 /** 342 * Returns true if the plugin downloaded from the specified URL has been downloaded. Plugins 343 * may be downloaded but not installed. The install process may take like 30 seconds to 344 * detect new plugins to install. 345 * 346 * @param url the URL of the latest version of the plugin. 347 * @return true if the plugin downloaded from the specified URL has been downloaded. 348 */ isPluginDownloaded(String url)349 public boolean isPluginDownloaded(String url) { 350 String pluginFilename = url.substring(url.lastIndexOf("/") + 1); 351 return XMPPServer.getInstance().getPluginManager().isInstalled( pluginFilename); 352 } 353 354 /** 355 * Returns the list of available plugins, sorted alphabetically, to install as reported by igniterealtime.org. 356 * 357 * Currently downloaded plugins will not be included, nor will plugins that require a newer or older server version. 358 * 359 * @return the list of available plugins to install as reported by igniterealtime.org. 360 */ getNotInstalledPlugins()361 public List<AvailablePlugin> getNotInstalledPlugins() 362 { 363 final List<AvailablePlugin> result = new ArrayList<>( availablePlugins.values() ); 364 final PluginManager pluginManager = XMPPServer.getInstance().getPluginManager(); 365 final Version currentServerVersion = XMPPServer.getInstance().getServerInfo().getVersion(); 366 367 // Iterate over the plugins, remove those that are of no interest. 368 final Iterator<AvailablePlugin> iterator = result.iterator(); 369 while ( iterator.hasNext() ) 370 { 371 final AvailablePlugin availablePlugin = iterator.next(); 372 373 // Remove plugins that are already downloaded from the list of available plugins. 374 if ( pluginManager.isInstalled( availablePlugin.getCanonicalName() ) ) 375 { 376 iterator.remove(); 377 continue; 378 } 379 380 // Remove plugins that require a newer server version. 381 if ( availablePlugin.getMinServerVersion() != null && availablePlugin.getMinServerVersion().isNewerThan( currentServerVersion ) ) 382 { 383 iterator.remove(); 384 } 385 386 // Remove plugins that require an older server version. 387 if ( availablePlugin.getPriorToServerVersion() != null && !availablePlugin.getPriorToServerVersion().isNewerThan( currentServerVersion ) ) 388 { 389 iterator.remove(); 390 } 391 } 392 393 // Sort alphabetically. 394 result.sort((o1, o2) -> o1.getName().compareToIgnoreCase(o2.getName())); 395 396 return result; 397 } 398 399 /** 400 * Returns the message to send to admins when new updates are available. When sending 401 * this message information about the new updates avaiable will be appended. 402 * 403 * @return the message to send to admins when new updates are available. 404 */ getNotificationMessage()405 public String getNotificationMessage() { 406 return LocaleUtils.getLocalizedString("update.notification-message"); 407 } 408 409 /** 410 * Returns true if the check for updates service is enabled. 411 * 412 * @return true if the check for updates service is enabled. 413 */ isServiceEnabled()414 public boolean isServiceEnabled() { 415 return ENABLED.getValue(); 416 } 417 418 /** 419 * Sets if the check for updates service is enabled. 420 * 421 * @param enabled true if the check for updates service is enabled. 422 */ setServiceEnabled(final boolean enabled)423 public void setServiceEnabled(final boolean enabled) { 424 ENABLED.setValue(enabled); 425 } 426 enableService(final boolean enabled)427 private void enableService(final boolean enabled) { 428 if (enabled && thread == null) { 429 startService(); 430 } else if (!enabled && thread != null) { 431 stopService(); 432 } 433 } 434 435 /** 436 * Returns true if admins should be notified by IM when new updates are available. 437 * 438 * @return true if admins should be notified by IM when new updates are available. 439 */ isNotificationEnabled()440 public boolean isNotificationEnabled() { 441 return NOTIFY_ADMINS.getValue(); 442 } 443 444 /** 445 * Sets if admins should be notified by IM when new updates are available. 446 * 447 * @param enabled true if admins should be notified by IM when new updates are available. 448 */ setNotificationEnabled(final boolean enabled)449 public void setNotificationEnabled(final boolean enabled) { 450 NOTIFY_ADMINS.setValue(enabled); 451 } 452 453 /** 454 * Returns true if a proxy is being used to connect to igniterealtime.org or false if 455 * a direct connection should be attempted. 456 * 457 * @return true if a proxy is being used to connect to igniterealtime.org. 458 */ isUsingProxy()459 public boolean isUsingProxy() { 460 return !StringUtils.isBlank(getProxyHost()) && getProxyPort() > 0; 461 } 462 463 /** 464 * Returns the host of the proxy to use to connect to igniterealtime.org or {@code null} 465 * if no proxy is used. 466 * 467 * @return the host of the proxy or null if no proxy is used. 468 */ getProxyHost()469 public String getProxyHost() { 470 return PROXY_HOST.getValue(); 471 } 472 473 /** 474 * Sets the host of the proxy to use to connect to igniterealtime.org or {@code null} 475 * if no proxy is used. 476 * 477 * @param host the host of the proxy or null if no proxy is used. 478 */ setProxyHost(String host)479 public void setProxyHost(String host) { 480 PROXY_HOST.setValue(host); 481 } 482 483 /** 484 * Returns the port of the proxy to use to connect to igniterealtime.org or -1 if no 485 * proxy is being used. 486 * 487 * @return the port of the proxy to use to connect to igniterealtime.org or -1 if no 488 * proxy is being used. 489 */ getProxyPort()490 public int getProxyPort() { 491 return PROXY_PORT.getValue(); 492 } 493 494 /** 495 * Sets the port of the proxy to use to connect to igniterealtime.org or -1 if no 496 * proxy is being used. 497 * 498 * @param port the port of the proxy to use to connect to igniterealtime.org or -1 if no 499 * proxy is being used. 500 */ setProxyPort(int port)501 public void setProxyPort(int port) { 502 PROXY_PORT.setValue(port); 503 } 504 505 /** 506 * Returns the server update or {@code null} if the server is up to date. 507 * 508 * @return the server update or null if the server is up to date. 509 */ getServerUpdate()510 public Update getServerUpdate() { 511 return serverUpdate; 512 } 513 514 /** 515 * Returns the plugin update or {@code null} if the plugin is up to date. 516 * 517 * @param pluginName the name of the plugin (as described in the meta-data). 518 * @param currentVersion current version of the plugin that is installed. 519 * @return the plugin update or null if the plugin is up to date. 520 */ getPluginUpdate(String pluginName, Version currentVersion)521 public Update getPluginUpdate(String pluginName, Version currentVersion) { 522 for (Update update : pluginUpdates) { 523 // Check if this is the requested plugin 524 if (update.getComponentName().equals(pluginName)) { 525 // Check if the plugin version is right 526 if (new Version(update.getLatestVersion()).isNewerThan( currentVersion ) ) { 527 return update; 528 } 529 } 530 } 531 return null; 532 } 533 getServerUpdateRequest()534 private String getServerUpdateRequest() { 535 XMPPServer server = XMPPServer.getInstance(); 536 Element xmlRequest = docFactory.createDocument().addElement("version"); 537 // Add current openfire version 538 Element openfire = xmlRequest.addElement("openfire"); 539 openfire.addAttribute("current", server.getServerInfo().getVersion().getVersionString()); 540 return xmlRequest.asXML(); 541 } 542 getAvailablePluginsUpdateRequest()543 private String getAvailablePluginsUpdateRequest() { 544 Element xmlRequest = docFactory.createDocument().addElement("available"); 545 // Add locale so we can get current name and description of plugins 546 Element locale = xmlRequest.addElement("locale"); 547 locale.addText(JiveGlobals.getLocale().toString()); 548 return xmlRequest.asXML(); 549 } 550 processServerUpdateResponse(String response, boolean notificationsEnabled)551 private void processServerUpdateResponse(String response, boolean notificationsEnabled) 552 throws ExecutionException, InterruptedException { 553 // Reset last known update information 554 serverUpdate = null; 555 Element xmlResponse = SAXReaderUtil.readRootElement(response); 556 // Parse response and keep info as Update objects 557 Element openfire = xmlResponse.element("openfire"); 558 if (openfire != null) { 559 // A new version of openfire was found 560 Version latestVersion = new Version(openfire.attributeValue("latest")); 561 if (latestVersion.isNewerThan(XMPPServer.getInstance().getServerInfo().getVersion())) { 562 URL changelog = null; 563 try 564 { 565 changelog = new URL( openfire.attributeValue("changelog") ); 566 } 567 catch ( MalformedURLException e ) 568 { 569 Log.warn( "Unable to parse URL from openfire changelog value '{}'.", openfire.attributeValue("changelog"), e ); 570 } 571 572 URL url = null; 573 try 574 { 575 url = new URL( openfire.attributeValue("url") ); 576 } 577 catch ( MalformedURLException e ) 578 { 579 Log.warn( "Unable to parse URL from openfire download url value '{}'.", openfire.attributeValue("url"), e ); 580 } 581 // Keep information about the available server update 582 serverUpdate = new Update("Openfire", latestVersion.getVersionString(), String.valueOf(changelog), String.valueOf(url)); 583 } 584 } 585 // Check if we need to send notifications to admins 586 if (notificationsEnabled && isNotificationEnabled() && serverUpdate != null) { 587 XMPPServer.getInstance().sendMessageToAdmins(getNotificationMessage() + 588 " " + serverUpdate.getComponentName() + 589 " " + serverUpdate.getLatestVersion()); 590 } 591 // Save response in a file for later retrieval 592 saveLatestServerInfo(); 593 } 594 processAvailablePluginsResponse(String response, boolean notificationsEnabled)595 private void processAvailablePluginsResponse(String response, boolean notificationsEnabled) 596 throws ExecutionException, InterruptedException { 597 // Reset last known list of available plugins 598 availablePlugins = new HashMap<>(); 599 600 // Parse response and keep info as AvailablePlugin objects 601 Element xmlResponse = SAXReaderUtil.readRootElement(response); 602 Iterator<Element> plugins = xmlResponse.elementIterator("plugin"); 603 while (plugins.hasNext()) { 604 Element plugin = plugins.next(); 605 AvailablePlugin available = AvailablePlugin.getInstance( plugin ); 606 // Add plugin to the list of available plugins at js.org 607 availablePlugins.put(available.getName(), available); 608 } 609 610 // Figure out local plugins that need to be updated 611 buildPluginsUpdateList(); 612 613 // Check if we need to send notifications to admins 614 if (notificationsEnabled && isNotificationEnabled() && !pluginUpdates.isEmpty()) { 615 for (Update update : pluginUpdates) { 616 //Send hostname of server only if clustering is enabled 617 if(ClusterManager.isClusteringStarted()){ 618 XMPPServer.getInstance().sendMessageToAdmins(String.format("%s %s %s, on node %s", getNotificationMessage(), 619 update.getComponentName(), update.getLatestVersion(), XMPPServer.getInstance().getServerInfo().getHostname())); 620 } else{ 621 XMPPServer.getInstance().sendMessageToAdmins(String.format("%s %s %s", getNotificationMessage(), 622 update.getComponentName(), update.getLatestVersion())); 623 } 624 } 625 } 626 627 // Save information of available plugins 628 saveAvailablePluginsInfo(); 629 } 630 631 /** 632 * Recreate the list of plugins that need to be updated based on the list of 633 * available plugins at igniterealtime.org. 634 */ buildPluginsUpdateList()635 private void buildPluginsUpdateList() { 636 // Reset list of plugins that need to be updated 637 pluginUpdates = new ArrayList<>(); 638 XMPPServer server = XMPPServer.getInstance(); 639 Version currentServerVersion = XMPPServer.getInstance().getServerInfo().getVersion(); 640 // Compare local plugins versions with latest ones 641 for ( final PluginMetadata plugin : server.getPluginManager().getMetadataExtractedPlugins().values() ) 642 { 643 final AvailablePlugin latestPlugin = availablePlugins.get( plugin.getName() ); 644 645 if (latestPlugin == null) 646 { 647 continue; 648 } 649 650 final Version latestPluginVersion = latestPlugin.getVersion(); 651 652 if ( latestPluginVersion.isNewerThan( plugin.getVersion() ) ) 653 { 654 // Check if the update can run in the current version of the server 655 final Version pluginMinServerVersion = latestPlugin.getMinServerVersion(); 656 if ( pluginMinServerVersion != null && pluginMinServerVersion.isNewerThan( currentServerVersion )) 657 { 658 continue; 659 } 660 661 final Version pluginPriorToServerVersion = latestPlugin.getPriorToServerVersion(); 662 if ( pluginPriorToServerVersion != null && !pluginPriorToServerVersion.isNewerThan( currentServerVersion )) 663 { 664 continue; 665 } 666 667 final Update update = new Update( plugin.getName(), latestPlugin.getVersion().getVersionString(), latestPlugin.getChangelog().toExternalForm(), latestPlugin.getDownloadURL().toExternalForm() ); 668 pluginUpdates.add(update); 669 } 670 } 671 } 672 673 /** 674 * Saves to conf/server-update.xml information about the latest Openfire release that is 675 * available for download. 676 */ saveLatestServerInfo()677 private void saveLatestServerInfo() { 678 Element xmlResponse = docFactory.createDocument().addElement("version"); 679 if (serverUpdate != null) { 680 Element component = xmlResponse.addElement("openfire"); 681 component.addAttribute("latest", serverUpdate.getLatestVersion()); 682 component.addAttribute( "changelog", serverUpdate.getChangelog() ); 683 component.addAttribute( "url", serverUpdate.getURL() ); 684 } 685 // Write data out to conf/server-update.xml file. 686 try { 687 // Create the conf folder if required 688 File file = new File(JiveGlobals.getHomeDirectory(), "conf"); 689 if (!file.exists()) { 690 file.mkdir(); 691 } 692 file = new File(JiveGlobals.getHomeDirectory() + File.separator + "conf", 693 "server-update.xml"); 694 // Delete the old server-update.xml file if it exists 695 if (file.exists()) { 696 file.delete(); 697 } 698 // Create new version.xml with returned data 699 try (Writer writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8))) { 700 OutputFormat prettyPrinter = OutputFormat.createPrettyPrint(); 701 XMLWriter xmlWriter = new XMLWriter(writer, prettyPrinter); 702 xmlWriter.write(xmlResponse); 703 } 704 } 705 catch (Exception e) { 706 Log.error(e.getMessage(), e); 707 } 708 } 709 710 /** 711 * Saves to conf/available-plugins.xml the list of plugins that are available 712 * at igniterealtime.org. 713 */ saveAvailablePluginsInfo()714 private void saveAvailablePluginsInfo() { 715 // XML to store in the file 716 Element xml = docFactory.createDocument().addElement("available"); 717 for (AvailablePlugin plugin : availablePlugins.values()) { 718 Element component = xml.addElement("plugin"); 719 component.addAttribute("name", plugin.getName()); 720 component.addAttribute("latest", plugin.getVersion() != null ? plugin.getVersion().getVersionString() : null); 721 component.addAttribute("changelog", plugin.getChangelog() != null ? plugin.getChangelog().toExternalForm() : null ); 722 component.addAttribute("url", plugin.getDownloadURL() != null ? plugin.getDownloadURL().toExternalForm() : null ); 723 component.addAttribute("author", plugin.getAuthor()); 724 component.addAttribute("description", plugin.getDescription()); 725 component.addAttribute("icon", plugin.getIcon() != null ? plugin.getIcon().toExternalForm() : null ); 726 component.addAttribute("minServerVersion", plugin.getMinServerVersion() != null ? plugin.getMinServerVersion().getVersionString() : null); 727 component.addAttribute("priorToServerVersion", plugin.getPriorToServerVersion() != null ? plugin.getPriorToServerVersion().getVersionString() : null); 728 component.addAttribute("readme", plugin.getReadme() != null ? plugin.getReadme().toExternalForm() : null ); 729 component.addAttribute( "licenseType", plugin.getLicense() ); 730 component.addAttribute("fileSize", Long.toString(plugin.getFileSize())); 731 } 732 // Write data out to conf/available-plugins.xml file. 733 Writer writer = null; 734 try { 735 // Create the conf folder if required 736 File file = new File(JiveGlobals.getHomeDirectory(), "conf"); 737 if (!file.exists()) { 738 file.mkdir(); 739 } 740 file = new File(JiveGlobals.getHomeDirectory() + File.separator + "conf", 741 "available-plugins.xml"); 742 // Delete the old version.xml file if it exists 743 if (file.exists()) { 744 file.delete(); 745 } 746 // Create new version.xml with returned data 747 writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8)); 748 OutputFormat prettyPrinter = OutputFormat.createPrettyPrint(); 749 XMLWriter xmlWriter = new XMLWriter(writer, prettyPrinter); 750 xmlWriter.write(xml); 751 } 752 catch (Exception e) { 753 Log.error(e.getMessage(), e); 754 } 755 finally { 756 if (writer != null) { 757 try { 758 writer.close(); 759 } 760 catch (IOException e1) { 761 Log.error(e1.getMessage(), e1); 762 } 763 } 764 } 765 } 766 767 /** 768 * Loads list of available plugins and latest available server version from 769 * conf/available-plugins.xml and conf/server-update.xml respectively. 770 */ loadSavedInfo()771 private void loadSavedInfo() { 772 // Load server update information 773 loadLatestServerInfo(); 774 // Load available plugins information 775 loadAvailablePluginsInfo(); 776 // Recreate list of plugins to update 777 buildPluginsUpdateList(); 778 } 779 loadLatestServerInfo()780 private void loadLatestServerInfo() { 781 Document xmlResponse; 782 File file = new File(JiveGlobals.getHomeDirectory() + File.separator + "conf", 783 "server-update.xml"); 784 if (!file.exists()) { 785 return; 786 } 787 // Check read privs. 788 if (!file.canRead()) { 789 Log.warn("Cannot retrieve server updates. File must be readable: " + file.getName()); 790 return; 791 } 792 try { 793 xmlResponse = SAXReaderUtil.readDocument(file); 794 } catch (Exception e) { 795 Log.error("Error reading server-update.xml", e); 796 if (e instanceof InterruptedException) { 797 Thread.currentThread().interrupt(); 798 } 799 return; 800 } 801 802 // Parse info and recreate update information (if still required) 803 Element openfire = xmlResponse.getRootElement().element("openfire"); 804 if (openfire != null) { 805 Version latestVersion = new Version(openfire.attributeValue("latest")); 806 URL changelog = null; 807 try 808 { 809 changelog = new URL( openfire.attributeValue("changelog") ); 810 } 811 catch ( MalformedURLException e ) 812 { 813 Log.warn( "Unable to parse URL from openfire changelog value '{}'.", openfire.attributeValue("changelog"), e ); 814 } 815 816 URL url = null; 817 try 818 { 819 url = new URL( openfire.attributeValue("url") ); 820 } 821 catch ( MalformedURLException e ) 822 { 823 Log.warn( "Unable to parse URL from openfire download url value '{}'.", openfire.attributeValue("url"), e ); 824 } 825 // Check if current server version is correct 826 Version currentServerVersion = XMPPServer.getInstance().getServerInfo().getVersion(); 827 if (latestVersion.isNewerThan(currentServerVersion)) { 828 serverUpdate = new Update("Openfire", latestVersion.getVersionString(), String.valueOf(changelog), String.valueOf(url) ); 829 } 830 } 831 } 832 loadAvailablePluginsInfo()833 private void loadAvailablePluginsInfo() { 834 Document xmlResponse; 835 File file = new File(JiveGlobals.getHomeDirectory() + File.separator + "conf", 836 "available-plugins.xml"); 837 if (!file.exists()) { 838 return; 839 } 840 // Check read privs. 841 if (!file.canRead()) { 842 Log.warn("Cannot retrieve available plugins. File must be readable: " + file.getName()); 843 return; 844 } 845 try { 846 xmlResponse = SAXReaderUtil.readDocument(file); 847 } catch (Exception e) { 848 Log.error("Error reading available-plugins.xml", e); 849 if (e instanceof InterruptedException) { 850 Thread.currentThread().interrupt(); 851 } 852 return; 853 } 854 855 // Parse info and recreate available plugins 856 Iterator it = xmlResponse.getRootElement().elementIterator("plugin"); 857 while (it.hasNext()) { 858 Element plugin = (Element) it.next(); 859 final AvailablePlugin instance = AvailablePlugin.getInstance( plugin ); 860 // Add plugin to the list of available plugins at js.org 861 availablePlugins.put(instance.getName(), instance); 862 } 863 } 864 865 /** 866 * Returns a previously fetched list of updates. 867 * 868 * @return a previously fetched list of updates. 869 */ getPluginUpdates()870 public Collection<Update> getPluginUpdates() { 871 return pluginUpdates; 872 } 873 874 } 875