1
0
mirror of https://github.com/qTox/qTox.git synced 2024-03-22 14:00:36 +08:00

docs(persistence): Change comment style

This commit is contained in:
Diadlo 2016-07-27 01:20:54 +03:00
parent 1552bfb114
commit 1c547fc73f
No known key found for this signature in database
GPG Key ID: 5AF9F2E29107C727
24 changed files with 601 additions and 173 deletions

View File

@ -29,6 +29,11 @@
#include <QDebug>
#include <QSqlError>
/**
@var static TOX_PASS_KEY EncryptedDb::decryptionKey
@note When importing, the decryption key may not be the same as the profile key
*/
qint64 EncryptedDb::encryptedChunkSize = 4096;
qint64 EncryptedDb::plainChunkSize = EncryptedDb::encryptedChunkSize - TOX_PASS_ENCRYPTION_EXTRA_LENGTH;

View File

@ -46,7 +46,7 @@ private:
static qint64 plainChunkSize;
static qint64 encryptedChunkSize;
static TOX_PASS_KEY decryptionKey; ///< When importing, the decryption key may not be the same as the profile key
static TOX_PASS_KEY decryptionKey;
qint64 chunkPosition;
QByteArray buffer;

View File

@ -14,6 +14,60 @@
#include <sqlcipher/sqlite3.h>
/**
@class RawDatabase
@brief Implements a low level RAII interface to a SQLCipher (SQlite3) database.
Thread-safe, does all database operations on a worker thread.
The queries must not contain transaction commands (BEGIN/COMMIT/...) or the behavior is undefined.
@var QMutex RawDatabase::transactionsMutex;
@brief Protects pendingTransactions
*/
/**
@class Query
@brief A query to be executed by the database.
Can be composed of one or more SQL statements in the query,
optional BLOB parameters to be bound, and callbacks fired when the query is executed
Calling any database method from a query callback is undefined behavior.
@var QByteArray RawDatabase::Query::query
@brief UTF-8 query string
@var QVector<QByteArray> RawDatabase::Query::blobs
@brief Bound data blobs
@var std::function<void(int64_t)> RawDatabase::Query::insertCallback
@brief Called after execution with the last insert rowid
@var std::function<void(const QVector<QVariant>&)> RawDatabase::Query::rowCallback
@brief Called during execution for each row
@var QVector<sqlite3_stmt*> RawDatabase::Query::statements
@brief Statements to be compiled from the query
*/
/**
@struct Transaction
@brief SQL transactions to be processed.
A transaction is made of queries, which can have bound BLOBs.
@var std::atomic_bool* RawDatabase::Transaction::success = nullptr;
@brief If not a nullptr, the result of the transaction will be set
@var std::atomic_bool* RawDatabase::Transaction::done = nullptr;
@brief If not a nullptr, will be set to true when the transaction has been executed
*/
/**
@brief Tries to open a database.
@param path Path to database.
@param password If empty, the database will be opened unencrypted.
Otherwise we will use toxencryptsave to derive a key and encrypt the database.
*/
RawDatabase::RawDatabase(const QString &path, const QString& password)
: workerThread{new QThread}, path{path}, currentHexKey{deriveKey(password)}
{
@ -33,6 +87,12 @@ RawDatabase::~RawDatabase()
workerThread->wait(50);
}
/**
@brief Tries to open the database with the given (possibly empty) key.
@param path Path to database.
@param hexKey Hex representation of the key in string.
@return True if success, false otherwise.
*/
bool RawDatabase::open(const QString& path, const QString &hexKey)
{
if (QThread::currentThread() != workerThread.get())
@ -75,6 +135,9 @@ bool RawDatabase::open(const QString& path, const QString &hexKey)
return true;
}
/**
@brief Close the database and free its associated resources.
*/
void RawDatabase::close()
{
if (QThread::currentThread() != workerThread.get())
@ -89,22 +152,41 @@ void RawDatabase::close()
qWarning() << "Error closing database:"<<sqlite3_errmsg(sqlite);
}
/**
@brief Checks, that the database is open.
@return True if the database was opened successfully.
*/
bool RawDatabase::isOpen()
{
// We don't need thread safety since only the ctor/dtor can write this pointer
return sqlite != nullptr;
}
/**
@brief Executes a SQL transaction synchronously.
@param statement Statement to execute.
@return Whether the transaction was successful.
*/
bool RawDatabase::execNow(const QString& statement)
{
return execNow(Query{statement});
}
/**
@brief Executes a SQL transaction synchronously.
@param statement Statement to execute.
@return Whether the transaction was successful.
*/
bool RawDatabase::execNow(const RawDatabase::Query &statement)
{
return execNow(QVector<Query>{statement});
}
/**
@brief Executes a SQL transaction synchronously.
@param statements List of statements to execute.
@return Whether the transaction was successful.
*/
bool RawDatabase::execNow(const QVector<RawDatabase::Query> &statements)
{
if (!sqlite)
@ -134,6 +216,10 @@ bool RawDatabase::execNow(const QVector<RawDatabase::Query> &statements)
return success.load(std::memory_order_acquire);
}
/**
@brief Executes a SQL transaction asynchronously.
@param statement Statement to execute.
*/
void RawDatabase::execLater(const QString &statement)
{
execLater(Query{statement});
@ -162,11 +248,20 @@ void RawDatabase::execLater(const QVector<RawDatabase::Query> &statements)
QMetaObject::invokeMethod(this, "process");
}
/**
@brief Waits until all the pending transactions are executed.
*/
void RawDatabase::sync()
{
QMetaObject::invokeMethod(this, "process", Qt::BlockingQueuedConnection);
}
/**
@brief Changes the database password, encrypting or decrypting if necessary.
@param password If password is empty, the database will be decrypted.
@return True if success, false otherwise.
@note Will process all transactions before changing the password.
*/
bool RawDatabase::setPassword(const QString& password)
{
if (!sqlite)
@ -260,6 +355,13 @@ bool RawDatabase::setPassword(const QString& password)
return true;
}
/**
@brief Moves the database file on disk to match the new path.
@param newPath Path to move database file.
@return True if success, false otherwise.
@note Will process all transactions before renaming
*/
bool RawDatabase::rename(const QString &newPath)
{
if (!sqlite)
@ -291,6 +393,11 @@ bool RawDatabase::rename(const QString &newPath)
return open(path, currentHexKey);
}
/**
@brief Deletes the on disk database file after closing it.
@note Will process all transactions before deletings.
@return True if success, false otherwise.
*/
bool RawDatabase::remove()
{
if (!sqlite)
@ -311,7 +418,11 @@ bool RawDatabase::remove()
return QFile::remove(path);
}
/**
@brief Derives a 256bit key from the password and returns it hex-encoded
@param password Password to decrypt database
@return String representation of key
*/
QString RawDatabase::deriveKey(const QString &password)
{
if (password.isEmpty())
@ -327,6 +438,12 @@ QString RawDatabase::deriveKey(const QString &password)
return QByteArray((char*)key.key, 32).toHex();
}
/**
@brief Implements the actual processing of pending transactions.
Unqueues, compiles, binds and executes queries, then notifies of results
@warning MUST only be called from the worker thread
*/
void RawDatabase::process()
{
assert(QThread::currentThread() == workerThread.get());
@ -460,6 +577,12 @@ void RawDatabase::process()
}
}
/**
@brief Extracts a variant from one column of a result row depending on the column type.
@param stmt Statement to execute.
@param col Number of column to extract.
@return Extracted data.
*/
QVariant RawDatabase::extractData(sqlite3_stmt *stmt, int col)
{
int type = sqlite3_column_type(stmt, col);

View File

@ -15,17 +15,11 @@
struct sqlite3;
struct sqlite3_stmt;
/// Implements a low level RAII interface to a SQLCipher (SQlite3) database
/// Thread-safe, does all database operations on a worker thread
/// The queries must not contain transaction commands (BEGIN/COMMIT/...) or the behavior is undefined
class RawDatabase : QObject
{
Q_OBJECT
public:
/// A query to be executed by the database. Can be composed of one or more SQL statements in the query,
/// optional BLOB parameters to be bound, and callbacks fired when the query is executed
/// Calling any database method from a query callback is undefined behavior
class Query
{
public:
@ -37,71 +31,49 @@ public:
: query{query.toUtf8()}, rowCallback{rowCallback} {}
Query() = default;
private:
QByteArray query; ///< UTF-8 query string
QVector<QByteArray> blobs; ///< Bound data blobs
std::function<void(int64_t)> insertCallback; ///< Called after execution with the last insert rowid
std::function<void(const QVector<QVariant>&)> rowCallback; ///< Called during execution for each row
QVector<sqlite3_stmt*> statements; ///< Statements to be compiled from the query
QByteArray query;
QVector<QByteArray> blobs;
std::function<void(int64_t)> insertCallback;
std::function<void(const QVector<QVariant>&)> rowCallback;
QVector<sqlite3_stmt*> statements;
friend class RawDatabase;
};
public:
/// Tries to open a database
/// If password is empty, the database will be opened unencrypted
/// Otherwise we will use toxencryptsave to derive a key and encrypt the database
RawDatabase(const QString& path, const QString& password);
~RawDatabase();
bool isOpen(); ///< Returns true if the database was opened successfully
/// Executes a SQL transaction synchronously.
/// Returns whether the transaction was successful.
bool isOpen();
bool execNow(const QString& statement);
bool execNow(const Query& statement);
bool execNow(const QVector<Query>& statements);
/// Executes a SQL transaction asynchronously.
void execLater(const QString& statement);
void execLater(const Query& statement);
void execLater(const QVector<Query>& statements);
/// Waits until all the pending transactions are executed
void sync();
public slots:
/// Changes the database password, encrypting or decrypting if necessary
/// If password is empty, the database will be decrypted
/// Will process all transactions before changing the password
bool setPassword(const QString& password);
/// Moves the database file on disk to match the new path
/// Will process all transactions before renaming
bool rename(const QString& newPath);
/// Deletes the on disk database file after closing it
/// Will process all transactions before deletings
bool remove();
protected slots:
/// Tries to open the database with the given (possibly empty) key
bool open(const QString& path, const QString& hexKey = {});
/// Closes the database and free its associated resources
void close();
/// Implements the actual processing of pending transactions
/// Unqueues, compiles, binds and executes queries, then notifies of results
/// MUST only be called from the worker thread
void process();
protected:
/// Derives a 256bit key from the password and returns it hex-encoded
static QString deriveKey(const QString &password);
/// Extracts a variant from one column of a result row depending on the column type
static QVariant extractData(sqlite3_stmt* stmt, int col);
private:
/// SQL transactions to be processed
/// A transaction is made of queries, which can have bound BLOBs
struct Transaction
{
QVector<Query> queries;
/// If not a nullptr, the result of the transaction will be set
std::atomic_bool* success = nullptr;
/// If not a nullptr, will be set to true when the transaction has been executed
std::atomic_bool* done = nullptr;
};
@ -109,7 +81,6 @@ private:
sqlite3* sqlite;
std::unique_ptr<QThread> workerThread;
QQueue<Transaction> pendingTransactions;
/// Protects pendingTransactions
QMutex transactionsMutex;
QString path;
QString currentHexKey;

View File

@ -8,12 +8,32 @@
using namespace std;
/**
@class History
@brief Interacts with the profile database to save the chat history.
@var QHash<QString, int64_t> History::peers
@brief Maps friend public keys to unique IDs by index.
Caches mappings to speed up message saving.
*/
/**
@brief Opens the profile database and prepares to work with the history.
@param profileName Profile name to load.
@param password If empty, the database will be opened unencrypted.
*/
History::History(const QString &profileName, const QString &password)
: db{getDbPath(profileName), password}
{
init();
}
/**
@brief Opens the profile database, and import from the old database.
@param profileName Profile name to load.
@param password If empty, the database will be opened unencrypted.
@param oldHistory Old history to import.
*/
History::History(const QString &profileName, const QString &password, const HistoryKeeper &oldHistory)
: History{profileName, password}
{
@ -27,26 +47,45 @@ History::~History()
db.sync();
}
/**
@brief Checks if the database was opened successfully
@return True if database if opened, false otherwise.
*/
bool History::isValid()
{
return db.isOpen();
}
/**
@brief Changes the database password, will encrypt or decrypt if necessary.
@param password Password to set.
*/
void History::setPassword(const QString& password)
{
db.setPassword(password);
}
/**
@brief Moves the database file on disk to match the new name.
@param newName New name.
*/
void History::rename(const QString &newName)
{
db.rename(getDbPath(newName));
}
/**
@brief Deletes the on-disk database file.
@return True if success, false otherwise.
*/
bool History::remove()
{
return db.remove();
}
/**
@brief Erases all the chat history from the database.
*/
void History::eraseHistory()
{
db.execNow("DELETE FROM faux_offline_pending;"
@ -56,6 +95,10 @@ void History::eraseHistory()
"VACUUM;");
}
/**
@brief Erases the chat history with one friend.
@param friendPk Friend public key to erase.
*/
void History::removeFriendHistory(const QString &friendPk)
{
if (!peers.contains(friendPk))
@ -81,6 +124,16 @@ void History::removeFriendHistory(const QString &friendPk)
}
}
/**
@brief Generate query to insert new message in database
@param friendPk Friend publick key to save.
@param message Message to save.
@param sender Sender to save.
@param time Time of message sending.
@param isSent True if message was already sent.
@param dispName Name, which should be displayed.
@param insertIdCallback Function, called after query execution.
*/
QVector<RawDatabase::Query> History::generateNewMessageQueries(const QString &friendPk, const QString &message,
const QString &sender, const QDateTime &time, bool isSent, QString dispName,
std::function<void(int64_t)> insertIdCallback)
@ -139,12 +192,29 @@ QVector<RawDatabase::Query> History::generateNewMessageQueries(const QString &fr
return queries;
}
/**
@brief Saves a chat message in the database.
@param friendPk Friend publick key to save.
@param message Message to save.
@param sender Sender to save.
@param time Time of message sending.
@param isSent True if message was already sent.
@param dispName Name, which should be displayed.
@param insertIdCallback Function, called after query execution.
*/
void History::addNewMessage(const QString &friendPk, const QString &message, const QString &sender,
const QDateTime &time, bool isSent, QString dispName, std::function<void(int64_t)> insertIdCallback)
{
db.execLater(generateNewMessageQueries(friendPk, message, sender, time, isSent, dispName, insertIdCallback));
}
/**
@brief Fetches chat messages from the database.
@param friendPk Friend publick key to fetch.
@param from Start of period to fetch.
@param to End of period to fetch.
@return List of messages.
*/
QList<History::HistMessage> History::getChatHistory(const QString &friendPk, const QDateTime &from, const QDateTime &to)
{
QList<HistMessage> messages;
@ -174,16 +244,30 @@ QList<History::HistMessage> History::getChatHistory(const QString &friendPk, con
return messages;
}
/**
@brief Marks a message as sent.
Removing message from the faux-offline pending messages list.
@param id Message ID.
*/
void History::markAsSent(qint64 id)
{
db.execLater(QString("DELETE FROM faux_offline_pending WHERE id=%1;").arg(id));
}
/**
@brief Retrieves the path to the database file for a given profile.
@param profileName Profile name.
@return Path to database.
*/
QString History::getDbPath(const QString &profileName)
{
return Settings::getInstance().getSettingsDirPath() + profileName + ".db";
}
/**
@brief Makes sure the history tables are created
*/
void History::init()
{
if (!isValid())
@ -207,6 +291,10 @@ void History::init()
}});
}
/**
@brief Imports messages from the old history file.
@param oldHistory Old history to import.
*/
void History::import(const HistoryKeeper &oldHistory)
{
if (!isValid())

View File

@ -12,7 +12,6 @@ class Profile;
class HistoryKeeper;
class RawDatabase;
/// Interacts with the profile database to save the chat history
class History
{
public:
@ -31,40 +30,26 @@ public:
};
public:
/// Opens the profile database and prepares to work with the history
/// If password is empty, the database will be opened unencrypted
History(const QString& profileName, const QString& password);
/// Opens the profile database, and import from the old database
/// If password is empty, the database will be opened unencrypted
History(const QString& profileName, const QString& password, const HistoryKeeper& oldHistory);
~History();
/// Checks if the database was opened successfully
bool isValid();
/// Imports messages from the old history file
void import(const HistoryKeeper& oldHistory);
/// Changes the database password, will encrypt or decrypt if necessary
void setPassword(const QString& password);
/// Moves the database file on disk to match the new name
void rename(const QString& newName);
/// Deletes the on-disk database file
bool remove();
/// Erases all the chat history from the database
void eraseHistory();
/// Erases the chat history with one friend
void removeFriendHistory(const QString& friendPk);
/// Saves a chat message in the database
void addNewMessage(const QString& friendPk, const QString& message, const QString& sender,
const QDateTime &time, bool isSent, QString dispName,
std::function<void(int64_t)> insertIdCallback={});
/// Fetches chat messages from the database
QList<HistMessage> getChatHistory(const QString& friendPk, const QDateTime &from, const QDateTime &to);
/// Marks a message as sent, removing it from the faux-offline pending messages list
void markAsSent(qint64 id);
/// Retrieves the path to the database file for a given profile.
static QString getDbPath(const QString& profileName);
protected:
/// Makes sure the history tables are created
void init();
QVector<RawDatabase::Query> generateNewMessageQueries(const QString& friendPk, const QString& message,
const QString& sender, const QDateTime &time, bool isSent, QString dispName,
@ -72,8 +57,7 @@ protected:
private:
RawDatabase db;
// Cached mappings to speed up message saving
QHash<QString, int64_t> peers; ///< Maps friend public keys to unique IDs by index
QHash<QString, int64_t> peers;
};
#endif // HISTORY_H

View File

@ -35,9 +35,19 @@
#include "src/persistence/db/plaindb.h"
#include "src/persistence/db/encrypteddb.h"
/**
@class HistoryKeeper
@brief THIS IS A LEGACY CLASS KEPT FOR BACKWARDS COMPATIBILITY
@deprecated See the History class instead
@warning DO NOT USE!
*/
static HistoryKeeper *historyInstance = nullptr;
QMutex HistoryKeeper::historyMutex;
/**
@brief Returns the singleton instance.
*/
HistoryKeeper *HistoryKeeper::getInstance(const Profile& profile)
{
historyMutex.lock();

View File

@ -27,12 +27,6 @@
#include <QMutex>
#include <tox/toxencryptsave.h>
/**
* THIS IS A LEGACY CLASS KEPT FOR BACKWARDS COMPATIBILITY
* DO NOT USE!
* See the History class instead
*/
class Profile;
class GenericDdInterface;
namespace Db { enum class syncType; }

View File

@ -26,6 +26,15 @@
#include <QMutexLocker>
#include <QTimer>
/**
@var static const int OfflineMsgEngine::offlineTimeout
@brief timeout after which faux offline messages get to be re-sent.
Originally was 2s, but since that was causing lots of duplicated
messages on receiving end, make qTox be more lazy about re-sending
should be 20s.
*/
const int OfflineMsgEngine::offlineTimeout = 20000;
QMutex OfflineMsgEngine::globalMutex;

View File

@ -57,10 +57,6 @@ private:
QHash<int, int64_t> receipts;
QMap<int64_t, MsgPtr> undeliveredMsgs;
// timeout after which faux offline messages get to be re-sent
// originally was 2s, but since that was causing lots of duplicated
// messages on receiving end, make qTox be more lazy about re-sending
// should be 20s
static const int offlineTimeout;
};

View File

@ -35,6 +35,21 @@
#include <QDebug>
#include <sodium.h>
/**
@class Profile
@brief Manages user profiles.
@var bool Profile::newProfile
@brief True if this is a newly created profile, with no .tox save file yet.
@var bool Profile::isRemoved
@brief True if the profile has been removed by remove().
@var static constexpr int Profile::encryptHeaderSize = 8
@brief How much data we need to read to check if the file is encrypted.
@note Must be >= TOX_ENC_SAVE_MAGIC_LENGTH (8), which isn't publicly defined.
*/
QVector<QString> Profile::profiles;
Profile::Profile(QString name, const QString &password, bool isNewProfile)
@ -65,6 +80,14 @@ Profile::Profile(QString name, const QString &password, bool isNewProfile)
QObject::connect(coreThread, &QThread::started, core, &Core::start);
}
/**
@brief Locks and loads an existing profile and creates the associate Core* instance.
@param name Profile name.
@param password Profile password.
@return Returns a nullptr on error. Profile pointer otherwise.
@example If the profile is already in use return nullptr.
*/
Profile* Profile::loadProfile(QString name, const QString &password)
{
if (ProfileLocker::hasLock())
@ -142,6 +165,14 @@ Profile* Profile::loadProfile(QString name, const QString &password)
return p;
}
/**
@brief Creates a new profile and the associated Core* instance.
@param name Username.
@param password If password is not empty, the profile will be encrypted.
@return Returns a nullptr on error. Profile pointer otherwise.
@example If the profile is already in use return nullptr.
*/
Profile* Profile::createProfile(QString name, QString password)
{
if (ProfileLocker::hasLock())
@ -182,6 +213,11 @@ Profile::~Profile()
}
}
/**
@brief Lists all the files in the config dir with a given extension
@param extension Raw extension, e.g. "jpeg" not ".jpeg".
@return Vector of filenames.
*/
QVector<QString> Profile::getFilesByExt(QString extension)
{
QDir dir(Settings::getInstance().getSettingsDirPath());
@ -195,6 +231,10 @@ QVector<QString> Profile::getFilesByExt(QString extension)
return out;
}
/**
@brief Scan for profile, automatically importing them if needed.
@warning NOT thread-safe.
*/
void Profile::scanProfiles()
{
profiles.clear();
@ -222,6 +262,9 @@ QString Profile::getName() const
return name;
}
/**
@brief Starts the Core thread
*/
void Profile::startCore()
{
coreThread->start();
@ -232,6 +275,10 @@ bool Profile::isNewProfile()
return newProfile;
}
/**
@brief Loads the profile's .tox save from file, unencrypted
@return Byte array of loaded profile save.
*/
QByteArray Profile::loadToxSave()
{
assert(!isRemoved);
@ -290,6 +337,10 @@ fail:
return data;
}
/**
@brief Saves the profile's .tox save, encrypted if needed.
@warning Invalid on deleted profiles.
*/
void Profile::saveToxSave()
{
assert(core->isReady());
@ -298,6 +349,11 @@ void Profile::saveToxSave()
saveToxSave(data);
}
/**
@brief Write the .tox save, encrypted if needed.
@param data Byte array of profile save.
@warning Invalid on deleted profiles.
*/
void Profile::saveToxSave(QByteArray data)
{
assert(!isRemoved);
@ -340,6 +396,12 @@ void Profile::saveToxSave(QByteArray data)
}
}
/**
@brief Gets the path of the avatar file cached by this profile and corresponding to this owner ID.
@param ownerId Path to avatar of friend with this ID will returned.
@param forceUnencrypted If true, return the path to the plaintext file even if this is an encrypted profile.
@return Path to the avatar.
*/
QString Profile::avatarPath(const QString &ownerId, bool forceUnencrypted)
{
if (password.isEmpty() || forceUnencrypted)
@ -357,11 +419,20 @@ QString Profile::avatarPath(const QString &ownerId, bool forceUnencrypted)
return Settings::getInstance().getSettingsDirPath() + "avatars/" + hash.toHex().toUpper() + ".png";
}
/**
@brief Get our avatar from cache.
@return Avatar as QPixmap.
*/
QPixmap Profile::loadAvatar()
{
return loadAvatar(core->getSelfId().publicKey);
}
/**
@brief Get a contact's avatar from cache.
@param ownerId Friend ID to load avatar.
@return Avatar as QPixmap.
*/
QPixmap Profile::loadAvatar(const QString &ownerId)
{
QPixmap pic;
@ -369,11 +440,22 @@ QPixmap Profile::loadAvatar(const QString &ownerId)
return pic;
}
/**
@brief Get a contact's avatar from cache
@param ownerId Friend ID to load avatar.
@return Avatar as QByteArray.
*/
QByteArray Profile::loadAvatarData(const QString &ownerId)
{
return loadAvatarData(ownerId, password);
}
/**
@brief Get a contact's avatar from cache, with a specified profile password.
@param ownerId Friend ID to load avatar.
@param password Profile password to decrypt data.
@return Avatar as QByteArray.
*/
QByteArray Profile::loadAvatarData(const QString &ownerId, const QString &password)
{
QString path = avatarPath(ownerId);
@ -401,6 +483,11 @@ QByteArray Profile::loadAvatarData(const QString &ownerId, const QString &passwo
return pic;
}
/**
@brief Save an avatar to cache.
@param pic Picture to save.
@param ownerId ID of avatar owner.
*/
void Profile::saveAvatar(QByteArray pic, const QString &ownerId)
{
if (!password.isEmpty() && !pic.isEmpty())
@ -425,6 +512,11 @@ void Profile::saveAvatar(QByteArray pic, const QString &ownerId)
}
}
/**
@brief Get the tox hash of a cached avatar.
@param ownerId Friend ID to get hash.
@return Avatar tox hash.
*/
QByteArray Profile::getAvatarHash(const QString &ownerId)
{
QByteArray pic = loadAvatarData(ownerId);
@ -433,21 +525,36 @@ QByteArray Profile::getAvatarHash(const QString &ownerId)
return avatarHash;
}
/**
@brief Removes our own avatar.
*/
void Profile::removeAvatar()
{
removeAvatar(core->getSelfId().publicKey);
}
/**
@brief Checks that the history is enabled in the settings, and loaded successfully for this profile.
@return True if enabled, false otherwise.
*/
bool Profile::isHistoryEnabled()
{
return Settings::getInstance().getEnableLogging() && history;
}
/**
@brief Get chat history.
@return May return a nullptr if the history failed to load.
*/
History *Profile::getHistory()
{
return history.get();
}
/**
@brief Removes a cached avatar.
@param ownerId Friend ID whose avater to delete.
*/
void Profile::removeAvatar(const QString &ownerId)
{
QFile::remove(avatarPath(ownerId));
@ -461,11 +568,21 @@ bool Profile::exists(QString name)
return QFile::exists(path+".tox");
}
/**
@brief Checks, if profile has a password.
@return True if we have a password set (doesn't check the actual file on disk).
*/
bool Profile::isEncrypted() const
{
return !password.isEmpty();
}
/**
@brief Checks if profile is encrypted.
@note Checks the actual file on disk.
@param name Profile name.
@return True if profile is encrypted, false otherwise.
*/
bool Profile::isEncrypted(QString name)
{
uint8_t data[encryptHeaderSize] = {0};
@ -483,6 +600,12 @@ bool Profile::isEncrypted(QString name)
return tox_is_data_encrypted(data);
}
/**
@brief Removes the profile permanently.
Updates the profiles vector.
@return Vector of filenames that could not be removed.
@warning It is invalid to call loadToxSave or saveToxSave on a deleted profile.
*/
QVector<QString> Profile::remove()
{
if (isRemoved)
@ -546,6 +669,11 @@ QVector<QString> Profile::remove()
return ret;
}
/**
@brief Tries to rename the profile.
@param newName New name for the profile.
@return False on error, true otherwise.
*/
bool Profile::rename(QString newName)
{
QString path = Settings::getInstance().getSettingsDirPath() + name,
@ -568,6 +696,10 @@ bool Profile::rename(QString newName)
return true;
}
/**
@brief Checks whether the password is valid.
@return True, if password is valid, false otherwise.
*/
bool Profile::checkPassword()
{
if (isRemoved)
@ -586,6 +718,9 @@ const TOX_PASS_KEY& Profile::getPasskey() const
return passkey;
}
/**
@brief Delete core and restart a new one
*/
void Profile::restartCore()
{
GUI::setEnabled(false); // Core::reset re-enables it
@ -594,6 +729,10 @@ void Profile::restartCore()
QMetaObject::invokeMethod(core, "reset");
}
/**
@brief Changes the encryption password and re-saves everything with it
@param newPassword Password for encryption.
*/
void Profile::setPassword(const QString &newPassword)
{
QByteArray avatar = loadAvatarData(core->getSelfId().publicKey);

View File

@ -32,76 +32,54 @@
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, const 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() const;
void startCore(); ///< Starts the Core thread
void restartCore(); ///< Delete core and restart a new one
void startCore();
void restartCore();
bool isNewProfile();
bool isEncrypted() const; ///< Returns true if we have a password set (doesn't check the actual file on disk)
bool checkPassword(); ///< Checks whether the password is valid
bool isEncrypted() const;
bool checkPassword();
QString getPassword() const;
void setPassword(const QString &newPassword); ///< Changes the encryption password and re-saves everything with it
void setPassword(const QString &newPassword);
const TOX_PASS_KEY& getPasskey() const;
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.
QByteArray loadToxSave();
void saveToxSave();
void saveToxSave(QByteArray data);
QPixmap loadAvatar(); ///< Get our avatar from cache
QPixmap loadAvatar(const QString& ownerId); ///< Get a contact's avatar from cache
QByteArray loadAvatarData(const QString& ownerId); ///< Get a contact's avatar from cache
QByteArray loadAvatarData(const QString& ownerId, const QString& password); ///< Get a contact's avatar from cache, with a specified profile password.
void saveAvatar(QByteArray pic, const QString& ownerId); ///< Save an avatar to cache
QByteArray getAvatarHash(const QString& ownerId); ///< Get the tox hash of a cached avatar
void removeAvatar(const QString& ownerId); ///< Removes a cached avatar
void removeAvatar(); ///< Removes our own avatar
QPixmap loadAvatar();
QPixmap loadAvatar(const QString& ownerId);
QByteArray loadAvatarData(const QString& ownerId);
QByteArray loadAvatarData(const QString& ownerId, const QString& password);
void saveAvatar(QByteArray pic, const QString& ownerId);
QByteArray getAvatarHash(const QString& ownerId);
void removeAvatar(const QString& ownerId);
void removeAvatar();
/// Returns true if the history is enabled in the settings, and loaded successfully for this profile
bool isHistoryEnabled();
/// May return a nullptr if the history failed to load
History* getHistory();
/// Removes the profile permanently
/// It is invalid to call loadToxSave or saveToxSave on a deleted profile
/// Updates the profiles vector
/// Returns a vector of filenames that could not be removed.
QVector<QString> 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 exists(QString name);
static bool isEncrypted(QString name); ///< Returns false on error. Checks the actual file on disk.
static bool isEncrypted(QString name);
private:
Profile(QString name, const 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);
/// Gets the path of the avatar file cached by this profile and corresponding to this owner ID
/// If forceUnencrypted, we return the path to the plaintext file even if we're an encrypted profile
QString avatarPath(const QString& ownerId, bool forceUnencrypted = false);
private:
@ -110,11 +88,9 @@ private:
QString name, password;
TOX_PASS_KEY passkey;
std::unique_ptr<History> history;
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()
bool newProfile;
bool isRemoved;
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;
};

View File

@ -23,6 +23,14 @@
#include <QDir>
#include <QDebug>
/**
@class ProfileLocker
@brief Locks a Tox profile so that multiple instances can not use the same profile.
Only one lock can be acquired at the same time, which means
that there is little need for manually unlocking.
The current lock will expire if you exit or acquire a new one.
*/
using namespace std;
unique_ptr<QLockFile> ProfileLocker::lockfile;
@ -33,6 +41,14 @@ QString ProfileLocker::lockPathFromName(const QString& name)
return Settings::getInstance().getSettingsDirPath()+'/'+name+".lock";
}
/**
@brief Checks if a profile is currently locked by *another* instance.
If we own the lock, we consider it lockable.
There is no guarantee that the result will still be valid by the
time it is returned, this is provided on a best effort basis.
@param profile Profile name to check.
@return True, if profile locked, false otherwise.
*/
bool ProfileLocker::isLockable(QString profile)
{
// If we already have the lock, it's definitely lockable
@ -43,6 +59,11 @@ bool ProfileLocker::isLockable(QString profile)
return newLock.tryLock();
}
/**
@brief Tries to acquire the lock on a profile, will not block.
@param profile Profile to lock.
@return Returns true if we already own the lock.
*/
bool ProfileLocker::lock(QString profile)
{
if (lockfile && curLockName == profile)
@ -62,16 +83,26 @@ bool ProfileLocker::lock(QString profile)
return true;
}
/**
@brief Releases the lock on the current profile.
*/
void ProfileLocker::unlock()
{
if (!lockfile)
return;
lockfile->unlock();
delete lockfile.release();
lockfile = nullptr;
curLockName.clear();
}
/**
@brief Check that we actually own the lock.
In case the file was deleted on disk, restore it.
If we can't get a lock, exit qTox immediately.
If we never had a lock in the first place, exit immediately.
*/
void ProfileLocker::assertLock()
{
if (!lockfile)
@ -96,17 +127,28 @@ void ProfileLocker::assertLock()
}
}
/**
@brief Print an error then exit immediately.
*/
void ProfileLocker::deathByBrokenLock()
{
qCritical() << "Lock is *BROKEN*, exiting immediately";
abort();
}
/**
@brief Chacks, that profile locked.
@return Returns true if we're currently holding a lock.
*/
bool ProfileLocker::hasLock()
{
return lockfile.operator bool();
}
/**
@brief Get current locked profile name.
@return Return the name of the currently loaded profile, a null string if there is none.
*/
QString ProfileLocker::getCurLockName()
{
if (lockfile)

View File

@ -24,39 +24,22 @@
#include <QLockFile>
#include <memory>
/// Locks a Tox profile so that multiple instances can not use the same profile.
/// Only one lock can be acquired at the same time, which means
/// that there is little need for manually unlocking.
/// The current lock will expire if you exit or acquire a new one.
class ProfileLocker
{
private:
ProfileLocker()=delete;
public:
/// Checks if a profile is currently locked by *another* instance
/// If we own the lock, we consider it lockable
/// There is no guarantee that the result will still be valid by the
/// time it is returned, this is provided on a best effort basis
static bool isLockable(QString profile);
/// Tries to acquire the lock on a profile, will not block
/// Returns true if we already own the lock
static bool lock(QString profile);
/// Releases the lock on the current profile
static void unlock();
/// Returns true if we're currently holding a lock
static bool hasLock();
/// Return the name of the currently loaded profile, a null string if there is none
static QString getCurLockName();
/// Check that we actually own the lock
/// In case the file was deleted on disk, restore it
/// If we can't get a lock, exit qTox immediately
/// If we never had a lock in the firt place, exit immediately
static void assertLock();
private:
static QString lockPathFromName(const QString& name);
static void deathByBrokenLock(); ///< Print an error then exit immediately
static void deathByBrokenLock();
private:
static std::unique_ptr<QLockFile> lockfile;

View File

@ -17,9 +17,14 @@
along with qTox. If not, see <http://www.gnu.org/licenses/>.
*/
#include "src/persistence/serialize.h"
/**
@file serialize.cpp
Most of functions in this file are unsafe unless otherwise specified.
@warning Do not use them on untrusted data (e.g. check a signature first).
*/
QByteArray doubleToData(double num)
{
union

View File

@ -25,9 +25,6 @@
#include <QByteArray>
#include <QString>
/// Most of those functions are unsafe unless otherwise specified
/// Do not use them on untrusted data (e.g. check a signature first)
QByteArray doubleToData(double num);
QByteArray floatToData(float num);
float dataToFloat(QByteArray data);

View File

@ -49,6 +49,17 @@
#define SHOW_SYSTEM_TRAY_DEFAULT (bool) true
/**
@var QHash<QString, QByteArray> Settings::widgetSettings
@brief Assume all widgets have unique names
@warning Don't use it to save every single thing you want to save, use it
for some general purpose widgets, such as MainWindows or Splitters,
which have widget->saveX() and widget->loadX() methods.
@var QString Settings::toxmeInfo
@brief Toxme info like name@server
*/
const QString Settings::globalSettingsFile = "qtox.ini";
Settings* Settings::settings{nullptr};
QMutex Settings::bigLock{QMutex::Recursive};
@ -73,6 +84,9 @@ Settings::~Settings()
delete settingsThread;
}
/**
@brief Returns the singleton instance.
*/
Settings& Settings::getInstance()
{
if (!settings)
@ -379,6 +393,9 @@ void Settings::loadPersonal(Profile* profile)
ps.endGroup();
}
/**
@brief Asynchronous, saves the global settings.
*/
void Settings::saveGlobal()
{
if (QThread::currentThread() != settingsThread)
@ -498,11 +515,18 @@ void Settings::saveGlobal()
s.endGroup();
}
/**
@brief Asynchronous, saves the current profile.
*/
void Settings::savePersonal()
{
savePersonal(Nexus::getProfile());
}
/**
@brief Asynchronous, saves the profile.
@param profile Profile to save.
*/
void Settings::savePersonal(Profile* profile)
{
if (!profile)
@ -600,6 +624,10 @@ uint32_t Settings::makeProfileId(const QString& profile)
return dwords[0] ^ dwords[1] ^ dwords[2] ^ dwords[3];
}
/**
@brief Get path to directory, where the settings files are stored.
@return Path to settings directory, ends with a directory separator.
*/
QString Settings::getSettingsDirPath()
{
QMutexLocker locker{&bigLock};
@ -619,6 +647,10 @@ QString Settings::getSettingsDirPath()
#endif
}
/**
@brief Get path to directory, where the application data are stored.
@return Path to application data, ends with a directory separator.
*/
QString Settings::getAppDataDirPath()
{
QMutexLocker locker{&bigLock};
@ -640,6 +672,10 @@ QString Settings::getAppDataDirPath()
#endif
}
/**
@brief Get path to directory, where the application cache are stored.
@return Path to application cache, ends with a directory separator.
*/
QString Settings::getAppCacheDirPath()
{
QMutexLocker locker{&bigLock};
@ -1846,6 +1882,11 @@ void Settings::setAutoLogin(bool state)
autoLogin = state;
}
/**
@brief Write a default personal .ini settings file for a profile.
@param basename Filename without extension to save settings.
@example If basename is "profile", settings will be saved in profile.ini
*/
void Settings::createPersonal(QString basename)
{
QString path = getSettingsDirPath() + QDir::separator() + basename + ".ini";
@ -1862,6 +1903,9 @@ void Settings::createPersonal(QString basename)
ps.endGroup();
}
/**
@brief Creates a path to the settings dir, if it doesn't already exist
*/
void Settings::createSettingsDir()
{
QString dir = Settings::getSettingsDirPath();
@ -1870,6 +1914,9 @@ void Settings::createSettingsDir()
qCritical() << "Error while creating directory " << dir;
}
/**
@brief Waits for all asynchronous operations to complete
*/
void Settings::sync()
{
if (QThread::currentThread() != settingsThread)

View File

@ -44,15 +44,15 @@ class Settings : public QObject
public:
static Settings& getInstance();
static void destroyInstance();
QString getSettingsDirPath(); ///< The returned path ends with a directory separator
QString getAppDataDirPath(); ///< The returned path ends with a directory separator
QString getAppCacheDirPath(); ///< The returned path ends with a directory separator
QString getSettingsDirPath();
QString getAppDataDirPath();
QString getAppCacheDirPath();
void createSettingsDir(); ///< Creates a path to the settings dir, if it doesn't already exist
void createPersonal(QString basename); ///< Write a default personal .ini settings file for a profile
void createSettingsDir();
void createPersonal(QString basename);
void savePersonal(); ///< Asynchronous, saves the current profile
void savePersonal(Profile *profile); ///< Asynchronous
void savePersonal();
void savePersonal(Profile *profile);
void loadGlobal();
void loadPersonal();
@ -67,8 +67,8 @@ public:
public slots:
void saveGlobal(); ///< Asynchronous
void sync(); ///< Waits for all asynchronous operations to complete
void saveGlobal();
void sync();
signals:
void dhtServerListChanged();
@ -76,7 +76,6 @@ signals:
void emojiFontChanged();
public:
// Getter/setters
const QList<DhtServer>& getDhtServerList() const;
void setDhtServerList(const QList<DhtServer>& newDhtServerList);
@ -329,10 +328,6 @@ public:
void removeFriendRequest(int index);
void readFriendRequest(int index);
// Assume all widgets have unique names
// Don't use it to save every single thing you want to save, use it
// for some general purpose widgets, such as MainWindows or Splitters,
// which have widget->saveX() and widget->loadX() methods.
QByteArray getWidgetData(const QString& uniqueName) const;
void setWidgetData(const QString& uniqueName, const QByteArray& data);
@ -401,7 +396,7 @@ private:
uint32_t currentProfileId;
// Toxme Info
QString toxmeInfo; // name@server
QString toxmeInfo;
QString toxmeBio;
bool toxmePriv;
QString toxmePass;

View File

@ -28,8 +28,37 @@
#include <memory>
#include <cassert>
/**
@class SettingsSerializer
@brief Serializes a QSettings's data in an (optionally) encrypted binary format.
SettingsSerializer can detect regular .ini files and serialized ones,
it will read both regular and serialized .ini, but only save in serialized format.
The file is encrypted with the current profile's password, if any.
The file is only written to disk if save() is called, the destructor does not save to disk
All member functions are reentrant, but not thread safe.
@enum SettingsSerializer::RecordTag
@var Value
Followed by a QString key then a QVariant value
@var GroupStart
Followed by a QString group name
@var ArrayStart
Followed by a QString array name and a vuint array size
@var ArrayValue
Followed by a vuint array index, a QString key then a QVariant value
@var ArrayEnd
Not followed by any data
*/
enum class RecordTag : uint8_t
{
};
using namespace std;
/**
@var static const char magic[];
@brief Little endian ASCII "QTOX" magic
*/
const char SettingsSerializer::magic[] = {0x51,0x54,0x4F,0x58};
QDataStream& writeStream(QDataStream& dataStream, const SettingsSerializer::RecordTag& tag)
@ -199,6 +228,11 @@ SettingsSerializer::Value* SettingsSerializer::findValue(const QString& key)
return const_cast<Value*>(const_cast<const SettingsSerializer*>(this)->findValue(key));
}
/**
@brief Checks if the file is serialized settings.
@param filePath Path to file to check.
@return False on error, true otherwise.
*/
bool SettingsSerializer::isSerializedFormat(QString filePath)
{
QFile f(filePath);
@ -210,6 +244,9 @@ bool SettingsSerializer::isSerializedFormat(QString filePath)
return !memcmp(fmagic, magic, 4) || tox_is_data_encrypted((uint8_t*)fmagic);
}
/**
@brief Loads the settings from file.
*/
void SettingsSerializer::load()
{
if (isSerializedFormat(path))
@ -218,6 +255,9 @@ void SettingsSerializer::load()
readIni();
}
/**
@brief Saves the current settings back to file
*/
void SettingsSerializer::save()
{
QSaveFile f(path);
@ -545,6 +585,11 @@ void SettingsSerializer::readIni()
group = array = -1;
}
/**
@brief Remove group.
@note The group must be empty.
@param group ID of group to remove.
*/
void SettingsSerializer::removeGroup(int group)
{
assert(group<groups.size());

View File

@ -25,21 +25,15 @@
#include <QString>
#include <QDataStream>
/// Serializes a QSettings's data in an (optionally) encrypted binary format
/// SettingsSerializer can detect regular .ini files and serialized ones,
/// it will read both regular and serialized .ini, but only save in serialized format.
/// The file is encrypted with the current profile's password, if any.
/// The file is only written to disk if save() is called, the destructor does not save to disk
/// All member functions are reentrant, but not thread safe.
class SettingsSerializer
{
public:
SettingsSerializer(QString filePath, const QString &password=QString());
static bool isSerializedFormat(QString filePath); ///< Check if the file is serialized settings. False on error.
static bool isSerializedFormat(QString filePath);
void load(); ///< Loads the settings from file
void save(); ///< Saves the current settings back to file
void load();
void save();
void beginGroup(const QString &prefix);
void endGroup();
@ -55,15 +49,10 @@ public:
private:
enum class RecordTag : uint8_t
{
/// Followed by a QString key then a QVariant value
Value=0,
/// Followed by a QString group name
GroupStart=1,
/// Followed by a QString array name and a vuint array size
ArrayStart=2,
/// Followed by a vuint array index, a QString key then a QVariant value
ArrayValue=3,
/// Not followed by any data
ArrayEnd=4,
};
friend QDataStream& writeStream(QDataStream& dataStream, const SettingsSerializer::RecordTag& tag);
@ -94,7 +83,7 @@ private:
void readSerialized();
void readIni();
void removeValue(const QString& key);
void removeGroup(int group); ///< The group must be empty
void removeGroup(int group);
void writePackedVariant(QDataStream& dataStream, const QVariant& v);
private:
@ -104,7 +93,7 @@ private:
QVector<QString> groups;
QVector<Array> arrays;
QVector<Value> values;
static const char magic[]; ///< Little endian ASCII "QTOX" magic
static const char magic[];
};
#endif // SETTINGSSERIALIZER_H

View File

@ -35,6 +35,23 @@
#include <QStringBuilder>
#include <QtConcurrent/QtConcurrentRun>
/**
@class SmileyPack
@brief Maps emoticons to smileys.
@var QHash<QString, QString> SmileyPack::filenameTable
@brief Matches an emoticon to its corresponding smiley ie. ":)" -> "happy.png"
@var QHash<QString, QIcon> SmileyPack::iconCache
@brief representation of a smiley ie. "happy.png" -> data
@var QList<QStringList> SmileyPack::emoticons
@brief {{ ":)", ":-)" }, {":(", ...}, ... }
@var QString SmileyPack::path
@brief directory containing the cfg and image files
*/
SmileyPack::SmileyPack()
{
loadingMutex.lock();
@ -42,6 +59,9 @@ SmileyPack::SmileyPack()
connect(&Settings::getInstance(), &Settings::smileyPackChanged, this, &SmileyPack::onSmileyPackChanged);
}
/**
@brief Returns the singleton instance.
*/
SmileyPack& SmileyPack::getInstance()
{
static SmileyPack smileyPack;
@ -92,6 +112,12 @@ bool SmileyPack::isValid(const QString &filename)
return QFile(filename).exists();
}
/**
@brief Load smile pack
@note The caller must lock loadingMutex and should run it in a thread
@param filename Filename of smilepack.
@return False if cannot open file, true otherwise.
*/
bool SmileyPack::load(const QString& filename)
{
// discard old data

View File

@ -32,7 +32,6 @@
":/smileys", "./smileys", "/usr/share/qtox/smileys", "/usr/share/emoticons", "~/.kde4/share/emoticons", "~/.kde/share/emoticons" \
}
//maps emoticons to smileys
class SmileyPack : public QObject
{
Q_OBJECT
@ -54,14 +53,14 @@ private:
SmileyPack(SmileyPack&) = delete;
SmileyPack& operator=(const SmileyPack&) = delete;
bool load(const QString& filename); ///< The caller must lock loadingMutex and should run it in a thread
bool load(const QString& filename);
void cacheSmiley(const QString& name);
QIcon getCachedSmiley(const QString& key);
QHash<QString, QString> filenameTable; // matches an emoticon to its corresponding smiley ie. ":)" -> "happy.png"
QHash<QString, QIcon> iconCache; // representation of a smiley ie. "happy.png" -> data
QList<QStringList> emoticons; // {{ ":)", ":-)" }, {":(", ...}, ... }
QString path; // directory containing the cfg and image files
QHash<QString, QString> filenameTable;
QHash<QString, QIcon> iconCache;
QList<QStringList> emoticons;
QString path;
mutable QMutex loadingMutex;
};

View File

@ -33,6 +33,12 @@ bool toxSaveEventHandler(const QByteArray& eventData)
return true;
}
/**
@brief Import new profile.
@note Will wait until the core is ready first.
@param path Path to .tox file.
@return True if import success, false, otherwise.
*/
bool handleToxSave(const QString& path)
{
Core* core = Core::getInstance();
@ -59,7 +65,7 @@ bool handleToxSave(const QString& path)
return false;
}
QString profilePath = Settings::getInstance().getSettingsDirPath()+profile+Core::TOX_EXT;
QString profilePath = Settings::getInstance().getSettingsDirPath() + profile + Core::TOX_EXT;
if (QFileInfo(profilePath).exists() && !GUI::askQuestion(QObject::tr("Profile already exists", "import confirm title"),
QObject::tr("A profile named \"%1\" already exists. Do you want to erase it?", "import confirm text").arg(profile)))

View File

@ -23,7 +23,6 @@
class QString;
class QByteArray;
/// Will wait until the core is ready first
bool handleToxSave(const QString& path);
// Internals