feather/src/utils/Utils.cpp

612 lines
19 KiB
C++
Raw Normal View History

// SPDX-License-Identifier: BSD-3-Clause
2023-01-02 19:30:11 +00:00
// SPDX-FileCopyrightText: 2020-2023 The Monero Project
#include <QMessageBox>
#include <QtNetwork>
#include <QClipboard>
#include <QDesktopServices>
#include <QPushButton>
2023-03-28 20:42:12 +00:00
#include <QFontDatabase>
2021-07-04 03:40:31 +00:00
#include "constants.h"
#include "networktype.h"
2021-07-06 19:36:27 +00:00
#include "Utils.h"
2021-07-04 03:40:31 +00:00
#include "utils/ColorScheme.h"
#include "utils/config.h"
2021-05-25 13:06:38 +00:00
#include "utils/os/tails.h"
#include "utils/os/whonix.h"
2021-07-06 19:36:27 +00:00
namespace Utils {
bool fileExists(const QString &path) {
QFileInfo check_file(path);
return check_file.exists() && check_file.isFile();
}
2021-07-06 19:36:27 +00:00
QByteArray fileOpen(const QString &path) {
QFile file(path);
2021-07-06 19:36:27 +00:00
if (!file.open(QFile::ReadOnly | QFile::Text)) {
return QByteArray();
}
QByteArray data = file.readAll();
file.close();
return data;
}
2021-07-06 19:36:27 +00:00
QByteArray fileOpenQRC(const QString &path) {
QFile file(path);
2021-07-06 19:36:27 +00:00
if (!file.open(QIODevice::ReadOnly)) {
qDebug() << "error: " << file.errorString();
2021-07-06 19:36:27 +00:00
return QByteArray();
}
QByteArray data = file.readAll();
file.close();
return data;
}
2021-07-06 19:36:27 +00:00
bool fileWrite(const QString &path, const QString &data) {
QFile file(path);
2021-07-06 19:36:27 +00:00
if (file.open(QIODevice::WriteOnly)) {
2020-10-16 03:05:05 +00:00
QTextStream out(&file); out << data << Qt::endl;
file.close();
return true;
}
return false;
}
2021-07-06 19:36:27 +00:00
bool pixmapWrite(const QString &path, const QPixmap &pixmap) {
qDebug() << "Writing xdg icon: " << path;
QFile file(path);
QFileInfo iconInfo(file);
QDir().mkpath(iconInfo.path());
if(file.open(QIODevice::WriteOnly)){
pixmap.save(&file, "PNG");
file.close();
return true;
}
2021-07-06 19:36:27 +00:00
return false;
}
2022-03-04 10:05:20 +00:00
QStringList fileFind(const QRegularExpression &pattern, const QString &baseDir, int level, int depth, const int maxPerDir) {
// like `find /foo -name -maxdepth 2 "*.jpg"`
QStringList rtn;
QDir dir(baseDir);
dir.setFilter(QDir::Dirs | QDir::Files | QDir::NoSymLinks | QDir::NoDot | QDir::NoDotDot);
int fileCount = 0;
2021-07-06 19:36:27 +00:00
for (const auto &fileInfo: dir.entryInfoList({"*"})) {
fileCount += 1;
if(fileCount > maxPerDir) return rtn;
if(!fileInfo.isReadable())
continue;
const auto fn = fileInfo.fileName();
const auto path = fileInfo.filePath();
2022-03-04 10:05:20 +00:00
QRegularExpression re(QRegularExpression::anchoredPattern(pattern.pattern()));
QRegularExpressionMatch match = re.match(fn);
if (fileInfo.isDir()) {
if (level + 1 <= depth)
2021-07-06 19:36:27 +00:00
rtn << fileFind(pattern, path, level + 1, depth, maxPerDir);
}
2022-03-04 10:05:20 +00:00
else if (match.hasMatch()) {
rtn << path;
2022-03-04 10:05:20 +00:00
}
}
return rtn;
}
2021-07-06 19:36:27 +00:00
bool dirExists(const QString &path) {
QDir pathDir(path);
return pathDir.exists();
}
2022-06-22 19:06:32 +00:00
QString portablePath() {
return Utils::applicationPath() + "/feather_data";
}
bool isPortableMode() {
return Utils::portableFileExists(Utils::applicationPath());
}
2022-05-22 17:52:03 +00:00
bool portableFileExists(const QString &dir) {
QStringList portableFiles = {".portable", ".portable.txt", "portable.txt"};
return std::find_if(portableFiles.begin(), portableFiles.end(), [dir](const QString &portableFile){
return QFile::exists(dir + "/" + portableFile);
}) != portableFiles.end();
}
2021-07-06 19:36:27 +00:00
QString defaultWalletDir() {
2022-06-22 19:06:32 +00:00
if (Utils::isPortableMode()) {
return Utils::portablePath() + "/wallets";
2021-07-06 19:36:27 +00:00
}
if (TailsOS::detect()) {
QString path = []{
// Starting in 1.1.0 the wallet and config directory were moved from ./.feather to ./feather_data
// A user might accidentally delete the folder containing the file hidden folder after moving the AppImage
// We return the old path if it still exists
2021-07-06 19:36:27 +00:00
QString appImagePath = qgetenv("APPIMAGE");
if (appImagePath.isEmpty()) {
qDebug() << "Not an appimage, using currentPath()";
if (QDir(QDir::currentPath() + "/.feather").exists()) {
return QDir::currentPath() + "/.feather/Monero/wallets";
}
return QDir::currentPath() + "/feather_data/wallets";
2021-07-06 19:36:27 +00:00
}
QFileInfo appImageDir(appImagePath);
QString absolutePath = appImageDir.absoluteDir().path();
if (QDir(absolutePath + "/.feather").exists()) {
return absolutePath + "/.feather/Monero/wallets";
}
return absolutePath + "/feather_data/wallets";
2021-07-06 19:36:27 +00:00
}();
return path;
}
#if defined(Q_OS_LINUX) or defined(Q_OS_MAC)
return QString("%1/Monero/wallets").arg(QDir::homePath());
#elif defined(Q_OS_WIN)
return QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation) + "/Monero/wallets";
#endif
2021-07-06 19:36:27 +00:00
}
2022-06-22 19:06:32 +00:00
QString ringDatabasePath() {
if (Utils::isPortableMode()) {
QString suffix = "";
if (constants::networkType != NetworkType::Type::MAINNET) {
suffix = "-" + Utils::QtEnumToString(constants::networkType);
}
return Utils::portablePath() + "/ringdb" + suffix;
}
return ""; // Use libwallet default
}
2021-10-28 16:55:47 +00:00
QString applicationPath() {
2022-06-22 19:06:32 +00:00
QString applicationPath = QCoreApplication::applicationDirPath();
QDir appDir(applicationPath);
#ifdef Q_OS_MACOS
// applicationDirPath will be inside the app bundle
if (applicationPath.endsWith("Contents/MacOS")) {
appDir.cd("../../..");
2021-10-28 16:55:47 +00:00
}
2022-06-22 19:06:32 +00:00
return appDir.absolutePath();
#endif
QString appimagePath = qgetenv("APPIMAGE");
if (!appimagePath.isEmpty()) {
return QFileInfo(appimagePath).absoluteDir().path();
}
2021-10-28 16:55:47 +00:00
return applicationPath;
}
2021-07-06 19:36:27 +00:00
bool validateJSON(const QByteArray &blob) {
QJsonDocument doc = QJsonDocument::fromJson(blob);
QString jsonString = doc.toJson(QJsonDocument::Indented);
return !jsonString.isEmpty();
}
bool readJsonFile(QIODevice &device, QSettings::SettingsMap &map) {
QJsonDocument json = QJsonDocument::fromJson(device.readAll());
map = json.object().toVariantMap();
return true;
}
bool writeJsonFile(QIODevice &device, const QSettings::SettingsMap &map) {
device.write(QJsonDocument(QJsonObject::fromVariantMap(map)).toJson(QJsonDocument::Indented));
return true;
}
void copyToClipboard(const QString &string){
QClipboard * clipboard = QApplication::clipboard();
if (!clipboard) {
qWarning() << "Unable to access clipboard";
return;
}
clipboard->setText(string, QClipboard::Clipboard);
if (clipboard->supportsSelection())
clipboard->setText(string, QClipboard::Selection);
#if defined(Q_OS_LINUX)
QThread::msleep(1);
#endif
}
2021-07-06 19:36:27 +00:00
QString copyFromClipboard() {
2020-10-16 03:05:05 +00:00
QClipboard * clipboard = QApplication::clipboard();
if (!clipboard) {
qWarning() << "Unable to access clipboard";
return "";
}
return clipboard->text();
}
2023-03-28 20:42:12 +00:00
void copyColumn(QModelIndex *index, int column) {
QString string(index->model()->data(index->siblingAtColumn(column), Qt::UserRole).toString());
QClipboard * clipboard = QApplication::clipboard();
if (!clipboard) {
qWarning() << "Unable to access clipboard";
return;
}
clipboard->setText(string);
}
2021-07-06 19:36:27 +00:00
QString xdgDesktopEntry(){
return QString(
2021-07-06 19:36:27 +00:00
"[Desktop Entry]\n"
"Name=Feather\n"
"GenericName=Feather\n"
"X-GNOME-FullName=Feather\n"
2023-02-06 12:27:31 +00:00
"Comment=a free, open source Monero desktop wallet\n"
2021-07-06 19:36:27 +00:00
"Keywords=Monero;\n"
"Exec=\"%1\" %u\n"
"Terminal=false\n"
"Type=Application\n"
2023-02-06 12:27:31 +00:00
"Icon=feather\n"
2021-07-06 19:36:27 +00:00
"Categories=Network;GNOME;Qt;\n"
2023-02-06 12:27:31 +00:00
"StartupNotify=false\n"
).arg(QApplication::applicationFilePath());
}
2021-07-06 19:36:27 +00:00
bool xdgDesktopEntryWrite(const QString &path){
QString mime = xdgDesktopEntry();
QFileInfo file(path);
QDir().mkpath(file.path());
qDebug() << "Writing xdg desktop entry: " << path;
2021-07-06 19:36:27 +00:00
return fileWrite(path, mime);
}
2021-07-06 19:36:27 +00:00
void xdgRefreshApplications(){
QStringList args = {QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation)};
2021-07-06 19:36:27 +00:00
QProcess::startDetached("update-desktop-database", args);
}
2021-07-06 19:36:27 +00:00
bool xdgDesktopEntryRegister() {
#if defined(Q_OS_MACOS)
return false;
#endif
#if defined(Q_OS_WIN)
// @TODO: implement
return false;
#endif
QPixmap appIcon(":assets/images/feather.png");
2021-07-06 19:36:27 +00:00
QString pathIcon = QString("%1/.local/share/icons/feather.png").arg(QDir::homePath());
if (!fileExists(pathIcon)) {
pixmapWrite(pathIcon, appIcon);
}
2023-02-06 12:27:31 +00:00
xdgDesktopEntryWrite(QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation) + "/feather-wallet.desktop");
2021-07-06 19:36:27 +00:00
xdgRefreshApplications();
return true;
}
2022-03-14 21:36:58 +00:00
bool portOpen(const QString &hostname, quint16 port) { // TODO: this call should be async
if (config()->get(Config::offlineMode).toBool()) {
return false;
}
2021-07-06 19:36:27 +00:00
QTcpSocket socket;
socket.connectToHost(hostname, port);
return socket.waitForConnected(600);
}
quint16 getDefaultRpcPort(NetworkType::Type type) {
switch (type) {
case NetworkType::Type::MAINNET:
return 18081;
case NetworkType::Type::TESTNET:
return 28081;
case NetworkType::Type::STAGENET:
return 38081;
}
2021-07-06 19:36:27 +00:00
return 18081;
}
2021-07-06 19:36:27 +00:00
bool isTorsocks() {
#if defined(Q_OS_MAC)
return qgetenv("DYLD_INSERT_LIBRARIES").indexOf("libtorsocks") >= 0;
#elif defined(Q_OS_LINUX)
return qgetenv("LD_PRELOAD").indexOf("libtorsocks") >= 0;
#else
return false;
#endif
}
2021-07-06 19:36:27 +00:00
double roundSignificant(double N, double n)
{
int h;
double b, d, e, i, j, m, f;
b = N;
for (i = 0; b >= 1; ++i)
b = b / 10;
d = n - i;
b = N;
b = b * pow(10, d);
e = b + 0.5;
if ((float)e == (float)ceil(b)) {
f = (ceil(b));
h = f - 2;
if (h % 2 != 0) {
e = e - 1;
}
}
j = floor(e);
m = pow(10, d);
j = j / m;
return j;
}
2021-07-06 19:36:27 +00:00
int maxLength(const QVector<QString> &array) {
int maxLength = 0;
for (const auto &str: array) {
auto length = str.length();
if (length > maxLength) {
maxLength = length;
}
}
return maxLength;
}
QString formatBytes(quint64 bytes)
{
QVector<QString> sizes = { "B", "KB", "MB", "GB", "TB" };
int i;
double _data;
2020-12-24 12:20:44 +00:00
for (i = 0; i < sizes.count() && bytes >= 10000; i++, bytes /= 1000)
_data = bytes / 1000.0;
if (_data < 0)
_data = 0;
// unrealistic
2020-12-24 12:20:44 +00:00
if (_data > 10000)
_data = 0;
return QString("%1 %2").arg(QString::number(_data, 'f', 1), sizes[i]);
}
2021-07-06 19:36:27 +00:00
QMap<QString, QLocale> localeCache = {};
QLocale getCurrencyLocale(const QString &currencyCode) {
QLocale locale;
if (localeCache.contains(currencyCode)) {
locale = localeCache[currencyCode];
} else {
QList<QLocale> allLocales = QLocale::matchingLocales(QLocale::AnyLanguage, QLocale::AnyScript, QLocale::AnyCountry);
for (const auto& locale_: allLocales) {
if (locale_.currencySymbol(QLocale::CurrencyIsoCode) == currencyCode) {
locale = locale_;
}
}
localeCache[currencyCode] = locale;
}
return locale;
}
2021-07-06 19:36:27 +00:00
QString amountToCurrencyString(double amount, const QString &currencyCode) {
QLocale locale = getCurrencyLocale(currencyCode);
// \xC2\xA0 = UTF-8 non-breaking space, it looks off.
if (currencyCode == "USD")
return locale.toCurrencyString(amount, "$").remove("\xC2\xA0");
return locale.toCurrencyString(amount).remove("\xC2\xA0");
2020-12-16 16:31:25 +00:00
}
2021-07-06 19:36:27 +00:00
QStandardItem *qStandardItem(const QString& text) {
auto font = QApplication::font();
return Utils::qStandardItem(text, font);
}
QStandardItem *qStandardItem(const QString& text, QFont &font) {
// stupid Qt doesnt set font sizes correctly on OSX
// @TODO: memleak
auto item = new QStandardItem(text);
item->setFont(font);
return item;
}
QString blockExplorerLink(const QString &blockExplorer, NetworkType::Type nettype, const QString &txid) {
if (blockExplorer == "exploremonero.com") {
if (nettype == NetworkType::MAINNET) {
return QString("https://exploremonero.com/transaction/%1").arg(txid);
2020-12-16 16:31:25 +00:00
}
}
2021-07-06 19:36:27 +00:00
else if (blockExplorer == "moneroblocks.info") {
if (nettype == NetworkType::MAINNET) {
return QString("https://moneroblocks.info/tx/%1").arg(txid);
}
}
else if (blockExplorer == "blockchair.com") {
if (nettype == NetworkType::MAINNET) {
return QString("https://blockchair.com/monero/transaction/%1").arg(txid);
}
}
else if (blockExplorer == "melo.tools") {
switch (nettype) {
case NetworkType::MAINNET:
return QString("https://melo.tools/explorer/mainnet/tx/%1").arg(txid);
case NetworkType::STAGENET:
return QString("https://melo.tools/explorer/stagenet/tx/%1").arg(txid);
case NetworkType::TESTNET:
return QString("https://melo.tools/explorer/testnet/tx/%1").arg(txid);
}
}
else if (blockExplorer == "blkchairbknpn73cfjhevhla7rkp4ed5gg2knctvv7it4lioy22defid.onion") {
if (nettype == NetworkType::MAINNET) {
return QString("http://blkchairbknpn73cfjhevhla7rkp4ed5gg2knctvv7it4lioy22defid.onion/monero/transaction/%1").arg(txid);
}
}
2022-09-18 10:38:58 +00:00
else if (blockExplorer == "127.0.0.1:31312") {
if (nettype == NetworkType::MAINNET) {
return QString("http://127.0.0.1:31312/tx?id=%1").arg(txid);
}
}
2021-07-06 19:36:27 +00:00
switch (nettype) {
case NetworkType::MAINNET:
return QString("https://xmrchain.net/tx/%1").arg(txid);
case NetworkType::STAGENET:
return QString("https://stagenet.xmrchain.net/tx/%1").arg(txid);
case NetworkType::TESTNET:
return QString("https://testnet.xmrchain.net/tx/%1").arg(txid);
}
return QString("");
}
2020-12-31 02:14:48 +00:00
2021-07-06 19:36:27 +00:00
void externalLinkWarning(QWidget *parent, const QString &url){
if (!config()->get(Config::warnOnExternalLink).toBool()) {
QDesktopServices::openUrl(QUrl(url));
return;
}
2022-02-10 10:37:53 +00:00
QString body = QString("You are about to open the following link:\n\n%1").arg(url);
2021-07-06 19:36:27 +00:00
QMessageBox linkWarning(parent);
linkWarning.setWindowTitle("External link warning");
linkWarning.setText(body);
QPushButton *copy = linkWarning.addButton("Copy link", QMessageBox::HelpRole);
linkWarning.addButton(QMessageBox::Cancel);
linkWarning.addButton(QMessageBox::Ok);
linkWarning.setDefaultButton(QMessageBox::Ok);
linkWarning.exec();
if (linkWarning.clickedButton() == copy) {
Utils::copyToClipboard(url);
} else if (linkWarning.result() == QMessageBox::Ok) {
QDesktopServices::openUrl(QUrl(url));
}
}
void desktopNotify(const QString &title, const QString &message, int duration) {
if (!Config::hideNotifications)
{
QStringList notify_send = QStringList() << title << message << "-t" << QString::number(duration);
QStringList kdialog = QStringList() << title << message;
QStringList macos = QStringList() << "-e" << QString(R"(display notification "%1" with title "%2")").arg(message).arg(title);
#if defined(Q_OS_LINUX)
QProcess process;
if (fileExists("/usr/bin/kdialog"))
process.start("/usr/bin/kdialog", kdialog);
else if (fileExists("/usr/bin/notify-send"))
process.start("/usr/bin/notify-send", notify_send);
process.waitForFinished(-1);
QString stdout = process.readAllStandardOutput();
QString stderr = process.readAllStandardError();
#elif defined(Q_OS_MACOS)
QProcess process;
// @TODO: need to escape special chars with "\"
process.start("osascript", macos);
process.waitForFinished(-1);
QString stdout = process.readAllStandardOutput();
QString stderr = process.readAllStandardError();
#endif
}
2021-07-06 19:36:27 +00:00
}
2023-03-28 20:42:12 +00:00
QString displayAddress(const QString& address, int sections, const QString& sep) {
QStringList list;
if (sections < 1) sections = 1;
for (int i = 0; i < sections; i += 1) {
list << address.mid(i*5, 5);
}
list << ""; // utf-8 Horizontal Ellipsis
for (int i = sections; i > 0; i -= 1) {
list << address.mid(address.length() - i * 5, 5);
}
return list.join(sep);
}
2021-07-06 19:36:27 +00:00
QTextCharFormat addressTextFormat(const SubaddressIndex &index, quint64 amount) {
2021-06-28 15:30:08 +00:00
QTextCharFormat rec;
2021-01-29 14:51:59 +00:00
if (index.isPrimary()) {
rec.setBackground(QBrush(ColorScheme::YELLOW.asColor(true)));
rec.setToolTip("Wallet change/primary address");
}
2021-06-28 15:30:08 +00:00
else if (index.isValid()) {
2021-01-29 14:51:59 +00:00
rec.setBackground(QBrush(ColorScheme::GREEN.asColor(true)));
rec.setToolTip("Wallet receive address");
}
2021-06-28 15:30:08 +00:00
else if (amount == 0) {
rec.setBackground(QBrush(ColorScheme::GRAY.asColor(true)));
rec.setToolTip("Dummy output (Min. 2 outs consensus rule)");
}
return rec;
2021-03-12 18:26:48 +00:00
}
2021-07-06 19:36:27 +00:00
void applicationLogHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg) {
const QString fn = context.function ? QString::fromUtf8(context.function) : "";
const QString date = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");
QString line;
2021-03-12 18:26:48 +00:00
2021-07-06 19:36:27 +00:00
switch (type) {
case QtDebugMsg:
line = QString("[%1 D] %2(:%3) %4\n").arg(date).arg(fn).arg(context.line).arg(msg);
fprintf(stderr, "%s", line.toLatin1().data());
break;
case QtInfoMsg:
line = QString("[%1 I] %2\n").arg(date).arg(msg);
fprintf(stdout, "%s", line.toLatin1().data());
break;
case QtWarningMsg:
line = QString("[%1 W] %2(:%3) %4\n").arg(date).arg(fn).arg(context.line).arg(msg);
fprintf(stdout, "%s", line.toLatin1().data());
break;
case QtCriticalMsg:
line = QString("[%1 C] %2(:%3) %4\n").arg(date).arg(fn).arg(context.line).arg(msg);
fprintf(stderr, "%s", line.toLatin1().data());
break;
case QtFatalMsg:
line = QString("[%1 F] %2(:%3) %4\n").arg(date).arg(fn).arg(context.line).arg(msg);
fprintf(stderr, "%s", line.toLatin1().data());
break;
2021-03-24 01:37:54 +00:00
}
2021-07-06 19:36:27 +00:00
}
2021-03-24 01:37:54 +00:00
2021-07-06 19:36:27 +00:00
QString barrayToString(const QByteArray &data) {
2022-05-24 15:27:39 +00:00
return QString::fromUtf8(data);
2021-07-06 19:36:27 +00:00
}
2021-03-24 01:37:54 +00:00
2021-07-06 19:36:27 +00:00
QString getAccountName() {
QString accountName = qgetenv("USER"); // mac/linux
if (accountName.isEmpty())
accountName = qgetenv("USERNAME"); // Windows
if (accountName.isEmpty())
throw std::runtime_error("Could derive system account name from env vars: USER or USERNAME");
return accountName;
2021-07-04 03:40:31 +00:00
}
2023-03-28 20:42:12 +00:00
QFont getMonospaceFont()
{
if (QFontInfo(QApplication::font()).fixedPitch()) {
return QApplication::font();
}
return QFontDatabase::systemFont(QFontDatabase::FixedFont);
}
2021-07-06 19:36:27 +00:00
QFont relativeFont(int delta) {
auto font = QApplication::font();
font.setPointSize(font.pointSize() + delta);
return font;
}
2023-02-11 17:11:21 +00:00
bool isLocalUrl(const QUrl &url) {
QRegularExpression localNetwork(R"((^127\.)|(^10\.)|(^172\.1[6-9]\.)|(^172\.2[0-9]\.)|(^172\.3[0-1]\.)|(^192\.168\.))");
return (localNetwork.match(url.host()).hasMatch() || url.host() == "localhost");
}
}