sneedmc/launcher/minecraft/auth/AccountList.cpp
jdp_ 9cb6200081 Cleanup codebase
for FileSystem.cpp:
Instead of checking if Linux or FreeBSD, check if its not Windows and not OSX. Chances are other operating systems run a DE that adheres to the XDG Desktop standard (.desktop). The check isn't good enough anyways since alternative shells for Windows exist, it will never be an accurate check. In any case this function is unused.

WorldListPage.cpp:
Redo confusing switch statement plagued with fall throughs, now well defined.

LaunchController.cpp:
Remove cringe. Also fix warning and make the unimplemented case(s) more explicit.

VersionProxyModel.cpp:
Add fallthrough for warning suppression.

WorldListPage.cpp:
redo `mceditState`

TranslationsModel.cpp:
Move up definition of `column` variable to when it is needed, clear up switch cases

FlameInstanceCreationTask.cpp:
Fallthrough intentionally

SkinUpload.cpp:
Make `getVariant`

ResourcePack.cpp:
Add new values for 1.19.3+

meta/Index.cpp:
Make clear switch statement behavior

JavaWizardPage.cpp:
Fix case fallthrough

Yggdrasil.cpp:
Fix case fallthrough

AccountList.cpp:
Fix case fallthrough,

WinDarkmode.cpp:
Add an explanation and fix warnings due to FARPROC casts.

Signed-off-by: jdp_ <42700985+jdpatdiscord@users.noreply.github.com>
2023-05-07 06:20:16 -04:00

777 lines
23 KiB
C++

// SPDX-License-Identifier: GPL-3.0-only
/*
* PolyMC - Minecraft Launcher
* Copyright (C) 2022 Sefa Eyeoglu <contact@scrumplex.net>
*
* 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, version 3.
*
* 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, see <https://www.gnu.org/licenses/>.
*
* This file incorporates work covered by the following copyright and
* permission notice:
*
* Copyright 2013-2021 MultiMC Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "AccountList.h"
#include "AccountData.h"
#include "AccountTask.h"
#include <QIODevice>
#include <QFile>
#include <QTextStream>
#include <QJsonDocument>
#include <QJsonArray>
#include <QJsonObject>
#include <QJsonParseError>
#include <QDir>
#include <QTimer>
#include <QDebug>
#include <FileSystem.h>
#include <QSaveFile>
#include <chrono>
enum AccountListVersion {
MojangOnly = 2,
MojangMSA = 3
};
AccountList::AccountList(QObject *parent) : QAbstractListModel(parent) {
m_refreshTimer = new QTimer(this);
m_refreshTimer->setSingleShot(true);
connect(m_refreshTimer, &QTimer::timeout, this, &AccountList::fillQueue);
m_nextTimer = new QTimer(this);
m_nextTimer->setSingleShot(true);
connect(m_nextTimer, &QTimer::timeout, this, &AccountList::tryNext);
}
AccountList::~AccountList() noexcept {}
int AccountList::findAccountByProfileId(const QString& profileId) const {
for (int i = 0; i < count(); i++) {
MinecraftAccountPtr account = at(i);
if (account->profileId() == profileId) {
return i;
}
}
return -1;
}
MinecraftAccountPtr AccountList::getAccountByProfileName(const QString& profileName) const {
for (int i = 0; i < count(); i++) {
MinecraftAccountPtr account = at(i);
if (account->profileName() == profileName) {
return account;
}
}
return nullptr;
}
const MinecraftAccountPtr AccountList::at(int i) const
{
return MinecraftAccountPtr(m_accounts.at(i));
}
QStringList AccountList::profileNames() const {
QStringList out;
for(auto & account: m_accounts) {
auto profileName = account->profileName();
if(profileName.isEmpty()) {
continue;
}
out.append(profileName);
}
return out;
}
void AccountList::addAccount(const MinecraftAccountPtr account)
{
// NOTE: Do not allow adding something that's already there. We shouldn't let it continue
// because of the signal / slot connections after this.
if (m_accounts.contains(account)) {
qDebug() << "Tried to add account that's already on the accounts list!";
return;
}
// hook up notifications for changes in the account
connect(account.get(), &MinecraftAccount::changed, this, &AccountList::accountChanged);
connect(account.get(), &MinecraftAccount::activityChanged, this, &AccountList::accountActivityChanged);
// override/replace existing account with the same profileId
auto profileId = account->profileId();
if(profileId.size()) {
auto existingAccount = findAccountByProfileId(profileId);
if(existingAccount != -1) {
qDebug() << "Replacing old account with a new one with the same profile ID!";
MinecraftAccountPtr existingAccountPtr = m_accounts[existingAccount];
m_accounts[existingAccount] = account;
if(m_defaultAccount == existingAccountPtr) {
m_defaultAccount = account;
}
// disconnect notifications for changes in the account being replaced
existingAccountPtr->disconnect(this);
emit dataChanged(index(existingAccount), index(existingAccount, columnCount(QModelIndex()) - 1));
onListChanged();
return;
}
}
// if we don't have this profileId yet, add the account to the end
int row = m_accounts.count();
qDebug() << "Inserting account at index" << row;
beginInsertRows(QModelIndex(), row, row);
m_accounts.append(account);
endInsertRows();
onListChanged();
}
void AccountList::removeAccount(QModelIndex index)
{
int row = index.row();
if(index.isValid() && row >= 0 && row < m_accounts.size())
{
auto & account = m_accounts[row];
if(account == m_defaultAccount)
{
m_defaultAccount = nullptr;
onDefaultAccountChanged();
}
account->disconnect(this);
beginRemoveRows(QModelIndex(), row, row);
m_accounts.removeAt(index.row());
endRemoveRows();
onListChanged();
}
}
MinecraftAccountPtr AccountList::defaultAccount() const
{
return m_defaultAccount;
}
void AccountList::setDefaultAccount(MinecraftAccountPtr newAccount)
{
if (!newAccount && m_defaultAccount)
{
int idx = 0;
auto previousDefaultAccount = m_defaultAccount;
m_defaultAccount = nullptr;
for (MinecraftAccountPtr account : m_accounts)
{
if (account == previousDefaultAccount)
{
emit dataChanged(index(idx), index(idx, columnCount(QModelIndex()) - 1));
}
idx ++;
}
onDefaultAccountChanged();
}
else
{
auto currentDefaultAccount = m_defaultAccount;
int currentDefaultAccountIdx = -1;
auto newDefaultAccount = m_defaultAccount;
int newDefaultAccountIdx = -1;
int idx = 0;
for (MinecraftAccountPtr account : m_accounts)
{
if (account == newAccount)
{
newDefaultAccount = account;
newDefaultAccountIdx = idx;
}
if(currentDefaultAccount == account)
{
currentDefaultAccountIdx = idx;
}
idx++;
}
if(currentDefaultAccount != newDefaultAccount)
{
emit dataChanged(index(currentDefaultAccountIdx), index(currentDefaultAccountIdx, columnCount(QModelIndex()) - 1));
emit dataChanged(index(newDefaultAccountIdx), index(newDefaultAccountIdx, columnCount(QModelIndex()) - 1));
m_defaultAccount = newDefaultAccount;
onDefaultAccountChanged();
}
}
}
void AccountList::accountChanged()
{
// the list changed. there is no doubt.
onListChanged();
}
void AccountList::accountActivityChanged(bool active)
{
MinecraftAccount *account = qobject_cast<MinecraftAccount *>(sender());
bool found = false;
for (int i = 0; i < count(); i++) {
if (at(i).get() == account) {
emit dataChanged(index(i), index(i, columnCount(QModelIndex()) - 1));
found = true;
break;
}
}
if(found) {
emit listActivityChanged();
if(active) {
beginActivity();
}
else {
endActivity();
}
}
}
void AccountList::onListChanged()
{
if (m_autosave)
// TODO: Alert the user if this fails.
saveList();
emit listChanged();
}
void AccountList::onDefaultAccountChanged()
{
if (m_autosave)
saveList();
emit defaultAccountChanged();
}
int AccountList::count() const
{
return m_accounts.count();
}
QVariant AccountList::data(const QModelIndex &index, int role) const
{
if (!index.isValid())
return QVariant();
if (index.row() > count())
return QVariant();
MinecraftAccountPtr account = at(index.row());
switch (role)
{
case Qt::DisplayRole:
switch (index.column())
{
case ProfileNameColumn: {
return account->profileName();
}
case NameColumn:
return account->accountDisplayString();
case TypeColumn: {
auto typeStr = account->typeString();
typeStr[0] = typeStr[0].toUpper();
return typeStr;
}
case StatusColumn: {
switch(account->accountState()) {
case AccountState::Unchecked: {
return tr("Unchecked", "Account status");
}
case AccountState::Offline: {
return tr("Offline", "Account status");
}
case AccountState::Online: {
return tr("Ready", "Account status");
}
case AccountState::Working: {
return tr("Working", "Account status");
}
case AccountState::Errored: {
return tr("Errored", "Account status");
}
case AccountState::Expired: {
return tr("Expired", "Account status");
}
case AccountState::Disabled: {
return tr("Disabled", "Account status");
}
case AccountState::Gone: {
return tr("Gone", "Account status");
}
case AccountState::Queued: {
qWarning() << "Unhandled account state Queued";
[[fallthrough]];
}
default:
return tr("Unknown", "Account status");
}
}
case MigrationColumn: {
if(account->isMSA() || account->isOffline()) {
return tr("N/A", "Can Migrate?");
}
if (account->canMigrate()) {
return tr("Yes", "Can Migrate?");
}
else {
return tr("No", "Can Migrate?");
}
qWarning() << "Unhandled case in MigrationColumn";
[[fallthrough]];
}
default:
return QVariant();
}
case Qt::ToolTipRole:
return account->accountDisplayString();
case PointerRole:
return QVariant::fromValue(account);
case Qt::CheckStateRole:
switch (index.column())
{
case ProfileNameColumn:
return account == m_defaultAccount ? Qt::Checked : Qt::Unchecked;
}
[[fallthrough]];
default:
return QVariant();
}
}
QVariant AccountList::headerData(int section, Qt::Orientation orientation, int role) const
{
switch (role)
{
case Qt::DisplayRole:
switch (section)
{
case ProfileNameColumn:
return tr("Username");
case NameColumn:
return tr("Account");
case TypeColumn:
return tr("Type");
case StatusColumn:
return tr("Status");
case MigrationColumn:
return tr("Can Migrate?");
default:
return QVariant();
}
case Qt::ToolTipRole:
switch (section)
{
case ProfileNameColumn:
return tr("Minecraft username associated with the account.");
case NameColumn:
return tr("User name of the account.");
case TypeColumn:
return tr("Type of the account - Mojang or MSA.");
case StatusColumn:
return tr("Current status of the account.");
case MigrationColumn:
return tr("Can this account migrate to a Microsoft account?");
default:
return QVariant();
}
default:
return QVariant();
}
}
int AccountList::rowCount(const QModelIndex &) const
{
// Return count
return count();
}
int AccountList::columnCount(const QModelIndex &) const
{
return NUM_COLUMNS;
}
Qt::ItemFlags AccountList::flags(const QModelIndex &index) const
{
if (index.row() < 0 || index.row() >= rowCount(index) || !index.isValid())
{
return Qt::NoItemFlags;
}
return Qt::ItemIsUserCheckable | Qt::ItemIsEnabled | Qt::ItemIsSelectable;
}
bool AccountList::setData(const QModelIndex &idx, const QVariant &value, int role)
{
if (idx.row() < 0 || idx.row() >= rowCount(idx) || !idx.isValid())
{
return false;
}
if(role == Qt::CheckStateRole)
{
if(value == Qt::Checked)
{
MinecraftAccountPtr account = at(idx.row());
setDefaultAccount(account);
}
}
emit dataChanged(idx, index(idx.row(), columnCount(QModelIndex()) - 1));
return true;
}
bool AccountList::loadList()
{
if (m_listFilePath.isEmpty())
{
qCritical() << "Can't load Mojang account list. No file path given and no default set.";
return false;
}
QFile file(m_listFilePath);
// Try to open the file and fail if we can't.
// TODO: We should probably report this error to the user.
if (!file.open(QIODevice::ReadOnly))
{
qCritical() << QString("Failed to read the account list file (%1).").arg(m_listFilePath).toUtf8();
return false;
}
// Read the file and close it.
QByteArray jsonData = file.readAll();
file.close();
QJsonParseError parseError;
QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData, &parseError);
// Fail if the JSON is invalid.
if (parseError.error != QJsonParseError::NoError)
{
qCritical() << QString("Failed to parse account list file: %1 at offset %2")
.arg(parseError.errorString(), QString::number(parseError.offset))
.toUtf8();
return false;
}
// Make sure the root is an object.
if (!jsonDoc.isObject())
{
qCritical() << "Invalid account list JSON: Root should be an array.";
return false;
}
QJsonObject root = jsonDoc.object();
// Make sure the format version matches.
auto listVersion = root.value("formatVersion").toVariant().toInt();
switch(listVersion) {
case AccountListVersion::MojangOnly: {
return loadV2(root);
}
break;
case AccountListVersion::MojangMSA: {
return loadV3(root);
}
break;
default: {
QString newName = "accounts-old.json";
qWarning() << "Unknown format version when loading account list. Existing one will be renamed to" << newName;
// Attempt to rename the old version.
file.rename(newName);
return false;
}
}
}
bool AccountList::loadV2(QJsonObject& root) {
beginResetModel();
auto defaultUserName = root.value("activeAccount").toString("");
QJsonArray accounts = root.value("accounts").toArray();
for (QJsonValue accountVal : accounts)
{
QJsonObject accountObj = accountVal.toObject();
MinecraftAccountPtr account = MinecraftAccount::loadFromJsonV2(accountObj);
if (account.get() != nullptr)
{
auto profileId = account->profileId();
if(!profileId.size()) {
continue;
}
if(findAccountByProfileId(profileId) != -1) {
continue;
}
connect(account.get(), &MinecraftAccount::changed, this, &AccountList::accountChanged);
connect(account.get(), &MinecraftAccount::activityChanged, this, &AccountList::accountActivityChanged);
m_accounts.append(account);
if (defaultUserName.size() && account->mojangUserName() == defaultUserName) {
m_defaultAccount = account;
}
}
else
{
qWarning() << "Failed to load an account.";
}
}
endResetModel();
return true;
}
bool AccountList::loadV3(QJsonObject& root) {
beginResetModel();
QJsonArray accounts = root.value("accounts").toArray();
for (QJsonValue accountVal : accounts)
{
QJsonObject accountObj = accountVal.toObject();
MinecraftAccountPtr account = MinecraftAccount::loadFromJsonV3(accountObj);
if (account.get() != nullptr)
{
auto profileId = account->profileId();
if(profileId.size()) {
if(findAccountByProfileId(profileId) != -1) {
continue;
}
}
connect(account.get(), &MinecraftAccount::changed, this, &AccountList::accountChanged);
connect(account.get(), &MinecraftAccount::activityChanged, this, &AccountList::accountActivityChanged);
m_accounts.append(account);
if(accountObj.value("active").toBool(false)) {
m_defaultAccount = account;
}
}
else
{
qWarning() << "Failed to load an account.";
}
}
endResetModel();
return true;
}
bool AccountList::saveList()
{
if (m_listFilePath.isEmpty())
{
qCritical() << "Can't save Mojang account list. No file path given and no default set.";
return false;
}
// make sure the parent folder exists
if(!FS::ensureFilePathExists(m_listFilePath))
return false;
// make sure the file wasn't overwritten with a folder before (fixes a bug)
QFileInfo finfo(m_listFilePath);
if(finfo.isDir())
{
QDir badDir(m_listFilePath);
badDir.removeRecursively();
}
qDebug() << "Writing account list to" << m_listFilePath;
qDebug() << "Building JSON data structure.";
// Build the JSON document to write to the list file.
QJsonObject root;
root.insert("formatVersion", AccountListVersion::MojangMSA);
// Build a list of accounts.
qDebug() << "Building account array.";
QJsonArray accounts;
for (MinecraftAccountPtr account : m_accounts)
{
QJsonObject accountObj = account->saveToJson();
if(m_defaultAccount == account) {
accountObj["active"] = true;
}
accounts.append(accountObj);
}
// Insert the account list into the root object.
root.insert("accounts", accounts);
// Create a JSON document object to convert our JSON to bytes.
QJsonDocument doc(root);
// Now that we're done building the JSON object, we can write it to the file.
qDebug() << "Writing account list to file.";
QSaveFile file(m_listFilePath);
// Try to open the file and fail if we can't.
// TODO: We should probably report this error to the user.
if (!file.open(QIODevice::WriteOnly))
{
qCritical() << QString("Failed to read the account list file (%1).").arg(m_listFilePath).toUtf8();
return false;
}
// Write the JSON to the file.
file.write(doc.toJson());
file.setPermissions(QFile::ReadOwner|QFile::WriteOwner|QFile::ReadUser|QFile::WriteUser);
if(file.commit()) {
qDebug() << "Saved account list to" << m_listFilePath;
return true;
}
else {
qDebug() << "Failed to save accounts to" << m_listFilePath;
return false;
}
}
void AccountList::setListFilePath(QString path, bool autosave)
{
m_listFilePath = path;
m_autosave = autosave;
}
bool AccountList::anyAccountIsValid()
{
for(auto account: m_accounts)
{
if(account->ownsMinecraft()) {
return true;
}
}
return false;
}
void AccountList::fillQueue() {
if(m_defaultAccount && m_defaultAccount->shouldRefresh()) {
auto idToRefresh = m_defaultAccount->internalId();
m_refreshQueue.push_back(idToRefresh);
qDebug() << "AccountList: Queued default account with internal ID " << idToRefresh << " to refresh first";
}
for(int i = 0; i < count(); i++) {
auto account = at(i);
if(account == m_defaultAccount) {
continue;
}
if(account->shouldRefresh()) {
auto idToRefresh = account->internalId();
queueRefresh(idToRefresh);
}
}
tryNext();
}
void AccountList::requestRefresh(QString accountId) {
auto index = m_refreshQueue.indexOf(accountId);
if(index != -1) {
m_refreshQueue.removeAt(index);
}
m_refreshQueue.push_front(accountId);
qDebug() << "AccountList: Pushed account with internal ID " << accountId << " to the front of the queue";
if(!isActive()) {
tryNext();
}
}
void AccountList::queueRefresh(QString accountId) {
if(m_refreshQueue.indexOf(accountId) != -1) {
return;
}
m_refreshQueue.push_back(accountId);
qDebug() << "AccountList: Queued account with internal ID " << accountId << " to refresh";
}
void AccountList::tryNext() {
while (m_refreshQueue.length()) {
auto accountId = m_refreshQueue.front();
m_refreshQueue.pop_front();
for(int i = 0; i < count(); i++) {
auto account = at(i);
if(account->internalId() == accountId) {
m_currentTask = account->refresh();
if(m_currentTask) {
connect(m_currentTask.get(), &AccountTask::succeeded, this, &AccountList::authSucceeded);
connect(m_currentTask.get(), &AccountTask::failed, this, &AccountList::authFailed);
m_currentTask->start();
qDebug() << "RefreshSchedule: Processing account " << account->accountDisplayString() << " with internal ID " << accountId;
return;
}
}
}
qDebug() << "RefreshSchedule: Account with with internal ID " << accountId << " not found.";
}
// if we get here, no account needed refreshing. Schedule refresh in an hour.
m_refreshTimer->start(1000 * 3600);
}
void AccountList::authSucceeded() {
qDebug() << "RefreshSchedule: Background account refresh succeeded";
m_currentTask.reset();
m_nextTimer->start(1000 * 20);
}
void AccountList::authFailed(QString reason) {
qDebug() << "RefreshSchedule: Background account refresh failed: " << reason;
m_currentTask.reset();
m_nextTimer->start(1000 * 20);
}
bool AccountList::isActive() const {
return m_activityCount != 0;
}
void AccountList::beginActivity() {
bool activating = m_activityCount == 0;
m_activityCount++;
if(activating) {
emit activityChanged(true);
}
}
void AccountList::endActivity() {
if(m_activityCount == 0) {
qWarning() << m_name << " - Activity count would become below zero";
return;
}
bool deactivating = m_activityCount == 1;
m_activityCount--;
if(deactivating) {
emit activityChanged(false);
}
}