replace SPDX identifier GPL-2.0+ with GPL-2.0-or-later.
[metze/wireshark/wip.git] / ui / qt / widgets / capture_filter_edit.cpp
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 #include "config.h"
10
11 #include <glib.h>
12
13 #include <epan/proto.h>
14
15 #include "capture_opts.h"
16
17 #include <ui/capture_globals.h>
18 #include <ui/filter_files.h>
19 #include <wsutil/utf8_entities.h>
20
21 #include <ui/qt/widgets/capture_filter_edit.h>
22 #include "capture_filter_syntax_worker.h"
23 #include "filter_dialog.h"
24 #include <ui/qt/widgets/stock_icon_tool_button.h>
25 #include "wireshark_application.h"
26
27 #include <QComboBox>
28 #include <QCompleter>
29 #include <QMenu>
30 #include <QMessageBox>
31 #include <QPainter>
32 #include <QStringListModel>
33 #include <QStyleOptionFrame>
34
35 #include <ui/qt/utils/qt_ui_utils.h>
36
37 // To do:
38 // - This duplicates some DisplayFilterEdit code.
39 // - We need simplified (button- and dropdown-free) versions for use in dialogs and field-only checking.
40
41 static const QString libpcap_primitive_chars_ = "-0123456789abcdefghijklmnopqrstuvwxyz";
42
43 // grep '^[a-z].*return [A-Z].*;$' scanner.l | awk '{gsub(/\|/, "\n") ; print "    << \"" $1 "\""}' | sort
44 // Remove "and" and "or".
45 static const QStringList libpcap_primitives_ = QStringList()
46         << "aarp" << "action" << "address1" << "address2" << "address3" << "address4"
47         << "ah" << "arp" << "atalk" << "bcc" << "broadcast" << "byte" << "carp"
48         << "clnp" << "connectmsg" << "csnp" << "decnet" << "direction" << "dpc"
49         << "dst" << "es-is" << "esis" << "esp" << "fddi" << "fisu" << "gateway"
50         << "greater" << "hdpc" << "hfisu" << "hlssu" << "hmsu" << "hopc" << "host"
51         << "hsio" << "hsls" << "icmp" << "icmp6" << "igmp" << "igrp" << "iih" << "ilmic"
52         << "inbound" << "ip" << "ip6" << "ipx" << "is-is" << "isis" << "iso" << "l1"
53         << "l2" << "lane" << "lat" << "len" << "less" << "link" << "llc" << "lsp"
54         << "lssu" << "lsu" << "mask" << "metac" << "metaconnect" << "mopdl" << "moprc"
55         << "mpls" << "msu" << "multicast" << "net" << "netbeui" << "oam" << "oamf4"
56         << "oamf4ec" << "oamf4sc" << "on" << "opc" << "outbound" << "pim"
57         << "port" << "portrange" << "pppoed" << "pppoes" << "proto" << "psnp" << "ra"
58         << "radio" << "rarp" << "reason" << "rnr" << "rset" << "sc" << "sca" << "sctp"
59         << "sio" << "sls" << "snp" << "src" << "srnr" << "stp" << "subtype" << "ta"
60         << "tcp" << "type" << "udp" << "vci" << "vlan" << "vpi" << "vrrp"
61         ;
62
63 CaptureFilterEdit::CaptureFilterEdit(QWidget *parent, bool plain) :
64     SyntaxLineEdit(parent),
65     plain_(plain),
66     field_name_only_(false),
67     enable_save_action_(false),
68     save_action_(NULL),
69     remove_action_(NULL),
70     bookmark_button_(NULL),
71     clear_button_(NULL),
72     apply_button_(NULL)
73 {
74     setAccessibleName(tr("Capture filter entry"));
75
76     completion_model_ = new QStringListModel(this);
77     setCompleter(new QCompleter(completion_model_, this));
78     setCompletionTokenChars(libpcap_primitive_chars_);
79
80     setConflict(false);
81
82     if (!plain_) {
83         bookmark_button_ = new StockIconToolButton(this, "x-capture-filter-bookmark");
84         bookmark_button_->setCursor(Qt::ArrowCursor);
85         bookmark_button_->setMenu(new QMenu(bookmark_button_));
86         bookmark_button_->setPopupMode(QToolButton::InstantPopup);
87         bookmark_button_->setToolTip(tr("Manage saved bookmarks."));
88         bookmark_button_->setIconSize(QSize(14, 14));
89         bookmark_button_->setStyleSheet(
90                     "QToolButton {"
91                     "  border: none;"
92                     "  background: transparent;" // Disables platform style on Windows.
93                     "  padding: 0 0 0 0;"
94                     "}"
95                     "QToolButton::menu-indicator { image: none; }"
96             );
97         connect(bookmark_button_, SIGNAL(clicked()), this, SLOT(bookmarkClicked()));
98     }
99
100     if (!plain_) {
101         clear_button_ = new StockIconToolButton(this, "x-filter-clear");
102         clear_button_->setCursor(Qt::ArrowCursor);
103         clear_button_->setToolTip(QString());
104         clear_button_->setIconSize(QSize(14, 14));
105         clear_button_->setStyleSheet(
106                 "QToolButton {"
107                 "  border: none;"
108                 "  background: transparent;" // Disables platform style on Windows.
109                 "  padding: 0 0 0 0;"
110                 "  margin-left: 1px;"
111                 "}"
112                 );
113         connect(clear_button_, SIGNAL(clicked()), this, SLOT(clearFilter()));
114     }
115
116     connect(this, SIGNAL(textChanged(const QString&)), this, SLOT(checkFilter(const QString&)));
117
118 #if 0
119     // Disable the apply button for now
120     if (!plain_) {
121         apply_button_ = new StockIconToolButton(this, "x-filter-apply");
122         apply_button_->setCursor(Qt::ArrowCursor);
123         apply_button_->setEnabled(false);
124         apply_button_->setToolTip(tr("Apply this filter string to the display."));
125         apply_button_->setIconSize(QSize(24, 14));
126         apply_button_->setStyleSheet(
127                 "QToolButton {"
128                 "  border: none;"
129                 "  background: transparent;" // Disables platform style on Windows.
130                 "  padding: 0 0 0 0;"
131                 "}"
132                 );
133         connect(apply_button_, SIGNAL(clicked()), this, SLOT(applyCaptureFilter()));
134     }
135 #endif
136     connect(this, SIGNAL(returnPressed()), this, SLOT(applyCaptureFilter()));
137
138     int frameWidth = style()->pixelMetric(QStyle::PM_DefaultFrameWidth);
139     QSize bksz;
140     if (bookmark_button_) bksz = bookmark_button_->sizeHint();
141     QSize cbsz;
142     if (clear_button_) cbsz = clear_button_->sizeHint();
143     QSize apsz;
144     if (apply_button_) apsz = apply_button_->sizeHint();
145
146     setStyleSheet(QString(
147             "CaptureFilterEdit {"
148             "  padding-left: %1px;"
149             "  margin-left: %2px;"
150             "  margin-right: %3px;"
151             "}"
152             )
153             .arg(frameWidth + 1)
154             .arg(bksz.width())
155             .arg(cbsz.width() + apsz.width() + frameWidth + 1)
156             );
157
158     QComboBox *cf_combo = qobject_cast<QComboBox *>(parent);
159     if (cf_combo) {
160         connect(cf_combo, SIGNAL(activated(QString)), this, SIGNAL(textEdited(QString)));
161     }
162
163     QThread *syntax_thread = new QThread;
164     syntax_worker_ = new CaptureFilterSyntaxWorker;
165     syntax_worker_->moveToThread(syntax_thread);
166     connect(wsApp, SIGNAL(appInitialized()), this, SLOT(updateBookmarkMenu()));
167     connect(wsApp, SIGNAL(captureFilterListChanged()), this, SLOT(updateBookmarkMenu()));
168     connect(syntax_thread, SIGNAL(started()), syntax_worker_, SLOT(start()));
169     connect(syntax_thread, SIGNAL(started()), this, SLOT(checkFilter()));
170     connect(syntax_worker_, SIGNAL(syntaxResult(QString,int,QString)),
171             this, SLOT(setFilterSyntaxState(QString,int,QString)));
172     connect(syntax_thread, SIGNAL(finished()), syntax_worker_, SLOT(deleteLater()));
173     syntax_thread->start();
174     updateBookmarkMenu();
175 }
176
177 void CaptureFilterEdit::paintEvent(QPaintEvent *evt) {
178     SyntaxLineEdit::paintEvent(evt);
179
180     if (bookmark_button_) {
181         // Draw the right border by hand. We could try to do this in the
182         // style sheet but it's a pain.
183 #ifdef Q_OS_MAC
184         QColor divider_color = Qt::gray;
185 #else
186         QColor divider_color = palette().shadow().color();
187 #endif
188         QPainter painter(this);
189         painter.setPen(divider_color);
190         QRect cr = contentsRect();
191         QSize bksz = bookmark_button_->size();
192         painter.drawLine(bksz.width(), cr.top(), bksz.width(), cr.bottom());
193     }
194 }
195
196 void CaptureFilterEdit::resizeEvent(QResizeEvent *)
197 {
198     QSize cbsz;
199     if (clear_button_) cbsz = clear_button_->sizeHint();
200     QSize apsz;
201     if (apply_button_) apsz = apply_button_->sizeHint();
202
203     int frameWidth = style()->pixelMetric(QStyle::PM_DefaultFrameWidth);
204     if (clear_button_) {
205         clear_button_->move(contentsRect().right() - frameWidth - cbsz.width() - apsz.width(),
206                             contentsRect().top());
207         clear_button_->setMinimumHeight(contentsRect().height());
208         clear_button_->setMaximumHeight(contentsRect().height());
209     }
210     if (apply_button_) {
211         apply_button_->move(contentsRect().right() - frameWidth - apsz.width(),
212                             contentsRect().top());
213         apply_button_->setMinimumHeight(contentsRect().height());
214         apply_button_->setMaximumHeight(contentsRect().height());
215     }
216     if (bookmark_button_) {
217         bookmark_button_->setMinimumHeight(contentsRect().height());
218         bookmark_button_->setMaximumHeight(contentsRect().height());
219     }
220 }
221
222 void CaptureFilterEdit::setConflict(bool conflict)
223 {
224     if (conflict) {
225         //: This is a very long concept that needs to fit into a short space.
226         placeholder_text_ = tr("Multiple filters selected. Override them here or leave this blank to preserve them.");
227         setToolTip(tr("<p>The interfaces you have selected have different capture filters."
228                       " Typing a filter here will override them. Doing nothing will"
229                       " preserve them.</p>"));
230     } else {
231         placeholder_text_ = QString(tr("Enter a capture filter %1")).arg(UTF8_HORIZONTAL_ELLIPSIS);
232         setToolTip(QString());
233     }
234     setPlaceholderText(placeholder_text_);
235 }
236
237 // XXX Make this private along with setConflict.
238 QPair<const QString, bool> CaptureFilterEdit::getSelectedFilter()
239 {
240     QString user_filter;
241     bool filter_conflict = false;
242 #ifdef HAVE_LIBPCAP
243     int selected_devices = 0;
244
245     for (guint i = 0; i < global_capture_opts.all_ifaces->len; i++) {
246         interface_t *device = &g_array_index(global_capture_opts.all_ifaces, interface_t, i);
247         if (device->selected) {
248             selected_devices++;
249             if (selected_devices == 1) {
250                 user_filter = device->cfilter;
251             } else {
252                 if (user_filter.compare(device->cfilter)) {
253                     filter_conflict = true;
254                 }
255             }
256         }
257     }
258 #endif // HAVE_LIBPCAP
259     return QPair<const QString, bool>(user_filter, filter_conflict);
260 }
261
262 void CaptureFilterEdit::checkFilter(const QString& filter)
263 {
264     setSyntaxState(Busy);
265     popFilterSyntaxStatus();
266     bool empty = filter.isEmpty();
267
268     setConflict(false);
269     if (bookmark_button_) {
270         bool match = false;
271
272         for (GList *cf_item = get_filter_list_first(CFILTER_LIST); cf_item; cf_item = g_list_next(cf_item)) {
273             if (!cf_item->data) continue;
274             filter_def *cf_def = (filter_def *) cf_item->data;
275             if (!cf_def->name || !cf_def->strval) continue;
276
277             if (filter.compare(cf_def->strval) == 0) {
278                 match = true;
279             }
280         }
281
282         if (match) {
283             bookmark_button_->setStockIcon("x-filter-matching-bookmark");
284             if (remove_action_) {
285                 remove_action_->setData(text());
286                 remove_action_->setVisible(true);
287             }
288         } else {
289             bookmark_button_->setStockIcon("x-capture-filter-bookmark");
290             if (remove_action_) {
291                 remove_action_->setVisible(false);
292             }
293         }
294
295         enable_save_action_ = (!match && !filter.isEmpty());
296         if (save_action_) {
297             save_action_->setEnabled(false);
298         }
299     }
300
301     if (apply_button_) {
302         apply_button_->setEnabled(false);
303     }
304
305     if (clear_button_) {
306         clear_button_->setVisible(!empty);
307     }
308
309     if (empty) {
310         setFilterSyntaxState(filter, Empty, QString());
311     } else {
312         syntax_worker_->checkFilter(filter);
313     }
314 }
315
316 void CaptureFilterEdit::checkFilter()
317 {
318     checkFilter(text());
319 }
320
321 void CaptureFilterEdit::updateBookmarkMenu()
322 {
323     if (!bookmark_button_)
324         return;
325
326     QMenu *bb_menu = bookmark_button_->menu();
327     bb_menu->clear();
328
329     save_action_ = bb_menu->addAction(tr("Save this filter"));
330     connect(save_action_, SIGNAL(triggered(bool)), this, SLOT(saveFilter()));
331     remove_action_ = bb_menu->addAction(tr("Remove this filter"));
332     connect(remove_action_, SIGNAL(triggered(bool)), this, SLOT(removeFilter()));
333     QAction *manage_action = bb_menu->addAction(tr("Manage Capture Filters"));
334     connect(manage_action, SIGNAL(triggered(bool)), this, SLOT(showFilters()));
335     bb_menu->addSeparator();
336
337     for (GList *cf_item = get_filter_list_first(CFILTER_LIST); cf_item; cf_item = g_list_next(cf_item)) {
338         if (!cf_item->data) continue;
339         filter_def *cf_def = (filter_def *) cf_item->data;
340         if (!cf_def->name || !cf_def->strval) continue;
341
342         int one_em = bb_menu->fontMetrics().height();
343         QString prep_text = QString("%1: %2").arg(cf_def->name).arg(cf_def->strval);
344         prep_text = bb_menu->fontMetrics().elidedText(prep_text, Qt::ElideRight, one_em * 40);
345
346         QAction *prep_action = bb_menu->addAction(prep_text);
347         prep_action->setData(cf_def->strval);
348         connect(prep_action, SIGNAL(triggered(bool)), this, SLOT(prepareFilter()));
349     }
350
351     checkFilter();
352 }
353
354 void CaptureFilterEdit::setFilterSyntaxState(QString filter, int state, QString err_msg)
355 {
356     if (filter.compare(text()) == 0) { // The user hasn't changed the filter
357         setSyntaxState((SyntaxState)state);
358         if (!err_msg.isEmpty()) {
359             emit pushFilterSyntaxStatus(err_msg);
360         }
361     }
362
363     bool valid = (state != Invalid);
364
365     if (valid) {
366         if (save_action_) {
367             save_action_->setEnabled(enable_save_action_);
368         }
369         if (apply_button_) {
370             apply_button_->setEnabled(true);
371         }
372     }
373
374     emit captureFilterSyntaxChanged(valid);
375 }
376
377 void CaptureFilterEdit::bookmarkClicked()
378 {
379     emit addBookmark(text());
380 }
381
382 void CaptureFilterEdit::clearFilter()
383 {
384     clear();
385     emit textEdited(text());
386 }
387
388 void CaptureFilterEdit::buildCompletionList(const QString &primitive_word)
389 {
390     if (primitive_word.length() < 1) {
391         completion_model_->setStringList(QStringList());
392         return;
393     }
394
395     // Grab matching capture filters from our parent combo and from the
396     // saved capture filters file. Skip ones that look like single fields
397     // and assume they will be added below.
398     QStringList complex_list;
399     QComboBox *cf_combo = qobject_cast<QComboBox *>(parent());
400     if (cf_combo) {
401         for (int i = 0; i < cf_combo->count() ; i++) {
402             QString recent_filter = cf_combo->itemText(i);
403
404             if (isComplexFilter(recent_filter)) {
405                 complex_list << recent_filter;
406             }
407         }
408     }
409     for (const GList *cf_item = get_filter_list_first(CFILTER_LIST); cf_item; cf_item = g_list_next(cf_item)) {
410         const filter_def *cf_def = (filter_def *) cf_item->data;
411         if (!cf_def || !cf_def->strval) continue;
412         QString saved_filter = cf_def->strval;
413
414         if (isComplexFilter(saved_filter) && !complex_list.contains(saved_filter)) {
415             complex_list << saved_filter;
416         }
417     }
418
419     // libpcap has a small number of primitives so we just add the whole list
420     // sans the current word.
421     QStringList primitive_list = libpcap_primitives_;
422     primitive_list.removeAll(primitive_word);
423
424     completion_model_->setStringList(complex_list + primitive_list);
425     completer()->setCompletionPrefix(primitive_word);
426 }
427
428 void CaptureFilterEdit::applyCaptureFilter()
429 {
430     if (syntaxState() == Invalid) {
431         return;
432     }
433
434     emit startCapture();
435 }
436
437 void CaptureFilterEdit::saveFilter()
438 {
439     FilterDialog capture_filter_dlg(window(), FilterDialog::CaptureFilter, text());
440     capture_filter_dlg.exec();
441 }
442
443 void CaptureFilterEdit::removeFilter()
444 {
445     QAction *ra = qobject_cast<QAction*>(sender());
446     if (!ra || ra->data().toString().isEmpty()) return;
447
448     QString remove_filter = ra->data().toString();
449
450     for (GList *cf_item = get_filter_list_first(CFILTER_LIST); cf_item; cf_item = g_list_next(cf_item)) {
451         if (!cf_item->data) continue;
452         filter_def *cf_def = (filter_def *) cf_item->data;
453         if (!cf_def->name || !cf_def->strval) continue;
454
455         if (remove_filter.compare(cf_def->strval) == 0) {
456             remove_from_filter_list(CFILTER_LIST, cf_item);
457         }
458     }
459
460     save_filter_list(CFILTER_LIST);
461
462     updateBookmarkMenu();
463 }
464
465 void CaptureFilterEdit::showFilters()
466 {
467     FilterDialog capture_filter_dlg(window(), FilterDialog::CaptureFilter);
468     capture_filter_dlg.exec();
469 }
470
471 void CaptureFilterEdit::prepareFilter()
472 {
473     QAction *pa = qobject_cast<QAction*>(sender());
474     if (!pa || pa->data().toString().isEmpty()) return;
475
476     QString filter(pa->data().toString());
477     setText(filter);
478     emit textEdited(filter);
479 }
480
481 /*
482  * Editor modelines
483  *
484  * Local Variables:
485  * c-basic-offset: 4
486  * tab-width: 8
487  * indent-tabs-mode: nil
488  * End:
489  *
490  * ex: set shiftwidth=4 tabstop=8 expandtab:
491  * :indentSize=4:tabSize=8:noTabs=true:
492  */