257 lines
8.7 KiB
C++
257 lines
8.7 KiB
C++
// Copyright 2021-2023 The Mumble Developers. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license
|
|
// that can be found in the LICENSE file at the root of the
|
|
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
|
|
|
|
#include "PluginInstaller.h"
|
|
#include "PluginManager.h"
|
|
#include "PluginManifest.h"
|
|
#include "Global.h"
|
|
|
|
#include <QMessageBox>
|
|
#include <QtCore/QDir>
|
|
#include <QtCore/QException>
|
|
#include <QtCore/QObject>
|
|
#include <QtCore/QString>
|
|
#include <QtCore/QStringList>
|
|
|
|
#include <QtGui/QIcon>
|
|
|
|
#include <exception>
|
|
#include <fstream>
|
|
#include <string>
|
|
|
|
#include <Poco/Exception.h>
|
|
#include <Poco/FileStream.h>
|
|
#include <Poco/StreamCopier.h>
|
|
#include <Poco/Zip/ZipArchive.h>
|
|
#include <Poco/Zip/ZipStream.h>
|
|
|
|
PluginInstallException::PluginInstallException(const QString &msg) : m_msg(msg) {
|
|
}
|
|
|
|
QString PluginInstallException::getMessage() const {
|
|
return m_msg;
|
|
}
|
|
|
|
const QString PluginInstaller::pluginFileExtension = QLatin1String("mumble_plugin");
|
|
|
|
bool PluginInstaller::canBePluginFile(const QFileInfo &fileInfo) noexcept {
|
|
if (!fileInfo.isFile()) {
|
|
// A plugin file has to be a file (obviously)
|
|
return false;
|
|
}
|
|
|
|
if (fileInfo.suffix().compare(PluginInstaller::pluginFileExtension, Qt::CaseInsensitive) == 0) {
|
|
// A plugin file has the extension given in PluginInstaller::pluginFileExtension
|
|
return true;
|
|
}
|
|
|
|
// We might also accept a shared library directly
|
|
return QLibrary::isLibrary(fileInfo.fileName());
|
|
}
|
|
|
|
PluginInstaller::PluginInstaller(const QFileInfo &fileInfo, QWidget *p)
|
|
: QDialog(p), m_pluginArchive(fileInfo), m_plugin(nullptr), m_pluginSource(), m_pluginDestination(),
|
|
m_copyPlugin(false) {
|
|
setupUi(this);
|
|
|
|
setWindowIcon(QIcon(QLatin1String("skin:mumble.svg")));
|
|
|
|
QObject::connect(qpbYes, &QPushButton::clicked, this, &PluginInstaller::on_qpbYesClicked);
|
|
QObject::connect(qpbNo, &QPushButton::clicked, this, &PluginInstaller::on_qpbNoClicked);
|
|
|
|
init();
|
|
}
|
|
|
|
PluginInstaller::~PluginInstaller() {
|
|
if (m_plugin) {
|
|
delete m_plugin;
|
|
}
|
|
}
|
|
|
|
void PluginInstaller::init() {
|
|
if (!PluginInstaller::canBePluginFile(m_pluginArchive)) {
|
|
throw PluginInstallException(tr("The file \"%1\" is not a valid plugin file!").arg(m_pluginArchive.fileName()));
|
|
}
|
|
|
|
if (QLibrary::isLibrary(m_pluginArchive.fileName())) {
|
|
// For a library the fileInfo provided is already the actual plugin library
|
|
m_pluginSource = m_pluginArchive;
|
|
|
|
m_copyPlugin = true;
|
|
} else {
|
|
// We have been provided with a zip-file
|
|
try {
|
|
Poco::FileInputStream zipInput(m_pluginArchive.filePath().toStdString());
|
|
Poco::Zip::ZipArchive archive(zipInput);
|
|
auto manifestIt = archive.findHeader("manifest.xml");
|
|
|
|
if (manifestIt == archive.headerEnd()) {
|
|
throw PluginInstallException(tr("Unable to locate the plugin manifest (manifest.xml)"));
|
|
}
|
|
|
|
zipInput.clear();
|
|
Poco::Zip::ZipInputStream manifestStream(zipInput, manifestIt->second);
|
|
|
|
PluginManifest manifest;
|
|
try {
|
|
manifest.parse(manifestStream);
|
|
} catch (const PluginManifestException &e) {
|
|
throw PluginInstallException(
|
|
tr("Error while processing manifest: %1").arg(QString::fromUtf8(e.what())));
|
|
}
|
|
|
|
if (!manifest.specifiesPluginPath(MUMBLE_TARGET_OS, MUMBLE_TARGET_ARCH)) {
|
|
throw PluginInstallException(
|
|
tr("Unable to find plugin for the current OS (\"%1\") and architecture (\"%2\")")
|
|
.arg(QString::fromUtf8(MUMBLE_TARGET_OS))
|
|
.arg(QString::fromUtf8(MUMBLE_TARGET_ARCH)));
|
|
}
|
|
|
|
std::string pluginPath = manifest.getPluginPath(MUMBLE_TARGET_OS, MUMBLE_TARGET_ARCH);
|
|
|
|
// Unpack the plugin library into the tmp dir
|
|
// We don't have to create the directory structure as we're only interested in the library itself
|
|
QString tmpPluginPath = QDir::temp().filePath(QFileInfo(QString::fromStdString(pluginPath)).fileName());
|
|
auto pluginIt = archive.findHeader(pluginPath);
|
|
|
|
if (pluginIt == archive.headerEnd()) {
|
|
throw PluginInstallException(
|
|
tr("Unable to locate plugin library specified in manifest (\"%1\") in the bundle")
|
|
.arg(QString::fromStdString(pluginPath)));
|
|
}
|
|
|
|
zipInput.clear();
|
|
Poco::Zip::ZipInputStream zipin(zipInput, pluginIt->second);
|
|
std::ofstream out(tmpPluginPath.toStdString(), std::ios::out | std::ios::binary);
|
|
Poco::StreamCopier::copyStream(zipin, out);
|
|
|
|
m_pluginSource = QFileInfo(tmpPluginPath);
|
|
} catch (const Poco::Exception &e) {
|
|
// Something didn't work out during the Zip processing
|
|
throw PluginInstallException(
|
|
QString::fromStdString(std::string("Failed to process zip archive: ") + e.message()));
|
|
}
|
|
}
|
|
|
|
QString pluginFileName = m_pluginSource.fileName();
|
|
|
|
// Try to load the plugin up to see if it is actually valid
|
|
try {
|
|
m_plugin = Plugin::createNew< Plugin >(m_pluginSource.absoluteFilePath());
|
|
} catch (const PluginError &) {
|
|
throw PluginInstallException(
|
|
tr("Unable to load plugin \"%1\" - check the plugin interface!").arg(pluginFileName));
|
|
}
|
|
|
|
m_pluginDestination = QFileInfo(QString::fromLatin1("%1/%2").arg(getInstallDir()).arg(pluginFileName));
|
|
|
|
|
|
// Now that we located the plugin, it is time to fill in its details in the UI
|
|
qlName->setText(m_plugin->getName());
|
|
|
|
mumble_version_t pluginVersion = m_plugin->getVersion();
|
|
mumble_version_t usedAPIVersion = m_plugin->getAPIVersion();
|
|
qlVersion->setText(
|
|
QString::fromLatin1("%1 (API %2)")
|
|
.arg(pluginVersion == MUMBLE_VERSION_UNKNOWN ? "Unknown" : static_cast< QString >(pluginVersion))
|
|
.arg(usedAPIVersion == MUMBLE_VERSION_UNKNOWN ? "Unknown" : static_cast< QString >(usedAPIVersion)));
|
|
|
|
qlAuthor->setText(m_plugin->getAuthor());
|
|
|
|
qlDescription->setText(m_plugin->getDescription());
|
|
}
|
|
|
|
bool PluginInstaller::install() {
|
|
if (!m_plugin) {
|
|
// This function shouldn't even be called, if the plugin object has not been created...
|
|
throw PluginInstallException(QLatin1String("[INTERNAL ERROR]: Trying to install an invalid plugin"));
|
|
}
|
|
|
|
if (m_pluginSource == m_pluginDestination) {
|
|
// Apparently the plugin is already installed
|
|
return false;
|
|
}
|
|
|
|
if (m_pluginDestination.exists()) {
|
|
// This most likely means that we already have some version of this plugin installed. Figure out which
|
|
// one that is and ask the user for confirmation for overwriting it.
|
|
// NOTE: Because we currently have multiple install directories, this branch may not execute if the old
|
|
// version of the plugin was installed into a different directory than the current one.
|
|
const_plugin_ptr_t oldPlugin;
|
|
for (const const_plugin_ptr_t ¤tPlugin : Global::get().pluginManager->getPlugins()) {
|
|
if (currentPlugin->getFilePath() == m_pluginDestination.absoluteFilePath()) {
|
|
// This is the one
|
|
oldPlugin = currentPlugin;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (oldPlugin) {
|
|
QMessageBox::StandardButton result = QMessageBox::question(
|
|
this, tr("Overwrite plugin?"),
|
|
tr("The new plugin \"%1\" (%2) is about to overwrite the already installed plugin "
|
|
"\"%3\" (%4). Do you wish to proceed?")
|
|
.arg(m_plugin->getName())
|
|
.arg(QString(m_plugin->getVersion()))
|
|
.arg(oldPlugin->getName())
|
|
.arg(QString(oldPlugin->getVersion())));
|
|
|
|
if (result != QMessageBox::StandardButton::Yes) {
|
|
// Abort as the user did not specify that they want to proceed
|
|
return false;
|
|
}
|
|
|
|
// If we proceed we have to make sure the plugin is unloaded as otherwise Mumble
|
|
// could still hold a handle to the underlying library file which on some OS (e.g. Windows)
|
|
// prevents it from being deleted/overwritten.
|
|
Global::get().pluginManager->clearPlugin(oldPlugin->getID());
|
|
|
|
// We have to let go of our handle of the plugin here as well in order to make sure it actually
|
|
// gets deleted
|
|
oldPlugin.reset();
|
|
}
|
|
|
|
// Delete old version first
|
|
if (!QFile(m_pluginDestination.absoluteFilePath()).remove()) {
|
|
throw PluginInstallException(
|
|
tr("Unable to delete old plugin at \"%1\"").arg(m_pluginDestination.absoluteFilePath()));
|
|
}
|
|
}
|
|
|
|
if (m_copyPlugin) {
|
|
if (!QFile(m_pluginSource.absoluteFilePath()).copy(m_pluginDestination.absoluteFilePath())) {
|
|
throw PluginInstallException(tr("Unable to copy plugin library from \"%1\" to \"%2\"")
|
|
.arg(m_pluginSource.absoluteFilePath())
|
|
.arg(m_pluginDestination.absoluteFilePath()));
|
|
}
|
|
} else {
|
|
// Move the plugin into the respective dir
|
|
if (!QFile(m_pluginSource.absoluteFilePath()).rename(m_pluginDestination.absoluteFilePath())) {
|
|
throw PluginInstallException(
|
|
tr("Unable to move plugin library to \"%1\"").arg(m_pluginDestination.absoluteFilePath()));
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
QString PluginInstaller::getInstallDir() {
|
|
// Get the path to the plugin-dir in "user-land" (aka: the user definitely has write access to this
|
|
// location).
|
|
return Global::get().qdBasePath.absolutePath() + QLatin1String("/Plugins");
|
|
}
|
|
|
|
void PluginInstaller::on_qpbYesClicked() {
|
|
if (install()) {
|
|
accept();
|
|
} else {
|
|
close();
|
|
}
|
|
}
|
|
|
|
void PluginInstaller::on_qpbNoClicked() {
|
|
close();
|
|
}
|