1 /* capture_filter_edit.cpp
2  *
3  * Wireshark - Network traffic analyzer
4  * By Gerald Combs <gerald@wireshark.org>
5  * Copyright 1998 Gerald Combs
6  *
7  * SPDX-License-Identifier: GPL-2.0-or-later
8  */
9 
10 #include "config.h"
11 
12 #include <glib.h>
13 
14 #include <epan/proto.h>
15 
16 #include "capture_opts.h"
17 
18 #include <ui/capture_globals.h>
19 #include <ui/filter_files.h>
20 #include <wsutil/utf8_entities.h>
21 
22 #include <ui/qt/widgets/capture_filter_edit.h>
23 #include "capture_filter_syntax_worker.h"
24 #include "filter_dialog.h"
25 #include <ui/qt/widgets/stock_icon_tool_button.h>
26 #include "wireshark_application.h"
27 
28 #include <QComboBox>
29 #include <QCompleter>
30 #include <QMenu>
31 #include <QMessageBox>
32 #include <QPainter>
33 #include <QStringListModel>
34 #include <QStyleOptionFrame>
35 
36 #include <ui/qt/utils/qt_ui_utils.h>
37 
38 // To do:
39 // - This duplicates some DisplayFilterEdit code.
40 // - We need simplified (button- and dropdown-free) versions for use in dialogs and field-only checking.
41 
42 static const QString libpcap_primitive_chars_ = "-0123456789abcdefghijklmnopqrstuvwxyz";
43 
44 // Primitives are from pcap-filter.manmisc
45 static const QStringList libpcap_primitives_ = QStringList()
46         // "Abbreviations for..."
47         << "ether proto"
48         << "ip" << "ip6" << "arp" << "rarp" << "atalk" << "aarp" << "decnet" << "iso" << "stp" << "ipx" << "netbeui"
49         << "moprc" << "mopdl"
50         // ip proto
51         << "tcp" << "udp" << "icmp"
52         // iso proto
53         << "clnp" << "esis" << "isis"
54         // IS‐IS PDU types
55         << "l1" << "l2" << "iih" << "lsp" << "snp" << "csnp" << "psnp"
56 
57         // grep -E '^\.IP "\\fB.*\\f(R"|P)$' pcap-filter.manmisc | sed -e 's/^\.IP "\\fB/<< "/' -e 's/ *\\f.*/"/' | sort -u
58         << "action"
59         << "clnp"
60         << "decnet dst"
61         << "decnet host"
62         << "decnet src"
63         << "dir"
64         << "dst host"
65         << "dst net"
66         << "dst port"
67         << "dst portrange"
68         << "ether broadcast"
69         << "ether dst"
70         << "ether host"
71         << "ether multicast"
72         << "ether src"
73         << "gateway"
74         << "greater"
75         << "host"
76         << "ifname"
77         << "ip broadcast"
78         << "ip multicast"
79         << "ip proto"
80         << "ip protochain"
81         << "ip6 multicast"
82         << "ip6 proto"
83         << "ip6 protochain"
84         << "iso proto"
85         << "l1"
86         << "lat"
87         << "less"
88         << "mpls"
89         << "net"
90         << "on"
91         << "port"
92         << "portrange"
93         << "reason"
94         << "rnr"
95         << "rset"
96         << "rulenum"
97         << "ruleset"
98         << "src host"
99         << "src net"
100         << "src port"
101         << "src portrange"
102         << "srnr"
103         << "subrulenum"
104         << "subtype"
105         << "type"
106         << "vlan"
107         << "wlan addr1"
108         << "wlan addr2"
109         << "wlan addr3"
110         << "wlan addr4"
111         << "wlan ra"
112         << "wlan ta"
113            ;
114 
CaptureFilterEdit(QWidget * parent,bool plain)115 CaptureFilterEdit::CaptureFilterEdit(QWidget *parent, bool plain) :
116     SyntaxLineEdit(parent),
117     plain_(plain),
118     field_name_only_(false),
119     enable_save_action_(false),
120     save_action_(NULL),
121     remove_action_(NULL),
122     actions_(Q_NULLPTR),
123     bookmark_button_(NULL),
124     clear_button_(NULL),
125     apply_button_(NULL)
126 {
127     setAccessibleName(tr("Capture filter entry"));
128 
129     completion_model_ = new QStringListModel(this);
130     setCompleter(new QCompleter(completion_model_, this));
131     setCompletionTokenChars(libpcap_primitive_chars_);
132 
133     setConflict(false);
134 
135     if (!plain_) {
136         bookmark_button_ = new StockIconToolButton(this, "x-capture-filter-bookmark");
137         bookmark_button_->setCursor(Qt::ArrowCursor);
138         bookmark_button_->setMenu(new QMenu(bookmark_button_));
139         bookmark_button_->setPopupMode(QToolButton::InstantPopup);
140         bookmark_button_->setToolTip(tr("Manage saved bookmarks."));
141         bookmark_button_->setIconSize(QSize(14, 14));
142         bookmark_button_->setStyleSheet(
143                     "QToolButton {"
144                     "  border: none;"
145                     "  background: transparent;" // Disables platform style on Windows.
146                     "  padding: 0 0 0 0;"
147                     "}"
148                     "QToolButton::menu-indicator { image: none; }"
149             );
150         connect(bookmark_button_, &StockIconToolButton::clicked, this, &CaptureFilterEdit::bookmarkClicked);
151 
152         clear_button_ = new StockIconToolButton(this, "x-filter-clear");
153         clear_button_->setCursor(Qt::ArrowCursor);
154         clear_button_->setToolTip(QString());
155         clear_button_->setIconSize(QSize(14, 14));
156         clear_button_->setStyleSheet(
157                 "QToolButton {"
158                 "  border: none;"
159                 "  background: transparent;" // Disables platform style on Windows.
160                 "  padding: 0 0 0 0;"
161                 "  margin-left: 1px;"
162                 "}"
163                 );
164         connect(clear_button_, &StockIconToolButton::clicked, this, &CaptureFilterEdit::clearFilter);
165     }
166 
167     connect(this, &CaptureFilterEdit::textChanged, this,
168             static_cast<void (CaptureFilterEdit::*)(const QString &)>(&CaptureFilterEdit::checkFilter));
169 
170 #if 0
171     // Disable the apply button for now
172     if (!plain_) {
173         apply_button_ = new StockIconToolButton(this, "x-filter-apply");
174         apply_button_->setCursor(Qt::ArrowCursor);
175         apply_button_->setEnabled(false);
176         apply_button_->setToolTip(tr("Apply this filter string to the display."));
177         apply_button_->setIconSize(QSize(24, 14));
178         apply_button_->setStyleSheet(
179                 "QToolButton {"
180                 "  border: none;"
181                 "  background: transparent;" // Disables platform style on Windows.
182                 "  padding: 0 0 0 0;"
183                 "}"
184                 );
185         connect(apply_button_, &StockIconToolButton::clicked, this, &CaptureFilterEdit::applyCaptureFilter);
186     }
187 #endif
188     connect(this, &CaptureFilterEdit::returnPressed, this, &CaptureFilterEdit::applyCaptureFilter);
189 
190     int frameWidth = style()->pixelMetric(QStyle::PM_DefaultFrameWidth);
191     QSize bksz;
192     if (bookmark_button_) bksz = bookmark_button_->sizeHint();
193     QSize cbsz;
194     if (clear_button_) cbsz = clear_button_->sizeHint();
195     QSize apsz;
196     if (apply_button_) apsz = apply_button_->sizeHint();
197 
198     setStyleSheet(QString(
199             "CaptureFilterEdit {"
200             "  padding-left: %1px;"
201             "  margin-left: %2px;"
202             "  margin-right: %3px;"
203             "}"
204             )
205             .arg(frameWidth + 1)
206             .arg(bksz.width())
207             .arg(cbsz.width() + apsz.width() + frameWidth + 1)
208             );
209 
210     QComboBox *cf_combo = qobject_cast<QComboBox *>(parent);
211     if (cf_combo) {
212 #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
213         connect(cf_combo, static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::textActivated),
214                 this, &CaptureFilterEdit::textEdited);
215 #else
216         connect(cf_combo, static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::activated),
217                 this, &CaptureFilterEdit::textEdited);
218 #endif
219     }
220 
221     syntax_thread_ = new QThread;
222     syntax_worker_ = new CaptureFilterSyntaxWorker;
223     syntax_worker_->moveToThread(syntax_thread_);
224     connect(wsApp, &WiresharkApplication::appInitialized, this, &CaptureFilterEdit::updateBookmarkMenu);
225     connect(wsApp, &WiresharkApplication::captureFilterListChanged, this, &CaptureFilterEdit::updateBookmarkMenu);
226     connect(syntax_thread_, &QThread::started, this,
227             static_cast<void (CaptureFilterEdit::*)()>(&CaptureFilterEdit::checkFilter));
228     connect(syntax_worker_, &CaptureFilterSyntaxWorker::syntaxResult,
229             this, &CaptureFilterEdit::setFilterSyntaxState);
230     connect(this, &CaptureFilterEdit::captureFilterChanged, syntax_worker_, &CaptureFilterSyntaxWorker::checkFilter);
231     syntax_thread_->start();
232     updateBookmarkMenu();
233 }
234 
~CaptureFilterEdit()235 CaptureFilterEdit::~CaptureFilterEdit()
236 {
237     syntax_thread_->quit();
238     syntax_thread_->wait();
239     delete syntax_thread_;
240     delete syntax_worker_;
241 }
242 
paintEvent(QPaintEvent * evt)243 void CaptureFilterEdit::paintEvent(QPaintEvent *evt) {
244     SyntaxLineEdit::paintEvent(evt);
245 
246     if (bookmark_button_) {
247         // Draw the right border by hand. We could try to do this in the
248         // style sheet but it's a pain.
249 #ifdef Q_OS_MAC
250         QColor divider_color = Qt::gray;
251 #else
252         QColor divider_color = palette().shadow().color();
253 #endif
254         QPainter painter(this);
255         painter.setPen(divider_color);
256         QRect cr = contentsRect();
257         QSize bksz = bookmark_button_->size();
258         painter.drawLine(bksz.width(), cr.top(), bksz.width(), cr.bottom());
259     }
260 }
261 
resizeEvent(QResizeEvent *)262 void CaptureFilterEdit::resizeEvent(QResizeEvent *)
263 {
264     QSize cbsz;
265     if (clear_button_) cbsz = clear_button_->sizeHint();
266     QSize apsz;
267     if (apply_button_) apsz = apply_button_->sizeHint();
268 
269     int frameWidth = style()->pixelMetric(QStyle::PM_DefaultFrameWidth);
270     if (clear_button_) {
271         clear_button_->move(contentsRect().right() - frameWidth - cbsz.width() - apsz.width(),
272                             contentsRect().top());
273         clear_button_->setMinimumHeight(contentsRect().height());
274         clear_button_->setMaximumHeight(contentsRect().height());
275     }
276     if (apply_button_) {
277         apply_button_->move(contentsRect().right() - frameWidth - apsz.width(),
278                             contentsRect().top());
279         apply_button_->setMinimumHeight(contentsRect().height());
280         apply_button_->setMaximumHeight(contentsRect().height());
281     }
282     if (bookmark_button_) {
283         bookmark_button_->setMinimumHeight(contentsRect().height());
284         bookmark_button_->setMaximumHeight(contentsRect().height());
285     }
286 }
287 
setConflict(bool conflict)288 void CaptureFilterEdit::setConflict(bool conflict)
289 {
290     if (conflict) {
291         //: This is a very long concept that needs to fit into a short space.
292         placeholder_text_ = tr("Multiple filters selected. Override them here or leave this blank to preserve them.");
293         setToolTip(tr("<p>The interfaces you have selected have different capture filters."
294                       " Typing a filter here will override them. Doing nothing will"
295                       " preserve them.</p>"));
296     } else {
297         placeholder_text_ = QString(tr("Enter a capture filter %1")).arg(UTF8_HORIZONTAL_ELLIPSIS);
298         setToolTip(QString());
299     }
300     setPlaceholderText(placeholder_text_);
301 }
302 
303 // XXX Make this private along with setConflict.
getSelectedFilter()304 QPair<const QString, bool> CaptureFilterEdit::getSelectedFilter()
305 {
306     QString user_filter;
307     bool filter_conflict = false;
308 #ifdef HAVE_LIBPCAP
309     int selected_devices = 0;
310 
311     for (guint i = 0; i < global_capture_opts.all_ifaces->len; i++) {
312         interface_t *device = &g_array_index(global_capture_opts.all_ifaces, interface_t, i);
313         if (device->selected) {
314             selected_devices++;
315             if (selected_devices == 1) {
316                 user_filter = device->cfilter;
317             } else {
318                 if (user_filter.compare(device->cfilter)) {
319                     filter_conflict = true;
320                 }
321             }
322         }
323     }
324 #endif // HAVE_LIBPCAP
325     return QPair<const QString, bool>(user_filter, filter_conflict);
326 }
327 
checkFilter(const QString & filter)328 void CaptureFilterEdit::checkFilter(const QString& filter)
329 {
330     if (text().length() == 0 && actions_ && actions_->checkedAction())
331         actions_->checkedAction()->setChecked(false);
332 
333     setSyntaxState(Busy);
334     wsApp->popStatus(WiresharkApplication::FilterSyntax);
335     setToolTip(QString());
336     bool empty = filter.isEmpty();
337 
338     setConflict(false);
339     if (bookmark_button_) {
340         bool match = false;
341 
342         FilterListModel model(FilterListModel::Capture);
343         QModelIndex idx = model.findByExpression(text());
344         if (idx.isValid()) {
345             match = true;
346 
347             bookmark_button_->setStockIcon("x-filter-matching-bookmark");
348             if (remove_action_) {
349                 remove_action_->setData(text());
350                 remove_action_->setEnabled(true);
351             }
352         } else {
353             bookmark_button_->setStockIcon("x-capture-filter-bookmark");
354             if (remove_action_) {
355                 remove_action_->setEnabled(false);
356             }
357         }
358 
359         enable_save_action_ = (!match && !filter.isEmpty());
360         if (save_action_) {
361             save_action_->setEnabled(false);
362         }
363     }
364 
365     if (apply_button_) {
366         apply_button_->setEnabled(false);
367     }
368 
369     if (clear_button_) {
370         clear_button_->setVisible(!empty);
371     }
372 
373     if (empty) {
374         setFilterSyntaxState(filter, Empty, QString());
375     } else {
376         emit captureFilterChanged(filter);
377     }
378 }
379 
checkFilter()380 void CaptureFilterEdit::checkFilter()
381 {
382     checkFilter(text());
383 }
384 
updateBookmarkMenu()385 void CaptureFilterEdit::updateBookmarkMenu()
386 {
387     if (!bookmark_button_)
388         return;
389 
390     QMenu *bb_menu = bookmark_button_->menu();
391     bb_menu->clear();
392 
393     save_action_ = bb_menu->addAction(tr("Save this filter"));
394     connect(save_action_, &QAction::triggered, this, &CaptureFilterEdit::saveFilter);
395     remove_action_ = bb_menu->addAction(tr("Remove this filter"));
396     connect(remove_action_, &QAction::triggered, this, &CaptureFilterEdit::removeFilter);
397     QAction *manage_action = bb_menu->addAction(tr("Manage Capture Filters"));
398     connect(manage_action, &QAction::triggered, this, &CaptureFilterEdit::showFilters);
399     bb_menu->addSeparator();
400 
401     FilterListModel model(FilterListModel::Capture);
402     QModelIndex idx = model.findByExpression(text());
403 
404     int one_em = bb_menu->fontMetrics().height();
405 
406     if (! actions_)
407         actions_ = new QActionGroup(this);
408 
409     for (int row = 0; row < model.rowCount(); row++)
410     {
411         QModelIndex nameIdx = model.index(row, FilterListModel::ColumnName);
412         QString name = nameIdx.data().toString();
413         QString expr = model.index(row, FilterListModel::ColumnExpression).data().toString();
414         QString prep_text = QString("%1: %2").arg(name).arg(expr);
415 
416         prep_text = bb_menu->fontMetrics().elidedText(prep_text, Qt::ElideRight, one_em * 40);
417         QAction * prep_action = bb_menu->addAction(prep_text);
418         prep_action->setCheckable(true);
419         if (nameIdx == idx)
420             prep_action->setChecked(true);
421 
422         actions_->addAction(prep_action);
423         prep_action->setProperty("capture_filter", expr);
424         connect(prep_action, &QAction::triggered, this, &CaptureFilterEdit::prepareFilter);
425     }
426 
427     checkFilter();
428 }
429 
setFilterSyntaxState(QString filter,int state,QString err_msg)430 void CaptureFilterEdit::setFilterSyntaxState(QString filter, int state, QString err_msg)
431 {
432     if (filter.compare(text()) == 0) { // The user hasn't changed the filter
433         setSyntaxState((SyntaxState)state);
434         if (!err_msg.isEmpty()) {
435             wsApp->pushStatus(WiresharkApplication::FilterSyntax, err_msg);
436             setToolTip(err_msg);
437         }
438     }
439 
440     bool valid = (state != Invalid);
441 
442     if (valid) {
443         if (save_action_) {
444             save_action_->setEnabled(enable_save_action_);
445         }
446         if (apply_button_) {
447             apply_button_->setEnabled(true);
448         }
449     }
450 
451     emit captureFilterSyntaxChanged(valid);
452 }
453 
bookmarkClicked()454 void CaptureFilterEdit::bookmarkClicked()
455 {
456     emit addBookmark(text());
457 }
458 
clearFilter()459 void CaptureFilterEdit::clearFilter()
460 {
461     clear();
462     emit textEdited(text());
463 }
464 
buildCompletionList(const QString & primitive_word)465 void CaptureFilterEdit::buildCompletionList(const QString &primitive_word)
466 {
467     if (primitive_word.length() < 1) {
468         completion_model_->setStringList(QStringList());
469         return;
470     }
471 
472     // Grab matching capture filters from our parent combo and from the
473     // saved capture filters file. Skip ones that look like single fields
474     // and assume they will be added below.
475     QStringList complex_list;
476     QComboBox *cf_combo = qobject_cast<QComboBox *>(parent());
477     if (cf_combo) {
478         for (int i = 0; i < cf_combo->count() ; i++) {
479             QString recent_filter = cf_combo->itemText(i);
480 
481             if (isComplexFilter(recent_filter)) {
482                 complex_list << recent_filter;
483             }
484         }
485     }
486     FilterListModel model(FilterListModel::Capture);
487     for (int row = 0; row < model.rowCount(); row++)
488     {
489         QString saved_filter = model.index(row, FilterListModel::ColumnExpression).data().toString();
490 
491         if (isComplexFilter(saved_filter) && !complex_list.contains(saved_filter)) {
492             complex_list << saved_filter;
493         }
494     }
495 
496     // libpcap has a small number of primitives so we just add the whole list
497     // sans the current word.
498     QStringList primitive_list = libpcap_primitives_;
499     primitive_list.removeAll(primitive_word);
500 
501     completion_model_->setStringList(complex_list + primitive_list);
502     completer()->setCompletionPrefix(primitive_word);
503 }
504 
applyCaptureFilter()505 void CaptureFilterEdit::applyCaptureFilter()
506 {
507     if (syntaxState() == Invalid) {
508         return;
509     }
510 
511     emit startCapture();
512 }
513 
saveFilter()514 void CaptureFilterEdit::saveFilter()
515 {
516     FilterDialog *capture_filter_dlg = new FilterDialog(window(), FilterDialog::CaptureFilter, text());
517     capture_filter_dlg->setWindowModality(Qt::ApplicationModal);
518     capture_filter_dlg->setAttribute(Qt::WA_DeleteOnClose);
519     capture_filter_dlg->show();
520 }
521 
removeFilter()522 void CaptureFilterEdit::removeFilter()
523 {
524     if (! actions_ || ! actions_->checkedAction())
525         return;
526 
527     QAction *ra = actions_->checkedAction();
528     if (ra->property("capture_filter").toString().isEmpty())
529         return;
530 
531     QString remove_filter = ra->property("capture_filter").toString();
532 
533     FilterListModel model(FilterListModel::Capture);
534     QModelIndex idx = model.findByExpression(remove_filter);
535     if (idx.isValid())
536     {
537         model.removeFilter(idx);
538         model.saveList();
539     }
540 
541     updateBookmarkMenu();
542 }
543 
showFilters()544 void CaptureFilterEdit::showFilters()
545 {
546     FilterDialog *capture_filter_dlg = new FilterDialog(window(), FilterDialog::CaptureFilter);
547     capture_filter_dlg->setWindowModality(Qt::ApplicationModal);
548     capture_filter_dlg->setAttribute(Qt::WA_DeleteOnClose);
549     capture_filter_dlg->show();
550 }
551 
prepareFilter()552 void CaptureFilterEdit::prepareFilter()
553 {
554     QAction *pa = qobject_cast<QAction*>(sender());
555     if (! pa || pa->property("capture_filter").toString().isEmpty())
556         return;
557 
558     QString filter = pa->property("capture_filter").toString();
559     setText(filter);
560 
561     emit textEdited(filter);
562 }
563