UpdateManager.java 34.8 KB
Newer Older
1 2 3 4 5
/**
 * $RCSfile$
 * $Revision: $
 * $Date: $
 *
6
 * Copyright (C) 2005-2008 Jive Software. All rights reserved.
7
 *
8 9 10 11 12 13 14 15 16 17 18
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
19 20
 */

21
package org.jivesoftware.openfire.update;
Gaston Dombiak's avatar
Gaston Dombiak committed
22

23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.StringReader;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

39
import org.apache.commons.httpclient.HostConfiguration;
Gaston Dombiak's avatar
Gaston Dombiak committed
40 41 42 43 44 45 46 47 48 49
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.NameValuePair;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.DocumentFactory;
import org.dom4j.Element;
import org.dom4j.io.OutputFormat;
import org.dom4j.io.SAXReader;
50 51 52 53
import org.jivesoftware.openfire.MessageRouter;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.container.BasicModule;
import org.jivesoftware.openfire.container.Plugin;
54 55 56
import org.jivesoftware.util.JiveConstants;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.LocaleUtils;
57
import org.jivesoftware.util.Version;
58 59 60
import org.jivesoftware.util.XMLWriter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Gaston Dombiak's avatar
Gaston Dombiak committed
61 62 63 64 65 66
import org.xmpp.packet.JID;
import org.xmpp.packet.Message;

/**
 * Service that frequently checks for new server or plugins releases. By default the service
 * will check every 48 hours for updates. Use the system property <tt>update.frequency</tt>
67 68
 * to set new values.
 * <p>
Gaston Dombiak's avatar
Gaston Dombiak committed
69
 * New versions of plugins can be downloaded and installed. However, new server releases
70
 * should be manually installed.</p>
Gaston Dombiak's avatar
Gaston Dombiak committed
71 72 73 74 75
 *
 * @author Gaston Dombiak
 */
public class UpdateManager extends BasicModule {

76 77
	private static final Logger Log = LoggerFactory.getLogger(UpdateManager.class);

Gaston Dombiak's avatar
Gaston Dombiak committed
78 79 80 81 82
    protected static DocumentFactory docFactory = DocumentFactory.getInstance();

    /**
     * URL of the servlet (JSP) that provides the "check for update" service.
     */
83
    private static String updateServiceURL = "http://www.igniterealtime.org/projects/openfire/versions.jsp";
Gaston Dombiak's avatar
Gaston Dombiak committed
84 85

    /**
86 87 88 89 90 91 92 93 94 95
     * Information about the available server update.
     */
    private Update serverUpdate;

    /**
     * List of plugins that need to be updated.
     */
    private Collection<Update> pluginUpdates = new ArrayList<Update>();

    /**
96
     * List of plugins available at igniterealtime.org.
Gaston Dombiak's avatar
Gaston Dombiak committed
97
     */
98
    private Map<String, AvailablePlugin> availablePlugins = new HashMap<String, AvailablePlugin>();
Gaston Dombiak's avatar
Gaston Dombiak committed
99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115

    /**
     * Thread that performs the periodic checks for updates.
     */
    private Thread thread;

    /**
     * Router to use for sending notitication messages to admins.
     */
    private MessageRouter router;
    private String serverName;


    public UpdateManager() {
        super("Update manager");
    }

116 117
    @Override
	public void start() throws IllegalStateException {
Gaston Dombiak's avatar
Gaston Dombiak committed
118
        super.start();
119
        startService();
Gaston Dombiak's avatar
Gaston Dombiak committed
120 121 122 123 124 125 126 127
    }

    /**
     * Starts sevice that checks for new updates.
     */
    private void startService() {
        // Thread that performs the periodic checks for updates
        thread = new Thread("Update Manager") {
128 129
            @Override
			public void run() {
Gaston Dombiak's avatar
Gaston Dombiak committed
130
                try {
Matt Tucker's avatar
Matt Tucker committed
131 132 133 134
                    // Sleep for 5 seconds before starting to work. This is required because
                    // this module has a dependency on the PluginManager, which is loaded
                    // after all other modules.
                    Thread.sleep(5000);
135 136
                    // Load last saved information (if any)
                    loadSavedInfo();
137
                    while (isServiceEnabled()) {
138
                        waitForNextCheck();
139
                        // Check if the service is still enabled
Gaston Dombiak's avatar
Gaston Dombiak committed
140
                        if (isServiceEnabled()) {
141 142 143 144 145 146 147 148
                            try {
                                // Check for server updates
                                checkForServerUpdate(true);
                                // Refresh list of available plugins and check for plugin updates
                                checkForPluginsUpdates(true);
                            }
                            catch (Exception e) {
                                Log.error("Error checking for updates", e);
Gaston Dombiak's avatar
Gaston Dombiak committed
149
                            }
150
                            // Keep track of the last time we checked for updates.
151 152 153 154
                            long now = System.currentTimeMillis();
                            JiveGlobals.setProperty("update.lastCheck", String.valueOf(now));
                            // As an extra precaution, make sure that that the value
                            // we just set is saved. If not, return to make sure that
155
                            // no additional update checks are performed until Openfire
156 157 158 159 160 161
                            // is restarted.
                            if (now != JiveGlobals.getLongProperty("update.lastCheck", 0)) {
                                Log.error("Error: update service check did not save correctly. " +
                                        "Stopping update service.");
                                return;
                            }
162
                        }
Gaston Dombiak's avatar
Gaston Dombiak committed
163 164 165
                    }
                }
                catch (InterruptedException e) {
166
                    Log.error(e.getMessage(), e);
Gaston Dombiak's avatar
Gaston Dombiak committed
167 168 169 170 171 172 173
                }
                finally {
                    // Clean up reference to this thread
                    thread = null;
                }
            }

174 175
            private void waitForNextCheck() throws InterruptedException {
                long lastCheck = JiveGlobals.getLongProperty("update.lastCheck", 0);
Gaston Dombiak's avatar
Gaston Dombiak committed
176 177
                if (lastCheck == 0) {
                    // This is the first time the server is used (since we added this feature)
Matt Tucker's avatar
Matt Tucker committed
178
                    Thread.sleep(30000);
179 180
                }
                else {
Gaston Dombiak's avatar
Gaston Dombiak committed
181
                    long elapsed = System.currentTimeMillis() - lastCheck;
182 183 184
                    long frequency = getCheckFrequency() * JiveConstants.HOUR;
                    // Sleep until we've waited the appropriate amount of time.
                    while (elapsed < frequency) {
Gaston Dombiak's avatar
Gaston Dombiak committed
185
                        Thread.sleep(frequency - elapsed);
186 187 188
                        // Update the elapsed time. This check is necessary just in case the
                        // thread woke up early.
                        elapsed = System.currentTimeMillis() - lastCheck;
Gaston Dombiak's avatar
Gaston Dombiak committed
189 190 191 192 193 194 195 196
                    }
                }
            }
        };
        thread.setDaemon(true);
        thread.start();
    }

197 198
    @Override
	public void initialize(XMPPServer server) {
Gaston Dombiak's avatar
Gaston Dombiak committed
199 200
        super.initialize(server);
        router = server.getMessageRouter();
201
        serverName = server.getServerInfo().getXMPPDomain();
202 203 204

        JiveGlobals.migrateProperty("update.service-enabled");
        JiveGlobals.migrateProperty("update.notify-admins");
Gaston Dombiak's avatar
Gaston Dombiak committed
205 206 207
    }

    /**
208
     * Queries the igniterealtime.org server for new server and plugin updates.
Gaston Dombiak's avatar
Gaston Dombiak committed
209 210 211 212
     *
     * @param notificationsEnabled true if admins will be notified when new updates are found.
     * @throws Exception if some error happens during the query.
     */
213
    public synchronized void checkForServerUpdate(boolean notificationsEnabled) throws Exception {
Gaston Dombiak's avatar
Gaston Dombiak committed
214
        // Get the XML request to include in the HTTP request
215
        String requestXML = getServerUpdateRequest();
Gaston Dombiak's avatar
Gaston Dombiak committed
216 217
        // Send the request to the server
        HttpClient httpClient = new HttpClient();
218 219 220 221 222 223
        // Check if a proxy should be used
        if (isUsingProxy()) {
            HostConfiguration hc = new HostConfiguration();
            hc.setProxy(getProxyHost(), getProxyPort());
            httpClient.setHostConfiguration(hc);
        }
Gaston Dombiak's avatar
Gaston Dombiak committed
224 225 226 227 228 229
        PostMethod postMethod = new PostMethod(updateServiceURL);
        NameValuePair[] data = {
                new NameValuePair("type", "update"),
                new NameValuePair("query", requestXML)
        };
        postMethod.setRequestBody(data);
230 231 232 233 234 235 236 237 238
        if (httpClient.executeMethod(postMethod) == 200) {
            // Process answer from the server
            String responseBody = postMethod.getResponseBodyAsString();
            processServerUpdateResponse(responseBody, notificationsEnabled);
        }
    }

    public synchronized void checkForPluginsUpdates(boolean notificationsEnabled) throws Exception {
        // Get the XML request to include in the HTTP request
239
        String requestXML = getAvailablePluginsUpdateRequest();
240 241
        // Send the request to the server
        HttpClient httpClient = new HttpClient();
242 243 244 245 246 247
        // Check if a proxy should be used
        if (isUsingProxy()) {
            HostConfiguration hc = new HostConfiguration();
            hc.setProxy(getProxyHost(), getProxyPort());
            httpClient.setHostConfiguration(hc);
        }
248 249 250 251 252 253 254 255 256 257 258
        PostMethod postMethod = new PostMethod(updateServiceURL);
        NameValuePair[] data = {
                new NameValuePair("type", "available"),
                new NameValuePair("query", requestXML)
        };
        postMethod.setRequestBody(data);
        if (httpClient.executeMethod(postMethod) == 200) {
            // Process answer from the server
            String responseBody = postMethod.getResponseBodyAsString();
            processAvailablePluginsResponse(responseBody, notificationsEnabled);
        }
Gaston Dombiak's avatar
Gaston Dombiak committed
259 260 261
    }

    /**
262
     * Download and install latest version of plugin.
Gaston Dombiak's avatar
Gaston Dombiak committed
263
     *
264 265
     * @param url the URL of the latest version of the plugin.
     * @return true if the plugin was successfully downloaded and installed.
Gaston Dombiak's avatar
Gaston Dombiak committed
266
     */
267 268 269 270
    public boolean downloadPlugin(String url) {
        boolean installed = false;
        // Download and install new version of plugin
        HttpClient httpClient = new HttpClient();
271 272 273 274 275 276
        // Check if a proxy should be used
        if (isUsingProxy()) {
            HostConfiguration hc = new HostConfiguration();
            hc.setProxy(getProxyHost(), getProxyPort());
            httpClient.setHostConfiguration(hc);
        }
277 278
        GetMethod getMethod = new GetMethod(url);
        //execute the method
Gaston Dombiak's avatar
Gaston Dombiak committed
279
        try {
280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298
            int statusCode = httpClient.executeMethod(getMethod);
            if (statusCode == 200) {
                //get the resonse as an InputStream
                InputStream in = getMethod.getResponseBodyAsStream();
                String pluginFilename = url.substring(url.lastIndexOf("/") + 1);
                installed = XMPPServer.getInstance().getPluginManager()
                        .installPlugin(in, pluginFilename);
                in.close();
                if (installed) {
                    // Remove the plugin from the list of plugins to update
                    for (Update update : pluginUpdates) {
                        if (update.getURL().equals(url)) {
                            update.setDownloaded(true);
                        }
                    }
                    // Save response in a file for later retrieval
                    saveLatestServerInfo();
                }
            }
Gaston Dombiak's avatar
Gaston Dombiak committed
299
        }
300 301
        catch (IOException e) {
            Log.warn("Error downloading new plugin version", e);
Gaston Dombiak's avatar
Gaston Dombiak committed
302
        }
303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319
        return installed;
    }

    /**
     * Returns true if the plugin downloaded from the specified URL has been downloaded. Plugins
     * may be downloaded but not installed. The install process may take like 30 seconds to
     * detect new plugins to install.
     *
     * @param url the URL of the latest version of the plugin.
     * @return true if the plugin downloaded from the specified URL has been downloaded.
     */
    public boolean isPluginDownloaded(String url) {
        String pluginFilename = url.substring(url.lastIndexOf("/") + 1);
        return XMPPServer.getInstance().getPluginManager().isPluginDownloaded(pluginFilename);
    }

    /**
320
     * Returns the list of available plugins to install as reported by igniterealtime.org.
321 322
     * Currently installed plugins will not be included or plugins that require a newer
     * server version.
323
     *
324
     * @return the list of available plugins to install as reported by igniterealtime.org.
325 326
     */
    public List<AvailablePlugin> getNotInstalledPlugins() {
327
        List<AvailablePlugin> plugins = new ArrayList<AvailablePlugin>(availablePlugins.values());
328 329 330 331
        XMPPServer server = XMPPServer.getInstance();
        // Remove installed plugins from the list of available plugins
        for (Plugin plugin : server.getPluginManager().getPlugins()) {
            String pluginName = server.getPluginManager().getName(plugin);
332
            for (Iterator<AvailablePlugin> it = plugins.iterator(); it.hasNext();) {
333 334 335 336 337 338 339
                AvailablePlugin availablePlugin = it.next();
                if (availablePlugin.getName().equals(pluginName)) {
                    it.remove();
                    break;
                }
            }
        }
340
        // Remove plugins that require a newer server version
341
        Version currentServerVersion = XMPPServer.getInstance().getServerInfo().getVersion();
342 343
        for (Iterator<AvailablePlugin> it=plugins.iterator(); it.hasNext();) {
            AvailablePlugin plugin = it.next();
344 345
            Version pluginMinServerVersion = new Version(plugin.getMinServerVersion());
            if (pluginMinServerVersion.isNewerThan(currentServerVersion)) {
346 347 348
                it.remove();
            }
        }
349
        return plugins;
Gaston Dombiak's avatar
Gaston Dombiak committed
350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377
    }

    /**
     * Returns the message to send to admins when new updates are available. When sending
     * this message information about the new updates avaiable will be appended.
     *
     * @return the message to send to admins when new updates are available.
     */
    public String getNotificationMessage() {
        return LocaleUtils.getLocalizedString("update.notification-message");
    }

    /**
     * Returns true if the check for updates service is enabled.
     *
     * @return true if the check for updates service is enabled.
     */
    public boolean isServiceEnabled() {
        return JiveGlobals.getBooleanProperty("update.service-enabled", true);
    }

    /**
     * Sets if the check for updates service is enabled.
     *
     * @param enabled true if the check for updates service is enabled.
     */
    public void setServiceEnabled(boolean enabled) {
        JiveGlobals.setProperty("update.service-enabled", enabled ? "true" : "false");
378 379 380
        if (enabled && thread == null) {
            startService();
        }
Gaston Dombiak's avatar
Gaston Dombiak committed
381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402
    }

    /**
     * Returns true if admins should be notified by IM when new updates are available.
     *
     * @return true if admins should be notified by IM when new updates are available.
     */
    public boolean isNotificationEnabled() {
        return JiveGlobals.getBooleanProperty("update.notify-admins", true);
    }

    /**
     * Sets if admins should be notified by IM when new updates are available.
     *
     * @param enabled true if admins should be notified by IM when new updates are available.
     */
    public void setNotificationEnabled(boolean enabled) {
        JiveGlobals.setProperty("update.notify-admins", enabled ? "true" : "false");
    }

    /**
     * Returns the frequency to check for updates. By default, this will happen every 48 hours.
403
     * The frequency returned will never be less than 12 hours.
Gaston Dombiak's avatar
Gaston Dombiak committed
404 405 406 407
     *
     * @return the frequency to check for updates in hours.
     */
    public int getCheckFrequency() {
408 409 410 411 412 413 414
        int frequency = JiveGlobals.getIntProperty("update.frequency", 48);
        if (frequency < 12) {
            return 12;
        }
        else {
            return frequency;
        }
Gaston Dombiak's avatar
Gaston Dombiak committed
415 416 417 418 419 420 421 422 423 424 425
    }

    /**
     * Sets the frequency to check for updates. By default, this will happen every 48 hours.
     *
     * @param checkFrequency the frequency to check for updates.
     */
    public void setCheckFrequency(int checkFrequency) {
        JiveGlobals.setProperty("update.frequency", Integer.toString(checkFrequency));
    }

426
    /**
427
     * Returns true if a proxy is being used to connect to igniterealtime.org or false if
428 429
     * a direct connection should be attempted.
     *
430
     * @return true if a proxy is being used to connect to igniterealtime.org.
431 432 433 434 435 436
     */
    public boolean isUsingProxy() {
        return getProxyHost() != null;
    }

    /**
437
     * Returns the host of the proxy to use to connect to igniterealtime.org or <tt>null</tt>
438 439 440 441 442 443 444 445 446
     * if no proxy is used.
     *
     * @return the host of the proxy or null if no proxy is used.
     */
    public String getProxyHost() {
        return JiveGlobals.getProperty("update.proxy.host");
    }

    /**
447
     * Sets the host of the proxy to use to connect to igniterealtime.org or <tt>null</tt>
448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463
     * if no proxy is used.
     *
     * @param host the host of the proxy or null if no proxy is used.
     */
    public void setProxyHost(String host) {
        if (host == null) {
            // Remove the property
            JiveGlobals.deleteProperty("update.proxy.host");
        }
        else {
            // Create or update the property
            JiveGlobals.setProperty("update.proxy.host", host);
        }
    }

    /**
464
     * Returns the port of the proxy to use to connect to igniterealtime.org or -1 if no
465 466
     * proxy is being used.
     *
467
     * @return the port of the proxy to use to connect to igniterealtime.org or -1 if no
468 469 470 471 472 473 474
     *         proxy is being used.
     */
    public int getProxyPort() {
        return JiveGlobals.getIntProperty("update.proxy.port", -1);
    }

    /**
475
     * Sets the port of the proxy to use to connect to igniterealtime.org or -1 if no
476 477
     * proxy is being used.
     *
478
     * @param port the port of the proxy to use to connect to igniterealtime.org or -1 if no
479 480 481 482 483 484
     *        proxy is being used.
     */
    public void setProxyPort(int port) {
        JiveGlobals.setProperty("update.proxy.port", Integer.toString(port));
    }

Gaston Dombiak's avatar
Gaston Dombiak committed
485 486 487 488 489 490
    /**
     * Returns the server update or <tt>null</tt> if the server is up to date.
     *
     * @return the server update or null if the server is up to date.
     */
    public Update getServerUpdate() {
491
        return serverUpdate;
Gaston Dombiak's avatar
Gaston Dombiak committed
492 493 494 495 496
    }

    /**
     * Returns the plugin update or <tt>null</tt> if the plugin is up to date.
     *
497
     * @param pluginName     the name of the plugin (as described in the meta-data).
Gaston Dombiak's avatar
Gaston Dombiak committed
498 499 500 501
     * @param currentVersion current version of the plugin that is installed.
     * @return the plugin update or null if the plugin is up to date.
     */
    public Update getPluginUpdate(String pluginName, String currentVersion) {
502
        for (Update update : pluginUpdates) {
Gaston Dombiak's avatar
Gaston Dombiak committed
503 504 505 506 507 508 509 510 511 512 513
            // Check if this is the requested plugin
            if (update.getComponentName().equals(pluginName)) {
                // Check if the plugin version is right
                if (update.getLatestVersion().compareTo(currentVersion) > 0) {
                    return update;
                }
            }
        }
        return null;
    }

514
    private String getServerUpdateRequest() {
Gaston Dombiak's avatar
Gaston Dombiak committed
515 516
        XMPPServer server = XMPPServer.getInstance();
        Element xmlRequest = docFactory.createDocument().addElement("version");
517
        // Add current openfire version
Gaston Dombiak's avatar
Gaston Dombiak committed
518
        Element openfire = xmlRequest.addElement("openfire");
519
        openfire.addAttribute("current", server.getServerInfo().getVersion().getVersionString());
Gaston Dombiak's avatar
Gaston Dombiak committed
520 521 522
        return xmlRequest.asXML();
    }

523 524 525 526 527 528 529 530
    private String getAvailablePluginsUpdateRequest() {
        Element xmlRequest = docFactory.createDocument().addElement("available");
        // Add locale so we can get current name and description of plugins
        Element locale = xmlRequest.addElement("locale");
        locale.addText(JiveGlobals.getLocale().toString());
        return xmlRequest.asXML();
    }

531
    private void processServerUpdateResponse(String response, boolean notificationsEnabled)
Gaston Dombiak's avatar
Gaston Dombiak committed
532
            throws DocumentException {
533 534
        // Reset last known update information
        serverUpdate = null;
535 536 537
        SAXReader xmlReader = new SAXReader();
        xmlReader.setEncoding("UTF-8");
        Element xmlResponse = xmlReader.read(new StringReader(response)).getRootElement();
Gaston Dombiak's avatar
Gaston Dombiak committed
538
        // Parse response and keep info as Update objects
Gaston Dombiak's avatar
Gaston Dombiak committed
539
        Element openfire = xmlResponse.element("openfire");
540 541
        if (openfire != null) {
            // A new version of openfire was found
542 543 544 545 546 547 548
            Version latestVersion = new Version(openfire.attributeValue("latest"));
            if (latestVersion.isNewerThan(XMPPServer.getInstance().getServerInfo().getVersion())) {
                String changelog = openfire.attributeValue("changelog");
                String url = openfire.attributeValue("url");
                // Keep information about the available server update
                serverUpdate = new Update("Openfire", latestVersion.getVersionString(), changelog, url);
            }
Gaston Dombiak's avatar
Gaston Dombiak committed
549
        }
550 551 552 553 554 555 556 557 558 559 560
        // Check if we need to send notifications to admins
        if (notificationsEnabled && isNotificationEnabled() && serverUpdate != null) {
            Collection<JID> admins = XMPPServer.getInstance().getAdmins();
            Message notification = new Message();
            notification.setFrom(serverName);
            notification.setBody(getNotificationMessage() + " " + serverUpdate.getComponentName() +
                    " " + serverUpdate.getLatestVersion());
            for (JID jid : admins) {
                notification.setTo(jid);
                router.route(notification);
            }
Gaston Dombiak's avatar
Gaston Dombiak committed
561
        }
562 563
        // Save response in a file for later retrieval
        saveLatestServerInfo();
Gaston Dombiak's avatar
Gaston Dombiak committed
564 565
    }

566 567 568 569 570
    private void processAvailablePluginsResponse(String response, boolean notificationsEnabled)
            throws DocumentException {
        // Reset last known list of available plugins
        availablePlugins = new HashMap<String, AvailablePlugin>();

Gaston Dombiak's avatar
Gaston Dombiak committed
571
        // Parse response and keep info as AvailablePlugin objects
572 573 574
        SAXReader xmlReader = new SAXReader();
        xmlReader.setEncoding("UTF-8");
        Element xmlResponse = xmlReader.read(new StringReader(response)).getRootElement();
Gaston Dombiak's avatar
Gaston Dombiak committed
575 576
        Iterator plugins = xmlResponse.elementIterator("plugin");
        while (plugins.hasNext()) {
577
            Element plugin = (Element) plugins.next();
Gaston Dombiak's avatar
Gaston Dombiak committed
578 579 580 581 582 583
            String pluginName = plugin.attributeValue("name");
            String latestVersion = plugin.attributeValue("latest");
            String icon = plugin.attributeValue("icon");
            String readme = plugin.attributeValue("readme");
            String changelog = plugin.attributeValue("changelog");
            String url = plugin.attributeValue("url");
584
            String licenseType = plugin.attributeValue("licenseType");
Gaston Dombiak's avatar
Gaston Dombiak committed
585 586 587
            String description = plugin.attributeValue("description");
            String author = plugin.attributeValue("author");
            String minServerVersion = plugin.attributeValue("minServerVersion");
588
            String fileSize = plugin.attributeValue("fileSize");
Gaston Dombiak's avatar
Gaston Dombiak committed
589
            AvailablePlugin available = new AvailablePlugin(pluginName, description, latestVersion,
590
                    author, icon, changelog, readme, licenseType, minServerVersion, url, fileSize);
591 592
            // Add plugin to the list of available plugins at js.org
            availablePlugins.put(pluginName, available);
Gaston Dombiak's avatar
Gaston Dombiak committed
593
        }
594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609

        // Figure out local plugins that need to be updated
        buildPluginsUpdateList();

        // Check if we need to send notifications to admins
        if (notificationsEnabled && isNotificationEnabled() && !pluginUpdates.isEmpty()) {
            Collection<JID> admins = XMPPServer.getInstance().getAdmins();
            for (Update update : pluginUpdates) {
                Message notification = new Message();
                notification.setFrom(serverName);
                notification.setBody(getNotificationMessage() + " " + update.getComponentName() +
                        " " + update.getLatestVersion());
                for (JID jid : admins) {
                    notification.setTo(jid);
                    router.route(notification);
                }
Gaston Dombiak's avatar
Gaston Dombiak committed
610
            }
611 612 613 614
        }

        // Save information of available plugins
        saveAvailablePluginsInfo();
Gaston Dombiak's avatar
Gaston Dombiak committed
615 616 617
    }

    /**
618
     * Recreate the list of plugins that need to be updated based on the list of
619
     * available plugins at igniterealtime.org.
Gaston Dombiak's avatar
Gaston Dombiak committed
620
     */
621 622 623 624
    private void buildPluginsUpdateList() {
        // Reset list of plugins that need to be updated
        pluginUpdates = new ArrayList<Update>();
        XMPPServer server = XMPPServer.getInstance();
625
        Version currentServerVersion = XMPPServer.getInstance().getServerInfo().getVersion();
626 627 628 629
        // Compare local plugins versions with latest ones
        for (Plugin plugin : server.getPluginManager().getPlugins()) {
            String pluginName = server.getPluginManager().getName(plugin);
            AvailablePlugin latestPlugin = availablePlugins.get(pluginName);
630 631 632 633 634 635 636 637 638 639 640 641 642

            if (latestPlugin != null) {
				Version currentPluginVersion = new Version(server.getPluginManager().getVersion(plugin));
				Version latestPluginVersion = new Version(latestPlugin.getLatestVersion());
            	if (latestPluginVersion.isNewerThan(currentPluginVersion)) {
					// Check if the update can run in the current version of the server
					Version pluginMinServerVersion = new Version(latestPlugin.getMinServerVersion());
					if (!pluginMinServerVersion.isNewerThan(currentServerVersion)) {
						Update update = new Update(pluginName, latestPlugin.getLatestVersion(),
								latestPlugin.getChangelog(), latestPlugin.getURL());
						pluginUpdates.add(update);
					}
				}
643 644 645 646 647
            }
        }
    }

    /**
648
     * Saves to conf/server-update.xml information about the latest Openfire release that is
649 650 651
     * available for download.
     */
    private void saveLatestServerInfo() {
Gaston Dombiak's avatar
Gaston Dombiak committed
652
        Element xmlResponse = docFactory.createDocument().addElement("version");
653
        if (serverUpdate != null) {
Gaston Dombiak's avatar
Gaston Dombiak committed
654
            Element component = xmlResponse.addElement("openfire");
655 656 657 658 659 660 661 662 663 664 665
            component.addAttribute("latest", serverUpdate.getLatestVersion());
            component.addAttribute("changelog", serverUpdate.getChangelog());
            component.addAttribute("url", serverUpdate.getURL());
        }
        // Write data out to conf/server-update.xml file.
        Writer writer = null;
        try {
            // Create the conf folder if required
            File file = new File(JiveGlobals.getHomeDirectory(), "conf");
            if (!file.exists()) {
                file.mkdir();
Gaston Dombiak's avatar
Gaston Dombiak committed
666
            }
667 668 669 670 671
            file = new File(JiveGlobals.getHomeDirectory() + File.separator + "conf",
                    "server-update.xml");
            // Delete the old server-update.xml file if it exists
            if (file.exists()) {
                file.delete();
Gaston Dombiak's avatar
Gaston Dombiak committed
672
            }
673
            // Create new version.xml with returned data
674
            writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), "UTF-8"));
675 676 677 678 679
            OutputFormat prettyPrinter = OutputFormat.createPrettyPrint();
            XMLWriter xmlWriter = new XMLWriter(writer, prettyPrinter);
            xmlWriter.write(xmlResponse);
        }
        catch (Exception e) {
680
            Log.error(e.getMessage(), e);
681 682 683 684 685 686 687
        }
        finally {
            if (writer != null) {
                try {
                    writer.close();
                }
                catch (IOException e1) {
688
                    Log.error(e1.getMessage(), e1);
689
                }
Gaston Dombiak's avatar
Gaston Dombiak committed
690 691
            }
        }
692 693 694 695
    }

    /**
     * Saves to conf/available-plugins.xml the list of plugins that are available
696
     * at igniterealtime.org.
697 698 699 700 701 702 703 704 705 706 707 708 709 710 711
     */
    private void saveAvailablePluginsInfo() {
        //  XML to store in the file
        Element xml = docFactory.createDocument().addElement("available");
        for (AvailablePlugin plugin : availablePlugins.values()) {
            Element component = xml.addElement("plugin");
            component.addAttribute("name", plugin.getName());
            component.addAttribute("latest", plugin.getLatestVersion());
            component.addAttribute("changelog", plugin.getChangelog());
            component.addAttribute("url", plugin.getURL());
            component.addAttribute("author", plugin.getAuthor());
            component.addAttribute("description", plugin.getDescription());
            component.addAttribute("icon", plugin.getIcon());
            component.addAttribute("minServerVersion", plugin.getMinServerVersion());
            component.addAttribute("readme", plugin.getReadme());
712 713
            component.addAttribute("licenseType", plugin.getLicenseType());
            component.addAttribute("fileSize", Long.toString(plugin.getFileSize()));
714 715
        }
        // Write data out to conf/available-plugins.xml file.
Gaston Dombiak's avatar
Gaston Dombiak committed
716 717 718 719 720 721 722 723
        Writer writer = null;
        try {
            // Create the conf folder if required
            File file = new File(JiveGlobals.getHomeDirectory(), "conf");
            if (!file.exists()) {
                file.mkdir();
            }
            file = new File(JiveGlobals.getHomeDirectory() + File.separator + "conf",
724
                    "available-plugins.xml");
Gaston Dombiak's avatar
Gaston Dombiak committed
725 726 727 728 729
            // Delete the old version.xml file if it exists
            if (file.exists()) {
                file.delete();
            }
            // Create new version.xml with returned data
730
            writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), "UTF-8"));
Gaston Dombiak's avatar
Gaston Dombiak committed
731 732
            OutputFormat prettyPrinter = OutputFormat.createPrettyPrint();
            XMLWriter xmlWriter = new XMLWriter(writer, prettyPrinter);
733
            xmlWriter.write(xml);
Gaston Dombiak's avatar
Gaston Dombiak committed
734 735
        }
        catch (Exception e) {
736
            Log.error(e.getMessage(), e);
Gaston Dombiak's avatar
Gaston Dombiak committed
737 738 739 740 741 742 743
        }
        finally {
            if (writer != null) {
                try {
                    writer.close();
                }
                catch (IOException e1) {
744
                    Log.error(e1.getMessage(), e1);
Gaston Dombiak's avatar
Gaston Dombiak committed
745 746 747 748 749 750
                }
            }
        }
    }

    /**
751 752
     * Loads list of available plugins and latest available server version from
     * conf/available-plugins.xml and conf/server-update.xml respectively.
Gaston Dombiak's avatar
Gaston Dombiak committed
753
     */
754 755 756 757 758 759 760 761 762 763
    private void loadSavedInfo() {
        // Load server update information
        loadLatestServerInfo();
        // Load available plugins information
        loadAvailablePluginsInfo();
        // Recreate list of plugins to update
        buildPluginsUpdateList();
    }

    private void loadLatestServerInfo() {
Gaston Dombiak's avatar
Gaston Dombiak committed
764
        Document xmlResponse;
765 766
        File file = new File(JiveGlobals.getHomeDirectory() + File.separator + "conf",
                "server-update.xml");
Gaston Dombiak's avatar
Gaston Dombiak committed
767
        if (!file.exists()) {
768
            return;
Gaston Dombiak's avatar
Gaston Dombiak committed
769 770 771
        }
        // Check read privs.
        if (!file.canRead()) {
772 773
            Log.warn("Cannot retrieve server updates. File must be readable: " + file.getName());
            return;
Gaston Dombiak's avatar
Gaston Dombiak committed
774 775 776 777 778
        }
        FileReader reader = null;
        try {
            reader = new FileReader(file);
            SAXReader xmlReader = new SAXReader();
779
            xmlReader.setEncoding("UTF-8");
Gaston Dombiak's avatar
Gaston Dombiak committed
780 781 782
            xmlResponse = xmlReader.read(reader);
        }
        catch (Exception e) {
783 784
            Log.error("Error reading server-update.xml", e);
            return;
Gaston Dombiak's avatar
Gaston Dombiak committed
785 786 787 788 789 790 791 792 793 794 795
        }
        finally {
            if (reader != null) {
                try {
                    reader.close();
                }
                catch (Exception e) {
                    // Do nothing
                }
            }
        }
796
        // Parse info and recreate update information (if still required)
Gaston Dombiak's avatar
Gaston Dombiak committed
797
        Element openfire = xmlResponse.getRootElement().element("openfire");
798
        if (openfire != null) {
799
            Version latestVersion = new Version(openfire.attributeValue("latest"));
800 801
            String changelog = openfire.attributeValue("changelog");
            String url = openfire.attributeValue("url");
802
            // Check if current server version is correct
803 804
            Version currentServerVersion = XMPPServer.getInstance().getServerInfo().getVersion();
            if (latestVersion.isNewerThan(currentServerVersion)) {
805
                serverUpdate = new Update("Openfire", latestVersion.getVersionString(), changelog, url);
806 807
            }
        }
Gaston Dombiak's avatar
Gaston Dombiak committed
808 809
    }

810 811 812 813 814 815 816 817 818 819 820 821 822
    private void loadAvailablePluginsInfo() {
        Document xmlResponse;
        File file = new File(JiveGlobals.getHomeDirectory() + File.separator + "conf",
                "available-plugins.xml");
        if (!file.exists()) {
            return;
        }
        // Check read privs.
        if (!file.canRead()) {
            Log.warn("Cannot retrieve available plugins. File must be readable: " + file.getName());
            return;
        }
        FileReader reader = null;
Gaston Dombiak's avatar
Gaston Dombiak committed
823
        try {
824 825
            reader = new FileReader(file);
            SAXReader xmlReader = new SAXReader();
826
            xmlReader.setEncoding("UTF-8");
827 828 829 830 831 832 833 834 835 836 837 838 839
            xmlResponse = xmlReader.read(reader);
        }
        catch (Exception e) {
            Log.error("Error reading available-plugins.xml", e);
            return;
        }
        finally {
            if (reader != null) {
                try {
                    reader.close();
                }
                catch (Exception e) {
                    // Do nothing
Gaston Dombiak's avatar
Gaston Dombiak committed
840 841 842
                }
            }
        }
843 844 845
        // Parse info and recreate available plugins
        Iterator it = xmlResponse.getRootElement().elementIterator("plugin");
        while (it.hasNext()) {
846
            Element plugin = (Element) it.next();
847 848 849 850 851 852
            String pluginName = plugin.attributeValue("name");
            String latestVersion = plugin.attributeValue("latest");
            String icon = plugin.attributeValue("icon");
            String readme = plugin.attributeValue("readme");
            String changelog = plugin.attributeValue("changelog");
            String url = plugin.attributeValue("url");
853
            String licenseType = plugin.attributeValue("licenseType");
854 855 856
            String description = plugin.attributeValue("description");
            String author = plugin.attributeValue("author");
            String minServerVersion = plugin.attributeValue("minServerVersion");
857
            String fileSize = plugin.attributeValue("fileSize");
858
            AvailablePlugin available = new AvailablePlugin(pluginName, description, latestVersion,
859
                    author, icon, changelog, readme, licenseType, minServerVersion, url, fileSize);
860 861
            // Add plugin to the list of available plugins at js.org
            availablePlugins.put(pluginName, available);
Gaston Dombiak's avatar
Gaston Dombiak committed
862 863
        }
    }
864 865 866 867 868 869 870 871 872

    /**
     * Returns a previously fetched list of updates.
     *
     * @return a previously fetched list of updates.
     */
    public Collection<Update> getPluginUpdates() {
        return pluginUpdates;
    }
Gaston Dombiak's avatar
Gaston Dombiak committed
873
}