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