From f76fee63ed9cb3a30d3c0c092d860b1cb93a481b Mon Sep 17 00:00:00 2001
From: Gerard Smyth <gerard.smyth@gmail.com>
Date: Thu, 08 May 2014 13:09:30 -0400
Subject: [PATCH] Updated the SyndicationServlet to provide an additional option to return details of the tags in the repository instead of the commits. This uses a new 'ot' request parameter to indicate the object type of the content to return, which can be ither TAG or COMMIT. If this is not provided, then COMMIT is assumed to maintain backwards compatability. If tags are returned, then the paging parameters, 'l' and 'pg' are still supported, but searching options are currently ignored.

---
 src/main/java/com/gitblit/manager/PluginManager.java |  406 +++++++++++++++++++++++++++++++++++++++++++++++----------
 1 files changed, 330 insertions(+), 76 deletions(-)

diff --git a/src/main/java/com/gitblit/manager/PluginManager.java b/src/main/java/com/gitblit/manager/PluginManager.java
index 7b03f50..2ee4855 100644
--- a/src/main/java/com/gitblit/manager/PluginManager.java
+++ b/src/main/java/com/gitblit/manager/PluginManager.java
@@ -18,13 +18,18 @@
 import java.io.BufferedInputStream;
 import java.io.File;
 import java.io.FileFilter;
+import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.HttpURLConnection;
 import java.net.Proxy;
 import java.net.URL;
 import java.net.URLConnection;
+import java.security.DigestInputStream;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
 import java.util.ArrayList;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.TreeMap;
@@ -33,11 +38,18 @@
 import org.slf4j.LoggerFactory;
 
 import ro.fortsoft.pf4j.DefaultPluginManager;
-import ro.fortsoft.pf4j.PluginVersion;
+import ro.fortsoft.pf4j.PluginClassLoader;
+import ro.fortsoft.pf4j.PluginState;
+import ro.fortsoft.pf4j.PluginStateEvent;
+import ro.fortsoft.pf4j.PluginStateListener;
 import ro.fortsoft.pf4j.PluginWrapper;
+import ro.fortsoft.pf4j.Version;
 
+import com.gitblit.Constants;
 import com.gitblit.Keys;
+import com.gitblit.extensions.GitblitPlugin;
 import com.gitblit.models.PluginRegistry;
+import com.gitblit.models.PluginRegistry.InstallState;
 import com.gitblit.models.PluginRegistry.PluginRegistration;
 import com.gitblit.models.PluginRegistry.PluginRelease;
 import com.gitblit.utils.Base64;
@@ -49,15 +61,18 @@
 
 /**
  * The plugin manager maintains the lifecycle of plugins. It is exposed as
- * Dagger bean. The extension consumers supposed to retrieve plugin  manager
- * from the Dagger DI and retrieve extensions provided by active plugins.
+ * Dagger bean. The extension consumers supposed to retrieve plugin manager from
+ * the Dagger DI and retrieve extensions provided by active plugins.
  *
  * @author David Ostrovsky
+ * @author James Moger
  *
  */
-public class PluginManager extends DefaultPluginManager implements IPluginManager {
+public class PluginManager implements IPluginManager, PluginStateListener {
 
 	private final Logger logger = LoggerFactory.getLogger(getClass());
+
+	private final DefaultPluginManager pf4j;
 
 	private final IRuntimeManager runtimeManager;
 
@@ -67,47 +82,230 @@
 	private int readTimeout = 12800;
 
 	public PluginManager(IRuntimeManager runtimeManager) {
-		super(runtimeManager.getFileOrFolder(Keys.plugins.folder, "${baseFolder}/plugins"));
+		File dir = runtimeManager.getFileOrFolder(Keys.plugins.folder, "${baseFolder}/plugins");
+		dir.mkdirs();
 		this.runtimeManager = runtimeManager;
+
+		this.pf4j = new DefaultPluginManager(dir);
+
+		try {
+			Version systemVersion = Version.createVersion(Constants.getVersion());
+			pf4j.setSystemVersion(systemVersion);
+		} catch (Exception e) {
+			logger.error(null, e);
+		}
+	}
+
+	@Override
+	public Version getSystemVersion() {
+		return pf4j.getSystemVersion();
+	}
+
+	@Override
+	public void pluginStateChanged(PluginStateEvent event) {
+		logger.debug(event.toString());
 	}
 
 	@Override
 	public PluginManager start() {
-		logger.info("Loading plugins...");
-		loadPlugins();
-		logger.info("Starting loaded plugins...");
-		startPlugins();
+		pf4j.loadPlugins();
+		logger.debug("Starting plugins");
+		pf4j.startPlugins();
 		return this;
 	}
 
 	@Override
 	public PluginManager stop() {
-		logger.info("Stopping loaded plugins...");
-		stopPlugins();
+		logger.debug("Stopping plugins");
+		pf4j.stopPlugins();
 		return null;
 	}
 
+	/**
+	 * Installs the plugin from the url.
+	 *
+	 * @param url
+	 * @param verifyChecksum
+	 * @return true if successful
+	 */
 	@Override
-	public boolean deletePlugin(PluginWrapper pw) {
-		File folder = runtimeManager.getFileOrFolder(Keys.plugins.folder, "${baseFolder}/plugins");
-		File pluginFolder = new File(folder, pw.getPluginPath());
-		File pluginZip = new File(folder, pw.getPluginPath() + ".zip");
+	public synchronized boolean installPlugin(String url, boolean verifyChecksum) throws IOException {
+		File file = download(url, verifyChecksum);
+		if (file == null || !file.exists()) {
+			logger.error("Failed to download plugin {}", url);
+			return false;
+		}
 
-		if (pluginFolder.exists()) {
-			FileUtils.delete(pluginFolder);
+		String pluginId = pf4j.loadPlugin(file);
+		if (StringUtils.isEmpty(pluginId)) {
+			logger.error("Failed to load plugin {}", file);
+			return false;
 		}
-		if (pluginZip.exists()) {
-			FileUtils.delete(pluginZip);
+
+		// allow the plugin to prepare for operation after installation
+		PluginWrapper pluginWrapper = pf4j.getPlugin(pluginId);
+		if (pluginWrapper.getPlugin() instanceof GitblitPlugin) {
+			((GitblitPlugin) pluginWrapper.getPlugin()).onInstall();
 		}
-		return true;
+
+		PluginState state = pf4j.startPlugin(pluginId);
+		return PluginState.STARTED.equals(state);
 	}
 
 	@Override
-	public boolean refreshRegistry() {
+	public synchronized boolean upgradePlugin(String pluginId, String url, boolean verifyChecksum) throws IOException {
+		// ensure we can download the update BEFORE we remove the existing one
+		File file = download(url, verifyChecksum);
+		if (file == null || !file.exists()) {
+			logger.error("Failed to download plugin {}", url);
+			return false;
+		}
+
+		Version oldVersion = pf4j.getPlugin(pluginId).getDescriptor().getVersion();
+		if (removePlugin(pluginId, false)) {
+			String newPluginId = pf4j.loadPlugin(file);
+			if (StringUtils.isEmpty(newPluginId)) {
+				logger.error("Failed to load plugin {}", file);
+				return false;
+			}
+
+			// the plugin to handle an upgrade
+			PluginWrapper pluginWrapper = pf4j.getPlugin(newPluginId);
+			if (pluginWrapper.getPlugin() instanceof GitblitPlugin) {
+				((GitblitPlugin) pluginWrapper.getPlugin()).onUpgrade(oldVersion);
+			}
+
+			PluginState state = pf4j.startPlugin(newPluginId);
+			return PluginState.STARTED.equals(state);
+		} else {
+			logger.error("Failed to delete plugin {}", pluginId);
+		}
+		return false;
+	}
+
+	@Override
+	public synchronized boolean disablePlugin(String pluginId) {
+		return pf4j.disablePlugin(pluginId);
+	}
+
+	@Override
+	public synchronized boolean enablePlugin(String pluginId) {
+		if (pf4j.enablePlugin(pluginId)) {
+			return PluginState.STARTED == pf4j.startPlugin(pluginId);
+		}
+		return false;
+	}
+
+	@Override
+	public synchronized boolean uninstallPlugin(String pluginId) {
+		return removePlugin(pluginId, true);
+	}
+
+	protected boolean removePlugin(String pluginId, boolean isUninstall) {
+		PluginWrapper pluginWrapper = getPlugin(pluginId);
+		final String name = pluginWrapper.getPluginPath().substring(1);
+
+		if (isUninstall) {
+			// allow the plugin to prepare for uninstallation
+			if (pluginWrapper.getPlugin() instanceof GitblitPlugin) {
+				((GitblitPlugin) pluginWrapper.getPlugin()).onUninstall();
+			}
+		}
+
+		if (pf4j.deletePlugin(pluginId)) {
+
+			// delete the checksums
+			File pFolder = runtimeManager.getFileOrFolder(Keys.plugins.folder, "${baseFolder}/plugins");
+			File [] checksums = pFolder.listFiles(new FileFilter() {
+				@Override
+				public boolean accept(File file) {
+					if (!file.isFile()) {
+						return false;
+					}
+
+					return file.getName().startsWith(name) &&
+							(file.getName().toLowerCase().endsWith(".sha1")
+									|| file.getName().toLowerCase().endsWith(".md5"));
+				}
+			});
+
+			if (checksums != null) {
+				for (File checksum : checksums) {
+					checksum.delete();
+				}
+			}
+			return true;
+		}
+		return false;
+	}
+
+	@Override
+	public synchronized PluginState startPlugin(String pluginId) {
+		return pf4j.startPlugin(pluginId);
+	}
+
+	@Override
+	public synchronized PluginState stopPlugin(String pluginId) {
+		return pf4j.stopPlugin(pluginId);
+	}
+
+	@Override
+	public synchronized void startPlugins() {
+		pf4j.startPlugins();
+	}
+
+	@Override
+	public synchronized void stopPlugins() {
+		pf4j.stopPlugins();
+	}
+
+	@Override
+	public synchronized List<PluginWrapper> getPlugins() {
+		return pf4j.getPlugins();
+	}
+
+	@Override
+	public synchronized PluginWrapper getPlugin(String pluginId) {
+		return pf4j.getPlugin(pluginId);
+	}
+
+	@Override
+	public synchronized List<Class<?>> getExtensionClasses(String pluginId) {
+		List<Class<?>> list = new ArrayList<Class<?>>();
+		PluginClassLoader loader = pf4j.getPluginClassLoader(pluginId);
+		for (String className : pf4j.getExtensionClassNames(pluginId)) {
+			try {
+				list.add(loader.loadClass(className));
+			} catch (ClassNotFoundException e) {
+				logger.error(String.format("Failed to find %s in %s", className, pluginId), e);
+			}
+		}
+		return list;
+	}
+
+	@Override
+	public synchronized <T> List<T> getExtensions(Class<T> type) {
+		return pf4j.getExtensions(type);
+	}
+
+	@Override
+	public synchronized PluginWrapper whichPlugin(Class<?> clazz) {
+		return pf4j.whichPlugin(clazz);
+	}
+
+	@Override
+	public synchronized boolean refreshRegistry(boolean verifyChecksum) {
 		String dr = "http://gitblit.github.io/gitblit-registry/plugins.json";
 		String url = runtimeManager.getSettings().getString(Keys.plugins.registry, dr);
 		try {
-			return download(url);
+			File file = download(url, verifyChecksum);
+			if (file != null && file.exists()) {
+				URL selfUrl = new URL(url.substring(0, url.lastIndexOf('/')));
+				// replace ${self} with the registry url
+				String content = FileUtils.readContent(file, "\n");
+				content = content.replace("${self}", selfUrl.toString());
+				FileUtils.writeContent(file, content);
+			}
 		} catch (Exception e) {
 			logger.error(String.format("Failed to retrieve plugins.json from %s", url), e);
 		}
@@ -124,10 +322,10 @@
 			}
 		};
 
-		File [] files = folder.listFiles(jsonFilter);
+		File[] files = folder.listFiles(jsonFilter);
 		if (files == null || files.length == 0) {
 			// automatically retrieve the registry if we don't have a local copy
-			refreshRegistry();
+			refreshRegistry(true);
 			files = folder.listFiles(jsonFilter);
 		}
 
@@ -140,6 +338,7 @@
 			try {
 				String json = FileUtils.readContent(file, "\n");
 				registry = JsonUtils.fromJsonString(json, PluginRegistry.class);
+				registry.setup();
 			} catch (Exception e) {
 				logger.error("Failed to deserialize " + file, e);
 			}
@@ -151,20 +350,20 @@
 	}
 
 	@Override
-	public List<PluginRegistration> getRegisteredPlugins() {
+	public synchronized List<PluginRegistration> getRegisteredPlugins() {
 		List<PluginRegistration> list = new ArrayList<PluginRegistration>();
 		Map<String, PluginRegistration> map = new TreeMap<String, PluginRegistration>();
 		for (PluginRegistry registry : getRegistries()) {
-			List<PluginRegistration> registrations = registry.registrations;
-			list.addAll(registrations);
-			for (PluginRegistration reg : registrations) {
+			list.addAll(registry.registrations);
+			for (PluginRegistration reg : list) {
 				reg.installedRelease = null;
 				map.put(reg.id, reg);
 			}
 		}
-		for (PluginWrapper pw : getPlugins()) {
+
+		for (PluginWrapper pw : pf4j.getPlugins()) {
 			String id = pw.getDescriptor().getPluginId();
-			PluginVersion pv = pw.getDescriptor().getVersion();
+			Version pv = pw.getDescriptor().getVersion();
 			PluginRegistration reg = map.get(id);
 			if (reg != null) {
 				reg.installedRelease = pv.toString();
@@ -174,10 +373,21 @@
 	}
 
 	@Override
-	public PluginRegistration lookupPlugin(String idOrName) {
-		for (PluginRegistry registry : getRegistries()) {
-			PluginRegistration reg = registry.lookup(idOrName);
-			if (reg != null) {
+	public synchronized List<PluginRegistration> getRegisteredPlugins(InstallState state) {
+		List<PluginRegistration> list = getRegisteredPlugins();
+		Iterator<PluginRegistration> itr = list.iterator();
+		while (itr.hasNext()) {
+			if (state != itr.next().getInstallState(getSystemVersion())) {
+				itr.remove();
+			}
+		}
+		return list;
+	}
+
+	@Override
+	public synchronized PluginRegistration lookupPlugin(String pluginId) {
+		for (PluginRegistration reg : getRegisteredPlugins()) {
+			if (reg.id.equalsIgnoreCase(pluginId)) {
 				return reg;
 			}
 		}
@@ -185,65 +395,109 @@
 	}
 
 	@Override
-	public PluginRelease lookupRelease(String idOrName, String version) {
-		for (PluginRegistry registry : getRegistries()) {
-			PluginRegistration reg = registry.lookup(idOrName);
-			if (reg != null) {
-				PluginRelease pv;
-				if (StringUtils.isEmpty(version)) {
-					pv = reg.getCurrentRelease();
-				} else {
-					pv = reg.getRelease(version);
-				}
-				if (pv != null) {
-					return pv;
-				}
-			}
+	public synchronized PluginRelease lookupRelease(String pluginId, String version) {
+		PluginRegistration reg = lookupPlugin(pluginId);
+		if (reg == null) {
+			return null;
 		}
-		return null;
-	}
 
-
-	/**
-	 * Installs the plugin from the plugin version.
-	 *
-	 * @param pv
-	 * @throws IOException
-	 * @return true if successful
-	 */
-	@Override
-	public boolean installPlugin(PluginRelease pv) {
-		return installPlugin(pv.url);
+		PluginRelease pv;
+		if (StringUtils.isEmpty(version)) {
+			pv = reg.getCurrentRelease(getSystemVersion());
+		} else {
+			pv = reg.getRelease(version);
+		}
+		return pv;
 	}
 
 	/**
-	 * Installs the plugin from the url.
+	 * Downloads a file with optional checksum verification.
 	 *
 	 * @param url
-	 * @return true if successful
+	 * @param verifyChecksum
+	 * @return
+	 * @throws IOException
 	 */
-	@Override
-	public boolean installPlugin(String url) {
+	protected File download(String url, boolean verifyChecksum) throws IOException {
+		File file = downloadFile(url);
+
+		File sha1File = null;
 		try {
-			if (!download(url)) {
-				return false;
-			}
-			// TODO stop, unload, load
+			sha1File = downloadFile(url + ".sha1");
 		} catch (IOException e) {
-			logger.error("Failed to install plugin from " + url, e);
 		}
-		return true;
+
+		File md5File = null;
+		try {
+			md5File = downloadFile(url + ".md5");
+		} catch (IOException e) {
+
+		}
+
+		if (sha1File == null && md5File == null && verifyChecksum) {
+			throw new IOException("Missing SHA1 and MD5 checksums for " + url);
+		}
+
+		String expected;
+		MessageDigest md = null;
+		if (sha1File != null && sha1File.exists()) {
+			// prefer SHA1 to MD5
+			expected = FileUtils.readContent(sha1File, "\n").split(" ")[0].trim();
+			try {
+				md = MessageDigest.getInstance("SHA-1");
+			} catch (NoSuchAlgorithmException e) {
+				logger.error(null, e);
+			}
+		} else {
+			expected = FileUtils.readContent(md5File, "\n").split(" ")[0].trim();
+			try {
+				md = MessageDigest.getInstance("MD5");
+			} catch (Exception e) {
+				logger.error(null, e);
+			}
+		}
+
+		// calculate the checksum
+		FileInputStream is = null;
+		try {
+			is = new FileInputStream(file);
+			DigestInputStream dis = new DigestInputStream(is, md);
+			byte [] buffer = new byte[1024];
+			while ((dis.read(buffer)) > -1) {
+				// read
+			}
+			dis.close();
+
+			byte [] digest = md.digest();
+			String calculated = StringUtils.toHex(digest).trim();
+
+			if (!expected.equals(calculated)) {
+				String msg = String.format("Invalid checksum for %s\nAlgorithm:  %s\nExpected:   %s\nCalculated: %s",
+						file.getAbsolutePath(),
+						md.getAlgorithm(),
+						expected,
+						calculated);
+				file.delete();
+				throw new IOException(msg);
+			}
+		} finally {
+			if (is != null) {
+				is.close();
+			}
+		}
+		return file;
 	}
 
 	/**
 	 * Download a file to the plugins folder.
 	 *
 	 * @param url
-	 * @return
+	 * @return the downloaded file
 	 * @throws IOException
 	 */
-	protected boolean download(String url) throws IOException {
+	protected File downloadFile(String url) throws IOException {
 		File pFolder = runtimeManager.getFileOrFolder(Keys.plugins.folder, "${baseFolder}/plugins");
+		pFolder.mkdirs();
 		File tmpFile = new File(pFolder, StringUtils.getSHA1(url) + ".tmp");
 		if (tmpFile.exists()) {
 			tmpFile.delete();
@@ -256,9 +510,9 @@
 		long lastModified = conn.getHeaderFieldDate("Last-Modified", System.currentTimeMillis());
 
 		Files.copy(new InputSupplier<InputStream>() {
-			 @Override
+			@Override
 			public InputStream getInput() throws IOException {
-				 return new BufferedInputStream(conn.getInputStream());
+				return new BufferedInputStream(conn.getInputStream());
 			}
 		}, tmpFile);
 
@@ -269,7 +523,7 @@
 		tmpFile.renameTo(destFile);
 		destFile.setLastModified(lastModified);
 
-		return true;
+		return destFile;
 	}
 
 	protected URLConnection getConnection(URL url) throws IOException {

--
Gitblit v1.9.1