381 lines
11 KiB
C++
381 lines
11 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 "PluginUpdater.h"
|
|
#include "Log.h"
|
|
#include "PluginInstaller.h"
|
|
#include "PluginManager.h"
|
|
#include "Global.h"
|
|
|
|
#include <QNetworkRequest>
|
|
#include <QtConcurrent>
|
|
#include <QtCore/QByteArray>
|
|
#include <QtCore/QDir>
|
|
#include <QtCore/QFile>
|
|
#include <QtCore/QHashIterator>
|
|
#include <QtCore/QSignalBlocker>
|
|
#include <QtWidgets/QCheckBox>
|
|
#include <QtWidgets/QLabel>
|
|
|
|
#include <algorithm>
|
|
|
|
PluginUpdater::PluginUpdater(QWidget *parent)
|
|
: QDialog(parent), m_wasInterrupted(false), m_dataMutex(), m_pluginsToUpdate(), m_networkManager(),
|
|
m_pluginUpdateWidgets() {
|
|
QObject::connect(&m_networkManager, &QNetworkAccessManager::finished, this, &PluginUpdater::on_updateDownloaded);
|
|
}
|
|
|
|
PluginUpdater::~PluginUpdater() {
|
|
m_wasInterrupted.store(true);
|
|
}
|
|
|
|
void PluginUpdater::checkForUpdates() {
|
|
// Dispatch a thread in which each plugin can check for updates
|
|
QtConcurrent::run([this]() {
|
|
QMutexLocker lock(&m_dataMutex);
|
|
|
|
const QVector< const_plugin_ptr_t > plugins = Global::get().pluginManager->getPlugins();
|
|
|
|
for (int i = 0; i < plugins.size(); i++) {
|
|
const_plugin_ptr_t plugin = plugins[i];
|
|
|
|
if (plugin->hasUpdate()) {
|
|
QUrl updateURL = plugin->getUpdateDownloadURL();
|
|
|
|
if (updateURL.isValid() && !updateURL.isEmpty() && !updateURL.fileName().isEmpty()) {
|
|
UpdateEntry entry = { plugin->getID(), updateURL, updateURL.fileName(), 0 };
|
|
m_pluginsToUpdate << entry;
|
|
}
|
|
}
|
|
|
|
// if the update has been asked to be interrupted, exit here
|
|
if (m_wasInterrupted.load()) {
|
|
emit updateInterrupted();
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!m_pluginsToUpdate.isEmpty()) {
|
|
emit updatesAvailable();
|
|
}
|
|
});
|
|
}
|
|
|
|
void PluginUpdater::promptAndUpdate() {
|
|
setupUi(this);
|
|
populateUI();
|
|
|
|
setWindowIcon(QIcon(QLatin1String("skin:mumble.svg")));
|
|
|
|
QObject::connect(qcbSelectAll, &QCheckBox::stateChanged, this, &PluginUpdater::on_selectAll);
|
|
QObject::connect(this, &QDialog::finished, this, &PluginUpdater::on_finished);
|
|
|
|
if (exec() == QDialog::Accepted) {
|
|
update();
|
|
}
|
|
}
|
|
|
|
void PluginUpdater::update() {
|
|
QMutexLocker l(&m_dataMutex);
|
|
|
|
for (int i = 0; i < m_pluginsToUpdate.size(); i++) {
|
|
UpdateEntry currentEntry = m_pluginsToUpdate[i];
|
|
|
|
// The network manager will be emit a signal once the request has finished processing.
|
|
// Thus we can ignore the returned QNetworkReply* here.
|
|
m_networkManager.get(QNetworkRequest(currentEntry.updateURL));
|
|
}
|
|
}
|
|
|
|
void PluginUpdater::populateUI() {
|
|
clearUI();
|
|
|
|
QMutexLocker l(&m_dataMutex);
|
|
for (int i = 0; i < m_pluginsToUpdate.size(); i++) {
|
|
UpdateEntry currentEntry = m_pluginsToUpdate[i];
|
|
plugin_id_t pluginID = currentEntry.pluginID;
|
|
|
|
const_plugin_ptr_t plugin = Global::get().pluginManager->getPlugin(pluginID);
|
|
|
|
if (!plugin) {
|
|
continue;
|
|
}
|
|
|
|
QCheckBox *checkBox = new QCheckBox(qwContent);
|
|
checkBox->setText(plugin->getName());
|
|
checkBox->setToolTip(plugin->getDescription());
|
|
|
|
checkBox->setProperty("pluginID", pluginID);
|
|
|
|
QLabel *urlLabel = new QLabel(qwContent);
|
|
urlLabel->setText(currentEntry.updateURL.toString());
|
|
urlLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
|
|
|
UpdateWidgetPair pair = { checkBox, urlLabel };
|
|
m_pluginUpdateWidgets << pair;
|
|
|
|
QObject::connect(checkBox, &QCheckBox::stateChanged, this, &PluginUpdater::on_singleSelectionChanged);
|
|
}
|
|
|
|
// sort the plugins alphabetically
|
|
std::sort(m_pluginUpdateWidgets.begin(), m_pluginUpdateWidgets.end(),
|
|
[](const UpdateWidgetPair &first, const UpdateWidgetPair &second) {
|
|
return first.pluginCheckBox->text().compare(second.pluginCheckBox->text(), Qt::CaseInsensitive) < 0;
|
|
});
|
|
|
|
// add the widgets to the layout
|
|
for (int i = 0; i < m_pluginUpdateWidgets.size(); i++) {
|
|
UpdateWidgetPair ¤tPair = m_pluginUpdateWidgets[i];
|
|
|
|
static_cast< QFormLayout * >(qwContent->layout())->addRow(currentPair.pluginCheckBox, currentPair.urlLabel);
|
|
}
|
|
}
|
|
|
|
void PluginUpdater::clearUI() {
|
|
// There are always as many checkboxes as there are labels
|
|
for (int i = 0; i < m_pluginUpdateWidgets.size(); i++) {
|
|
UpdateWidgetPair ¤tPair = m_pluginUpdateWidgets[i];
|
|
|
|
qwContent->layout()->removeWidget(currentPair.pluginCheckBox);
|
|
qwContent->layout()->removeWidget(currentPair.urlLabel);
|
|
|
|
delete currentPair.pluginCheckBox;
|
|
delete currentPair.urlLabel;
|
|
}
|
|
}
|
|
|
|
void PluginUpdater::on_selectAll(int checkState) {
|
|
// failsafe for partially selected state (shouldn't happen though)
|
|
if (checkState == Qt::PartiallyChecked) {
|
|
checkState = Qt::Unchecked;
|
|
}
|
|
|
|
// Select or deselect all plugins
|
|
for (int i = 0; i < m_pluginUpdateWidgets.size(); i++) {
|
|
UpdateWidgetPair ¤tPair = m_pluginUpdateWidgets[i];
|
|
|
|
currentPair.pluginCheckBox->setCheckState(static_cast< Qt::CheckState >(checkState));
|
|
}
|
|
}
|
|
|
|
void PluginUpdater::on_singleSelectionChanged(int checkState) {
|
|
bool isChecked = checkState == Qt::Checked;
|
|
|
|
// Block signals for the selectAll checkBox in order to not trigger its
|
|
// check-logic when changing its check-state here
|
|
const QSignalBlocker blocker(qcbSelectAll);
|
|
|
|
if (!isChecked) {
|
|
// If even a single item is unchecked, the selectAll checkbox has to be unchecked
|
|
qcbSelectAll->setCheckState(Qt::Unchecked);
|
|
return;
|
|
}
|
|
|
|
// iterate through all checkboxes to see whether we have to toggle the selectAll checkbox
|
|
for (int i = 0; i < m_pluginUpdateWidgets.size(); i++) {
|
|
const UpdateWidgetPair ¤tPair = m_pluginUpdateWidgets[i];
|
|
|
|
if (!currentPair.pluginCheckBox->isChecked()) {
|
|
// One unchecked checkBox is enough to know that the selectAll
|
|
// CheckBox can't be checked, so we can abort at this point
|
|
return;
|
|
}
|
|
}
|
|
|
|
qcbSelectAll->setCheckState(Qt::Checked);
|
|
}
|
|
|
|
void PluginUpdater::on_finished(int result) {
|
|
if (result == QDialog::Accepted) {
|
|
if (qcbSelectAll->isChecked()) {
|
|
// all plugins shall be updated, so we don't have to check them individually
|
|
return;
|
|
}
|
|
|
|
QMutexLocker l(&m_dataMutex);
|
|
|
|
// The user wants to update the selected plugins only
|
|
// remove the plugins that shouldn't be updated from m_pluginsToUpdate
|
|
auto it = m_pluginsToUpdate.begin();
|
|
while (it != m_pluginsToUpdate.end()) {
|
|
plugin_id_t id = it->pluginID;
|
|
|
|
// find the corresponding checkbox
|
|
bool updateCurrent = false;
|
|
for (int k = 0; k < m_pluginUpdateWidgets.size(); k++) {
|
|
QCheckBox *checkBox = m_pluginUpdateWidgets[k].pluginCheckBox;
|
|
QVariant idVariant = checkBox->property("pluginID");
|
|
|
|
if (idVariant.isValid() && static_cast< plugin_id_t >(idVariant.toInt()) == id) {
|
|
updateCurrent = checkBox->isChecked();
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!updateCurrent) {
|
|
// remove this entry from the update-vector
|
|
it = m_pluginsToUpdate.erase(it);
|
|
} else {
|
|
it++;
|
|
}
|
|
}
|
|
} else {
|
|
// Nothing to do as the user doesn't want to update anyways
|
|
}
|
|
}
|
|
|
|
void PluginUpdater::interrupt() {
|
|
m_wasInterrupted.store(true);
|
|
}
|
|
|
|
void PluginUpdater::on_updateDownloaded(QNetworkReply *reply) {
|
|
if (reply) {
|
|
// Schedule reply for deletion
|
|
reply->deleteLater();
|
|
|
|
if (m_wasInterrupted.load()) {
|
|
emit updateInterrupted();
|
|
return;
|
|
}
|
|
|
|
// Find the ID of the plugin this update is for by comparing the URLs
|
|
UpdateEntry entry;
|
|
bool foundID = false;
|
|
{
|
|
QMutexLocker l(&m_dataMutex);
|
|
|
|
for (int i = 0; i < m_pluginsToUpdate.size(); i++) {
|
|
if (m_pluginsToUpdate[i].updateURL == reply->url()) {
|
|
foundID = true;
|
|
|
|
// remove that entry from the vector as it is being updated right here
|
|
entry = m_pluginsToUpdate.takeAt(i);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!foundID) {
|
|
// Can't match the URL to a pluginID
|
|
qWarning() << "PluginUpdater: Requested update for plugin from" << reply->url()
|
|
<< "but didn't find corresponding plugin again!";
|
|
return;
|
|
}
|
|
|
|
// Now get a handle to that plugin
|
|
const_plugin_ptr_t plugin = Global::get().pluginManager->getPlugin(entry.pluginID);
|
|
|
|
if (!plugin) {
|
|
// Can't find plugin with given ID
|
|
qWarning() << "PluginUpdater: Got update for plugin with id" << entry.pluginID
|
|
<< "but it doesn't seem to exist anymore!";
|
|
return;
|
|
}
|
|
|
|
// We can start actually checking the reply here
|
|
if (reply->error() != QNetworkReply::NoError) {
|
|
// There was an error during this request. Report it
|
|
Log::logOrDefer(Log::Warning,
|
|
tr("Unable to download plugin update for \"%1\" from \"%2\" (%3)")
|
|
.arg(plugin->getName())
|
|
.arg(reply->url().toString())
|
|
.arg(QString::fromLatin1(
|
|
QMetaEnum::fromType< QNetworkReply::NetworkError >().valueToKey(reply->error()))));
|
|
return;
|
|
}
|
|
|
|
// Check HTTP status code (just because the request was successful, doesn't
|
|
// mean the data was downloaded successfully
|
|
int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
|
|
|
if (httpStatusCode >= 300 && httpStatusCode < 400) {
|
|
// We have been redirected
|
|
if (entry.redirects >= MAX_REDIRECTS - 1) {
|
|
// Maximum redirect count exceeded
|
|
Log::logOrDefer(Log::Warning,
|
|
tr("Update for plugin \"%1\" failed due to too many redirects").arg(plugin->getName()));
|
|
|
|
return;
|
|
}
|
|
|
|
QUrl redirectedUrl = reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl();
|
|
// Because the redirection url can be relative,
|
|
// we have to use the previous one to resolve it
|
|
redirectedUrl = reply->url().resolved(redirectedUrl);
|
|
|
|
// Re-insert the current plugin into the list of updating plugins (using the
|
|
// new URL so that it will be associated with that instead of the old one)
|
|
entry.updateURL = redirectedUrl;
|
|
entry.redirects++;
|
|
{
|
|
QMutexLocker l(&m_dataMutex);
|
|
|
|
m_pluginsToUpdate << entry;
|
|
}
|
|
|
|
// Post a new request for the file to the new URL
|
|
m_networkManager.get(QNetworkRequest(redirectedUrl));
|
|
|
|
return;
|
|
}
|
|
|
|
if (httpStatusCode < 200 || httpStatusCode >= 300) {
|
|
// HTTP request has failed
|
|
Log::logOrDefer(Log::Warning,
|
|
tr("Unable to download plugin update for \"%1\" from \"%2\" (HTTP status code %3)")
|
|
.arg(plugin->getName())
|
|
.arg(reply->url().toString())
|
|
.arg(httpStatusCode));
|
|
|
|
return;
|
|
}
|
|
|
|
// Reply seems fine -> write file to disk and fire installer
|
|
QByteArray content = reply->readAll();
|
|
|
|
// Write the content to a file in the temp-dir
|
|
if (content.isEmpty()) {
|
|
qWarning() << "PluginUpdater: Update for" << plugin->getName() << "from" << reply->url().toString()
|
|
<< "resulted in no content!";
|
|
return;
|
|
}
|
|
|
|
QFile file(QDir::temp().filePath(entry.fileName));
|
|
if (!file.open(QIODevice::WriteOnly)) {
|
|
qWarning() << "PluginUpdater: Can't open" << file.fileName() << "for writing!";
|
|
return;
|
|
}
|
|
|
|
file.write(content);
|
|
file.close();
|
|
|
|
try {
|
|
const QString pluginName = plugin->getName();
|
|
// We have to release the plugin handle here by resetting the smart-pointer in order to make sure the
|
|
// installer can really unload the plugin in order to overwrite it.
|
|
plugin.reset();
|
|
|
|
// Launch installer
|
|
PluginInstaller installer(QFileInfo(file.fileName()));
|
|
installer.install();
|
|
|
|
Log::logOrDefer(Log::Information, tr("Successfully updated plugin \"%1\"").arg(pluginName));
|
|
|
|
// Make sure Mumble won't use the old version of the plugin
|
|
Global::get().pluginManager->rescanPlugins();
|
|
} catch (const PluginInstallException &e) {
|
|
Log::logOrDefer(Log::CriticalError, e.getMessage());
|
|
}
|
|
|
|
{
|
|
QMutexLocker l(&m_dataMutex);
|
|
|
|
if (m_pluginsToUpdate.isEmpty()) {
|
|
emit updatingFinished();
|
|
}
|
|
}
|
|
}
|
|
}
|