cmake: avoid de-duplication of user's CXXFLAGS
[quassel.git] / src / core / ctcpparser.cpp
1 /***************************************************************************
2  *   Copyright (C) 2005-2022 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 "ctcpparser.h"
22
23 #include "corenetworkconfig.h"
24 #include "coresession.h"
25 #include "coreuserinputhandler.h"
26 #include "ctcpevent.h"
27 #include "irctags.h"
28 #include "messageevent.h"
29
30 const QByteArray XDELIM = "\001";
31
32 CtcpParser::CtcpParser(CoreSession* coreSession, QObject* parent)
33     : QObject(parent)
34     , _coreSession(coreSession)
35 {
36     QByteArray MQUOTE = QByteArray("\020");
37     _ctcpMDequoteHash[MQUOTE + '0'] = QByteArray(1, '\000');
38     _ctcpMDequoteHash[MQUOTE + 'n'] = QByteArray(1, '\n');
39     _ctcpMDequoteHash[MQUOTE + 'r'] = QByteArray(1, '\r');
40     _ctcpMDequoteHash[MQUOTE + MQUOTE] = MQUOTE;
41
42     setStandardCtcp(_coreSession->networkConfig()->standardCtcp());
43
44     connect(_coreSession->networkConfig(), &NetworkConfig::standardCtcpSet, this, &CtcpParser::setStandardCtcp);
45     connect(this, &CtcpParser::newEvent, _coreSession->eventManager(), &EventManager::postEvent);
46 }
47
48 void CtcpParser::setStandardCtcp(bool enabled)
49 {
50     QByteArray XQUOTE = QByteArray(R"(\)");
51     if (enabled)
52         _ctcpXDelimDequoteHash[XQUOTE + XQUOTE] = XQUOTE;
53     else
54         _ctcpXDelimDequoteHash.remove(XQUOTE + XQUOTE);
55     _ctcpXDelimDequoteHash[XQUOTE + QByteArray("a")] = XDELIM;
56 }
57
58 void CtcpParser::displayMsg(NetworkEvent* event,
59                             Message::Type msgType,
60                             QString msg,
61                             QString sender,
62                             QString target,
63                             Message::Flags msgFlags)
64 {
65     if (event->testFlag(EventManager::Silent))
66         return;
67
68     MessageEvent* msgEvent = new MessageEvent(msgType, event->network(), std::move(msg), std::move(sender), std::move(target), msgFlags, event->timestamp());
69     if (event->testFlag(EventManager::Self)) {
70         msgEvent->setFlag(EventManager::Self);
71     }
72     emit newEvent(msgEvent);
73 }
74
75 QByteArray CtcpParser::lowLevelQuote(const QByteArray& message)
76 {
77     QByteArray quotedMessage = message;
78
79     QHash<QByteArray, QByteArray> quoteHash = _ctcpMDequoteHash;
80     QByteArray MQUOTE = QByteArray("\020");
81     quoteHash.remove(MQUOTE + MQUOTE);
82     quotedMessage.replace(MQUOTE, MQUOTE + MQUOTE);
83
84     QHash<QByteArray, QByteArray>::const_iterator quoteIter = quoteHash.constBegin();
85     while (quoteIter != quoteHash.constEnd()) {
86         quotedMessage.replace(quoteIter.value(), quoteIter.key());
87         ++quoteIter;
88     }
89     return quotedMessage;
90 }
91
92 QByteArray CtcpParser::lowLevelDequote(const QByteArray& message)
93 {
94     QByteArray dequotedMessage;
95     QByteArray messagepart;
96     QHash<QByteArray, QByteArray>::iterator ctcpquote;
97
98     // copy dequote Message
99     for (int i = 0; i < message.size(); i++) {
100         messagepart = message.mid(i, 1);
101         if (i + 1 < message.size()) {
102             for (ctcpquote = _ctcpMDequoteHash.begin(); ctcpquote != _ctcpMDequoteHash.end(); ++ctcpquote) {
103                 if (message.mid(i, 2) == ctcpquote.key()) {
104                     messagepart = ctcpquote.value();
105                     ++i;
106                     break;
107                 }
108             }
109         }
110         dequotedMessage += messagepart;
111     }
112     return dequotedMessage;
113 }
114
115 QByteArray CtcpParser::xdelimQuote(const QByteArray& message)
116 {
117     QByteArray quotedMessage = message;
118     QHash<QByteArray, QByteArray>::const_iterator quoteIter = _ctcpXDelimDequoteHash.constBegin();
119     while (quoteIter != _ctcpXDelimDequoteHash.constEnd()) {
120         quotedMessage.replace(quoteIter.value(), quoteIter.key());
121         ++quoteIter;
122     }
123     return quotedMessage;
124 }
125
126 QByteArray CtcpParser::xdelimDequote(const QByteArray& message)
127 {
128     QByteArray dequotedMessage;
129     QByteArray messagepart;
130     QHash<QByteArray, QByteArray>::iterator xdelimquote;
131
132     for (int i = 0; i < message.size(); i++) {
133         messagepart = message.mid(i, 1);
134         if (i + 1 < message.size()) {
135             for (xdelimquote = _ctcpXDelimDequoteHash.begin(); xdelimquote != _ctcpXDelimDequoteHash.end(); ++xdelimquote) {
136                 if (message.mid(i, 2) == xdelimquote.key()) {
137                     messagepart = xdelimquote.value();
138                     i++;
139                     break;
140                 }
141             }
142         }
143         dequotedMessage += messagepart;
144     }
145     return dequotedMessage;
146 }
147
148 void CtcpParser::processIrcEventRawNotice(IrcEventRawMessage* event)
149 {
150     parse(event, Message::Notice);
151 }
152
153 void CtcpParser::processIrcEventRawPrivmsg(IrcEventRawMessage* event)
154 {
155     parse(event, Message::Plain);
156 }
157
158 void CtcpParser::parse(IrcEventRawMessage* e, Message::Type messagetype)
159 {
160     // lowlevel message dequote
161     QByteArray dequotedMessage = lowLevelDequote(e->rawMessage());
162
163     CtcpEvent::CtcpType ctcptype = e->type() == EventManager::IrcEventRawNotice ? CtcpEvent::Reply : CtcpEvent::Query;
164
165     Message::Flags flags = (ctcptype == CtcpEvent::Reply && !e->network()->isChannelName(e->target())) ? Message::Redirected : Message::None;
166
167     bool isStatusMsg = false;
168
169     // First remove all statusmsg prefix characters that are not also channel prefix characters.
170     while (e->network()->isStatusMsg(e->target()) && !e->network()->isChannelName(e->target())) {
171         isStatusMsg = true;
172         e->setTarget(e->target().remove(0, 1));
173     }
174
175     // Then continue removing statusmsg characters as long as removing the character will still result in a
176     // valid channel name.  This prevents removing the channel prefix character if said character is in the
177     // overlap between the statusmsg characters and the channel prefix characters.
178     while (e->network()->isStatusMsg(e->target()) && e->network()->isChannelName(e->target().remove(0, 1))) {
179         isStatusMsg = true;
180         e->setTarget(e->target().remove(0, 1));
181     }
182
183     // If any statusmsg characters were removed, Flag the message as a StatusMsg.
184     if (isStatusMsg) {
185         flags |= Message::StatusMsg;
186     }
187
188     // For self-messages, pass the flag on to the message, too
189     if (e->testFlag(EventManager::Self)) {
190         flags |= Message::Self;
191     }
192
193     if (coreSession()->networkConfig()->standardCtcp())
194         parseStandard(e, messagetype, dequotedMessage, ctcptype, flags);
195     else
196         parseSimple(e, messagetype, dequotedMessage, ctcptype, flags);
197 }
198
199 // only accept CTCPs in their simplest form, i.e. one ctcp, from start to
200 // end, no text around it; not as per the 'specs', but makes people happier
201 void CtcpParser::parseSimple(IrcEventRawMessage* e,
202                              Message::Type messagetype,
203                              const QByteArray& dequotedMessage,
204                              CtcpEvent::CtcpType ctcptype,
205                              Message::Flags flags)
206 {
207     if (dequotedMessage.count(XDELIM) != 2 || dequotedMessage[0] != '\001' || dequotedMessage[dequotedMessage.count() - 1] != '\001') {
208         displayMsg(e, messagetype, targetDecode(e, dequotedMessage), e->prefix(), e->target(), flags);
209     }
210     else {
211         int spacePos;
212         QString ctcpcmd, ctcpparam;
213
214         QByteArray ctcp = xdelimDequote(dequotedMessage.mid(1, dequotedMessage.count() - 2));
215         spacePos = ctcp.indexOf(' ');
216         if (spacePos != -1) {
217             ctcpcmd = targetDecode(e, ctcp.left(spacePos));
218             ctcpparam = targetDecode(e, ctcp.mid(spacePos + 1));
219         }
220         else {
221             ctcpcmd = targetDecode(e, ctcp);
222             ctcpparam = QString();
223         }
224         ctcpcmd = ctcpcmd.toUpper();
225
226         bool isAction = ctcpcmd == QLatin1String("ACTION");
227         // we don't want to block /me messages by the CTCP ignore list
228         if (isAction
229             || !coreSession()->ignoreListManager()->ctcpMatch(e->prefix(), e->network()->networkName(), ctcpcmd)) {
230             QUuid uuid = QUuid::createUuid();
231             _replies.insert(uuid, CtcpReply(coreNetwork(e), nickFromMask(e->prefix())));
232             CtcpEvent* event = new CtcpEvent(EventManager::CtcpEvent,
233                                              e->network(),
234                                              isAction ? QHash<IrcTagKey, QString>() : e->tags(),
235                                              e->prefix(),
236                                              e->target(),
237                                              ctcptype,
238                                              ctcpcmd,
239                                              ctcpparam,
240                                              e->timestamp(),
241                                              uuid);
242             if (e->testFlag(EventManager::Self)) {
243                 event->setFlag(EventManager::Self);
244             }
245             emit newEvent(event);
246             CtcpEvent* flushEvent = new CtcpEvent(EventManager::CtcpEventFlush,
247                                                   e->network(),
248                                                   {},
249                                                   e->prefix(),
250                                                   e->target(),
251                                                   ctcptype,
252                                                   "INVALID",
253                                                   QString(),
254                                                   e->timestamp(),
255                                                   uuid);
256             emit newEvent(flushEvent);
257         }
258     }
259 }
260
261 void CtcpParser::parseStandard(IrcEventRawMessage* e,
262                                Message::Type messagetype,
263                                const QByteArray& dequotedMessage_,
264                                CtcpEvent::CtcpType ctcptype,
265                                Message::Flags flags)
266 {
267     auto dequotedMessage = dequotedMessage_;
268     QByteArray ctcp;
269
270     QList<CtcpEvent*> ctcpEvents;
271     QUuid uuid;  // needed to group all replies together
272
273     // extract tagged / extended data
274     int xdelimPos = -1;
275     int xdelimEndPos = -1;
276     int spacePos = -1;
277     while ((xdelimPos = dequotedMessage.indexOf(XDELIM)) != -1) {
278         if (xdelimPos > 0)
279             displayMsg(e, messagetype, targetDecode(e, dequotedMessage.left(xdelimPos)), e->prefix(), e->target(), flags);
280
281         xdelimEndPos = dequotedMessage.indexOf(XDELIM, xdelimPos + 1);
282         if (xdelimEndPos == -1) {
283             // no matching end delimiter found... treat rest of the message as ctcp
284             xdelimEndPos = dequotedMessage.count();
285         }
286         ctcp = xdelimDequote(dequotedMessage.mid(xdelimPos + 1, xdelimEndPos - xdelimPos - 1));
287         dequotedMessage = dequotedMessage.mid(xdelimEndPos + 1);
288
289         // dispatch the ctcp command
290         QString ctcpcmd = targetDecode(e, ctcp.left(spacePos));
291         QString ctcpparam = targetDecode(e, ctcp.mid(spacePos + 1));
292
293         spacePos = ctcp.indexOf(' ');
294         if (spacePos != -1) {
295             ctcpcmd = targetDecode(e, ctcp.left(spacePos));
296             ctcpparam = targetDecode(e, ctcp.mid(spacePos + 1));
297         }
298         else {
299             ctcpcmd = targetDecode(e, ctcp);
300             ctcpparam = QString();
301         }
302
303         ctcpcmd = ctcpcmd.toUpper();
304
305         bool isAction = ctcpcmd == QLatin1String("ACTION");
306         // we don't want to block /me messages by the CTCP ignore list
307         if (isAction
308             || !coreSession()->ignoreListManager()->ctcpMatch(e->prefix(), e->network()->networkName(), ctcpcmd)) {
309             if (uuid.isNull())
310                 uuid = QUuid::createUuid();
311
312             CtcpEvent* event = new CtcpEvent(EventManager::CtcpEvent,
313                                              e->network(),
314                                              isAction ? QHash<IrcTagKey, QString>() : e->tags(),
315                                              e->prefix(),
316                                              e->target(),
317                                              ctcptype,
318                                              ctcpcmd,
319                                              ctcpparam,
320                                              e->timestamp(),
321                                              uuid);
322             if (e->testFlag(EventManager::Self)) {
323                 event->setFlag(EventManager::Self);
324             }
325             ctcpEvents << event;
326         }
327     }
328     if (!ctcpEvents.isEmpty()) {
329         _replies.insert(uuid, CtcpReply(coreNetwork(e), nickFromMask(e->prefix())));
330         CtcpEvent* flushEvent = new CtcpEvent(EventManager::CtcpEventFlush,
331                                               e->network(),
332                                               {},
333                                               e->prefix(),
334                                               e->target(),
335                                               ctcptype,
336                                               "INVALID",
337                                               QString(),
338                                               e->timestamp(),
339                                               uuid);
340         ctcpEvents << flushEvent;
341         for (CtcpEvent* event : ctcpEvents) {
342             emit newEvent(event);
343         }
344     }
345
346     if (!dequotedMessage.isEmpty())
347         displayMsg(e, messagetype, targetDecode(e, dequotedMessage), e->prefix(), e->target(), flags);
348 }
349
350 void CtcpParser::sendCtcpEvent(CtcpEvent* e)
351 {
352     CoreNetwork* net = coreNetwork(e);
353     if (e->type() == EventManager::CtcpEvent) {
354         QByteArray quotedReply;
355         QString bufname = nickFromMask(e->prefix());
356         if (e->ctcpType() == CtcpEvent::Query && !e->reply().isNull()) {
357             if (_replies.contains(e->uuid()))
358                 _replies[e->uuid()].replies << lowLevelQuote(pack(net->serverEncode(e->ctcpCmd()), net->userEncode(bufname, e->reply())));
359             else
360                 // reply not caused by a request processed in here, so send it off immediately
361                 reply(net, bufname, e->ctcpCmd(), e->reply());
362         }
363     }
364     else if (e->type() == EventManager::CtcpEventFlush && _replies.contains(e->uuid())) {
365         CtcpReply reply = _replies.take(e->uuid());
366         if (reply.replies.count())
367             packedReply(net, reply.bufferName, reply.replies);
368     }
369 }
370
371 QByteArray CtcpParser::pack(const QByteArray& ctcpTag, const QByteArray& message)
372 {
373     if (message.isEmpty())
374         return XDELIM + ctcpTag + XDELIM;
375
376     return XDELIM + ctcpTag + ' ' + xdelimQuote(message) + XDELIM;
377 }
378
379 void CtcpParser::query(CoreNetwork* net, const QString& bufname, const QString& ctcpTag, const QString& message)
380 {
381     QString cmd("PRIVMSG");
382
383     std::function<QList<QByteArray>(QString&)> cmdGenerator = [&](QString& splitMsg) -> QList<QByteArray> {
384         return QList<QByteArray>() << net->serverEncode(bufname)
385                                    << lowLevelQuote(pack(net->serverEncode(ctcpTag), net->userEncode(bufname, splitMsg)));
386     };
387
388     net->putCmd(cmd, net->splitMessage(cmd, message, cmdGenerator));
389 }
390
391 void CtcpParser::reply(CoreNetwork* net, const QString& bufname, const QString& ctcpTag, const QString& message)
392 {
393     QList<QByteArray> params;
394     params << net->serverEncode(bufname) << lowLevelQuote(pack(net->serverEncode(ctcpTag), net->userEncode(bufname, message)));
395     net->putCmd("NOTICE", params);
396 }
397
398 void CtcpParser::packedReply(CoreNetwork* net, const QString& bufname, const QList<QByteArray>& replies)
399 {
400     QList<QByteArray> params;
401
402     int answerSize = 0;
403     for (int i = 0; i < replies.count(); i++) {
404         answerSize += replies.at(i).size();
405     }
406
407     QByteArray quotedReply;
408     quotedReply.reserve(answerSize);
409     for (int i = 0; i < replies.count(); i++) {
410         quotedReply.append(replies.at(i));
411     }
412
413     params << net->serverEncode(bufname) << quotedReply;
414     // FIXME user proper event
415     net->putCmd("NOTICE", params);
416 }