File D:\maven-qcr-plugin-testsunitaires\qcr4java\jenkins-master\target\site\qc\core\src\main\java\hudson\model\UpdateSite.java.html

1 /*
2 * The MIT License
3 *
4 * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Yahoo! Inc., Seiji Sogabe,
5 * Andrew Bayer
6 *
7 * Permission is hereby granted, free of charge, to any person obtaining a copy
8 * of this software and associated documentation files (the "Software"), to deal
9 * in the Software without restriction, including without limitation the rights
10 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 * copies of the Software, and to permit persons to whom the Software is
12 * furnished to do so, subject to the following conditions:
13 *
14 * The above copyright notice and this permission notice shall be included in
15 * all copies or substantial portions of the Software.
16 *
17 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 * THE SOFTWARE.
24 */
25
26 package hudson.model;
27
28 import hudson.PluginManager;
29 import hudson.PluginWrapper;
30 import hudson.ProxyConfiguration;
31 import hudson.lifecycle.Lifecycle;
32 import hudson.model.UpdateCenter.UpdateCenterJob;
33 import hudson.util.FormValidation;
34 import hudson.util.FormValidation.Kind;
35 import hudson.util.HttpResponses;
36 import hudson.util.IOUtils;
37 import hudson.util.TextFile;
38 import hudson.util.VersionNumber;
39 import jenkins.model.Jenkins;
40 import jenkins.util.JSONSignatureValidator;
41 import net.sf.json.JSONException;
42 import net.sf.json.JSONObject;
43 import org.kohsuke.stapler.DataBoundConstructor;
44 import org.kohsuke.stapler.HttpResponse;
45 import org.kohsuke.stapler.StaplerRequest;
46 import org.kohsuke.stapler.export.Exported;
47 import org.kohsuke.stapler.export.ExportedBean;
48 import org.kohsuke.stapler.interceptor.RequirePOST;
49
50 import java.io.File;
51 import java.io.IOException;
52 import java.io.InputStream;
53 import java.net.URI;
54 import java.net.URL;
55 import java.net.URLConnection;
56 import java.net.URLEncoder;
57 import java.security.GeneralSecurityException;
58 import java.util.ArrayList;
59 import java.util.Collections;
60 import java.util.HashMap;
61 import java.util.List;
62 import java.util.Map;
63 import java.util.Set;
64 import java.util.TreeMap;
65 import java.util.concurrent.Callable;
66 import java.util.concurrent.Future;
67 import java.util.logging.Level;
68 import java.util.logging.Logger;
69
70 import static hudson.util.TimeUnit2.*;
71
72
73 /**
74 * Source of the update center information, like "http://jenkins-ci.org/update-center.json"
75 *
76 * <p>
77 * Jenkins can have multiple {@link UpdateSite}s registered in the system, so that it can pick up plugins
78 * from different locations.
79 *
80 * @author Andrew Bayer
81 * @author Kohsuke Kawaguchi
82 * @since 1.333
83 */
84 @ExportedBean
85 public class UpdateSite {
86 /**
87 * What's the time stamp of data file?
88 */
89 private transient long dataTimestamp = -1;
90
91 /**
92 * When was the last time we asked a browser to check the data for us?
93 *
94 * <p>
95 * There's normally some delay between when we send HTML that includes the check code,
96 * until we get the data back, so this variable is used to avoid asking too many browseres
97 * all at once.
98 */
99 private transient volatile long lastAttempt = -1;
100
101 /**
102 * If the attempt to fetch data fails, we progressively use longer time out before retrying,
103 * to avoid overloading the server.
104 */
105 private transient volatile long retryWindow;
106
107 /**
108 * lastModified time of the data file when it was last read.
109 */
110 private transient long dataLastReadFromFile;
111
112 /**
113 * Latest data as read from the data file.
114 */
115 private transient Data data;
116
117 /**
118 * ID string for this update source.
119 */
120 private final String id;
121
122 /**
123 * Path to <tt>update-center.json</tt>, like <tt>http://jenkins-ci.org/update-center.json</tt>.
124 */
125 private final String url;
126
127 public UpdateSite(String id, String url) {
128 this.id = id;
129 this.url = url;
130 }
131
132 /**
133 * When read back from XML, initialize them back to -1.
134 */
135 private Object readResolve() {
136 dataTimestamp = lastAttempt = -1;
137 return this;
138 }
139
140 /**
141 * Get ID string.
142 */
143 @Exported
144 public String getId() {
145 return id;
146 }
147
148 @Exported
149 public long getDataTimestamp() {
150 return dataTimestamp;
151 }
152
153 /**
154 * Update the data file from the given URL if the file
155 * does not exist, or is otherwise due for update.
156 * Accepted formats are JSONP or HTML with {@code postMessage}, not raw JSON.
157 * @param signatureCheck whether to enforce the signature (may be off only for testing!)
158 * @return null if no updates are necessary, or the future result
159 * @since 1.502
160 */
161 public Future<FormValidation> updateDirectly(final boolean signatureCheck) {
162 if (! getDataFile().exists() || isDue()) {
163 return Jenkins.getInstance().getUpdateCenter().updateService.submit(new Callable<FormValidation>() {
164
165 public FormValidation call() throws Exception {
166 URL src = new URL(getUrl() + "?id=" + URLEncoder.encode(getId(),"UTF-8")
167 + "&version="+URLEncoder.encode(Jenkins.VERSION, "UTF-8"));
168 URLConnection conn = ProxyConfiguration.open(src);
169 InputStream is = conn.getInputStream();
170 try {
171 String uncleanJson = IOUtils.toString(is,"UTF-8");
172 int jsonStart = uncleanJson.indexOf("{\"");
173 if (jsonStart >= 0) {
174 return updateData(uncleanJson.substring(jsonStart), signatureCheck);
175 } else {
176 throw new IOException("Could not find json in content of " +
177 "update center from url: "+src.toExternalForm());
178 }
179 } finally {
180 if (is != null)
181 is.close();
182 }
183 }
184 });
185 }
186 return null;
187 }
188
189 /**
190 * This is the endpoint that receives the update center data file from the browser.
191 */
192 public FormValidation doPostBack(StaplerRequest req) throws IOException, GeneralSecurityException {
193 return updateData(IOUtils.toString(req.getInputStream(),"UTF-8"), true);
194 }
195
196 private FormValidation updateData(String json, boolean signatureCheck)
197 throws IOException {
198
199 dataTimestamp = System.currentTimeMillis();
200
201 JSONObject o = JSONObject.fromObject(json);
202
203 try {
204 int v = o.getInt("updateCenterVersion");
205 if (v != 1) {
206 throw new IllegalArgumentException("Unrecognized update center version: " + v);
207 }
208 } catch (JSONException x) {
209 throw new IllegalArgumentException("Could not find (numeric) updateCenterVersion in " + json, x);
210 }
211
212 if (signatureCheck) {
213 FormValidation e = verifySignature(o);
214 if (e.kind!=Kind.OK) {
215 LOGGER.severe(e.toString());
216 return e;
217 }
218 }
219
220 LOGGER.info("Obtained the latest update center data file for UpdateSource " + id);
221 retryWindow = 0;
222 getDataFile().write(json);
223 return FormValidation.ok();
224 }
225
226 public FormValidation doVerifySignature() throws IOException {
227 return verifySignature(getJSONObject());
228 }
229
230 /**
231 * Verifies the signature in the update center data file.
232 */
233 private FormValidation verifySignature(JSONObject o) throws IOException {
234 return new JSONSignatureValidator("update site '"+id+"'").verifySignature(o);
235 }
236
237 /**
238 * Returns true if it's time for us to check for new version.
239 */
240 public boolean isDue() {
241 if(neverUpdate) return false;
242 if(dataTimestamp==-1)
243 dataTimestamp = getDataFile().file.lastModified();
244 long now = System.currentTimeMillis();
245
246 retryWindow = Math.max(retryWindow,SECONDS.toMillis(15));
247
248 boolean due = now - dataTimestamp > DAY && now - lastAttempt > retryWindow;
249 if(due) {
250 lastAttempt = now;
251 retryWindow = Math.min(retryWindow*2, HOURS.toMillis(1)); // exponential back off but at most 1 hour
252 }
253 return due;
254 }
255
256 /**
257 * Invalidates the cached data and force retrieval.
258 *
259 * @since 1.432
260 */
261 @RequirePOST
262 public HttpResponse doInvalidateData() {
263 Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
264 dataTimestamp = 0;
265 return HttpResponses.ok();
266 }
267
268 /**
269 * Loads the update center data, if any and if modified since last read.
270 *
271 * @return null if no data is available.
272 */
273 public Data getData() {
274 TextFile df = getDataFile();
275 if (df.exists() && dataLastReadFromFile != df.file.lastModified()) {
276 JSONObject o = getJSONObject();
277 if (o!=null) {
278 data = new Data(o);
279 dataLastReadFromFile = df.file.lastModified();
280 } else {
281 data = null;
282 }
283 }
284 return data;
285 }
286
287 /**
288 * Gets the raw update center JSON data.
289 */
290 public JSONObject getJSONObject() {
291 TextFile df = getDataFile();
292 if(df.exists()) {
293 try {
294 return JSONObject.fromObject(df.read());
295 } catch (JSONException e) {
296 LOGGER.log(Level.SEVERE,"Failed to parse "+df,e);
297 df.delete(); // if we keep this file, it will cause repeated failures
298 return null;
299 } catch (IOException e) {
300 LOGGER.log(Level.SEVERE,"Failed to parse "+df,e);
301 df.delete(); // if we keep this file, it will cause repeated failures
302 return null;
303 }
304 } else {
305 return null;
306 }
307 }
308
309 /**
310 * Returns a list of plugins that should be shown in the "available" tab.
311 * These are "all plugins - installed plugins".
312 */
313 @Exported
314 public List<Plugin> getAvailables() {
315 List<Plugin> r = new ArrayList<Plugin>();
316 Data data = getData();
317 if(data==null) return Collections.emptyList();
318 for (Plugin p : data.plugins.values()) {
319 if(p.getInstalled()==null)
320 r.add(p);
321 }
322 return r;
323 }
324
325 /**
326 * Gets the information about a specific plugin.
327 *
328 * @param artifactId
329 * The short name of the plugin. Corresponds to {@link PluginWrapper#getShortName()}.
330 *
331 * @return
332 * null if no such information is found.
333 */
334 public Plugin getPlugin(String artifactId) {
335 Data dt = getData();
336 if(dt==null) return null;
337 return dt.plugins.get(artifactId);
338 }
339
340 public Api getApi() {
341 return new Api(this);
342 }
343
344 /**
345 * Returns an "always up" server for Internet connectivity testing, or null if we are going to skip the test.
346 */
347 @Exported
348 public String getConnectionCheckUrl() {
349 Data dt = getData();
350 if(dt==null) return "http://www.google.com/";
351 return dt.connectionCheckUrl;
352 }
353
354 /**
355 * This is where we store the update center data.
356 */
357 private TextFile getDataFile() {
358 return new TextFile(new File(Jenkins.getInstance().getRootDir(),
359 "updates/" + getId()+".json"));
360 }
361
362 /**
363 * Returns the list of plugins that are updates to currently installed ones.
364 *
365 * @return
366 * can be empty but never null.
367 */
368 @Exported
369 public List<Plugin> getUpdates() {
370 Data data = getData();
371 if(data==null) return Collections.emptyList(); // fail to determine
372
373 List<Plugin> r = new ArrayList<Plugin>();
374 for (PluginWrapper pw : Jenkins.getInstance().getPluginManager().getPlugins()) {
375 Plugin p = pw.getUpdateInfo();
376 if(p!=null) r.add(p);
377 }
378
379 return r;
380 }
381
382 /**
383 * Does any of the plugin has updates?
384 */
385 @Exported
386 public boolean hasUpdates() {
387 Data data = getData();
388 if(data==null) return false;
389
390 for (PluginWrapper pw : Jenkins.getInstance().getPluginManager().getPlugins()) {
391 if(!pw.isBundled() && pw.getUpdateInfo()!=null)
392 // do not advertize updates to bundled plugins, since we generally want users to get them
393 // as a part of jenkins.war updates. This also avoids unnecessary pinning of plugins.
394 return true;
395 }
396 return false;
397 }
398
399
400 /**
401 * Exposed to get rid of hardcoding of the URL that serves up update-center.json
402 * in Javascript.
403 */
404 @Exported
405 public String getUrl() {
406 return url;
407 }
408
409 /**
410 * Where to actually download the update center?
411 *
412 * @deprecated
413 * Exposed only for UI.
414 */
415 public String getDownloadUrl() {
416 /*
417 HACKISH:
418
419 Loading scripts in HTTP from HTTPS pages cause browsers to issue a warning dialog.
420 The elegant way to solve the problem is to always load update center from HTTPS,
421 but our backend mirroring scheme isn't ready for that. So this hack serves regular
422 traffic in HTTP server, and only use HTTPS update center for Jenkins in HTTPS.
423
424 We'll monitor the traffic to see if we can sustain this added traffic.
425 */
426 if (url.equals("http://updates.jenkins-ci.org/update-center.json") && Jenkins.getInstance().isRootUrlSecure())
427 return "https"+url.substring(4);
428 return url;
429 }
430
431 /**
432 * Is this the legacy default update center site?
433 */
434 public boolean isLegacyDefault() {
435 return id.equals(UpdateCenter.ID_DEFAULT) && url.startsWith("http://hudson-ci.org/") || url.startsWith("http://updates.hudson-labs.org/");
436 }
437
438 /**
439 * In-memory representation of the update center data.
440 */
441 public final class Data {
442 /**
443 * The {@link UpdateSite} ID.
444 */
445 public final String sourceId;
446
447 /**
448 * The latest jenkins.war.
449 */
450 public final Entry core;
451 /**
452 * Plugins in the repository, keyed by their artifact IDs.
453 */
454 public final Map<String,Plugin> plugins = new TreeMap<String,Plugin>(String.CASE_INSENSITIVE_ORDER);
455
456 /**
457 * If this is non-null, Jenkins is going to check the connectivity to this URL to make sure
458 * the network connection is up. Null to skip the check.
459 */
460 public final String connectionCheckUrl;
461
462 Data(JSONObject o) {
463 this.sourceId = (String)o.get("id");
464 JSONObject c = o.optJSONObject("core");
465 if (c!=null) {
466 core = new Entry(sourceId, c, url);
467 } else {
468 core = null;
469 }
470 for(Map.Entry<String,JSONObject> e : (Set<Map.Entry<String,JSONObject>>)o.getJSONObject("plugins").entrySet()) {
471 plugins.put(e.getKey(),new Plugin(sourceId, e.getValue()));
472 }
473
474 connectionCheckUrl = (String)o.get("connectionCheckUrl");
475 }
476
477 /**
478 * Is there a new version of the core?
479 */
480 public boolean hasCoreUpdates() {
481 return core != null && core.isNewerThan(Jenkins.VERSION);
482 }
483
484 /**
485 * Do we support upgrade?
486 */
487 public boolean canUpgrade() {
488 return Lifecycle.get().canRewriteHudsonWar();
489 }
490 }
491
492 @ExportedBean
493 public static class Entry {
494 /**
495 * {@link UpdateSite} ID.
496 */
497 @Exported
498 public final String sourceId;
499
500 /**
501 * Artifact ID.
502 */
503 @Exported
504 public final String name;
505 /**
506 * The version.
507 */
508 @Exported
509 public final String version;
510 /**
511 * Download URL.
512 */
513 @Exported
514 public final String url;
515
516 public Entry(String sourceId, JSONObject o) {
517 this(sourceId, o, null);
518 }
519
520 Entry(String sourceId, JSONObject o, String baseURL) {
521 this.sourceId = sourceId;
522 this.name = o.getString("name");
523 this.version = o.getString("version");
524 String url = o.getString("url");
525 if (!URI.create(url).isAbsolute()) {
526 if (baseURL == null) {
527 throw new IllegalArgumentException("Cannot resolve " + url + " without a base URL");
528 }
529 url = URI.create(baseURL).resolve(url).toString();
530 }
531 this.url = url;
532 }
533
534 /**
535 * Checks if the specified "current version" is older than the version of this entry.
536 *
537 * @param currentVersion
538 * The string that represents the version number to be compared.
539 * @return
540 * true if the version listed in this entry is newer.
541 * false otherwise, including the situation where the strings couldn't be parsed as version numbers.
542 */
543 public boolean isNewerThan(String currentVersion) {
544 try {
545 return new VersionNumber(currentVersion).compareTo(new VersionNumber(version)) < 0;
546 } catch (IllegalArgumentException e) {
547 // couldn't parse as the version number.
548 return false;
549 }
550 }
551
552 public Api getApi() {
553 return new Api(this);
554 }
555
556 }
557
558 public final class Plugin extends Entry {
559 /**
560 * Optional URL to the Wiki page that discusses this plugin.
561 */
562 @Exported
563 public final String wiki;
564 /**
565 * Human readable title of the plugin, taken from Wiki page.
566 * Can be null.
567 *
568 * <p>
569 * beware of XSS vulnerability since this data comes from Wiki
570 */
571 @Exported
572 public final String title;
573 /**
574 * Optional excerpt string.
575 */
576 @Exported
577 public final String excerpt;
578 /**
579 * Optional version # from which this plugin release is configuration-compatible.
580 */
581 @Exported
582 public final String compatibleSinceVersion;
583 /**
584 * Version of Jenkins core this plugin was compiled against.
585 */
586 @Exported
587 public final String requiredCore;
588 /**
589 * Categories for grouping plugins, taken from labels assigned to wiki page.
590 * Can be null.
591 */
592 @Exported
593 public final String[] categories;
594
595 /**
596 * Dependencies of this plugin.
597 */
598 @Exported
599 public final Map<String,String> dependencies = new HashMap<String,String>();
600
601 @DataBoundConstructor
602 public Plugin(String sourceId, JSONObject o) {
603 super(sourceId, o, UpdateSite.this.url);
604 this.wiki = get(o,"wiki");
605 this.title = get(o,"title");
606 this.excerpt = get(o,"excerpt");
607 this.compatibleSinceVersion = get(o,"compatibleSinceVersion");
608 this.requiredCore = get(o,"requiredCore");
609 this.categories = o.has("labels") ? (String[])o.getJSONArray("labels").toArray(new String[0]) : null;
610 for(Object jo : o.getJSONArray("dependencies")) {
611 JSONObject depObj = (JSONObject) jo;
612 // Make sure there's a name attribute, that that name isn't maven-plugin - we ignore that one -
613 // and that the optional value isn't true.
614 if (get(depObj,"name")!=null
615 && !get(depObj,"name").equals("maven-plugin")
616 && get(depObj,"optional").equals("false")) {
617 dependencies.put(get(depObj,"name"), get(depObj,"version"));
618 }
619
620 }
621
622 }
623
624 private String get(JSONObject o, String prop) {
625 if(o.has(prop))
626 return o.getString(prop);
627 else
628 return null;
629 }
630
631 public String getDisplayName() {
632 if(title!=null) return title;
633 return name;
634 }
635
636 /**
637 * If some version of this plugin is currently installed, return {@link PluginWrapper}.
638 * Otherwise null.
639 */
640 @Exported
641 public PluginWrapper getInstalled() {
642 PluginManager pm = Jenkins.getInstance().getPluginManager();
643 return pm.getPlugin(name);
644 }
645
646 /**
647 * If the plugin is already installed, and the new version of the plugin has a "compatibleSinceVersion"
648 * value (i.e., it's only directly compatible with that version or later), this will check to
649 * see if the installed version is older than the compatible-since version. If it is older, it'll return false.
650 * If it's not older, or it's not installed, or it's installed but there's no compatibleSinceVersion
651 * specified, it'll return true.
652 */
653 @Exported
654 public boolean isCompatibleWithInstalledVersion() {
655 PluginWrapper installedVersion = getInstalled();
656 if (installedVersion != null) {
657 if (compatibleSinceVersion != null) {
658 if (new VersionNumber(installedVersion.getVersion())
659 .isOlderThan(new VersionNumber(compatibleSinceVersion))) {
660 return false;
661 }
662 }
663 }
664 return true;
665 }
666
667 /**
668 * Returns a list of dependent plugins which need to be installed or upgraded for this plugin to work.
669 */
670 @Exported
671 public List<Plugin> getNeededDependencies() {
672 List<Plugin> deps = new ArrayList<Plugin>();
673
674 for(Map.Entry<String,String> e : dependencies.entrySet()) {
675 Plugin depPlugin = Jenkins.getInstance().getUpdateCenter().getPlugin(e.getKey());
676 if (depPlugin == null) {
677 LOGGER.log(Level.WARNING, "Could not find dependency {0} of {1}", new Object[] {e.getKey(), name});
678 continue;
679 }
680 VersionNumber requiredVersion = new VersionNumber(e.getValue());
681
682 // Is the plugin installed already? If not, add it.
683 PluginWrapper current = depPlugin.getInstalled();
684
685 if (current ==null) {
686 deps.add(depPlugin);
687 }
688 // If the dependency plugin is installed, is the version we depend on newer than
689 // what's installed? If so, upgrade.
690 else if (current.isOlderThan(requiredVersion)) {
691 deps.add(depPlugin);
692 }
693 }
694
695 return deps;
696 }
697
698 public boolean isForNewerHudson() {
699 try {
700 return requiredCore!=null && new VersionNumber(requiredCore).isNewerThan(
701 new VersionNumber(Jenkins.VERSION.replaceFirst("SHOT *\\(private.*\\)", "SHOT")));
702 } catch (NumberFormatException nfe) {
703 return true; // If unable to parse version
704 }
705 }
706
707 /**
708 * @deprecated as of 1.326
709 * Use {@link #deploy()}.
710 */
711 public void install() {
712 deploy();
713 }
714
715 public Future<UpdateCenterJob> deploy() {
716 return deploy(false);
717 }
718
719 /**
720 * Schedules the installation of this plugin.
721 *
722 * <p>
723 * This is mainly intended to be called from the UI. The actual installation work happens
724 * asynchronously in another thread.
725 *
726 * @param dynamicLoad
727 * If true, the plugin will be dynamically loaded into this Jenkins. If false,
728 * the plugin will only take effect after the reboot.
729 * See {@link UpdateCenter#isRestartRequiredForCompletion()}
730 */
731 public Future<UpdateCenterJob> deploy(boolean dynamicLoad) {
732 Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
733 UpdateCenter uc = Jenkins.getInstance().getUpdateCenter();
734 for (Plugin dep : getNeededDependencies()) {
735 UpdateCenter.InstallationJob job = uc.getJob(dep);
736 if (job == null || job.status instanceof UpdateCenter.DownloadJob.Failure) {
737 LOGGER.log(Level.WARNING, "Adding dependent install of " + dep.name + " for plugin " + name);
738 dep.deploy(dynamicLoad);
739 } else {
740 LOGGER.log(Level.WARNING, "Dependent install of " + dep.name + " for plugin " + name + " already added, skipping");
741 }
742 }
743 return uc.addJob(uc.new InstallationJob(this, UpdateSite.this, Jenkins.getAuthentication(), dynamicLoad));
744 }
745
746 /**
747 * Schedules the downgrade of this plugin.
748 */
749 public Future<UpdateCenterJob> deployBackup() {
750 Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
751 UpdateCenter uc = Jenkins.getInstance().getUpdateCenter();
752 return uc.addJob(uc.new PluginDowngradeJob(this, UpdateSite.this, Jenkins.getAuthentication()));
753 }
754 /**
755 * Making the installation web bound.
756 */
757 @RequirePOST
758 public HttpResponse doInstall() throws IOException {
759 deploy(false);
760 return HttpResponses.redirectTo("../..");
761 }
762
763 @RequirePOST
764 public HttpResponse doInstallNow() throws IOException {
765 deploy(true);
766 return HttpResponses.redirectTo("../..");
767 }
768
769 /**
770 * Performs the downgrade of the plugin.
771 */
772 @RequirePOST
773 public HttpResponse doDowngrade() throws IOException {
774 deployBackup();
775 return HttpResponses.redirectTo("../..");
776 }
777 }
778
779 private static final long DAY = DAYS.toMillis(1);
780
781 private static final Logger LOGGER = Logger.getLogger(UpdateSite.class.getName());
782
783 // The name uses UpdateCenter for compatibility reason.
784 public static boolean neverUpdate = Boolean.getBoolean(UpdateCenter.class.getName()+".never");
785
786 }
787