1 /*
2     SPDX-License-Identifier: GPL-2.0-or-later
3 
4     SPDX-FileCopyrightText: 2002 Dario Abatianni <eisfuchs@tigress.com>
5     SPDX-FileCopyrightText: 2005-2008 Eike Hein <hein@kde.org>
6 */
7 
8 #include "query.h"
9 
10 #include "channel.h"
11 #include "server.h"
12 #include "application.h"
13 #include "mainwindow.h"
14 #include "viewcontainer.h"
15 #include "ircinput.h"
16 #include "ircview.h"
17 #include "ircviewbox.h"
18 #include "awaylabel.h"
19 #include "common.h"
20 
21 #include <QSplitter>
22 
23 #include <KMessageBox>
24 #include <KSqueezedTextLabel>
25 
26 using namespace Konversation;
27 
Query(QWidget * parent,const QString & _name)28 Query::Query(QWidget* parent, const QString& _name) : ChatWindow(parent)
29 {
30     name=_name; // need the name a little bit earlier for setServer
31     // don't setName here! It will break logfiles!
32     //   setName("QueryWidget");
33     setType(ChatWindow::Query);
34     m_isTopLevelView = false;
35 
36     setChannelEncodingSupported(true);
37 
38     m_headerSplitter = new QSplitter(Qt::Vertical, this);
39 
40     m_initialShow = true;
41     awayChanged=false;
42     awayState=false;
43 
44     queryHostmask=new KSqueezedTextLabel(m_headerSplitter);
45     m_headerSplitter->setStretchFactor(m_headerSplitter->indexOf(queryHostmask), 0);
46     queryHostmask->setTextElideMode(Qt::ElideRight);
47     queryHostmask->setObjectName(QStringLiteral("query_hostmask"));
48 
49     QString whatsthis = i18n("<qt><p>Some details of the person you are talking to in this query is shown in this bar. The full name and hostmask is shown.</p><p>See the <i>Konversation Handbook</i> for an explanation of what the hostmask is.</p></qt>");
50     queryHostmask->setWhatsThis(whatsthis);
51 
52     auto* ircViewBox = new IRCViewBox(m_headerSplitter);
53     m_headerSplitter->setStretchFactor(m_headerSplitter->indexOf(ircViewBox), 1);
54     setTextView(ircViewBox->ircView());               // Server will be set later in setServer();
55     ircViewBox->ircView()->setContextMenuOptions(IrcContextMenus::ShowNickActions, true);
56     textView->setAcceptDrops(true);
57     connect(textView,&IRCView::urlsDropped,this,&Query::urlsDropped);
58 
59     // This box holds the input line
60     auto* inputBox=new QWidget(this);
61     auto* inputBoxLayout = new QHBoxLayout(inputBox);
62     inputBox->setObjectName(QStringLiteral("input_log_box"));
63     inputBoxLayout->setSpacing(spacing());
64     inputBoxLayout->setContentsMargins(0, 0, 0, 0);
65 
66     awayLabel=new AwayLabel(inputBox);
67     inputBoxLayout->addWidget(awayLabel);
68     awayLabel->hide();
69     blowfishLabel = new QLabel(inputBox);
70     inputBoxLayout->addWidget(blowfishLabel);
71     blowfishLabel->hide();
72     const int toolBarIconSize = blowfishLabel->style()->pixelMetric(QStyle::PixelMetric::PM_ToolBarIconSize);
73     blowfishLabel->setPixmap(QIcon::fromTheme(QStringLiteral("document-encrypt")).pixmap(toolBarIconSize));
74     m_inputBar=new IRCInput(inputBox);
75     inputBoxLayout->addWidget(m_inputBar);
76 
77     getTextView()->installEventFilter(m_inputBar);
78     m_inputBar->installEventFilter(this);
79 
80     // connect the signals and slots
81     connect(m_inputBar, &IRCInput::submit, this, &Query::queryTextEntered);
82     connect(m_inputBar, &IRCInput::envelopeCommand, this, &Query::queryPassthroughCommand);
83     connect(m_inputBar, &IRCInput::textPasted, this, &Query::textPasted);
84     connect(getTextView(), &IRCView::textPasted, m_inputBar, &IRCInput::paste);
85     connect(getTextView(), &IRCView::gotFocus, m_inputBar, QOverload<>::of(&IRCInput::setFocus));
86 
87     connect(textView,&IRCView::sendFile,this,&Query::sendFileMenu );
88     connect(textView, &IRCView::autoText, this, &Query::sendText);
89 
90     updateAppearance();
91 
92     #ifdef HAVE_QCA2
93     m_cipher = nullptr;
94     #endif
95 }
96 
~Query()97 Query::~Query()
98 {
99     if (m_recreationScheduled)
100     {
101         qRegisterMetaType<NickInfoPtr>("NickInfoPtr");
102 
103         QMetaObject::invokeMethod(m_server, "addQuery", Qt::QueuedConnection,
104             Q_ARG(NickInfoPtr, m_nickInfo), Q_ARG(bool, true));
105     }
106 }
107 
setServer(Server * newServer)108 void Query::setServer(Server* newServer)
109 {
110     if (m_server != newServer)
111     {
112         connect(newServer, &Server::connectionStateChanged,
113                 this, &Query::connectionStateChanged);
114         connect(newServer, QOverload<Server*,NickInfoPtr>::of(&Server::nickInfoChanged),
115                 this, &Query::updateNickInfo);
116     }
117 
118     ChatWindow::setServer(newServer);
119 
120     if (!(newServer->getKeyForRecipient(getName()).isEmpty()))
121         blowfishLabel->show();
122 
123     connect(awayLabel, &AwayLabel::unaway, m_server, &Server::requestUnaway);
124     connect(awayLabel, &AwayLabel::awayMessageChanged, m_server, &Server::requestAway);
125 }
126 
connectionStateChanged(Server * server,Konversation::ConnectionState state)127 void Query::connectionStateChanged(Server* server, Konversation::ConnectionState state)
128 {
129     if (server == m_server)
130     {
131         ViewContainer* viewContainer = Application::instance()->getMainWindow()->getViewContainer();
132 
133         if (state ==  Konversation::SSConnected)
134         {
135             //HACK the way the notification priorities work sucks, this forces the tab text color to ungray right now.
136             if (viewContainer->getFrontView() == this
137                 || m_currentTabNotify == Konversation::tnfNone
138                 || !Preferences::self()->tabNotificationsEvents())
139             {
140                 viewContainer->unsetViewNotification(this);
141             }
142         }
143         else
144         {
145             //HACK the way the notification priorities work sucks, this forces the tab text color to gray right now.
146             if (viewContainer->getFrontView() == this
147                 || m_currentTabNotify == Konversation::tnfNone
148                 || (!Preferences::self()->tabNotificationsEvents() && m_currentTabNotify == Konversation::tnfControl))
149             {
150                 viewContainer->unsetViewNotification(this);
151             }
152         }
153     }
154 }
155 
setName(const QString & newName)156 void Query::setName(const QString& newName)
157 {
158     //if(ChatWindow::getName() == newName) return;  // no change, so return
159 
160 
161     if(ChatWindow::getName() != newName)
162     {
163         appendCommandMessage(i18n("Nick"),i18n("%1 is now known as %2.", getName(), newName));
164     }
165 
166 
167     ChatWindow::setName(newName);
168 
169     // don't change logfile name if query name changes
170     // This will prevent Nick-Changers to create more than one log file,
171     if (logName.isEmpty())
172     {
173         QString logName =  (Preferences::self()->lowerLog()) ? getName().toLower() : getName() ;
174 
175         if(Preferences::self()->addHostnameToLog())
176         {
177             if(m_nickInfo)
178                 logName += m_nickInfo->getHostmask();
179         }
180 
181         setLogfileName(logName);
182     }
183 }
184 
setEncryptedOutput(bool e)185 void Query::setEncryptedOutput(bool e)
186 {
187     if (e)
188         blowfishLabel->show();
189     else
190         blowfishLabel->hide();
191 }
192 
queryTextEntered()193 void Query::queryTextEntered()
194 {
195     QString line=m_inputBar->toPlainText();
196 
197     m_inputBar->clear();
198 
199     if (!line.isEmpty()) sendText(sterilizeUnicode(line));
200 }
201 
queryPassthroughCommand()202 void Query::queryPassthroughCommand()
203 {
204     QString commandChar = Preferences::self()->commandChar();
205     QString line = m_inputBar->toPlainText();
206 
207     m_inputBar->clear();
208 
209     if(!line.isEmpty())
210     {
211         // Prepend commandChar on Ctrl+Enter to bypass outputfilter command recognition
212         if (line.startsWith(commandChar))
213         {
214             line = commandChar + line;
215         }
216         sendText(sterilizeUnicode(line));
217     }
218 }
219 
sendText(const QString & sendLine)220 void Query::sendText(const QString& sendLine)
221 {
222     // create a work copy
223     QString outputAll(sendLine);
224 
225     // replace aliases and wildcards
226     OutputFilter::replaceAliases(outputAll, this);
227 
228     // Send all strings, one after another
229 #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
230     const QStringList outList = outputAll.split(QLatin1Char('\n'), QString::SkipEmptyParts);
231 #else
232     const QStringList outList = outputAll.split(QLatin1Char('\n'), Qt::SkipEmptyParts);
233 #endif
234     for (const QString& output : outList) {
235 
236         // encoding stuff is done in Server()
237         Konversation::OutputFilterResult result = m_server->getOutputFilter()->parse(m_server->getNickname(), output, getName(), this);
238 
239         if(!result.output.isEmpty())
240         {
241             if(result.type == Konversation::Action) appendAction(m_server->getNickname(), result.output);
242             else if(result.type == Konversation::Command) appendCommandMessage(result.typeString, result.output);
243             else if(result.type == Konversation::Program) appendServerMessage(result.typeString, result.output);
244             else if(result.type == Konversation::PrivateMessage) msgHelper(result.typeString, result.output);
245             else if(!result.typeString.isEmpty()) appendQuery(result.typeString, result.output);
246             else appendQuery(m_server->getNickname(), result.output);
247         }
248         else if (!result.outputList.isEmpty()) {
249             if (result.type == Konversation::Message)
250             {
251                 for (const QString& out : qAsConst(result.outputList)) {
252                     appendQuery(m_server->getNickname(), out);
253                 }
254             }
255             else if (result.type == Konversation::Action)
256             {
257                 for (int i = 0; i < result.outputList.count(); ++i)
258                 {
259                     if (i == 0)
260                         appendAction(m_server->getNickname(), result.outputList.at(i));
261                     else
262                         appendQuery(m_server->getNickname(), result.outputList.at(i));
263                 }
264             }
265         }
266 
267         // Send anything else to the server
268         if (!result.toServerList.empty())
269             m_server->queueList(result.toServerList);
270         else
271             m_server->queue(result.toServer);
272     } // for
273 }
274 
textPasted(const QString & text)275 void Query::textPasted(const QString& text)
276 {
277     if(m_server)
278     {
279 #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
280         const QStringList multiline = text.split(QLatin1Char('\n'), QString::SkipEmptyParts);
281 #else
282         const QStringList multiline = text.split(QLatin1Char('\n'), Qt::SkipEmptyParts);
283 #endif
284         for (QString line : multiline) {
285             QString cChar(Preferences::self()->commandChar());
286             // make sure that lines starting with command char get escaped
287             if(line.startsWith(cChar)) line=cChar+line;
288             sendText(line);
289         }
290     }
291 }
292 
indicateAway(bool show)293 void Query::indicateAway(bool show)
294 {
295     // QT does not redraw the label properly when they are not on screen
296     // while getting hidden, so we remember the "soon to be" state here.
297     if(isHidden())
298     {
299         awayChanged=true;
300         awayState=show;
301     }
302     else
303     {
304         if(show)
305             awayLabel->show();
306         else
307             awayLabel->hide();
308     }
309 }
310 
311 // fix QTs broken behavior on hidden QListView pages
showEvent(QShowEvent *)312 void Query::showEvent(QShowEvent*)
313 {
314     if(awayChanged)
315     {
316         awayChanged=false;
317         indicateAway(awayState);
318     }
319 
320     if(m_initialShow) {
321         m_initialShow = false;
322         QList<int> sizes;
323         sizes << queryHostmask->sizeHint().height() << (height() - queryHostmask->sizeHint().height());
324         m_headerSplitter->setSizes(sizes);
325     }
326 }
327 
sendFileMenu()328 void Query::sendFileMenu()
329 {
330     Q_EMIT sendFile(getName());
331 }
332 
childAdjustFocus()333 void Query::childAdjustFocus()
334 {
335     m_inputBar->setFocus();
336 }
337 
setNickInfo(const NickInfoPtr & nickInfo)338 void Query::setNickInfo(const NickInfoPtr & nickInfo)
339 {
340     m_nickInfo = nickInfo;
341     Q_ASSERT(m_nickInfo); if(!m_nickInfo) return;
342     nickInfoChanged();
343 }
344 
updateNickInfo(Server * server,const NickInfoPtr & nickInfo)345 void Query::updateNickInfo(Server* server, const NickInfoPtr &nickInfo)
346 {
347     if (!m_nickInfo || server != m_server || nickInfo != m_nickInfo)
348         return;
349 
350     nickInfoChanged();
351 }
352 
nickInfoChanged()353 void Query::nickInfoChanged()
354 {
355     if (m_nickInfo)
356     {
357         setName(m_nickInfo->getNickname());
358         QString text = m_nickInfo->getBestAddresseeName();
359         if(!m_nickInfo->getHostmask().isEmpty() && !text.isEmpty())
360             text += QStringLiteral(" - ");
361         text += m_nickInfo->getHostmask();
362         if(m_nickInfo->isAway() && !m_nickInfo->getAwayMessage().isEmpty())
363             text += QLatin1String(" (") + m_nickInfo->getAwayMessage() + QLatin1String(") ");
364         queryHostmask->setText(Konversation::removeIrcMarkup(text));
365 
366         QString strTooltip;
367         QTextStream tooltip( &strTooltip, QIODevice::WriteOnly );
368 
369         tooltip << "<qt>";
370 
371         tooltip << R"(<table cellspacing="5" cellpadding="0">)";
372 
373         m_nickInfo->tooltipTableData(tooltip);
374 
375         tooltip << "</table></qt>";
376         queryHostmask->setToolTip(strTooltip);
377     }
378 
379     Q_EMIT updateQueryChrome(this,getName());
380     emitUpdateInfo();
381 }
382 
getNickInfo() const383 NickInfoPtr Query::getNickInfo() const
384 {
385     return m_nickInfo;
386 }
387 
canBeFrontView() const388 bool Query::canBeFrontView() const  { return true; }
searchView() const389 bool Query::searchView() const      { return true; }
390                                                   // virtual
setChannelEncoding(const QString & encoding)391 void Query::setChannelEncoding(const QString& encoding)
392 {
393     if(m_server->getServerGroup())
394         Preferences::setChannelEncoding(m_server->getServerGroup()->id(), getName(), encoding);
395     else
396         Preferences::setChannelEncoding(m_server->getDisplayName(), getName(), encoding);
397 }
398 
getChannelEncoding() const399 QString Query::getChannelEncoding() const              // virtual
400 {
401     if(m_server->getServerGroup())
402         return Preferences::channelEncoding(m_server->getServerGroup()->id(), getName());
403     return Preferences::channelEncoding(m_server->getDisplayName(), getName());
404 }
405 
getChannelEncodingDefaultDesc() const406 QString Query::getChannelEncodingDefaultDesc() const   // virtual
407 {
408     return i18n("Identity Default ( %1 )",getServer()->getIdentity()->getCodecName());
409 }
410 
closeYourself(bool confirm)411 bool Query::closeYourself(bool confirm)
412 {
413     int result = KMessageBox::Continue;
414 
415     if (confirm)
416         result=KMessageBox::warningContinueCancel(
417             this,
418             i18n("Do you want to close your query with %1?", getName()),
419             i18n("Close Query"),
420             KStandardGuiItem::close(),
421             KStandardGuiItem::cancel(),
422             QStringLiteral("QuitQueryTab"));
423 
424     if (result == KMessageBox::Continue)
425     {
426         m_server->removeQuery(this);
427 
428         return true;
429     }
430     else
431         m_recreationScheduled = false;
432 
433     return false;
434 }
435 
urlsDropped(const QList<QUrl> & urls)436 void Query::urlsDropped(const QList<QUrl>& urls)
437 {
438     m_server->sendURIs(urls, getName());
439 }
440 
emitUpdateInfo()441 void Query::emitUpdateInfo()
442 {
443     QString info;
444     if(m_nickInfo->loweredNickname() == m_server->loweredNickname())
445         info = i18n("Talking to yourself");
446     else if(m_nickInfo)
447         info = m_nickInfo->getBestAddresseeName();
448     else
449         info = getName();
450 
451     Q_EMIT updateInfo(info);
452 }
453 
454 // show quit message of nick if we see it
quitNick(const QString & reason,const QHash<QString,QString> & messageTags)455 void Query::quitNick(const QString& reason, const QHash<QString, QString> &messageTags)
456 {
457     QString displayReason = reason;
458 
459     if (displayReason.isEmpty())
460     {
461         appendCommandMessage(i18nc("Message type", "Quit"), i18nc("%1 = nick, %2 = hostmask", "%1 (%2) has left this server.",
462             getName(), getNickInfo()->getHostmask()), messageTags, false);
463     }
464     else
465     {
466         if (hasIRCMarkups(displayReason))
467             displayReason+=QStringLiteral("\017");
468 
469         appendCommandMessage(i18nc("Message type", "Quit"), i18nc("%1 = nick, %2 = hostmask, %3 = reason", "%1 (%2) has left this server (%3).",
470             getName(), getNickInfo()->getHostmask(), displayReason), messageTags, false);
471     }
472 }
473 
474 #ifdef HAVE_QCA2
getCipher() const475 Konversation::Cipher* Query::getCipher() const
476 {
477     if(!m_cipher)
478         m_cipher = new Konversation::Cipher();
479     return m_cipher;
480 }
481 #endif
482