diff --git a/core/src/main/java/hudson/Functions.java b/core/src/main/java/hudson/Functions.java
index 60b23cca7fb7b4d46c5befddaed593127e326d15..a5bbc68493b3282f2361d13e50c5844c07df048f 100644
--- a/core/src/main/java/hudson/Functions.java
+++ b/core/src/main/java/hudson/Functions.java
@@ -1067,13 +1067,6 @@ public class Functions {
return null;
}
- /**
- * Gets the URL for the update center server
- */
- public String getUpdateCenterUrl() {
- return Hudson.getInstance().getUpdateCenter().getUrl();
- }
-
/**
* If the given href link is matching the current page, return true.
*
diff --git a/core/src/main/java/hudson/PluginManager.java b/core/src/main/java/hudson/PluginManager.java
index d4cbe81e6fbd58b0d8c0e05a918b0ce2b084262a..9086bc8501a49da8d6bd8e294a3e41c24ca6161b 100644
--- a/core/src/main/java/hudson/PluginManager.java
+++ b/core/src/main/java/hudson/PluginManager.java
@@ -26,6 +26,7 @@ package hudson;
import hudson.model.AbstractModelObject;
import hudson.model.Failure;
import hudson.model.Hudson;
+import hudson.model.UpdateSite;
import hudson.model.UpdateCenter;
import hudson.util.Service;
import org.apache.commons.fileupload.FileItem;
@@ -352,6 +353,25 @@ public final class PluginManager extends AbstractModelObject {
LogFactory.release(uberClassLoader);
}
+ public HttpResponse doUpdateSources(StaplerRequest req) throws IOException {
+ Hudson.getInstance().checkPermission(Hudson.ADMINISTER);
+
+ if (req.hasParameter("remove")) {
+ UpdateCenter uc = Hudson.getInstance().getUpdateCenter();
+ BulkChange bc = new BulkChange(uc);
+ try {
+ for (String id : req.getParameterValues("sources"))
+ uc.getSites().remove(uc.getById(id));
+ } finally {
+ bc.commit();
+ }
+ } else
+ if (req.hasParameter("add"))
+ return new HttpRedirect("addSite");
+
+ return new HttpRedirect("./sites");
+ }
+
/**
* Performs the installation of the plugins.
*/
@@ -361,10 +381,13 @@ public final class PluginManager extends AbstractModelObject {
String n = en.nextElement();
if(n.startsWith("plugin.")) {
n = n.substring(7);
- UpdateCenter.Plugin p = Hudson.getInstance().getUpdateCenter().getPlugin(n);
- if(p==null)
- throw new Failure("No such plugin: "+n);
- p.deploy();
+ if (n.indexOf(".") > 0) {
+ String[] pluginInfo = n.split("\\.");
+ UpdateSite.Plugin p = Hudson.getInstance().getUpdateCenter().getById(pluginInfo[1]).getPlugin(pluginInfo[0]);
+ if(p==null)
+ throw new Failure("No such plugin: "+n);
+ p.deploy();
+ }
}
}
rsp.sendRedirect("../updateCenter/");
@@ -376,9 +399,9 @@ public final class PluginManager extends AbstractModelObject {
@QueryParameter("proxy.userName") String userName,
@QueryParameter("proxy.password") String password,
StaplerResponse rsp) throws IOException {
- Hudson.getInstance().checkPermission(Hudson.ADMINISTER);
-
Hudson hudson = Hudson.getInstance();
+ hudson.checkPermission(Hudson.ADMINISTER);
+
server = Util.fixEmptyAndTrim(server);
if(server==null) {
hudson.proxy = null;
diff --git a/core/src/main/java/hudson/PluginWrapper.java b/core/src/main/java/hudson/PluginWrapper.java
index 4a9c9a81191e57976efa3394754598b56b5997ac..4f610a0910439ee2c0b2475c0c8f5ab26ee56c8f 100644
--- a/core/src/main/java/hudson/PluginWrapper.java
+++ b/core/src/main/java/hudson/PluginWrapper.java
@@ -25,6 +25,7 @@ package hudson;
import hudson.model.Hudson;
import hudson.model.UpdateCenter;
+import hudson.model.UpdateSite;
import java.io.File;
import java.io.FileOutputStream;
@@ -252,7 +253,7 @@ public final class PluginWrapper {
if(url!=null) return url;
// fallback to update center metadata
- UpdateCenter.Plugin ui = getInfo();
+ UpdateSite.Plugin ui = getInfo();
if(ui!=null) return ui.wiki;
return null;
@@ -360,23 +361,23 @@ public final class PluginWrapper {
/**
* If the plugin has {@link #getUpdateInfo() an update},
- * returns the {@link UpdateCenter.Plugin} object.
+ * returns the {@link UpdateSite.Plugin} object.
*
* @return
* This method may return null — for example,
* the user may have installed a plugin locally developed.
*/
- public UpdateCenter.Plugin getUpdateInfo() {
+ public UpdateSite.Plugin getUpdateInfo() {
UpdateCenter uc = Hudson.getInstance().getUpdateCenter();
- UpdateCenter.Plugin p = uc.getPlugin(getShortName());
+ UpdateSite.Plugin p = uc.getPlugin(getShortName());
if(p!=null && p.isNewerThan(getVersion())) return p;
return null;
}
/**
- * returns the {@link UpdateCenter.Plugin} object, or null.
+ * returns the {@link UpdateSite.Plugin} object, or null.
*/
- public UpdateCenter.Plugin getInfo() {
+ public UpdateSite.Plugin getInfo() {
UpdateCenter uc = Hudson.getInstance().getUpdateCenter();
return uc.getPlugin(getShortName());
}
diff --git a/core/src/main/java/hudson/XmlFile.java b/core/src/main/java/hudson/XmlFile.java
index 6ed07f8083d163d79ac797a74bc026c77941e50e..7318c16ed0a1e02469e7e39708e16b07388f1f98 100644
--- a/core/src/main/java/hudson/XmlFile.java
+++ b/core/src/main/java/hudson/XmlFile.java
@@ -140,7 +140,7 @@ public final class XmlFile {
*
* @return
* The unmarshalled object. Usually the same as o, but would be different
- * if the XML representation if completely new.
+ * if the XML representation is completely new.
*/
public Object unmarshal( Object o ) throws IOException {
Reader r = new BufferedReader(new InputStreamReader(new FileInputStream(file),"UTF-8"));
diff --git a/core/src/main/java/hudson/model/DownloadService.java b/core/src/main/java/hudson/model/DownloadService.java
index f5b0beaa61825f4a2358fe5d953e9d5c5ec4fd9e..edaf8614b010a3e40188092e607f1f9692c3e272 100644
--- a/core/src/main/java/hudson/model/DownloadService.java
+++ b/core/src/main/java/hudson/model/DownloadService.java
@@ -152,7 +152,7 @@ public class DownloadService extends PageDecorator {
* URL to download.
*/
public String getUrl() {
- return Hudson.getInstance().getUpdateCenter().getUrl()+"updates/"+url;
+ return Hudson.getInstance().getUpdateCenter().getDefaultBaseUrl()+"updates/"+url;
}
/**
diff --git a/core/src/main/java/hudson/model/Hudson.java b/core/src/main/java/hudson/model/Hudson.java
index 5dd6dd42ba204060cfe807f5757a54d74f86ab33..60d6ecfa42b5db295228fb6fb22871dd4f82685f 100644
--- a/core/src/main/java/hudson/model/Hudson.java
+++ b/core/src/main/java/hudson/model/Hudson.java
@@ -490,7 +490,7 @@ public final class Hudson extends Node implements ItemGroup, Stapl
*/
private transient final String secretKey;
- private transient final UpdateCenter updateCenter = new UpdateCenter(this);
+ private transient final UpdateCenter updateCenter = new UpdateCenter();
/**
* True if the user opted out from the statistics tracking. We'll never send anything if this is true.
@@ -522,7 +522,7 @@ public final class Hudson extends Node implements ItemGroup, Stapl
Trigger.timer = new Timer("Hudson cron thread");
queue = new Queue(CONSISTENT_HASH?LoadBalancer.CONSISTENT_HASH:LoadBalancer.DEFAULT);
-
+
try {
dependencyGraph = DependencyGraph.EMPTY;
} catch (InternalError e) {
@@ -549,7 +549,7 @@ public final class Hudson extends Node implements ItemGroup, Stapl
} catch (IOException e) {
LOGGER.log(SEVERE, "Failed to load proxy configuration", e);
}
-
+
// load plugins.
pluginManager = new PluginManager(context);
pluginManager.initialize();
@@ -620,6 +620,8 @@ public final class Hudson extends Node implements ItemGroup, Stapl
FileUtils.writeStringToFile(new File(userContentDir,"readme.txt"),Messages.Hudson_USER_CONTENT_README());
}
+ updateCenter.load(); // this has to wait until after all plugins load, to let custom UpdateCenterConfiguration take effect first.
+
Trigger.init();
// pending SEZPOZ-8
// // invoke post initialization methods
diff --git a/core/src/main/java/hudson/model/UpdateCenter.java b/core/src/main/java/hudson/model/UpdateCenter.java
index 722b868cf321ce80c4e660a20ddf399202ac5e11..11962f43c8c180d5e4cf6ef5cb49976fbcd11a0e 100644
--- a/core/src/main/java/hudson/model/UpdateCenter.java
+++ b/core/src/main/java/hudson/model/UpdateCenter.java
@@ -23,69 +23,51 @@
*/
package hudson.model;
+import hudson.BulkChange;
+import hudson.Extension;
import hudson.ExtensionPoint;
import hudson.Functions;
import hudson.PluginManager;
import hudson.PluginWrapper;
-import hudson.Util;
import hudson.ProxyConfiguration;
-import hudson.Extension;
+import hudson.Util;
+import hudson.XmlFile;
import hudson.lifecycle.Lifecycle;
+import hudson.model.UpdateSite.Data;
+import hudson.model.UpdateSite.Plugin;
import hudson.util.DaemonThreadFactory;
-import hudson.util.TextFile;
-import hudson.util.VersionNumber;
import hudson.util.IOException2;
-import static hudson.util.TimeUnit2.DAYS;
-import net.sf.json.JSONObject;
+import hudson.util.PersistedList;
+import hudson.util.XStream2;
import org.acegisecurity.Authentication;
-import org.apache.commons.io.input.CountingInputStream;
import org.apache.commons.io.IOUtils;
+import org.apache.commons.io.input.CountingInputStream;
import org.apache.commons.io.output.NullOutputStream;
-import org.apache.commons.io.output.TeeOutputStream;
-import org.kohsuke.stapler.DataBoundConstructor;
-import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
-import org.jvnet.hudson.crypto.SignatureOutputStream;
-import org.jvnet.hudson.crypto.CertificateUtil;
-import javax.servlet.ServletException;
-import javax.servlet.ServletContext;
import javax.net.ssl.SSLHandshakeException;
+import javax.servlet.ServletException;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
-import java.io.OutputStream;
import java.io.InputStream;
-import java.io.ByteArrayInputStream;
-import java.io.OutputStreamWriter;
+import java.io.OutputStream;
+import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.net.UnknownHostException;
-import java.net.MalformedURLException;
import java.util.ArrayList;
-import java.util.Collections;
+import java.util.HashSet;
import java.util.List;
-import java.util.Map;
import java.util.Set;
-import java.util.TreeMap;
import java.util.Vector;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
-import java.util.concurrent.ThreadFactory;
import java.util.concurrent.Future;
+import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;
-import java.security.MessageDigest;
-import java.security.DigestOutputStream;
-import java.security.GeneralSecurityException;
-import java.security.Signature;
-import java.security.cert.CertificateFactory;
-import java.security.cert.X509Certificate;
-import java.security.cert.TrustAnchor;
-import java.security.cert.Certificate;
-
-import com.trilead.ssh2.crypto.Base64;
/**
* Controls update center capability.
@@ -102,22 +84,7 @@ import com.trilead.ssh2.crypto.Base64;
* @author Kohsuke Kawaguchi
* @since 1.220
*/
-public class UpdateCenter extends AbstractModelObject {
- /**
- * What's the time stamp of data file?
- */
- private long dataTimestamp = -1;
-
- /**
- * When was the last time we asked a browser to check the data for us?
- *
- *
- * There's normally some delay between when we send HTML that includes the check code,
- * until we get the data back, so this variable is used to avoid asking too many browseres
- * all at once.
- */
- private volatile long lastAttempt = -1;
-
+public class UpdateCenter extends AbstractModelObject implements Saveable {
/**
* {@link ExecutorService} that performs installation.
*/
@@ -136,22 +103,30 @@ public class UpdateCenter extends AbstractModelObject {
private final Vector jobs = new Vector();
/**
- * Update center configuration data
+ * {@link UpdateSite}s from which we've already installed a plugin at least once.
+ * This is used to skip network tests.
*/
- private UpdateCenterConfiguration config;
-
+ private final Set sourcesUsed = new HashSet();
+
/**
- * Create update center to get plugins/updates from hudson.dev.java.net
+ * List of {@link UpdateSite}s to be used.
*/
- public UpdateCenter(Hudson parent) {
+ private final PersistedList sites = new PersistedList(this);
+
+ /**
+ * Update center configuration data
+ */
+ private UpdateCenterConfiguration config;
+
+ public UpdateCenter() {
configure(new UpdateCenterConfiguration());
}
-
+
/**
* Configures update center to get plugins/updates from alternate servers,
* and optionally using alternate strategies for downloading, installing
* and upgrading.
- *
+ *
* @param config Configuration data
* @see UpdateCenterConfiguration
*/
@@ -160,19 +135,6 @@ public class UpdateCenter extends AbstractModelObject {
this.config = config;
}
}
-
- /**
- * Returns true if it's time for us to check for new version.
- */
- public boolean isDue() {
- if(neverUpdate) return false;
- if(dataTimestamp==-1)
- dataTimestamp = getDataFile().file.lastModified();
- long now = System.currentTimeMillis();
- boolean due = now - dataTimestamp > DAY && now - lastAttempt > 15000;
- if(due) lastAttempt = now;
- return due;
- }
/**
* Returns the list of {@link UpdateCenterJob} representing scheduled installation attempts.
@@ -187,90 +149,77 @@ public class UpdateCenter extends AbstractModelObject {
}
/**
- * Gets the string representing how long ago the data was obtained.
+ * Returns the list of {@link UpdateSite}s to be used.
+ * This is a live list, whose change will be persisted automatically.
+ *
+ * @return
+ * can be empty but never null.
*/
- public String getLastUpdatedString() {
- if(dataTimestamp<0) return "N/A";
- return Util.getPastTimeString(System.currentTimeMillis()-dataTimestamp);
+ public PersistedList getSites() {
+ return sites;
}
/**
- * This is the endpoint that receives the update center data file from the browser.
+ * Gets the string representing how long ago the data was obtained.
+ * Will be the newest of all {@link UpdateSite}s.
*/
- public void doPostBack(StaplerRequest req) throws IOException, GeneralSecurityException {
- dataTimestamp = System.currentTimeMillis();
- String p = req.getParameter("json");
- JSONObject o = JSONObject.fromObject(p);
-
- int v = o.getInt("updateCenterVersion");
- if(v !=1) {
- LOGGER.warning("Unrecognized update center version: "+v);
- return;
+ public String getLastUpdatedString() {
+ long newestTs = -1;
+ for (UpdateSite s : sites) {
+ if (s.getDataTimestamp()>newestTs) {
+ newestTs = s.getDataTimestamp();
+ }
}
-
- if (signatureCheck)
- verifySignature(o);
-
- LOGGER.info("Obtained the latest update center data file");
- getDataFile().write(p);
+ if(newestTs<0) return "N/A";
+ return Util.getPastTimeString(System.currentTimeMillis()-newestTs);
}
/**
- * Verifies the signature in the update center data file.
+ * Gets {@link UpdateSite} by its ID.
+ * Used to bind them to URL.
*/
- private boolean verifySignature(JSONObject o) throws GeneralSecurityException, IOException {
- JSONObject signature = o.getJSONObject("signature");
- if (signature.isNullObject()) {
- LOGGER.severe("No signature block found");
- return false;
- }
- o.remove("signature");
-
- List certs = new ArrayList();
- {// load and verify certificates
- CertificateFactory cf = CertificateFactory.getInstance("X509");
- for (Object cert : o.getJSONArray("certificates")) {
- X509Certificate c = (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(Base64.decode(cert.toString().toCharArray())));
- c.checkValidity();
- certs.add(c);
- }
-
- // all default root CAs in JVM are trusted, plus certs bundled in Hudson
- Set anchors = CertificateUtil.getDefaultRootCAs();
- ServletContext context = Hudson.getInstance().servletContext;
- for (String cert : (Set) context.getResourcePaths("/WEB-INF/update-center-rootCAs")) {
- if (cert.endsWith(".txt")) continue; // skip text files that are meant to be documentation
- anchors.add(new TrustAnchor((X509Certificate)cf.generateCertificate(context.getResourceAsStream(cert)),null));
+ public UpdateSite getById(String id) {
+ for (UpdateSite s : sites) {
+ if (s.getId().equals(id)) {
+ return s;
}
- CertificateUtil.validatePath(certs);
}
+ return null;
+ }
- // this is for computing a digest to check sanity
- MessageDigest sha1 = MessageDigest.getInstance("SHA1");
- DigestOutputStream dos = new DigestOutputStream(new NullOutputStream(),sha1);
-
- // this is for computing a signature
- Signature sig = Signature.getInstance("SHA1withRSA");
- sig.initVerify(certs.get(0));
- SignatureOutputStream sos = new SignatureOutputStream(sig);
-
- o.writeCanonical(new OutputStreamWriter(new TeeOutputStream(dos,sos),"UTF-8"));
+ /**
+ * Gets the {@link UpdateSite} from which we receive updates for hudson.war.
+ *
+ * @return
+ * null if no such update center is provided.
+ */
+ public UpdateSite getCoreSource() {
+ for (UpdateSite s : sites)
+ if (s.getData().core!=null)
+ return s;
+ return null;
+ }
- // did the digest match? this is not a part of the signature validation, but if we have a bug in the c14n
- // (which is more likely than someone tampering with update center), we can tell
- String computedDigest = new String(Base64.encode(sha1.digest()));
- String providedDigest = signature.getString("digest");
- if (!computedDigest.equalsIgnoreCase(providedDigest)) {
- LOGGER.severe("Digest mismatch: "+computedDigest+" vs "+providedDigest);
- return false;
- }
+ /**
+ * Gets the default base URL.
+ *
+ * @deprecated
+ * TODO: revisit tool update mechanism, as that should be de-centralized, too. In the mean time,
+ * please try not to use this method, and instead ping us to get this part completed.
+ */
+ public String getDefaultBaseUrl() {
+ return config.getUpdateCenterUrl();
+ }
- if (!sig.verify(Base64.decode(signature.getString("signature").toCharArray()))) {
- LOGGER.severe("Signature in the update center doesn't match with the certificate");
- return false;
+ /**
+ * Gets the plugin with the given name from the first {@link UpdateSite} to contain it.
+ */
+ public Plugin getPlugin(String artifactId) {
+ for (UpdateSite s : sites) {
+ Plugin p = s.getPlugin(artifactId);
+ if (p!=null) return p;
}
-
- return true;
+ return null;
}
/**
@@ -279,7 +228,7 @@ public class UpdateCenter extends AbstractModelObject {
public void doUpgrade(StaplerResponse rsp) throws IOException, ServletException {
requirePOST();
Hudson.getInstance().checkPermission(Hudson.ADMINISTER);
- HudsonUpgradeJob job = new HudsonUpgradeJob(Hudson.getAuthentication());
+ HudsonUpgradeJob job = new HudsonUpgradeJob(getCoreSource(), Hudson.getAuthentication());
if(!Lifecycle.get().canRewriteHudsonWar()) {
sendError("Hudson upgrade not supported in this running mode");
return;
@@ -290,120 +239,78 @@ public class UpdateCenter extends AbstractModelObject {
rsp.sendRedirect2(".");
}
- private Future addJob(UpdateCenterJob job) {
+ /*package*/ synchronized Future addJob(UpdateCenterJob job) {
// the first job is always the connectivity check
- if(jobs.size()==0)
- new ConnectionCheckJob().submit();
+ if (sourcesUsed.add(job.site))
+ new ConnectionCheckJob(job.site).submit();
return job.submit();
}
- /**
- * Loads the update center data, if any.
- *
- * @return null if no data is available.
- */
- public Data getData() {
- TextFile df = getDataFile();
- if(df.exists()) {
- try {
- return new Data(JSONObject.fromObject(df.read()));
- } catch (IOException e) {
- LOGGER.log(Level.SEVERE,"Failed to parse "+df,e);
- df.delete(); // if we keep this file, it will cause repeated failures
- return null;
- }
- } else {
- return null;
- }
+ public String getDisplayName() {
+ return "Update center";
+ }
+
+ public String getSearchUrl() {
+ return "updateCenter";
}
/**
- * Returns a list of plugins that should be shown in the "available" tab.
- * These are "all plugins - installed plugins".
+ * Saves the configuration info to the disk.
*/
- public List getAvailables() {
- List r = new ArrayList();
- Data data = getData();
- if(data ==null) return Collections.emptyList();
- for (Plugin p : data.plugins.values()) {
- if(p.getInstalled()==null)
- r.add(p);
+ public synchronized void save() {
+ if(BulkChange.contains(this)) return;
+ try {
+ getConfigFile().write(sites);
+ } catch (IOException e) {
+ LOGGER.log(Level.WARNING, "Failed to save "+getConfigFile(),e);
}
- return r;
}
/**
- * Gets the information about a specific plugin.
- *
- * @param artifactId
- * The short name of the plugin. Corresponds to {@link PluginWrapper#getShortName()}.
- *
- * @return
- * null if no such information is found.
+ * Loads the data from the disk into this object.
*/
- public Plugin getPlugin(String artifactId) {
- Data dt = getData();
- if(dt==null) return null;
- return dt.plugins.get(artifactId);
+ public synchronized void load() throws IOException {
+ XmlFile file = getConfigFile();
+ if(file.exists()) {
+ try {
+ sites.replaceBy(((PersistedList)file.unmarshal(sites)).toList());
+ } catch (IOException e) {
+ LOGGER.log(Level.WARNING, "Failed to load "+file, e);
+ }
+ } else {
+ // If there aren't already any UpdateSources, add the default one.
+ if (sites.isEmpty()) {
+ // to maintain compatibility with existing UpdateCenterConfiguration, create the default one as specified by UpdateCenterConfiguration
+ sites.add(new UpdateSite("default",config.getUpdateCenterUrl()+"update-center.json"));
+ }
+ }
}
- /**
- * This is where we store the update center data.
- */
- private TextFile getDataFile() {
- return new TextFile(new File(Hudson.getInstance().root,"update-center.json"));
+ private XmlFile getConfigFile() {
+ return new XmlFile(XSTREAM,new File(Hudson.getInstance().root,
+ UpdateCenter.class.getName()+".xml"));
}
- /**
- * Returns the list of plugins that are updates to currently installed ones.
- *
- * @return
- * can be empty but never null.
- */
- public List getUpdates() {
- Data data = getData();
- if(data==null) return Collections.emptyList(); // fail to determine
+ public List getAvailables() {
+ List plugins = new ArrayList();
- List r = new ArrayList();
- for (PluginWrapper pw : Hudson.getInstance().getPluginManager().getPlugins()) {
- Plugin p = pw.getUpdateInfo();
- if(p!=null) r.add(p);
+ for (UpdateSite s : sites) {
+ plugins.addAll(s.getAvailables());
}
- return r;
+ return plugins;
}
- /**
- * Does any of the plugin has updates?
- */
- public boolean hasUpdates() {
- Data data = getData();
- if(data==null) return false;
-
- for (PluginWrapper pw : Hudson.getInstance().getPluginManager().getPlugins()) {
- if(!pw.isBundled() && pw.getUpdateInfo()!=null)
- // do not advertize updates to bundled plugins, since we generally want users to get them
- // as a part of hudson.war updates. This also avoids unnecessary pinning of plugins.
- return true;
- }
- return false;
- }
+ public List getUpdates() {
+ List plugins = new ArrayList();
- public String getDisplayName() {
- return "Update center";
- }
+ for (UpdateSite s : sites) {
+ plugins.addAll(s.getUpdates());
+ }
- public String getSearchUrl() {
- return "updateCenter";
+ return plugins;
}
- /**
- * Exposed to get rid of hardcoding of the URL that serves up update-center.json
- * in Javascript.
- */
- public String getUrl() {
- return config.getUpdateCenterUrl();
- }
/**
* {@link AdministrativeMonitor} that checks if there's Hudson update.
@@ -416,213 +323,39 @@ public class UpdateCenter extends AbstractModelObject {
}
public Data getData() {
- return Hudson.getInstance().getUpdateCenter().getData();
+ UpdateSite cs = Hudson.getInstance().getUpdateCenter().getCoreSource();
+ if (cs!=null) return cs.getData();
+ return null;
}
}
+
/**
- * In-memory representation of the update center data.
+ * Strategy object for controlling the update center's behaviors.
+ *
+ *
+ * Until 1.MULTIUPDATE, this extension point used to control the configuration of
+ * where to get updates (hence the name of this class), but with the introduction
+ * of multiple update center sites capability, that functionality is achieved by
+ * simply installing another {@link UpdateSite}.
+ *
+ *
+ * See {@link UpdateSite} for how to manipulate them programmatically.
+ *
+ * @since 1.266
*/
- public final class Data {
- /**
- * The latest hudson.war.
- */
- public final Entry core;
- /**
- * Plugins in the official repository, keyed by their artifact IDs.
- */
- public final Map plugins = new TreeMap(String.CASE_INSENSITIVE_ORDER);
-
- Data(JSONObject o) {
- core = new Entry(o.getJSONObject("core"));
- for(Map.Entry e : (Set>)o.getJSONObject("plugins").entrySet()) {
- plugins.put(e.getKey(),new Plugin(e.getValue()));
- }
- }
-
- /**
- * Is there a new version of the core?
- */
- public boolean hasCoreUpdates() {
- return core.isNewerThan(Hudson.VERSION);
- }
-
- /**
- * Do we support upgrade?
- */
- public boolean canUpgrade() {
- return Lifecycle.get().canRewriteHudsonWar();
- }
- }
-
- public static class Entry {
- /**
- * Artifact ID.
- */
- public final String name;
- /**
- * The version.
- */
- public final String version;
- /**
- * Download URL.
- */
- public final String url;
-
- public Entry(JSONObject o) {
- this.name = o.getString("name");
- this.version = o.getString("version");
- this.url = o.getString("url");
- }
-
- /**
- * Checks if the specified "current version" is older than the version of this entry.
- *
- * @param currentVersion
- * The string that represents the version number to be compared.
- * @return
- * true if the version listed in this entry is newer.
- * false otherwise, including the situation where the strings couldn't be parsed as version numbers.
- */
- public boolean isNewerThan(String currentVersion) {
- return isNewerThan(currentVersion, version);
- }
-
- /**
- * Compares two versions - returns true if the first version is newer than the second.
- *
- * @param firstVersion
- * The first version to test against.
- * @param secondVersion
- * The second version to test against.
- * @return
- * True if the first version is newer than the second version. False in all other cases.
- */
- public boolean isNewerThan(String firstVersion, String secondVersion) {
- try {
- return new VersionNumber(firstVersion).compareTo(new VersionNumber(secondVersion)) < 0;
- } catch (IllegalArgumentException e) {
- // couldn't parse as the version number.
- return false;
- }
- }
- }
-
- public final class Plugin extends Entry {
- /**
- * Optional URL to the Wiki page that discusses this plugin.
- */
- public final String wiki;
- /**
- * Human readable title of the plugin, taken from Wiki page.
- * Can be null.
- *
- *
- * beware of XSS vulnerability since this data comes from Wiki
- */
- public final String title;
- /**
- * Optional excerpt string.
- */
- public final String excerpt;
- /**
- * Optional version # from which this plugin release is configuration-compatible.
- */
- public final String compatibleSinceVersion;
-
- @DataBoundConstructor
- public Plugin(JSONObject o) {
- super(o);
- this.wiki = get(o,"wiki");
- this.title = get(o,"title");
- this.excerpt = get(o,"excerpt");
- this.compatibleSinceVersion = get(o,"compatibleSinceVersion");
- }
-
- private String get(JSONObject o, String prop) {
- if(o.has(prop))
- return o.getString(prop);
- else
- return null;
- }
-
- public String getDisplayName() {
- if(title!=null) return title;
- return name;
- }
-
- /**
- * If some version of this plugin is currently installed, return {@link PluginWrapper}.
- * Otherwise null.
- */
- public PluginWrapper getInstalled() {
- PluginManager pm = Hudson.getInstance().getPluginManager();
- return pm.getPlugin(name);
- }
-
- /**
- * If the plugin is already installed, and the new version of the plugin has a "compatibleSinceVersion"
- * value (i.e., it's only directly compatible with that version or later), this will check to
- * see if the installed version is older than the compatible-since version. If it is older, it'll return false.
- * If it's not older, or it's not installed, or it's installed but there's no compatibleSinceVersion
- * specified, it'll return true.
- */
- public boolean isCompatibleWithInstalledVersion() {
- PluginWrapper installedVersion = getInstalled();
- if (installedVersion != null) {
- if (compatibleSinceVersion != null) {
- if (new VersionNumber(installedVersion.getVersion())
- .isOlderThan(new VersionNumber(compatibleSinceVersion))) {
- return false;
- }
- }
- }
- return true;
- }
-
- /**
- * @deprecated as of 1.326
- * Use {@link #deploy()}.
- */
- public void install() {
- deploy();
- }
-
- /**
- * Schedules the installation of this plugin.
- *
- *
- * This is mainly intended to be called from the UI. The actual installation work happens
- * asynchronously in another thread.
- */
- public Future deploy() {
- Hudson.getInstance().checkPermission(Hudson.ADMINISTER);
- return addJob(new InstallationJob(this, Hudson.getAuthentication()));
- }
-
+ @SuppressWarnings({"UnusedDeclaration"})
+ public static class UpdateCenterConfiguration implements ExtensionPoint {
/**
- * Making the installation web bound.
+ * Creates default update center configuration - uses settings for global update center.
*/
- public void doInstall(StaplerResponse rsp) throws IOException {
- install();
- rsp.sendRedirect2("../..");
+ public UpdateCenterConfiguration() {
}
- }
- /**
- * Configuration data for controlling the update center's behaviors. The update
- * center's defaults will check internet connectivity by trying to connect
- * to www.google.com; will download plugins, the plugin catalog and updates
- * from hudson-ci.org; and will install plugins with file system
- * operations.
- *
- * @since 1.266
- */
- public static class UpdateCenterConfiguration implements ExtensionPoint {
/**
* Check network connectivity by trying to establish a connection to
* the host in connectionCheckUrl.
- *
+ *
* @param job The connection checker that is invoking this strategy.
* @param connectionCheckUrl A string containing the URL of a domain
* that is assumed to be always available.
@@ -631,10 +364,10 @@ public class UpdateCenter extends AbstractModelObject {
public void checkConnection(ConnectionCheckJob job, String connectionCheckUrl) throws IOException {
testConnection(new URL(connectionCheckUrl));
}
-
+
/**
* Check connection to update center server.
- *
+ *
* @param job The connection checker that is invoking this strategy.
* @param updateCenterUrl A sting containing the URL of the update center host.
* @throws IOException if a connection to the update center server can't be established.
@@ -642,29 +375,22 @@ public class UpdateCenter extends AbstractModelObject {
public void checkUpdateCenter(ConnectionCheckJob job, String updateCenterUrl) throws IOException {
testConnection(new URL(updateCenterUrl + "?uctest"));
}
-
+
/**
- * Validate the URL of the resource before downloading it. The default
- * implementation enforces that the base of the resource URL starts
- * with the string returned by {@link #getPluginRepositoryBaseUrl()}.
- *
+ * Validate the URL of the resource before downloading it.
+ *
* @param job The download job that is invoking this strategy. This job is
* responsible for managing the status of the download and installation.
* @param src The location of the resource on the network
* @throws IOException if the validation fails
*/
public void preValidate(DownloadJob job, URL src) throws IOException {
- // In the future if we are to open up update center to 3rd party, we need more elaborate scheme
- // like signing to ensure the safety of the bits.
- if(!src.toExternalForm().startsWith(getPluginRepositoryBaseUrl())) {
- throw new IOException("Installation of plugin from "+src+" is not allowed");
- }
}
-
+
/**
* Validate the resource after it has been downloaded, before it is
* installed. The default implementation does nothing.
- *
+ *
* @param job The download job that is invoking this strategy. This job is
* responsible for managing the status of the download and installation.
* @param src The location of the downloaded resource.
@@ -672,13 +398,13 @@ public class UpdateCenter extends AbstractModelObject {
*/
public void postValidate(DownloadJob job, File src) throws IOException {
}
-
+
/**
* Download a plugin or core upgrade in preparation for installing it
* into its final location. Implementations will normally download the
* resource into a temporary location and hand off a reference to this
* location to the install or upgrade strategy to move into the final location.
- *
+ *
* @param job The download job that is invoking this strategy. This job is
* responsible for managing the status of the download and installation.
* @param src The URL to the resource to be downloaded.
@@ -716,14 +442,14 @@ public class UpdateCenter extends AbstractModelObject {
// indicates that this kind of inconsistency can happen. So let's be defensive
throw new IOException("Inconsistent file length: expected "+total+" but only got "+tmp.length());
}
-
+
return tmp;
}
-
+
/**
* Called after a plugin has been downloaded to move it into its final
* location. The default implementation is a file rename.
- *
+ *
* @param job The install job that is invoking this strategy.
* @param src The temporary location of the plugin.
* @param dst The final destination to install the plugin to.
@@ -732,11 +458,11 @@ public class UpdateCenter extends AbstractModelObject {
public void install(DownloadJob job, File src, File dst) throws IOException {
job.replace(dst, src);
}
-
+
/**
* Called after an upgrade has been downloaded to move it into its final
* location. The default implementation is a file rename.
- *
+ *
* @param job The upgrade job that is invoking this strategy.
* @param src The temporary location of the upgrade.
* @param dst The final destination to install the upgrade to.
@@ -744,34 +470,46 @@ public class UpdateCenter extends AbstractModelObject {
*/
public void upgrade(DownloadJob job, File src, File dst) throws IOException {
job.replace(dst, src);
- }
+ }
/**
- * Returns an "always up" server for Internet connectivity testing
+ * Returns an "always up" server for Internet connectivity testing.
+ *
+ * @deprecated as of 1.MULTIUPDATE
+ * With the introduction of multiple update center capability, this information
+ * is now a part of the update-center.json file. See
+ * http://hudson-ci.org/update-center.json as an example.
*/
public String getConnectionCheckUrl() {
return "http://www.google.com";
}
-
+
/**
* Returns the URL of the server that hosts the update-center.json
* file.
*
+ * @deprecated as of 1.MULTIUPDATE
+ * With the introduction of multiple update center capability, this information
+ * is now moved to {@link UpdateSite}.
* @return
* Absolute URL that ends with '/'.
*/
public String getUpdateCenterUrl() {
return "http://hudson-ci.org/";
}
-
+
/**
* Returns the URL of the server that hosts plugins and core updates.
+ *
+ * @deprecated as of 1.MULTIUPDATE
+ * update-center.json is now signed, so we don't have to further make sure that
+ * we aren't downloading from anywhere unsecure.
*/
public String getPluginRepositoryBaseUrl() {
return "http://hudson-ci.org/";
}
-
+
private void testConnection(URL url) throws IOException {
try {
InputStream in = ProxyConfiguration.open(url).getInputStream();
@@ -782,15 +520,24 @@ public class UpdateCenter extends AbstractModelObject {
// fix up this crappy error message from JDK
throw new IOException2("Failed to validate the SSL certificate of "+url,e);
}
- }
+ }
}
-
+
/**
* Things that {@link UpdateCenter#installerService} executes.
*
* This object will have the row.jelly which renders the job on UI.
*/
public abstract class UpdateCenterJob implements Runnable {
+ /**
+ * Which {@link UpdateSite} does this belong to?
+ */
+ public final UpdateSite site;
+
+ protected UpdateCenterJob(UpdateSite site) {
+ this.site = site;
+ }
+
/**
* @deprecated as of 1.326
* Use {@link #submit()} instead.
@@ -817,24 +564,29 @@ public class UpdateCenter extends AbstractModelObject {
public final class ConnectionCheckJob extends UpdateCenterJob {
private final Vector statuses= new Vector();
+ public ConnectionCheckJob(UpdateSite site) {
+ super(site);
+ }
+
public void run() {
LOGGER.fine("Doing a connectivity check");
try {
- String connectionCheckUrl = config.getConnectionCheckUrl();
-
- statuses.add(Messages.UpdateCenter_Status_CheckingInternet());
- try {
- config.checkConnection(this, connectionCheckUrl);
- } catch (IOException e) {
- if(e.getMessage().contains("Connection timed out")) {
- // Google can't be down, so this is probably a proxy issue
- statuses.add(Messages.UpdateCenter_Status_ConnectionFailed(connectionCheckUrl));
- return;
+ String connectionCheckUrl = site.getConnectionCheckUrl();
+ if (connectionCheckUrl!=null) {
+ statuses.add(Messages.UpdateCenter_Status_CheckingInternet());
+ try {
+ config.checkConnection(this, connectionCheckUrl);
+ } catch (IOException e) {
+ if(e.getMessage().contains("Connection timed out")) {
+ // Google can't be down, so this is probably a proxy issue
+ statuses.add(Messages.UpdateCenter_Status_ConnectionFailed(connectionCheckUrl));
+ return;
+ }
}
}
statuses.add(Messages.UpdateCenter_Status_CheckingJavaNet());
- config.checkUpdateCenter(this, config.getUpdateCenterUrl());
+ config.checkUpdateCenter(this, site.getUrl());
statuses.add(Messages.UpdateCenter_Status_Success());
} catch (UnknownHostException e) {
@@ -886,9 +638,9 @@ public class UpdateCenter extends AbstractModelObject {
*/
protected abstract void onSuccess();
-
+
private Authentication authentication;
-
+
/**
* Get the user that initiated this job
*/
@@ -896,18 +648,18 @@ public class UpdateCenter extends AbstractModelObject {
{
return this.authentication;
}
-
- protected DownloadJob(Authentication authentication)
- {
+
+ protected DownloadJob(UpdateSite site, Authentication authentication) {
+ super(site);
this.authentication = authentication;
}
-
+
public void run() {
try {
LOGGER.info("Starting the installation of "+getName()+" on behalf of "+getUser().getName());
_run();
-
+
LOGGER.info("Installation successful: "+getName());
status = new Success();
onSuccess();
@@ -1005,8 +757,8 @@ public class UpdateCenter extends AbstractModelObject {
private final PluginManager pm = Hudson.getInstance().getPluginManager();
- public InstallationJob(Plugin plugin, Authentication auth) {
- super(auth);
+ public InstallationJob(Plugin plugin, UpdateSite site, Authentication auth) {
+ super(site, auth);
this.plugin = plugin;
}
@@ -1047,12 +799,12 @@ public class UpdateCenter extends AbstractModelObject {
* Represents the state of the upgrade activity of Hudson core.
*/
public final class HudsonUpgradeJob extends DownloadJob {
- public HudsonUpgradeJob(Authentication auth) {
- super(auth);
+ public HudsonUpgradeJob(UpdateSite site, Authentication auth) {
+ super(site, auth);
}
protected URL getURL() throws MalformedURLException {
- return new URL(getData().core.url);
+ return new URL(site.getData().core.url);
}
protected File getDestination() {
@@ -1088,14 +840,13 @@ public class UpdateCenter extends AbstractModelObject {
*/
private static final AtomicInteger iota = new AtomicInteger();
- private static final long DAY = DAYS.toMillis(1);
-
private static final Logger LOGGER = Logger.getLogger(UpdateCenter.class.getName());
public static boolean neverUpdate = Boolean.getBoolean(UpdateCenter.class.getName()+".never");
- /**
- * Off by default until we know this is reasonably working.
- */
- public static boolean signatureCheck = Boolean.getBoolean(UpdateCenter.class.getName()+".signatureCheck");
+ public static final XStream2 XSTREAM = new XStream2();
+ static {
+ XSTREAM.alias("site",UpdateSite.class);
+ XSTREAM.alias("sites",PersistedList.class);
+ }
}
diff --git a/core/src/main/java/hudson/model/UpdateSite.java b/core/src/main/java/hudson/model/UpdateSite.java
new file mode 100644
index 0000000000000000000000000000000000000000..e6d31442d670cca58f6d63e2cf839994a7752338
--- /dev/null
+++ b/core/src/main/java/hudson/model/UpdateSite.java
@@ -0,0 +1,558 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Yahoo! Inc., Seiji Sogabe,
+ * Andrew Bayer
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package hudson.model;
+
+import hudson.PluginWrapper;
+import hudson.PluginManager;
+import hudson.model.UpdateCenter.UpdateCenterJob;
+import hudson.lifecycle.Lifecycle;
+import hudson.util.TextFile;
+import hudson.util.VersionNumber;
+import static hudson.util.TimeUnit2.DAYS;
+import net.sf.json.JSONObject;
+import org.kohsuke.stapler.QueryParameter;
+import org.kohsuke.stapler.DataBoundConstructor;
+import org.kohsuke.stapler.StaplerResponse;
+import org.jvnet.hudson.crypto.CertificateUtil;
+import org.jvnet.hudson.crypto.SignatureOutputStream;
+import org.apache.commons.io.output.NullOutputStream;
+import org.apache.commons.io.output.TeeOutputStream;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.ByteArrayInputStream;
+import java.io.OutputStreamWriter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.Set;
+import java.util.concurrent.Future;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.security.GeneralSecurityException;
+import java.security.MessageDigest;
+import java.security.DigestOutputStream;
+import java.security.Signature;
+import java.security.cert.X509Certificate;
+import java.security.cert.CertificateFactory;
+import java.security.cert.TrustAnchor;
+
+import com.trilead.ssh2.crypto.Base64;
+
+import javax.servlet.ServletContext;
+
+
+/**
+ * Source of the update center information, like "http://hudson-ci.org/update-center.json"
+ *
+ *
+ * Hudson can have multiple {@link UpdateSite}s registered in the system, so that it can pick up plugins
+ * from different locations.
+ *
+ * @author Andrew Bayer
+ * @author Kohsuke Kawaguchi
+ * @since 1.MULTIUPDATE
+ */
+public class UpdateSite {
+ /**
+ * What's the time stamp of data file?
+ */
+ private transient long dataTimestamp = -1;
+
+ /**
+ * When was the last time we asked a browser to check the data for us?
+ *
+ *
+ * There's normally some delay between when we send HTML that includes the check code,
+ * until we get the data back, so this variable is used to avoid asking too many browseres
+ * all at once.
+ */
+ private transient volatile long lastAttempt = -1;
+
+ /**
+ * ID string for this update source.
+ */
+ private final String id;
+
+ /**
+ * Path to update-center.json, like http://hudson-ci.org/update-center.json.
+ */
+ private final String url;
+
+ public UpdateSite(String id, String url) {
+ this.id = id;
+ this.url = url;
+ }
+
+ /**
+ * When read back from XML, initialize them back to -1.
+ */
+ private Object readResolve() {
+ dataTimestamp = lastAttempt = -1;
+ return this;
+ }
+
+ /**
+ * Get ID string.
+ */
+ public String getId() {
+ return id;
+ }
+
+ public long getDataTimestamp() {
+ return dataTimestamp;
+ }
+
+ /**
+ * This is the endpoint that receives the update center data file from the browser.
+ */
+ public void doPostBack(@QueryParameter String json) throws IOException, GeneralSecurityException {
+ dataTimestamp = System.currentTimeMillis();
+ JSONObject o = JSONObject.fromObject(json);
+
+ int v = o.getInt("updateCenterVersion");
+ if(v !=1) {
+ LOGGER.warning("Unrecognized update center version: "+v);
+ return;
+ }
+
+ if (signatureCheck)
+ verifySignature(o);
+
+ LOGGER.info("Obtained the latest update center data file for UpdateSource "+ id);
+ getDataFile().write(json);
+ }
+
+ /**
+ * Verifies the signature in the update center data file.
+ */
+ private boolean verifySignature(JSONObject o) throws GeneralSecurityException, IOException {
+ JSONObject signature = o.getJSONObject("signature");
+ if (signature.isNullObject()) {
+ LOGGER.severe("No signature block found");
+ return false;
+ }
+ o.remove("signature");
+
+ List certs = new ArrayList();
+ {// load and verify certificates
+ CertificateFactory cf = CertificateFactory.getInstance("X509");
+ for (Object cert : o.getJSONArray("certificates")) {
+ X509Certificate c = (X509Certificate) cf.generateCertificate(new ByteArrayInputStream(Base64.decode(cert.toString().toCharArray())));
+ c.checkValidity();
+ certs.add(c);
+ }
+
+ // all default root CAs in JVM are trusted, plus certs bundled in Hudson
+ Set anchors = CertificateUtil.getDefaultRootCAs();
+ ServletContext context = Hudson.getInstance().servletContext;
+ for (String cert : (Set) context.getResourcePaths("/WEB-INF/update-center-rootCAs")) {
+ if (cert.endsWith(".txt")) continue; // skip text files that are meant to be documentation
+ anchors.add(new TrustAnchor((X509Certificate)cf.generateCertificate(context.getResourceAsStream(cert)),null));
+ }
+ CertificateUtil.validatePath(certs);
+ }
+
+ // this is for computing a digest to check sanity
+ MessageDigest sha1 = MessageDigest.getInstance("SHA1");
+ DigestOutputStream dos = new DigestOutputStream(new NullOutputStream(),sha1);
+
+ // this is for computing a signature
+ Signature sig = Signature.getInstance("SHA1withRSA");
+ sig.initVerify(certs.get(0));
+ SignatureOutputStream sos = new SignatureOutputStream(sig);
+
+ o.writeCanonical(new OutputStreamWriter(new TeeOutputStream(dos,sos),"UTF-8"));
+
+ // did the digest match? this is not a part of the signature validation, but if we have a bug in the c14n
+ // (which is more likely than someone tampering with update center), we can tell
+ String computedDigest = new String(Base64.encode(sha1.digest()));
+ String providedDigest = signature.getString("digest");
+ if (!computedDigest.equalsIgnoreCase(providedDigest)) {
+ LOGGER.severe("Digest mismatch: "+computedDigest+" vs "+providedDigest);
+ return false;
+ }
+
+ if (!sig.verify(Base64.decode(signature.getString("signature").toCharArray()))) {
+ LOGGER.severe("Signature in the update center doesn't match with the certificate");
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns true if it's time for us to check for new version.
+ */
+ public boolean isDue() {
+ if(neverUpdate) return false;
+ if(dataTimestamp==-1)
+ dataTimestamp = getDataFile().file.lastModified();
+ long now = System.currentTimeMillis();
+ boolean due = now - dataTimestamp > DAY && now - lastAttempt > 15000;
+ if(due) lastAttempt = now;
+ return due;
+ }
+
+ /**
+ * Loads the update center data, if any.
+ *
+ * @return null if no data is available.
+ */
+ public Data getData() {
+ TextFile df = getDataFile();
+ if(df.exists()) {
+ try {
+ return new Data(JSONObject.fromObject(df.read()));
+ } catch (IOException e) {
+ LOGGER.log(Level.SEVERE,"Failed to parse "+df,e);
+ df.delete(); // if we keep this file, it will cause repeated failures
+ return null;
+ }
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Returns a list of plugins that should be shown in the "available" tab.
+ * These are "all plugins - installed plugins".
+ */
+ public List getAvailables() {
+ List r = new ArrayList();
+ Data data = getData();
+ if(data ==null) return Collections.emptyList();
+ for (Plugin p : data.plugins.values()) {
+ if(p.getInstalled()==null)
+ r.add(p);
+ }
+ return r;
+ }
+
+ /**
+ * Gets the information about a specific plugin.
+ *
+ * @param artifactId
+ * The short name of the plugin. Corresponds to {@link PluginWrapper#getShortName()}.
+ *
+ * @return
+ * null if no such information is found.
+ */
+ public Plugin getPlugin(String artifactId) {
+ Data dt = getData();
+ if(dt==null) return null;
+ return dt.plugins.get(artifactId);
+ }
+
+ /**
+ * Returns an "always up" server for Internet connectivity testing, or null if we are going to skip the test.
+ */
+ public String getConnectionCheckUrl() {
+ Data dt = getData();
+ if(dt==null) return "http://www.google.com/";
+ return dt.connectionCheckUrl;
+ }
+
+ /**
+ * This is where we store the update center data.
+ */
+ private TextFile getDataFile() {
+ return new TextFile(new File(Hudson.getInstance().getRootDir(),
+ "updates/" + getId()+".json"));
+ }
+
+ /**
+ * Returns the list of plugins that are updates to currently installed ones.
+ *
+ * @return
+ * can be empty but never null.
+ */
+ public List getUpdates() {
+ Data data = getData();
+ if(data==null) return Collections.emptyList(); // fail to determine
+
+ List r = new ArrayList();
+ for (PluginWrapper pw : Hudson.getInstance().getPluginManager().getPlugins()) {
+ Plugin p = pw.getUpdateInfo();
+ if(p!=null) r.add(p);
+ }
+
+ return r;
+ }
+
+ /**
+ * Does any of the plugin has updates?
+ */
+ public boolean hasUpdates() {
+ Data data = getData();
+ if(data==null) return false;
+
+ for (PluginWrapper pw : Hudson.getInstance().getPluginManager().getPlugins()) {
+ if(!pw.isBundled() && pw.getUpdateInfo()!=null)
+ // do not advertize updates to bundled plugins, since we generally want users to get them
+ // as a part of hudson.war updates. This also avoids unnecessary pinning of plugins.
+ return true;
+ }
+ return false;
+ }
+
+
+ /**
+ * Exposed to get rid of hardcoding of the URL that serves up update-center.json
+ * in Javascript.
+ */
+ public String getUrl() {
+ return url;
+ }
+
+ /**
+ * In-memory representation of the update center data.
+ */
+ public final class Data {
+ /**
+ * The {@link UpdateSite} ID.
+ */
+ public final String sourceId;
+
+ /**
+ * The latest hudson.war.
+ */
+ public final Entry core;
+ /**
+ * Plugins in the repository, keyed by their artifact IDs.
+ */
+ public final Map plugins = new TreeMap(String.CASE_INSENSITIVE_ORDER);
+
+ /**
+ * If this is non-null, Hudson is going to check the connectivity to this URL to make sure
+ * the network connection is up. Null to skip the check.
+ */
+ public final String connectionCheckUrl;
+
+ Data(JSONObject o) {
+ this.sourceId = (String)o.get("id");
+ if (sourceId.equals("default")) {
+ core = new Entry(sourceId, o.getJSONObject("core"));
+ }
+ else {
+ core = null;
+ }
+ for(Map.Entry e : (Set>)o.getJSONObject("plugins").entrySet()) {
+ plugins.put(e.getKey(),new Plugin(sourceId, e.getValue()));
+ }
+
+ connectionCheckUrl = (String)o.get("connectionCheckUrl");
+ }
+
+ /**
+ * Is there a new version of the core?
+ */
+ public boolean hasCoreUpdates() {
+ return core != null && core.isNewerThan(Hudson.VERSION);
+ }
+
+ /**
+ * Do we support upgrade?
+ */
+ public boolean canUpgrade() {
+ return Lifecycle.get().canRewriteHudsonWar();
+ }
+ }
+
+ public static class Entry {
+ /**
+ * {@link UpdateSite} ID.
+ */
+ public final String sourceId;
+
+ /**
+ * Artifact ID.
+ */
+ public final String name;
+ /**
+ * The version.
+ */
+ public final String version;
+ /**
+ * Download URL.
+ */
+ public final String url;
+
+ public Entry(String sourceId, JSONObject o) {
+ this.sourceId = sourceId;
+ this.name = o.getString("name");
+ this.version = o.getString("version");
+ this.url = o.getString("url");
+ }
+
+ /**
+ * Checks if the specified "current version" is older than the version of this entry.
+ *
+ * @param currentVersion
+ * The string that represents the version number to be compared.
+ * @return
+ * true if the version listed in this entry is newer.
+ * false otherwise, including the situation where the strings couldn't be parsed as version numbers.
+ */
+ public boolean isNewerThan(String currentVersion) {
+ return isNewerThan(currentVersion, version);
+ }
+
+ /**
+ * Compares two versions - returns true if the first version is newer than the second.
+ *
+ * @param firstVersion
+ * The first version to test against.
+ * @param secondVersion
+ * The second version to test against.
+ * @return
+ * True if the first version is newer than the second version. False in all other cases.
+ */
+ private static boolean isNewerThan(String firstVersion, String secondVersion) {
+ try {
+ return new VersionNumber(firstVersion).compareTo(new VersionNumber(secondVersion)) < 0;
+ } catch (IllegalArgumentException e) {
+ // couldn't parse as the version number.
+ return false;
+ }
+ }
+ }
+
+ public final class Plugin extends Entry {
+ /**
+ * Optional URL to the Wiki page that discusses this plugin.
+ */
+ public final String wiki;
+ /**
+ * Human readable title of the plugin, taken from Wiki page.
+ * Can be null.
+ *
+ *
+ * beware of XSS vulnerability since this data comes from Wiki
+ */
+ public final String title;
+ /**
+ * Optional excerpt string.
+ */
+ public final String excerpt;
+ /**
+ * Optional version # from which this plugin release is configuration-compatible.
+ */
+ public final String compatibleSinceVersion;
+
+ @DataBoundConstructor
+ public Plugin(String sourceId, JSONObject o) {
+ super(sourceId, o);
+ this.wiki = get(o,"wiki");
+ this.title = get(o,"title");
+ this.excerpt = get(o,"excerpt");
+ this.compatibleSinceVersion = get(o,"compatibleSinceVersion");
+ }
+
+ private String get(JSONObject o, String prop) {
+ if(o.has(prop))
+ return o.getString(prop);
+ else
+ return null;
+ }
+
+ public String getDisplayName() {
+ if(title!=null) return title;
+ return name;
+ }
+
+ /**
+ * If some version of this plugin is currently installed, return {@link PluginWrapper}.
+ * Otherwise null.
+ */
+ public PluginWrapper getInstalled() {
+ PluginManager pm = Hudson.getInstance().getPluginManager();
+ return pm.getPlugin(name);
+ }
+
+ /**
+ * If the plugin is already installed, and the new version of the plugin has a "compatibleSinceVersion"
+ * value (i.e., it's only directly compatible with that version or later), this will check to
+ * see if the installed version is older than the compatible-since version. If it is older, it'll return false.
+ * If it's not older, or it's not installed, or it's installed but there's no compatibleSinceVersion
+ * specified, it'll return true.
+ */
+ public boolean isCompatibleWithInstalledVersion() {
+ PluginWrapper installedVersion = getInstalled();
+ if (installedVersion != null) {
+ if (compatibleSinceVersion != null) {
+ if (new VersionNumber(installedVersion.getVersion())
+ .isOlderThan(new VersionNumber(compatibleSinceVersion))) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * @deprecated as of 1.326
+ * Use {@link #deploy()}.
+ */
+ public void install() {
+ deploy();
+ }
+
+ /**
+ * Schedules the installation of this plugin.
+ *
+ *
+ * This is mainly intended to be called from the UI. The actual installation work happens
+ * asynchronously in another thread.
+ */
+ public Future deploy() {
+ Hudson.getInstance().checkPermission(Hudson.ADMINISTER);
+ UpdateCenter uc = Hudson.getInstance().getUpdateCenter();
+ return uc.addJob(uc.new InstallationJob(this, UpdateSite.this, Hudson.getAuthentication()));
+ }
+
+ /**
+ * Making the installation web bound.
+ */
+ public void doInstall(StaplerResponse rsp) throws IOException {
+ deploy();
+ rsp.sendRedirect2("../..");
+ }
+ }
+
+ private static final long DAY = DAYS.toMillis(1);
+
+ private static final Logger LOGGER = Logger.getLogger(UpdateSite.class.getName());
+
+ public static boolean neverUpdate = Boolean.getBoolean(UpdateCenter.class.getName()+".never");
+
+ /**
+ * Off by default until we know this is reasonably working.
+ */
+ public static boolean signatureCheck = Boolean.getBoolean(UpdateCenter.class.getName()+".signatureCheck");
+}
diff --git a/core/src/main/java/hudson/util/DescribableList.java b/core/src/main/java/hudson/util/DescribableList.java
index df0988024fe680990617a4159b7003dc60f2a227..2597e0369ac3507be6caa2244078c96e84dc7d43 100644
--- a/core/src/main/java/hudson/util/DescribableList.java
+++ b/core/src/main/java/hudson/util/DescribableList.java
@@ -61,10 +61,7 @@ import java.util.Map;
*
* @author Kohsuke Kawaguchi
*/
-public class DescribableList, D extends Descriptor> implements Iterable {
- private final CopyOnWriteList data = new CopyOnWriteList();
- private Saveable owner;
-
+public class DescribableList, D extends Descriptor> extends PersistedList {
protected DescribableList() {
}
@@ -88,20 +85,6 @@ public class DescribableList, D extends Descriptor>
this.owner = owner;
}
- public void setOwner(Saveable owner) {
- this.owner = owner;
- }
-
- public void add(T item) throws IOException {
- data.add(item);
- onModified();
- }
-
- public void addAll(Collection extends T> items) throws IOException {
- data.addAll(items);
- onModified();
- }
-
/**
* Removes all instances of the same type, then add the new one.
*/
@@ -111,11 +94,6 @@ public class DescribableList, D extends Descriptor>
onModified();
}
- public void replaceBy(Collection extends T> col) throws IOException {
- data.replaceBy(col);
- onModified();
- }
-
public T get(D descriptor) {
for (T t : data)
if(t.getDescriptor()==descriptor)
@@ -123,57 +101,10 @@ public class DescribableList, D extends Descriptor>
return null;
}
- public U get(Class type) {
- for (T t : data)
- if(type.isInstance(t))
- return type.cast(t);
- return null;
- }
-
- /**
- * Gets all instances that matches the given type.
- */
- public List getAll(Class type) {
- List r = new ArrayList();
- for (T t : data)
- if(type.isInstance(t))
- r.add(type.cast(t));
- return r;
- }
-
public boolean contains(D d) {
return get(d)!=null;
}
- public int size() {
- return data.size();
- }
-
- /**
- * Removes an instance by its type.
- */
- public void remove(Class extends T> type) throws IOException {
- for (T t : data) {
- if(t.getClass()==type) {
- data.remove(t);
- onModified();
- return;
- }
- }
- }
-
- public void removeAll(Class extends T> type) throws IOException {
- boolean modified=false;
- for (T t : data) {
- if(t.getClass()==type) {
- data.remove(t);
- modified=true;
- }
- }
- if(modified)
- onModified();
- }
-
public void remove(D descriptor) throws IOException {
for (T t : data) {
if(t.getDescriptor()==descriptor) {
@@ -184,44 +115,11 @@ public class DescribableList, D extends Descriptor>
}
}
- public void clear() {
- data.clear();
- }
-
- public Iterator iterator() {
- return data.iterator();
- }
-
- /**
- * Called when a list is mutated.
- */
- protected void onModified() throws IOException {
- owner.save();
- }
-
@SuppressWarnings("unchecked")
public Map toMap() {
return (Map)Descriptor.toMap(data);
}
- /**
- * Returns the snapshot view of instances as list.
- */
- public List toList() {
- return data.getView();
- }
-
- /**
- * Gets all the {@link Describable}s in an array.
- */
- public T[] toArray(T[] array) {
- return data.toArray(array);
- }
-
- public void addAllTo(Collection super T> dst) {
- data.addAllTo(dst);
- }
-
/**
* Rebuilds the list by creating a fresh instances from the submitted form.
*
diff --git a/core/src/main/java/hudson/util/PersistedList.java b/core/src/main/java/hudson/util/PersistedList.java
new file mode 100644
index 0000000000000000000000000000000000000000..f478602dcdfd911b598d7c7c69bb17ecd4bb70e6
--- /dev/null
+++ b/core/src/main/java/hudson/util/PersistedList.java
@@ -0,0 +1,211 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2004-2009, Sun Microsystems, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+package hudson.util;
+
+import com.thoughtworks.xstream.converters.Converter;
+import com.thoughtworks.xstream.converters.MarshallingContext;
+import com.thoughtworks.xstream.converters.UnmarshallingContext;
+import com.thoughtworks.xstream.converters.collections.AbstractCollectionConverter;
+import com.thoughtworks.xstream.io.HierarchicalStreamReader;
+import com.thoughtworks.xstream.io.HierarchicalStreamWriter;
+import com.thoughtworks.xstream.mapper.Mapper;
+import hudson.model.Describable;
+import hudson.model.Saveable;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Collection whose change is notified to the parent object for persistence.
+ *
+ * @author Kohsuke Kawaguchi
+ * @since 1.MULTISOURCE
+ */
+public class PersistedList implements Iterable {
+ protected final CopyOnWriteList data = new CopyOnWriteList();
+ protected Saveable owner;
+
+ protected PersistedList() {
+ }
+
+ public PersistedList(Saveable owner) {
+ setOwner(owner);
+ }
+
+ public void setOwner(Saveable owner) {
+ this.owner = owner;
+ }
+
+ public void add(T item) throws IOException {
+ data.add(item);
+ onModified();
+ }
+
+ public void addAll(Collection extends T> items) throws IOException {
+ data.addAll(items);
+ onModified();
+ }
+
+ public void replaceBy(Collection extends T> col) throws IOException {
+ data.replaceBy(col);
+ onModified();
+ }
+
+ public U get(Class type) {
+ for (T t : data)
+ if(type.isInstance(t))
+ return type.cast(t);
+ return null;
+ }
+
+ /**
+ * Gets all instances that matches the given type.
+ */
+ public List getAll(Class type) {
+ List r = new ArrayList();
+ for (T t : data)
+ if(type.isInstance(t))
+ r.add(type.cast(t));
+ return r;
+ }
+
+ public int size() {
+ return data.size();
+ }
+
+ /**
+ * Removes an instance by its type.
+ */
+ public void remove(Class extends T> type) throws IOException {
+ for (T t : data) {
+ if(t.getClass()==type) {
+ data.remove(t);
+ onModified();
+ return;
+ }
+ }
+ }
+
+ public boolean remove(T o) throws IOException {
+ boolean b = data.remove(o);
+ if (b) onModified();
+ return b;
+ }
+
+ public void removeAll(Class extends T> type) throws IOException {
+ boolean modified=false;
+ for (T t : data) {
+ if(t.getClass()==type) {
+ data.remove(t);
+ modified=true;
+ }
+ }
+ if(modified)
+ onModified();
+ }
+
+
+ public void clear() {
+ data.clear();
+ }
+
+ public Iterator iterator() {
+ return data.iterator();
+ }
+
+ /**
+ * Called when a list is mutated.
+ */
+ protected void onModified() throws IOException {
+ owner.save();
+ }
+
+ /**
+ * Returns the snapshot view of instances as list.
+ */
+ public List toList() {
+ return data.getView();
+ }
+
+ /**
+ * Gets all the {@link Describable}s in an array.
+ */
+ public T[] toArray(T[] array) {
+ return data.toArray(array);
+ }
+
+ public void addAllTo(Collection super T> dst) {
+ data.addAllTo(dst);
+ }
+
+ public boolean isEmpty() {
+ return data.isEmpty();
+ }
+
+ /**
+ * {@link Converter} implementation for XStream.
+ *
+ * Serializaion form is compatible with plain {@link List}.
+ */
+ public static class ConverterImpl extends AbstractCollectionConverter {
+ CopyOnWriteList.ConverterImpl copyOnWriteListConverter;
+
+ public ConverterImpl(Mapper mapper) {
+ super(mapper);
+ copyOnWriteListConverter = new CopyOnWriteList.ConverterImpl(mapper());
+ }
+
+ public boolean canConvert(Class type) {
+ // handle subtypes in case the onModified method is overridden.
+ return PersistedList.class.isAssignableFrom(type);
+ }
+
+ public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {
+ for (Object o : (PersistedList) source)
+ writeItem(o, context, writer);
+ }
+
+ public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
+ CopyOnWriteList core = copyOnWriteListConverter.unmarshal(reader, context);
+
+ try {
+ PersistedList r = (PersistedList)context.getRequiredType().newInstance();
+ r.data.replaceBy(core);
+ return r;
+ } catch (InstantiationException e) {
+ InstantiationError x = new InstantiationError();
+ x.initCause(e);
+ throw x;
+ } catch (IllegalAccessException e) {
+ IllegalAccessError x = new IllegalAccessError();
+ x.initCause(e);
+ throw x;
+ }
+ }
+ }
+}
+
diff --git a/core/src/main/resources/hudson/PluginManager/checkUpdates.jelly b/core/src/main/resources/hudson/PluginManager/checkUpdates.jelly
index b8b251568f44bc228a9713712f0b1244d24f3f4b..b46db05c6c34d65a024e9606616b16723aa5c8e8 100644
--- a/core/src/main/resources/hudson/PluginManager/checkUpdates.jelly
+++ b/core/src/main/resources/hudson/PluginManager/checkUpdates.jelly
@@ -38,7 +38,7 @@ THE SOFTWARE.
diff --git a/core/src/main/resources/hudson/PluginManager/sites.jelly b/core/src/main/resources/hudson/PluginManager/sites.jelly
new file mode 100644
index 0000000000000000000000000000000000000000..52adf178c607f4c9cfe3985a0945651fbea36a1f
--- /dev/null
+++ b/core/src/main/resources/hudson/PluginManager/sites.jelly
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/core/src/main/resources/hudson/PluginManager/tabBar.jelly b/core/src/main/resources/hudson/PluginManager/tabBar.jelly
index a8e6f4183d44d3dd9029fdbc6c05c49892c49849..7ad4c03b6ab10d4767a31a9d8b04518914ccfd96 100644
--- a/core/src/main/resources/hudson/PluginManager/tabBar.jelly
+++ b/core/src/main/resources/hudson/PluginManager/tabBar.jelly
@@ -30,6 +30,7 @@ THE SOFTWARE.
+
diff --git a/core/src/main/resources/hudson/PluginManager/table.jelly b/core/src/main/resources/hudson/PluginManager/table.jelly
index b364680f71ccd96f0c4d149fb35b6a0c5fcceb55..33017c5582647cc9c7643bbd003deaad6427168c 100644
--- a/core/src/main/resources/hudson/PluginManager/table.jelly
+++ b/core/src/main/resources/hudson/PluginManager/table.jelly
@@ -45,7 +45,7 @@ THE SOFTWARE.
-
+
@@ -54,9 +54,9 @@ THE SOFTWARE.
${p.excerpt}
-
${%compatWarning}
-
-
+
${%compatWarning}
+
+
diff --git a/core/src/main/resources/hudson/model/UpdateCenter/PageDecoratorImpl/footer.jelly b/core/src/main/resources/hudson/model/UpdateCenter/PageDecoratorImpl/footer.jelly
index 3b4a4633a8620df2e31a38a9dd600635a7ffee91..2f5b00a7d9a053e254bee9425dafc10fe46c5e02 100644
--- a/core/src/main/resources/hudson/model/UpdateCenter/PageDecoratorImpl/footer.jelly
+++ b/core/src/main/resources/hudson/model/UpdateCenter/PageDecoratorImpl/footer.jelly
@@ -30,12 +30,18 @@ THE SOFTWARE.
This file is pulled into the layout.jelly
-->
-
-
-
+
+
+
+
+
diff --git a/core/src/test/java/hudson/model/UpdateCenterTest.java b/core/src/test/java/hudson/model/UpdateCenterTest.java
index cfbd59b4d20b9c799a08120729ad8c5675bb3a11..eac818007644870abefa1d6f822d61034a30a198 100644
--- a/core/src/test/java/hudson/model/UpdateCenterTest.java
+++ b/core/src/test/java/hudson/model/UpdateCenterTest.java
@@ -49,8 +49,8 @@ public class UpdateCenterTest extends TestCase {
String jsonp = IOUtils.toString(url.openStream());
String json = jsonp.substring(jsonp.indexOf('(')+1,jsonp.lastIndexOf(')'));
- UpdateCenter uc = new UpdateCenter(null);
- UpdateCenter.Data data = uc.new Data(JSONObject.fromObject(json));
+ UpdateSite us = new UpdateSite("default", url.toExternalForm());
+ UpdateSite.Data data = us.new Data(JSONObject.fromObject(json));
assertTrue(data.core.url.startsWith("https://hudson.dev.java.net/"));
assertTrue(data.plugins.containsKey("rake"));
System.out.println(data.core.url);
diff --git a/test/src/main/java/org/jvnet/hudson/test/HudsonTestCase.java b/test/src/main/java/org/jvnet/hudson/test/HudsonTestCase.java
index a4b66c8dbc7bdbd77d210dff638c9c3f4cd30e05..ddbde07a8d130c23fe22b821125734a9d14e5c4b 100644
--- a/test/src/main/java/org/jvnet/hudson/test/HudsonTestCase.java
+++ b/test/src/main/java/org/jvnet/hudson/test/HudsonTestCase.java
@@ -53,10 +53,10 @@ import hudson.model.Run;
import hudson.model.Saveable;
import hudson.model.TaskListener;
import hudson.model.UpdateCenter;
+import hudson.model.UpdateSite;
import hudson.model.AbstractProject;
import hudson.model.View;
import hudson.model.RootAction;
-import hudson.model.UpdateCenter.UpdateCenterConfiguration;
import hudson.model.Node.Mode;
import hudson.security.csrf.CrumbIssuer;
import hudson.slaves.CommandLauncher;
@@ -238,11 +238,10 @@ public abstract class HudsonTestCase extends TestCase implements RootAction {
// load updates from local proxy to avoid network traffic.
final String updateCenterUrl = "http://localhost:"+JavaNetReverseProxy.getInstance().localPort+"/";
- hudson.getUpdateCenter().configure(new UpdateCenterConfiguration() {
- @Override public String getUpdateCenterUrl() {
- return updateCenterUrl;
- }
- });
+ List newSites = new ArrayList();
+ newSites.add(new UpdateSite("default", updateCenterUrl));
+ hudson.getUpdateCenter().replaceSources(newSites);
+
// don't waste bandwidth talking to the update center
DownloadService.neverUpdate = true;
UpdateCenter.neverUpdate = true;
diff --git a/war/resources/scripts/hudson-behavior.js b/war/resources/scripts/hudson-behavior.js
index 3483210a9ac65273ee0f2f82a2d08428be8c8057..2e020ec20c03d0c3b535be241ad13e9df4d1e8a3 100644
--- a/war/resources/scripts/hudson-behavior.js
+++ b/war/resources/scripts/hudson-behavior.js
@@ -1552,6 +1552,11 @@ var downloadService = {
},
post : function(id,data) {
+ if (data==undefined) {
+ // default to id in data
+ data = id;
+ id = data.id;
+ }
var o = this.continuations[id];
new Ajax.Request(o.postBack, {
parameters:{json:Object.toJSON(data)},
@@ -1563,28 +1568,8 @@ var downloadService = {
}
};
-// update center service. for historical reasons,
-// this is separate from downloadSerivce
-var updateCenter = {
- postBackURL : null,
- info: {},
- completionHandler: null,
- url: "http://hudson-ci.org/",
-
- checkUpdates : function() {
- loadScript(updateCenter.url+"update-center.json?"+Hash.toQueryString(updateCenter.info));
- },
-
- post : function(data) {
- new Ajax.Request(updateCenter.postBackURL, {
- parameters:{json:Object.toJSON(data)},
- onSuccess: function() {
- if(updateCenter.completionHandler!=null)
- updateCenter.completionHandler();
- }
- });
- }
-};
+// update center service. to remain compatible with earlier version of Hudson, aliased.
+var updateCenter = downloadService;
/*
redirects to a page once the page is ready.