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