1 /*
2     SPDX-License-Identifier: GPL-2.0-or-later
3 
4     SPDX-FileCopyrightText: 2002 Dario Abatianni <eisfuchs@tigress.com>
5     SPDX-FileCopyrightText: 2006-2008 Eike Hein <hein@kde.org>
6 */
7 
8 #include "chatwindow.h"
9 
10 #include "channel.h"
11 #include "query.h"
12 #include "ircview.h"
13 #include "ircinput.h"
14 #include "server.h"
15 #include "application.h"
16 #include "logfilereader.h"
17 #include "viewcontainer.h"
18 #include "konversation_log.h"
19 
20 #include <KUser>
21 
22 #include <QDateTime>
23 #include <QDir>
24 #include <QTextCodec>
25 #include <QKeyEvent>
26 #include <QScrollBar>
27 #include <QLocale>
28 
29 
ChatWindow(QWidget * parent)30 ChatWindow::ChatWindow(QWidget* parent) : QWidget(parent)
31 {
32     auto* mainLayout = new QVBoxLayout(this);
33     mainLayout->setContentsMargins(margin(), margin(), margin(), margin());
34     mainLayout->setSpacing(spacing());
35 
36     setName(QStringLiteral("ChatWindowObject"));
37     setTextView(nullptr);
38     setInputBar(nullptr);
39     firstLog = true;
40     m_server = nullptr;
41     m_recreationScheduled = false;
42     m_isTopLevelView = true;
43     m_notificationsEnabled = true;
44     m_channelEncodingSupported = false;
45     m_currentTabNotify = Konversation::tnfNone;
46 }
47 
~ChatWindow()48 ChatWindow::~ChatWindow()
49 {
50     if (getInputBar() && getServer())
51     {
52         const QString& language = getInputBar()->spellCheckingLanguage();
53 
54         if (!language.isEmpty())
55         {
56             Konversation::ServerGroupSettingsPtr serverGroup = getServer()->getConnectionSettings().serverGroup();
57 
58             if (serverGroup)
59                 Preferences::setSpellCheckingLanguage(serverGroup, getName(), language);
60             else
61                 Preferences::setSpellCheckingLanguage(getServer()->getDisplayName(), getName(), language);
62         }
63     }
64 
65     Q_EMIT closing(this);
66     m_server=nullptr;
67 }
68 
childEvent(QChildEvent * event)69 void ChatWindow::childEvent(QChildEvent* event)
70 {
71     if(event->type() == QChildEvent::ChildAdded)
72     {
73         if(event->child()->isWidgetType())
74         {
75             layout()->addWidget(qobject_cast< QWidget* >(event->child()));
76         }
77     }
78     else if(event->type() == QChildEvent::ChildRemoved)
79     {
80         if(event->child()->isWidgetType())
81         {
82             layout()->removeWidget(qobject_cast<QWidget*>(event->child()));
83         }
84     }
85 }
86 
87 // reimplement this if your window needs special close treatment
closeYourself(bool)88 bool ChatWindow::closeYourself(bool /* askForConfirmation */)
89 {
90     deleteLater();
91 
92     return true;
93 }
94 
cycle()95 void ChatWindow::cycle()
96 {
97     m_recreationScheduled = true;
98 
99     closeYourself(false);
100 }
101 
updateAppearance()102 void ChatWindow::updateAppearance()
103 {
104     if (getTextView()) getTextView()->updateAppearance();
105 
106     // The font size of the KTabWidget container may be inappropriately
107     // small due to the "Tab bar" font size setting.
108     setFont(QFontDatabase::systemFont(QFontDatabase::GeneralFont));
109 }
110 
setName(const QString & newName)111 void ChatWindow::setName(const QString& newName)
112 {
113     name=newName;
114     Q_EMIT nameChanged(this,newName);
115 }
116 
getName() const117 QString ChatWindow::getName() const
118 {
119     return name;
120 }
121 
getTitle() const122 QString ChatWindow::getTitle() const
123 {
124     QString title;
125     if (getType() == Channel)
126     {
127        title = QStringLiteral("%1 (%2)")
128              .arg(getName(), getServer()->getDisplayName());
129     }
130     else
131     {
132        title = getName();
133     }
134 
135     return title;
136 }
137 
getURI(bool passNetwork)138 QString ChatWindow::getURI(bool passNetwork)
139 {
140     QString protocol;
141     QString url;
142     QString port;
143     QString server;
144     QString channel;
145 
146     if (getServer()->getUseSSL())
147         protocol = QStringLiteral("ircs://");
148     else
149         protocol = QStringLiteral("irc://");
150 
151     if (getType() == Channel)
152     {
153         channel = getName();
154         if (channel.startsWith(QLatin1Char('#'))) {
155             channel.remove(0, 1);
156         }
157 
158         // must protect second #, but might as well protect all of them
159         channel.replace(QLatin1String("#"), QLatin1String("%23"));
160     }
161 
162     if (passNetwork)
163     {
164         server = getServer()->getDisplayName();
165 
166         QUrl test(protocol+server);
167 
168         // QUrl (ultimately used by the bookmark system, which is the
169         // primary consumer here) doesn't like spaces in hostnames as
170         // well as other things which are possible in user-chosen net-
171         // work names, so let's fall back to the hostname if we can't
172         // get the network name by it.
173         if (!test.isValid())
174             passNetwork = false;
175     }
176 
177     if (!passNetwork)
178     {
179         server = getServer()->getServerName();
180         port = QLatin1Char(':') + QString::number(getServer()->getPort());
181     }
182 
183     if (server.contains(QLatin1Char(':'))) // IPv6
184         server = QLatin1Char('[') + server + QLatin1Char(']');
185 
186     url = protocol + server + port + QLatin1Char('/') + channel;
187 
188     return url;
189 }
190 
setType(WindowType newType)191 void ChatWindow::setType(WindowType newType)
192 {
193     type=newType;
194 }
195 
getType() const196 ChatWindow::WindowType ChatWindow::getType() const
197 {
198     return type;
199 }
200 
isTopLevelView() const201 bool ChatWindow::isTopLevelView() const
202 {
203     return m_isTopLevelView;
204 }
205 
setServer(Server * newServer)206 void ChatWindow::setServer(Server* newServer)
207 {
208     if (!newServer)
209     {
210         qCDebug(KONVERSATION_LOG) << "ChatWindow::setServer(0)!";
211     }
212     else
213     {
214         m_server=newServer;
215         connect(m_server, &Server::serverOnline, this, &ChatWindow::serverOnline);
216 
217         // check if we need to set up the signals
218         if(getType() != ChannelList)
219         {
220             if(textView) textView->setServer(newServer);
221             else qCDebug(KONVERSATION_LOG) << "textView==0!";
222         }
223 
224         serverOnline(m_server->isConnected());
225     }
226 
227     if (getInputBar())
228     {
229         QString language;
230 
231         Konversation::ServerGroupSettingsPtr serverGroup = newServer->getConnectionSettings().serverGroup();
232 
233         if (serverGroup)
234             language = Preferences::spellCheckingLanguage(serverGroup, getName());
235         else
236             language = Preferences::spellCheckingLanguage(newServer->getDisplayName(), getName());
237 
238         if (!language.isEmpty())
239             getInputBar()->setSpellCheckingLanguage(language);
240     }
241 }
242 
getServer() const243 Server* ChatWindow::getServer() const
244 {
245     return m_server;
246 }
247 
serverOnline(bool)248 void ChatWindow::serverOnline(bool /* state */)
249 {
250     //Q_EMIT online(this,state);
251 }
252 
setTextView(IRCView * newView)253 void ChatWindow::setTextView(IRCView* newView)
254 {
255     textView = newView;
256 
257     if(!textView)
258     {
259         return;
260     }
261 
262     textView->setVerticalScrollBarPolicy(Preferences::self()->showIRCViewScrollBar() ? Qt::ScrollBarAlwaysOn : Qt::ScrollBarAlwaysOff);
263 
264     textView->setChatWin(this);
265     connect(textView, &IRCView::textToLog, this, &ChatWindow::logText);
266     connect(textView, &IRCView::setStatusBarTempText, this, &ChatWindow::setStatusBarTempText);
267     connect(textView, &IRCView::clearStatusBarTempText, this, &ChatWindow::clearStatusBarTempText);
268 }
269 
appendRaw(const QString & message,bool self)270 void ChatWindow::appendRaw(const QString& message, bool self)
271 {
272     if(!textView) return;
273     textView->appendRaw(message, self);
274 }
275 
appendLog(const QString & message)276 void ChatWindow::appendLog(const QString& message)
277 {
278     if(!textView) return;
279     textView->appendLog(message);
280 }
281 
append(const QString & nickname,const QString & message,const QHash<QString,QString> & messageTags,const QString & label)282 void ChatWindow::append(const QString& nickname, const QString& message, const QHash<QString, QString> &messageTags, const QString& label)
283 {
284     if(!textView) return;
285     textView->append(nickname, message, messageTags, label);
286 }
287 
appendQuery(const QString & nickname,const QString & message,const QHash<QString,QString> & messageTags,bool inChannel)288 void ChatWindow::appendQuery(const QString& nickname, const QString& message, const QHash<QString, QString> &messageTags, bool inChannel)
289 {
290     if(!textView) return ;
291     textView->appendQuery(nickname, message, messageTags, inChannel);
292 }
293 
appendAction(const QString & nickname,const QString & message,const QHash<QString,QString> & messageTags)294 void ChatWindow::appendAction(const QString& nickname, const QString& message, const QHash<QString, QString> &messageTags)
295 {
296     if(!textView) return;
297 
298     if (getType() == Query || getType() == DccChat)
299         textView->appendQueryAction(nickname, message, messageTags);
300     else
301         textView->appendChannelAction(nickname, message, messageTags);
302 }
303 
appendServerMessage(const QString & type,const QString & message,const QHash<QString,QString> & messageTags,bool parseURL)304 void ChatWindow::appendServerMessage(const QString& type, const QString& message, const QHash<QString, QString> &messageTags, bool parseURL)
305 {
306     if(!textView) return ;
307     textView->appendServerMessage(type, message, messageTags, parseURL);
308 }
309 
appendCommandMessage(const QString & command,const QString & message,const QHash<QString,QString> & messageTags,bool parseURL,bool self)310 void ChatWindow::appendCommandMessage(const QString& command, const QString& message, const QHash<QString, QString> &messageTags, bool parseURL, bool self)
311 {
312     if(!textView) return ;
313     textView->appendCommandMessage(command, message, messageTags, parseURL, self);
314 }
315 
appendBacklogMessage(const QString & firstColumn,const QString & message)316 void ChatWindow::appendBacklogMessage(const QString& firstColumn,const QString& message)
317 {
318     if(!textView) return ;
319     textView->appendBacklogMessage(firstColumn,Konversation::sterilizeUnicode(message));
320 }
321 
clear()322 void ChatWindow::clear()
323 {
324     if (!textView) return;
325 
326     textView->clear();
327 
328     resetTabNotification();
329 
330     if (m_server)
331         m_server->getViewContainer()->unsetViewNotification(this);
332 }
333 
cdIntoLogPath()334 void ChatWindow::cdIntoLogPath()
335 {
336     QString home = KUser(KUser::UseRealUserID).homeDir();
337     QUrl logUrl = Preferences::self()->logfilePath();
338 
339     if(!logUrl.isLocalFile())
340     {
341         return;
342     }
343 
344     QString logPath = logUrl.toLocalFile();
345 
346     QDir logDir(home);
347 
348     // Try to "cd" into the logfile path.
349     if (!logDir.cd(logPath))
350     {
351         // Only create log path if logging is enabled.
352         if (log())
353         {
354             // Try to create the logfile path and "cd" into it again.
355             logDir.mkpath(logPath);
356             logDir.cd(logPath);
357         }
358     }
359 
360     // Add the logfile name to the path.
361     logfile.setFileName(logDir.path() + QLatin1Char('/') + logName);
362 }
363 
setLogfileName(const QString & name)364 void ChatWindow::setLogfileName(const QString& name)
365 {
366     // Only change name of logfile if the window was new.
367     if(firstLog)
368     {
369         if (getTextView())
370             getTextView()->setContextMenuOptions(IrcContextMenus::ShowLogAction, true);
371 
372         // status panels get special treatment here, since they have no server at the beginning
373         if (getType() == Status || getType() == DccChat)
374         {
375             logName = name + QLatin1String(".log");
376         }
377         else if (m_server)
378         {
379             // make sure that no path delimiters are in the name
380             logName = QString(m_server->getDisplayName().toLower() + QLatin1Char('_') + name + QLatin1String(".log")).replace(QLatin1Char('/'), QLatin1Char('_'));
381         }
382 
383         // load backlog to show
384         if(Preferences::self()->showBacklog())
385         {
386             // "cd" into log path or create path, if it's not there
387             cdIntoLogPath();
388             // Show last log lines. This idea was stole ... um ... inspired by PMP :)
389             // Don't do this for the server status windows, though
390             if((getType() != Status) && logfile.open(QIODevice::ReadOnly))
391             {
392                 qint64 filePosition;
393 
394                 QTextStream backlog(&logfile);
395                 backlog.setCodec(QTextCodec::codecForName("UTF-8"));
396                 backlog.setAutoDetectUnicode(true);
397 
398                 QStringList firstColumns;
399                 QStringList messages;
400                 int offset = 0;
401                 qint64 lastPacketHeadPosition = backlog.device()->size();
402                 const unsigned int packetSize = 4096;
403                 while(messages.count() < Preferences::self()->backlogLines() && backlog.device()->size() > packetSize * offset)
404                 {
405                     QStringList firstColumnsInPacket;
406                     QStringList messagesInPacket;
407 
408                     // packetSize * offset < size <= packetSize * ( offset + 1 )
409 
410                     // Check if the log is bigger than packetSize * ( offset + 1 )
411                     if(backlog.device()->size() > packetSize * ( offset + 1 ))
412                     {
413                         // Set file pointer to the packet size above the offset
414                         backlog.seek(backlog.device()->size() - packetSize * ( offset + 1 ));
415                         // Skip first line, since it may be incomplete
416                         backlog.readLine();
417                     }
418                     else
419                     {
420                         // Set file pointer to the head
421 
422                         // Qt 4.5 Doc: Note that when using a QTextStream on a
423                         // QFile, calling reset() on the QFile will not have the
424                         // expected result because QTextStream buffers the  file.
425                         // Use the QTextStream::seek() function instead.
426                         // backlog.device()->reset();
427                         backlog.seek( 0 );
428                     }
429 
430                     // remember actual file position to check for deadlocks
431                     filePosition = backlog.pos();
432 
433                     qint64 currentPacketHeadPosition = filePosition;
434 
435                     // Loop until end of file reached
436                     while(!backlog.atEnd() && filePosition < lastPacketHeadPosition)
437                     {
438                         const QString backlogFileLine = backlog.readLine();
439 
440                         // check for deadlocks
441                         if(backlog.pos() == filePosition)
442                         {
443                             backlog.seek(filePosition + 1);
444                         }
445 
446                         // if a tab character is present in the line, meaning it is a valid chatline
447                         const int tabIndex = backlogFileLine.indexOf(QLatin1Char('\t'));
448                         if (tabIndex != -1) {
449                             // extract first column from log
450                             const QString backlogFirst = backlogFileLine.left(tabIndex);
451                             // cut first column from line
452                             const QString backlogLine = backlogFileLine.mid(tabIndex + 1);
453                             // Logfile is in utf8 so we don't need to do encoding stuff here
454                             // append backlog with time and first column to text view
455                             firstColumnsInPacket << backlogFirst;
456                             messagesInPacket << backlogLine;
457                         }
458 
459                         // remember actual file position to check for deadlocks
460                         filePosition = backlog.pos();
461                     } // while
462 
463                     // remember the position not to read the same lines again
464                     lastPacketHeadPosition = currentPacketHeadPosition;
465                     ++offset;
466 
467                     firstColumns = firstColumnsInPacket + firstColumns;
468                     messages = messagesInPacket + messages;
469                 }
470                 backlog.setDevice(nullptr);
471                 logfile.close();
472 
473                 // trim
474                 int surplus = messages.count() - Preferences::self()->backlogLines();
475                 // "surplus" can be a minus value. (when the backlog is too short)
476                 if(surplus > 0)
477                 {
478                     for(int i = 0 ; i < surplus ; ++i)
479                     {
480                         firstColumns.pop_front();
481                         messages.pop_front();
482                     }
483                 }
484 
485                 QStringList::Iterator itFirstColumn = firstColumns.begin();
486                 QStringList::Iterator itMessage = messages.begin();
487                 for( ; itFirstColumn != firstColumns.end() ; ++itFirstColumn, ++itMessage )
488                     appendBacklogMessage(*itFirstColumn, *itMessage);
489             }
490         } // if(Preferences::showBacklog())
491     }
492 }
493 
logText(const QString & text)494 void ChatWindow::logText(const QString& text)
495 {
496     if(log())
497     {
498         // "cd" into log path or create path, if it's not there
499         cdIntoLogPath();
500 
501         if(logfile.open(QIODevice::WriteOnly | QIODevice::Append))
502         {
503             // wrap the file into a stream
504             QTextStream logStream(&logfile);
505             // write log in utf8 to help i18n
506             logStream.setCodec(QTextCodec::codecForName("UTF-8"));
507             logStream.setAutoDetectUnicode(true);
508 
509             if(firstLog)
510             {
511                 QString intro(i18n("\n*** Logfile started\n*** on %1\n\n", QDateTime::currentDateTime().toString()));
512                 logStream << intro;
513                 firstLog=false;
514             }
515 
516             QDateTime dateTime = QDateTime::currentDateTime();
517             QString logLine = QStringLiteral("[%1] [%2] %3\n").arg(QLocale().toString(dateTime.date(), QLocale::LongFormat), QLocale().toString(dateTime.time(), QLocale::LongFormat), text);
518             logStream << logLine;
519 
520             // detach stream from file
521             logStream.setDevice(nullptr);
522 
523             // close file
524             logfile.close();
525         }
526         else qCWarning(KONVERSATION_LOG) << "open(QIODevice::Append) for " << logfile.fileName() << " failed!";
527     }
528 }
529 
setChannelEncodingSupported(bool enabled)530 void ChatWindow::setChannelEncodingSupported(bool enabled)
531 {
532     m_channelEncodingSupported = enabled;
533 }
534 
isChannelEncodingSupported() const535 bool ChatWindow::isChannelEncodingSupported() const
536 {
537     return m_channelEncodingSupported;
538 }
539 
spacing()540 int ChatWindow::spacing()
541 {
542     if(Preferences::self()->useSpacing())
543         return Preferences::self()->spacing();
544     else
545         return style()->layoutSpacing(QSizePolicy::DefaultType, QSizePolicy::DefaultType, Qt::Vertical);
546 }
547 
margin()548 int ChatWindow::margin()
549 {
550     if(Preferences::self()->useSpacing())
551         return Preferences::self()->margin();
552     else
553         return 0;
554 }
555 
556 // Accessors
getTextView() const557 IRCView* ChatWindow::getTextView() const
558 {
559   return textView;
560 }
561 
log() const562 bool ChatWindow::log() const
563 {
564   return Preferences::self()->log();
565 }
566 
567 // reimplement this in all panels that have user input
getTextInLine() const568 QString ChatWindow::getTextInLine() const
569 {
570     if (m_inputBar)
571         return m_inputBar->toPlainText();
572     else
573         return QString();
574 }
575 
canBeFrontView() const576 bool ChatWindow::canBeFrontView() const
577 {
578   return false;
579 }
580 
searchView() const581 bool ChatWindow::searchView() const
582 {
583   return false;
584 }
585 
586 // reimplement this in all panels that have user input
indicateAway(bool)587 void ChatWindow::indicateAway(bool)
588 {
589 }
590 
591 // reimplement this in all panels that have user input
appendInputText(const QString & text,bool fromCursor)592 void ChatWindow::appendInputText(const QString& text, bool fromCursor)
593 {
594     if (!fromCursor)
595         m_inputBar->append(text);
596     else
597     {
598         const int position = m_inputBar->textCursor().position();
599         m_inputBar->textCursor().insertText(text);
600         QTextCursor cursor = m_inputBar->textCursor();
601         cursor.setPosition(position + text.length());
602         m_inputBar->setTextCursor(cursor);
603     }
604 }
605 
eventFilter(QObject * watched,QEvent * e)606 bool ChatWindow::eventFilter(QObject* watched, QEvent* e)
607 {
608     if(e->type() == QEvent::KeyPress)
609     {
610         auto* ke = static_cast<QKeyEvent*>(e);
611 
612         bool scrollMod = (Preferences::self()->useMultiRowInputBox() ? false : (ke->modifiers() == Qt::ShiftModifier));
613 
614         if(ke->key() == Qt::Key_Up && scrollMod)
615         {
616             if(textView)
617             {
618                 QScrollBar* sbar = textView->verticalScrollBar();
619                 sbar->setValue(sbar->value() - sbar->singleStep());
620             }
621 
622             return true;
623         }
624         else if(ke->key() == Qt::Key_Down && scrollMod)
625         {
626             if(textView)
627             {
628                 QScrollBar* sbar = textView->verticalScrollBar();
629                 sbar->setValue(sbar->value() + sbar->singleStep());
630             }
631 
632             return true;
633         }
634         else if(ke->modifiers() == Qt::NoModifier && ke->key() == Qt::Key_PageUp)
635         {
636             if(textView)
637             {
638                 QScrollBar* sbar = textView->verticalScrollBar();
639                 sbar->setValue(sbar->value() - sbar->pageStep());
640             }
641 
642             return true;
643         }
644         else if(ke->modifiers() == Qt::NoModifier && ke->key() == Qt::Key_PageDown)
645         {
646             if(textView)
647             {
648                 QScrollBar* sbar = textView->verticalScrollBar();
649                 sbar->setValue(sbar->value() + sbar->pageStep());
650             }
651 
652             return true;
653         }
654 
655     }
656 
657     return QWidget::eventFilter(watched, e);
658 }
659 
adjustFocus()660 void ChatWindow::adjustFocus()
661 {
662     childAdjustFocus();
663 }
664 
emitUpdateInfo()665 void ChatWindow::emitUpdateInfo()
666 {
667     QString info = getName();
668     Q_EMIT updateInfo(info);
669 }
670 
highlightColor()671 QColor ChatWindow::highlightColor()
672 {
673     return getTextView()->highlightColor();
674 }
675 
activateTabNotification(Konversation::TabNotifyType type)676 void ChatWindow::activateTabNotification(Konversation::TabNotifyType type)
677 {
678     if (!notificationsEnabled())
679         return;
680 
681     if(type > m_currentTabNotify)
682         return;
683 
684     m_currentTabNotify = type;
685 
686     Q_EMIT updateTabNotification(this,type);
687 }
688 
resetTabNotification()689 void ChatWindow::resetTabNotification()
690 {
691     m_currentTabNotify = Konversation::tnfNone;
692 }
693 
msgHelper(const QString & recipient,const QString & message)694 void ChatWindow::msgHelper(const QString& recipient, const QString& message)
695     {
696     // A helper method for handling the 'msg' and 'query' (with a message
697     // payload) commands. When the user uses either, we show a visualiza-
698     // tion of what he/she has sent in the form of '<-> target> message>'
699     // in the chat view of the tab the command was issued in, as well as
700     // add the resulting message to the target view (if present), in that
701     // order. The order is especially important as the origin and target
702     // views may be the same, and the two messages may thus appear toge-
703     // ther and should be sensibly ordered.
704 
705     if (recipient.isEmpty() || message.isEmpty())
706         return;
707 
708     bool isAction = false;
709     QString result = message;
710     QString visualization;
711 
712     if (result.startsWith(Preferences::self()->commandChar() + QLatin1String("me")))
713     {
714         isAction = true;
715 
716         result.remove(0, 4);
717         visualization = QStringLiteral("* %1 %2").arg(m_server->getNickname(), result);
718     }
719     else
720         visualization = result;
721 
722     appendQuery(recipient, visualization, QHash<QString, QString>(), true);
723 
724     if (!getServer())
725         return;
726 
727     ::Query* query = m_server->getQueryByName(recipient);
728 
729     if (query)
730     {
731         if (isAction)
732             query->appendAction(m_server->getNickname(), result);
733         else
734             query->appendQuery(m_server->getNickname(), result);
735 
736         return;
737     }
738 
739     ::Channel* channel = m_server->getChannelByName(recipient);
740 
741     if (channel)
742     {
743         if (isAction)
744             channel->appendAction(m_server->getNickname(), result);
745         else
746             channel->append(m_server->getNickname(), result);
747     }
748 }
749 
activateView()750 void ChatWindow::activateView()
751 {
752     Q_EMIT windowActivationRequested();
753     Q_EMIT showView(this);
754 }
755