← Back to team overview

linuxdcpp-team team mailing list archive

[Branch ~dcplusplus-team/dcplusplus/trunk] Rev 3270: Package plugins as .dcext files

 

------------------------------------------------------------
revno: 3270
committer: poy <poy@xxxxxxxxxx>
branch nick: trunk
timestamp: Tue 2013-04-23 00:31:37 +0200
message:
  Package plugins as .dcext files
added:
  Plugin format (dcext).txt
  win32/PluginInfoDlg.cpp
  win32/PluginInfoDlg.h
renamed:
  Extensions.txt => NMDC extensions.txt
modified:
  Compile.txt
  changelog.txt
  dcpp/Archive.cpp
  dcpp/Archive.h
  dcpp/PluginManager.cpp
  dcpp/PluginManager.h
  dcpp/SettingsManager.cpp
  dcpp/SettingsManager.h
  help/settings_advanced.html
  win32/AdvancedPage.cpp
  win32/MainWindow.cpp
  win32/PluginPage.cpp
  win32/PluginPage.h
  win32/WinUtil.cpp
  win32/WinUtil.h


--
lp:dcplusplus
https://code.launchpad.net/~dcplusplus-team/dcplusplus/trunk

Your team Dcplusplus-team is subscribed to branch lp:dcplusplus.
To unsubscribe from this branch go to https://code.launchpad.net/~dcplusplus-team/dcplusplus/trunk/+edit-subscription
=== modified file 'Compile.txt'
--- Compile.txt	2013-03-27 16:24:06 +0000
+++ Compile.txt	2013-04-22 22:31:37 +0000
@@ -184,3 +184,12 @@
 			many have done), and if you're lucky it might become more popular than the original =). Please
 			state explicitly when submitting the patch that you give me copyright over the code if the submission is larger 
 			than trivial.
+
+		f. Developing plugins
+
+			See <https://launchpad.net/dcpp-plugin-sdk-c> and
+			<https://launchpad.net/dcpp-plugin-sdk-cpp> for C and C++ plugin SDKs.
+
+			See dcpp/Plugin* files for implementation details.
+
+			The "Plugin format (dcext).txt" document describes the way plugins are packaged.

=== renamed file 'Extensions.txt' => 'NMDC extensions.txt'
=== added file 'Plugin format (dcext).txt'
--- Plugin format (dcext).txt	1970-01-01 00:00:00 +0000
+++ Plugin format (dcext).txt	2013-04-22 22:31:37 +0000
@@ -0,0 +1,63 @@
+This document describes the way DC plugins are packaged and distributed.
+
+More resources:
+- C SDK: <https://launchpad.net/dcpp-plugin-sdk-c>.
+- C++ SDK: <https://launchpad.net/dcpp-plugin-sdk-cpp>.
+- Implementation details: dcpp/Plugin* files.
+
+Alternative names for a DC plugin: DC extension, DC++ plugin, DC++ extension - stemming from the
+host-agnostic design of the DC plugin API.
+
+A DC plugin is, at the very least, a shared extension (.so Linux file, .dll Windows file) that
+defines a "pluginInit" function and handles basic events from the plugin API (ON_INSTALL, ON_LOAD,
+etc). It can then subscribe to interfaces such as DCHooks to catch more events.
+
+Shared extensions are fine for testing but impractical to distribute and to have users install.
+Therefore, a DC plugin is preferably packaged as a .dcext file.
+
+A .dcext file is an archive. Currently, it is required to be a tar file, either uncompressed or
+compressed with bzip2 or gzip. This may be expanded in the future if needed.
+
+That archive must contain an XML file named "info.xml" at its root, whose contents shall validate
+against the schemas/dcext.xsd schema.
+
+Description of the XML tags:
+
+- "dcext" (compulsory): Root tag.
+
+- "UUID" (compulsory): UUID to uniquely identify this plugin.
+- "Name" (compulsory): Friendly name of the plugin.
+- "Version" (compulsory): Version of the plugin; used for updates.
+- "ApiVersion" (compulsory): Plugin API version the plugin has been built with.
+
+- "Author" (optional): Author of the plugin.
+- "Description" (optional): Short description of the plugin.
+- "Website" (optional): Plugin website.
+
+- "Plugin" (compulsory): Location of the loadable shared extension within the archive. The optional
+	"Arch" attribute of this tag designates the architecture this plugin has been compiled for. It
+	may be one of "x86", "x64"; "x86" is assumed by default in the absence of this attribute.
+	Multiple "Plugin" tags may be provided for different architectures.
+- "Files" (optional): Additional files required by the plugin, each within a "File" tag. "File"
+	tags may contain an "Arch" attribute to specify the architecture the file works on; files are
+	assumed to target every architecture by default.
+
+Example info.xml:
+
+<?xml version="1.0" encoding="utf-8" standalone="yes"?>
+<dcext>
+	<UUID>{f62ed829-def5-4332-a0d7-84d2ec692006}</UUID>
+	<Name>Test plugin</Name>
+	<Version>2.3</Version>
+	<ApiVersion>6</ApiVersion>
+	<Author>Test team</Author>
+	<Description>Plugin to do X</Description>
+	<Website>http://example.com</Website>
+	<Plugin Arch="x86">x86/TestPlugin.so</Plugin>
+	<Plugin Arch="x64">x64/TestPlugin.so</Plugin>
+	<Files>
+		<File>icons/TestPlugin.ico</File>
+		<File>fonts/cool.font</File>
+		<File Arch="x64">FasterHash.so</File>
+	</Files>
+</dcext>

=== modified file 'changelog.txt'
--- changelog.txt	2013-04-18 19:46:26 +0000
+++ changelog.txt	2013-04-22 22:31:37 +0000
@@ -8,6 +8,7 @@
 * [L#190964] Handle more connection errors (poy)
 * Support city-level GeoIP databases - new params such as %[city] (poy)
 * Distribute an x64 version
+* Package plugins as .dcext files (poy)
 
 -- 0.811 2013-03-04 --
 * Fix status bar parts when the window is too small (poy)

=== modified file 'dcpp/Archive.cpp'
--- dcpp/Archive.cpp	2013-04-21 17:39:36 +0000
+++ dcpp/Archive.cpp	2013-04-22 22:31:37 +0000
@@ -60,25 +60,26 @@
 }
 
 void Archive::extract(const string& path) {
-	dcassert(!path.empty() && (*(path.end() - 1) == '/' || *(path.end() - 1) == '\\'));
+	auto isDir = [](const string& path) { return *(path.end() - 1) == '/' || *(path.end() - 1) == '\\'; };
+
+	dcassert(!path.empty() && isDir(path));
 
 	::archive_entry* entry;
-	while(true) {
-		if(check(archive_read_next_header(a, &entry)) == ARCHIVE_EOF) {
-			break;
+	while(check(archive_read_next_header(a, &entry)) != ARCHIVE_EOF) {
+
+		string path_out(archive_entry_pathname(entry));
+		if(path_out.empty() || isDir(path_out)) {
+			continue;
 		}
+		path_out = path + path_out;
 
-		auto path_out = path + archive_entry_pathname(entry);
 		File::ensureDirectory(path_out);
 		File f_out(path_out, File::WRITE, File::CREATE | File::TRUNCATE);
 
 		const void* buf;
 		size_t size;
 		__LA_INT64_T offset;
-		while(true) {
-			if(check(archive_read_data_block(a, &buf, &size, &offset)) == ARCHIVE_EOF) {
-				break;
-			}
+		while(check(archive_read_data_block(a, &buf, &size, &offset)) != ARCHIVE_EOF) {
 			f_out.write(buf, size);
 		}
 	}

=== modified file 'dcpp/Archive.h'
--- dcpp/Archive.h	2013-04-21 17:39:36 +0000
+++ dcpp/Archive.h	2013-04-22 22:31:37 +0000
@@ -33,6 +33,7 @@
 	Archive(const string& path);
 	~Archive();
 
+	/** Extract all the files in the archive to the specified directory. Throws on errors. */
 	void extract(const string& path);
 
 private:

=== modified file 'dcpp/PluginManager.cpp'
--- dcpp/PluginManager.cpp	2013-04-21 17:35:50 +0000
+++ dcpp/PluginManager.cpp	2013-04-22 22:31:37 +0000
@@ -19,9 +19,11 @@
 #include "stdinc.h"
 #include "PluginManager.h"
 
+#include "Archive.h"
 #include "Client.h"
 #include "ClientManager.h"
 #include "ConnectionManager.h"
+#include "File.h"
 #include "LogManager.h"
 #include "QueueManager.h"
 #include "SimpleXML.h"
@@ -68,6 +70,109 @@
 PluginManager::~PluginManager() {
 }
 
+DcextInfo PluginManager::extract(const string& path) {
+	const auto dir = Util::getTempPath() + "dcext" PATH_SEPARATOR_STR;
+	const auto info_path = dir + "info.xml";
+	File::deleteFile(info_path);
+	Archive(path).extract(dir);
+
+	SimpleXML xml;
+	xml.fromXML(File(info_path, File::READ, File::OPEN).read());
+
+	DcextInfo info;
+
+	if(xml.findChild("dcext")) {
+		xml.stepIn();
+
+		auto parse = [&xml](string tag, string& out) {
+			xml.resetCurrentChild();
+			if(xml.findChild(tag)) {
+				out = xml.getChildData();
+			}
+		};
+
+		auto checkArch = [&xml] {
+			auto arch = xml.getChildAttrib("Arch", "x86");
+#if defined(__x86_64__) || defined(_WIN64)
+			return arch == "x64";
+#elif defined(__i386__) || defined(_M_IX86)
+			return arch == "x86";
+#else
+#error Unknown architecture
+#endif
+		};
+
+		string version;
+		parse("ApiVersion", version);
+		if(Util::toInt(version) < DCAPI_CORE_VER) {
+			throw str(F_("%1% is too old, contact the plugin author for an update") % Util::getFileName(path));
+		}
+
+		parse("UUID", info.uuid);
+		parse("Name", info.name);
+		version.clear(); parse("Version", version); info.version = Util::toDouble(version);
+		parse("Author", info.author);
+		parse("Description", info.description);
+		parse("Website", info.website);
+
+		xml.resetCurrentChild();
+		while(xml.findChild("Plugin")) {
+			if(checkArch()) {
+				info.plugin = xml.getChildData();
+			}
+		}
+
+		xml.resetCurrentChild();
+		if(xml.findChild("Files")) {
+			xml.stepIn();
+
+			while(xml.findChild("File")) {
+				if(checkArch()) {
+					info.files.push_back(xml.getChildData());
+				}
+			}
+
+			xml.stepOut();
+		}
+
+		xml.stepOut();
+	}
+
+	if(info.uuid.empty() || info.name.empty() || info.version == 0 || info.plugin.empty()) {
+		throw str(F_("%1% is not a valid DC extension") % Util::getFileName(path));
+	}
+
+	{
+		Lock l(cs);
+
+		if(isLoaded(info.uuid)) {
+			throw str(F_("%1% is already installed") % Util::getFileName(path));
+		}
+	}
+
+	return info;
+}
+
+void PluginManager::install(const string& name, const string& plugin, const StringList& files) {
+	if(name.empty() || plugin.empty()) {
+		throw Exception();
+	}
+
+	const auto source = Util::getTempPath() + "dcext" PATH_SEPARATOR_STR;
+	const auto target = Util::getPath(Util::PATH_USER_LOCAL) + "Plugins" PATH_SEPARATOR_STR + name + PATH_SEPARATOR_STR;
+	const auto lib = target + Util::getFileName(plugin);
+
+	File::ensureDirectory(lib);
+	File::renameFile(source + plugin, lib);
+
+	for(auto& file: files) {
+		File::ensureDirectory(target + file);
+		File::renameFile(source + file, target + file);
+	}
+
+	loadPlugin(lib, [](const string& err) { throw Exception(err); }, true);
+}
+
 void PluginManager::loadPlugins(function<void (const string&)> f) {
 	TimerManager::getInstance()->addListener(this);
 	ClientManager::getInstance()->addListener(this);

=== modified file 'dcpp/PluginManager.h'
--- dcpp/PluginManager.h	2013-04-21 17:35:50 +0000
+++ dcpp/PluginManager.h	2013-04-22 22:31:37 +0000
@@ -87,6 +87,18 @@
 	PluginHandle handle;
 };
 
+/** Information about a dcext-packaged plugin that has just been extracted. */
+struct DcextInfo {
+	string uuid;
+	string name;
+	double version;
+	string author;
+	string description;
+	string website;
+	string plugin;
+	StringList files;
+};
+
 class PluginManager : public Singleton<PluginManager>, private TimerManagerListener,
 	private ClientManagerListener, private QueueManagerListener, private SettingsManagerListener
 {
@@ -94,6 +106,10 @@
 	PluginManager();
 	~PluginManager();
 
+	/** Extract a dcext-packaged plugin. Throws on errors. */
+	DcextInfo extract(const string& path);
+	void install(const string& name, const string& plugin, const StringList& files);
+
 	void loadPlugins(function<void (const string&)> f);
 	bool loadPlugin(const string& fileName, function<void (const string&)> err, bool install = false);
 	bool isLoaded(const string& guid);

=== modified file 'dcpp/SettingsManager.cpp'
--- dcpp/SettingsManager.cpp	2013-04-16 16:11:50 +0000
+++ dcpp/SettingsManager.cpp	2013-04-22 22:31:37 +0000
@@ -92,7 +92,7 @@
 	"AwayCompLock", "AwayTimeStamp", "BoldFinishedDownloads", "BoldFinishedUploads", "BoldFL",
 	"BoldHub", "BoldPm", "BoldQueue", "BoldSearch", "BoldSystemLog", "ClearSearch",
 	"CompressTransfers", "ConfirmADLSRemoval", "ConfirmExit", "ConfirmHubClosing",
-	"ConfirmHubRemoval", "ConfirmItemRemoval", "ConfirmUserRemoval", "Coral",
+	"ConfirmHubRemoval", "ConfirmItemRemoval", "ConfirmUserRemoval", "Coral", "DcextRegister",
 	"DontDlAlreadyQueued", "DontDLAlreadyShared", "FavShowJoins", "FilterMessages",
 	"FinishedDLOnlyFull", "FollowLinks", "GeoCity", "GetUserCountry", "GetUserInfo",
 	"HubUserCommands", "IgnoreBotPms", "IgnoreHubPms", "OpenNewWindow", "KeepFinishedFiles",
@@ -250,6 +250,7 @@
 	setDefault(POPUNDER_PM, false);
 	setDefault(POPUNDER_FILELIST, false);
 	setDefault(MAGNET_REGISTER, true);
+	setDefault(DCEXT_REGISTER, true);
 	setDefault(MAGNET_ASK, true);
 	setDefault(MAGNET_ACTION, MAGNET_AUTO_SEARCH);
 	setDefault(ADD_FINISHED_INSTANTLY, false);

=== modified file 'dcpp/SettingsManager.h'
--- dcpp/SettingsManager.h	2013-04-16 16:11:50 +0000
+++ dcpp/SettingsManager.h	2013-04-22 22:31:37 +0000
@@ -129,7 +129,7 @@
 		AWAY_COMP_LOCK, AWAY_TIMESTAMP, BOLD_FINISHED_DOWNLOADS, BOLD_FINISHED_UPLOADS, BOLD_FL,
 		BOLD_HUB, BOLD_PM, BOLD_QUEUE, BOLD_SEARCH, BOLD_SYSTEM_LOG, CLEAR_SEARCH,
 		COMPRESS_TRANSFERS, CONFIRM_ADLS_REMOVAL, CONFIRM_EXIT, CONFIRM_HUB_CLOSING,
-		CONFIRM_HUB_REMOVAL, CONFIRM_ITEM_REMOVAL, CONFIRM_USER_REMOVAL, CORAL,
+		CONFIRM_HUB_REMOVAL, CONFIRM_ITEM_REMOVAL, CONFIRM_USER_REMOVAL, CORAL, DCEXT_REGISTER,
 		DONT_DL_ALREADY_QUEUED, DONT_DL_ALREADY_SHARED, FAV_SHOW_JOINS, FILTER_MESSAGES,
 		FINISHED_DL_ONLY_FULL, FOLLOW_LINKS, GEO_CITY, GET_USER_COUNTRY, GET_USER_INFO,
 		HUB_USER_COMMANDS, IGNORE_BOT_PMS, IGNORE_HUB_PMS, JOIN_OPEN_NEW_WINDOW, KEEP_FINISHED_FILES,

=== modified file 'help/settings_advanced.html'
--- help/settings_advanced.html	2013-04-16 16:11:50 +0000
+++ help/settings_advanced.html	2013-04-22 22:31:37 +0000
@@ -39,7 +39,14 @@
 information appropriate for the Direct Connect network.
 Disabling will make DC++ remove the key from
 the Windows registry.
-  </dd>
+</dd>
+
+<dt>Register with Windows to handle .dcext files</dt>
+<dd cshelp="IDH_SETTINGS_ADVANCED_DCEXT_REGISTER">
+Associate DC++ with the .dcext file extension in Windows. Those files are plugins that can be
+installed into DC++ to extend its functionality.
+</dd>
+
   <dt>Don't delete file lists when exiting</dt>
   <dd cshelp="IDH_SETTINGS_ADVANCED_KEEP_LISTS">If this option is disabled, DC++ will delete the contents of the
 file list directory, where file lists are stored, when

=== modified file 'win32/AdvancedPage.cpp'
--- win32/AdvancedPage.cpp	2013-04-16 16:11:50 +0000
+++ win32/AdvancedPage.cpp	2013-04-22 22:31:37 +0000
@@ -34,6 +34,7 @@
 	{ SettingsManager::LIST_DUPES, N_("Keep duplicate files in your file list"), IDH_SETTINGS_ADVANCED_LIST_DUPES },
 	{ SettingsManager::URL_HANDLER, N_("Register with Windows to handle dchub://, adc:// and adcs:// URL links"), IDH_SETTINGS_ADVANCED_URL_HANDLER },
 	{ SettingsManager::MAGNET_REGISTER, N_("Register with Windows to handle magnet: URI links"), IDH_SETTINGS_ADVANCED_MAGNET_REGISTER },
+	{ SettingsManager::DCEXT_REGISTER, N_("Register with Windows to handle .dcext files"), IDH_SETTINGS_ADVANCED_DCEXT_REGISTER },
 	{ SettingsManager::KEEP_LISTS, N_("Don't delete file lists when exiting"), IDH_SETTINGS_ADVANCED_KEEP_LISTS },
 	{ SettingsManager::AUTO_KICK, N_("Automatically disconnect users who leave the hub"), IDH_SETTINGS_ADVANCED_AUTO_KICK },
 	{ SettingsManager::SFV_CHECK, N_("Enable automatic SFV checking"), IDH_SETTINGS_ADVANCED_SFV_CHECK },

=== modified file 'win32/MainWindow.cpp'
--- win32/MainWindow.cpp	2013-04-16 16:11:50 +0000
+++ win32/MainWindow.cpp	2013-04-22 22:31:37 +0000
@@ -53,30 +53,29 @@
 #include <dwt/widgets/SplitterContainer.h>
 #include <dwt/widgets/ToolBar.h>
 
-#include "resource.h"
-
+#include "AboutDlg.h"
+#include "ADLSearchFrame.h"
 #include "CrashLogger.h"
-#include "ParamDlg.h"
-#include "HashProgressDlg.h"
-#include "SettingsDialog.h"
-#include "TextFrame.h"
-#include "SingleInstance.h"
-#include "AboutDlg.h"
-#include "TransferView.h"
-#include "HubFrame.h"
-#include "PrivateFrame.h"
 #include "DirectoryListingFrame.h"
-#include "SearchFrame.h"
-#include "ADLSearchFrame.h"
 #include "FavHubsFrame.h"
 #include "FinishedDLFrame.h"
 #include "FinishedULFrame.h"
+#include "HashProgressDlg.h"
+#include "HubFrame.h"
 #include "NotepadFrame.h"
+#include "ParamDlg.h"
+#include "PluginInfoDlg.h"
+#include "PrivateFrame.h"
 #include "PublicHubsFrame.h"
 #include "QueueFrame.h"
+#include "resource.h"
 #include "SearchFrame.h"
+#include "SettingsDialog.h"
+#include "SingleInstance.h"
 #include "StatsFrame.h"
 #include "SystemFrame.h"
+#include "TextFrame.h"
+#include "TransferView.h"
 #include "UsersFrame.h"
 
 #ifdef HAVE_HTMLHELP_H
@@ -1186,6 +1185,7 @@
 	auto prevSortFavUsersFirst = SETTING(SORT_FAVUSERS_FIRST);
 	auto prevURLReg = SETTING(URL_HANDLER);
 	auto prevMagnetReg = SETTING(MAGNET_REGISTER);
+	auto prevDcextReg = SETTING(DCEXT_REGISTER);
 	auto prevSettingsSave = SETTING(SETTINGS_SAVE_INTERVAL);
 
 	if(SettingsDialog(this).run() == IDOK) {
@@ -1261,6 +1261,8 @@
 			WinUtil::registerHubHandlers();
 		if(SETTING(MAGNET_REGISTER) != prevMagnetReg)
 			WinUtil::registerMagnetHandler();
+		if(SETTING(DCEXT_REGISTER) != prevDcextReg)
+			WinUtil::registerDcextHandler();
 
 		if(SETTING(SETTINGS_SAVE_INTERVAL) != prevSettingsSave)
 			setSaveTimer();
@@ -1532,21 +1534,27 @@
 	}
 }
 
-void MainWindow::parseCommandLine(const tstring& cmdLine)
-{
-	string::size_type i;
-
-	if( (i = cmdLine.find(_T("dchub://"))) != string::npos ||
-		(i = cmdLine.find(_T("adc://"))) != string::npos ||
-		(i = cmdLine.find(_T("adcs://"))) != string::npos ||
-		(i = cmdLine.find(_T("magnet:?"))) != string::npos )
+void MainWindow::parseCommandLine(const tstring& cmdLine) {
+	// this string may or may not contain the executable's path at the beginning.
+
+	tstring::size_type i;
+
+	if( (i = cmdLine.find(_T("dchub://"))) != tstring::npos ||
+		(i = cmdLine.find(_T("adc://"))) != tstring::npos ||
+		(i = cmdLine.find(_T("adcs://"))) != tstring::npos ||
+		(i = cmdLine.find(_T("magnet:?"))) != tstring::npos )
 	{
 		WinUtil::parseLink(cmdLine.substr(i), false);
+
+	} else if((i = cmdLine.find(_T("dcext:"))) != tstring::npos) {
+		auto path = Text::fromT(cmdLine.substr(i + 6));
+		Util::sanitizeUrl(path);
+		PluginInfoDlg(this, path).run();
 	}
 }
 
 LRESULT MainWindow::handleCopyData(LPARAM lParam) {
-	parseCommandLine(dwt::Application::instance().getModuleFileName() + _T(" ") + reinterpret_cast<LPCTSTR>(reinterpret_cast<COPYDATASTRUCT*>(lParam)->lpData));
+	parseCommandLine(reinterpret_cast<LPCTSTR>(reinterpret_cast<COPYDATASTRUCT*>(lParam)->lpData));
 	return TRUE;
 }
 

=== added file 'win32/PluginInfoDlg.cpp'
--- win32/PluginInfoDlg.cpp	1970-01-01 00:00:00 +0000
+++ win32/PluginInfoDlg.cpp	2013-04-22 22:31:37 +0000
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2001-2013 Jacek Sieka, arnetheduck on gmail point com
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+#include "stdafx.h"
+#include "PluginInfoDlg.h"
+
+#include <dcpp/PluginManager.h>
+
+#include <dwt/widgets/Grid.h>
+#include <dwt/widgets/Label.h>
+#include <dwt/widgets/Link.h>
+#include <dwt/widgets/MessageBox.h>
+
+#include "WinUtil.h"
+
+using dwt::Grid;
+using dwt::GridInfo;
+using dwt::Label;
+using dwt::Link;
+
+PluginInfoDlg::PluginInfoDlg(dwt::Widget* parent, const string& path) :
+dwt::ModalDialog(parent),
+grid(0)
+{
+	onInitDialog([this, path] { return handleInitDialog(path); });
+}
+
+PluginInfoDlg::~PluginInfoDlg() {
+}
+
+int PluginInfoDlg::run() {
+	create(dwt::Point(400, 300));
+	return show();
+}
+
+bool PluginInfoDlg::handleInitDialog(const string& path) {
+	grid = addChild(Grid::Seed(0, 2));
+	grid->column(1).mode = GridInfo::FILL;
+	grid->setSpacing(6);
+
+	DcextInfo info;
+	try {
+		info = PluginManager::getInstance()->extract(path);
+
+	} catch(const Exception& e) {
+		resize(dwt::Rectangle());
+		auto err = Text::toT(e.getError());
+		callAsync([this, err, path] {
+			error(err, Text::toT(Util::getFileName(path)));
+			endDialog(IDCANCEL);
+		});
+		return true;
+	}
+
+	// similar to PluginPage.cpp
+
+	enum Type { Name, Version, Description, Author, Website };
+
+	auto addInfo = [this](tstring name, const string& value, Type type) {
+		if(type == Description) {
+			grid->addRow(GridInfo(0, GridInfo::FILL, GridInfo::STRETCH));
+		} else {
+			grid->addRow();
+		}
+		grid->addChild(Label::Seed(name));
+		if(type == Website && !value.empty()) {
+			grid->addChild(Link::Seed(Text::toT(value), true));
+		} else {
+			grid->addChild(Label::Seed(value.empty() ?
+				T_("<Information unavailable>") : Text::toT(value)));
+		}
+	};
+
+	addInfo(T_("Name: "), info.name, Name);
+	addInfo(T_("Version: "), Util::toString(info.version), Version);
+	addInfo(T_("Description: "), info.description, Description);
+	addInfo(T_("Author: "), info.author, Author);
+	addInfo(T_("Website: "), info.website, Website);
+
+	{
+		grid->addRow();
+		auto cur = grid->addChild(Grid::Seed(1, 2));
+		grid->setWidget(cur, grid->rowCount() - 1, 0, 1, 2);
+		cur->column(0).mode = GridInfo::FILL;
+		cur->column(0).align = GridInfo::BOTTOM_RIGHT;
+		cur->setSpacing(grid->getSpacing());
+		WinUtil::addDlgButtons(cur,
+			[this, info] { handleOK(info.name, info.plugin, info.files); },
+			[this] { endDialog(IDCANCEL); }).first->setText(T_("Install the plugin"));
+	}
+
+	setText(T_("Adding a plugin"));
+
+	layout();
+	centerWindow();
+
+	return false;
+}
+
+void PluginInfoDlg::handleOK(const string& name, const string& plugin, const StringList& files) {
+	try {
+		PluginManager::getInstance()->install(name, plugin, files);
+		endDialog(IDOK);
+
+	} catch(const Exception& e) {
+		error(Text::toT(e.getError()), Text::toT(name));
+		endDialog(IDCANCEL);
+	}
+}
+
+void PluginInfoDlg::layout() {
+	dwt::Point sz = getClientSize();
+	grid->resize(dwt::Rectangle(3, 3, sz.x - 6, sz.y - 6));
+}
+
+void PluginInfoDlg::error(const tstring& message, const tstring& title) {
+	dwt::MessageBox(this).show(tstring(T_("Cannot install the plugin:")) + _T("\r\n\r\n") + message, title,
+		dwt::MessageBox::BOX_OK, dwt::MessageBox::BOX_ICONSTOP);
+}

=== added file 'win32/PluginInfoDlg.h'
--- win32/PluginInfoDlg.h	1970-01-01 00:00:00 +0000
+++ win32/PluginInfoDlg.h	2013-04-22 22:31:37 +0000
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2001-2013 Jacek Sieka, arnetheduck on gmail point com
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+#ifndef DCPLUSPLUS_WIN32_PLUGININFODLG_H
+#define DCPLUSPLUS_WIN32_PLUGININFODLG_H
+
+#include <dcpp/typedefs.h>
+
+#include <dwt/widgets/ModalDialog.h>
+
+#include "forward.h"
+
+class PluginInfoDlg : public dwt::ModalDialog
+{
+public:
+	PluginInfoDlg(dwt::Widget* parent, const string& path);
+	virtual ~PluginInfoDlg();
+
+	int run();
+
+private:
+	bool handleInitDialog(const string& path);
+	void handleOK(const string& name, const string& plugin, const StringList& files);
+
+	void layout();
+
+	void error(const tstring& message, const tstring& title);
+
+	GridPtr grid;
+};
+
+#endif

=== modified file 'win32/PluginPage.cpp'
--- win32/PluginPage.cpp	2013-03-16 14:57:07 +0000
+++ win32/PluginPage.cpp	2013-04-22 22:31:37 +0000
@@ -21,6 +21,7 @@
 
 #include "HoldRedraw.h"
 #include "resource.h"
+#include "PluginInfoDlg.h"
 #include "WinUtil.h"
 
 #include <dwt/widgets/Grid.h>
@@ -98,27 +99,22 @@
 
 				add = buttons->addChild(Button::Seed(T_("&Add")));
 				add->onClicked([this] { handleAddPlugin(); });
-				add->setImage(WinUtil::buttonIcon(IDI_OK));
 				add->setHelpId(IDH_SETTINGS_PLUGINS_ADD);
 
 				configure = buttons->addChild(Button::Seed(T_("&Configure")));
 				configure->onClicked([this] { handleConfigurePlugin(); });
-				configure->setImage(WinUtil::buttonIcon(IDI_SETTINGS));
 				configure->setHelpId(IDH_SETTINGS_PLUGINS_CONFIGURE);
 
-				moveUp = buttons->addChild(Button::Seed(T_("Move &up")));
+				moveUp = buttons->addChild(Button::Seed(T_("Move &Up")));
 				moveUp->onClicked([this] { handleMovePluginUp(); });
-				moveUp->setImage(WinUtil::buttonIcon(IDI_UPLOAD));
 				moveUp->setHelpId(IDH_SETTINGS_PLUGINS_MOVE_UP);
 
-				moveDown = buttons->addChild(Button::Seed(T_("Move &down")));
+				moveDown = buttons->addChild(Button::Seed(T_("Move &Down")));
 				moveDown->onClicked([this] { handleMovePluginDown(); });
-				moveDown->setImage(WinUtil::buttonIcon(IDI_DOWNLOAD));
 				moveDown->setHelpId(IDH_SETTINGS_PLUGINS_MOVE_DOWN);
 
 				remove = buttons->addChild(Button::Seed(T_("&Remove")));
 				remove->onClicked([this] { handleRemovePlugin(); });
-				remove->setImage(WinUtil::buttonIcon(IDI_CANCEL));
 				remove->setHelpId(IDH_SETTINGS_PLUGINS_REMOVE);
 			}
 	}
@@ -203,6 +199,8 @@
 	infoGrid->column(1).mode = GridInfo::FILL;
 	infoGrid->setSpacing(pluginInfo->getSpacing());
 
+	// similar to PluginInfoDlg.cpp
+
 	enum Type { Name, Version, Description, Author, Website };
 
 	auto addInfo = [this, infoGrid](tstring name, const string& value, Type type) {
@@ -230,44 +228,32 @@
 }
 
 bool PluginPage::handleContextMenu(dwt::ScreenCoordinate pt) {
-	if(plugins->countSelected() > 0) {
-		if(pt.x() == -1 && pt.y() == -1) {
-			pt = plugins->getContextMenuPos();
-		}
-		MenuPtr contextMenu = makeMenu();
-		contextMenu->open(pt);
-		return true;
+	if(pt.x() == -1 && pt.y() == -1) {
+		pt = plugins->getContextMenuPos();
 	}
-	return false;
-}
-
-MenuPtr PluginPage::makeMenu() {
-	MenuPtr menu = addChild(WinUtil::Seeds::menu);
-	
-	menu->setTitle(T_("Plugin options"), WinUtil::menuIcon(IDI_PLUGINS));
-	menu->appendItem(T_("Add plugin"), [this] { handleAddPlugin(); }, WinUtil::menuIcon(IDI_OK));
-	menu->appendItem(T_("Configure plugin"), [this] { handleConfigurePlugin(); }, WinUtil::menuIcon(IDI_SETTINGS));
-	menu->appendSeparator();
-	menu->appendItem(T_("Move plugin up"), [this] { handleMovePluginUp(); }, WinUtil::menuIcon(IDI_UPLOAD));
-	menu->appendItem(T_("Move plugin down"), [this] { handleMovePluginDown(); }, WinUtil::menuIcon(IDI_DOWNLOAD));
-	menu->appendSeparator();
-	menu->appendItem(T_("Remove plugin"), [this] { handleRemovePlugin(); }, WinUtil::menuIcon(IDI_CANCEL));
-	
-	return menu;
+
+	auto menu = addChild(WinUtil::Seeds::menu);
+
+	menu->setTitle(T_("Plugins"), WinUtil::menuIcon(IDI_PLUGINS));
+	menu->appendItem(T_("&Add"), [this] { handleAddPlugin(); });
+	menu->appendItem(T_("&Configure"), [this] { handleConfigurePlugin(); });
+	menu->appendSeparator();
+	menu->appendItem(T_("Move &Up"), [this] { handleMovePluginUp(); });
+	menu->appendItem(T_("Move &Down"), [this] { handleMovePluginDown(); });
+	menu->appendSeparator();
+	menu->appendItem(T_("&Remove"), [this] { handleRemovePlugin(); });
+
+	menu->open(pt);
+	return true;
 }
 
 void PluginPage::handleAddPlugin() {
 	tstring path;
-	if(LoadDialog(this).addFilter(T_("DLL files"), _T("*.dll"))
-		.setInitialDirectory(Text::toT(Util::getPath(Util::PATH_GLOBAL_CONFIG) + "Plugins")).open(path))
+	if(LoadDialog(this).addFilter(T_("dcext files"), _T("*.dcext")).open(path) &&
+		PluginInfoDlg(this, Text::fromT(path)).run() == IDOK)
 	{
-		auto idx = plugins->size();
-		if(PluginManager::getInstance()->loadPlugin(Text::fromT(path), [this, &path](const string& str) {
-			dwt::MessageBox(this).show(Text::toT(str), Text::toT(Util::getFileName(Text::fromT(path))),
-				dwt::MessageBox::BOX_OK, dwt::MessageBox::BOX_ICONSTOP);
-		}, true)) {
-			addEntry(idx, PluginManager::getInstance()->getPlugin(idx)->getInfo());
-		}
+		auto pos = plugins->size();
+		addEntry(pos, PluginManager::getInstance()->getPlugin(pos)->getInfo());
 	}
 }
 
@@ -314,6 +300,9 @@
 }
 
 void PluginPage::handleRemovePlugin() {
+	if(plugins->countSelected() != 1)
+		return;
+
 	auto sel = plugins->getSelected();
 	PluginManager::getInstance()->unloadPlugin(sel);
 	plugins->erase(sel);

=== modified file 'win32/PluginPage.h'
--- win32/PluginPage.h	2013-01-18 21:28:38 +0000
+++ win32/PluginPage.h	2013-04-22 22:31:37 +0000
@@ -42,7 +42,6 @@
 	void handleSelectionChanged();
 
 	bool handleContextMenu(dwt::ScreenCoordinate pt);
-	MenuPtr makeMenu();
 
 	void handleAddPlugin();
 	void handleConfigurePlugin();

=== modified file 'win32/WinUtil.cpp'
--- win32/WinUtil.cpp	2013-04-12 21:10:13 +0000
+++ win32/WinUtil.cpp	2013-04-22 22:31:37 +0000
@@ -114,6 +114,7 @@
 MainWindow* WinUtil::mainWindow = 0;
 bool WinUtil::urlDcADCRegistered = false;
 bool WinUtil::urlMagnetRegistered = false;
+bool WinUtil::dcextRegistered = false;
 DWORD WinUtil::helpCookie = 0;
 tstring WinUtil::helpPath;
 StringList WinUtil::helpTexts;
@@ -231,6 +232,7 @@
 
 	registerHubHandlers();
 	registerMagnetHandler();
+	registerDcextHandler();
 
 	initHelpPath();
 
@@ -1295,7 +1297,13 @@
 	return RGB(r, g, b);
 }
 
-bool registerHandler_(const tstring& name) {
+namespace {
+
+void regChanged() {
+	::SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, nullptr, nullptr);
+}
+
+bool registerHandler_(const tstring& name, const tstring& descr, bool url, const tstring& prefix) {
 	HKEY hk;
 	TCHAR Buf[512];
 	Buf[0] = 0;
@@ -1309,7 +1317,7 @@
 		::RegCloseKey(hk);
 	}
 
-	tstring app = _T("\"") + dwt::Application::instance().getModuleFileName() + _T("\" \"%1\"");
+	tstring app = _T("\"") + dwt::Application::instance().getModuleFileName() + _T("\" \"") + prefix + _T("%1\"");
 	if(Util::stricmp(app.c_str(), Buf) == 0) {
 		// already registered to us
 		return true;
@@ -1317,16 +1325,21 @@
 
 	if(::RegCreateKeyEx(HKEY_CURRENT_USER, (_T("Software\\Classes\\") + name).c_str(),
 		0, NULL, REG_OPTION_NON_VOLATILE, KEY_WRITE, NULL, &hk, NULL) != ERROR_SUCCESS)
+	{
 		return false;
+	}
 
-	TCHAR tmp[] = _T("URL:Direct Connect Protocol");
-	::RegSetValueEx(hk, NULL, 0, REG_SZ, (LPBYTE) tmp, sizeof(TCHAR) * (_tcslen(tmp) + 1));
-	::RegSetValueEx(hk, _T("URL Protocol"), 0, REG_SZ, (LPBYTE) _T(""), sizeof(TCHAR));
+	::RegSetValueEx(hk, NULL, 0, REG_SZ, (LPBYTE) descr.c_str(), sizeof(TCHAR) * (descr.size() + 1));
+	if(url) {
+		::RegSetValueEx(hk, _T("URL Protocol"), 0, REG_SZ, (LPBYTE) _T(""), sizeof(TCHAR));
+	}
 	::RegCloseKey(hk);
 
 	if(::RegCreateKeyEx(HKEY_CURRENT_USER, (_T("Software\\Classes\\") + name + _T("\\Shell\\Open\\Command")).c_str(),
 		0, NULL, REG_OPTION_NON_VOLATILE, KEY_WRITE, NULL, &hk, NULL) != ERROR_SUCCESS)
+	{
 		return false;
+	}
 	bool ret = ::RegSetValueEx(hk, _T(""), 0, REG_SZ, (LPBYTE) app.c_str(), sizeof(TCHAR) * (app.length() + 1)) == ERROR_SUCCESS;
 	::RegCloseKey(hk);
 
@@ -1341,33 +1354,54 @@
 	return ret;
 }
 
-bool registerHandler(const tstring& name) {
-	bool ret = registerHandler_(name);
-	if(!ret)
-		LogManager::getInstance()->message(str(F_("Error registering %1%:// link handler") % Text::fromT(name)));
+/// @todo var template
+bool registerHandler(const tstring& name, const tstring& description, bool url, const tstring& prefix = Util::emptyStringT) {
+	bool ret = registerHandler_(name, description, url, prefix);
+	if(ret) {
+		regChanged();
+	} else {
+		LogManager::getInstance()->message(str(F_("Error registering the %1% handler") % Text::fromT(name)));
+	}
 	return ret;
 }
 
+} // unnamed namespace
+
 void WinUtil::registerHubHandlers() {
 	if(SETTING(URL_HANDLER)) {
 		if(!urlDcADCRegistered) {
-			urlDcADCRegistered = registerHandler(_T("dchub")) && registerHandler(_T("adc")) && registerHandler(_T("adcs"));
+			urlDcADCRegistered = registerHandler(_T("dchub"), _T("URL:Direct Connect Protocol"), true) &&
+				registerHandler(_T("adc"), _T("URL:Direct Connect Protocol"), true) &&
+				registerHandler(_T("adcs"), _T("URL:Direct Connect Protocol"), true);
 		}
 	} else if(urlDcADCRegistered) {
-		urlDcADCRegistered = !(
-		(::SHDeleteKey(HKEY_CURRENT_USER, _T("Software\\Classes\\dchub")) == ERROR_SUCCESS) &&
-		(::SHDeleteKey(HKEY_CURRENT_USER, _T("Software\\Classes\\adc")) == ERROR_SUCCESS) &&
-		(::SHDeleteKey(HKEY_CURRENT_USER, _T("Software\\Classes\\adcs")) == ERROR_SUCCESS));
+		urlDcADCRegistered =
+			::SHDeleteKey(HKEY_CURRENT_USER, _T("Software\\Classes\\dchub")) != ERROR_SUCCESS ||
+			::SHDeleteKey(HKEY_CURRENT_USER, _T("Software\\Classes\\adc")) == ERROR_SUCCESS ||
+			::SHDeleteKey(HKEY_CURRENT_USER, _T("Software\\Classes\\adcs")) == ERROR_SUCCESS;
+		regChanged();
 	}
 }
 
 void WinUtil::registerMagnetHandler() {
 	if(SETTING(MAGNET_REGISTER)) {
 		if(!urlMagnetRegistered) {
-			urlMagnetRegistered = registerHandler(_T("magnet"));
+			urlMagnetRegistered = registerHandler(_T("magnet"), _T("URL:Magnet"), true);
 		}
 	} else if(urlMagnetRegistered) {
 		urlMagnetRegistered = ::SHDeleteKey(HKEY_CURRENT_USER, _T("Software\\Classes\\magnet")) != ERROR_SUCCESS;
+		regChanged();
+	}
+}
+
+void WinUtil::registerDcextHandler() {
+	if(SETTING(DCEXT_REGISTER)) {
+		if(!dcextRegistered) {
+			dcextRegistered = registerHandler(_T(".dcext"), _T("DC plugin"), false, _T("dcext:"));
+		}
+	} else if(dcextRegistered) {
+		dcextRegistered = ::SHDeleteKey(HKEY_CURRENT_USER, _T("Software\\Classes\\.dcext")) != ERROR_SUCCESS;
+		regChanged();
 	}
 }
 

=== modified file 'win32/WinUtil.h'
--- win32/WinUtil.h	2013-03-30 15:55:28 +0000
+++ win32/WinUtil.h	2013-04-22 22:31:37 +0000
@@ -298,9 +298,10 @@
 	static void killHelpTooltip();
 	static pair<bool, string> getHelpText(unsigned id);
 
-	// URL related
+	// file type & protocol associations
 	static void registerHubHandlers();
 	static void registerMagnetHandler();
+	static void registerDcextHandler();
 
 	static void addUserItems(Menu* menu, const HintedUserList& users, TabViewPtr parent, const StringList& dirs = StringList());
 
@@ -323,6 +324,7 @@
 
 	static bool urlDcADCRegistered;
 	static bool urlMagnetRegistered;
+	static bool dcextRegistered;
 };
 
 #endif // !defined(WIN_UTIL_H)