tests: Convert ExpressionMatchTests into a GTest-based test case
[quassel.git] / src / common / highlightrulemanager.cpp
1 /***************************************************************************
2  *   Copyright (C) 2005-2018 by the Quassel Project                        *
3  *   devel@quassel-irc.org                                                 *
4  *                                                                         *
5  *   This program is free software; you can redistribute it and/or modify  *
6  *   it under the terms of the GNU General Public License as published by  *
7  *   the Free Software Foundation; either version 2 of the License, or     *
8  *   (at your option) version 3.                                           *
9  *                                                                         *
10  *   This program is distributed in the hope that it will be useful,       *
11  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
12  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
13  *   GNU General Public License for more details.                          *
14  *                                                                         *
15  *   You should have received a copy of the GNU General Public License     *
16  *   along with this program; if not, write to the                         *
17  *   Free Software Foundation, Inc.,                                       *
18  *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.         *
19  ***************************************************************************/
20
21 #include "highlightrulemanager.h"
22
23 #include <QDebug>
24
25 #include "expressionmatch.h"
26 #include "util.h"
27
28 HighlightRuleManager &HighlightRuleManager::operator=(const HighlightRuleManager &other)
29 {
30     if (this == &other)
31         return *this;
32
33     SyncableObject::operator=(other);
34     _highlightRuleList = other._highlightRuleList;
35     _nicksCaseSensitive = other._nicksCaseSensitive;
36     _highlightNick = other._highlightNick;
37     return *this;
38 }
39
40
41 int HighlightRuleManager::indexOf(int id) const
42 {
43     for (int i = 0; i < _highlightRuleList.count(); i++) {
44         if (_highlightRuleList[i].id() == id)
45             return i;
46     }
47     return -1;
48 }
49
50
51 int HighlightRuleManager::nextId()
52 {
53     int max = 0;
54     for (int i = 0; i < _highlightRuleList.count(); i++) {
55         int id = _highlightRuleList[i].id();
56         if (id > max) {
57             max = id;
58         }
59     }
60     return max + 1;
61 }
62
63
64 QVariantMap HighlightRuleManager::initHighlightRuleList() const
65 {
66     QVariantList id;
67     QVariantMap highlightRuleListMap;
68     QStringList name;
69     QVariantList isRegEx;
70     QVariantList isCaseSensitive;
71     QVariantList isActive;
72     QVariantList isInverse;
73     QStringList sender;
74     QStringList channel;
75
76     for (int i = 0; i < _highlightRuleList.count(); i++) {
77         id << _highlightRuleList[i].id();
78         name << _highlightRuleList[i].contents();
79         isRegEx << _highlightRuleList[i].isRegEx();
80         isCaseSensitive << _highlightRuleList[i].isCaseSensitive();
81         isActive << _highlightRuleList[i].isEnabled();
82         isInverse << _highlightRuleList[i].isInverse();
83         sender << _highlightRuleList[i].sender();
84         channel << _highlightRuleList[i].chanName();
85     }
86
87     highlightRuleListMap["id"] = id;
88     highlightRuleListMap["name"] = name;
89     highlightRuleListMap["isRegEx"] = isRegEx;
90     highlightRuleListMap["isCaseSensitive"] = isCaseSensitive;
91     highlightRuleListMap["isEnabled"] = isActive;
92     highlightRuleListMap["isInverse"] = isInverse;
93     highlightRuleListMap["sender"] = sender;
94     highlightRuleListMap["channel"] = channel;
95     return highlightRuleListMap;
96 }
97
98
99 void HighlightRuleManager::initSetHighlightRuleList(const QVariantMap &highlightRuleList)
100 {
101     QVariantList id = highlightRuleList["id"].toList();
102     QStringList name = highlightRuleList["name"].toStringList();
103     QVariantList isRegEx = highlightRuleList["isRegEx"].toList();
104     QVariantList isCaseSensitive = highlightRuleList["isCaseSensitive"].toList();
105     QVariantList isActive = highlightRuleList["isEnabled"].toList();
106     QVariantList isInverse = highlightRuleList["isInverse"].toList();
107     QStringList sender = highlightRuleList["sender"].toStringList();
108     QStringList channel = highlightRuleList["channel"].toStringList();
109
110     int count = id.count();
111     if (count != name.count() || count != isRegEx.count() || count != isCaseSensitive.count() ||
112         count != isActive.count() || count != isInverse.count() || count != sender.count() ||
113         count != channel.count()) {
114         qWarning() << "Corrupted HighlightRuleList settings! (Count mismatch)";
115         return;
116     }
117
118     _highlightRuleList.clear();
119     for (int i = 0; i < name.count(); i++) {
120         _highlightRuleList << HighlightRule(id[i].toInt(), name[i], isRegEx[i].toBool(), isCaseSensitive[i].toBool(),
121                                             isActive[i].toBool(), isInverse[i].toBool(), sender[i], channel[i]);
122     }
123 }
124
125
126 void HighlightRuleManager::addHighlightRule(int id, const QString &name, bool isRegEx, bool isCaseSensitive,
127                                             bool isActive, bool isInverse, const QString &sender,
128                                             const QString &channel)
129 {
130     if (contains(id)) {
131         return;
132     }
133
134     HighlightRule newItem = HighlightRule(id, name, isRegEx, isCaseSensitive, isActive, isInverse, sender, channel);
135     _highlightRuleList << newItem;
136
137     SYNC(ARG(id), ARG(name), ARG(isRegEx), ARG(isCaseSensitive), ARG(isActive), ARG(isInverse), ARG(sender),
138          ARG(channel))
139 }
140
141
142 bool HighlightRuleManager::match(const NetworkId &netId,
143                                  const QString &msgContents,
144                                  const QString &msgSender,
145                                  Message::Type msgType,
146                                  Message::Flags msgFlags,
147                                  const QString &bufferName,
148                                  const QString &currentNick,
149                                  const QStringList &identityNicks)
150 {
151     if (!((msgType & (Message::Plain | Message::Notice | Message::Action)) && !(msgFlags & Message::Self))) {
152        return false;
153     }
154
155     bool matches = false;
156
157     for (int i = 0; i < _highlightRuleList.count(); i++) {
158         auto &rule = _highlightRuleList.at(i);
159         if (!rule.isEnabled())
160             continue;
161
162         // Skip if channel name doesn't match and channel rule is not empty
163         //
164         // Match succeeds if...
165         //   Channel name matches a defined rule
166         //   Defined rule is empty
167         // And take the inverse of the above
168         if (!rule.chanNameMatcher().match(bufferName, true)) {
169             // A channel name rule is specified and does NOT match the current buffer name, skip
170             // this rule
171             continue;
172         }
173
174         // Check message according to specified rule, allowing empty rules to match
175         bool contentsMatch = rule.contentsMatcher().match(stripFormatCodes(msgContents), true);
176
177         // Check sender according to specified rule, allowing empty rules to match
178         bool senderMatch = rule.senderMatcher().match(msgSender, true);
179
180         if (contentsMatch && senderMatch) {
181             // If an inverse rule matches, then we know that we never want to return a highlight.
182             if (rule.isInverse()) {
183                 return false;
184             }
185             else {
186                 matches = true;
187             }
188         }
189     }
190
191     if (matches)
192         return true;
193
194     // Check nicknames
195     if (_highlightNick != HighlightNickType::NoNick && !currentNick.isEmpty()) {
196         // Nickname matching allowed and current nickname is known
197         // Run the nickname matcher on the unformatted string
198         if (_nickMatcher.match(stripFormatCodes(msgContents), netId, currentNick, identityNicks)) {
199             return true;
200         }
201     }
202
203     return false;
204 }
205
206
207 void HighlightRuleManager::removeHighlightRule(int highlightRule)
208 {
209     removeAt(indexOf(highlightRule));
210     SYNC(ARG(highlightRule))
211 }
212
213
214 void HighlightRuleManager::toggleHighlightRule(int highlightRule)
215 {
216     int idx = indexOf(highlightRule);
217     if (idx == -1)
218         return;
219     _highlightRuleList[idx].setIsEnabled(!_highlightRuleList[idx].isEnabled());
220     SYNC(ARG(highlightRule))
221 }
222
223
224 bool HighlightRuleManager::match(const Message &msg, const QString &currentNick, const QStringList &identityNicks)
225 {
226     return match(msg.bufferInfo().networkId(), msg.contents(), msg.sender(), msg.type(), msg.flags(),
227                  msg.bufferInfo().bufferName(), currentNick, identityNicks);
228 }
229
230
231 /**************************************************************************
232  * HighlightRule
233  *************************************************************************/
234 bool HighlightRuleManager::HighlightRule::operator!=(const HighlightRule &other) const
235 {
236     return (_id != other._id ||
237             _contents != other._contents ||
238             _isRegEx != other._isRegEx ||
239             _isCaseSensitive != other._isCaseSensitive ||
240             _isEnabled != other._isEnabled ||
241             _isInverse != other._isInverse ||
242             _sender != other._sender ||
243             _chanName != other._chanName);
244     // Don't compare ExpressionMatch objects as they are created as needed from the above
245 }
246
247
248 void HighlightRuleManager::HighlightRule::determineExpressions() const
249 {
250     // Don't update if not needed
251     if (!_cacheInvalid) {
252         return;
253     }
254
255     // Set up matching rules
256     // Message is either phrase or regex
257     ExpressionMatch::MatchMode contentsMode =
258             _isRegEx ? ExpressionMatch::MatchMode::MatchRegEx :
259                        ExpressionMatch::MatchMode::MatchPhrase;
260     // Sender and channel are either multiple wildcard entries or regex
261     ExpressionMatch::MatchMode scopeMode =
262             _isRegEx ? ExpressionMatch::MatchMode::MatchRegEx :
263                        ExpressionMatch::MatchMode::MatchMultiWildcard;
264
265     _contentsMatch = ExpressionMatch(_contents, contentsMode, _isCaseSensitive);
266     _senderMatch = ExpressionMatch(_sender, scopeMode, _isCaseSensitive);
267     _chanNameMatch = ExpressionMatch(_chanName, scopeMode, _isCaseSensitive);
268
269     _cacheInvalid = false;
270 }