From 8e640722c62692818ab840d50b3758f89a41a54e Mon Sep 17 00:00:00 2001 From: Unit 193 Date: Wed, 25 Nov 2015 16:48:41 -0500 Subject: Imported Upstream version 3.0.7 --- Plugins/ConfigMigration/ConfigMigration.pro | 4 +- Plugins/ConfigMigration/ConfigMigration_it.ts | 150 ++++++ Plugins/CsvExport/CsvExport.pro | 4 +- Plugins/CsvExport/CsvExport_it.ts | 57 ++ Plugins/CsvExport/CsvExport_zh_CN.ts | 20 +- Plugins/CsvImport/CsvImport.pro | 4 +- Plugins/CsvImport/CsvImport_it.ts | 85 +++ Plugins/DbAndroid/DbAndroid.pro | 53 ++ Plugins/DbAndroid/adbmanager.cpp | 426 +++++++++++++++ Plugins/DbAndroid/adbmanager.h | 65 +++ Plugins/DbAndroid/dbandroid.cpp | 209 ++++++++ Plugins/DbAndroid/dbandroid.h | 63 +++ Plugins/DbAndroid/dbandroid.json | 9 + Plugins/DbAndroid/dbandroid.qrc | 1 + Plugins/DbAndroid/dbandroid_global.h | 12 + Plugins/DbAndroid/dbandroidconnection.cpp | 14 + Plugins/DbAndroid/dbandroidconnection.h | 44 ++ Plugins/DbAndroid/dbandroidconnectionfactory.cpp | 28 + Plugins/DbAndroid/dbandroidconnectionfactory.h | 21 + Plugins/DbAndroid/dbandroidinstance.cpp | 143 +++++ Plugins/DbAndroid/dbandroidinstance.h | 51 ++ Plugins/DbAndroid/dbandroidjsonconnection.cpp | 430 +++++++++++++++ Plugins/DbAndroid/dbandroidjsonconnection.h | 65 +++ Plugins/DbAndroid/dbandroidmode.h | 13 + Plugins/DbAndroid/dbandroidpathdialog.cpp | 579 +++++++++++++++++++++ Plugins/DbAndroid/dbandroidpathdialog.h | 80 +++ Plugins/DbAndroid/dbandroidpathdialog.ui | 244 +++++++++ Plugins/DbAndroid/dbandroidshellconnection.cpp | 363 +++++++++++++ Plugins/DbAndroid/dbandroidshellconnection.h | 59 +++ Plugins/DbAndroid/dbandroidurl.cpp | 217 ++++++++ Plugins/DbAndroid/dbandroidurl.h | 59 +++ Plugins/DbAndroid/sqlqueryandroid.cpp | 160 ++++++ Plugins/DbAndroid/sqlqueryandroid.h | 47 ++ Plugins/DbAndroid/sqlresultrowandroid.cpp | 12 + Plugins/DbAndroid/sqlresultrowandroid.h | 13 + Plugins/DbSqlite2/DbSqlite2.pro | 1 + Plugins/HtmlExport/HtmlExport.pro | 4 +- Plugins/HtmlExport/HtmlExport_it.ts | 173 ++++++ Plugins/JsonExport/JsonExport.pro | 4 +- Plugins/JsonExport/JsonExport_it.ts | 22 + Plugins/JsonExport/JsonExport_zh_CN.ts | 6 +- Plugins/PdfExport/PdfExport.pro | 4 +- Plugins/PdfExport/PdfExport_de.ts | 96 ++-- Plugins/PdfExport/PdfExport_it.ts | 256 +++++++++ Plugins/Plugins.pro | 3 +- Plugins/Printing/Printing.pro | 4 +- Plugins/Printing/Printing_de.ts | 12 +- Plugins/Printing/Printing_it.ts | 40 ++ Plugins/Printing/Printing_zh_CN.ts | 12 +- Plugins/RegExpImport/RegExpImport.pro | 4 +- Plugins/RegExpImport/RegExpImport_it.ts | 83 +++ Plugins/ScriptingTcl/ScriptingTcl.pro | 8 +- Plugins/ScriptingTcl/ScriptingTcl_it.ts | 22 + .../SqlEnterpriseFormatter.pro | 4 +- .../SqlEnterpriseFormatter_de.ts | 31 +- .../SqlEnterpriseFormatter_es.ts | 31 +- .../SqlEnterpriseFormatter_fr.ts | 31 +- .../SqlEnterpriseFormatter_it.ts | 233 +++++++++ .../SqlEnterpriseFormatter_pl.ts | 31 +- .../SqlEnterpriseFormatter_pt_BR.ts | 31 +- .../SqlEnterpriseFormatter_ru.ts | 33 +- .../SqlEnterpriseFormatter_sk.ts | 31 +- .../SqlEnterpriseFormatter_zh_CN.ts | 31 +- Plugins/SqlEnterpriseFormatter/formatempty.cpp | 5 +- Plugins/SqlEnterpriseFormatter/formatempty.h | 3 - .../sqlenterpriseformatter.cpp | 206 +++++++- .../sqlenterpriseformatter.h | 23 + .../sqlenterpriseformatter.ui | 69 ++- Plugins/SqlExport/SqlExport.pro | 4 +- Plugins/SqlExport/SqlExport_it.ts | 98 ++++ Plugins/SqlExport/SqlExport_zh_CN.ts | 14 +- Plugins/SqlFormatterSimple/SqlFormatterSimple.pro | 4 +- .../SqlFormatterSimple/SqlFormatterSimple_it.ts | 17 + .../SqlFormatterSimple/SqlFormatterSimple_zh_CN.ts | 4 +- Plugins/XmlExport/XmlExport.pro | 4 +- Plugins/XmlExport/XmlExport_it.ts | 70 +++ 76 files changed, 5433 insertions(+), 133 deletions(-) create mode 100644 Plugins/ConfigMigration/ConfigMigration_it.ts create mode 100644 Plugins/CsvExport/CsvExport_it.ts create mode 100644 Plugins/CsvImport/CsvImport_it.ts create mode 100644 Plugins/DbAndroid/DbAndroid.pro create mode 100644 Plugins/DbAndroid/adbmanager.cpp create mode 100644 Plugins/DbAndroid/adbmanager.h create mode 100644 Plugins/DbAndroid/dbandroid.cpp create mode 100644 Plugins/DbAndroid/dbandroid.h create mode 100644 Plugins/DbAndroid/dbandroid.json create mode 100644 Plugins/DbAndroid/dbandroid.qrc create mode 100644 Plugins/DbAndroid/dbandroid_global.h create mode 100644 Plugins/DbAndroid/dbandroidconnection.cpp create mode 100644 Plugins/DbAndroid/dbandroidconnection.h create mode 100644 Plugins/DbAndroid/dbandroidconnectionfactory.cpp create mode 100644 Plugins/DbAndroid/dbandroidconnectionfactory.h create mode 100644 Plugins/DbAndroid/dbandroidinstance.cpp create mode 100644 Plugins/DbAndroid/dbandroidinstance.h create mode 100644 Plugins/DbAndroid/dbandroidjsonconnection.cpp create mode 100644 Plugins/DbAndroid/dbandroidjsonconnection.h create mode 100644 Plugins/DbAndroid/dbandroidmode.h create mode 100644 Plugins/DbAndroid/dbandroidpathdialog.cpp create mode 100644 Plugins/DbAndroid/dbandroidpathdialog.h create mode 100644 Plugins/DbAndroid/dbandroidpathdialog.ui create mode 100644 Plugins/DbAndroid/dbandroidshellconnection.cpp create mode 100644 Plugins/DbAndroid/dbandroidshellconnection.h create mode 100644 Plugins/DbAndroid/dbandroidurl.cpp create mode 100644 Plugins/DbAndroid/dbandroidurl.h create mode 100644 Plugins/DbAndroid/sqlqueryandroid.cpp create mode 100644 Plugins/DbAndroid/sqlqueryandroid.h create mode 100644 Plugins/DbAndroid/sqlresultrowandroid.cpp create mode 100644 Plugins/DbAndroid/sqlresultrowandroid.h create mode 100644 Plugins/HtmlExport/HtmlExport_it.ts create mode 100644 Plugins/JsonExport/JsonExport_it.ts create mode 100644 Plugins/PdfExport/PdfExport_it.ts create mode 100644 Plugins/Printing/Printing_it.ts create mode 100644 Plugins/RegExpImport/RegExpImport_it.ts create mode 100644 Plugins/ScriptingTcl/ScriptingTcl_it.ts create mode 100644 Plugins/SqlEnterpriseFormatter/SqlEnterpriseFormatter_it.ts create mode 100644 Plugins/SqlExport/SqlExport_it.ts create mode 100644 Plugins/SqlFormatterSimple/SqlFormatterSimple_it.ts create mode 100644 Plugins/XmlExport/XmlExport_it.ts (limited to 'Plugins') diff --git a/Plugins/ConfigMigration/ConfigMigration.pro b/Plugins/ConfigMigration/ConfigMigration.pro index 8983b8d..52eeaec 100644 --- a/Plugins/ConfigMigration/ConfigMigration.pro +++ b/Plugins/ConfigMigration/ConfigMigration.pro @@ -31,7 +31,8 @@ RESOURCES += \ configmigration.qrc -TRANSLATIONS += ConfigMigration_zh_CN.ts \ +TRANSLATIONS += ConfigMigration_it.ts \ + ConfigMigration_zh_CN.ts \ ConfigMigration_sk.ts \ ConfigMigration_de.ts \ ConfigMigration_ru.ts \ @@ -51,3 +52,4 @@ TRANSLATIONS += ConfigMigration_zh_CN.ts \ + diff --git a/Plugins/ConfigMigration/ConfigMigration_it.ts b/Plugins/ConfigMigration/ConfigMigration_it.ts new file mode 100644 index 0000000..c71c354 --- /dev/null +++ b/Plugins/ConfigMigration/ConfigMigration_it.ts @@ -0,0 +1,150 @@ + + + + + ConfigMigration + + + A configuration from old SQLiteStudio 2.x.x has been detected. Would you like to migrate old settings into the current version? <a href="%1">Click here to do that</a>. + + + + + Bug reports history (%1) + + + + + Database list (%1) + + + + + Custom SQL functions (%1) + + + + + SQL queries history (%1) + + + + + ConfigMigrationWizard + + + Configuration migration + + + + + Items to migrate + + + + + This is a list of items found in the old configuration file, which can be migrated into the current configuration. + + + + + Options + + + + + Put imported databases into separate group + + + + + Group name + + + + + Enter a non-empty name. + + + + + Top level group named '%1' already exists. Enter a group name that does not exist yet. + + + + + Could not open old configuration file in order to migrate settings from it. + + + + + Could not open current configuration file in order to migrate settings from old configuration file. + + + + + Could not commit migrated data into new configuration file: %1 + + + + + Could not read bug reports history from old configuration file in order to migrate it: %1 + + + + + Could not insert a bug reports history entry into new configuration file: %1 + + + + + Could not read database list from old configuration file in order to migrate it: %1 + + + + + Could not query for available order for containing group in new configuration file in order to migrate the database list: %1 + + + + + Could not create containing group in new configuration file in order to migrate the database list: %1 + + + + + Could not insert a database entry into new configuration file: %1 + + + + + Could not query for available order for next database in new configuration file in order to migrate the database list: %1 + + + + + Could not create group referencing the database in new configuration file: %1 + + + + + Could not read function list from old configuration file in order to migrate it: %1 + + + + + Could not read SQL queries history from old configuration file in order to migrate it: %1 + + + + + Could not read next ID for SQL queries history in new configuration file: %1 + + + + + Could not insert SQL history entry into new configuration file: %1 + + + + diff --git a/Plugins/CsvExport/CsvExport.pro b/Plugins/CsvExport/CsvExport.pro index 1599a13..9e509b2 100644 --- a/Plugins/CsvExport/CsvExport.pro +++ b/Plugins/CsvExport/CsvExport.pro @@ -29,7 +29,8 @@ RESOURCES += \ -TRANSLATIONS += CsvExport_zh_CN.ts \ +TRANSLATIONS += CsvExport_it.ts \ + CsvExport_zh_CN.ts \ CsvExport_sk.ts \ CsvExport_de.ts \ CsvExport_ru.ts \ @@ -49,3 +50,4 @@ TRANSLATIONS += CsvExport_zh_CN.ts \ + diff --git a/Plugins/CsvExport/CsvExport_it.ts b/Plugins/CsvExport/CsvExport_it.ts new file mode 100644 index 0000000..30f4907 --- /dev/null +++ b/Plugins/CsvExport/CsvExport_it.ts @@ -0,0 +1,57 @@ + + + + + CsvExport + + + Column names in first row + + + + + Column separator: + + + + + , (comma) + + + + + ; (semicolon) + + + + + \t (tab) + + + + + (whitespace) + + + + + Custom: + + + + + Export NULL values as: + + + + + Empty string + + + + + Enter the custom separator character. + + + + diff --git a/Plugins/CsvExport/CsvExport_zh_CN.ts b/Plugins/CsvExport/CsvExport_zh_CN.ts index e73fe4e..6d31621 100644 --- a/Plugins/CsvExport/CsvExport_zh_CN.ts +++ b/Plugins/CsvExport/CsvExport_zh_CN.ts @@ -6,52 +6,52 @@ Column names in first row - + 第一行显示列名 Column separator: - + 列分隔符: , (comma) - + ,(逗号) ; (semicolon) - + ;(分号) \t (tab) - + \t(tab位) (whitespace) - + (空格) Custom: - + 自定义: Export NULL values as: - + NULL导出为: Empty string - + 空字符串 Enter the custom separator character. - + 输入自定义分隔符 。 diff --git a/Plugins/CsvImport/CsvImport.pro b/Plugins/CsvImport/CsvImport.pro index 6ce08d1..7e8dce0 100644 --- a/Plugins/CsvImport/CsvImport.pro +++ b/Plugins/CsvImport/CsvImport.pro @@ -28,7 +28,8 @@ RESOURCES += \ csvimport.qrc -TRANSLATIONS += CsvImport_zh_CN.ts \ +TRANSLATIONS += CsvImport_it.ts \ + CsvImport_zh_CN.ts \ CsvImport_sk.ts \ CsvImport_de.ts \ CsvImport_ru.ts \ @@ -48,3 +49,4 @@ TRANSLATIONS += CsvImport_zh_CN.ts \ + diff --git a/Plugins/CsvImport/CsvImport_it.ts b/Plugins/CsvImport/CsvImport_it.ts new file mode 100644 index 0000000..bc60d95 --- /dev/null +++ b/Plugins/CsvImport/CsvImport_it.ts @@ -0,0 +1,85 @@ + + + + + CsvImport + + + Cannot read file %1 + + + + + Could not find any data in the file %1. + + + + + Enter the custom separator character. + + + + + Enter the value that will be interpreted as a NULL. + + + + + CSV files (*.csv);;Text files (*.txt);;All files (*) + + + + + csvImportOptions + + + , (comma) + + + + + ; (semicolon) + + + + + \t (tab) + + + + + (whitespace) + + + + + Custom: + + + + + <p>Enable this if the first data line in your CSV file represents column names. You don't want column names to be imported into the table as a regular data.</p> + + + + + First line represents CSV column names + + + + + Field separator: + + + + + NULL values: + + + + + If your CSV data contains null values, define how are they represented in the CSV. + + + + diff --git a/Plugins/DbAndroid/DbAndroid.pro b/Plugins/DbAndroid/DbAndroid.pro new file mode 100644 index 0000000..2895c7f --- /dev/null +++ b/Plugins/DbAndroid/DbAndroid.pro @@ -0,0 +1,53 @@ +#------------------------------------------------- +# +# Project created by QtCreator 2015-01-04T19:37:23 +# +#------------------------------------------------- + +QT += widgets network + +include($$PWD/../../../sqlitestudio/SQLiteStudio3/plugins.pri) + +TARGET = DbAndroid +TEMPLATE = lib + +DEFINES += DBANDROID_LIBRARY + +SOURCES += dbandroid.cpp \ + dbandroidinstance.cpp \ + sqlqueryandroid.cpp \ + dbandroidurl.cpp \ + dbandroidpathdialog.cpp \ + adbmanager.cpp \ + sqlresultrowandroid.cpp \ + dbandroidjsonconnection.cpp \ + dbandroidshellconnection.cpp \ + dbandroidconnection.cpp \ + dbandroidconnectionfactory.cpp + +HEADERS += dbandroid.h\ + dbandroid_global.h \ + dbandroidinstance.h \ + sqlqueryandroid.h \ + dbandroidurl.h \ + dbandroidpathdialog.h \ + adbmanager.h \ + dbandroidconnection.h \ + dbandroidmode.h \ + sqlresultrowandroid.h \ + dbandroidjsonconnection.h \ + dbandroidshellconnection.h \ + dbandroidconnectionfactory.h + +win32: { + LIBS += -lcoreSQLiteStudio -lguiSQLiteStudio +} + +DISTFILES += \ + dbandroid.json + +FORMS += \ + dbandroidpathdialog.ui + +RESOURCES += \ + dbandroid.qrc diff --git a/Plugins/DbAndroid/adbmanager.cpp b/Plugins/DbAndroid/adbmanager.cpp new file mode 100644 index 0000000..ed012a1 --- /dev/null +++ b/Plugins/DbAndroid/adbmanager.cpp @@ -0,0 +1,426 @@ +#include "adbmanager.h" +#include "dbandroid.h" +#include "common/utils.h" +#include +#include +#include +#include + +AdbManager::AdbManager(DbAndroid* dbAndroidPlugin) : + QObject(dbAndroidPlugin), plugin(dbAndroidPlugin) +{ + connect(this, SIGNAL(internalDeviceListUpdate(QStringList)), this, SLOT(handleNewDeviceList(QStringList))); + connect(this, SIGNAL(deviceDetailsChanged(QList)), this, SLOT(handleNewDetails(QList))); + + adbRunMonitor = new QTimer(this); + connect(adbRunMonitor, SIGNAL(timeout()), this, SLOT(updateDeviceList())); + adbRunMonitor->setSingleShot(false); + adbRunMonitor->setInterval(1000); + adbRunMonitor->start(); + updateDeviceList(); +} + +AdbManager::~AdbManager() +{ + adbRunMonitor->stop(); + updateDevicesFuture.waitForFinished(); +} + +const QStringList& AdbManager::getDevices(bool forceSyncUpdate) +{ + if (forceSyncUpdate) + syncDeviceListUpdate(); + + return currentDeviceList; +} + +AdbManager::Device AdbManager::getDetails(const QString& deviceId) +{ + if (!currentDeviceDetails.contains(deviceId)) + { + AdbManager::Device device; + device.id = deviceId; + return device; + } + + return currentDeviceDetails[deviceId]; +} + +QList AdbManager::getDeviceDetails() +{ + return currentDeviceDetails.values(); +} + +QHash> AdbManager::getForwards() +{ + QHash> forwards; + QString stdOut; + if (!exec(QStringList({"forward", "--list"}), &stdOut)) + return forwards; + + QRegularExpression re("(.*)\\s+tcp:(\\d+)\\s+tcp:(\\d+)"); + QRegularExpressionMatch match; + QPair forward; + QStringList lines = stdOut.split("\n"); + for (const QString& line : lines) + { + match = re.match(line); + if (!match.hasMatch()) + continue; + + forward.first = match.captured(2).toInt(); + forward.second = match.captured(3).toInt(); + forwards[match.captured(1)] = forward; + } + + return forwards; +} + +int AdbManager::makeForwardFor(const QString& device, int targetPort) +{ + static_qstring(portTpl, "tcp:%1"); + + QHash> forwards = getForwards(); + if (forwards.contains(device) && forwards[device].second == targetPort) + return forwards[device].first; + + int localPort = targetPort; + QStringList args = QStringList({"-s", device, "forward"}); + args << portTpl.arg(localPort); + args << portTpl.arg(targetPort); + + int tryCount = 0; + QString stdOut; + bool res; + while (!(res = exec(args, &stdOut)) && tryCount++ < 3) + { + localPort = rand(1025, 65000); + args.replace(3, portTpl.arg(localPort)); + } + + if (!res) + return -1; + + return localPort; +} + +QString AdbManager::findAdb() +{ + QStringList candidates; +#ifdef Q_OS_WIN32 + candidates << "adb.exe"; +#endif + +#ifdef Q_OS_MACX + candidates << (QDir::homePath() + "/Library/Android/sdk/platform-tools/adb"); +#endif + +#ifdef Q_OS_UNIX + candidates << "adb" << "./adb"; + + QProcess locate; + locate.start("locate", QStringList({"adb"})); + if (waitForProc(locate, true)) + { + QFileInfo fi; + QStringList locateLines = decode(locate.readAllStandardOutput()).split("\n"); + for (const QString& filePath : locateLines) + { + fi.setFile(filePath); + if (fi.fileName() != "adb" || !fi.isReadable() || !fi.isExecutable()) + continue; + + candidates << filePath; + } + } +#endif + +#ifdef Q_OS_WIN32 + if (testAdb("adb.exe", true)) + return "adb.exe"; + + static_qstring(winAdbPath, "/../Android/sdk/platform-tools/adb.exe"); + QString fullPath; + for (const QString& path : QStandardPaths::standardLocations(QStandardPaths::AppLocalDataLocation)) + { + fullPath = QDir::cleanPath(path + winAdbPath); + if (testAdb(fullPath, true)) + return fullPath; + } +#endif + + return QString(); +} + +bool AdbManager::testCurrentAdb() +{ + return testAdb(plugin->getCurrentAdb(), false); +} + +bool AdbManager::testAdb(const QString& adbPath, bool quiet) +{ + if (adbPath.isEmpty()) + return false; + + QProcess adbApp; + adbApp.start(adbPath, QStringList({"version"})); + if (!waitForProc(adbApp, quiet)) + return false; + + QString verStr = decode(adbApp.readAllStandardOutput()); + bool res = verStr.startsWith("Android Debug Bridge", Qt::CaseInsensitive); + if (!res && !quiet) + qWarning() << "Adb binary correct, but its version string is incorrect:" << verStr; + + return res; +} + +bool AdbManager::execBytes(const QStringList& arguments, QByteArray* stdOut, QByteArray* stdErr) +{ + if (!ensureAdbRunning()) + return false; + + QProcess proc; + if (arguments.join(" ").size() > 800) + { + if (!execLongCommand(arguments, proc, stdErr)) + return false; + } + else + { + proc.start(plugin->getCurrentAdb(), arguments); + if (!waitForProc(proc, false)) + return false; + } + + if (stdOut) + *stdOut = proc.readAllStandardOutput(); + + if (stdErr) + *stdErr = proc.readAllStandardError(); + + return true; +} + +bool AdbManager::waitForProc(QProcess& proc, bool quiet) +{ + if (!proc.waitForFinished(-1)) + { + if (!quiet) + qDebug() << "DbAndroid QProcess timed out."; + + return false; + } + + if (proc.exitStatus() == QProcess::CrashExit) + { + if (!quiet) + { + qDebug() << "DbAndroid QProcess finished by crashing."; + qDebug() << proc.readAllStandardOutput() << proc.readAllStandardError(); + } + + return false; + } + + if (proc.exitCode() != 0) + { + if (!quiet) + { + qDebug() << "DbAndroid QProcess finished with code:" << proc.exitCode(); + qDebug() << proc.readAllStandardOutput() << proc.readAllStandardError(); + } + + return false; + } + + return true; +} + +bool AdbManager::ensureAdbRunning() +{ + if (!plugin->isAdbValid()) + return false; + + QProcess adbApp; + adbApp.start(plugin->getCurrentAdb(), QStringList({"start-server"})); + if (!waitForProc(adbApp, false)) + return false; + + return true; +} + +bool AdbManager::exec(const QStringList& arguments, QString* stdOut, QString* stdErr) +{ + QByteArray* out = stdOut ? new QByteArray() : nullptr; + QByteArray* err = stdErr ? new QByteArray() : nullptr; + bool res = execBytes(arguments, out, err); + + if (stdOut) + { + *stdOut = decode(*out); + delete out; + } + + if (stdErr) + { + *stdErr = decode(*err); + delete err; + } + + return res; +} + +QByteArray AdbManager::encode(const QString& input) +{ + return input.toUtf8(); +} + +QString AdbManager::decode(const QByteArray& input) +{ + return QString::fromUtf8(input); +} + +bool AdbManager::execLongCommand(const QStringList& arguments, QProcess& proc, QByteArray* stdErr) +{ + // Take off initial arguments from ADB, store it to use with "push". + QStringList primaryArguments; + QStringList args = arguments; + while (args.first() != "shell") + primaryArguments << args.takeFirst(); + + args.removeFirst(); // remove the shell itself + + // Escape remaining arguments for the script + QString cmd = " '" + args.replaceInStrings("'", "'\''").join("' '") + "'"; + + // Now, the temporary file for the script + QTemporaryFile tmpFile("SQLiteStudio-XXXXXX.sh"); + if (!tmpFile.open()) + { + if (stdErr) + *stdErr = encode(QString("Could not create temporary file: %1").arg(tmpFile.fileName())); + + return false; + } + + tmpFile.write(cmd.toUtf8()); + tmpFile.close(); + + // Push the file + args = primaryArguments; + args << "push" << tmpFile.fileName() << "/data/local/tmp"; + proc.start(plugin->getCurrentAdb(), args); + if (!waitForProc(proc, false)) + return false; + + QString remoteFile = ("/data/local/tmp/" + QFileInfo(tmpFile.fileName()).fileName()); + + // Execute the file + args = primaryArguments; + args << "shell" << "sh" << remoteFile; + proc.start(plugin->getCurrentAdb(), args); + if (!waitForProc(proc, false)) + return false; + + // Delete the file from device + args = primaryArguments; + args << "shell" << "rm" << remoteFile; + QProcess localProc; + localProc.start(plugin->getCurrentAdb(), args); + if (!waitForProc(localProc, false)) + { + // Not a critical issue... + qWarning() << "Could not clean up execution script from the device: " << remoteFile << "\nDetails:\n" + << localProc.readAllStandardOutput() << "\n" << localProc.readAllStandardError(); + } + + return true; +} + +QStringList AdbManager::getDevicesInternal(bool emitSignal) +{ + QStringList devices; + QString stdOut; + if (!exec(QStringList({"devices"}), &stdOut)) + { + if (emitSignal) + emit internalDeviceListUpdate(devices); + + return devices; + } + + QRegularExpression re("(.*)\\s+device$"); + QRegularExpressionMatch match; + QStringList lines = stdOut.split("\n"); + for (const QString& line : lines) + { + match = re.match(line.trimmed()); + if (!match.hasMatch()) + continue; + + devices << match.captured(1).trimmed(); + } + + if (emitSignal) + emit internalDeviceListUpdate(devices); + + return devices; +} + +void AdbManager::syncDeviceListUpdate() +{ + currentDeviceList = getDevicesInternal(false); + updateDetails(currentDeviceList); +} + +void AdbManager::updateDetails(const QStringList& devices) +{ + QString stdOut; + QList detailList; + for (const QString& deviceId : devices) + { + Device deviceDetails; + deviceDetails.id = deviceId; + if (exec(QStringList({"-s", deviceId, "shell", "getprop", "ro.product.manufacturer"}), &stdOut)) + deviceDetails.fullName = stdOut.trimmed(); + else + qWarning() << "Could not read brand for device" << deviceId; + + if (exec(QStringList({"-s", deviceId, "shell", "getprop", "ro.product.model"}), &stdOut)) + deviceDetails.fullName += " " + stdOut.trimmed(); + else + qWarning() << "Could not read brand for device" << deviceId; + + deviceDetails.fullName = deviceDetails.fullName.trimmed(); + detailList << deviceDetails; + } + + emit deviceDetailsChanged(detailList); +} + +void AdbManager::updateDeviceList() +{ + if (!plugin->isAdbValid()) + return; + + updateDevicesFuture = QtConcurrent::run(this, &AdbManager::getDevicesInternal, true); +} + +void AdbManager::handleNewDeviceList(const QStringList& devices) +{ + if (currentDeviceList == devices) + return; + + currentDeviceList = devices; + QtConcurrent::run(this, &AdbManager::updateDetails, devices); + + emit deviceListChanged(devices); +} + +void AdbManager::handleNewDetails(const QList& devices) +{ + currentDeviceDetails.clear(); + for (Device device : devices) + currentDeviceDetails[device.id] = device; +} diff --git a/Plugins/DbAndroid/adbmanager.h b/Plugins/DbAndroid/adbmanager.h new file mode 100644 index 0000000..adeca90 --- /dev/null +++ b/Plugins/DbAndroid/adbmanager.h @@ -0,0 +1,65 @@ +#ifndef ADBMANAGER_H +#define ADBMANAGER_H + +#include +#include +#include + +class DbAndroid; +class QTimer; + +class AdbManager : public QObject +{ + Q_OBJECT + public: + struct Device + { + QString id; + QString fullName; + }; + + AdbManager(DbAndroid* plugin); + ~AdbManager(); + + const QStringList& getDevices(bool forceSyncUpdate = false); + Device getDetails(const QString& deviceId); + QList getDeviceDetails(); + QHash> getForwards(); + int makeForwardFor(const QString& device, int targetPort); + QString findAdb(); + bool testCurrentAdb(); + bool testAdb(const QString& adbPath, bool quiet = false); + bool execBytes(const QStringList& arguments, QByteArray* stdOut = nullptr, QByteArray* stdErr = nullptr); + bool exec(const QStringList& arguments, QString* stdOut = nullptr, QString* stdErr = nullptr); + + static QByteArray encode(const QString& input); + static QString decode(const QByteArray& input); + + private: + bool execLongCommand(const QStringList& arguments, QProcess& proc, QByteArray* stdErr); + bool waitForProc(QProcess& proc, bool quiet = false); + bool ensureAdbRunning(); + QStringList getDevicesInternal(bool emitSignal); + void syncDeviceListUpdate(); + void updateDetails(const QStringList& devices); + + DbAndroid* plugin; + QTimer* adbRunMonitor = nullptr; + QStringList currentDeviceList; + QHash currentDeviceDetails; + QFuture updateDevicesFuture; + + private slots: + void updateDeviceList(); + void handleNewDeviceList(const QStringList& devices); + void handleNewDetails(const QList& devices); + + signals: + void internalDeviceListUpdate(const QStringList& devices); + void deviceListChanged(const QStringList& devices); + void deviceDetailsChanged(const QList& details); +}; + +Q_DECLARE_METATYPE(QList) + +#endif // ADBMANAGER_H diff --git a/Plugins/DbAndroid/dbandroid.cpp b/Plugins/DbAndroid/dbandroid.cpp new file mode 100644 index 0000000..99150cd --- /dev/null +++ b/Plugins/DbAndroid/dbandroid.cpp @@ -0,0 +1,209 @@ +#include "adbmanager.h" +#include "dbandroid.h" +#include "dbandroidinstance.h" +#include "dbandroidpathdialog.h" +#include "mainwindow.h" +#include "services/notifymanager.h" +#include "uiconfig.h" +#include "statusfield.h" +#include "services/dbmanager.h" +#include "dbandroidconnectionfactory.h" +#include +#include +#include +#include +#include + +DbAndroid::DbAndroid() +{ +} + +QString DbAndroid::getLabel() const +{ + return "Android SQLite"; +} + +bool DbAndroid::checkIfDbServedByPlugin(Db* db) const +{ + return (db && dynamic_cast(db)); +} + +Db* DbAndroid::getInstance(const QString& name, const QString& path, const QHash& options, QString* errorMessage) +{ + DbAndroidUrl url(path); + if (!url.isValid()) + { + if (errorMessage) + *errorMessage = tr("Invalid or incomplete Android Database URL."); + + return nullptr; + } + + DbAndroidInstance* db = new DbAndroidInstance(this, name, path, options); + return db; +} + +QList DbAndroid::getOptionsList() const +{ + QList options; + + DbPluginOption customBrowseOpt; + customBrowseOpt.type = DbPluginOption::Type::CUSTOM_PATH_BROWSE; + customBrowseOpt.label = tr("Android database URL"); + customBrowseOpt.toolTip = tr("Select Android database"); + customBrowseOpt.customBrowseHandler = [this](const QString& initialPath) -> QString + { + DbAndroidPathDialog dialog(this, MAINWINDOW); + dialog.setUrl(initialPath); + if (!dialog.exec()) + return QString(); + + return dialog.getUrl().toUrlString(); + }; + options << customBrowseOpt; + + return options; +} + +QString DbAndroid::generateDbName(const QVariant& baseValue) +{ + // android://drgh:port/dbName + QUrl url(baseValue.toString()); + if (!url.isValid()) + return baseValue.toString(); + + return url.fileName(); +} + +bool DbAndroid::init() +{ + Q_INIT_RESOURCE(dbandroid); + + qRegisterMetaType>("QList"); + + connect(this, SIGNAL(adbReady(bool)), this, SLOT(handleValidAdb(bool))); + connect(this, SIGNAL(invalidAdb()), this, SLOT(handleInvalidAdb())); + connect(MAINWINDOW->getStatusField(), SIGNAL(linkActivated(QString)), this, SLOT(statusFieldLinkClicked(QString))); + + connectionFactory = new DbAndroidConnectionFactory(this); + + adbManager = new AdbManager(this); + connect(adbManager, SIGNAL(deviceListChanged(QStringList)), this, SLOT(deviceListChanged())); + + if (adbManager->testCurrentAdb()) + { + qDebug() << "Using ADB binary:" << cfg.DbAndroid.AdbPath.get(); + adbValid = true; + adbManager->getDevices(true); + } + else + { + QtConcurrent::run(this, &DbAndroid::initAdb); + } + return true; +} + +void DbAndroid::deinit() +{ + safe_delete(connectionFactory); + safe_delete(adbManager); + Q_CLEANUP_RESOURCE(dbandroid); +} + +QString DbAndroid::getCurrentAdb() +{ + return cfg.DbAndroid.AdbPath.get(); +} + +void DbAndroid::initAdb() +{ + QString adbPath = adbManager->findAdb(); + if (!adbPath.isEmpty()) + { + cfg.DbAndroid.AdbPath.set(adbPath); + qDebug() << "Found ADB binary:" << cfg.DbAndroid.AdbPath.get(); + emit adbReady(true); + return; + } + + emit invalidAdb(); +} + +QString DbAndroid::askForAdbPath() +{ +#if defined(Q_OS_UNIX) + QString adbAppName = "adb"; +#elif defined(Q_OS_WIN32) + QString adbAppName = "adb.exe"; +#else + qCritical() << "Unsupported OS for DbAndroid."; + return QString(); +#endif + QString file = QFileDialog::getOpenFileName(MAINWINDOW, tr("Select ADB"), getFileDialogInitPath(), QString("Android Debug Bridge (%1)").arg(adbAppName)); + if (file.isEmpty()) + return file; + + setFileDialogInitPathByFile(file); + return file; +} + +bool DbAndroid::isAdbValid() const +{ + return adbValid; +} + +DbAndroidConnectionFactory*DbAndroid::getConnectionFactory() const +{ + return connectionFactory; +} + +void DbAndroid::handleValidAdb(bool showMessage) +{ + adbValid = true; + if (showMessage) + notifyInfo(tr("Using Android Debug Bridge: %1").arg(cfg.DbAndroid.AdbPath.get())); + + DBLIST->rescanInvalidDatabasesForPlugin(this); +} + +void DbAndroid::handleInvalidAdb() +{ + notifyError(tr("Could not find Android Debug Bridge application. Click here to point out the location of the ADB application, " + "otherwise the %2 plugin will not support USB cable connections, only the network connection..").arg(SELECT_ADB_URL, getLabel())); +} + +void DbAndroid::statusFieldLinkClicked(const QString& link) +{ + if (link == SELECT_ADB_URL) + { + QString file = askForAdbPath(); + while (!file.isEmpty()) + { + if (adbManager->testAdb(file)) + { + cfg.DbAndroid.AdbPath.set(file); + emit adbReady(true); + return; + } + + int res = QMessageBox::warning(MAINWINDOW, tr("Invalid ADB"), tr("The selected ADB is incorrect.\n" + "Would you like to select another one, or leave it unconfigured?"), + tr("Select another ADB"), tr("Leave unconfigured")); + + if (res == 1) + return; + + file = askForAdbPath(); + } + } +} + +void DbAndroid::deviceListChanged() +{ + DBLIST->rescanInvalidDatabasesForPlugin(this); +} + +AdbManager* DbAndroid::getAdbManager() const +{ + return adbManager; +} diff --git a/Plugins/DbAndroid/dbandroid.h b/Plugins/DbAndroid/dbandroid.h new file mode 100644 index 0000000..68893d0 --- /dev/null +++ b/Plugins/DbAndroid/dbandroid.h @@ -0,0 +1,63 @@ +#ifndef DBANDROID_H +#define DBANDROID_H + +#include "dbandroid_global.h" +#include "plugins/dbplugin.h" +#include "plugins/genericplugin.h" +#include "config_builder.h" + +class AdbManager; +class DbAndroidConnectionFactory; + +CFG_CATEGORIES(DbAndroidConfig, + CFG_CATEGORY(DbAndroid, + CFG_ENTRY(QString, AdbPath, QString()) + ) +) + +class DBANDROIDSHARED_EXPORT DbAndroid : public GenericPlugin, public DbPlugin +{ + Q_OBJECT + SQLITESTUDIO_PLUGIN("dbandroid.json") + + public: + DbAndroid(); + + QString getLabel() const; + bool checkIfDbServedByPlugin(Db* db) const; + Db* getInstance(const QString& name, const QString& path, const QHash& options, QString* errorMessage); + QList getOptionsList() const; + QString generateDbName(const QVariant& baseValue); + bool init(); + void deinit(); + QString getCurrentAdb(); + AdbManager* getAdbManager() const; + bool isAdbValid() const; + DbAndroidConnectionFactory* getConnectionFactory() const; + + static_char* PASSWORD_OPT = "remote_access_password"; + + private: + void initAdb(); + QString askForAdbPath(); + + AdbManager* adbManager = nullptr; + DbAndroidConnectionFactory* connectionFactory = nullptr; + bool adbValid = false; + + static_char* SELECT_ADB_URL = "select_adb://"; + + CFG_LOCAL_PERSISTABLE(DbAndroidConfig, cfg) + + private slots: + void handleValidAdb(bool showMessage); + void handleInvalidAdb(); + void statusFieldLinkClicked(const QString& link); + void deviceListChanged(); + + signals: + void adbReady(bool showMessage); + void invalidAdb(); +}; + +#endif // DBANDROID_H diff --git a/Plugins/DbAndroid/dbandroid.json b/Plugins/DbAndroid/dbandroid.json new file mode 100644 index 0000000..0808bb4 --- /dev/null +++ b/Plugins/DbAndroid/dbandroid.json @@ -0,0 +1,9 @@ +{ + "type": "DbPlugin", + "title": "Android SQLite", + "description": "Provides support for remote SQLite databases on Android devices.", + "version": 10100, + "author": "SalSoft", + "minAppVersion": 30006, + "gui": true +} diff --git a/Plugins/DbAndroid/dbandroid.qrc b/Plugins/DbAndroid/dbandroid.qrc new file mode 100644 index 0000000..7646d2b --- /dev/null +++ b/Plugins/DbAndroid/dbandroid.qrc @@ -0,0 +1 @@ + diff --git a/Plugins/DbAndroid/dbandroid_global.h b/Plugins/DbAndroid/dbandroid_global.h new file mode 100644 index 0000000..aa128c1 --- /dev/null +++ b/Plugins/DbAndroid/dbandroid_global.h @@ -0,0 +1,12 @@ +#ifndef DBANDROID_GLOBAL_H +#define DBANDROID_GLOBAL_H + +#include + +#if defined(DBANDROID_LIBRARY) +# define DBANDROIDSHARED_EXPORT Q_DECL_EXPORT +#else +# define DBANDROIDSHARED_EXPORT Q_DECL_IMPORT +#endif + +#endif // DBANDROID_GLOBAL_H diff --git a/Plugins/DbAndroid/dbandroidconnection.cpp b/Plugins/DbAndroid/dbandroidconnection.cpp new file mode 100644 index 0000000..1d5ba05 --- /dev/null +++ b/Plugins/DbAndroid/dbandroidconnection.cpp @@ -0,0 +1,14 @@ +#include "dbandroidconnection.h" +#include + +QByteArray DbAndroidConnection::convertBlob(const QString& value) +{ + if (!value.startsWith("X'", Qt::CaseInsensitive) || !value.endsWith("'")) + { + qCritical() << "Invalid BLOB value from Android. Doesn't match BLOB pattern:" << value; + return QByteArray(); + } + + return QByteArray::fromHex(value.mid(2, value.length() - 3).toLatin1()); +} + diff --git a/Plugins/DbAndroid/dbandroidconnection.h b/Plugins/DbAndroid/dbandroidconnection.h new file mode 100644 index 0000000..11e9be0 --- /dev/null +++ b/Plugins/DbAndroid/dbandroidconnection.h @@ -0,0 +1,44 @@ +#ifndef DBANDROIDCONNECTION_H +#define DBANDROIDCONNECTION_H + +#include "dbandroidurl.h" +#include +#include +#include + +class DbAndroidConnection : public QObject +{ + Q_OBJECT + + public: + struct ExecutionResult + { + bool wasError = false; + int errorCode = 0; + QString errorMsg; + QStringList resultColumns; + QList resultDataMap; + QList resultDataList; + }; + + DbAndroidConnection(QObject* parent = 0) : QObject(parent) {} + virtual ~DbAndroidConnection() {} + + virtual bool connectToAndroid(const DbAndroidUrl& url) = 0; + virtual void disconnectFromAndroid() = 0; + virtual bool isConnected() const = 0; + virtual QString getDbName() const = 0; + virtual QStringList getDbList() = 0; + virtual QStringList getAppList() = 0; + virtual bool isAppOkay() const = 0; + virtual bool deleteDatabase(const QString& dbName) = 0; + virtual ExecutionResult executeQuery(const QString& query) = 0; + + protected: + static QByteArray convertBlob(const QString& value); + + signals: + void disconnected(); +}; + +#endif // DBANDROIDCONNECTION_H diff --git a/Plugins/DbAndroid/dbandroidconnectionfactory.cpp b/Plugins/DbAndroid/dbandroidconnectionfactory.cpp new file mode 100644 index 0000000..22377b4 --- /dev/null +++ b/Plugins/DbAndroid/dbandroidconnectionfactory.cpp @@ -0,0 +1,28 @@ +#include "dbandroidconnectionfactory.h" +#include "dbandroidjsonconnection.h" +#include "dbandroidshellconnection.h" + +DbAndroidConnectionFactory::DbAndroidConnectionFactory(DbAndroid* plugin) : + plugin(plugin) +{ +} + +DbAndroidConnection* DbAndroidConnectionFactory::create(const QString& url, QObject* parent) +{ + return create(DbAndroidUrl(url), parent); +} + +DbAndroidConnection* DbAndroidConnectionFactory::create(const DbAndroidUrl& url, QObject* parent) +{ + switch (url.getMode()) + { + case DbAndroidMode::SHELL: + return new DbAndroidShellConnection(plugin, parent); + case DbAndroidMode::NETWORK: + case DbAndroidMode::USB: + return new DbAndroidJsonConnection(plugin, parent); + case DbAndroidMode::null: + break; + } + return nullptr; +} diff --git a/Plugins/DbAndroid/dbandroidconnectionfactory.h b/Plugins/DbAndroid/dbandroidconnectionfactory.h new file mode 100644 index 0000000..0e46c9f --- /dev/null +++ b/Plugins/DbAndroid/dbandroidconnectionfactory.h @@ -0,0 +1,21 @@ +#ifndef DBANDROIDCONNECTIONFACTORY_H +#define DBANDROIDCONNECTIONFACTORY_H + +#include "dbandroidurl.h" + +class DbAndroidConnection; +class DbAndroid; + +class DbAndroidConnectionFactory +{ + public: + explicit DbAndroidConnectionFactory(DbAndroid* plugin); + + DbAndroidConnection* create(const QString& url, QObject* parent = nullptr); + DbAndroidConnection* create(const DbAndroidUrl& url, QObject* parent = nullptr); + + private: + DbAndroid* plugin = nullptr; +}; + +#endif // DBANDROIDCONNECTIONFACTORY_H diff --git a/Plugins/DbAndroid/dbandroidinstance.cpp b/Plugins/DbAndroid/dbandroidinstance.cpp new file mode 100644 index 0000000..b60203e --- /dev/null +++ b/Plugins/DbAndroid/dbandroidinstance.cpp @@ -0,0 +1,143 @@ +#include "dbandroidconnection.h" +#include "dbandroidinstance.h" +#include "sqlqueryandroid.h" +#include "db/sqlerrorcodes.h" +#include "common/unused.h" +#include "dbandroid.h" +#include "dbandroidjsonconnection.h" +#include "dbandroidconnectionfactory.h" +#include "dbandroidurl.h" +#include "schemaresolver.h" +#include "services/notifymanager.h" +#include +#include +#include +#include + +DbAndroidInstance::DbAndroidInstance(DbAndroid* plugin, const QString& name, const QString& path, const QHash& connOptions) : + AbstractDb(name, path, connOptions), plugin(plugin) +{ + this->connOptions[SchemaResolver::USE_SCHEMA_CACHING] = true; +} + +DbAndroidInstance::~DbAndroidInstance() +{ + closeInternal(); +} + +SqlQueryPtr DbAndroidInstance::prepare(const QString& query) +{ + return SqlQueryPtr(new SqlQueryAndroid(this, connection, query)); +} + +QString DbAndroidInstance::getTypeLabel() +{ + return plugin->getLabel(); +} + +bool DbAndroidInstance::deregisterFunction(const QString& name, int argCount) +{ + // Unsupported by native Android driver + UNUSED(name); + UNUSED(argCount); + return true; +} + +bool DbAndroidInstance::registerScalarFunction(const QString& name, int argCount) +{ + // Unsupported by native Android driver + UNUSED(name); + UNUSED(argCount); + return true; +} + +bool DbAndroidInstance::registerAggregateFunction(const QString& name, int argCount) +{ + // Unsupported by native Android driver + UNUSED(name); + UNUSED(argCount); + return true; +} + +bool DbAndroidInstance::initAfterCreated() +{ + version = 3; + return AbstractDb::initAfterCreated(); +} + +bool DbAndroidInstance::isOpenInternal() +{ + return (connection && connection->isConnected()); +} + +void DbAndroidInstance::interruptExecution() +{ + // Unsupported by native Android driver +} + +QString DbAndroidInstance::getErrorTextInternal() +{ + return errorText; +} + +int DbAndroidInstance::getErrorCodeInternal() +{ + return errorCode; +} + +bool DbAndroidInstance::openInternal() +{ + connection = createConnection(); + bool res = connection->connectToAndroid(DbAndroidUrl(path)); + if (!res) + { + safe_delete(connection); + } + else + { + connect(connection, SIGNAL(disconnected()), this, SLOT(handleDisconnected())); + } + + return res; +} + +bool DbAndroidInstance::closeInternal() +{ + if (!connection) + return false; + + disconnect(connection, SIGNAL(disconnected()), this, SLOT(handleDisconnected())); + connection->disconnectFromAndroid(); + safe_delete(connection); + return true; +} + +bool DbAndroidInstance::registerCollationInternal(const QString& name) +{ + // Unsupported by native Android driver + UNUSED(name); + return true; +} + +bool DbAndroidInstance::deregisterCollationInternal(const QString& name) +{ + // Unsupported by native Android driver + UNUSED(name); + return true; +} + +DbAndroidConnection* DbAndroidInstance::createConnection() +{ + DbAndroidUrl url(path); + if (!url.isValid(false)) + return nullptr; + + return plugin->getConnectionFactory()->create(url, this); +} + +void DbAndroidInstance::handleDisconnected() +{ + safe_delete(connection); + notifyWarn(tr("Connection with Android database '%1' lost.").arg(getName())); + emit disconnected(); +} diff --git a/Plugins/DbAndroid/dbandroidinstance.h b/Plugins/DbAndroid/dbandroidinstance.h new file mode 100644 index 0000000..453b370 --- /dev/null +++ b/Plugins/DbAndroid/dbandroidinstance.h @@ -0,0 +1,51 @@ +#ifndef DBANDROIDINSTANCE_H +#define DBANDROIDINSTANCE_H + +#include "db/abstractdb.h" +#include +#include +#include + +class DbAndroidConnection; +class DbAndroid; + +class DbAndroidInstance : public AbstractDb +{ + Q_OBJECT + + public: + typedef std::function AsyncDbListResponseHandler; + + DbAndroidInstance(DbAndroid* plugin, const QString& name, const QString& path, const QHash& connOptions); + ~DbAndroidInstance(); + + SqlQueryPtr prepare(const QString& query); + QString getTypeLabel(); + bool deregisterFunction(const QString& name, int argCount); + bool registerScalarFunction(const QString& name, int argCount); + bool registerAggregateFunction(const QString& name, int argCount); + bool initAfterCreated(); + + protected: + bool isOpenInternal(); + void interruptExecution(); + QString getErrorTextInternal(); + int getErrorCodeInternal(); + bool openInternal(); + bool closeInternal(); + bool registerCollationInternal(const QString& name); + bool deregisterCollationInternal(const QString& name); + + private: + DbAndroidConnection* createConnection(); + + DbAndroid* plugin = nullptr; + DbAndroidConnection* connection = nullptr; + int errorCode = 0; + QString errorText; + + private slots: + void handleDisconnected(); +}; + +#endif // DBANDROIDINSTANCE_H diff --git a/Plugins/DbAndroid/dbandroidjsonconnection.cpp b/Plugins/DbAndroid/dbandroidjsonconnection.cpp new file mode 100644 index 0000000..05d6469 --- /dev/null +++ b/Plugins/DbAndroid/dbandroidjsonconnection.cpp @@ -0,0 +1,430 @@ +#include "dbandroidjsonconnection.h" +#include "dbandroid.h" +#include "adbmanager.h" +#include "services/notifymanager.h" +#include "common/blockingsocket.h" +#include "db/sqlerrorcodes.h" +#include +#include +#include + +DbAndroidJsonConnection::DbAndroidJsonConnection(DbAndroid* plugin, QObject *parent) : + DbAndroidConnection(parent), plugin(plugin) +{ + socket = new BlockingSocket(this); + adbManager = plugin->getAdbManager(); + connect(socket, SIGNAL(disconnected()), this, SLOT(handlePossibleDisconnection())); +} + +DbAndroidJsonConnection::~DbAndroidJsonConnection() +{ + cleanUp(); +} + +bool DbAndroidJsonConnection::connectToAndroid(const DbAndroidUrl& url) +{ + if (isConnected()) + { + qWarning() << "Already connected while calling DbAndroidConnection::connect()."; + return false; + } + + dbUrl = url; + mode = url.getMode(); + + switch (mode) + { + case DbAndroidMode::NETWORK: + return connectToNetwork(); + case DbAndroidMode::USB: + return connectToDevice(); + case DbAndroidMode::SHELL: + qCritical() << "SHELL mode encountered in DbAndroidJsonConnection"; + break; + case DbAndroidMode::null: + qCritical() << "Null mode encountered in DbAndroidJsonConnection"; + break; + } + + qCritical() << "Invalid Android db mode while connecting:" << static_cast(mode); + return false; +} + +void DbAndroidJsonConnection::disconnectFromAndroid() +{ + socket->disconnectFromHost(); + connectedState = false; +} + +bool DbAndroidJsonConnection::isConnected() const +{ + if (!socket) + return false; + + return connectedState; +} + +QByteArray DbAndroidJsonConnection::send(const QByteArray& data) +{ + QByteArray bytes = sizeToBytes(data.size()); + bytes.append(data); + return sendBytes(bytes); +} + +QString DbAndroidJsonConnection::getDbName() const +{ + return dbUrl.getDbName(); +} + +QByteArray DbAndroidJsonConnection::sendBytes(const QByteArray& data) +{ + //qDebug() << "Sending" << data; + bool success = socket->send(data); + if (!success) + { + qCritical() << "Error writing bytes to Android socket:" << socket->getErrorText(); + return QByteArray(); + } + + QByteArray sizeBytes = socket->read(4, 5000, &success); + if (!success) + { + qCritical() << "Error reading response size from Android socket:" << socket->getErrorText(); + return QByteArray(); + } + + qint32 size = bytesToSize(sizeBytes); + QByteArray responseBytes = socket->read(size, 5000, &success); + if (!success) + { + qCritical() << "Error reading response from Android socket:" << socket->getErrorText(); + return QByteArray(); + } + //qDebug() << "Received" << responseBytes; + return responseBytes; +} + +void DbAndroidJsonConnection::handleSocketError() +{ + qWarning() << "Blocking socket error in Android connection:" << socket->getErrorText(); + handlePossibleDisconnection(); +} + +void DbAndroidJsonConnection::handlePossibleDisconnection() +{ + if (connectedState && !socket->isConnected()) + { + connectedState = false; + emit disconnected(); + } +} + +QByteArray DbAndroidJsonConnection::sizeToBytes(qint32 size) +{ + QByteArray bytes; + for (int i = 0; i < 4; i++) + bytes.append((size >> (8*i)) & 0xff); + + return bytes; +} + +qint32 DbAndroidJsonConnection::bytesToSize(const QByteArray& bytes) +{ + int size = (((unsigned char)bytes[3]) << 24) | + (((unsigned char)bytes[2]) << 16) | + (((unsigned char)bytes[1]) << 8) | + ((unsigned char)bytes[0]); + + return size; +} + +QVariant DbAndroidJsonConnection::convertJsonValue(const QJsonValue& value) +{ + if (value.isArray()) + { + // BLOB + QJsonArray blobContainer = value.toArray(); + if (blobContainer.size() < 1) + { + qCritical() << "Invalid blob value from Android - empty array."; + return QByteArray(); + } + + return convertBlob(blobContainer.first().toString()); + } + + // Regular value + return value.toVariant(); +} + +bool DbAndroidJsonConnection::connectToNetwork() +{ + if (!dbUrl.isHostValid()) + return false; + + return connectToTcp(dbUrl.getHost(), dbUrl.getPort()); +} + +bool DbAndroidJsonConnection::connectToDevice() +{ + if (!plugin->isAdbValid()) + return false; + + if (!plugin->getAdbManager()->getDevices().contains(dbUrl.getDevice())) + { + notifyWarn(tr("Cannot connect to device %1, because it's not visible to your computer.").arg(dbUrl.getDevice())); + return false; + } + + int localPort = plugin->getAdbManager()->makeForwardFor(dbUrl.getDevice(), dbUrl.getPort()); + if (localPort < 0) + { + notifyError(tr("Failed to create port forwarding for device %1 for port %2.") + .arg(dbUrl.getDevice(), QString::number(dbUrl.getPort()))); + return false; + } + + return connectToTcp("127.0.0.1", localPort); +} + +bool DbAndroidJsonConnection::connectToTcp(const QString& ip, int port) +{ + bool success = socket->connectToHost(ip, port); + if (!success) + { + qWarning() << "Could not connect to network host for Android DB:" << ip << ":" << port << ", details:" << socket->getErrorText(); + notifyWarn(tr("Could not connect to network host: %1:%2").arg(ip, QString::number(port))); + return false; + } + + connectedState = true; + + // Evaluate the connection and send the security token + static_qstring(pingCmd, "{ping:\"%1\"}"); + QDate date = QDateTime::currentDateTime().date(); + QString tokenString = "06fn43" + QString::number(date.dayOfYear()) + "3ig7ws" + QString::number(date.year()) + "53"; + QByteArray md5 = QCryptographicHash::hash(tokenString.toUtf8(), QCryptographicHash::Md5); + QByteArray token = md5.toBase64(); + + QByteArray response = send(pingCmd.arg(QString::fromLatin1(token)).toUtf8()); + if (response != PING_RESPONSE_OK) + { + qWarning() << "Connection to" << ip << ":" << port << "has failed, because response to PING was:" << response; + notifyWarn(tr("Cannot connect to SQLiteStudio service on Android device. The remote service is unavailable or invalid.")); + handleConnectionFailed(); + return false; + } + + // Authenticate + QString pass = dbUrl.getPassword(); + if (!pass.isEmpty()) + { + static_qstring(passPharse, "{auth:\"%1\"}"); + response = send(passPharse.arg(pass.replace("\"", "\\\"")).toUtf8()); + if (response != PASS_RESPONSE_OK) + { + notifyWarn(tr("Cannot connect to %1:%2, because password is invalid.").arg(ip, QString::number(port))); + handleConnectionFailed(); + return false; + } + } + + return true; +} + +void DbAndroidJsonConnection::handleConnectionFailed() +{ + connectedState = false; + socket->disconnectFromHost(); +} + +void DbAndroidJsonConnection::cleanUp() +{ + disconnectFromAndroid(); + safe_delete(socket); +} + +QStringList DbAndroidJsonConnection::getDbList() +{ + if (!isConnected()) + { + qWarning() << "Called DbAndroidJsonConnection::getDbList() on closed connection."; + return QStringList(); + } + + QByteArray result = send(LIST_CMD); + return handleDbListResult(result); +} + +QStringList DbAndroidJsonConnection::getAppList() +{ + return QStringList(); +} + +bool DbAndroidJsonConnection::isAppOkay() const +{ + return true; +} + +QStringList DbAndroidJsonConnection::handleDbListResult(const QByteArray& results) +{ + QJsonParseError jsonError; + QJsonDocument jsonResponse = QJsonDocument::fromJson(results, &jsonError); + if (jsonError.error != QJsonParseError::NoError) + { + qCritical() << "Error while parsing response from Android:" << jsonError.errorString(); + return QStringList(); + } + + QJsonObject responseObject = jsonResponse.object(); + if (responseObject.contains("generic_error")) + { + qCritical() << "Generic error from Android:" << responseObject["generic_error"].toInt(); + return QStringList(); + } + + if (!responseObject.contains("list")) + { + qCritical() << "Missing 'list' in response from Android."; + return QStringList(); + } + + QStringList dbNames; + for (const QVariant& name : responseObject["list"].toArray().toVariantList()) + dbNames << name.toString(); + + return dbNames; +} + +bool DbAndroidJsonConnection::deleteDatabase(const QString& dbName) +{ + if (!isConnected()) + { + qWarning() << "Called DbAndroidConnection::deleteDatabase() on closed database."; + return false; + } + + QByteArray result = send(QString(DELETE_DB_CMD).arg(dbName).toUtf8()); + return handleStdResult(result); +} + +DbAndroidConnection::ExecutionResult DbAndroidJsonConnection::executeQuery(const QString& query) +{ + DbAndroidConnection::ExecutionResult executionResults; + + QJsonDocument json = wrapQueryInJson(query); + QByteArray responseBytes = send(json.toJson(QJsonDocument::Compact)); + + QJsonParseError jsonError; + QJsonDocument jsonResponse = QJsonDocument::fromJson(responseBytes, &jsonError); + if (jsonError.error != QJsonParseError::NoError) + { + executionResults.wasError = true; + executionResults.errorMsg = tr("Error while parsing response from Android: %1").arg(jsonError.errorString()); + return executionResults; + } + + QJsonObject responseObject = jsonResponse.object(); + if (responseObject.contains("generic_error")) + { + executionResults.wasError = true; + executionResults.errorMsg = tr("Generic error from Android: %1").arg(responseObject["generic_error"].toInt()); + return executionResults; + } + + if (responseObject.contains("error_code")) + { + executionResults.errorCode = responseObject["error_code"].toInt(); + executionResults.errorMsg = responseObject["error_message"].toString(); + return executionResults; + } + + if (!responseObject.contains("columns")) + { + executionResults.wasError = true; + executionResults.errorMsg = tr("Missing 'columns' in response from Android."); + return executionResults; + } + + if (!responseObject.contains("data")) + { + executionResults.wasError = true; + executionResults.errorMsg = tr("Missing 'columns' in response from Android."); + return executionResults; + } + + for (const QVariant& col : responseObject["columns"].toArray().toVariantList()) + executionResults.resultColumns << col.toString(); + + QJsonArray jsonRows = responseObject["data"].toArray(); + QJsonObject jsonRow; + QJsonValue jsonValue; + QVariantHash rowAsMap; + QVariantList rowAsList; + QVariant cellValue; + for (int i = 0, total = jsonRows.size(); i < total; ++i) + { + jsonRow = jsonRows[i].toObject(); + for (const QString& colName : executionResults.resultColumns) + { + if (!jsonRow.contains(colName)) + { + executionResults.wasError = true; + executionResults.errorMsg = tr("Response from Android has missing data for column '%1' in row %2.").arg(colName, QString::number(i+1)); + return executionResults; + } + + jsonValue = jsonRow[colName]; + cellValue = convertJsonValue(jsonValue); + rowAsMap[colName] = cellValue; + rowAsList << cellValue; + } + + executionResults.resultDataMap << rowAsMap; + executionResults.resultDataList << rowAsList; + + rowAsMap.clear(); + rowAsList.clear(); + } + + return executionResults; +} + +QJsonDocument DbAndroidJsonConnection::wrapQueryInJson(const QString& query) +{ + QJsonDocument doc; + + QJsonObject rootObj; + rootObj["cmd"] = "QUERY"; + rootObj["db"] = dbUrl.getDbName(); + rootObj["query"] = query; + + doc.setObject(rootObj); + return doc; +} + +bool DbAndroidJsonConnection::handleStdResult(const QByteArray& results) +{ + QJsonParseError jsonError; + QJsonDocument jsonResponse = QJsonDocument::fromJson(results, &jsonError); + if (jsonError.error != QJsonParseError::NoError) + { + qCritical() << "Error while parsing response from Android:" << jsonError.errorString(); + return false; + } + + QJsonObject responseObject = jsonResponse.object(); + if (responseObject.contains("generic_error")) + { + qCritical() << "Generic error from Android:" << responseObject["generic_error"].toInt(); + return false; + } + + if (!responseObject.contains("result")) + { + qCritical() << "Missing 'result' in response from Android."; + return false; + } + + return (responseObject["result"].toString() == "ok"); +} diff --git a/Plugins/DbAndroid/dbandroidjsonconnection.h b/Plugins/DbAndroid/dbandroidjsonconnection.h new file mode 100644 index 0000000..ef0c943 --- /dev/null +++ b/Plugins/DbAndroid/dbandroidjsonconnection.h @@ -0,0 +1,65 @@ +#ifndef DBANDROIDJSONCONNECTION_H +#define DBANDROIDJSONCONNECTION_H + +#include "dbandroidmode.h" +#include "common/global.h" +#include "common/expiringcache.h" +#include "dbandroidconnection.h" +#include + +class DbAndroid; +class AdbManager; +class BlockingSocket; + +class DbAndroidJsonConnection : public DbAndroidConnection +{ + Q_OBJECT + + public: + DbAndroidJsonConnection(DbAndroid* plugin, QObject *parent = 0); + ~DbAndroidJsonConnection(); + + bool connectToAndroid(const DbAndroidUrl& url); + void disconnectFromAndroid(); + bool isConnected() const; + QByteArray send(const QByteArray& data); + QString getDbName() const; + QStringList getDbList(); + QStringList getAppList(); + bool isAppOkay() const; + bool deleteDatabase(const QString& dbName); + ExecutionResult executeQuery(const QString& query); + + private: + QJsonDocument wrapQueryInJson(const QString& query); + bool connectToNetwork(); + bool connectToDevice(); + bool connectToTcp(const QString& ip, int port); + void cleanUp(); + QByteArray sendBytes(const QByteArray& data); + void handleSocketError(); + void handleConnectionFailed(); + QStringList handleDbListResult(const QByteArray& results); + bool handleStdResult(const QByteArray& results); + + static QByteArray sizeToBytes(qint32 size); + static qint32 bytesToSize(const QByteArray& bytes); + static QVariant convertJsonValue(const QJsonValue& value); + + DbAndroid* plugin = nullptr; + AdbManager* adbManager = nullptr; + BlockingSocket* socket = nullptr; + DbAndroidUrl dbUrl; + DbAndroidMode mode = DbAndroidMode::NETWORK; + bool connectedState = false; + + static_char* PASS_RESPONSE_OK = "{\"result\":\"ok\"}"; + static_char* PING_RESPONSE_OK = "{\"result\":\"pong\"}"; + static_char* LIST_CMD = "{cmd:\"LIST\"}"; + static_char* DELETE_DB_CMD = "{cmd:\"DELETE_DB\",db:\"%1\"}"; + + private slots: + void handlePossibleDisconnection(); +}; + +#endif // DBANDROIDJSONCONNECTION_H diff --git a/Plugins/DbAndroid/dbandroidmode.h b/Plugins/DbAndroid/dbandroidmode.h new file mode 100644 index 0000000..523fdd8 --- /dev/null +++ b/Plugins/DbAndroid/dbandroidmode.h @@ -0,0 +1,13 @@ +#ifndef DBANDROIDMODE +#define DBANDROIDMODE + +enum class DbAndroidMode +{ + USB, + NETWORK, + SHELL, + null +}; + +#endif // DBANDROIDMODE + diff --git a/Plugins/DbAndroid/dbandroidpathdialog.cpp b/Plugins/DbAndroid/dbandroidpathdialog.cpp new file mode 100644 index 0000000..da1cf5c --- /dev/null +++ b/Plugins/DbAndroid/dbandroidpathdialog.cpp @@ -0,0 +1,579 @@ +#include "dbandroidpathdialog.h" +#include "ui_dbandroidpathdialog.h" +#include "common/ipvalidator.h" +#include "dbandroid.h" +#include "common/widgetcover.h" +#include "adbmanager.h" +#include "uiutils.h" +#include "iconmanager.h" +#include "dbandroidconnection.h" +#include "dbandroidconnectionfactory.h" +#include +#include +#include +#include +#include +#include +#include + +DbAndroidPathDialog::DbAndroidPathDialog(const DbAndroid* plugin, QWidget *parent) : + QDialog(parent), + plugin(plugin), + ui(new Ui::DbAndroidPathDialog) +{ + init(); +} + +DbAndroidPathDialog::~DbAndroidPathDialog() +{ + delete ui; +} + +void DbAndroidPathDialog::setUrl(const QString& url) +{ + dbUrl = DbAndroidUrl(url); + loadUrl(); +} + +void DbAndroidPathDialog::setUrl(const DbAndroidUrl& url) +{ + dbUrl = url; + loadUrl(); +} + +const DbAndroidUrl& DbAndroidPathDialog::getUrl() const +{ + return dbUrl; +} + +void DbAndroidPathDialog::init() +{ + ui->setupUi(this); + + dbListCover = new WidgetCover(ui->databaseCombo); + appListCover = new WidgetCover(ui->appCombo); + new UserInputFilter(ui->appFilterEdit, this, SLOT(applyAppFilter(QString))); + + ui->createDatabaseButton->setIcon(ICONS.PLUS); + ui->deleteDatabaseButton->setIcon(ICONS.DELETE); + + dbListUpdateTimer = new QTimer(this); + dbListUpdateTimer->setSingleShot(true); + dbListUpdateTimer->setInterval(500); + connect(dbListUpdateTimer, SIGNAL(timeout()), this, SLOT(refreshDbList())); + + appListUpdateTimer = new QTimer(this); + appListUpdateTimer->setSingleShot(true); + appListUpdateTimer->setInterval(500); + connect(appListUpdateTimer, SIGNAL(timeout()), this, SLOT(refreshAppList())); + + connect(ui->deviceCombo, SIGNAL(currentTextChanged(QString)), this, SLOT(scheduleAppListUpdate())); + connect(ui->databaseCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(updateState())); + connect(ui->portSpin, SIGNAL(valueChanged(int)), this, SLOT(scheduleDbListUpdate())); + connect(ui->createDatabaseButton, SIGNAL(clicked()), this, SLOT(createNewDatabase())); + connect(ui->deleteDatabaseButton, SIGNAL(clicked()), this, SLOT(deleteSelectedDatabase())); + connect(ui->passwordGroup, SIGNAL(toggled(bool)), this, SLOT(updateState())); + connect(ui->passwordGroup, SIGNAL(toggled(bool)), this, SLOT(scheduleDbListUpdate())); + connect(ui->passwordEdit, SIGNAL(textChanged(QString)), this, SLOT(scheduleDbListUpdate())); + + connect(this, SIGNAL(asyncDbListUpdatingFinished(bool)), this, SLOT(handleFinishedAsyncDbListUpdate(bool))); + connect(this, SIGNAL(asyncAppListUpdatingFinished()), this, SLOT(handleFinishedAsyncAppListUpdate())); + connect(this, SIGNAL(callForDbListUpdate(QStringList)), this, SLOT(handleUpdateDbList(QStringList))); + connect(this, SIGNAL(callForAppListUpdate(QStringList)), this, SLOT(handleUpdateAppList(QStringList))); + connect(this, SIGNAL(callForValidations()), this, SLOT(updateValidations())); + connect(this, SIGNAL(callForDbCreationUpdate(bool)), this, SLOT(handleDbCreationUpdate(bool))); + + if (!plugin->isAdbValid()) + { + ui->ipRadio->setChecked(true); + ui->usbRadio->setEnabled(false); + ui->shellRadio->setEnabled(false); + } + else + { + refreshDevices(); + connect(plugin->getAdbManager(), SIGNAL(deviceDetailsChanged(QList)), this, SLOT(updateDeviceList())); + } + + connect(ui->ipRadio, SIGNAL(toggled(bool)), this, SLOT(modeChanged(bool))); + connect(ui->usbRadio, SIGNAL(toggled(bool)), this, SLOT(modeChanged(bool))); + connect(ui->shellRadio, SIGNAL(toggled(bool)), this, SLOT(modeChanged(bool))); + connect(ui->ipEdit, SIGNAL(textChanged(QString)), this, SLOT(scheduleDbListUpdate())); + setDbListUpdatesEnabled(true); + + handleDbCreationUpdate(false); + updateState(); + adjustSize(); +} + +void DbAndroidPathDialog::updateUrl() +{ + DbAndroidMode mode = getSelectedMode(); + dbUrl.setEnforcedMode(mode); + switch (mode) + { + case DbAndroidMode::NETWORK: + dbUrl.setHost(ui->ipEdit->text()); + dbUrl.setPort(ui->portSpin->value()); + break; + case DbAndroidMode::USB: + dbUrl.setDevice(ui->deviceCombo->currentData().toString()); + dbUrl.setPort(ui->portSpin->value()); + break; + case DbAndroidMode::SHELL: + dbUrl.setDevice(ui->deviceCombo->currentData().toString()); + dbUrl.setApplication(ui->appCombo->currentText()); + break; + case DbAndroidMode::null: + qCritical() << "Unknown mode in DbAndroidPathDialog::updateUrl()"; + return; + } + + dbUrl.setDbName(ui->databaseCombo->currentText()); + if (ui->passwordGroup->isChecked()) + dbUrl.setPassword(ui->passwordEdit->text()); + else + dbUrl.setPassword(QString()); +} + +void DbAndroidPathDialog::loadUrl() +{ + if (!dbUrl.isValid()) + return; + + switch (dbUrl.getMode()) + { + case DbAndroidMode::NETWORK: + ui->ipRadio->setChecked(true); + ui->ipEdit->setText(dbUrl.getHost()); + break; + case DbAndroidMode::SHELL: + ui->shellRadio->setChecked(true); + ui->deviceCombo->setCurrentIndex(ui->deviceCombo->findData(dbUrl.getDevice())); + setDbListUpdatesEnabled(false); + if (ui->appCombo->findText(dbUrl.getApplication()) == -1) + ui->appCombo->addItem(dbUrl.getApplication()); + + ui->appCombo->setCurrentText(dbUrl.getApplication()); + setDbListUpdatesEnabled(true); + break; + case DbAndroidMode::USB: + ui->usbRadio->setChecked(true); + ui->deviceCombo->setCurrentIndex(ui->deviceCombo->findData(dbUrl.getDevice())); + break; + case DbAndroidMode::null: + qCritical() << "Cannot load URL of mode 'null' in DbAndroidPathDialog::loadUrl()."; + return; + } + + ui->portSpin->setValue(dbUrl.getPort()); + if (ui->databaseCombo->findText(dbUrl.getDbName()) == -1) + ui->databaseCombo->addItem(dbUrl.getDbName()); + + ui->databaseCombo->setCurrentText(dbUrl.getDbName()); + + if (!dbUrl.getPassword().isNull()) + { + ui->passwordGroup->setChecked(true); + ui->passwordEdit->setText(dbUrl.getPassword()); + } +} + +void DbAndroidPathDialog::scheduleDbListUpdate() +{ + if (suspendDbListUpdates) + return; + + bool startCover = true; + if (dbListUpdateTimer->isActive()) + { + dbListUpdateTimer->stop(); + startCover = false; + } + + dbListUpdateTimer->start(); + if (startCover) + dbListCover->show(); + + handleDbCreationUpdate(false); + updateValidations(); +} + +void DbAndroidPathDialog::scheduleAppListUpdate() +{ + if (getSelectedMode() != DbAndroidMode::SHELL) + return; + + if (suspendAppListUpdates) + return; + + bool startCover = true; + if (appListUpdateTimer->isActive()) + { + appListUpdateTimer->stop(); + startCover = false; + } + + appListUpdateTimer->start(); + if (startCover) + appListCover->show(); + + updateValidations(); +} + +void DbAndroidPathDialog::refreshDbList() +{ + if (updatingDbList) + { + // Already busy, schedule next update afterwards. + scheduleDbListUpdate(); + return; + } + + updateUrl(); + ui->databaseCombo->clear(); + + if (!dbUrl.isValid(false)) + { + dbListCover->hide(); + return; + } + + updatingDbList = true; + QtConcurrent::run(this, &DbAndroidPathDialog::asyncDbUpdate, dbUrl.toUrlString(), dbUrl.getMode()); +} + +void DbAndroidPathDialog::refreshAppList() +{ + if (updatingAppList) + { + // Already busy, schedule next update afterwards. + scheduleAppListUpdate(); + return; + } + + updateUrl(); + setDbListUpdatesEnabled(false); + ui->appCombo->clear(); + setDbListUpdatesEnabled(true); + + if (!dbUrl.isValid(false)) + { + appListCover->hide(); + return; + } + + updatingAppList = true; + QtConcurrent::run(this, &DbAndroidPathDialog::asyncAppUpdate, dbUrl.toUrlString(), dbUrl.getMode()); +} + +void DbAndroidPathDialog::asyncDbUpdate(const QString& connectionUrl, DbAndroidMode enforcedMode) +{ + DbAndroidUrl url(connectionUrl); + url.setEnforcedMode(enforcedMode); + + QScopedPointer connection(plugin->getConnectionFactory()->create(url)); + if (!connection->connectToAndroid(url)) + { + qDebug() << "Could not open db connection" << connectionUrl; + emit asyncDbListUpdatingFinished(connection->isAppOkay()); + emit callForValidations(); + return; + } + + QStringList dbList = connection->getDbList(); + bool appOk = connection->isAppOkay(); + + connection->disconnectFromAndroid(); + + emit callForDbCreationUpdate(appOk); + emit callForDbListUpdate(dbList); + emit asyncDbListUpdatingFinished(appOk); + emit callForValidations(); +} + +void DbAndroidPathDialog::asyncAppUpdate(const QString& connectionUrl, DbAndroidMode enforcedMode) +{ + DbAndroidUrl url(connectionUrl); + url.setEnforcedMode(enforcedMode); + + QScopedPointer connection(plugin->getConnectionFactory()->create(url)); + QStringList appList = connection->getAppList(); + emit callForAppListUpdate(appList); + emit asyncAppListUpdatingFinished(); + emit callForValidations(); +} + +void DbAndroidPathDialog::refreshDevices() +{ + static_qstring(displayNameTpl, "%1 (%2)"); + ui->deviceCombo->clear(); + + QString displayName; + QList deviceDetails = plugin->getAdbManager()->getDeviceDetails(); + for (const AdbManager::Device& details : deviceDetails) + { + if (details.fullName.isEmpty()) + displayName = details.id; + else + displayName = displayNameTpl.arg(details.fullName, details.id); + + ui->deviceCombo->addItem(displayName, details.id); + } +} + +DbAndroidMode DbAndroidPathDialog::getSelectedMode() const +{ + if (ui->ipRadio->isChecked()) + return DbAndroidMode::NETWORK; + + if (ui->usbRadio->isChecked()) + return DbAndroidMode::USB; + + return DbAndroidMode::SHELL; +} + +void DbAndroidPathDialog::setDbListUpdatesEnabled(bool enabled) +{ + suspendDbListUpdates = !enabled; + if (enabled) + { + connect(ui->deviceCombo, SIGNAL(currentTextChanged(QString)), this, SLOT(scheduleDbListUpdate())); + connect(ui->appCombo, SIGNAL(currentTextChanged(QString)), this, SLOT(scheduleDbListUpdate())); + } + else + { + disconnect(ui->deviceCombo, SIGNAL(currentTextChanged(QString)), this, SLOT(scheduleDbListUpdate())); + disconnect(ui->appCombo, SIGNAL(currentTextChanged(QString)), this, SLOT(scheduleDbListUpdate())); + } +} + +void DbAndroidPathDialog::updateDeviceList() +{ + suspendDbListUpdates = true; + + bool dbListNeedsUpdate = false; + QString oldValue = ui->deviceCombo->currentData().toString(); + + refreshDevices(); + int idx = ui->deviceCombo->findData(oldValue); + if (idx > -1) + ui->deviceCombo->setCurrentIndex(idx); + else + dbListNeedsUpdate = true; + + suspendDbListUpdates = false; + + updateValidations(); + + if (dbListNeedsUpdate) + scheduleDbListUpdate(); +} + +void DbAndroidPathDialog::updateValidations() +{ + bool isUpdating = dbListUpdateTimer->isActive(); + bool ipOk = true; + bool deviceOk = true; + if (ui->ipRadio->isChecked()) + { + ipOk = IpValidator::check(ui->ipEdit->text()); + setValidState(ui->ipEdit, ipOk, tr("Enter valid IP address.")); + } + else + { + deviceOk = !ui->deviceCombo->currentData().toString().isEmpty(); + setValidState(ui->deviceCombo, deviceOk, tr("Pick Android device.")); + } + + bool dbOk = !ui->databaseCombo->currentText().isEmpty(); + setValidState(ui->databaseCombo, dbOk, tr("Pick Android database.")); + + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(ipOk && deviceOk && dbOk && !isUpdating); +} + +void DbAndroidPathDialog::handleUpdateDbList(const QStringList& dbList) +{ + ui->databaseCombo->addItems(dbList); + if (dbList.contains(dbUrl.getDbName())) + ui->databaseCombo->setCurrentText(dbUrl.getDbName()); +} + +void DbAndroidPathDialog::handleUpdateAppList(const QStringList& apps) +{ + fullAppList = apps; + QStringList filtered = apps.filter(ui->appFilterEdit->text(), Qt::CaseInsensitive); + ui->appCombo->addItems(filtered); + if (filtered.contains(dbUrl.getApplication())) + ui->appCombo->setCurrentText(dbUrl.getApplication()); +} + +void DbAndroidPathDialog::handleFinishedAsyncDbListUpdate(bool appOkay) +{ + if (getSelectedMode() == DbAndroidMode::SHELL) + setValidState(ui->appCombo, appOkay, tr("Selected Android application is unknown, or not debuggable.")); + + dbListCover->hide(); + updatingDbList = false; +} + +void DbAndroidPathDialog::handleFinishedAsyncAppListUpdate() +{ + appListCover->hide(); + updatingAppList = false; +} + +void DbAndroidPathDialog::handleDbCreationUpdate(bool canCreateDatabases) +{ + ui->createDatabaseButton->setEnabled(canCreateDatabases); +} + +void DbAndroidPathDialog::createNewDatabase() +{ + DbAndroidUrl tmpUrl(dbUrl); + tmpUrl.setDbName(QString()); + + DbAndroidConnection::ExecutionResult results; + QString name; + bool ok = false; + while (!ok) + { + name = QInputDialog::getText(this, tr("Create new database"), tr("Please provide name for the new database.\n" + "It's the name which Android application will use to connect to the database:")); + + if (name.isNull()) + break; + + if (ui->databaseCombo->findText(name) > -1) + { + QMessageBox::warning(this, tr("Invalid name"), tr("Database with the same name (%1) already exists on the device.\n" + "The name must be unique.")); + continue; + } + + tmpUrl.setDbName(name); + QScopedPointer connection(plugin->getConnectionFactory()->create(tmpUrl)); + if (!connection->connectToAndroid(tmpUrl)) + { + QMessageBox::warning(this, tr("Invalid name"), tr("Could not create database '%1', because could not connect to the device.").arg(name)); + continue; + } + + results = connection->executeQuery("PRAGMA encoding;"); + ok = !results.wasError && results.resultDataList.size() > 0; + connection->disconnectFromAndroid(); + + if (!ok) + QMessageBox::warning(this, tr("Invalid name"), tr("Could not create database '%1'.\nDetails: %2").arg(name, results.errorMsg)); + } + + if (ok) + { + ui->databaseCombo->addItem(name); + ui->databaseCombo->setCurrentText(name); + } +} + +void DbAndroidPathDialog::deleteSelectedDatabase() +{ + updateUrl(); + QString dbName = dbUrl.getDbName(); + + QMessageBox::StandardButton res = QMessageBox::question(this, tr("Delete database"), tr("Are you sure you want to delete database '%1' from %2?") + .arg(dbName, dbUrl.getDisplayName())); + + if (res != QMessageBox::Yes) + return; + + int idx = ui->databaseCombo->findText(dbName); + if (idx < 0) + { + QStringList dbList; + for (int i = 0, total = ui->databaseCombo->count(); i < total; ++i) + dbList << ui->databaseCombo->itemText(i); + + qCritical() << "Tried to delete database, but it's not in the list of databases:" << dbName << "and the list is:" << dbList; + return; + } + + // Local db instance, will close on deletion. + QScopedPointer connection(plugin->getConnectionFactory()->create(dbUrl)); + if (!connection->connectToAndroid(dbUrl)) + { + QMessageBox::critical(this, tr("Error deleting"), tr("Could not connect to %1 in order to delete database '%2'.").arg(dbUrl.getDisplayName(), dbName)); + return; + } + + if (!connection->deleteDatabase(dbName)) + { + QMessageBox::critical(this, tr("Error deleting"), tr("Could not delete database named '%1' from the device.\n" + "Android device refused deletion, or it was impossible.").arg(dbName)); + + connection->disconnectFromAndroid(); + return; + } + connection->disconnectFromAndroid(); + + ui->databaseCombo->removeItem(idx); + if (ui->databaseCombo->count() > 0) + { + if (idx < ui->databaseCombo->count()) + ui->databaseCombo->setCurrentIndex(idx); + else + ui->databaseCombo->setCurrentIndex(ui->databaseCombo->count() - 1); + } +} + +void DbAndroidPathDialog::modeChanged(bool checked) +{ + if (!checked) + return; + + updateState(); + adjustSize(); + scheduleAppListUpdate(); + + if (getSelectedMode() != DbAndroidMode::SHELL) + scheduleDbListUpdate(); +} + +void DbAndroidPathDialog::applyAppFilter(const QString& value) +{ + QString selectedApp = ui->appCombo->currentText(); + QStringList filtered = fullAppList.filter(value, Qt::CaseInsensitive); + bool callDbListUpdate = false; + + setDbListUpdatesEnabled(false); + ui->appCombo->clear(); + ui->appCombo->addItems(filtered); + if (filtered.contains(selectedApp)) + ui->appCombo->setCurrentText(selectedApp); + else + callDbListUpdate = true; + + setDbListUpdatesEnabled(true); + + if (callDbListUpdate) + scheduleDbListUpdate(); +} + +void DbAndroidPathDialog::accept() +{ + updateUrl(); + QDialog::accept(); +} + +void DbAndroidPathDialog::updateState() +{ + DbAndroidMode mode = getSelectedMode(); + + ui->deviceGroup->setVisible(mode == DbAndroidMode::SHELL || mode == DbAndroidMode::USB); + ui->ipGroup->setVisible(mode == DbAndroidMode::NETWORK); + ui->portGroup->setVisible(mode == DbAndroidMode::NETWORK || mode == DbAndroidMode::USB); + ui->appGroup->setVisible(mode == DbAndroidMode::SHELL); + ui->passwordGroup->setVisible(mode == DbAndroidMode::NETWORK || mode == DbAndroidMode::USB); + + ui->deleteDatabaseButton->setEnabled(ui->databaseCombo->currentIndex() > -1); + ui->passwordEdit->setEnabled(ui->passwordGroup->isChecked()); + updateValidations(); +} diff --git a/Plugins/DbAndroid/dbandroidpathdialog.h b/Plugins/DbAndroid/dbandroidpathdialog.h new file mode 100644 index 0000000..4c4496f --- /dev/null +++ b/Plugins/DbAndroid/dbandroidpathdialog.h @@ -0,0 +1,80 @@ +#ifndef DBANDROIDPATHDIALOG_H +#define DBANDROIDPATHDIALOG_H + +#include "dbandroidurl.h" +#include + +namespace Ui { + class DbAndroidPathDialog; +} + +class DbAndroid; +class QTimer; +class WidgetCover; +class DbAndroidInstance; + +class DbAndroidPathDialog : public QDialog +{ + Q_OBJECT + + public: + DbAndroidPathDialog(const DbAndroid* plugin, QWidget *parent = 0); + ~DbAndroidPathDialog(); + void setUrl(const QString& url); + void setUrl(const DbAndroidUrl& url); + const DbAndroidUrl& getUrl() const; + + private: + void init(); + void updateUrl(); + void loadUrl(); + void asyncDbUpdate(const QString& connectionUrl, DbAndroidMode enforcedMode); + void asyncAppUpdate(const QString& connectionUrl, DbAndroidMode enforcedMode); + void refreshDevices(); + DbAndroidMode getSelectedMode() const; + void setDbListUpdatesEnabled(bool enabled); + + const DbAndroid* plugin = nullptr; + DbAndroidUrl dbUrl; + Ui::DbAndroidPathDialog *ui; + QTimer* dbListUpdateTimer = nullptr; + QTimer* appListUpdateTimer = nullptr; + WidgetCover* dbListCover = nullptr; + WidgetCover* appListCover = nullptr; + bool updatingDbList = false; + bool updatingAppList = false; + bool suspendDbListUpdates = false; + bool suspendAppListUpdates = false; + QStringList fullAppList; + + private slots: + void scheduleDbListUpdate(); + void scheduleAppListUpdate(); + void updateState(); + void refreshDbList(); + void refreshAppList(); + void updateDeviceList(); + void updateValidations(); + void handleUpdateDbList(const QStringList& dbList); + void handleUpdateAppList(const QStringList& apps); + void handleFinishedAsyncDbListUpdate(bool appOkay); + void handleFinishedAsyncAppListUpdate(); + void handleDbCreationUpdate(bool canCreateDatabases); + void createNewDatabase(); + void deleteSelectedDatabase(); + void modeChanged(bool checked); + void applyAppFilter(const QString& value); + + public slots: + void accept(); + + signals: + void callForValidations(); + void callForDbCreationUpdate(bool canCreateDatabases); + void asyncDbListUpdatingFinished(bool appOkay); + void asyncAppListUpdatingFinished(); + void callForDbListUpdate(const QStringList& newList); + void callForAppListUpdate(const QStringList& newList); +}; + +#endif // DBANDROIDPATHDIALOG_H diff --git a/Plugins/DbAndroid/dbandroidpathdialog.ui b/Plugins/DbAndroid/dbandroidpathdialog.ui new file mode 100644 index 0000000..3643a9c --- /dev/null +++ b/Plugins/DbAndroid/dbandroidpathdialog.ui @@ -0,0 +1,244 @@ + + + DbAndroidPathDialog + + + + 0 + 0 + 402 + 546 + + + + + 400 + 0 + + + + Android database URL + + + + + + Connection method + + + + + + USB cable - port forwarding + + + true + + + + + + + USB cable - sqlite3 command + + + + + + + Network (IP address) + + + + + + + + + + Device + + + + + + + + + + + + IP address + + + + + + 15 + + + ???.???.???.??? + + + true + + + + + + + + + + Port + + + + + + 1 + + + 65535 + + + 12121 + + + + + + + + + + Remote access password + + + true + + + false + + + + + + <p>This is password configured in the SQLiteStudio service being embeded in the Android application.</p> + + + QLineEdit::Password + + + + + + + + + + Application + + + + + + + + + + 100 + 16777215 + + + + Filter + + + true + + + + + + + + + + Database + + + + + + + + + Create a new database directly on the device. + + + + + + + + + + Delete currently selected database from the device. The currently selected database is the one picked in the list on the left of this button. + + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + DbAndroidPathDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + DbAndroidPathDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/Plugins/DbAndroid/dbandroidshellconnection.cpp b/Plugins/DbAndroid/dbandroidshellconnection.cpp new file mode 100644 index 0000000..1a76f8d --- /dev/null +++ b/Plugins/DbAndroid/dbandroidshellconnection.cpp @@ -0,0 +1,363 @@ +#include "dbandroidshellconnection.h" +#include "adbmanager.h" +#include "dbandroid.h" +#include "services/notifymanager.h" +#include "common/utils_sql.h" +#include "csvserializer.h" +#include + +const CsvFormat DbAndroidShellConnection::CSV_FORMAT = CsvFormat(",", "\r\n", true, true); + +DbAndroidShellConnection::DbAndroidShellConnection(DbAndroid* plugin, QObject* parent) : + DbAndroidConnection(parent), plugin(plugin) +{ + this->adbManager = plugin->getAdbManager(); + connect(adbManager, SIGNAL(deviceListChanged(QStringList)), this, SLOT(checkForDisconnection(QStringList))); +} + +DbAndroidShellConnection::~DbAndroidShellConnection() +{ + +} + +bool DbAndroidShellConnection::connectToAndroid(const DbAndroidUrl& url) +{ + if (url.getMode() != DbAndroidMode::SHELL) + return false; + + if (!adbManager->getDevices().contains(url.getDevice())) + { + notifyWarn(tr("Cannot connect to device %1, because it's not visible to your computer.").arg(url.getDevice())); + return false; + } + + // Check if application is correct + if (url.getApplication().isEmpty()) + { + qCritical() << "Tried to connect to an empty application in DbAndroidShellConnection::connectToAndroid()"; + return false; + } + + QString stdOut; + bool res = adbManager->exec(QStringList({"shell", "run-as", url.getApplication(), "echo", "1"}), &stdOut); + if (!res) + { + notifyWarn(tr("Cannot connect to device %1, because the application %2 doesn't seem to be installed on the device.").arg(url.getDevice(), url.getApplication())); + return false; + } + + QMutexLocker lock(&appOkMutex); + appOkay = true; + if (stdOut.startsWith("run-as:")) + { + appOkay = false; + qWarning() << "Cannot connect to device" << url.getDevice() << "/" << url.getApplication() << "\nDetails:\n" << stdOut.trimmed(); + notifyWarn(tr("Cannot connect to device %1, because the application %2 is not debuggable.") + .arg(url.getDevice(), url.getApplication())); + return false; + } + + // Check if sqlite3 is available + res = adbManager->exec(QStringList({"shell", "run-as", url.getApplication(), "sqlite3", "--version"})); + if (!res) + { + notifyWarn(tr("Cannot connect to device %1, because '%2' command doesn't seem to be available on the device.").arg(url.getDevice(), "sqlite3")); + return false; + } + + // Check if databases directory exists + res = adbManager->exec(QStringList({"shell", "run-as", url.getApplication(), "ls", "databases"})); + if (!res) + { + // Doesn't exist. Create if possible. + res = adbManager->exec(QStringList({"shell", "run-as", url.getApplication(), "mkdir", "databases"})); + if (!res) + { + notifyWarn(tr("Cannot connect to device %1, because '%2' database cannot be accessed on the device.").arg(url.getDevice(), "sqlite3")); + return false; + } + } + + // Try to connect to target database. + connectionUrl = url; + connected = true; + + ExecutionResult response = executeQuery("select sqlite_version()"); + if (response.wasError) + { + disconnectFromAndroid(); + notifyWarn(tr("Cannot connect to device %1, because '%2' database cannot be accessed on the device. Details: %3") + .arg(url.getDevice(), "sqlite3", response.errorMsg)); + return false; + } + + return true; +} + +void DbAndroidShellConnection::disconnectFromAndroid() +{ + connectionUrl = DbAndroidUrl(); + connected = false; +} + +bool DbAndroidShellConnection::isConnected() const +{ + return connected; +} + +QString DbAndroidShellConnection::getDbName() const +{ + return connectionUrl.getDbName(); +} + +QStringList DbAndroidShellConnection::getDbList() +{ + QMutexLocker lock(&appOkMutex); + appOkay = true; + QString out; + bool res = adbManager->exec(QStringList({"shell", "run-as", connectionUrl.getApplication(), "ls", "databases"}), &out); + if (!res) + return QStringList(); + + if (out.startsWith("run-as:")) // means error + { + appOkay = false; + notifyWarn(tr("Cannot get list of databases for application %1. Details: %2").arg(connectionUrl.getApplication(), out.trimmed())); + qWarning() << "DbAndroidShellConnection::getDbList():" << out; + return QStringList(); + } + + QStringList finalList; + for (const QString& dbName : out.trimmed().split("\n", QString::SkipEmptyParts)) + { + if (dbName.trimmed().endsWith("-journal")) + continue; + + finalList << dbName.trimmed(); + } + + return finalList; +} + +QStringList DbAndroidShellConnection::getAppList() +{ + QString out; + bool res = adbManager->exec(QStringList({"shell", "pm list packages -3"}), &out); + if (!res) + return QStringList(); + + QStringList appList; + for (const QString& line : out.trimmed().split("\n", QString::SkipEmptyParts)) + appList << line.mid(8).trimmed(); // skip "package:" prefix + + return appList; +} + +bool DbAndroidShellConnection::isAppOkay() const +{ + QMutexLocker lock(&appOkMutex); + return appOkay; +} + +bool DbAndroidShellConnection::deleteDatabase(const QString& dbName) +{ + return adbManager->exec(QStringList({"shell", "run-as", connectionUrl.getApplication(), "rm", "-f", "databases/" + dbName, "databases/" + dbName + "-journal"})); +} + +DbAndroidConnection::ExecutionResult DbAndroidShellConnection::executeQuery(const QString& query) +{ + const static QStringList stdArguments = QStringList({"shell", "run-as", "", "sqlite3", "-csv", "-separator", ",", "-batch", "-header"}); + + // Prepare usual arguments + QStringList args = stdArguments; + args.replace(2, connectionUrl.getApplication()); + args << "databases/" + connectionUrl.getDbName(); + args << AdbManager::encode(query); + + // In case of SELECT we want to union typeof() for all columns first, then original query + bool isSelect = false; + getQueryAccessMode(query, Dialect::Sqlite3, &isSelect); + QStringList columnNames; + bool firstHalfForTypes = false; + if (isSelect) + { + columnNames = findColumns(args, query); + if (columnNames.size() > 0) + { + firstHalfForTypes = true; + args.removeLast(); + args << appendTypeQueryPart(query, columnNames); + } + } + + // Execute query and handle results + DbAndroidConnection::ExecutionResult results; + QByteArray out; + QByteArray err; + bool res = adbManager->execBytes(args, &out, &err); + if (!res) + { + results.wasError = true; + results.errorMsg = tr("Could not execute query on database '%1': %2").arg(connectionUrl.getDbName(), AdbManager::decode(err)); + return results; + } + + if (out.startsWith("run-as:")) // means error + { + results.wasError = true; + results.errorMsg = tr("Could not execute query on database '%1': %2").arg(connectionUrl.getDbName(), AdbManager::decode(out).trimmed()); + return results; + } + + + QList> deserialized = CsvSerializer::deserialize(out, CSV_FORMAT); + if (deserialized.size() == 0) + return results; // no results + + extractResultData(deserialized, firstHalfForTypes, results); + return results; +} + +QStringList DbAndroidShellConnection::findColumns(const QStringList& originalArgs, const QString& query) +{ + static_qstring(colQueryTpl, "SELECT * FROM (%1) LIMIT 1"); + + QStringList tmpArgs = originalArgs; + QString tmpQuery = query.trimmed(); + if (tmpQuery.endsWith(";")) + tmpQuery.chop(1); + + tmpQuery = colQueryTpl.arg(tmpQuery); + + tmpArgs.removeLast(); + tmpArgs << tmpQuery; + + QString out; + QString err; + bool res = adbManager->exec(tmpArgs, &out, &err); + if (!res) + { + qCritical() << "Error querying columns in DbAndroidShellConnection::findColumns(): " << out << "\n" << err; + return QStringList(); + } + + QList deserialized = CsvSerializer::deserialize(out, CSV_FORMAT); + if (deserialized.size() < 1) + { + // There will be no results. + return QStringList(); + } + + return deserialized.first(); +} + +QString DbAndroidShellConnection::appendTypeQueryPart(const QString& query, const QStringList& columnNames) +{ + static_qstring(typeTpl, "typeof(%1)"); + static_qstring(hexTpl, "hex(%1) AS %1"); + static_qstring(finalQueryTpl, "SELECT %3 FROM (%2) UNION ALL SELECT %1 FROM (%2)"); + + QString tmpQuery = query.trimmed(); + if (tmpQuery.endsWith(";")) + tmpQuery.chop(1); + + QStringList hexColumns; + QStringList typeColumns; + QString wrappedCol; + for (const QString& colName : columnNames) + { + wrappedCol = wrapObjIfNeeded(colName, Dialect::Sqlite3); + typeColumns << typeTpl.arg(wrappedCol); + hexColumns << hexTpl.arg(wrappedCol); + } + + return finalQueryTpl.arg(typeColumns.join(", "), tmpQuery, hexColumns.join(", ")); +} + +void DbAndroidShellConnection::extractResultData(const QList>& deserialized, bool firstHalfForTypes, DbAndroidConnection::ExecutionResult& results) +{ + for (const QByteArray& cell : deserialized.first()) + results.resultColumns << AdbManager::decode(cell); + + QList> data = deserialized.mid(1); // first row are column names + QList> types; + if (firstHalfForTypes) + { + types = data.mid(data.size() / 2); + data = data.mid(0, data.size() / 2); + + QVariantList rowDataList; + QVariantHash rowDataMap; + QList rowData; + QList rowTypes; + QVariant value; + for (int rowIdx = 0, totalRows = data.size(); rowIdx < totalRows; ++rowIdx) + { + rowData = data[rowIdx]; + rowTypes = types[rowIdx]; + + rowDataList.clear(); + rowDataMap.clear(); + for (int i = 0, total = rowData.size(); i < total; ++i) + { + value = valueFromString(rowData[i], rowTypes[i]); + rowDataList << value; + rowDataMap[results.resultColumns[i]] = value; + } + results.resultDataList << rowDataList; + results.resultDataMap << rowDataMap; + } + } + else + { + QVariantList rowDataList; + QVariantHash rowDataMap; + for (const QList& row : data) + { + rowDataList.clear(); + rowDataMap.clear(); + for (int i = 0, total = row.size(); i < total; ++i) + { + rowDataList << AdbManager::decode(row[i]); + rowDataMap[results.resultColumns[i]] = row[i]; + } + results.resultDataList << rowDataList; + results.resultDataMap << rowDataMap; + } + } +} + +QVariant DbAndroidShellConnection::valueFromString(const QByteArray& bytes, const QByteArray& type) +{ + static const QStringList types = QStringList({"null", "integer", "real", "text", "blob"}); + + DataType dataType = static_cast(types.indexOf(AdbManager::decode(type))); + QByteArray decodedBytes = QByteArray::fromHex(bytes); + switch (dataType) + { + case DataType::BLOB: + return decodedBytes; + case DataType::INTEGER: + return QString::fromLatin1(decodedBytes).toLongLong(); + case DataType::REAL: + return QString::fromLatin1(decodedBytes).toDouble(); + case DataType::TEXT: + return QString::fromUtf8(decodedBytes); + case DataType::_NULL: + break; + case DataType::UNKNOWN: + qCritical() << "Unknown type passed to DbAndroidShellConnection::valueFromString():" << type; + break; + } + return QVariant(QString::null); +} + +void DbAndroidShellConnection::checkForDisconnection(const QStringList& devices) +{ + if (connected && !devices.contains(connectionUrl.getDevice())) + { + disconnectFromAndroid(); + emit disconnected(); + } +} + diff --git a/Plugins/DbAndroid/dbandroidshellconnection.h b/Plugins/DbAndroid/dbandroidshellconnection.h new file mode 100644 index 0000000..1c0ae0f --- /dev/null +++ b/Plugins/DbAndroid/dbandroidshellconnection.h @@ -0,0 +1,59 @@ +#ifndef DBANDROIDSHELLCONNECTION_H +#define DBANDROIDSHELLCONNECTION_H + +#include "dbandroidconnection.h" +#include "csvformat.h" + +#include + +class DbAndroid; +class AdbManager; + +class DbAndroidShellConnection : public DbAndroidConnection +{ + Q_OBJECT + + public: + DbAndroidShellConnection(DbAndroid* plugin, QObject *parent = 0); + ~DbAndroidShellConnection(); + + bool connectToAndroid(const DbAndroidUrl& url); + void disconnectFromAndroid(); + bool isConnected() const; + QString getDbName() const; + QStringList getDbList(); + QStringList getAppList(); + bool isAppOkay() const; + bool deleteDatabase(const QString& dbName); + ExecutionResult executeQuery(const QString& query); + + private: + enum class DataType + { + UNKNOWN = -1, + _NULL = 0, + INTEGER = 1, + REAL = 2, + TEXT = 3, + BLOB = 4 + }; + + QStringList findColumns(const QStringList& originalArgs, const QString& query); + QString appendTypeQueryPart(const QString& query, const QStringList& columnNames); + void extractResultData(const QList >& deserialized, bool firstHalfForTypes, ExecutionResult& results); + QVariant valueFromString(const QByteArray& bytes, const QByteArray& type); + + DbAndroid* plugin = nullptr; + AdbManager* adbManager = nullptr; + bool connected = false; + DbAndroidUrl connectionUrl; + bool appOkay = false; + mutable QMutex appOkMutex; + + static const CsvFormat CSV_FORMAT; + + private slots: + void checkForDisconnection(const QStringList& devices); +}; + +#endif // DBANDROIDSHELLCONNECTION_H diff --git a/Plugins/DbAndroid/dbandroidurl.cpp b/Plugins/DbAndroid/dbandroidurl.cpp new file mode 100644 index 0000000..766d810 --- /dev/null +++ b/Plugins/DbAndroid/dbandroidurl.cpp @@ -0,0 +1,217 @@ +#include "dbandroidurl.h" +#include "common/ipvalidator.h" +#include + +DbAndroidUrl::DbAndroidUrl() +{ +} + +DbAndroidUrl::DbAndroidUrl(DbAndroidMode enforcedMode) : + enforcedMode(enforcedMode) +{ +} + +DbAndroidUrl::DbAndroidUrl(const DbAndroidUrl& other) : + enforcedMode(other.enforcedMode), host(other.host), device(other.device), port(other.port), dbName(other.dbName), password(other.password), + application(other.application) +{ +} + +DbAndroidUrl::DbAndroidUrl(const QString& path, bool obfuscatedPassword) +{ + parse(path, obfuscatedPassword); +} + +DbAndroidUrl::~DbAndroidUrl() +{ +} + +QString DbAndroidUrl::toUrlString(bool obfuscatedPassword) const +{ + return toUrl(obfuscatedPassword).toString(); +} + +QUrl DbAndroidUrl::toUrl(bool obfuscatedPassword) const +{ + QUrl url; + url.setScheme(SCHEME); + url.setHost(host); + url.setUserName(device); + url.setPort(port); + url.setPassword(getPassword(obfuscatedPassword)); + url.setPath("/" + (application.isEmpty() ? "!" : application) + "/" + dbName); + return url; +} + +QString DbAndroidUrl::getDisplayName() const +{ + if (!device.isNull()) + return device; + + return host; +} + +void DbAndroidUrl::parse(const QString& path, bool obfuscatedPassword) +{ + QUrl url(path); + if (url.scheme() != SCHEME) + return; + + host = url.host(); + device = url.userName(); + port = url.port(); + + QString urlPath = url.path(); + if (urlPath.startsWith("/")) + urlPath = urlPath.mid(1); + + QStringList pathParts = urlPath.split("/"); + + application = QString(); + if (pathParts.first() != "!") + application = pathParts.first(); + + dbName = QStringList(pathParts.mid(1)).join("/"); + if (!url.password().isEmpty()) + setPassword(url.password(), obfuscatedPassword); + else + setPassword(QString()); +} + +QString DbAndroidUrl::getApplication() const +{ + return application; +} + +void DbAndroidUrl::setApplication(const QString& value) +{ + application = value; +} + +QString DbAndroidUrl::getDevice() const +{ + return device; +} + +void DbAndroidUrl::setDevice(const QString& value) +{ + device = value; +} + +QString DbAndroidUrl::getHost() const +{ + return host; +} + +void DbAndroidUrl::setHost(const QString& value) +{ + host = value; +} + + +QString DbAndroidUrl::getPassword(bool obfuscated) const +{ + if (obfuscated) + return QString::fromLatin1(password.toUtf8().toHex().toBase64()); + + return password; +} + +void DbAndroidUrl::setPassword(const QString& value, bool obfuscated) +{ + if (obfuscated) + { + password = QString::fromUtf8(QByteArray::fromHex(QByteArray::fromBase64(value.toLatin1()))); + return; + } + + password = value; +} + + +QString DbAndroidUrl::getDbName() const +{ + return dbName; +} + +void DbAndroidUrl::setDbName(const QString& value) +{ + dbName = value; +} + +DbAndroidMode DbAndroidUrl::getMode() const +{ + if (enforcedMode != DbAndroidMode::null) + return enforcedMode; + + if (!application.isEmpty()) + return DbAndroidMode::SHELL; + + return host.isEmpty() ? DbAndroidMode::USB : DbAndroidMode::NETWORK; +} + +void DbAndroidUrl::setEnforcedMode(DbAndroidMode mode) +{ + enforcedMode = mode; +} + +bool DbAndroidUrl::isValid(bool validateConnectionIrrelevantParts) const +{ + if (isNull()) + return false; + + if (validateConnectionIrrelevantParts && dbName.isEmpty()) + return false; + + switch (getMode()) + { + case DbAndroidMode::NETWORK: + { + if (!isHostValid()) + return false; + + if (port <= 0) + return false; + + break; + } + case DbAndroidMode::USB: + { + if (port <= 0) + return false; + + break; + } + case DbAndroidMode::SHELL: + { + if (validateConnectionIrrelevantParts && application.isEmpty()) + return false; + + break; + } + case DbAndroidMode::null: + return false; + } + + return true; +} + +bool DbAndroidUrl::isHostValid() const +{ + return IpValidator::check(host); +} + +bool DbAndroidUrl::isNull() const +{ + return host.isEmpty() && device.isEmpty(); +} + +int DbAndroidUrl::getPort() const +{ + return port; +} + +void DbAndroidUrl::setPort(int value) +{ + port = value; +} diff --git a/Plugins/DbAndroid/dbandroidurl.h b/Plugins/DbAndroid/dbandroidurl.h new file mode 100644 index 0000000..594b21c --- /dev/null +++ b/Plugins/DbAndroid/dbandroidurl.h @@ -0,0 +1,59 @@ +#ifndef DBANDROIDURL_H +#define DBANDROIDURL_H + +#include "dbandroidmode.h" +#include +#include + +class DbAndroidUrl +{ + public: + DbAndroidUrl(); + DbAndroidUrl(const DbAndroidUrl& other); + explicit DbAndroidUrl(DbAndroidMode enforcedMode); + explicit DbAndroidUrl(const QString& path, bool obfuscatedPassword = true); + ~DbAndroidUrl(); + + QString toUrlString(bool obfuscatedPassword = true) const; + QUrl toUrl(bool obfuscatedPassword = true) const; + QString getDisplayName() const; + + int getPort() const; + void setPort(int value); + + QString getDbName() const; + void setDbName(const QString& value); + + DbAndroidMode getMode() const; + void setEnforcedMode(DbAndroidMode mode); + bool isValid(bool validateConnectionIrrelevantParts = true) const; + bool isHostValid() const; + bool isNull() const; + + QString getPassword(bool obfuscated = false) const; + void setPassword(const QString& value, bool obfuscated = false); + + QString getHost() const; + void setHost(const QString& value); + + QString getDevice() const; + void setDevice(const QString& value); + + QString getApplication() const; + void setApplication(const QString& value); + + private: + void parse(const QString& path, bool obfuscatedPassword = false); + + static const constexpr char* SCHEME = "android"; + + DbAndroidMode enforcedMode = DbAndroidMode::null; + QString host; + QString device; + int port = -1; + QString dbName; + QString password; + QString application; +}; + +#endif // DBANDROIDURL_H diff --git a/Plugins/DbAndroid/sqlqueryandroid.cpp b/Plugins/DbAndroid/sqlqueryandroid.cpp new file mode 100644 index 0000000..08fe5cc --- /dev/null +++ b/Plugins/DbAndroid/sqlqueryandroid.cpp @@ -0,0 +1,160 @@ +#include "dbandroidconnection.h" +#include "sqlqueryandroid.h" +#include "sqlresultrowandroid.h" +#include "parser/lexer.h" +#include "db/sqlerrorcodes.h" +#include "common/utils_sql.h" +#include "log.h" +#include "dbandroidinstance.h" +#include + +SqlQueryAndroid::SqlQueryAndroid(DbAndroidInstance* db, DbAndroidConnection* connection, const QString& query) : + db(db), connection(connection), queryString(query) +{ + tokenizedQuery = Lexer::tokenize(query, Dialect::Sqlite3); +} + +SqlQueryAndroid::~SqlQueryAndroid() +{ +} + +QString SqlQueryAndroid::getErrorText() +{ + return errorText; +} + +int SqlQueryAndroid::getErrorCode() +{ + return errorCode; +} + +QStringList SqlQueryAndroid::getColumnNames() +{ + return resultColumns; +} + +int SqlQueryAndroid::columnCount() +{ + return resultColumns.size(); +} + +void SqlQueryAndroid::rewind() +{ + currentRow = -1; +} + +SqlResultsRowPtr SqlQueryAndroid::nextInternal() +{ + if (resultDataList.size() == 0) + return SqlResultsRowPtr(); + + currentRow++; + SqlResultRowAndroid* resultRow = new SqlResultRowAndroid(resultDataMap[currentRow], resultDataList[currentRow]); + return SqlResultsRowPtr(resultRow); +} + +bool SqlQueryAndroid::hasNextInternal() +{ + return (currentRow + 1 < resultDataList.size()); +} + +bool SqlQueryAndroid::execInternal(const QList& args) +{ + resetResponse(); + logSql(db, queryString, args, flags); + + int argIdx = 0; + QString query; + for (const TokenPtr& token : tokenizedQuery) + { + if (token->type != Token::BIND_PARAM) + { + query += token->value; + continue; + } + + query += convertArg(args[argIdx++]); + } + + return executeAndHandleResponse(query); +} + +bool SqlQueryAndroid::execInternal(const QHash& args) +{ + resetResponse(); + logSql(db, queryString, args, flags); + + QString argName; + QString query; + for (const TokenPtr& token : tokenizedQuery) + { + if (token->type != Token::BIND_PARAM) + { + query += token->value; + continue; + } + + argName = token->value; + if (!args.contains(argName)) + { + errorCode = SqlErrorCode::OTHER_EXECUTION_ERROR; + errorText = QObject::tr("Cannot bind argument '%1' of the query, because it's value is missing.").arg(argName); + return false; + } + + query += convertArg(args[argName]); + } + + return executeAndHandleResponse(query); +} + +QString SqlQueryAndroid::convertArg(const QVariant& value) +{ + if (value.isNull() || !value.isValid()) + return "NULL"; + + switch (value.type()) + { + case QVariant::Int: + case QVariant::UInt: + case QVariant::LongLong: + case QVariant::ULongLong: + case QVariant::Double: + return value.toString(); + case QVariant::String: + return "'" + value.toString().replace("'", "''") + "'"; + case QVariant::ByteArray: + return "x'" + value.toByteArray().toHex() + "'"; + default: + break; + } + + qCritical() << "Unhandled argument type in SqlQueryAndroid::convertArg():" << value.type(); + return ""; +} + +bool SqlQueryAndroid::executeAndHandleResponse(const QString& query) +{ + DbAndroidConnection::ExecutionResult results = connection->executeQuery(query); + if (results.wasError) + { + errorCode = (results.errorCode != 0) ? results.errorCode : SqlErrorCode::OTHER_EXECUTION_ERROR; + errorText = results.errorMsg; + return false; + } + + resultColumns = results.resultColumns; + resultDataMap = results.resultDataMap; + resultDataList = results.resultDataList; + return true; +} + +void SqlQueryAndroid::resetResponse() +{ + resultColumns.clear(); + resultDataMap.clear(); + resultDataList.clear(); + currentRow = -1; + errorCode = 0; + errorText = QString(); +} diff --git a/Plugins/DbAndroid/sqlqueryandroid.h b/Plugins/DbAndroid/sqlqueryandroid.h new file mode 100644 index 0000000..3944c21 --- /dev/null +++ b/Plugins/DbAndroid/sqlqueryandroid.h @@ -0,0 +1,47 @@ +#ifndef SQLQUERYANDROID_H +#define SQLQUERYANDROID_H + +#include "db/sqlquery.h" +#include "parser/token.h" +#include + +class DbAndroidConnection; +class DbAndroidInstance; + +class SqlQueryAndroid : public SqlQuery +{ + public: + SqlQueryAndroid(DbAndroidInstance* db, DbAndroidConnection* connection, const QString& query); + ~SqlQueryAndroid(); + + QString getErrorText(); + int getErrorCode(); + QStringList getColumnNames(); + int columnCount(); + void rewind(); + + protected: + SqlResultsRowPtr nextInternal(); + bool hasNextInternal(); + bool execInternal(const QList& args); + bool execInternal(const QHash& args); + + private: + bool executeAndHandleResponse(const QString& query); + void resetResponse(); + + static QString convertArg(const QVariant& value); + + DbAndroidInstance* db = nullptr; + DbAndroidConnection* connection = nullptr; + QString queryString; + TokenList tokenizedQuery; + int errorCode = 0; + QString errorText; + QStringList resultColumns; + QList resultDataMap; + QList resultDataList; + int currentRow = -1; +}; + +#endif // SQLQUERYANDROID_H diff --git a/Plugins/DbAndroid/sqlresultrowandroid.cpp b/Plugins/DbAndroid/sqlresultrowandroid.cpp new file mode 100644 index 0000000..cc08fa5 --- /dev/null +++ b/Plugins/DbAndroid/sqlresultrowandroid.cpp @@ -0,0 +1,12 @@ +#include "sqlresultrowandroid.h" + +SqlResultRowAndroid::SqlResultRowAndroid(const QVariantHash& resultMap, const QVariantList& resultList) +{ + valuesMap = resultMap; + values = resultList; +} + +SqlResultRowAndroid::~SqlResultRowAndroid() +{ +} + diff --git a/Plugins/DbAndroid/sqlresultrowandroid.h b/Plugins/DbAndroid/sqlresultrowandroid.h new file mode 100644 index 0000000..5acc95a --- /dev/null +++ b/Plugins/DbAndroid/sqlresultrowandroid.h @@ -0,0 +1,13 @@ +#ifndef SQLRESULTROWANDROID_H +#define SQLRESULTROWANDROID_H + +#include "db/sqlresultsrow.h" + +class SqlResultRowAndroid : public SqlResultsRow +{ + public: + SqlResultRowAndroid(const QVariantHash& resultMap, const QVariantList& resultList); + ~SqlResultRowAndroid(); +}; + +#endif // SQLRESULTROWANDROID_H diff --git a/Plugins/DbSqlite2/DbSqlite2.pro b/Plugins/DbSqlite2/DbSqlite2.pro index de31058..b254ca5 100644 --- a/Plugins/DbSqlite2/DbSqlite2.pro +++ b/Plugins/DbSqlite2/DbSqlite2.pro @@ -35,3 +35,4 @@ OTHER_FILES += \ + diff --git a/Plugins/HtmlExport/HtmlExport.pro b/Plugins/HtmlExport/HtmlExport.pro index 8f3578e..3ef30d0 100644 --- a/Plugins/HtmlExport/HtmlExport.pro +++ b/Plugins/HtmlExport/HtmlExport.pro @@ -29,7 +29,8 @@ FORMS += \ htmlexport.ui -TRANSLATIONS += HtmlExport_zh_CN.ts \ +TRANSLATIONS += HtmlExport_it.ts \ + HtmlExport_zh_CN.ts \ HtmlExport_sk.ts \ HtmlExport_de.ts \ HtmlExport_ru.ts \ @@ -49,3 +50,4 @@ TRANSLATIONS += HtmlExport_zh_CN.ts \ + diff --git a/Plugins/HtmlExport/HtmlExport_it.ts b/Plugins/HtmlExport/HtmlExport_it.ts new file mode 100644 index 0000000..208900c --- /dev/null +++ b/Plugins/HtmlExport/HtmlExport_it.ts @@ -0,0 +1,173 @@ + + + + + HtmlExport + + + SQL query results + + + + + + no type + + + + + + Exported table: %1 + + + + + + Table: %1 + + + + + virtual + + + + + Exported database: %1 + + + + + Index: %1 + + + + + For table: + + + + + Unique: + + + + + Yes + + + + + No + + + + + Column + + + + + Collating + + + + + Sort order + + + + + Trigger: %1 + + + + + Activated: + + + + + Action: + + + + + On view: + + + + + On table: + + + + + Activate condition: + + + + + Code executed: + + + + + View: %1 + + + + + Document generated by SQLiteStudio v%1 on %2 + + + + + HtmlExportConfig + + + Maximum number of characters per cell: + + + + + Include data types in first row + + + + + Column names as first row + + + + + Row numbers as first column + + + + + Output format + + + + + Format document (new lines, indentation) + + + + + Compress (everything in one line) + + + + + <p>When enabled, HTML characters such as &lt;, &gt; and &amp; are not escaped in exported values. This allows you for example to export hyper-link enabled documents, but it also may result in incorrect HTML document (unmatched pairs of &lt; and &gt; characters). Be warned.</p> + + + + + Don't escape HTML characters + + + + diff --git a/Plugins/JsonExport/JsonExport.pro b/Plugins/JsonExport/JsonExport.pro index 6f8ae18..47fcb05 100644 --- a/Plugins/JsonExport/JsonExport.pro +++ b/Plugins/JsonExport/JsonExport.pro @@ -28,7 +28,8 @@ RESOURCES += \ jsonexport.qrc -TRANSLATIONS += JsonExport_zh_CN.ts \ +TRANSLATIONS += JsonExport_it.ts \ + JsonExport_zh_CN.ts \ JsonExport_sk.ts \ JsonExport_de.ts \ JsonExport_ru.ts \ @@ -48,3 +49,4 @@ TRANSLATIONS += JsonExport_zh_CN.ts \ + diff --git a/Plugins/JsonExport/JsonExport_it.ts b/Plugins/JsonExport/JsonExport_it.ts new file mode 100644 index 0000000..74b0169 --- /dev/null +++ b/Plugins/JsonExport/JsonExport_it.ts @@ -0,0 +1,22 @@ + + + + + JsonExportConfig + + + Output format + + + + + Format document (new lines, indentation) + + + + + Compress (everything in one line) + + + + diff --git a/Plugins/JsonExport/JsonExport_zh_CN.ts b/Plugins/JsonExport/JsonExport_zh_CN.ts index be101c4..82b3b1d 100644 --- a/Plugins/JsonExport/JsonExport_zh_CN.ts +++ b/Plugins/JsonExport/JsonExport_zh_CN.ts @@ -6,17 +6,17 @@ Output format - + 输出格式 Format document (new lines, indentation) - + 格式化的文本(多行,缩进) Compress (everything in one line) - + 压缩(产生单行文件) diff --git a/Plugins/PdfExport/PdfExport.pro b/Plugins/PdfExport/PdfExport.pro index ff86d24..95848c1 100644 --- a/Plugins/PdfExport/PdfExport.pro +++ b/Plugins/PdfExport/PdfExport.pro @@ -26,7 +26,8 @@ RESOURCES += \ pdfexport.qrc -TRANSLATIONS += PdfExport_zh_CN.ts \ +TRANSLATIONS += PdfExport_it.ts \ + PdfExport_zh_CN.ts \ PdfExport_sk.ts \ PdfExport_de.ts \ PdfExport_ru.ts \ @@ -46,3 +47,4 @@ TRANSLATIONS += PdfExport_zh_CN.ts \ + diff --git a/Plugins/PdfExport/PdfExport_de.ts b/Plugins/PdfExport/PdfExport_de.ts index 7e5d8e6..7498c43 100644 --- a/Plugins/PdfExport/PdfExport_de.ts +++ b/Plugins/PdfExport/PdfExport_de.ts @@ -6,164 +6,165 @@ SQLiteStudio v%1 - + SQLiteStudio v%1 SQL query results - + SQL Abfragenergebnis Exported table: %1 - + Exportierte Tabelle: %1 Table: %1 - + Tabelle: %1 Column - + Spalte Data type - + Datentyp Constraints - + Bedingungen Global table constraints - + Globale Tabellenbedingungen Exported database: %1 - + Exportierte Datenbank: %1 Index: %1 - + Index: %1 Property index header - + Eigenschaft Value index header - + Wert Indexed table - + Indexierte Tabelle Unique index - + Singularer Index? Fuer Unique finde ich keine richtige Übersetzung. + Index (duplikatfrei) Yes - + Ja No - + Nein Collation - + Kollation Sort order - + Sortierreihenfolge Partial index condition - + Teilindexbedingung Trigger: %1 - + Trigger: %1 Property trigger header - + Eigenschaft Value trigger header - + Wert Activation time - + Ausführungszeit For action - + Für Aktion On view - + auf Index On table - + auf Tabelle Activation condition - + Ausführungskondition Code executed - + Code ausgeführt View: %1 - + View: %1 Query: - + Abfrage: Document generated with SQLiteStudio v%1 - + Dokument mit SQLiteStudio v%1 generiert @@ -171,32 +172,33 @@ Size and layout - + Format und Layout Page size: - + Format: + Right margin: - + Seitenabstand Rechts: Left margin: - + Seitenabstand Links: Cell padding: - + Zellenabstand: Limit characters in single cell: - + Maximale Anzahl an Zeichen pro Zelle: @@ -205,52 +207,52 @@ mm - + mm Bottom margin: - + Seitenabstand Unten: Top margin: - + Seitenabstand oben: Font - + Schriftart Colors - + Farben Headers background: - + Hintergrundfarbe Kopfzeile: NULL value color: - + NULL Wert Farbe: Other settings - + Sonstige Einstellungen Print row numbers for data - + Zeilennummerierung drucken Print page numbers - + Seitenzahl drucken diff --git a/Plugins/PdfExport/PdfExport_it.ts b/Plugins/PdfExport/PdfExport_it.ts new file mode 100644 index 0000000..913432b --- /dev/null +++ b/Plugins/PdfExport/PdfExport_it.ts @@ -0,0 +1,256 @@ + + + + + PdfExport + + + SQLiteStudio v%1 + + + + + SQL query results + + + + + + Exported table: %1 + + + + + + Table: %1 + + + + + + Column + + + + + Data type + + + + + Constraints + + + + + Global table constraints + + + + + Exported database: %1 + + + + + Index: %1 + + + + + Property + index header + + + + + Value + index header + + + + + Indexed table + + + + + Unique index + + + + + Yes + + + + + No + + + + + Collation + + + + + Sort order + + + + + Partial index condition + + + + + Trigger: %1 + + + + + Property + trigger header + + + + + Value + trigger header + + + + + Activation time + + + + + For action + + + + + On view + + + + + On table + + + + + Activation condition + + + + + Code executed + + + + + View: %1 + + + + + Query: + + + + + Document generated with SQLiteStudio v%1 + + + + + PdfExportConfig + + + Size and layout + + + + + Page size: + + + + + Right margin: + + + + + Left margin: + + + + + Cell padding: + + + + + Limit characters in single cell: + + + + + + + + + mm + + + + + Bottom margin: + + + + + Top margin: + + + + + Font + + + + + Colors + + + + + Headers background: + + + + + NULL value color: + + + + + Other settings + + + + + Print row numbers for data + + + + + Print page numbers + + + + diff --git a/Plugins/Plugins.pro b/Plugins/Plugins.pro index 805521a..81b2337 100644 --- a/Plugins/Plugins.pro +++ b/Plugins/Plugins.pro @@ -16,4 +16,5 @@ SUBDIRS += \ Printing \ SqlEnterpriseFormatter \ ConfigMigration \ - ScriptingTcl + ScriptingTcl \ + DbAndroid diff --git a/Plugins/Printing/Printing.pro b/Plugins/Printing/Printing.pro index c1afb9f..5006d60 100644 --- a/Plugins/Printing/Printing.pro +++ b/Plugins/Printing/Printing.pro @@ -35,7 +35,8 @@ RESOURCES += \ printing.qrc -TRANSLATIONS += Printing_zh_CN.ts \ +TRANSLATIONS += Printing_it.ts \ + Printing_zh_CN.ts \ Printing_sk.ts \ Printing_de.ts \ Printing_ru.ts \ @@ -55,3 +56,4 @@ TRANSLATIONS += Printing_zh_CN.ts \ + diff --git a/Plugins/Printing/Printing_de.ts b/Plugins/Printing/Printing_de.ts index 6a165a2..fb45beb 100644 --- a/Plugins/Printing/Printing_de.ts +++ b/Plugins/Printing/Printing_de.ts @@ -6,27 +6,27 @@ Print data - + Daten drucken Print query - + Abfrage drucken No data to print. - + Keine Daten, die gedruckt werden müssen. Printing data. - + Drucke Daten. Printing query. - + Drucke Abfrage. @@ -34,7 +34,7 @@ Printing - + Drucken diff --git a/Plugins/Printing/Printing_it.ts b/Plugins/Printing/Printing_it.ts new file mode 100644 index 0000000..0a21af2 --- /dev/null +++ b/Plugins/Printing/Printing_it.ts @@ -0,0 +1,40 @@ + + + + + Printing + + + Print data + + + + + Print query + + + + + No data to print. + + + + + Printing data. + + + + + Printing query. + + + + + PrintingExport + + + Printing + + + + diff --git a/Plugins/Printing/Printing_zh_CN.ts b/Plugins/Printing/Printing_zh_CN.ts index 0b1b255..e3a7a2d 100644 --- a/Plugins/Printing/Printing_zh_CN.ts +++ b/Plugins/Printing/Printing_zh_CN.ts @@ -6,27 +6,27 @@ Print data - + 打印数据 Print query - + 打印 query No data to print. - + 没有数据可打印。 Printing data. - + 打印数据中。 Printing query. - + 打印query中。 @@ -34,7 +34,7 @@ Printing - + 打印 diff --git a/Plugins/RegExpImport/RegExpImport.pro b/Plugins/RegExpImport/RegExpImport.pro index aa790b2..8ef5298 100644 --- a/Plugins/RegExpImport/RegExpImport.pro +++ b/Plugins/RegExpImport/RegExpImport.pro @@ -28,7 +28,8 @@ RESOURCES += \ regexpimport.qrc -TRANSLATIONS += RegExpImport_zh_CN.ts \ +TRANSLATIONS += RegExpImport_it.ts \ + RegExpImport_zh_CN.ts \ RegExpImport_sk.ts \ RegExpImport_de.ts \ RegExpImport_ru.ts \ @@ -48,3 +49,4 @@ TRANSLATIONS += RegExpImport_zh_CN.ts \ + diff --git a/Plugins/RegExpImport/RegExpImport_it.ts b/Plugins/RegExpImport/RegExpImport_it.ts new file mode 100644 index 0000000..00869f1 --- /dev/null +++ b/Plugins/RegExpImport/RegExpImport_it.ts @@ -0,0 +1,83 @@ + + + + + RegExpImport + + + Text files (*.txt);;All files (*) + + + + + Cannot read file %1 + + + + + Enter the regular expression pattern. + + + + + Invalid pattern: %1 + + + + + Requested capture index %1 is out of range. + + + + + <p>Requested capture group name '%1', but it's not defined in the pattern: <pre>%2</pre></p> + + + + + RegExpImportConfig + + + Capture groups + + + + + Treat all RegExp capture groups as columns + + + + + Import only following groups: + + + + + <p>Enter comma separated list of capture group indexes. The 0 index refers to the entire matched string.</p> +<p>If you used named groups in the pattern, you can use names instead of indexes. You can mix indexes and names in this list.</p> + + + + + Example: 1, 3, 4 + + + + + Pattern: + + + + + <p>Use Regular Expression groups to enclose parts of the expression that you want to import. If you want to use a group, that you don't want to import, then use "import only following groups" option below. + +You can use named groups and refer to them in group list below. To name a group use: <pre>(?&lt;myGroupName&gt;\s+\d+\s+)</pre></p> + + + + + Example: (\d+)\s+((\d+)\w+)\s+(\w+) + + + + diff --git a/Plugins/ScriptingTcl/ScriptingTcl.pro b/Plugins/ScriptingTcl/ScriptingTcl.pro index fc5cf5b..d888a32 100644 --- a/Plugins/ScriptingTcl/ScriptingTcl.pro +++ b/Plugins/ScriptingTcl/ScriptingTcl.pro @@ -37,6 +37,10 @@ linux: { TCL_CONFIG_DIR = $$system(echo "puts [info library]" | tclsh) TCL_CONFIG = $$TCL_CONFIG_DIR/tclConfig.sh message("Looking for $$TCL_CONFIG") + !exists($$TCL_CONFIG) { + TCL_CONFIG = $$TCL_CONFIG_DIR/../tclConfig.sh + message("Looking for $$TCL_CONFIG") + } !exists($$TCL_CONFIG) { # Debian case DEBIAN_ARCH_PATH=$$system(dpkg-architecture -qDEB_HOST_MULTIARCH) @@ -138,7 +142,8 @@ RESOURCES += \ scriptingtcl.qrc -TRANSLATIONS += ScriptingTcl_zh_CN.ts \ +TRANSLATIONS += ScriptingTcl_it.ts \ + ScriptingTcl_zh_CN.ts \ ScriptingTcl_sk.ts \ ScriptingTcl_de.ts \ ScriptingTcl_ru.ts \ @@ -158,3 +163,4 @@ TRANSLATIONS += ScriptingTcl_zh_CN.ts \ + diff --git a/Plugins/ScriptingTcl/ScriptingTcl_it.ts b/Plugins/ScriptingTcl/ScriptingTcl_it.ts new file mode 100644 index 0000000..dc735d6 --- /dev/null +++ b/Plugins/ScriptingTcl/ScriptingTcl_it.ts @@ -0,0 +1,22 @@ + + + + + ScriptingTcl + + + No database available in current context, while called Tcl's '%1' command. + + + + + Invalid '%1' command sytax. Should be: %2 + + + + + Error from Tcl's' '%1' command: %2 + + + + diff --git a/Plugins/SqlEnterpriseFormatter/SqlEnterpriseFormatter.pro b/Plugins/SqlEnterpriseFormatter/SqlEnterpriseFormatter.pro index 965767b..5af5bfa 100644 --- a/Plugins/SqlEnterpriseFormatter/SqlEnterpriseFormatter.pro +++ b/Plugins/SqlEnterpriseFormatter/SqlEnterpriseFormatter.pro @@ -100,7 +100,8 @@ RESOURCES += \ sqlenterpriseformatter.qrc -TRANSLATIONS += SqlEnterpriseFormatter_zh_CN.ts \ +TRANSLATIONS += SqlEnterpriseFormatter_it.ts \ + SqlEnterpriseFormatter_zh_CN.ts \ SqlEnterpriseFormatter_sk.ts \ SqlEnterpriseFormatter_de.ts \ SqlEnterpriseFormatter_ru.ts \ @@ -120,3 +121,4 @@ TRANSLATIONS += SqlEnterpriseFormatter_zh_CN.ts \ + diff --git a/Plugins/SqlEnterpriseFormatter/SqlEnterpriseFormatter_de.ts b/Plugins/SqlEnterpriseFormatter/SqlEnterpriseFormatter_de.ts index a83f5df..37751d0 100644 --- a/Plugins/SqlEnterpriseFormatter/SqlEnterpriseFormatter_de.ts +++ b/Plugins/SqlEnterpriseFormatter/SqlEnterpriseFormatter_de.ts @@ -4,8 +4,8 @@ QObject - - + + name example name wrapper @@ -200,7 +200,32 @@ - + + Comments + + + + + Preferred comment marker (where possible): + + + + + SqlEnterpriseFormatter.CommentMarkers + + + + + Move all comments to the line end + + + + + Line up comments at the line end + + + + Preview diff --git a/Plugins/SqlEnterpriseFormatter/SqlEnterpriseFormatter_es.ts b/Plugins/SqlEnterpriseFormatter/SqlEnterpriseFormatter_es.ts index 4d873e5..b0b5eed 100644 --- a/Plugins/SqlEnterpriseFormatter/SqlEnterpriseFormatter_es.ts +++ b/Plugins/SqlEnterpriseFormatter/SqlEnterpriseFormatter_es.ts @@ -4,8 +4,8 @@ QObject - - + + name example name wrapper @@ -200,7 +200,32 @@ - + + Comments + + + + + Preferred comment marker (where possible): + + + + + SqlEnterpriseFormatter.CommentMarkers + + + + + Move all comments to the line end + + + + + Line up comments at the line end + + + + Preview diff --git a/Plugins/SqlEnterpriseFormatter/SqlEnterpriseFormatter_fr.ts b/Plugins/SqlEnterpriseFormatter/SqlEnterpriseFormatter_fr.ts index 8a9178b..d0e62f7 100644 --- a/Plugins/SqlEnterpriseFormatter/SqlEnterpriseFormatter_fr.ts +++ b/Plugins/SqlEnterpriseFormatter/SqlEnterpriseFormatter_fr.ts @@ -4,8 +4,8 @@ QObject - - + + name example name wrapper Nom @@ -200,7 +200,32 @@ Mots clé en majuscule - + + Comments + + + + + Preferred comment marker (where possible): + + + + + SqlEnterpriseFormatter.CommentMarkers + + + + + Move all comments to the line end + + + + + Line up comments at the line end + + + + Preview Aperçu diff --git a/Plugins/SqlEnterpriseFormatter/SqlEnterpriseFormatter_it.ts b/Plugins/SqlEnterpriseFormatter/SqlEnterpriseFormatter_it.ts new file mode 100644 index 0000000..9e721a4 --- /dev/null +++ b/Plugins/SqlEnterpriseFormatter/SqlEnterpriseFormatter_it.ts @@ -0,0 +1,233 @@ + + + + + QObject + + + + name + example name wrapper + + + + + SqlEnterpriseFormatter + + + Indentation + + + + + Line up keywords in multi-line queries + + + + + Indent contents of parenthesis block + + + + + Tab size: + + + + + New lines + + + + + Before opening parenthesis in column definitions + + + + + After opening parenthesis in column definitions + + + + + Before closing parenthesis in column definitions + + + + + After closing parenthesis in column definitions + + + + + Before opening parenthesis in expressions + + + + + After opening parenthesis in expressions + + + + + Before closing parenthesis in expressions + + + + + After closing parenthesis in expressions + + + + + After JOIN keywords in FROM clause + + + + + Put each column constraint in CREATE TABLE into new line + + + + + After comma + + + + + After comma in expressions + + + + + After semicolon + + + + + + Never before semicolon + + + + + White spaces + + + + + Before comma in lists + + + + + After comma in lists + + + + + Before opening parenthesis + + + + + After opening parenthesis + + + + + Before closing parenthesis + + + + + After closing parenthesis + + + + + No space between SQL function name and opening parenthesis + + + + + Before dot operator (in path to database object) + + + + + After dot operator (in path to database object) + + + + + Before mathematical operator + + + + + After mathematical operator + + + + + Never before comma + + + + + Names + + + + + Preferred name wrapper + + + + + Always use name wrapping + + + + + Uppercase data type names + + + + + Uppercase keywords + + + + + Comments + + + + + Preferred comment marker (where possible): + + + + + SqlEnterpriseFormatter.CommentMarkers + + + + + Move all comments to the line end + + + + + Line up comments at the line end + + + + + Preview + + + + diff --git a/Plugins/SqlEnterpriseFormatter/SqlEnterpriseFormatter_pl.ts b/Plugins/SqlEnterpriseFormatter/SqlEnterpriseFormatter_pl.ts index 5cfbf32..2c03a46 100644 --- a/Plugins/SqlEnterpriseFormatter/SqlEnterpriseFormatter_pl.ts +++ b/Plugins/SqlEnterpriseFormatter/SqlEnterpriseFormatter_pl.ts @@ -4,8 +4,8 @@ QObject - - + + name example name wrapper nazwa @@ -201,7 +201,32 @@ Zmieniaj litery słów kluczowych na duże - + + Comments + + + + + Preferred comment marker (where possible): + + + + + SqlEnterpriseFormatter.CommentMarkers + + + + + Move all comments to the line end + + + + + Line up comments at the line end + + + + Preview Podgląd diff --git a/Plugins/SqlEnterpriseFormatter/SqlEnterpriseFormatter_pt_BR.ts b/Plugins/SqlEnterpriseFormatter/SqlEnterpriseFormatter_pt_BR.ts index b89804a..a408ffc 100644 --- a/Plugins/SqlEnterpriseFormatter/SqlEnterpriseFormatter_pt_BR.ts +++ b/Plugins/SqlEnterpriseFormatter/SqlEnterpriseFormatter_pt_BR.ts @@ -4,8 +4,8 @@ QObject - - + + name example name wrapper @@ -200,7 +200,32 @@ - + + Comments + + + + + Preferred comment marker (where possible): + + + + + SqlEnterpriseFormatter.CommentMarkers + + + + + Move all comments to the line end + + + + + Line up comments at the line end + + + + Preview diff --git a/Plugins/SqlEnterpriseFormatter/SqlEnterpriseFormatter_ru.ts b/Plugins/SqlEnterpriseFormatter/SqlEnterpriseFormatter_ru.ts index a5656d8..4e5e98d 100644 --- a/Plugins/SqlEnterpriseFormatter/SqlEnterpriseFormatter_ru.ts +++ b/Plugins/SqlEnterpriseFormatter/SqlEnterpriseFormatter_ru.ts @@ -1,11 +1,11 @@ - + QObject - - + + name example name wrapper имя @@ -200,7 +200,32 @@ Приводить ключевые слова к верхнему регистру - + + Comments + Комментарии + + + + Preferred comment marker (where possible): + Предпочитаемый символ комментирования (где применимо): + + + + SqlEnterpriseFormatter.CommentMarkers + SqlEnterpriseFormatter.CommentMarkers + + + + Move all comments to the line end + Перемещать все комментарии в конец строки + + + + Line up comments at the line end + Выравнивать комментарии в конце строки + + + Preview Предпросмотр diff --git a/Plugins/SqlEnterpriseFormatter/SqlEnterpriseFormatter_sk.ts b/Plugins/SqlEnterpriseFormatter/SqlEnterpriseFormatter_sk.ts index 407cc6e..37a42a4 100644 --- a/Plugins/SqlEnterpriseFormatter/SqlEnterpriseFormatter_sk.ts +++ b/Plugins/SqlEnterpriseFormatter/SqlEnterpriseFormatter_sk.ts @@ -4,8 +4,8 @@ QObject - - + + name example name wrapper @@ -200,7 +200,32 @@ - + + Comments + + + + + Preferred comment marker (where possible): + + + + + SqlEnterpriseFormatter.CommentMarkers + + + + + Move all comments to the line end + + + + + Line up comments at the line end + + + + Preview diff --git a/Plugins/SqlEnterpriseFormatter/SqlEnterpriseFormatter_zh_CN.ts b/Plugins/SqlEnterpriseFormatter/SqlEnterpriseFormatter_zh_CN.ts index 3c80603..d1a361b 100644 --- a/Plugins/SqlEnterpriseFormatter/SqlEnterpriseFormatter_zh_CN.ts +++ b/Plugins/SqlEnterpriseFormatter/SqlEnterpriseFormatter_zh_CN.ts @@ -4,8 +4,8 @@ QObject - - + + name example name wrapper @@ -200,7 +200,32 @@ - + + Comments + + + + + Preferred comment marker (where possible): + + + + + SqlEnterpriseFormatter.CommentMarkers + + + + + Move all comments to the line end + + + + + Line up comments at the line end + + + + Preview diff --git a/Plugins/SqlEnterpriseFormatter/formatempty.cpp b/Plugins/SqlEnterpriseFormatter/formatempty.cpp index 976694e..d0696e0 100644 --- a/Plugins/SqlEnterpriseFormatter/formatempty.cpp +++ b/Plugins/SqlEnterpriseFormatter/formatempty.cpp @@ -1,8 +1,9 @@ #include "formatempty.h" +#include "common/unused.h" -FormatEmpty::FormatEmpty(SqliteEmptyQuery* eq) : - eq(eq) +FormatEmpty::FormatEmpty(SqliteEmptyQuery* eq) { + UNUSED(eq); } void FormatEmpty::formatInternal() diff --git a/Plugins/SqlEnterpriseFormatter/formatempty.h b/Plugins/SqlEnterpriseFormatter/formatempty.h index 3279925..2fa3d01 100644 --- a/Plugins/SqlEnterpriseFormatter/formatempty.h +++ b/Plugins/SqlEnterpriseFormatter/formatempty.h @@ -12,9 +12,6 @@ class FormatEmpty : public FormatStatement protected: void formatInternal(); - - private: - SqliteEmptyQuery* eq = nullptr; }; #endif // FORMATEMPTY_H diff --git a/Plugins/SqlEnterpriseFormatter/sqlenterpriseformatter.cpp b/Plugins/SqlEnterpriseFormatter/sqlenterpriseformatter.cpp index 8f75960..a08f95c 100644 --- a/Plugins/SqlEnterpriseFormatter/sqlenterpriseformatter.cpp +++ b/Plugins/SqlEnterpriseFormatter/sqlenterpriseformatter.cpp @@ -3,6 +3,7 @@ #include "common/unused.h" #include "common/global.h" #include +#include #include SqlEnterpriseFormatter::SqlEnterpriseFormatter() @@ -11,6 +12,8 @@ SqlEnterpriseFormatter::SqlEnterpriseFormatter() QString SqlEnterpriseFormatter::format(SqliteQueryPtr query) { + QList comments = collectComments(query->tokens); + int wrapperIdx = cfg.SqlEnterpriseFormatter.Wrappers.get().indexOf(cfg.SqlEnterpriseFormatter.PrefferedWrapper.get()); NameWrapper wrapper = getAllNameWrappers()[wrapperIdx]; @@ -24,7 +27,11 @@ QString SqlEnterpriseFormatter::format(SqliteQueryPtr query) QString formatted = formatStmt->format(); delete formatStmt; - return formatted; + QString formattedWithComments = applyComments(formatted, comments, query->dialect); + for (Comment* c : comments) + delete c; + + return formattedWithComments; } bool SqlEnterpriseFormatter::init() @@ -34,7 +41,9 @@ bool SqlEnterpriseFormatter::init() static_qstring(query1, "SELECT (2 + 4) AND (3 + 5), 4 NOT IN (SELECT t1.'some[_]name' + t2.[some'name2] FROM xyz t1 JOIN zxc t2 ON (t1.aaa = t2.aaa)) " "FROM a, (SELECT id FROM table2);"); static_qstring(query2, "INSERT INTO table1 (id, value1, value2) VALUES (1, (2 + 5), (SELECT id FROM table2));"); - static_qstring(query3, "CREATE TABLE tab (id INTEGER PRIMARY KEY, value1 VARCHAR(6), value2 NUMBER(8,2) NOT NULL DEFAULT 1.0);"); + static_qstring(query3, "CREATE TABLE tab (id INTEGER PRIMARY KEY, /*a primary key column*/ value1 VARCHAR(6), " + "value2 /*column with constraints*/ NUMBER(8,2) NOT NULL DEFAULT 1.0" + ");"); static_qstring(query4, "CREATE UNIQUE INDEX IF NOT EXISTS dbName.idx1 ON [messages column] (id COLLATE x ASC, lang DESC, description);"); Parser parser(Dialect::Sqlite3); @@ -110,3 +119,196 @@ void SqlEnterpriseFormatter::configDialogClosed() { disconnect(&cfg.SqlEnterpriseFormatter, SIGNAL(changed(CfgEntry*)), this, SLOT(configModified(CfgEntry*))); } + +QList SqlEnterpriseFormatter::collectComments(const TokenList &tokens) +{ + QList results; + + QList tokensInLines = tokensByLines(tokens); + Comment* prevCommentInThisLine = nullptr; + Comment* cmt = nullptr; + bool tokensBefore = false; + int pos = 0; + int line = 0; + for (const TokenList& tokensInLine : tokensInLines) + { + tokensBefore = true; + prevCommentInThisLine = nullptr; + for (const TokenPtr& token : tokensInLine) + { + if (token->type == Token::Type::SPACE) + continue; + + if (prevCommentInThisLine) + prevCommentInThisLine->tokensAfter = true; + + if (token->type == Token::Type::COMMENT) + { + cmt = new Comment; + cmt->tokensBefore = tokensBefore; + cmt->position = pos; + cmt->multiline = token->value.startsWith("/*"); + if (cmt->multiline) + cmt->contents = token->value.mid(2, token->value.length() - 4).trimmed(); + else + cmt->contents = token->value.mid(2).trimmed(); + + results << cmt; + prevCommentInThisLine = cmt; + continue; + } + + tokensBefore = true; + pos++; + } + line++; + } + + return results; +} + +QList SqlEnterpriseFormatter::tokensByLines(const TokenList &tokens, bool includeSpaces) +{ + QList tokensInLines; + TokenList tokensInLine; + for (const TokenPtr& token : tokens) + { + if (includeSpaces || token->type != Token::Type::SPACE) + tokensInLine << token; + + if (token->type == Token::Type::SPACE && token->value.contains('\n')) + { + tokensInLines << tokensInLine; + tokensInLine.clear(); + } + } + if (tokensInLine.size() > 0) + tokensInLines << tokensInLine; + + return tokensInLines; +} + +TokenList SqlEnterpriseFormatter::adjustCommentsToEnd(const TokenList &inputTokens) +{ + QList tokensInLines = tokensByLines(inputTokens, true); + TokenList newTokens; + TokenList commentTokensForLine; + TokenPtr newLineToken; + for (const TokenList& tokensInLine : tokensInLines) + { + commentTokensForLine.clear(); + newLineToken.clear(); + for (const TokenPtr& token : tokensInLine) + { + if (token->type == Token::Type::COMMENT) + { + wrapComment(token, true); + //token->value = " " + endLineCommentTpl.arg(token->value); + commentTokensForLine << token; + } + else if (token->type == Token::Type::SPACE && token->value.contains("\n")) + newLineToken = token; + else + newTokens << token; + } + + newTokens += commentTokensForLine; + if (newLineToken) + newTokens << newLineToken; + } + return newTokens; +} + +TokenList SqlEnterpriseFormatter::wrapOnlyComments(const TokenList &inputTokens) +{ + QList tokensInLines = tokensByLines(inputTokens, true); + TokenList newTokens; + bool lineEnd = true; + for (const TokenList& tokensInLine : reverse(tokensInLines)) + { + lineEnd = true; + for (const TokenPtr& token : reverse(tokensInLine)) + { + if (!token->isWhitespace()) + lineEnd = false; + + if (token->type == Token::Type::COMMENT) + wrapComment(token, lineEnd); + + newTokens << token; + } + } + return reverse(newTokens); +} + +TokenList SqlEnterpriseFormatter::optimizeInnerComments(const TokenList &inputTokens) +{ + // TODO + return inputTokens; +} + +TokenList SqlEnterpriseFormatter::optimizeEndLineComments(const TokenList &inputTokens) +{ + // TODO + return inputTokens; +} + +void SqlEnterpriseFormatter::indentMultiLineComments(const TokenList &inputTokens) +{ + // TODO +} + +void SqlEnterpriseFormatter::wrapComment(const TokenPtr &token, bool isAtLineEnd) +{ + static_qstring(multiCommentTpl, "/* %1 */"); + static_qstring(endLineCommentTpl, "-- %1"); + + bool isMultiLine = token->value.contains("\n"); + if (isAtLineEnd && !isMultiLine && cfg.SqlEnterpriseFormatter.PreferredCommentMarker.get() == "--") + token->value = endLineCommentTpl.arg(token->value); + else + token->value = multiCommentTpl.arg(token->value); +} + +QString SqlEnterpriseFormatter::applyComments(const QString& formatted, QList comments, Dialect dialect) +{ + if (comments.size() == 0) + return formatted; + + int currentCommentPosition = comments.first()->position; + + TokenList allTokens = Lexer::tokenize(formatted, dialect); + TokenList newTokens; + int currentTokenPosition = 0; + for (const TokenPtr& token : allTokens) + { + if (currentTokenPosition == currentCommentPosition) + { + newTokens << TokenPtr::create(Token::Type::COMMENT, comments.first()->contents); + comments.removeFirst(); + if (comments.size() > 0) + currentCommentPosition = comments.first()->position; + else + currentCommentPosition = -1; + } + + newTokens << token; + if (token->type != Token::Type::SPACE) + currentTokenPosition++; + } + + // Any remaining comments + for (Comment* cmt : comments) + newTokens << TokenPtr::create(Token::Type::COMMENT, cmt->contents); + + if (cfg.SqlEnterpriseFormatter.MoveAllCommentsToLineEnd.get()) + newTokens = adjustCommentsToEnd(newTokens); + else + newTokens = wrapOnlyComments(newTokens); + + newTokens = optimizeInnerComments(newTokens); + newTokens = optimizeEndLineComments(newTokens); + indentMultiLineComments(newTokens); + + return newTokens.detokenize(); +} diff --git a/Plugins/SqlEnterpriseFormatter/sqlenterpriseformatter.h b/Plugins/SqlEnterpriseFormatter/sqlenterpriseformatter.h index 2701745..1f9b6d8 100644 --- a/Plugins/SqlEnterpriseFormatter/sqlenterpriseformatter.h +++ b/Plugins/SqlEnterpriseFormatter/sqlenterpriseformatter.h @@ -53,6 +53,10 @@ CFG_CATEGORIES(SqlEnterpriseFormatterConfig, CFG_ENTRY(QString, PrefferedWrapper, getNameWrapperStr(NameWrapper::BRACKET)) CFG_ENTRY(QStringList, Wrappers, getNameWrapperStrings(), false) CFG_ENTRY(QString, PreviewCode, QString(), false) + CFG_ENTRY(bool, MoveAllCommentsToLineEnd, false) + CFG_ENTRY(bool, LineUpCommentsAtLineEnd, true) + CFG_ENTRY(QString, PreferredCommentMarker, "--") + CFG_ENTRY(QStringList, CommentMarkers, QStringList({"--", "/* */"})) ) ) @@ -73,6 +77,25 @@ class SQLENTERPRISEFORMATTERSHARED_EXPORT SqlEnterpriseFormatter : public Generi void configDialogClosed(); private: + struct Comment + { + int position = 0; + QString contents; + bool tokensBefore = false; + bool tokensAfter = false; + bool multiline = false; + }; + + QList collectComments(const TokenList& tokens); + QString applyComments(const QString& formatted, QList comments, Dialect dialect); + QList tokensByLines(const TokenList& tokens, bool includeSpaces = false); + TokenList adjustCommentsToEnd(const TokenList& inputTokens); + TokenList wrapOnlyComments(const TokenList& inputTokens); + TokenList optimizeInnerComments(const TokenList& inputTokens); + TokenList optimizeEndLineComments(const TokenList& inputTokens); + void indentMultiLineComments(const TokenList& inputTokens); + void wrapComment(const TokenPtr& token, bool isAtLineEnd); + QList previewQueries; CFG_LOCAL_PERSISTABLE(SqlEnterpriseFormatterConfig, cfg) diff --git a/Plugins/SqlEnterpriseFormatter/sqlenterpriseformatter.ui b/Plugins/SqlEnterpriseFormatter/sqlenterpriseformatter.ui index 2ebfbdf..b21de64 100644 --- a/Plugins/SqlEnterpriseFormatter/sqlenterpriseformatter.ui +++ b/Plugins/SqlEnterpriseFormatter/sqlenterpriseformatter.ui @@ -39,7 +39,7 @@ - 1 + 0 @@ -133,9 +133,9 @@ 0 - -166 - 578 - 350 + 0 + 390 + 299 @@ -366,8 +366,8 @@ 0 0 - 424 - 325 + 418 + 278 @@ -626,6 +626,63 @@ + + + Comments + + + + + + Preferred comment marker (where possible): + + + + + + + SqlEnterpriseFormatter.PreferredCommentMarker + + + SqlEnterpriseFormatter.CommentMarkers + + + + + + + Move all comments to the line end + + + SqlEnterpriseFormatter.MoveAllCommentsToLineEnd + + + + + + + Line up comments at the line end + + + SqlEnterpriseFormatter.LineUpCommentsAtLineEnd + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + diff --git a/Plugins/SqlExport/SqlExport.pro b/Plugins/SqlExport/SqlExport.pro index 30e4632..eb18a7c 100644 --- a/Plugins/SqlExport/SqlExport.pro +++ b/Plugins/SqlExport/SqlExport.pro @@ -29,7 +29,8 @@ RESOURCES += \ sqlexport.qrc -TRANSLATIONS += SqlExport_zh_CN.ts \ +TRANSLATIONS += SqlExport_it.ts \ + SqlExport_zh_CN.ts \ SqlExport_sk.ts \ SqlExport_de.ts \ SqlExport_ru.ts \ @@ -49,3 +50,4 @@ TRANSLATIONS += SqlExport_zh_CN.ts \ + diff --git a/Plugins/SqlExport/SqlExport_it.ts b/Plugins/SqlExport/SqlExport_it.ts new file mode 100644 index 0000000..7f5368b --- /dev/null +++ b/Plugins/SqlExport/SqlExport_it.ts @@ -0,0 +1,98 @@ + + + + + SqlExport + + + -- Results of query: + + + + + -- Table: %1 + + + + + -- Index: %1 + + + + + -- Trigger: %1 + + + + + -- View: %1 + + + + + -- File generated with SQLiteStudio v%1 on %2 + + + + + -- Text encoding used: %1 + + + + + Table name for INSERT statements is mandatory. + + + + + sqlExportCommonConfig + + + Generate "DROP IF EXISTS" statement before "CREATE" statement + + + + + Format DDL statements only (excludes "INSERT" statements) + + + + + Use SQL formatter to format exported SQL statements + + + + + sqlExportQueryConfig + + + Use SQL formatter to format exported SQL statements + + + + + Table name to use for INSERT statements: + + + + + Generate "CREATE TABLE" statement at the begining + + + + + Include the query in comments + + + + + Generate "DROP IF EXISTS" statement before "CREATE" statement + + + + + Format DDL statements only (excludes "INSERT" statements) + + + + diff --git a/Plugins/SqlExport/SqlExport_zh_CN.ts b/Plugins/SqlExport/SqlExport_zh_CN.ts index d29cae8..c6c72de 100644 --- a/Plugins/SqlExport/SqlExport_zh_CN.ts +++ b/Plugins/SqlExport/SqlExport_zh_CN.ts @@ -6,37 +6,37 @@ -- Results of query: - + -- 执行结果: -- Table: %1 - + -- 表:%1 -- Index: %1 - + -- 索引:%1 -- Trigger: %1 - + -- 触发器:%1 -- View: %1 - + -- 视图:%1 -- File generated with SQLiteStudio v%1 on %2 - + -- 由SQLiteStudio v%1 产生的文件 %2 -- Text encoding used: %1 - + -- 文本编码:%1 diff --git a/Plugins/SqlFormatterSimple/SqlFormatterSimple.pro b/Plugins/SqlFormatterSimple/SqlFormatterSimple.pro index 61ec27a..7329f93 100644 --- a/Plugins/SqlFormatterSimple/SqlFormatterSimple.pro +++ b/Plugins/SqlFormatterSimple/SqlFormatterSimple.pro @@ -28,7 +28,8 @@ RESOURCES += \ sqlformattersimple.qrc -TRANSLATIONS += SqlFormatterSimple_zh_CN.ts \ +TRANSLATIONS += SqlFormatterSimple_it.ts \ + SqlFormatterSimple_zh_CN.ts \ SqlFormatterSimple_sk.ts \ SqlFormatterSimple_de.ts \ SqlFormatterSimple_ru.ts \ @@ -48,3 +49,4 @@ TRANSLATIONS += SqlFormatterSimple_zh_CN.ts \ + diff --git a/Plugins/SqlFormatterSimple/SqlFormatterSimple_it.ts b/Plugins/SqlFormatterSimple/SqlFormatterSimple_it.ts new file mode 100644 index 0000000..91208f1 --- /dev/null +++ b/Plugins/SqlFormatterSimple/SqlFormatterSimple_it.ts @@ -0,0 +1,17 @@ + + + + + SqlFormatterSimplePlugin + + + Upper case keywords + + + + + Reduce multiple whitespaces to single whitespace + + + + diff --git a/Plugins/SqlFormatterSimple/SqlFormatterSimple_zh_CN.ts b/Plugins/SqlFormatterSimple/SqlFormatterSimple_zh_CN.ts index aaa7f79..40c7f1e 100644 --- a/Plugins/SqlFormatterSimple/SqlFormatterSimple_zh_CN.ts +++ b/Plugins/SqlFormatterSimple/SqlFormatterSimple_zh_CN.ts @@ -6,12 +6,12 @@ Upper case keywords - + 大写关键字 Reduce multiple whitespaces to single whitespace - + 将多个空白转换为一个空白 diff --git a/Plugins/XmlExport/XmlExport.pro b/Plugins/XmlExport/XmlExport.pro index e22c320..bf34076 100644 --- a/Plugins/XmlExport/XmlExport.pro +++ b/Plugins/XmlExport/XmlExport.pro @@ -27,7 +27,8 @@ RESOURCES += \ xmlexport.qrc -TRANSLATIONS += XmlExport_zh_CN.ts \ +TRANSLATIONS += XmlExport_it.ts \ + XmlExport_zh_CN.ts \ XmlExport_sk.ts \ XmlExport_de.ts \ XmlExport_ru.ts \ @@ -47,3 +48,4 @@ TRANSLATIONS += XmlExport_zh_CN.ts \ + diff --git a/Plugins/XmlExport/XmlExport_it.ts b/Plugins/XmlExport/XmlExport_it.ts new file mode 100644 index 0000000..58265f6 --- /dev/null +++ b/Plugins/XmlExport/XmlExport_it.ts @@ -0,0 +1,70 @@ + + + + + XmlExport + + + Enter the namespace to use (for example: http://my.namespace.org) + + + + + XmlExportConfig + + + Output format + + + + + Format document (new lines, indentation) + + + + + Compress (everything in one line) + + + + + Special characters escaping + + + + + <p>Ampersands will be used for shorter values and CDATA will be used for larger values. This applies only to values that require character escaping. Other values will be exported as they are.</p> + + + + + Use CDATA and ampersands + + + + + <p>Every value requiring character escepe will be enclosed in CDATA block.</p> + + + + + Always use CDATA + + + + + <p>Every character that require esceping will be replaced with its ampersand escape sequence. No CDATA blocks will be used.</p> + + + + + Always use ampersand + + + + + Define XML namespace + + + + -- cgit v1.2.3