mirror of https://github.com/qTox/qTox.git
Browse Source
Rewrite the profile management code and implement a new login screen. Fixes #1776 Fixes #1746 Fixes #1596 Fixes #1562pull/1793/head
33 changed files with 2212 additions and 1181 deletions
@ -0,0 +1,339 @@
@@ -0,0 +1,339 @@
|
||||
#include "profile.h" |
||||
#include "profilelocker.h" |
||||
#include "src/misc/settings.h" |
||||
#include "src/core/core.h" |
||||
#include "src/historykeeper.h" |
||||
#include "src/widget/gui.h" |
||||
#include "src/widget/widget.h" |
||||
#include "src/nexus.h" |
||||
#include <cassert> |
||||
#include <QDir> |
||||
#include <QFileInfo> |
||||
#include <QSaveFile> |
||||
#include <QThread> |
||||
#include <QObject> |
||||
#include <QDebug> |
||||
QVector<QString> Profile::profiles; |
||||
|
||||
Profile::Profile(QString name, QString password, bool isNewProfile) |
||||
: name{name}, password{password}, |
||||
newProfile{isNewProfile}, isRemoved{false} |
||||
{ |
||||
Settings::getInstance().setCurrentProfile(name); |
||||
HistoryKeeper::resetInstance(); |
||||
|
||||
coreThread = new QThread(); |
||||
coreThread->setObjectName("qTox Core"); |
||||
core = new Core(coreThread, *this); |
||||
core->moveToThread(coreThread); |
||||
QObject::connect(coreThread, &QThread::started, core, &Core::start); |
||||
} |
||||
|
||||
Profile* Profile::loadProfile(QString name, QString password) |
||||
{ |
||||
if (ProfileLocker::hasLock()) |
||||
{ |
||||
qCritical() << "Tried to load profile "<<name<<", but another profile is already locked!"; |
||||
return nullptr; |
||||
} |
||||
|
||||
if (!ProfileLocker::lock(name)) |
||||
{ |
||||
qWarning() << "Failed to lock profile "<<name; |
||||
return nullptr; |
||||
} |
||||
|
||||
return new Profile(name, password, false); |
||||
} |
||||
|
||||
Profile* Profile::createProfile(QString name, QString password) |
||||
{ |
||||
if (ProfileLocker::hasLock()) |
||||
{ |
||||
qCritical() << "Tried to create profile "<<name<<", but another profile is already locked!"; |
||||
return nullptr; |
||||
} |
||||
|
||||
if (profileExists(name)) |
||||
{ |
||||
qCritical() << "Tried to create profile "<<name<<", but it already exists!"; |
||||
return nullptr; |
||||
} |
||||
|
||||
if (!ProfileLocker::lock(name)) |
||||
{ |
||||
qWarning() << "Failed to lock profile "<<name; |
||||
return nullptr; |
||||
} |
||||
|
||||
Settings::getInstance().createPersonal(name); |
||||
return new Profile(name, password, true); |
||||
} |
||||
|
||||
Profile::~Profile() |
||||
{ |
||||
if (!isRemoved && core->isReady()) |
||||
saveToxSave(); |
||||
delete core; |
||||
delete coreThread; |
||||
ProfileLocker::assertLock(); |
||||
assert(ProfileLocker::getCurLockName() == name); |
||||
ProfileLocker::unlock(); |
||||
} |
||||
|
||||
QVector<QString> Profile::getFilesByExt(QString extension) |
||||
{ |
||||
QDir dir(Settings::getInstance().getSettingsDirPath()); |
||||
QVector<QString> out; |
||||
dir.setFilter(QDir::Files | QDir::NoDotAndDotDot); |
||||
dir.setNameFilters(QStringList("*."+extension)); |
||||
QFileInfoList list = dir.entryInfoList(); |
||||
out.reserve(list.size()); |
||||
for (QFileInfo file : list) |
||||
out += file.completeBaseName(); |
||||
return out; |
||||
} |
||||
|
||||
void Profile::scanProfiles() |
||||
{ |
||||
profiles.clear(); |
||||
QVector<QString> toxfiles = getFilesByExt("tox"), inifiles = getFilesByExt("ini"); |
||||
for (QString toxfile : toxfiles) |
||||
{ |
||||
if (!inifiles.contains(toxfile)) |
||||
importProfile(toxfile); |
||||
profiles.append(toxfile); |
||||
} |
||||
} |
||||
|
||||
void Profile::importProfile(QString name) |
||||
{ |
||||
assert(!profileExists(name)); |
||||
Settings::getInstance().createPersonal(name); |
||||
} |
||||
|
||||
QVector<QString> Profile::getProfiles() |
||||
{ |
||||
return profiles; |
||||
} |
||||
|
||||
Core* Profile::getCore() |
||||
{ |
||||
return core; |
||||
} |
||||
|
||||
QString Profile::getName() |
||||
{ |
||||
return name; |
||||
} |
||||
|
||||
void Profile::startCore() |
||||
{ |
||||
coreThread->start(); |
||||
} |
||||
|
||||
bool Profile::isNewProfile() |
||||
{ |
||||
return newProfile; |
||||
} |
||||
|
||||
QByteArray Profile::loadToxSave() |
||||
{ |
||||
assert(!isRemoved); |
||||
|
||||
/// TODO: Cache the data, invalidate it only when we save
|
||||
QByteArray data; |
||||
|
||||
QString path = Settings::getSettingsDirPath() + QDir::separator() + name + ".tox"; |
||||
QFile saveFile(path); |
||||
qint64 fileSize; |
||||
qDebug() << "Loading tox save "<<path; |
||||
|
||||
if (!saveFile.exists()) |
||||
{ |
||||
qWarning() << "The tox save file "<<path<<" was not found"; |
||||
goto fail; |
||||
} |
||||
|
||||
if (!saveFile.open(QIODevice::ReadOnly)) |
||||
{ |
||||
qCritical() << "The tox save file " << path << " couldn't' be opened"; |
||||
goto fail; |
||||
} |
||||
|
||||
fileSize = saveFile.size(); |
||||
if (fileSize <= 0) |
||||
{ |
||||
qWarning() << "The tox save file"<<path<<" is empty!"; |
||||
goto fail; |
||||
} |
||||
|
||||
data = saveFile.readAll(); |
||||
if (tox_is_data_encrypted((uint8_t*)data.data())) |
||||
{ |
||||
if (password.isEmpty()) |
||||
{ |
||||
qCritical() << "The tox save file is encrypted, but we don't have a password!"; |
||||
data.clear(); |
||||
goto fail; |
||||
} |
||||
|
||||
uint8_t salt[TOX_PASS_SALT_LENGTH]; |
||||
tox_get_salt(reinterpret_cast<uint8_t *>(data.data()), salt); |
||||
core->setPassword(password, salt); |
||||
|
||||
data = core->decryptData(data); |
||||
if (data.isEmpty()) |
||||
qCritical() << "Failed to decrypt the tox save file"; |
||||
} |
||||
else |
||||
{ |
||||
if (!password.isEmpty()) |
||||
qWarning() << "We have a password, but the tox save file is not encrypted"; |
||||
} |
||||
|
||||
fail: |
||||
saveFile.close(); |
||||
return data; |
||||
} |
||||
|
||||
void Profile::saveToxSave() |
||||
{ |
||||
assert(core->isReady()); |
||||
QByteArray data = core->getToxSaveData(); |
||||
assert(data.size()); |
||||
saveToxSave(data); |
||||
} |
||||
|
||||
void Profile::saveToxSave(QByteArray data) |
||||
{ |
||||
assert(!isRemoved); |
||||
ProfileLocker::assertLock(); |
||||
assert(ProfileLocker::getCurLockName() == name); |
||||
|
||||
QString path = Settings::getSettingsDirPath() + QDir::separator() + name + ".tox"; |
||||
qDebug() << "Saving tox save to "<<path; |
||||
QSaveFile saveFile(path); |
||||
if (!saveFile.open(QIODevice::WriteOnly)) |
||||
{ |
||||
qCritical() << "Tox save file " << path << " couldn't be opened"; |
||||
return; |
||||
} |
||||
|
||||
if (!password.isEmpty()) |
||||
{ |
||||
core->setPassword(password); |
||||
data = core->encryptData(data); |
||||
if (data.isEmpty()) |
||||
{ |
||||
qCritical() << "Failed to encrypt, can't save!"; |
||||
saveFile.cancelWriting(); |
||||
return; |
||||
} |
||||
} |
||||
|
||||
saveFile.write(data); |
||||
saveFile.commit(); |
||||
newProfile = false; |
||||
} |
||||
|
||||
bool Profile::profileExists(QString name) |
||||
{ |
||||
QString path = Settings::getSettingsDirPath() + QDir::separator() + name; |
||||
return QFile::exists(path+".tox") && QFile::exists(path+".ini"); |
||||
} |
||||
|
||||
bool Profile::isEncrypted() |
||||
{ |
||||
return !password.isEmpty(); |
||||
} |
||||
|
||||
bool Profile::isEncrypted(QString name) |
||||
{ |
||||
uint8_t data[encryptHeaderSize] = {0}; |
||||
QString path = Settings::getSettingsDirPath() + QDir::separator() + name + ".tox"; |
||||
QFile saveFile(path); |
||||
if (!saveFile.open(QIODevice::ReadOnly)) |
||||
{ |
||||
qWarning() << "Couldn't open tox save "<<path; |
||||
return false; |
||||
} |
||||
|
||||
saveFile.read((char*)data, encryptHeaderSize); |
||||
saveFile.close(); |
||||
|
||||
return tox_is_data_encrypted(data); |
||||
} |
||||
|
||||
void Profile::remove() |
||||
{ |
||||
if (isRemoved) |
||||
{ |
||||
qWarning() << "Profile "<<name<<" is already removed!"; |
||||
return; |
||||
} |
||||
isRemoved = true; |
||||
|
||||
qDebug() << "Removing profile"<<name; |
||||
profiles.removeAll(name); |
||||
QString path = Settings::getSettingsDirPath() + QDir::separator() + name; |
||||
QFile::remove(path+".tox"); |
||||
QFile::remove(path+".ini"); |
||||
|
||||
QFile::remove(HistoryKeeper::getHistoryPath(name, 0)); |
||||
QFile::remove(HistoryKeeper::getHistoryPath(name, 1)); |
||||
} |
||||
|
||||
bool Profile::rename(QString newName) |
||||
{ |
||||
QString path = Settings::getSettingsDirPath() + QDir::separator() + name, |
||||
newPath = Settings::getSettingsDirPath() + QDir::separator() + newName; |
||||
|
||||
if (!ProfileLocker::lock(newName)) |
||||
return false; |
||||
|
||||
QFile::rename(path+".tox", newPath+".tox"); |
||||
QFile::rename(path+".ini", newPath+".ini"); |
||||
HistoryKeeper::renameHistory(name, newName); |
||||
bool resetAutorun = Settings::getInstance().getAutorun(); |
||||
Settings::getInstance().setAutorun(false); |
||||
Settings::getInstance().setCurrentProfile(newName); |
||||
if (resetAutorun) |
||||
Settings::getInstance().setAutorun(true); // fixes -p flag in autostart command line
|
||||
|
||||
name = newName; |
||||
return true; |
||||
} |
||||
|
||||
bool Profile::checkPassword() |
||||
{ |
||||
if (isRemoved) |
||||
return false; |
||||
|
||||
return !loadToxSave().isEmpty(); |
||||
} |
||||
|
||||
QString Profile::getPassword() |
||||
{ |
||||
return password; |
||||
} |
||||
|
||||
void Profile::restartCore() |
||||
{ |
||||
GUI::setEnabled(false); // Core::reset re-enables it
|
||||
if (!isRemoved && core->isReady()) |
||||
saveToxSave(); |
||||
QMetaObject::invokeMethod(core, "reset"); |
||||
} |
||||
|
||||
void Profile::setPassword(QString newPassword) |
||||
{ |
||||
QList<HistoryKeeper::HistMessage> oldMessages = HistoryKeeper::exportMessagesDeleteFile(); |
||||
|
||||
password = newPassword; |
||||
core->setPassword(password); |
||||
saveToxSave(); |
||||
|
||||
HistoryKeeper::getInstance()->importMessages(oldMessages); |
||||
Nexus::getDesktopGUI()->reloadHistory(); |
||||
} |
@ -0,0 +1,76 @@
@@ -0,0 +1,76 @@
|
||||
#ifndef PROFILE_H |
||||
#define PROFILE_H |
||||
|
||||
#include <QVector> |
||||
#include <QString> |
||||
#include <QByteArray> |
||||
|
||||
class Core; |
||||
class QThread; |
||||
|
||||
/// Manages user profiles
|
||||
class Profile |
||||
{ |
||||
public: |
||||
/// Locks and loads an existing profile and create the associate Core* instance
|
||||
/// Returns a nullptr on error, for example if the profile is already in use
|
||||
static Profile* loadProfile(QString name, QString password = QString()); |
||||
/// Creates a new profile and the associated Core* instance
|
||||
/// If password is not empty, the profile will be encrypted
|
||||
/// Returns a nullptr on error, for example if the profile already exists
|
||||
static Profile* createProfile(QString name, QString password); |
||||
~Profile(); |
||||
|
||||
Core* getCore(); |
||||
QString getName(); |
||||
|
||||
void startCore(); ///< Starts the Core thread
|
||||
void restartCore(); ///< Delete core and restart a new one
|
||||
bool isNewProfile(); |
||||
bool isEncrypted(); ///< Returns true if we have a password set (doesn't check the actual file on disk)
|
||||
bool checkPassword(); ///< Checks whether the password is valid
|
||||
QString getPassword(); |
||||
void setPassword(QString newPassword); ///< Changes the encryption password and re-saves everything with it
|
||||
|
||||
QByteArray loadToxSave(); ///< Loads the profile's .tox save from file, unencrypted
|
||||
void saveToxSave(); ///< Saves the profile's .tox save, encrypted if needed. Invalid on deleted profiles.
|
||||
void saveToxSave(QByteArray data); ///< Write the .tox save, encrypted if needed. Invalid on deleted profiles.
|
||||
|
||||
/// Removes the profile permanently
|
||||
/// It is invalid to call loadToxSave or saveToxSave on a deleted profile
|
||||
/// Updates the profiles vector
|
||||
void remove(); |
||||
|
||||
/// Tries to rename the profile
|
||||
bool rename(QString newName); |
||||
|
||||
/// Scan for profile, automatically importing them if needed
|
||||
/// NOT thread-safe
|
||||
static void scanProfiles(); |
||||
static QVector<QString> getProfiles(); |
||||
|
||||
static bool profileExists(QString name); |
||||
static bool isEncrypted(QString name); ///< Returns false on error. Checks the actual file on disk.
|
||||
|
||||
private: |
||||
Profile(QString name, QString password, bool newProfile); |
||||
/// Lists all the files in the config dir with a given extension
|
||||
/// Pass the raw extension, e.g. "jpeg" not ".jpeg".
|
||||
static QVector<QString> getFilesByExt(QString extension); |
||||
/// Creates a .ini file for the given .tox profile
|
||||
/// Only pass the basename, without extension
|
||||
static void importProfile(QString name); |
||||
|
||||
private: |
||||
Core* core; |
||||
QThread* coreThread; |
||||
QString name, password; |
||||
bool newProfile; ///< True if this is a newly created profile, with no .tox save file yet.
|
||||
bool isRemoved; ///< True if the profile has been removed by remove()
|
||||
static QVector<QString> profiles; |
||||
/// How much data we need to read to check if the file is encrypted
|
||||
/// Must be >= TOX_ENC_SAVE_MAGIC_LENGTH (8), which isn't publicly defined
|
||||
static constexpr int encryptHeaderSize = 8; |
||||
}; |
||||
|
||||
#endif // PROFILE_H
|
@ -0,0 +1,167 @@
@@ -0,0 +1,167 @@
|
||||
#include "loginscreen.h" |
||||
#include "ui_loginscreen.h" |
||||
#include "src/profile.h" |
||||
#include "src/profilelocker.h" |
||||
#include "src/nexus.h" |
||||
#include "src/misc/settings.h" |
||||
#include "src/widget/form/setpassworddialog.h" |
||||
#include <QMessageBox> |
||||
#include <QDebug> |
||||
|
||||
LoginScreen::LoginScreen(QWidget *parent) : |
||||
QWidget(parent), |
||||
ui(new Ui::LoginScreen) |
||||
{ |
||||
ui->setupUi(this); |
||||
|
||||
connect(ui->newProfilePgbtn, &QPushButton::clicked, this, &LoginScreen::onNewProfilePageClicked); |
||||
connect(ui->loginPgbtn, &QPushButton::clicked, this, &LoginScreen::onLoginPageClicked); |
||||
connect(ui->createAccountButton, &QPushButton::clicked, this, &LoginScreen::onCreateNewProfile); |
||||
connect(ui->newUsername, &QLineEdit::returnPressed, this, &LoginScreen::onCreateNewProfile); |
||||
connect(ui->newPass, &QLineEdit::returnPressed, this, &LoginScreen::onCreateNewProfile); |
||||
connect(ui->newPassConfirm, &QLineEdit::returnPressed, this, &LoginScreen::onCreateNewProfile); |
||||
connect(ui->loginButton, &QPushButton::clicked, this, &LoginScreen::onLogin); |
||||
connect(ui->loginUsernames, &QComboBox::currentTextChanged, this, &LoginScreen::onLoginUsernameSelected); |
||||
connect(ui->loginPassword, &QLineEdit::returnPressed, this, &LoginScreen::onLogin); |
||||
connect(ui->newPass, &QLineEdit::textChanged, this, &LoginScreen::onPasswordEdited); |
||||
connect(ui->newPassConfirm, &QLineEdit::textChanged, this, &LoginScreen::onPasswordEdited); |
||||
|
||||
reset(); |
||||
} |
||||
|
||||
LoginScreen::~LoginScreen() |
||||
{ |
||||
delete ui; |
||||
} |
||||
|
||||
void LoginScreen::reset() |
||||
{ |
||||
ui->newUsername->clear(); |
||||
ui->newPass->clear(); |
||||
ui->newPassConfirm->clear(); |
||||
ui->loginPassword->clear(); |
||||
|
||||
ui->loginUsernames->clear(); |
||||
Profile::scanProfiles(); |
||||
QString lastUsed = Settings::getInstance().getCurrentProfile(); |
||||
qDebug() << "Last used is "<<lastUsed; |
||||
QVector<QString> profiles = Profile::getProfiles(); |
||||
for (QString profile : profiles) |
||||
{ |
||||
ui->loginUsernames->addItem(profile); |
||||
if (profile == lastUsed) |
||||
ui->loginUsernames->setCurrentIndex(ui->loginUsernames->count()-1); |
||||
} |
||||
|
||||
if (profiles.isEmpty()) |
||||
ui->stackedWidget->setCurrentIndex(0); |
||||
else |
||||
ui->stackedWidget->setCurrentIndex(1); |
||||
} |
||||
|
||||
void LoginScreen::onNewProfilePageClicked() |
||||
{ |
||||
ui->stackedWidget->setCurrentIndex(0); |
||||
} |
||||
|
||||
void LoginScreen::onLoginPageClicked() |
||||
{ |
||||
ui->stackedWidget->setCurrentIndex(1); |
||||
} |
||||
|
||||
void LoginScreen::onCreateNewProfile() |
||||
{ |
||||
QString name = ui->newUsername->text(); |
||||
QString pass = ui->newPass->text(); |
||||
|
||||
if (name.isEmpty()) |
||||
{ |
||||
QMessageBox::critical(this, tr("Couldn't create a new profile"), tr("The username must not be empty.")); |
||||
return; |
||||
} |
||||
|
||||
if (pass.size()!=0 && pass.size() < 6) |
||||
{ |
||||
QMessageBox::critical(this, tr("Couldn't create a new profile"), tr("The password must be at least 6 characters.")); |
||||
return; |
||||
} |
||||
|
||||
if (ui->newPassConfirm->text() != pass) |
||||
{ |
||||
QMessageBox::critical(this, tr("Couldn't create a new profile"), tr("The passwords are different.")); |
||||
return; |
||||
} |
||||
|
||||
if (Profile::profileExists(name)) |
||||
{ |
||||
QMessageBox::critical(this, tr("Couldn't create a new profile"), tr("This profile already exists.")); |
||||
return; |
||||
} |
||||
|
||||
Profile* profile = Profile::createProfile(name, pass); |
||||
if (!profile) |
||||
{ |
||||
// Unknown error
|
||||
QMessageBox::critical(this, tr("Couldn't create a new profile"), tr("Couldn't create a new profile.")); |
||||
return; |
||||
} |
||||
|
||||
Nexus& nexus = Nexus::getInstance(); |
||||
|
||||
nexus.setProfile(profile); |
||||
nexus.showMainGUI(); |
||||
} |
||||
|
||||
void LoginScreen::onLoginUsernameSelected(const QString &name) |
||||
{ |
||||
if (name.isEmpty()) |
||||
return; |
||||
|
||||
ui->loginPassword->clear(); |
||||
if (Profile::isEncrypted(name)) |
||||
{ |
||||
ui->loginPasswordLabel->show(); |
||||
ui->loginPassword->show(); |
||||
} |
||||
else |
||||
{ |
||||
ui->loginPasswordLabel->hide(); |
||||
ui->loginPassword->hide(); |
||||
} |
||||
} |
||||
|
||||
void LoginScreen::onLogin() |
||||
{ |
||||
QString name = ui->loginUsernames->currentText(); |
||||
QString pass = ui->loginPassword->text(); |
||||
|
||||
if (!ProfileLocker::isLockable(name)) |
||||
{ |
||||
QMessageBox::critical(this, tr("Couldn't load this profile"), tr("This profile is already in use.")); |
||||
return; |
||||
} |
||||
|
||||
Profile* profile = Profile::loadProfile(name, pass); |
||||
if (!profile) |
||||
{ |
||||
// Unknown error
|
||||
QMessageBox::critical(this, tr("Couldn't load this profile"), tr("Couldn't load this profile.")); |
||||
return; |
||||
} |
||||
if (!profile->checkPassword()) |
||||
{ |
||||
QMessageBox::critical(this, tr("Couldn't load this profile"), tr("Wrong password.")); |
||||
delete profile; |
||||
return; |
||||
} |
||||
|
||||
Nexus& nexus = Nexus::getInstance(); |
||||
|
||||
nexus.setProfile(profile); |
||||
nexus.showMainGUI(); |
||||
} |
||||
|
||||
void LoginScreen::onPasswordEdited() |
||||
{ |
||||
ui->passStrengthMeter->setValue(SetPasswordDialog::getPasswordStrength(ui->newPass->text())); |
||||
} |
@ -0,0 +1,33 @@
@@ -0,0 +1,33 @@
|
||||
#ifndef LOGINSCREEN_H |
||||
#define LOGINSCREEN_H |
||||
|
||||
#include <QWidget> |
||||
|
||||
namespace Ui { |
||||
class LoginScreen; |
||||
} |
||||
|
||||
class LoginScreen : public QWidget |
||||
{ |
||||
Q_OBJECT |
||||
|
||||
public: |
||||
explicit LoginScreen(QWidget *parent = 0); |
||||
~LoginScreen(); |
||||
void reset(); ///< Resets the UI, clears all fields
|
||||
|
||||
private slots: |
||||
void onLoginUsernameSelected(const QString& name); |
||||
void onPasswordEdited(); |
||||
// Buttons to change page
|
||||
void onNewProfilePageClicked(); |
||||
void onLoginPageClicked(); |
||||
// Buttons to submit form
|
||||
void onCreateNewProfile(); |
||||
void onLogin(); |
||||
|
||||
private: |
||||
Ui::LoginScreen *ui; |
||||
}; |
||||
|
||||
#endif // LOGINSCREEN_H
|
Loading…
Reference in new issue