summaryrefslogtreecommitdiffstats
path: root/ui/qt/widgets/traffic_tree.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'ui/qt/widgets/traffic_tree.cpp')
-rw-r--r--ui/qt/widgets/traffic_tree.cpp879
1 files changed, 879 insertions, 0 deletions
diff --git a/ui/qt/widgets/traffic_tree.cpp b/ui/qt/widgets/traffic_tree.cpp
new file mode 100644
index 0000000..be154d7
--- /dev/null
+++ b/ui/qt/widgets/traffic_tree.cpp
@@ -0,0 +1,879 @@
+/** @file
+ *
+ * Wireshark - Network traffic analyzer
+ * By Gerald Combs <gerald@wireshark.org>
+ * Copyright 1998 Gerald Combs
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include <epan/proto.h>
+#include <epan/addr_resolv.h>
+#include <epan/prefs.h>
+#include <epan/maxmind_db.h>
+#include <epan/conversation_table.h>
+
+#include <wsutil/utf8_entities.h>
+#include <wsutil/filesystem.h>
+#include <wsutil/str_util.h>
+
+#include "ui/recent.h"
+
+#include <ui/qt/utils/qt_ui_utils.h>
+#include <ui/qt/main_application.h>
+#include <ui/qt/main_window.h>
+#include <ui/qt/filter_action.h>
+#include <ui/qt/models/atap_data_model.h>
+#include <ui/qt/utils/variant_pointer.h>
+#include <ui/qt/widgets/traffic_tab.h>
+#include <ui/qt/widgets/traffic_tree.h>
+
+#include <QStringList>
+#include <QTreeView>
+#include <QList>
+#include <QMap>
+#include <QMenu>
+#include <QSortFilterProxyModel>
+#include <QTextStream>
+#include <QClipboard>
+#include <QMessageBox>
+#include <QJsonArray>
+#include <QJsonObject>
+#include <QJsonDocument>
+#include <QHeaderView>
+#include <QWidgetAction>
+#include <QLineEdit>
+#include <QActionGroup>
+#include <QDateTime>
+#include <QTime>
+
+MenuEditAction::MenuEditAction(QString text, QString hintText, QObject * parent) :
+ QWidgetAction(parent),
+ _hintText(hintText),
+ _text(text),
+ _lineEdit(nullptr)
+{}
+
+QWidget * MenuEditAction::createWidget(QWidget *parent) {
+ _lineEdit = new QLineEdit(parent);
+ _lineEdit->setAlignment(Qt::AlignRight);
+ _lineEdit->setText(_text);
+ _lineEdit->setPlaceholderText(_hintText);
+ connect(_lineEdit, &QLineEdit::returnPressed, this, &MenuEditAction::triggerEntry);
+ return _lineEdit;
+}
+
+void MenuEditAction::triggerEntry() {
+ if (_lineEdit)
+ _text = _lineEdit->text();
+
+ emit trigger();
+}
+
+QString MenuEditAction::text() const {
+ return _text;
+}
+
+
+TrafficTreeHeaderView::TrafficTreeHeaderView(GList ** recentColumnList, QWidget * parent):
+ QHeaderView(Qt::Horizontal, parent)
+{
+ _recentColumnList = recentColumnList;
+
+ setContextMenuPolicy(Qt::CustomContextMenu);
+
+ _actions = new QActionGroup(this);
+
+ QAction * filterAction = _actions->addAction(tr("Less than"));
+ filterAction->setCheckable(true);
+ filterAction->setChecked(true);
+ filterAction->setProperty("filter_action", (int)TrafficDataFilterProxy::TRAFFIC_DATA_LESS);
+ filterAction = _actions->addAction(tr("Greater than"));
+ filterAction->setCheckable(true);
+ filterAction->setProperty("filter_action", (int)TrafficDataFilterProxy::TRAFFIC_DATA_GREATER);
+ filterAction = _actions->addAction(tr("Equal"));
+ filterAction->setCheckable(true);
+ filterAction->setProperty("filter_action", (int)TrafficDataFilterProxy::TRAFFIC_DATA_EQUAL);
+
+ connect(this, &QHeaderView::customContextMenuRequested, this, &TrafficTreeHeaderView::headerContextMenu);
+}
+
+TrafficTreeHeaderView::~TrafficTreeHeaderView()
+{}
+
+void TrafficTreeHeaderView::headerContextMenu(const QPoint &pos)
+{
+ TrafficTree * tree = qobject_cast<TrafficTree *>(parent());
+ if (!tree)
+ return;
+
+ TrafficDataFilterProxy * proxy = qobject_cast<TrafficDataFilterProxy *>(tree->model());
+ if (sender() != this || ! proxy)
+ return;
+
+ QMenu * ctxMenu = new QMenu(this);
+ ctxMenu->setAttribute(Qt::WA_DeleteOnClose);
+
+ QAction * headerAction = ctxMenu->addAction(tr("Columns to display"));
+ headerAction->setEnabled(false);
+
+ for (int col = 0; col < tree->dataModel()->columnCount(); col++)
+ {
+ QString name = tree->dataModel()->headerData(col).toString();
+ QAction * action = new QAction(name);
+ action->setCheckable(true);
+ action->setChecked(proxy->columnVisible(col));
+ action->setProperty("col_nr", col);
+ ctxMenu->addAction(action);
+
+ connect(action, &QAction::triggered, this, &TrafficTreeHeaderView::columnTriggered);
+ }
+
+ ctxMenu->addSeparator();
+
+ int column = logicalIndexAt(pos);
+
+ bool is_address = false;
+ QModelIndex sourceIdx = proxy->mapToSource(proxy->index(0, column));
+ if (qobject_cast<EndpointDataModel *>(proxy->sourceModel()) && sourceIdx.column() == EndpointDataModel::ENDP_COLUMN_ADDR) {
+ is_address = true;
+ } else if (qobject_cast<ConversationDataModel *>(proxy->sourceModel()) && (sourceIdx.column() == ConversationDataModel::CONV_COLUMN_SRC_ADDR ||
+ sourceIdx.column() == ConversationDataModel::CONV_COLUMN_DST_ADDR)) {
+ is_address = true;
+ }
+
+ if (! is_address) {
+ QString columnText = model()->headerData(column, Qt::Horizontal).toString();
+ QAction * filterAction = ctxMenu->addAction(tr("Filter %1 by").arg(columnText));
+ filterAction->setEnabled(false);
+ ctxMenu->addActions(_actions->actions());
+
+ MenuEditAction * editAction = new MenuEditAction(_filterText, tr("Enter filter value"));
+ editAction->setProperty("column", column);
+ ctxMenu->addAction(editAction);
+ connect(editAction, &MenuEditAction::triggered, this, &TrafficTreeHeaderView::filterColumn);
+ }
+
+ connect(ctxMenu, &QMenu::triggered, this, &TrafficTreeHeaderView::menuActionTriggered);
+
+ ctxMenu->popup(mapToGlobal(pos));
+}
+
+void TrafficTreeHeaderView::applyRecent()
+{
+ TrafficTree * tree = qobject_cast<TrafficTree *>(parent());
+ if (!tree)
+ return;
+
+ QList<int> columns;
+ for (GList * endTab = *_recentColumnList; endTab; endTab = endTab->next) {
+ QString colStr = QString((const char *)endTab->data);
+ bool ok = false;
+ int col = colStr.toInt(&ok);
+ if (ok)
+ columns << col;
+ }
+
+ if (columns.count() > 0) {
+ TrafficDataFilterProxy * proxy = qobject_cast<TrafficDataFilterProxy *>(tree->model());
+ for (int col = 0; col < tree->dataModel()->columnCount(); col++) {
+ proxy->setColumnVisibility(col, columns.contains(col));
+ }
+ }
+}
+
+void TrafficTreeHeaderView::columnTriggered(bool checked)
+{
+ TrafficTree * tree = qobject_cast<TrafficTree *>(parent());
+ if (!tree)
+ return;
+
+ TrafficDataFilterProxy * proxy = qobject_cast<TrafficDataFilterProxy *>(tree->model());
+ QAction * entry = qobject_cast<QAction *>(sender());
+ if (! proxy || ! entry || ! entry->property("col_nr").isValid())
+ return;
+
+ int col = entry->property("col_nr").toInt();
+ proxy->setColumnVisibility(col, checked);
+
+ prefs_clear_string_list(*_recentColumnList);
+ *_recentColumnList = NULL;
+
+ QList<int> visible;
+
+ for (int col = 0; col < tree->dataModel()->columnCount(); col++) {
+ if (proxy->columnVisible(col)) {
+ visible << col;
+ gchar *nr = qstring_strdup(QString::number(col));
+ *_recentColumnList = g_list_append(*_recentColumnList, nr);
+ }
+ }
+
+ emit columnsHaveChanged(visible);
+}
+
+void TrafficTreeHeaderView::menuActionTriggered(QAction * act)
+{
+ if (_actions && _actions->actions().contains(act)) {
+ QMenu * menu = qobject_cast<QMenu *>(sender());
+ if (menu) {
+ MenuEditAction * menuAction = nullptr;
+ foreach(QAction * _act, menu->actions()) {
+ if (qobject_cast<MenuEditAction *>(_act)) {
+ menuAction = qobject_cast<MenuEditAction *>(_act);
+ break;
+ }
+ }
+
+ int column = menuAction ? menuAction->property("column").toInt() : -1;
+ if (column >= 0) {
+ _filterText = menuAction->text().trimmed();
+ if (_filterText.length() == 0)
+ column = -1;
+ int filterOn = act->property("filter_action").toInt();
+
+ emit filterOnColumn(column, filterOn, _filterText);
+ }
+ }
+ }
+}
+
+void TrafficTreeHeaderView::filterColumn(bool)
+{
+ MenuEditAction * menuAction = qobject_cast<MenuEditAction *>(sender());
+ if (!menuAction)
+ return;
+
+ int filterOn = TrafficDataFilterProxy::TRAFFIC_DATA_LESS;
+ foreach(QAction * act, _actions->actions()) {
+ if (act->isChecked() && act->property("filter_action").isValid()) {
+ filterOn = act->property("filter_action").toInt();
+ break;
+ }
+ }
+
+ int column = menuAction->property("column").toInt();
+ _filterText = menuAction->text().trimmed();
+ if (_filterText.length() == 0)
+ column = -1;
+
+ emit filterOnColumn(column, filterOn, _filterText);
+}
+
+
+TrafficDataFilterProxy::TrafficDataFilterProxy(QObject *parent) :
+ QSortFilterProxyModel(parent),
+ _filterColumn(-1),
+ _filterOn(-1),
+ _filterText(QString())
+{
+ setSortRole(ATapDataModel::UNFORMATTED_DISPLAYDATA);
+}
+
+
+void TrafficDataFilterProxy::filterForColumn(int column, int filterOn, QString filterText)
+{
+ if (filterOn < 0 || filterOn > TrafficDataFilterProxy::TRAFFIC_DATA_EQUAL)
+ column = -1;
+
+ _filterColumn = mapToSourceColumn(column);
+ _filterOn = filterOn;
+ _filterText = filterText;
+ invalidateFilter();
+}
+
+int TrafficDataFilterProxy::mapToSourceColumn(int proxyColumn) const
+{
+ ATapDataModel * model = qobject_cast<ATapDataModel *>(sourceModel());
+ if (!model || proxyColumn == -1) {
+ return proxyColumn;
+ }
+
+ if (rowCount() > 0) {
+ return mapToSource(index(0, proxyColumn)).column();
+ }
+
+ /* mapToSource() requires a valid QModelIndex, and thus does not work when
+ * all rows are filtered out by the current filter. (E.g., the user has
+ * accidentally entered an incorrect filter or operator and wants to fix
+ * it.) Since our filterAcceptsColumn doesn't depend on the row, we can
+ * determine the mapping between the currently displayed column number and
+ * the column number in the model this way, even if no rows are displayed.
+ * It is linear time in the number of columns, though.
+ */
+ int currentProxyColumn = 0;
+ for (int column=0; column < model->columnCount(); ++column) {
+ if (filterAcceptsColumn(column, QModelIndex())) {
+ if (currentProxyColumn++ == proxyColumn) {
+ return column;
+ }
+ }
+ }
+
+ return -1;
+}
+
+bool TrafficDataFilterProxy::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
+{
+ ATapDataModel * dataModel = qobject_cast<ATapDataModel *>(sourceModel());
+ if (dataModel) {
+ bool isFiltered = dataModel->data(dataModel->index(source_row, 0), ATapDataModel::ROW_IS_FILTERED).toBool();
+ if (isFiltered && dataModel->filter().length() > 0)
+ return false;
+ /* XXX: What if the filter column is now hidden? Should the filter
+ * still apply or should it be cleared? Right now it is still applied.
+ */
+
+ QModelIndex srcIdx = dataModel->index(source_row, _filterColumn);
+ if (srcIdx.isValid()) {
+ QVariant data = srcIdx.data(ATapDataModel::UNFORMATTED_DISPLAYDATA);
+
+ bool filtered = false;
+ /* QVariant comparisons coerce to the first parameter type, so
+ * putting data first and converting the string to it is important.
+ */
+#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
+ /* QVariant::compare coerces strings to numeric types, but does
+ * not try to automatically convert them to datetime related types.
+ */
+ QVariant rhs = QVariant(_filterText);
+ if (data.userType() == QMetaType::QDateTime) {
+ /* Try to parse with a date included in the filter, and
+ * fallback to time only if that fails.
+ */
+ QDateTime filter_dt = QDateTime::fromString(_filterText, Qt::ISODateWithMs);
+ if (filter_dt.isValid()) {
+ rhs.setValue(filter_dt);
+ } else {
+ QTime filterTime = QTime::fromString(_filterText, Qt::ISODateWithMs);
+ if (filterTime.isValid()) {
+ rhs.setValue(filterTime);
+ data.setValue(data.toTime());
+ } else {
+ rhs = QVariant();
+ }
+ }
+ }
+ QPartialOrdering result = QVariant::compare(data, rhs);
+ if (_filterOn == TrafficDataFilterProxy::TRAFFIC_DATA_LESS)
+ filtered = result < 0;
+ else if (_filterOn == TrafficDataFilterProxy::TRAFFIC_DATA_GREATER)
+ filtered = result > 0;
+ else if (_filterOn == TrafficDataFilterProxy::TRAFFIC_DATA_EQUAL)
+ filtered = result == 0;
+#else
+ /* The comparisons are deprecated in 5.15. This is most of the
+ * implementation of QAbstractItemModelPrivate::isVariantLessThan
+ * from the Qt source.
+ */
+ if (_filterText.isEmpty())
+ filtered = true;
+ else if (data.isNull())
+ filtered = false;
+ else {
+ switch (data.userType()) {
+ case QMetaType::Int:
+ case QMetaType::UInt:
+ case QMetaType::LongLong:
+ if (_filterOn == TrafficDataFilterProxy::TRAFFIC_DATA_LESS)
+ filtered = data.toLongLong() < _filterText.toLongLong();
+ else if (_filterOn == TrafficDataFilterProxy::TRAFFIC_DATA_GREATER)
+ filtered = data.toLongLong() > _filterText.toLongLong();
+ else if (_filterOn == TrafficDataFilterProxy::TRAFFIC_DATA_EQUAL)
+ filtered = data.toLongLong() == _filterText.toLongLong();
+ break;
+ case QMetaType::Float:
+ case QMetaType::Double:
+ if (_filterOn == TrafficDataFilterProxy::TRAFFIC_DATA_LESS)
+ filtered = data.toDouble() < _filterText.toDouble();
+ else if (_filterOn == TrafficDataFilterProxy::TRAFFIC_DATA_GREATER)
+ filtered = data.toDouble() > _filterText.toDouble();
+ else if (_filterOn == TrafficDataFilterProxy::TRAFFIC_DATA_EQUAL)
+ filtered = data.toDouble() == _filterText.toDouble();
+ break;
+ case QMetaType::QDateTime:
+ {
+ /* Try to parse with a date included, and fall back to time
+ * only if that fails.
+ */
+ QDateTime filter_dt = QDateTime::fromString(_filterText, Qt::ISODateWithMs);
+ if (filter_dt.isValid()) {
+ if (_filterOn == TrafficDataFilterProxy::TRAFFIC_DATA_LESS)
+ filtered = data.toDateTime() < filter_dt;
+ else if (_filterOn == TrafficDataFilterProxy::TRAFFIC_DATA_GREATER)
+ filtered = data.toDateTime() > filter_dt;
+ else if (_filterOn == TrafficDataFilterProxy::TRAFFIC_DATA_EQUAL)
+ filtered = data.toDateTime() == filter_dt;
+ break;
+ }
+ }
+ /* FALLTHROUGH */
+ case QMetaType::QTime:
+ {
+ QTime filter_t = QTime::fromString(_filterText, Qt::ISODateWithMs);
+ if (_filterOn == TrafficDataFilterProxy::TRAFFIC_DATA_LESS)
+ filtered = data.toTime() < filter_t;
+ else if (_filterOn == TrafficDataFilterProxy::TRAFFIC_DATA_GREATER)
+ filtered = data.toTime() > filter_t;
+ else if (_filterOn == TrafficDataFilterProxy::TRAFFIC_DATA_EQUAL)
+ filtered = data.toTime() == filter_t;
+ break;
+ }
+ case QMetaType::QString:
+ default:
+ /* XXX: We don't do UTF-8 aware coallating in Packet List
+ * (because it's slow), but possibly could here.
+ */
+ if (_filterOn == TrafficDataFilterProxy::TRAFFIC_DATA_LESS)
+ filtered = data.toString() < _filterText;
+ else if (_filterOn == TrafficDataFilterProxy::TRAFFIC_DATA_GREATER)
+ filtered = data.toString() > _filterText;
+ else if (_filterOn == TrafficDataFilterProxy::TRAFFIC_DATA_EQUAL)
+ filtered = data.toString() == _filterText;
+ break;
+ }
+ }
+#endif
+
+ if (!filtered)
+ return false;
+ }
+ }
+
+ return QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent);
+}
+
+bool TrafficDataFilterProxy::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const
+{
+ if (! source_left.isValid() || ! qobject_cast<const ATapDataModel *>(source_left.model()))
+ return false;
+ if (! source_right.isValid() || ! qobject_cast<const ATapDataModel *>(source_right.model()))
+ return false;
+
+ ATapDataModel * model = qobject_cast<ATapDataModel *>(sourceModel());
+
+ if (! model || source_left.model() != model || source_right.model() != model)
+ return false;
+
+ QVariant datA = source_left.data(ATapDataModel::UNFORMATTED_DISPLAYDATA);
+ QVariant datB = source_right.data(ATapDataModel::UNFORMATTED_DISPLAYDATA);
+
+ bool is_address = false;
+ if (qobject_cast<EndpointDataModel *>(model) && source_left.column() == EndpointDataModel::ENDP_COLUMN_ADDR &&
+ source_left.column() == source_right.column()) {
+ is_address = true;
+ } else if (qobject_cast<ConversationDataModel *>(model) && (source_left.column() == ConversationDataModel::CONV_COLUMN_SRC_ADDR ||
+ source_left.column() == ConversationDataModel::CONV_COLUMN_DST_ADDR) && source_left.column() == source_right.column()) {
+ is_address = true;
+ }
+
+ if (is_address) {
+ bool result = false;
+ bool identical = false;
+ int addressTypeA = model->data(source_left, ATapDataModel::DATA_ADDRESS_TYPE).toInt();
+ int addressTypeB = model->data(source_right, ATapDataModel::DATA_ADDRESS_TYPE).toInt();
+ if (addressTypeA != 0 && addressTypeB != 0 && addressTypeA != addressTypeB) {
+ result = addressTypeA < addressTypeB;
+ } else if (addressTypeA != 0 && addressTypeA == addressTypeB) {
+
+ if (addressTypeA == AT_IPv4) {
+ quint32 valA = model->data(source_left, ATapDataModel::DATA_IPV4_INTEGER).value<quint32>();
+ quint32 valB = model->data(source_right, ATapDataModel::DATA_IPV4_INTEGER).value<quint32>();
+
+ result = valA < valB;
+ identical = valA == valB;
+ } else if (addressTypeA == AT_NUMERIC) {
+ quint32 valA = datA.toInt();
+ quint32 valB = datB.toInt();
+ result = valA < valB;
+ identical = valA == valB;
+ } else {
+ result = QString::compare(datA.toString(), datB.toString(), Qt::CaseInsensitive) < 0;
+ identical = QString::compare(datA.toString(), datB.toString(), Qt::CaseInsensitive) == 0;
+ }
+
+ int portColumn = EndpointDataModel::ENDP_COLUMN_PORT;
+ if (identical && qobject_cast<ConversationDataModel *>(model)) {
+ QModelIndex tstA, tstB;
+ if (source_left.column() == ConversationDataModel::CONV_COLUMN_SRC_ADDR) {
+ portColumn = ConversationDataModel::CONV_COLUMN_SRC_PORT;
+ int col = ConversationDataModel::CONV_COLUMN_DST_ADDR;
+ tstA = model->index(source_left.row(), col);
+ tstB = model->index(source_right.row(), col);
+ } else if (source_left.column() == ConversationDataModel::CONV_COLUMN_DST_ADDR) {
+ portColumn = ConversationDataModel::CONV_COLUMN_DST_PORT;
+ int col = ConversationDataModel::CONV_COLUMN_SRC_ADDR;
+ tstA = model->index(source_left.row(), col);
+ tstB = model->index(source_right.row(), col);
+ }
+
+ if (addressTypeA == AT_IPv4) {
+ quint32 valX = model->data(tstA, ATapDataModel::DATA_IPV4_INTEGER).value<quint32>();
+ quint32 valY = model->data(tstB, ATapDataModel::DATA_IPV4_INTEGER).value<quint32>();
+
+ result = valX < valY;
+ identical = valX == valY;
+ } else {
+ result = QString::compare(model->data(tstA).toString().toLower(), model->data(tstB).toString(), Qt::CaseInsensitive) < 0;
+ identical = QString::compare(model->data(tstA).toString().toLower(), model->data(tstB).toString(), Qt::CaseInsensitive) == 0;
+ }
+ }
+
+ if (! result && identical && ! model->portsAreHidden()) {
+ int portA = model->data(model->index(source_left.row(), portColumn)).toInt();
+ int portB = model->data(model->index(source_right.row(), portColumn)).toInt();
+ return portA < portB;
+ }
+ }
+
+ return result;
+ }
+
+ return QSortFilterProxyModel::lessThan(source_left, source_right);
+}
+
+bool TrafficDataFilterProxy::filterAcceptsColumn(int source_column, const QModelIndex &) const
+{
+ if (hideColumns_.contains(source_column))
+ return false;
+
+ ATapDataModel * model = qobject_cast<ATapDataModel *>(sourceModel());
+ if (model) {
+ if (model->portsAreHidden()) {
+ if (qobject_cast<EndpointDataModel *>(model) && source_column == EndpointDataModel::ENDP_COLUMN_PORT)
+ return false;
+ if (qobject_cast<ConversationDataModel *>(model) &&
+ (source_column == ConversationDataModel::CONV_COLUMN_SRC_PORT || source_column == ConversationDataModel::CONV_COLUMN_DST_PORT))
+ return false;
+ }
+ if (! model->showTotalColumn()) {
+ if (qobject_cast<EndpointDataModel *>(model) &&
+ (source_column == EndpointDataModel::ENDP_COLUMN_PACKETS_TOTAL || source_column == EndpointDataModel::ENDP_COLUMN_BYTES_TOTAL))
+ return false;
+ if (qobject_cast<ConversationDataModel *>(model) &&
+ (source_column == ConversationDataModel::CONV_COLUMN_PACKETS_TOTAL || source_column == ConversationDataModel::CONV_COLUMN_BYTES_TOTAL))
+ return false;
+ }
+ if (qobject_cast<ConversationDataModel *>(model)) {
+ ConversationDataModel * convModel = qobject_cast<ConversationDataModel *>(model);
+ if (source_column == ConversationDataModel::CONV_COLUMN_CONV_ID && ! convModel->showConversationId())
+ return false;
+ }
+ }
+
+ return true;
+}
+
+void TrafficDataFilterProxy::setColumnVisibility(int column, bool visible)
+{
+ hideColumns_.removeAll(column);
+ if (!visible)
+ hideColumns_.append(column);
+ invalidateFilter();
+}
+
+bool TrafficDataFilterProxy::columnVisible(int column) const
+{
+ return ! hideColumns_.contains(column);
+}
+
+
+TrafficTree::TrafficTree(QString baseName, GList ** recentColumnList, QWidget *parent) :
+ QTreeView(parent)
+{
+ _tapEnabled = true;
+ _saveRaw = true;
+ _baseName = baseName;
+ _exportRole = ATapDataModel::UNFORMATTED_DISPLAYDATA;
+ _header = nullptr;
+
+ setAlternatingRowColors(true);
+ setRootIsDecorated(false);
+ setSortingEnabled(true);
+ setContextMenuPolicy(Qt::CustomContextMenu);
+
+ _header = new TrafficTreeHeaderView(recentColumnList);
+ setHeader(_header);
+
+ connect(_header, &TrafficTreeHeaderView::columnsHaveChanged, this, &TrafficTree::columnsHaveChanged);
+ connect(this, &QTreeView::customContextMenuRequested, this, &TrafficTree::customContextMenu);
+}
+
+void TrafficTree::setModel(QAbstractItemModel * model)
+{
+ if (model) {
+ TrafficDataFilterProxy * proxy = qobject_cast<TrafficDataFilterProxy *>(model);
+ if (proxy) {
+ connect(_header, &TrafficTreeHeaderView::filterOnColumn, proxy, &TrafficDataFilterProxy::filterForColumn);
+ }
+ }
+
+ QTreeView::setModel(model);
+}
+
+void TrafficTree::tapListenerEnabled(bool enable)
+{
+ _tapEnabled = enable;
+}
+
+ATapDataModel * TrafficTree::dataModel()
+{
+ QSortFilterProxyModel * proxy = qobject_cast<QSortFilterProxyModel *>(model());
+ if (proxy)
+ return qobject_cast<ATapDataModel *>(proxy->sourceModel());
+ return nullptr;
+}
+
+void TrafficTree::customContextMenu(const QPoint &pos)
+{
+ if (sender() != this)
+ return;
+
+ QMenu * ctxMenu = new QMenu(this);
+ ctxMenu->setAttribute(Qt::WA_DeleteOnClose);
+ bool isConv = false;
+
+ QModelIndex idx = indexAt(pos);
+ TrafficDataFilterProxy * proxy = qobject_cast<TrafficDataFilterProxy *>(model());
+ if (proxy)
+ idx = proxy->mapToSource(idx);
+
+ ConversationDataModel * model = qobject_cast<ConversationDataModel *>(dataModel());
+ if (model)
+ isConv = true;
+
+ ctxMenu->addMenu(createActionSubMenu(FilterAction::ActionApply, idx, isConv));
+ ctxMenu->addMenu(createActionSubMenu(FilterAction::ActionPrepare, idx, isConv));
+ ctxMenu->addMenu(createActionSubMenu(FilterAction::ActionFind, idx, isConv));
+ ctxMenu->addMenu(createActionSubMenu(FilterAction::ActionColorize, idx, isConv));
+
+ ctxMenu->addSeparator();
+ ctxMenu->addMenu(createCopyMenu());
+
+ ctxMenu->addSeparator();
+ QAction * act = ctxMenu->addAction(tr("Resize all columns to content"));
+ connect(act, &QAction::triggered, this, &TrafficTree::resizeAction);
+
+ ctxMenu->popup(mapToGlobal(pos));
+}
+
+static QMap<FilterAction::ActionDirection, int> fad_to_cd_;
+static void initDirection()
+{
+ if (fad_to_cd_.count() == 0) {
+ fad_to_cd_[FilterAction::ActionDirectionAToFromB] = CONV_DIR_A_TO_FROM_B;
+ fad_to_cd_[FilterAction::ActionDirectionAToB] = CONV_DIR_A_TO_B;
+ fad_to_cd_[FilterAction::ActionDirectionAFromB] = CONV_DIR_A_FROM_B;
+ fad_to_cd_[FilterAction::ActionDirectionAToFromAny] = CONV_DIR_A_TO_FROM_ANY;
+ fad_to_cd_[FilterAction::ActionDirectionAToAny] = CONV_DIR_A_TO_ANY;
+ fad_to_cd_[FilterAction::ActionDirectionAFromAny] = CONV_DIR_A_FROM_ANY;
+ fad_to_cd_[FilterAction::ActionDirectionAnyToFromB] = CONV_DIR_ANY_TO_FROM_B;
+ fad_to_cd_[FilterAction::ActionDirectionAnyToB] = CONV_DIR_ANY_TO_B;
+ fad_to_cd_[FilterAction::ActionDirectionAnyFromB] = CONV_DIR_ANY_FROM_B;
+ }
+}
+
+QMenu * TrafficTree::createActionSubMenu(FilterAction::Action cur_action, QModelIndex idx, bool isConversation)
+{
+ initDirection();
+
+ conv_item_t * conv_item = nullptr;
+ bool hasConvId = false;
+ if (isConversation)
+ {
+ ConversationDataModel * model = qobject_cast<ConversationDataModel *>(dataModel());
+ if (model) {
+ conv_item = model->itemForRow(idx.row());
+ hasConvId = model->showConversationId(idx.row());
+ }
+ }
+
+ QMenu * subMenu = new QMenu(FilterAction::actionName(cur_action));
+ subMenu->setEnabled(_tapEnabled);
+ foreach (FilterAction::ActionType at, FilterAction::actionTypes()) {
+ if (isConversation && conv_item) {
+ QMenu *subsubmenu = subMenu->addMenu(FilterAction::actionTypeName(at));
+ if (hasConvId && (cur_action == FilterAction::ActionApply || cur_action == FilterAction::ActionPrepare)) {
+ QString filter = QString("%1.stream eq %2").arg(conv_item->ctype == CONVERSATION_TCP ? "tcp" : "udp").arg(conv_item->conv_id);
+ FilterAction * act = new FilterAction(subsubmenu, cur_action, at, tr("Filter on stream id"));
+ act->setProperty("filter", filter);
+ subsubmenu->addAction(act);
+ connect(act, &QAction::triggered, this, &TrafficTree::useFilterAction);
+ }
+ foreach (FilterAction::ActionDirection ad, FilterAction::actionDirections()) {
+ FilterAction *fa = new FilterAction(subsubmenu, cur_action, at, ad);
+ QString filter = get_conversation_filter(conv_item, (conv_direction_e) fad_to_cd_[fa->actionDirection()]);
+ fa->setProperty("filter", filter);
+ subsubmenu->addAction(fa);
+ connect(fa, &QAction::triggered, this, &TrafficTree::useFilterAction);
+ }
+ } else {
+ FilterAction *fa = new FilterAction(subMenu, cur_action, at);
+ fa->setProperty("filter", idx.data(ATapDataModel::DISPLAY_FILTER));
+ subMenu->addAction(fa);
+
+ connect(fa, &QAction::triggered, this, &TrafficTree::useFilterAction);
+ }
+ }
+
+ return subMenu;
+}
+
+QMenu * TrafficTree::createCopyMenu(QWidget *parent)
+{
+ QMenu *copy_menu = new QMenu(tr("Copy %1 table").arg(_baseName), parent);
+ QAction *ca;
+ ca = copy_menu->addAction(tr("as CSV"));
+ ca->setToolTip(tr("Copy all values of this page to the clipboard in CSV (Comma Separated Values) format."));
+ ca->setProperty("copy_as", TrafficTree::CLIPBOARD_CSV);
+ connect(ca, &QAction::triggered, this, &TrafficTree::clipboardAction);
+ ca = copy_menu->addAction(tr("as YAML"));
+ ca->setToolTip(tr("Copy all values of this page to the clipboard in the YAML data serialization format."));
+ ca->setProperty("copy_as", TrafficTree::CLIPBOARD_YAML);
+ connect(ca, &QAction::triggered, this, &TrafficTree::clipboardAction);
+ ca = copy_menu->addAction(tr("as JSON"));
+ ca->setToolTip(tr("Copy all values of this page to the clipboard in the JSON data serialization format."));
+ ca->setProperty("copy_as", TrafficTree::CLIPBOARD_JSON);
+ connect(ca, &QAction::triggered, this, &TrafficTree::clipboardAction);
+
+ copy_menu->addSeparator();
+ ca = copy_menu->addAction(tr("Save data as raw"));
+ ca->setToolTip(tr("Disable data formatting for export/clipboard and save as raw data"));
+ ca->setCheckable(true);
+ ca->setChecked(_exportRole == ATapDataModel::UNFORMATTED_DISPLAYDATA);
+ connect(ca, &QAction::triggered, this, &TrafficTree::toggleSaveRawAction);
+
+ return copy_menu;
+}
+
+void TrafficTree::useFilterAction()
+{
+ FilterAction *fa = qobject_cast<FilterAction *>(sender());
+ if (!fa || !_tapEnabled)
+ return;
+
+ QString filter = fa->property("filter").toString();
+ if (filter.length() > 0)
+ {
+ MainWindow * mainWin = (MainWindow *)(mainApp->mainWindow());
+ mainWin->setDisplayFilter(filter, fa->action(), fa->actionType());
+ }
+}
+
+void TrafficTree::clipboardAction()
+{
+ QAction * ca = qobject_cast<QAction *>(sender());
+ if (ca && ca->property("copy_as").isValid())
+ copyToClipboard((eTrafficTreeClipboard)ca->property("copy_as").toInt());
+}
+
+void TrafficTree::resizeAction()
+{
+ for (int col = 0; col < model()->columnCount(); col++)
+ resizeColumnToContents(col);
+}
+
+void TrafficTree::toggleSaveRawAction()
+{
+ if (_exportRole == ATapDataModel::UNFORMATTED_DISPLAYDATA)
+ _exportRole = Qt::DisplayRole;
+ else
+ _exportRole = ATapDataModel::UNFORMATTED_DISPLAYDATA;
+}
+
+void TrafficTree::copyToClipboard(eTrafficTreeClipboard type)
+{
+ if (!model())
+ return;
+
+ QString clipText;
+ QTextStream stream(&clipText, QIODevice::Text);
+
+ if (type == CLIPBOARD_CSV) {
+ QMap<int, QString> headers;
+ QStringList rdsl;
+ for (int cnt = 0; cnt < model()->columnCount(); cnt++)
+ {
+ rdsl << model()->headerData(cnt, Qt::Horizontal, Qt::DisplayRole).toString();
+ }
+ stream << rdsl.join(",") << "\n";
+
+ for (int row = 0; row < model()->rowCount(); row++) {
+ rdsl.clear();
+ for (int col = 0; col < model()->columnCount(); col++) {
+ QModelIndex idx = model()->index(row, col);
+ QVariant v = model()->data(idx, _exportRole);
+ if (!v.isValid()) {
+ rdsl << "\"\"";
+ } else if (v.userType() == QMetaType::QString) {
+ rdsl << QString("\"%1\"").arg(v.toString());
+ } else {
+ rdsl << v.toString();
+ }
+ }
+ stream << rdsl.join(",") << '\n';
+ }
+ } else if (type == CLIPBOARD_YAML) {
+ stream << "---" << '\n';
+ QMap<int, QString> headers;
+ for (int cnt = 0; cnt < model()->columnCount(); cnt++)
+ headers.insert(cnt, model()->headerData(cnt, Qt::Horizontal, Qt::DisplayRole).toString());
+
+ for (int row = 0; row < model()->rowCount(); row++) {
+ stream << "-" << '\n';
+ for (int col = 0; col < model()->columnCount(); col++) {
+ QModelIndex idx = model()->index(row, col);
+ QVariant v = model()->data(idx, _exportRole);
+ stream << " - " << headers[col] << ": " << v.toString() << '\n';
+ }
+ }
+ } else if (type == CLIPBOARD_JSON) {
+ QMap<int, QString> headers;
+ for (int cnt = 0; cnt < model()->columnCount(); cnt++)
+ headers.insert(cnt, model()->headerData(cnt, Qt::Horizontal, Qt::DisplayRole).toString());
+
+ QJsonArray records;
+
+ for (int row = 0; row < model()->rowCount(); row++) {
+ QJsonObject rowData;
+ foreach(int col, headers.keys()) {
+ QModelIndex idx = model()->index(row, col);
+ rowData.insert(headers[col], model()->data(idx, _exportRole).toString());
+ }
+ records.push_back(rowData);
+ }
+
+ QJsonDocument json;
+ json.setArray(records);
+ stream << json.toJson();
+ }
+
+ mainApp->clipboard()->setText(stream.readAll());
+}
+
+void TrafficTree::disableTap()
+{
+ ATapDataModel * model = dataModel();
+ if (!model)
+ return;
+ model->disableTap();
+}
+
+void TrafficTree::applyRecentColumns()
+{
+ if (_header)
+ _header->applyRecent();
+}
+
+void TrafficTree::columnsChanged(QList<int> columns)
+{
+ TrafficDataFilterProxy * proxy = qobject_cast<TrafficDataFilterProxy *>(model());
+ if (!proxy)
+ return;
+
+ for (int col = 0; col < dataModel()->columnCount(); col++) {
+ proxy->setColumnVisibility(col, columns.contains(col));
+ }
+
+ resizeAction();
+}