diff options
Diffstat (limited to 'ui/qt/widgets/capture_filter_edit.cpp')
-rw-r--r-- | ui/qt/widgets/capture_filter_edit.cpp | 562 |
1 files changed, 562 insertions, 0 deletions
diff --git a/ui/qt/widgets/capture_filter_edit.cpp b/ui/qt/widgets/capture_filter_edit.cpp new file mode 100644 index 0000000..3c72fe4 --- /dev/null +++ b/ui/qt/widgets/capture_filter_edit.cpp @@ -0,0 +1,562 @@ +/* capture_filter_edit.cpp + * + * 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" + +#include <glib.h> + +#include <epan/proto.h> + +#include "capture_opts.h" + +#include <ui/capture_globals.h> +#include <wsutil/filter_files.h> +#include <wsutil/utf8_entities.h> + +#include <ui/qt/widgets/capture_filter_edit.h> +#include "capture_filter_syntax_worker.h" +#include "filter_dialog.h" +#include <ui/qt/widgets/stock_icon_tool_button.h> +#include "main_application.h" + +#include <QComboBox> +#include <QCompleter> +#include <QMenu> +#include <QMessageBox> +#include <QPainter> +#include <QStringListModel> +#include <QStyleOptionFrame> + +#include <ui/qt/utils/qt_ui_utils.h> + +// To do: +// - This duplicates some DisplayFilterEdit code. +// - We need simplified (button- and dropdown-free) versions for use in dialogs and field-only checking. + +static const QString libpcap_primitive_chars_ = "-0123456789abcdefghijklmnopqrstuvwxyz"; + +// Primitives are from pcap-filter.manmisc +static const QStringList libpcap_primitives_ = QStringList() + // "Abbreviations for..." + << "ether proto" + << "ip" << "ip6" << "arp" << "rarp" << "atalk" << "aarp" << "decnet" << "iso" << "stp" << "ipx" << "netbeui" + << "moprc" << "mopdl" + // ip proto + << "tcp" << "udp" << "icmp" + // iso proto + << "clnp" << "esis" << "isis" + // IS‐IS PDU types + << "l1" << "l2" << "iih" << "lsp" << "snp" << "csnp" << "psnp" + + // grep -E '^\.IP "\\fB.*\\f(R"|P)$' pcap-filter.manmisc | sed -e 's/^\.IP "\\fB/<< "/' -e 's/ *\\f.*/"/' | sort -u + << "action" + << "clnp" + << "decnet dst" + << "decnet host" + << "decnet src" + << "dir" + << "dst host" + << "dst net" + << "dst port" + << "dst portrange" + << "ether broadcast" + << "ether dst" + << "ether host" + << "ether multicast" + << "ether src" + << "gateway" + << "greater" + << "host" + << "ifname" + << "ip broadcast" + << "ip multicast" + << "ip proto" + << "ip protochain" + << "ip6 multicast" + << "ip6 proto" + << "ip6 protochain" + << "iso proto" + << "l1" + << "lat" + << "less" + << "mpls" + << "net" + << "on" + << "port" + << "portrange" + << "reason" + << "rnr" + << "rset" + << "rulenum" + << "ruleset" + << "src host" + << "src net" + << "src port" + << "src portrange" + << "srnr" + << "subrulenum" + << "subtype" + << "type" + << "vlan" + << "wlan addr1" + << "wlan addr2" + << "wlan addr3" + << "wlan addr4" + << "wlan ra" + << "wlan ta" + ; + +CaptureFilterEdit::CaptureFilterEdit(QWidget *parent, bool plain) : + SyntaxLineEdit(parent), + plain_(plain), + field_name_only_(false), + enable_save_action_(false), + save_action_(NULL), + remove_action_(NULL), + actions_(Q_NULLPTR), + bookmark_button_(NULL), + clear_button_(NULL), + apply_button_(NULL) +{ + setAccessibleName(tr("Capture filter entry")); + + completion_model_ = new QStringListModel(this); + setCompleter(new QCompleter(completion_model_, this)); + setCompletionTokenChars(libpcap_primitive_chars_); + + setConflict(false); + + QString buttonStyle = QString( + "QToolButton {" + " border: none;" + " background: transparent;" // Disables platform style on Windows. + " padding: 0 0 0 0;" + "}" + "QToolButton::menu-indicator {" + " image: none;" + "}" + ); + + if (!plain_) { + bookmark_button_ = new StockIconToolButton(this, "x-capture-filter-bookmark"); + bookmark_button_->setCursor(Qt::ArrowCursor); + bookmark_button_->setMenu(new QMenu(bookmark_button_)); + bookmark_button_->setPopupMode(QToolButton::InstantPopup); + bookmark_button_->setToolTip(tr("Manage saved bookmarks.")); + bookmark_button_->setIconSize(QSize(14, 14)); + bookmark_button_->setStyleSheet(buttonStyle); + connect(bookmark_button_, &StockIconToolButton::clicked, this, &CaptureFilterEdit::bookmarkClicked); + + clear_button_ = new StockIconToolButton(this, "x-filter-clear"); + clear_button_->setCursor(Qt::ArrowCursor); + clear_button_->setToolTip(QString()); + clear_button_->setIconSize(QSize(14, 14)); + clear_button_->setStyleSheet(buttonStyle); + connect(clear_button_, &StockIconToolButton::clicked, this, &CaptureFilterEdit::clearFilter); + } + + connect(this, &CaptureFilterEdit::textChanged, this, + static_cast<void (CaptureFilterEdit::*)(const QString &)>(&CaptureFilterEdit::checkFilter)); + +#if 0 + // Disable the apply button for now + if (!plain_) { + apply_button_ = new StockIconToolButton(this, "x-filter-apply"); + apply_button_->setCursor(Qt::ArrowCursor); + apply_button_->setEnabled(false); + apply_button_->setToolTip(tr("Apply this filter string to the display.")); + apply_button_->setIconSize(QSize(24, 14)); + apply_button_->setStyleSheet(buttonStyle); + connect(apply_button_, &StockIconToolButton::clicked, this, &CaptureFilterEdit::applyCaptureFilter); + } +#endif + connect(this, &CaptureFilterEdit::returnPressed, this, &CaptureFilterEdit::applyCaptureFilter); + + int frameWidth = style()->pixelMetric(QStyle::PM_DefaultFrameWidth); + QSize bksz(0, 0); + if (bookmark_button_) bksz = bookmark_button_->sizeHint(); + QSize cbsz(0, 0); + if (clear_button_) cbsz = clear_button_->sizeHint(); + QSize apsz(0, 0); + if (apply_button_) apsz = apply_button_->sizeHint(); + + setStyleSheet(QString( + "CaptureFilterEdit {" + " padding-left: %1px;" + " margin-left: %2px;" + " margin-right: %3px;" + "}" + ) + .arg(frameWidth + 1) + .arg(bksz.width()) + .arg(cbsz.width() + apsz.width() + frameWidth + 2) + ); + + QComboBox *cf_combo = qobject_cast<QComboBox *>(parent); + if (cf_combo) { +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) + connect(cf_combo, static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::textActivated), + this, &CaptureFilterEdit::textEdited); +#else + connect(cf_combo, static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::activated), + this, &CaptureFilterEdit::textEdited); +#endif + } + + syntax_thread_ = new QThread; + syntax_worker_ = new CaptureFilterSyntaxWorker; + syntax_worker_->moveToThread(syntax_thread_); + connect(mainApp, &MainApplication::appInitialized, this, &CaptureFilterEdit::updateBookmarkMenu); + connect(mainApp, &MainApplication::captureFilterListChanged, this, &CaptureFilterEdit::updateBookmarkMenu); + connect(syntax_thread_, &QThread::started, this, + static_cast<void (CaptureFilterEdit::*)()>(&CaptureFilterEdit::checkFilter)); + connect(syntax_worker_, &CaptureFilterSyntaxWorker::syntaxResult, + this, &CaptureFilterEdit::setFilterSyntaxState); + connect(this, &CaptureFilterEdit::captureFilterChanged, syntax_worker_, &CaptureFilterSyntaxWorker::checkFilter); + syntax_thread_->start(); + updateBookmarkMenu(); +} + +CaptureFilterEdit::~CaptureFilterEdit() +{ + syntax_thread_->quit(); + syntax_thread_->wait(); + delete syntax_thread_; + delete syntax_worker_; +} + +void CaptureFilterEdit::paintEvent(QPaintEvent *evt) { + SyntaxLineEdit::paintEvent(evt); + + if (bookmark_button_) { + // Draw the borders by hand. We could try to do this in the + // style sheet but it's a pain. +#ifdef Q_OS_MAC + QColor divider_color = Qt::gray; +#else + QColor divider_color = palette().shadow().color(); +#endif + QPainter painter(this); + painter.setPen(divider_color); + QRect cr = contentsRect(); + QSize bksz = bookmark_button_->size(); + painter.drawLine(bksz.width(), cr.top(), bksz.width(), cr.bottom() + 1); + + if (!text().isEmpty()) { + int xpos = cr.width() - 4; + if (clear_button_ && clear_button_->isVisible()) + xpos -= clear_button_->width(); + if (apply_button_ && apply_button_->isVisible()) + xpos -= apply_button_->width(); + painter.drawLine(xpos, cr.top(), xpos, cr.bottom() + 1); + } + } +} + +void CaptureFilterEdit::resizeEvent(QResizeEvent *) +{ + QSize cbsz(0, 0); + if (clear_button_) cbsz = clear_button_->sizeHint(); + QSize apsz(0, 0); + if (apply_button_) apsz = apply_button_->sizeHint(); + + int frameWidth = style()->pixelMetric(QStyle::PM_DefaultFrameWidth); + if (clear_button_) { + clear_button_->move(contentsRect().right() - frameWidth - cbsz.width() - apsz.width(), + contentsRect().top()); + clear_button_->setMinimumHeight(contentsRect().height()); + clear_button_->setMaximumHeight(contentsRect().height()); + } + if (apply_button_) { + apply_button_->move(contentsRect().right() - frameWidth - apsz.width(), + contentsRect().top()); + apply_button_->setMinimumHeight(contentsRect().height()); + apply_button_->setMaximumHeight(contentsRect().height()); + } + if (bookmark_button_) { + bookmark_button_->setMinimumHeight(contentsRect().height()); + bookmark_button_->setMaximumHeight(contentsRect().height()); + } +} + +void CaptureFilterEdit::setConflict(bool conflict) +{ + if (conflict) { + //: This is a very long concept that needs to fit into a short space. + placeholder_text_ = tr("Multiple filters selected. Override them here or leave this blank to preserve them."); + setToolTip(tr("<p>The interfaces you have selected have different capture filters." + " Typing a filter here will override them. Doing nothing will" + " preserve them.</p>")); + } else { + placeholder_text_ = QString(tr("Enter a capture filter %1")).arg(UTF8_HORIZONTAL_ELLIPSIS); + setToolTip(QString()); + } + setPlaceholderText(placeholder_text_); +} + +// XXX Make this private along with setConflict. +QPair<const QString, bool> CaptureFilterEdit::getSelectedFilter() +{ + QString user_filter; + bool filter_conflict = false; +#ifdef HAVE_LIBPCAP + int selected_devices = 0; + + for (guint i = 0; i < global_capture_opts.all_ifaces->len; i++) { + interface_t *device = &g_array_index(global_capture_opts.all_ifaces, interface_t, i); + if (device->selected) { + selected_devices++; + if (selected_devices == 1) { + user_filter = device->cfilter; + } else { + if (user_filter.compare(device->cfilter)) { + filter_conflict = true; + } + } + } + } +#endif // HAVE_LIBPCAP + return QPair<const QString, bool>(user_filter, filter_conflict); +} + +void CaptureFilterEdit::checkFilter(const QString& filter) +{ + if (text().length() == 0 && actions_ && actions_->checkedAction()) + actions_->checkedAction()->setChecked(false); + + setSyntaxState(Busy); + mainApp->popStatus(MainApplication::FilterSyntax); + setToolTip(QString()); + bool empty = filter.isEmpty(); + + setConflict(false); + if (bookmark_button_) { + bool match = false; + + FilterListModel model(FilterListModel::Capture); + QModelIndex idx = model.findByExpression(text()); + if (idx.isValid()) { + match = true; + + bookmark_button_->setStockIcon("x-filter-matching-bookmark"); + if (remove_action_) { + remove_action_->setData(text()); + remove_action_->setEnabled(true); + } + } else { + bookmark_button_->setStockIcon("x-capture-filter-bookmark"); + if (remove_action_) { + remove_action_->setEnabled(false); + } + } + + enable_save_action_ = (!match && !filter.isEmpty()); + if (save_action_) { + save_action_->setEnabled(false); + } + } + + if (apply_button_) { + apply_button_->setEnabled(false); + } + + if (clear_button_) { + clear_button_->setVisible(!empty); + } + + if (empty) { + setFilterSyntaxState(filter, Empty, QString()); + } else { + emit captureFilterChanged(filter); + } +} + +void CaptureFilterEdit::checkFilter() +{ + checkFilter(text()); +} + +void CaptureFilterEdit::updateBookmarkMenu() +{ + if (!bookmark_button_) + return; + + QMenu *bb_menu = bookmark_button_->menu(); + bb_menu->clear(); + + save_action_ = bb_menu->addAction(tr("Save this filter")); + connect(save_action_, &QAction::triggered, this, &CaptureFilterEdit::saveFilter); + remove_action_ = bb_menu->addAction(tr("Remove this filter")); + connect(remove_action_, &QAction::triggered, this, &CaptureFilterEdit::removeFilter); + QAction *manage_action = bb_menu->addAction(tr("Manage Capture Filters")); + connect(manage_action, &QAction::triggered, this, &CaptureFilterEdit::showFilters); + bb_menu->addSeparator(); + + FilterListModel model(FilterListModel::Capture); + QModelIndex idx = model.findByExpression(text()); + + int one_em = bb_menu->fontMetrics().height(); + + if (! actions_) + actions_ = new QActionGroup(this); + + for (int row = 0; row < model.rowCount(); row++) + { + QModelIndex nameIdx = model.index(row, FilterListModel::ColumnName); + QString name = nameIdx.data().toString(); + QString expr = model.index(row, FilterListModel::ColumnExpression).data().toString(); + QString prep_text = QString("%1: %2").arg(name).arg(expr); + + prep_text = bb_menu->fontMetrics().elidedText(prep_text, Qt::ElideRight, one_em * 40); + QAction * prep_action = bb_menu->addAction(prep_text); + prep_action->setCheckable(true); + if (nameIdx == idx) + prep_action->setChecked(true); + + actions_->addAction(prep_action); + prep_action->setProperty("capture_filter", expr); + connect(prep_action, &QAction::triggered, this, &CaptureFilterEdit::prepareFilter); + } + + checkFilter(); +} + +void CaptureFilterEdit::setFilterSyntaxState(QString filter, int state, QString err_msg) +{ + if (filter.compare(text()) == 0) { // The user hasn't changed the filter + setSyntaxState((SyntaxState)state); + if (!err_msg.isEmpty()) { + mainApp->pushStatus(MainApplication::FilterSyntax, err_msg); + setToolTip(err_msg); + } + } + + bool valid = (state != Invalid); + + if (valid) { + if (save_action_) { + save_action_->setEnabled(enable_save_action_); + } + if (apply_button_) { + apply_button_->setEnabled(true); + } + } + + emit captureFilterSyntaxChanged(valid); +} + +void CaptureFilterEdit::bookmarkClicked() +{ + emit addBookmark(text()); +} + +void CaptureFilterEdit::clearFilter() +{ + clear(); + emit textEdited(text()); +} + +void CaptureFilterEdit::buildCompletionList(const QString &primitive_word, const QString &preamble _U_) +{ + if (primitive_word.length() < 1) { + completion_model_->setStringList(QStringList()); + return; + } + + // Grab matching capture filters from our parent combo and from the + // saved capture filters file. Skip ones that look like single fields + // and assume they will be added below. + QStringList complex_list; + QComboBox *cf_combo = qobject_cast<QComboBox *>(parent()); + if (cf_combo) { + for (int i = 0; i < cf_combo->count() ; i++) { + QString recent_filter = cf_combo->itemText(i); + + if (isComplexFilter(recent_filter)) { + complex_list << recent_filter; + } + } + } + FilterListModel model(FilterListModel::Capture); + for (int row = 0; row < model.rowCount(); row++) + { + QString saved_filter = model.index(row, FilterListModel::ColumnExpression).data().toString(); + + if (isComplexFilter(saved_filter) && !complex_list.contains(saved_filter)) { + complex_list << saved_filter; + } + } + + // libpcap has a small number of primitives so we just add the whole list + // sans the current word. + QStringList primitive_list = libpcap_primitives_; + primitive_list.removeAll(primitive_word); + + completion_model_->setStringList(complex_list + primitive_list); + completer()->setCompletionPrefix(primitive_word); +} + +void CaptureFilterEdit::applyCaptureFilter() +{ + if (syntaxState() == Invalid) { + return; + } + + emit startCapture(); +} + +void CaptureFilterEdit::saveFilter() +{ + FilterDialog *capture_filter_dlg = new FilterDialog(window(), FilterDialog::CaptureFilter, text()); + capture_filter_dlg->setWindowModality(Qt::ApplicationModal); + capture_filter_dlg->setAttribute(Qt::WA_DeleteOnClose); + capture_filter_dlg->show(); +} + +void CaptureFilterEdit::removeFilter() +{ + if (! actions_ || ! actions_->checkedAction()) + return; + + QAction *ra = actions_->checkedAction(); + if (ra->property("capture_filter").toString().isEmpty()) + return; + + QString remove_filter = ra->property("capture_filter").toString(); + + FilterListModel model(FilterListModel::Capture); + QModelIndex idx = model.findByExpression(remove_filter); + if (idx.isValid()) + { + model.removeFilter(idx); + model.saveList(); + } + + updateBookmarkMenu(); +} + +void CaptureFilterEdit::showFilters() +{ + FilterDialog *capture_filter_dlg = new FilterDialog(window(), FilterDialog::CaptureFilter); + capture_filter_dlg->setWindowModality(Qt::ApplicationModal); + capture_filter_dlg->setAttribute(Qt::WA_DeleteOnClose); + capture_filter_dlg->show(); +} + +void CaptureFilterEdit::prepareFilter() +{ + QAction *pa = qobject_cast<QAction*>(sender()); + if (! pa || pa->property("capture_filter").toString().isEmpty()) + return; + + QString filter = pa->property("capture_filter").toString(); + setText(filter); + + emit textEdited(filter); +} |