mumble-voip_mumble/src/mumble/PluginUpdater.cpp

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 &currentPair = 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 &currentPair = 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 &currentPair = 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 &currentPair = 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();
}
}
}
}