262 lines
10 KiB
C++
262 lines
10 KiB
C++
/** @file
|
|
*
|
|
* Wireshark - Network traffic analyzer
|
|
* By Gerald Combs <gerald@wireshark.org>
|
|
* Copyright 1998 Gerald Combs
|
|
*
|
|
* SPDX-License-Identifier: GPL-2.0-or-later
|
|
*/
|
|
|
|
#include <config.h>
|
|
#define WS_LOG_DOMAIN LOG_DOMAIN_QTUI
|
|
|
|
#include <ui/qt/widgets/resolved_addresses_view.h>
|
|
#include <ui/qt/models/resolved_addresses_models.h>
|
|
#include <ui/qt/widgets/wireshark_file_dialog.h>
|
|
|
|
#include <QHeaderView>
|
|
#include <QMessageBox>
|
|
#include <QClipboard>
|
|
#include <QTextStream>
|
|
#include <QJsonArray>
|
|
#include <QJsonObject>
|
|
#include <QJsonDocument>
|
|
#include <QContextMenuEvent>
|
|
|
|
#include "main_application.h"
|
|
|
|
#include <wsutil/wslog.h>
|
|
|
|
ResolvedAddressesView::ResolvedAddressesView(QWidget *parent) : QTableView(parent)
|
|
{
|
|
setEditTriggers(QAbstractItemView::NoEditTriggers);
|
|
setSortingEnabled(true);
|
|
setSelectionBehavior(QAbstractItemView::SelectRows);
|
|
horizontalHeader()->setStretchLastSection(true);
|
|
verticalHeader()->setVisible(false);
|
|
|
|
// creating this action is mostly to override the default Ctrl-C handling
|
|
// (which could also be done by overriding KeyPressEvent) and to make the
|
|
// keyboard shortcut show up in the context menu.
|
|
#if QT_VERSION < QT_VERSION_CHECK(6, 3, 0)
|
|
clip_action_ = new QAction(tr("as Plain Text"), this);
|
|
clip_action_->setShortcut(QKeySequence(QKeySequence::Copy));
|
|
connect(clip_action_, &QAction::triggered, this, &ResolvedAddressesView::clipboardAction);
|
|
addAction(clip_action_);
|
|
#else
|
|
clip_action_ = addAction(tr("as Plain Text"), QKeySequence(QKeySequence::Copy), this, &ResolvedAddressesView::clipboardAction);
|
|
#endif
|
|
clip_action_->setProperty("copy_as", ResolvedAddressesView::EXPORT_TEXT);
|
|
clip_action_->setProperty("selected", true);
|
|
}
|
|
|
|
QMenu* ResolvedAddressesView::createCopyMenu(bool selected, QWidget *parent)
|
|
{
|
|
QMenu *copy_menu;
|
|
if (selected) {
|
|
copy_menu = new QMenu(tr("Copy selected rows"), parent);
|
|
} else {
|
|
copy_menu = new QMenu(tr("Copy table"), parent);
|
|
}
|
|
copy_menu->setIcon(QIcon::fromTheme(QStringLiteral("edit-copy")));
|
|
QAction *ca;
|
|
if (selected) {
|
|
copy_menu->addAction(clip_action_);
|
|
} else {
|
|
ca = copy_menu->addAction(tr("as Plain Text"), this, &ResolvedAddressesView::clipboardAction);
|
|
ca->setProperty("copy_as", ResolvedAddressesView::EXPORT_TEXT);
|
|
ca->setProperty("selected", selected);
|
|
}
|
|
ca = copy_menu->addAction(tr("as CSV"), this, &ResolvedAddressesView::clipboardAction);
|
|
ca->setProperty("copy_as", ResolvedAddressesView::EXPORT_CSV);
|
|
ca->setProperty("selected", selected);
|
|
ca = copy_menu->addAction(tr("as JSON"), this, &ResolvedAddressesView::clipboardAction);
|
|
ca->setProperty("copy_as", ResolvedAddressesView::EXPORT_JSON);
|
|
ca->setProperty("selected", selected);
|
|
|
|
return copy_menu;
|
|
}
|
|
|
|
void ResolvedAddressesView::contextMenuEvent(QContextMenuEvent *e)
|
|
{
|
|
if (!e)
|
|
return;
|
|
|
|
QMenu *ctxMenu = new QMenu(this);
|
|
ctxMenu->setAttribute(Qt::WA_DeleteOnClose);
|
|
ctxMenu->addMenu(createCopyMenu(true, ctxMenu));
|
|
QAction *act = ctxMenu->addAction(tr("Save selected rows as…"));
|
|
act->setIcon(QIcon::fromTheme(QStringLiteral("document-save-as")));
|
|
act->setProperty("selected", true);
|
|
connect(act, &QAction::triggered, this, &ResolvedAddressesView::saveAs);
|
|
ctxMenu->addSeparator();
|
|
ctxMenu->addMenu(createCopyMenu(false, ctxMenu));
|
|
act = ctxMenu->addAction(QIcon::fromTheme(QStringLiteral("document-save-as")), tr("Save table as…"), this, &ResolvedAddressesView::saveAs);
|
|
act->setProperty("selected", false);
|
|
|
|
ctxMenu->popup(e->globalPos());
|
|
}
|
|
|
|
AStringListListModel* ResolvedAddressesView::dataModel() const
|
|
{
|
|
QSortFilterProxyModel *proxy = qobject_cast<QSortFilterProxyModel *>(model());
|
|
|
|
if (proxy) {
|
|
QAbstractItemModel *source = proxy->sourceModel();
|
|
while (qobject_cast<QSortFilterProxyModel *>(source) != nullptr) {
|
|
proxy = qobject_cast<QSortFilterProxyModel *>(source);
|
|
source = proxy->sourceModel();
|
|
}
|
|
return qobject_cast<AStringListListModel *>(source);
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
void ResolvedAddressesView::clipboardAction()
|
|
{
|
|
QAction *ca = qobject_cast<QAction *>(sender());
|
|
if (ca && ca->property("copy_as").isValid()) {
|
|
copyToClipboard(static_cast<eResolvedAddressesExport>(ca->property("copy_as").toInt()),
|
|
ca->property("selected").toBool());
|
|
}
|
|
}
|
|
|
|
void ResolvedAddressesView::copyToClipboard(eResolvedAddressesExport format, bool selected)
|
|
{
|
|
QString clipText;
|
|
QTextStream stream(&clipText, QIODevice::Text);
|
|
toTextStream(stream, format, selected);
|
|
mainApp->clipboard()->setText(stream.readAll());
|
|
}
|
|
|
|
void ResolvedAddressesView::saveAs()
|
|
{
|
|
bool selected = false;
|
|
QAction *ca = qobject_cast<QAction *>(sender());
|
|
if (ca && ca->property("selected").isValid()) {
|
|
selected = true;
|
|
}
|
|
QString caption(mainApp->windowTitleString(tr("Save Resolved Addresses As…")));
|
|
QString txtFilter = tr("Plain text (*.txt)");
|
|
QString csvFilter = tr("CSV Document (*.csv)");
|
|
QString jsonFilter = tr("JSON Document (*.json)");
|
|
QString selectedFilter;
|
|
QString fileName = WiresharkFileDialog::getSaveFileName(this, caption,
|
|
mainApp->openDialogInitialDir().canonicalPath(),
|
|
QString("%1;;%2;;%3").arg(txtFilter).arg(csvFilter).arg(jsonFilter),
|
|
&selectedFilter);
|
|
if (fileName.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
eResolvedAddressesExport format(EXPORT_TEXT);
|
|
if (selectedFilter.compare(csvFilter) == 0) {
|
|
format = EXPORT_CSV;
|
|
} else if (selectedFilter.compare(jsonFilter) == 0) {
|
|
format = EXPORT_JSON;
|
|
}
|
|
|
|
// macOS and Windows use the native file dialog, which enforces file
|
|
// extensions. UN*X dialogs generally don't. That's ok here, at
|
|
// least for the text format, because hosts and ethers and services
|
|
// files don't have an extension.
|
|
QFile saveFile(fileName);
|
|
if (saveFile.open(QFile::WriteOnly | QFile::Text)) {
|
|
QTextStream stream(&saveFile);
|
|
toTextStream(stream, format, selected);
|
|
saveFile.close();
|
|
} else {
|
|
QMessageBox::warning(this, tr("Warning").arg(saveFile.fileName()),
|
|
tr("Unable to save %1: %2").arg(saveFile.fileName().arg(saveFile.errorString())));
|
|
}
|
|
}
|
|
|
|
|
|
void ResolvedAddressesView::toTextStream(QTextStream& stream,
|
|
eResolvedAddressesExport format, bool selected) const
|
|
{
|
|
if (model() == nullptr) {
|
|
return;
|
|
}
|
|
|
|
// XXX: TrafficTree and TapParameterDialog have similar "export a
|
|
// "QAbstractItemModel to a QTextStream in TEXT, CSV or JSON"
|
|
// functions that could be made into common code.
|
|
QStringList rowText;
|
|
if (format == EXPORT_TEXT) {
|
|
if (qobject_cast<PortsModel*>(dataModel()) != nullptr) {
|
|
// Format of services(5)
|
|
if (!selected) {
|
|
stream << "# service-name\tport/protocol\n";
|
|
}
|
|
for (int row = 0; row < model()->rowCount(); row++) {
|
|
if (selected && !selectionModel()->isRowSelected(row, QModelIndex())) continue;
|
|
rowText.clear();
|
|
rowText << model()->data(model()->index(row, PORTS_COL_NAME)).toString();
|
|
rowText << QString("%1/%2")
|
|
.arg(model()->data(model()->index(row, PORTS_COL_PORT)).toString())
|
|
.arg(model()->data(model()->index(row, PORTS_COL_PROTOCOL)).toString());
|
|
stream << rowText.join("\t") << "\n";
|
|
}
|
|
} else {
|
|
// Format as hosts(5) and ethers(5)
|
|
if (!selected) {
|
|
for (int col = 0; col < model()->columnCount(); col++) {
|
|
rowText << model()->headerData(col, Qt::Horizontal).toString();
|
|
}
|
|
stream << "# " << rowText.join("\t") << "\n";
|
|
}
|
|
for (int row = 0; row < model()->rowCount(); row++) {
|
|
if (selected && !selectionModel()->isRowSelected(row, QModelIndex())) continue;
|
|
rowText.clear();
|
|
for (int col = 0; col < model()->columnCount(); col++) {
|
|
rowText << model()->data(model()->index(row, col)).toString();
|
|
}
|
|
stream << rowText.join("\t") << "\n";
|
|
}
|
|
}
|
|
} else if (format == EXPORT_CSV) {
|
|
for (int col = 0; col < model()->columnCount(); col++) {
|
|
rowText << model()->headerData(col, Qt::Horizontal).toString();
|
|
}
|
|
if (!selected) {
|
|
stream << rowText.join(",") << "\n";
|
|
}
|
|
for (int row = 0; row < model()->rowCount(); row++) {
|
|
if (selected && !selectionModel()->isRowSelected(row, QModelIndex())) continue;
|
|
rowText.clear();
|
|
for (int col = 0; col < model()->columnCount(); col++) {
|
|
QVariant v = model()->data(model()->index(row, col));
|
|
if (!v.isValid()) {
|
|
rowText << QStringLiteral("\"\"");
|
|
} else if (v.userType() == QMetaType::QString) {
|
|
rowText << QString("\"%1\"").arg(v.toString().replace('\"', "\"\""));
|
|
} else {
|
|
rowText << v.toString();
|
|
}
|
|
}
|
|
stream << rowText.join(",") << "\n";
|
|
}
|
|
} else if (format == EXPORT_JSON) {
|
|
QMap<int, QString> headers;
|
|
for (int col = 0; col < model()->columnCount(); col++)
|
|
headers.insert(col, model()->headerData(col, Qt::Horizontal, Qt::DisplayRole).toString());
|
|
|
|
QJsonArray records;
|
|
|
|
for (int row = 0; row < model()->rowCount(); row++) {
|
|
if (selected && !selectionModel()->isRowSelected(row, QModelIndex())) continue;
|
|
QJsonObject rowData;
|
|
foreach(int col, headers.keys()) {
|
|
QModelIndex idx = model()->index(row, col);
|
|
rowData.insert(headers[col], model()->data(idx).toString());
|
|
}
|
|
records.push_back(rowData);
|
|
}
|
|
|
|
QJsonDocument json;
|
|
json.setArray(records);
|
|
stream << json.toJson();
|
|
}
|
|
}
|