1435 lines
50 KiB
C++
1435 lines
50 KiB
C++
/***************************************************************************
|
|
networkwindow.cpp - description
|
|
-------------------
|
|
begin : Wed Jan 28 2005
|
|
copyright : (C) 2005 by Diederik van der Boor
|
|
email : "vdboor" --at-- "codingdomain.com"
|
|
***************************************************************************/
|
|
|
|
/***************************************************************************
|
|
* *
|
|
* This program is free software; you can redistribute it and/or modify *
|
|
* it under the terms of the GNU General Public License as published by *
|
|
* the Free Software Foundation; either version 2 of the License, or *
|
|
* (at your option) any later version. *
|
|
* *
|
|
***************************************************************************/
|
|
|
|
#include "../kmessdebug.h"
|
|
|
|
#include "networkwindow.h"
|
|
|
|
#include "../network/mimemessage.h"
|
|
#include "../network/msnnotificationconnection.h"
|
|
#include "../network/p2pmessage.h"
|
|
#include "../utils/kmessconfig.h"
|
|
|
|
#include <QFile>
|
|
#include <QRegExp>
|
|
#include <QSize>
|
|
#include <QDateTime>
|
|
#include <QTabWidget>
|
|
#include <QTextCodec>
|
|
#include <QTextStream>
|
|
|
|
#include <QFileDialog>
|
|
#include <KLocalizedString>
|
|
#include <KMessageBox>
|
|
#include <KTextBrowser>
|
|
|
|
|
|
// Declare the instance
|
|
NetworkWindow * NetworkWindow::instance_(0);
|
|
|
|
|
|
|
|
#ifdef KMESS_NETWORK_WINDOW
|
|
|
|
// The network window can optionally be compiled.
|
|
// The macro's from kmessdebug.h make it easy to handle this
|
|
// without optionally depending on networkwindow.h everywhere.
|
|
|
|
/**
|
|
* @brief Inform the network window the connection was closed, used by <code>KMESS_NET_CLOSE</code>.
|
|
* @param connection Reference to the caller object, to distinguish between connections.
|
|
* @ingroup Debug
|
|
*/
|
|
void _kmessNetClose(QObject *connection)
|
|
{
|
|
NetworkWindow::instance()->connectionClosed(connection);
|
|
}
|
|
|
|
/**
|
|
* @brief Inform the network window about a new connection, used by <code>KMESS_NET_INIT</code>.
|
|
* @param connection Reference to the caller object, to distinguish between connections.
|
|
* @param title The title for the connection in the network window.
|
|
* @ingroup Debug
|
|
*/
|
|
void _kmessNetInit(QObject *connection, const QString &title)
|
|
{
|
|
NetworkWindow::instance()->setConnectionTitle(connection, title);
|
|
}
|
|
|
|
/**
|
|
* @brief Add the received data to the network window, used by <code>KMESS_NET_RECEIVED</code>.
|
|
* @param connection Reference to the caller object, to distinguish between connections.
|
|
* @param message The data received from the connection, should be a complete message.
|
|
* @ingroup Debug
|
|
*/
|
|
void _kmessNetReceived(QObject *connection, const QByteArray &message)
|
|
{
|
|
NetworkWindow::instance()->addIncomingMessage(connection, message);
|
|
}
|
|
|
|
/**
|
|
* @brief Add the sent data to the network window, used by <code>KMESS_NET_SENT</code>.
|
|
* @param connection Reference to the caller object, to distinguish between connections.
|
|
* @param message The data sent to the connection, should be a complete message.
|
|
* @ingroup Debug
|
|
*/
|
|
void _kmessNetSent(QObject *connection, const QByteArray &message)
|
|
{
|
|
NetworkWindow::instance()->addOutgoingMessage(connection, message);
|
|
}
|
|
|
|
#endif
|
|
|
|
|
|
// Constructor
|
|
NetworkWindow::NetworkWindow( QWidget *parent )
|
|
: KMessDialog( parent, Qt::Dialog ) // Explicitly set the window type to disable the destructive close behavior
|
|
, Ui::NetworkWindow()
|
|
{
|
|
// Set up the dialog and its buttons
|
|
setObjectName( "Network" );
|
|
setButtons( Close | User1 | User2 | User3 );
|
|
setDefaultButton( Close );
|
|
setCaption( i18n("Network Window") );
|
|
restoreDialogSize( KMessConfig::instance()->getGlobalConfig( "NetworkWindow" ) );
|
|
|
|
// Load the ui file in a main qwidget
|
|
QWidget *mainWidget = new QWidget(this);
|
|
setupUi( mainWidget );
|
|
setMainWidget( mainWidget );
|
|
|
|
// Set up the behavior of the tab widget
|
|
connectionTabs_->setTabsClosable( true );
|
|
connect( connectionTabs_, &QTabWidget::tabCloseRequested, this, [this]( int index )
|
|
{
|
|
closeTab( connectionTabs_->widget( index ) );
|
|
} );
|
|
connect( connectionTabs_, SIGNAL( currentChanged(int) ), this, SLOT( slotUpdateWidgets(int) ) );
|
|
|
|
// Set up the behavior of the Send button
|
|
connect( sendCommandButton_, SIGNAL( released() ), this, SLOT( slotSendCommand() ) );
|
|
|
|
// Name our custom buttons and setup their behavior
|
|
setButtonText( User1, i18n("S&ave Tab") );
|
|
setButtonText( User2, i18n("C&lear Tab") );
|
|
setButtonText( User3, i18n("C&lose All Tabs") );
|
|
setButtonIcon( User1, QIcon::fromTheme( "document-save" ) );
|
|
setButtonIcon( User2, QIcon::fromTheme( "edit-clear-list" ) );
|
|
setButtonIcon( User3, QIcon::fromTheme( "edit-clear" ) );
|
|
connect( this, SIGNAL( user1Clicked() ), this, SLOT( saveCurrentTab() ) );
|
|
connect( this, SIGNAL( user2Clicked() ), this, SLOT( clearCurrentTab() ) );
|
|
connect( this, SIGNAL( user3Clicked() ), this, SLOT( closeAllTabs() ) );
|
|
|
|
// Set the default state of the ui widgets
|
|
sendStandardCmdRadio_->setChecked( true );
|
|
}
|
|
|
|
|
|
|
|
// Destructor
|
|
NetworkWindow::~NetworkWindow()
|
|
{
|
|
KConfigGroup group = KMessConfig::instance()->getGlobalConfig( "NetworkWindow" );
|
|
saveDialogSize( group );
|
|
|
|
// Disconnect all signals, prevent i.e. slotUpdateWidgets from being called
|
|
disconnect( connectionTabs_, 0, this, 0 );
|
|
disconnect( sendCommandButton_, 0, this, 0 );
|
|
disconnect( this, 0, this, 0 );
|
|
|
|
foreach( ConnectionEntry *entry, connectionViews_ )
|
|
{
|
|
delete entry->logView;
|
|
delete entry;
|
|
}
|
|
connectionViews_.clear();
|
|
}
|
|
|
|
|
|
// The internal function to add a message
|
|
void NetworkWindow::addMessage(ConnectionEntry *entry, QString html)
|
|
{
|
|
html = html.replace( " ", " ")
|
|
.replace( QRegExp("[\r\n] "), "\n ");
|
|
|
|
// Add spacer if previous message was some time ago.
|
|
QDateTime now = QDateTime::currentDateTime();
|
|
if( entry->lastMessage.secsTo( now ) > 20 )
|
|
{
|
|
entry->logView->append( "<p style='margin: 0; font-size: 8pt'> <!-- time spacer --></p>" );
|
|
}
|
|
|
|
entry->logView->append( html.toUtf8() );
|
|
entry->lastMessage = now;
|
|
|
|
// Update the widgets for the currently visible tab
|
|
slotUpdateWidgets();
|
|
}
|
|
|
|
|
|
|
|
// Add a log message to the last log entry
|
|
void NetworkWindow::addLogMessage(QObject *connection, const QString &message)
|
|
{
|
|
ConnectionEntry *entry = getConnectionEntry(connection);
|
|
QString html( "<p style='margin: 0'><font color='#333333'> " + message + "</font></p>\n" );
|
|
addMessage( entry, html );
|
|
}
|
|
|
|
|
|
|
|
// Add an incoming message to the log.
|
|
void NetworkWindow::addIncomingMessage(QObject *connection, const QByteArray &message)
|
|
{
|
|
ConnectionEntry *entry = getConnectionEntry(connection);
|
|
QString html( "<p style='margin: 0; color: #0033FF'><hr style='padding: 0; margin: 0;'>" + formatMessage(entry, true, message) + "</p>\n" );
|
|
addMessage(entry, html);
|
|
}
|
|
|
|
|
|
|
|
// Add an outgoing message to the log.
|
|
void NetworkWindow::addOutgoingMessage(QObject *connection, const QByteArray &message)
|
|
{
|
|
ConnectionEntry *entry = getConnectionEntry(connection);
|
|
QString html( "<p style='margin: 0; color: #FF3300'><hr style='padding: 0; margin: 0;'>" + formatMessage(entry, false, message) + "</p>\n" );
|
|
addMessage(entry, html);
|
|
}
|
|
|
|
|
|
|
|
// Inform the connection was closed
|
|
void NetworkWindow::connectionClosed(QObject *connection)
|
|
{
|
|
// Add the message
|
|
ConnectionEntry *entry = getConnectionEntry(connection);
|
|
entry->logView->append("<p style='margin:0; color: #C2C2C2'>the connection was closed<br>" + getTime() + "</p>\n");
|
|
entry->lastMessage = QDateTime::currentDateTime();
|
|
entry->isClosed = true;
|
|
// // Unregister the connection, so new attempts from the same class generate a new object.
|
|
// connectionViews_.remove(browser);
|
|
}
|
|
|
|
|
|
|
|
// Return a log description for a standard command.
|
|
QString NetworkWindow::describeCommand(const QString &command)
|
|
{
|
|
QString firstPart( command.left(3) ); // command[0] in normal code.
|
|
int errorCode = firstPart.toInt();
|
|
|
|
if( errorCode != 0 )
|
|
{
|
|
switch( errorCode )
|
|
{
|
|
// Command is a 3 digit error code.
|
|
case 200: return formatDescription("Error: Invalid syntax");
|
|
case 201: return formatDescription("Error: Invalid parameter");
|
|
case 205: return formatDescription("Error: Invalid principal; not an MSN account");
|
|
case 206: return formatDescription("Error: Domain name missing");
|
|
case 207: return formatDescription("Error: Already logged in");
|
|
case 208: return formatDescription("Error: Invalid principal; not an MSN account");
|
|
case 209: return formatDescription("Error: Nickname change illegal");
|
|
case 210: return formatDescription("Error: Principal list full");
|
|
case 213: return formatDescription("Error: Invalid rename request");
|
|
case 215: return formatDescription("Error: Principal already on list");
|
|
case 216: return formatDescription("Error: Principal not on list");
|
|
case 217: return formatDescription("Error: Principal not online");
|
|
case 218: return formatDescription("Error: Already in this mode");
|
|
case 219: return formatDescription("Error: Principal is in the opposite list");
|
|
case 223: return formatDescription("Error: Too many groups");
|
|
case 224: return formatDescription("Error: Invalid group");
|
|
case 225: return formatDescription("Error: Principal not in group");
|
|
case 227: return formatDescription("Error: Group not empty");
|
|
case 228: return formatDescription("Error: Group with same name already exists");
|
|
case 229: return formatDescription("Error: Group name too long");
|
|
case 230: return formatDescription("Error: Cannot remove group zero");
|
|
case 231: return formatDescription("Error: Invalid group");
|
|
case 240: return formatDescription("Error: Empty domain");
|
|
case 280: return formatDescription("Error: Switchboard server failed");
|
|
case 281: return formatDescription("Error: Transfer to switchboard failed");
|
|
case 282: return formatDescription("Error: P2P header error");
|
|
case 300: return formatDescription("Error: Required field missing");
|
|
case 302: return formatDescription("Error: Not logged in");
|
|
case 402: // same as 403
|
|
case 403: return formatDescription("Error: Error accessing contact list");
|
|
case 420: return formatDescription("Error: Invalid account permissions"); // CVR with unofficial client, non beta service tester
|
|
case 500: return formatDescription("Error: Service Temporarily Unavailable");
|
|
case 501: return formatDescription("Error: Database server error");
|
|
case 502: return formatDescription("Error: Protocol command is disabled");
|
|
case 510: return formatDescription("Error: File operation failed");
|
|
case 511: return formatDescription("Error: Account is banned");
|
|
case 520: return formatDescription("Error: Memory allocation failed");
|
|
case 540: return formatDescription("Error: Challenge response failed");
|
|
case 600: return formatDescription("Error: Server is busy");
|
|
case 601: return formatDescription("Error: Server is unavailable");
|
|
case 602: return formatDescription("Error: Peer nameserver is down");
|
|
case 603: return formatDescription("Error: Database connection failed");
|
|
case 604: return formatDescription("Error: Server is going down");
|
|
case 605: return formatDescription("Error: Server unavailable");
|
|
case 700: return formatDescription("Error: Connection failed");
|
|
case 710: return formatDescription("Error: Bad command parameter");
|
|
case 711: return formatDescription("Error: Write is blocking");
|
|
case 712: return formatDescription("Error: Session is overloaded");
|
|
case 713: return formatDescription("Error: Opening chat sessions too fast");
|
|
case 714: return formatDescription("Error: Too many chat sessions open");
|
|
case 715: return formatDescription("Error: Unexpected command value");
|
|
case 717: return formatDescription("Error: Bad friend file");
|
|
case 731: return formatDescription("Error: Not expected");
|
|
case 800: return formatDescription("Error: Changing names too fast");
|
|
case 911: return formatDescription("Error: Authentication ticket was incorrect");
|
|
case 913: return formatDescription("Error: not allowed when hidden");
|
|
case 923: return formatDescription("Error: Kids' Passport without parental consent");
|
|
case 924: return formatDescription("Error: passport account not verified yet");
|
|
case 928: return formatDescription("Error: bad ticket");
|
|
case 931: return formatDescription("Error: account not on this server"); // if using a cached NS instead of the main dispatch point.
|
|
|
|
case 900:
|
|
case 912:
|
|
case 918:
|
|
case 919:
|
|
case 921:
|
|
case 922: return formatDescription("Error: Server too busy");
|
|
|
|
case 914:
|
|
case 915:
|
|
case 916: return formatDescription("Error: Server unavailable");
|
|
case 917: return formatDescription("Error: authentication failed");
|
|
default: return formatDescription("Error, type is unknown!");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if( firstPart == "VER" )
|
|
{
|
|
return formatDescription("Protocol version negotiation");
|
|
}
|
|
else if ( firstPart == "CVR" )
|
|
{
|
|
return formatDescription("Application version exchange");
|
|
}
|
|
else if ( firstPart == "XFR" )
|
|
{
|
|
return formatDescription("Server transfer request");
|
|
}
|
|
else if ( firstPart == "USR" )
|
|
{
|
|
return formatDescription("User login information");
|
|
}
|
|
else if ( firstPart == "SYN" )
|
|
{
|
|
return formatDescription("Contactlist sync request");
|
|
}
|
|
else if ( firstPart == "CHG" )
|
|
{
|
|
return formatDescription("Client status message (change of online state, client capabilities or msn object)");
|
|
}
|
|
else if ( firstPart == "CHL" )
|
|
{
|
|
return formatDescription("Challenge query");
|
|
}
|
|
else if ( firstPart == "IRO" )
|
|
{
|
|
return formatDescription("Contact already in chat");
|
|
}
|
|
else if ( firstPart == "CAL" )
|
|
{
|
|
return formatDescription("Calling a contact to join chat");
|
|
}
|
|
else if ( firstPart == "RNG" )
|
|
{
|
|
return formatDescription("Invitation for a chat");
|
|
}
|
|
else if ( firstPart == "ANS" )
|
|
{
|
|
return formatDescription("Chat invitation answer with user login information");
|
|
}
|
|
else if ( firstPart == "JOI" )
|
|
{
|
|
return formatDescription("Contact joins chat");
|
|
}
|
|
else if ( firstPart == "FIL" )
|
|
{
|
|
return formatDescription("Indicates the size of the entire file");
|
|
}
|
|
else if ( firstPart == "TFR" )
|
|
{
|
|
return formatDescription("Start of file transfer");
|
|
}
|
|
}
|
|
|
|
// Default
|
|
// return QString();
|
|
return formatDescription( QString() ); // always add time.
|
|
}
|
|
|
|
|
|
|
|
// Return a log description for the direct connection preamble
|
|
QString NetworkWindow::describeDirectConnectionPreamble(const QByteArray &message)
|
|
{
|
|
if( message.size() == 4 && memcmp(message.data(), "\x04" "\0\0\0", 4) == 0 )
|
|
{
|
|
return formatDescription("Indicates the size of the next block");
|
|
}
|
|
else if( message.size() == 4 && memcmp(message.data(), "foo\0", 4) == 0 )
|
|
{
|
|
return formatDescription("The preamble of the direct connection: foo\\0");
|
|
}
|
|
else if( message.size() == 8 && memcmp(message.data(), "\x04" "\0\0\0foo\0", 8) == 0 )
|
|
{
|
|
return formatDescription("The block size followed by the preamble of the direct connection: foo\\0");
|
|
}
|
|
else
|
|
{
|
|
// return QString();
|
|
return formatDescription( QString() ); // always add time.
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Return a log description for the mime message
|
|
QString NetworkWindow::describeMimeMessage(const QString &mimeMessage)
|
|
{
|
|
QRegExp rx("Content-Type: ([^\r\n; ]+)");
|
|
if( rx.indexIn(mimeMessage) == -1 )
|
|
{
|
|
return formatDescription("Message could not be parsed!");
|
|
}
|
|
|
|
// Return a description based on the content type
|
|
QString contentType( rx.cap(1) );
|
|
|
|
|
|
// Notification server messages
|
|
if( contentType == "text/x-msmsgsprofile" )
|
|
{
|
|
// Message with all meta-data of the profile
|
|
return formatDescription("Profile data message");
|
|
}
|
|
else if( contentType == "text/x-msmsgsinitialemailnotification" )
|
|
{
|
|
// The initial status of the mailbox
|
|
return formatDescription("Initial email status message");
|
|
}
|
|
else if( contentType == "text/x-msmsgsemailnotification" )
|
|
{
|
|
// A new email was received
|
|
return formatDescription("New email notification message");
|
|
}
|
|
else if( contentType == "text/x-msmsgsactivemailnotification" )
|
|
{
|
|
// The number of unread emails has changed
|
|
return formatDescription("Email activity message");
|
|
}
|
|
else if( contentType == "application/x-msmsgssystemmessage" )
|
|
{
|
|
// The server is going down for maintenance
|
|
return formatDescription("System maintenance notification");
|
|
}
|
|
else if( contentType == "text/x-msmsgsinitialmdatanotification" )
|
|
{
|
|
// Another contact sent an Offline-IM, which is received upon logging-in
|
|
return formatDescription("Offline-IM notification or Hotmail email status");
|
|
}
|
|
else if( contentType == "text/x-msmsgsoimnotification" )
|
|
{
|
|
// Another contact sent an Offline-IM while we're in invisible mode.
|
|
return formatDescription("Offline-IM notification");
|
|
}
|
|
|
|
// Switchboard server messages
|
|
if( contentType == "text/plain" )
|
|
{
|
|
// A standard chat message
|
|
return formatDescription("Chat text message");
|
|
}
|
|
else if( contentType == "text/x-msmsgscontrol" )
|
|
{
|
|
// The contact is typing a message
|
|
return formatDescription("Typing notification message");
|
|
}
|
|
else if( contentType == "text/x-mms-emoticon" )
|
|
{
|
|
// List of custom emoticons
|
|
return formatDescription("Custom emoticon list");
|
|
}
|
|
else if( contentType == "text/x-mms-animemoticon" )
|
|
{
|
|
// List of animated custom emoticons
|
|
return formatDescription("Custom animated emoticon list");
|
|
}
|
|
else if( contentType == "image/gif" || contentType == "application/x-ms-ink" )
|
|
{
|
|
// Ink (drawing) message
|
|
return formatDescription("Ink drawing message");
|
|
}
|
|
else if( contentType == "text/x-clientcaps" )
|
|
{
|
|
// Third party client-capabilities message
|
|
return formatDescription("Third-party client capabilities message");
|
|
}
|
|
else if( contentType == "text/x-keepalive" )
|
|
{
|
|
// Third party keep-alive message
|
|
return formatDescription("Third-party keep-alive message");
|
|
}
|
|
else if( contentType == "text/x-msmsgsinvite" )
|
|
{
|
|
// Old invitation method
|
|
return formatDescription("Mime-style invitatation message");
|
|
}
|
|
else if( contentType == "application/x-msnmsgrp2p" )
|
|
{
|
|
// New invitation method
|
|
return formatDescription("P2P-style invitation message");
|
|
}
|
|
else if( contentType == "text/x-msnmsgr-datacast" )
|
|
{
|
|
// Datacast (nudge, voice-clip)
|
|
return formatDescription("Datacase message, nudge, wink or voice-clip");
|
|
}
|
|
|
|
|
|
// Unknown message type
|
|
return formatDescription("Unknown content-type: " + contentType + "!");
|
|
}
|
|
|
|
|
|
|
|
// Return a log description for the message
|
|
QString NetworkWindow::describeP2PMessage(const QByteArray &message, const int headerStart)
|
|
{
|
|
// Prepare to extract certain P2P header fields
|
|
QDataStream binaryStream( message );
|
|
binaryStream.setByteOrder( QDataStream::LittleEndian );
|
|
|
|
// 0 4 8 16 24 28 32 36 40 48
|
|
// |....|....|....|....|....|....|....|....|....|....|....|....|
|
|
// |sid |mid |offset |totalsize|size|flag|asid|auid|a-datasz |
|
|
|
|
quint32 sessionId, messageId, dataSize, flags, ackSessionId, ackUniqueId;
|
|
quint64 dataOffset, totalSize, ackDataSize;
|
|
|
|
// Get header fields
|
|
bool posFound = binaryStream.device()->seek( headerStart );
|
|
if( ! posFound )
|
|
{
|
|
return formatDescription("Message could not be parsed!");
|
|
}
|
|
|
|
binaryStream >> sessionId;
|
|
binaryStream >> messageId;
|
|
binaryStream >> dataOffset;
|
|
binaryStream >> totalSize;
|
|
binaryStream >> dataSize;
|
|
binaryStream >> flags;
|
|
binaryStream >> ackSessionId;
|
|
binaryStream >> ackUniqueId;
|
|
binaryStream >> ackDataSize;
|
|
|
|
switch( flags )
|
|
{
|
|
case 0:
|
|
case 0x1000000: // since WLM 2009
|
|
// Check for abuse of the P2P system (datacast emulation)
|
|
if( sessionId == 64 )
|
|
{
|
|
if( dataOffset > 0 )
|
|
{
|
|
QString partName( ( dataOffset + dataSize ) >= totalSize ? "last part" : "next part" );
|
|
return formatDescription("Ink message over P2P, " + partName);
|
|
}
|
|
else
|
|
{
|
|
return formatDescription("Ink message over P2P");
|
|
}
|
|
}
|
|
|
|
// Another awkward data message (webcam setup)
|
|
if( sessionId != 0
|
|
&& flags == 0
|
|
&& dataSize >= 18
|
|
&& message[ headerStart + 48 ] == '\x80' )
|
|
{
|
|
return formatDescription("Webcam setup message");
|
|
}
|
|
|
|
// No flags means the contents is a string with SLP-like mime fields, used to negotiate the session.
|
|
// datasize of 0 is not handed here, is an invalid packet.
|
|
// KMess does parse it as ACK message for compatibility with broken clients.
|
|
if( dataSize == 4 && dataOffset == 0 )
|
|
{
|
|
return formatDescription("P2P Data preparation message");
|
|
}
|
|
else if( dataOffset > 0 )
|
|
{
|
|
QString partName( ( dataOffset + dataSize ) >= totalSize ? "last part" : "next part" );
|
|
return formatDescription("P2P Session negotiation message, " + partName);
|
|
}
|
|
else if( sessionId == 0 && dataSize > 4 )
|
|
{
|
|
int preambleStart = headerStart + 48;
|
|
QString slpMessage ( QString::fromUtf8( message.data() + preambleStart, message.size() - preambleStart - 5 ) ); // last 5 is for the footer code.
|
|
bool isTransfer = slpMessage.contains("Content-Type: application/x-msnmsgr-trans");
|
|
if( slpMessage.startsWith("INVITE ") )
|
|
{
|
|
return isTransfer ? formatDescription("P2P Transfer invitation message")
|
|
: formatDescription("P2P Session invititation message");
|
|
}
|
|
else if( slpMessage.startsWith("MSNSLP/") )
|
|
{
|
|
int statusCode = slpMessage.section(' ', 1, 1).toInt();
|
|
if( statusCode == 200 )
|
|
{
|
|
return isTransfer ? formatDescription("P2P Transfer accept message")
|
|
: formatDescription("P2P Session accept message");
|
|
}
|
|
else if( statusCode == 404 )
|
|
{
|
|
return formatDescription("P2P Session error message");
|
|
}
|
|
else if( statusCode == 481 )
|
|
{
|
|
return formatDescription("P2P Session error message");
|
|
}
|
|
else if( statusCode == 500 )
|
|
{
|
|
return formatDescription("P2P Session error message");
|
|
}
|
|
else if( statusCode == 603 )
|
|
{
|
|
return isTransfer ? formatDescription("P2P Transfer decline message")
|
|
: formatDescription("P2P Session decline message");
|
|
}
|
|
else
|
|
{
|
|
formatDescription("P2P Session status message, type is unknown!");
|
|
}
|
|
}
|
|
else if( slpMessage.startsWith("ACK ") )
|
|
{
|
|
return formatDescription("P2P Session acknowledgement message (meaning unknown!)");
|
|
}
|
|
else if( slpMessage.startsWith("BYE ") )
|
|
{
|
|
return formatDescription("P2P Session bye message");
|
|
}
|
|
else
|
|
{
|
|
return formatDescription("P2P Session negotiation message, type is unknown!");
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 0x01:
|
|
return formatDescription("P2P Negative ack, message with ID " + QString::number(ackSessionId) +
|
|
" failed at byte " + QString::number(ackDataSize));
|
|
|
|
case 0x02:
|
|
if( dataSize == 4 )
|
|
{
|
|
return formatDescription("P2P Data preparation message (invalid flag!)");
|
|
}
|
|
else if ( ackDataSize == 4 )
|
|
{
|
|
return formatDescription("P2P Data preparation ack");
|
|
}
|
|
else
|
|
{
|
|
return formatDescription("P2P Ack message for message with"
|
|
" ID " + QString::number(ackSessionId) +
|
|
" and ackSid " + QString::number(ackUniqueId));
|
|
}
|
|
|
|
case 0x04:
|
|
return formatDescription("P2P Timeout message, session should terminate");
|
|
|
|
case 0x06:
|
|
return formatDescription("P2P Timeout message, still waiting for ACK message");
|
|
|
|
case 0x08:
|
|
// The logs of the official client call this a "RST" message.
|
|
return formatDescription("P2P Reset message for message with "
|
|
"ackSid " + QString::number(ackSessionId) + ", session should terminate");
|
|
|
|
case 0x20:
|
|
case 0x1000020: // since WLM 2009
|
|
return formatDescription("P2P MsnObject data,"
|
|
" bytes " + QString::number(dataOffset) + "-" + QString::number(dataOffset + dataSize) +
|
|
" of " + QString::number(totalSize));
|
|
|
|
case 0x40:
|
|
return formatDescription("P2P Close ack, sender of message with ID " + QString::number(ackSessionId) + " aborted it's own data stream");
|
|
|
|
case 0x80:
|
|
return formatDescription("P2P Close ack, receiver of message with ID " + QString::number(ackSessionId) + " aborted the data stream");
|
|
|
|
case 0x100:
|
|
return formatDescription("P2P Direct-connection handshake");
|
|
|
|
case 0x1000030:
|
|
return formatDescription("P2P File data,"
|
|
" bytes " + QString::number(dataOffset) + "-" + QString::number(dataOffset + dataSize) +
|
|
" of " + QString::number(totalSize));
|
|
}
|
|
|
|
return formatDescription("P2P Message, type is unknown!");
|
|
}
|
|
|
|
|
|
|
|
// Destroy the singleton.
|
|
void NetworkWindow::destroy()
|
|
{
|
|
if( ! instance_ )
|
|
{
|
|
// The singleton has already been destroyed, or was never used
|
|
return;
|
|
}
|
|
|
|
delete instance_;
|
|
instance_ = 0;
|
|
}
|
|
|
|
|
|
|
|
// Format the description to be displayed
|
|
QString NetworkWindow::formatDescription(const QString &description)
|
|
{
|
|
return "<br>" + getTime() + "<font color='#333333'> " + description + "</font>\n";
|
|
}
|
|
|
|
|
|
|
|
// Format the message to be displayed
|
|
QString NetworkWindow::formatMessage(ConnectionEntry *entry, bool incoming, const QByteArray &message)
|
|
{
|
|
// If the connection type is unknown, auto-detect it.
|
|
if( entry->type == TYPE_UNKNOWN )
|
|
{
|
|
// Try to detemine which type of connection this is.
|
|
// TODO: As of Qt4 use QByteArray functions instead of memcmp().
|
|
if( ( message.size() == 4 && memcmp(message.data(), "\x04" "\0\0\0", 4) == 0 )
|
|
|| ( message.size() == 8 && memcmp(message.data(), "\x04" "\0\0\0foo\0", 8) == 0 ) )
|
|
{
|
|
// Start of direct connection: size field of 4 + foo\0
|
|
entry->type = TYPE_DC;
|
|
}
|
|
else if( message.size() >= 10 )
|
|
{
|
|
if( memcmp(message.data(), "VER MSNFTP", 10) == 0 )
|
|
{
|
|
entry->type = TYPE_FTP;
|
|
}
|
|
else if( memcmp(message.data(), "VER 0 MSNP", 10) == 0
|
|
|| memcmp(message.data(), "VER 1 MSNP", 10) == 0 )
|
|
{
|
|
entry->type = TYPE_NS;
|
|
}
|
|
else if( memcmp(message.data(), "USR ", 4) == 0
|
|
|| memcmp(message.data(), "ANS ", 4) == 0)
|
|
{
|
|
entry->type = TYPE_SB;
|
|
}
|
|
else if( memcmp(message.data(), "GET ", 4) == 0
|
|
|| memcmp(message.data(), "POST ", 5) == 0
|
|
|| memcmp(message.data(), "<?xml", 5) == 0)
|
|
{
|
|
entry->type = TYPE_HTTP;
|
|
}
|
|
}
|
|
|
|
// If no type is found, assume it's a standard MSN connection.
|
|
if( entry->type == TYPE_UNKNOWN )
|
|
{
|
|
kmWarning() << "Could not determine connection type for message: " << message.data();
|
|
entry->type = TYPE_NS;
|
|
}
|
|
}
|
|
|
|
|
|
// Format the message.
|
|
QString logMessage;
|
|
switch( entry->type )
|
|
{
|
|
// Messages from a DC are always sent as binary
|
|
case TYPE_DC:
|
|
if( message.size() < 48 )
|
|
{
|
|
// Not a p2p message, but the length field sent in advance.
|
|
logMessage += formatRawData(incoming, message, 0, message.size());
|
|
logMessage += describeDirectConnectionPreamble(message);
|
|
}
|
|
else
|
|
{
|
|
// A p2p message from the direct connection
|
|
logMessage += formatP2PMessage(incoming, message, 0);
|
|
logMessage += describeP2PMessage(message, 0);
|
|
}
|
|
break;
|
|
|
|
// After the TFR is received, the FTP connection sends raw data.
|
|
case TYPE_FTP_RAW:
|
|
if( message.size() == 3 )
|
|
{
|
|
// This is a control message
|
|
logMessage += formatRawData(incoming, message, 0, message.size());
|
|
if( message[0] == 0 )
|
|
{
|
|
logMessage += formatDescription("Indicates the size of the next block");
|
|
}
|
|
else if( memcmp(message.data(), "\x01" "\0\0", 3) == 0 )
|
|
{
|
|
logMessage += formatDescription("Cancel message");
|
|
}
|
|
break;
|
|
}
|
|
else if( message.size() == 14 && memcmp(message.data(), "BYE 16777989\r\n", 14) == 0 )
|
|
{
|
|
// This is the last BYE message
|
|
entry->type = TYPE_FTP;
|
|
// fallthrough
|
|
}
|
|
else
|
|
{
|
|
logMessage += "<font color=gray>(" + QString::number(message.size()) + " bytes of binary data)</font>";
|
|
break;
|
|
}
|
|
|
|
// Assume it's a standard connection.
|
|
default:
|
|
// Convert the message to UTF-8
|
|
QString utf8Message( QString::fromUtf8( message.data(), message.size() ) );
|
|
|
|
// Treat Mime messages with P2P content different.
|
|
if( utf8Message.contains("Content-Type: application/x-msnmsgrp2p") )
|
|
{
|
|
// Find end of Mime, start of binary P2P
|
|
int mimeEnd = utf8Message.indexOf("\r\n\r\n");
|
|
if( mimeEnd != -1 )
|
|
{
|
|
// Extract the first utf-8 mime part, locate start of p2p header
|
|
QString p2pMimePart( utf8Message.left(mimeEnd + 4) );
|
|
int headerStart = p2pMimePart.toUtf8().size(); // convert mimeEnd index, utf-8 may have double characters
|
|
|
|
// Add the mime part and binary p2p part
|
|
logMessage += formatString( p2pMimePart );
|
|
logMessage += formatP2PMessage(incoming, message, headerStart);
|
|
logMessage += describeP2PMessage(message, headerStart);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Whole message is in utf-8
|
|
|
|
// First hide things we don't want to log.
|
|
if( entry->type == TYPE_HTTP )
|
|
{
|
|
// Hide passwords from the passport login, if they are included in the message
|
|
int passStart = utf8Message.indexOf("<wsse:Password>");
|
|
if( passStart != -1 )
|
|
{
|
|
passStart += 15; // size of xml tag
|
|
int passEnd = utf8Message.indexOf("</wsse:Password>", passStart);
|
|
int passLen = ( passEnd - passStart );
|
|
utf8Message = utf8Message.replace( passStart, passLen, QString( passLen, '*' ) );
|
|
}
|
|
}
|
|
|
|
// Convert to HTML.
|
|
// Remove last <br> because KTextBrowser::append() already adds it.
|
|
logMessage = formatString( utf8Message );
|
|
logMessage = logMessage.replace(QRegExp("<br>$"), QString());
|
|
|
|
if( entry->type == TYPE_FTP
|
|
|| entry->type == TYPE_NS
|
|
|| entry->type == TYPE_SB )
|
|
{
|
|
if( utf8Message.startsWith("MIME-Version:") )
|
|
{
|
|
logMessage += describeMimeMessage( utf8Message );
|
|
}
|
|
else
|
|
{
|
|
logMessage += describeCommand( utf8Message );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// At a certain point, a MSNFTP connection starts sending raw data.
|
|
// Detect this so the next formatMessage() invocation handles this.
|
|
if( entry->type == TYPE_FTP )
|
|
{
|
|
if( message.size() >= 3 && memcmp(message.data(), "TFR", 3) == 0 )
|
|
{
|
|
entry->type = TYPE_FTP_RAW;
|
|
}
|
|
}
|
|
|
|
return logMessage;
|
|
}
|
|
|
|
|
|
|
|
// Format a msnp2p message to be displayed.
|
|
QString NetworkWindow::formatP2PMessage(bool incoming, const QByteArray &message, const int headerStart)
|
|
{
|
|
QString logMessage;
|
|
|
|
// Prepare to extract certain P2P header fields
|
|
QDataStream binaryStream( message );
|
|
binaryStream.setByteOrder( QDataStream::LittleEndian );
|
|
int p2pDataStart = headerStart + 48;
|
|
bool posFound;
|
|
|
|
// 0 4 8 16 24 28 32 36 40 48
|
|
// |....|....|....|....|....|....|....|....|....|....|....|....|
|
|
// |sid |mid |offset |totalsize|size|flag|asid|auid|a-datasz |
|
|
|
|
quint32 sessionId, messageId, dataSize, flags, ackSessionId, ackUniqueId;
|
|
quint64 dataOffset, totalSize, ackDataSize;
|
|
|
|
// Get header fields
|
|
posFound = binaryStream.device()->seek( headerStart ); // return value ignored.
|
|
binaryStream >> sessionId;
|
|
binaryStream >> messageId;
|
|
binaryStream >> dataOffset;
|
|
binaryStream >> totalSize;
|
|
binaryStream >> dataSize;
|
|
binaryStream >> flags;
|
|
binaryStream >> ackSessionId;
|
|
binaryStream >> ackUniqueId;
|
|
binaryStream >> ackDataSize;
|
|
|
|
#ifdef KMESSTEST
|
|
KMESS_ASSERT( posFound );
|
|
#endif
|
|
|
|
// If the data size is completely wrong, we're dealing with partially sent data here,
|
|
// caused by DirectConnectionBase::slotSocketReadyWrite().
|
|
if( dataSize > 3000 )
|
|
{
|
|
return "<font color=gray>(" + QString::number( message.size() ) + " bytes of binary data, flushed afterwards, can't parse)</font>";
|
|
}
|
|
|
|
// Show header information
|
|
logMessage += formatRawData(incoming, message, headerStart, 48, 4, 16);
|
|
|
|
// Add header debug details
|
|
logMessage += "<font color='#333333'>";
|
|
logMessage += "\n<br>session id: " + QString::number(sessionId);
|
|
logMessage += "\n<br>message id: " + QString::number(messageId);
|
|
logMessage += "\n<br>offset: " + QString::number(dataOffset);
|
|
logMessage += "\n<br>total: " + QString::number(totalSize);
|
|
logMessage += "\n<br>size: " + QString::number(dataSize);
|
|
logMessage += "\n<br>flags: 0x" + QString::number(flags, 16);
|
|
if( flags == 0x100 )
|
|
{
|
|
// For the handshake message, the last three fields are replaced with the nonce.
|
|
logMessage += "\n<br>nonce: " + P2PMessage::extractNonce( message.data(), headerStart + 32 );
|
|
}
|
|
else
|
|
{
|
|
logMessage += "\n<br>ackSid: " + QString::number(ackSessionId);
|
|
logMessage += "\n<br>ackUid: " + QString::number(ackUniqueId);
|
|
logMessage += "\n<br>ackSize: " + QString::number(ackDataSize);
|
|
}
|
|
|
|
logMessage += "</font>";
|
|
|
|
// The message a utf-8 (MSNSLP) payload if the session id is zero.
|
|
const char *dataPtr = ( message.data() + p2pDataStart );
|
|
bool hasSlpPayload = ( sessionId == 0 && dataSize > 0 );
|
|
int safeEnd = qMin( message.size(), (int) ( p2pDataStart + dataSize ) );
|
|
int safeSize = qMin( (int) dataSize, ( message.size() - p2pDataStart ) );
|
|
if( hasSlpPayload )
|
|
{
|
|
// See if the SLP body is terminated with a final null character (should be).
|
|
int slpLength = safeSize;
|
|
bool hasSlpNullEnd = ( message[ p2pDataStart + slpLength - 1 ] == 0x00 );
|
|
if( hasSlpNullEnd )
|
|
{
|
|
slpLength--;
|
|
}
|
|
|
|
// Display the SLP body
|
|
QString slpMessage( QString::fromUtf8( dataPtr, slpLength ) );
|
|
logMessage += "\n<br>" + formatString( slpMessage );
|
|
|
|
// Add message footer
|
|
if( hasSlpNullEnd )
|
|
{
|
|
// Display the \0 that was dropped in the QString::fromUtf8() call.
|
|
logMessage += formatRawData( incoming, message, slpLength, 1 );
|
|
}
|
|
}
|
|
else if( dataSize >= 18
|
|
&& message.size() > p2pDataStart
|
|
&& message[ p2pDataStart ] == '\x80' )
|
|
{
|
|
// Webcam invitation
|
|
QString camMessage( QString::fromUtf16( reinterpret_cast<const char16_t *>( dataPtr + 10 ),
|
|
( safeSize - 10 - 1 ) / int( sizeof( char16_t ) ) ) );
|
|
|
|
logMessage += "\n<br>" + formatRawData( incoming, message, p2pDataStart, 10, 1 ) // 1 per col.
|
|
+ "\n<br>" + formatString( camMessage )
|
|
+ "\n<br>" + formatRawData( incoming, message, safeEnd - 2, 2 ); // final null (in utf16).
|
|
}
|
|
else
|
|
{
|
|
// Don't display the data.
|
|
logMessage += "\n<br><font color=gray>(" + QString::number(dataSize) + " bytes of binary data)</font>";
|
|
}
|
|
|
|
// Display the remaining bytes, if any.
|
|
// This is the footer when the p2p message is sent over the SB.
|
|
if( safeEnd < message.size() )
|
|
{
|
|
logMessage += "<br>" + formatRawData( incoming, message, safeEnd, message.size() - safeEnd );
|
|
}
|
|
|
|
return logMessage;
|
|
}
|
|
|
|
|
|
|
|
// Format a utf-8 string to be displayed
|
|
QString NetworkWindow::formatRawData(bool incoming, const QByteArray &message, const int start, const int length, const int bytesPerCol, const int bytesPerRow)
|
|
{
|
|
QString logMessage;
|
|
logMessage += "<tt><font color=";
|
|
logMessage += (incoming? "#4747C2" : "#CC334D");
|
|
logMessage += ">";
|
|
|
|
// If length field is corrupt (e.g. 432712149, avoid looping though 4GB of memory..
|
|
int safeLength = qMin( length, message.size() - start );
|
|
|
|
for(int i = 0; i < safeLength; i++)
|
|
{
|
|
// Add spacers between ints, make it span multiple rows.
|
|
if( i > 0 )
|
|
{
|
|
if( i % bytesPerRow == 0 )
|
|
{
|
|
logMessage += "<br>";
|
|
}
|
|
else if( i % bytesPerCol == 0 )
|
|
{
|
|
logMessage += " ";
|
|
}
|
|
}
|
|
|
|
// Convert to hex string
|
|
const char *hexMap = "0123456789abcdef";
|
|
char byte = message[start + i];
|
|
int upper = (byte & 0xf0) >> 4;
|
|
int lower = (byte & 0x0f);
|
|
logMessage += hexMap[upper];
|
|
logMessage += hexMap[lower];
|
|
}
|
|
logMessage += "</font></tt>";
|
|
|
|
return logMessage;
|
|
}
|
|
|
|
|
|
|
|
// Format a utf-8 string to be displayed
|
|
QString NetworkWindow::formatString(const QString &message)
|
|
{
|
|
// Parse any HTML characters.
|
|
QString logMessage( Qt::escape( message ) );
|
|
|
|
// Format newline characters
|
|
logMessage = logMessage.replace("\r\n", "<tt><font color=gray>\\r\\n</font></tt><br>");
|
|
logMessage = logMessage.replace("\n", "<tt><font color=gray>\\n</font></tt><br>");
|
|
logMessage = logMessage.replace("\r", "<tt><font color=gray>\\r</font></tt><br>");
|
|
|
|
return logMessage;
|
|
}
|
|
|
|
|
|
|
|
// Find the tab for the connection, or create a new one.
|
|
NetworkWindow::ConnectionEntry * NetworkWindow::getConnectionEntry(QObject *connection)
|
|
{
|
|
#ifdef KMESSTEST
|
|
KMESS_ASSERT( connection != 0 ); // no KMESS_NULL(), this is debug code already.
|
|
#endif
|
|
|
|
// Find the entry
|
|
ConnectionEntry *entry = connectionViews_.value( connection, 0 );
|
|
|
|
// If the view didn't exist before, create a new one.
|
|
if( entry == 0 )
|
|
{
|
|
// Create new entry
|
|
entry = new ConnectionEntry;
|
|
entry->logView = new KTextBrowser( connectionTabs_ );
|
|
entry->logView->setObjectName( connection->objectName() + "_log" );
|
|
entry->type = TYPE_UNKNOWN;
|
|
entry->lastMessage = QDateTime::currentDateTime();
|
|
connectionViews_.insert( connection, entry );
|
|
|
|
// Add new tab
|
|
connectionTabs_->addTab( entry->logView, QIcon::fromTheme( "edit-select-all" ), connection->objectName() );
|
|
|
|
// Enable the Close All Tabs button
|
|
enableButton( User3, ( connectionViews_.count() > 1 ) );
|
|
}
|
|
|
|
// Initialize new connections and reinitialize old ones as open
|
|
entry->isClosed = false;
|
|
|
|
return entry;
|
|
}
|
|
|
|
|
|
|
|
// Return the current time.
|
|
QString NetworkWindow::getTime() const
|
|
{
|
|
return "<font color=black size=1>" + QTime::currentTime().toString( "hh:mm:ss:zzz" ) + "</font>";
|
|
}
|
|
|
|
|
|
|
|
// Set the title of the connection in the display
|
|
void NetworkWindow::setConnectionTitle(QObject *connection, const QString &title)
|
|
{
|
|
ConnectionEntry *entry = getConnectionEntry(connection);
|
|
|
|
// If empty, set the initial label. Otherwise add a IRC-like rename message
|
|
if( entry->type == TYPE_UNKNOWN )
|
|
{
|
|
connectionTabs_->setTabText( connectionTabs_->indexOf( entry->logView ), title );
|
|
entry->logView->append("<p><font color='#C2C2C2'>Log started by " + QString(connection->objectName()) + "</font></p>");
|
|
}
|
|
else
|
|
{
|
|
entry->logView->append("<p><font color='#C2C2C2'>re-initialized as: " + title + "</font></p>");
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Show the window if there are connection logs to show
|
|
void NetworkWindow::show()
|
|
{
|
|
if( connectionTabs_->count() < 1 )
|
|
{
|
|
KMessageBox::error( this, i18n("No connections are present.\nCannot open the Network Window.") );
|
|
return;
|
|
}
|
|
|
|
KMessDialog::show();
|
|
}
|
|
|
|
|
|
|
|
// The 'save tab' button was pressed.
|
|
void NetworkWindow::saveCurrentTab()
|
|
{
|
|
if( connectionTabs_->currentWidget() == 0 )
|
|
return;
|
|
|
|
QString path;
|
|
QString name( connectionTabs_->tabText( connectionTabs_->currentIndex() ) );
|
|
|
|
path = QFileDialog::getSaveFileName( this, QString(), QString(), "*.html" );
|
|
if( path.isEmpty() )
|
|
return;
|
|
|
|
kmDebug() << "NetworkWindow - saving network log for window '" << name << "' to '" << path << "'.";
|
|
|
|
bool failed = false;
|
|
|
|
// Create and open the file.
|
|
QFile file( path );
|
|
if( ! file.open( QIODevice::WriteOnly ) )
|
|
{
|
|
failed = true;
|
|
kmWarning() << "File save failed - couldn't open file.";
|
|
}
|
|
|
|
// Output the HTML with the right text encoding
|
|
QString text( static_cast<KTextBrowser *>( connectionTabs_->currentWidget() )->toHtml() );
|
|
QTextStream textStream( &file );
|
|
textStream.setCodec( QTextCodec::codecForLocale() );
|
|
textStream << text;
|
|
file.close();
|
|
file.flush();
|
|
|
|
if( failed || ! file.exists() )
|
|
{
|
|
KMessageBox::error( this, i18n("Could not save the Network Window log. Make sure you have permission to write in the folder where it is being saved.") );
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// The 'clear tab' button was pressed.
|
|
void NetworkWindow::clearCurrentTab()
|
|
{
|
|
if( connectionTabs_->currentWidget() == 0 )
|
|
return;
|
|
|
|
static_cast<KTextBrowser *>( connectionTabs_->currentWidget() )->clear();
|
|
}
|
|
|
|
|
|
|
|
// The 'close all tabs' button was pressed.
|
|
void NetworkWindow::closeAllTabs()
|
|
{
|
|
// Activate the tab of the main connection
|
|
connectionTabs_->setCurrentIndex( 0 );
|
|
|
|
// Get the main connection widget
|
|
QWidget *mainConnectionWidget = connectionTabs_->widget( 0 );
|
|
|
|
// Loop through all tabs
|
|
foreach( ConnectionEntry *entry, connectionViews_ )
|
|
{
|
|
// Don't close the main connection tab
|
|
if( entry->logView == mainConnectionWidget )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
connectionTabs_->removeTab( connectionTabs_->indexOf( entry->logView ) );
|
|
closeTab( entry->logView );
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// A 'close tab' button was pressed.
|
|
void NetworkWindow::closeTab( QWidget *removedWidget )
|
|
{
|
|
if( connectionTabs_->indexOf( removedWidget ) == 0 )
|
|
{
|
|
KMessageBox::information( this, i18n("Cannot close the main connection tab.") );
|
|
return;
|
|
}
|
|
|
|
// Remove the widget from the list of views
|
|
QHashIterator<void*,ConnectionEntry*>it( connectionViews_ );
|
|
while( it.hasNext() )
|
|
{
|
|
it.next();
|
|
if( it.value()->logView == removedWidget )
|
|
{
|
|
connectionViews_.remove( it.key() );
|
|
delete it.value();
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Delete the page of the removed item
|
|
delete removedWidget;
|
|
|
|
// Disable the Close All Tabs button when there are no tabs
|
|
enableButton( User3, ( connectionViews_.count() > 1 ) );
|
|
}
|
|
|
|
|
|
|
|
// Return the primary instance of the network window.
|
|
NetworkWindow* NetworkWindow::instance()
|
|
{
|
|
// Create on demand
|
|
if( instance_ == 0 )
|
|
{
|
|
instance_ = new NetworkWindow();
|
|
}
|
|
|
|
// Return active instance
|
|
return instance_;
|
|
}
|
|
|
|
|
|
// A tab was selected.
|
|
void NetworkWindow::slotUpdateWidgets( int selectedTab )
|
|
{
|
|
bool enable = false;
|
|
|
|
// Automatically obtain the tab index instead of getting it as a parameter
|
|
if( selectedTab < -1 )
|
|
{
|
|
selectedTab = connectionTabs_->currentIndex();
|
|
}
|
|
|
|
// If there are open tabs, find out which one is selected.
|
|
// If there are none, just disable the command sending group
|
|
if( selectedTab != -1 )
|
|
{
|
|
QWidget *currentWidget = connectionTabs_->currentWidget();
|
|
QHashIterator<void*,ConnectionEntry*> it( connectionViews_ );
|
|
|
|
// Iterate through all connections to find whether the current one is connected or not
|
|
while( it.hasNext() )
|
|
{
|
|
it.next();
|
|
if( it.value()->logView == currentWidget )
|
|
{
|
|
// The active tab's connection is active, enable the widgets
|
|
if( ! it.value()->isClosed )
|
|
{
|
|
enable = true;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
commandSendingGroup_->setEnabled( enable );
|
|
}
|
|
|
|
|
|
|
|
// Send a command
|
|
void NetworkWindow::slotSendCommand()
|
|
{
|
|
// Get the text fields' text
|
|
QString command( commandEdit_->text().toUpper() );
|
|
QString payload( payloadEdit_->toPlainText() );
|
|
|
|
// Empty commands can't be sent
|
|
if( command.isEmpty() || connectionTabs_->currentIndex() == -1 )
|
|
{
|
|
commandEdit_->setFocus();
|
|
return;
|
|
}
|
|
|
|
|
|
// Warn the developer about what he or she is doing.
|
|
int result = KMessageBox::warningContinueCancel( this,
|
|
i18n( "Sending commands to the server is a risky operation.<br />"
|
|
"If you do not know how to exactly do it, "
|
|
"you could be lucky and just get disconnected, or <i>you may "
|
|
"incur in more serious consequences</i>.<br />"
|
|
"You have been warned!<br />"
|
|
"<b>Do you want to continue sending this message?</b>" ),
|
|
i18n( "Network Window" ),
|
|
KStandardGuiItem::cont(),
|
|
KStandardGuiItem::cancel(),
|
|
"sendServerCommandWarning" );
|
|
// Bail out, just in time
|
|
if( result != KMessageBox::Continue )
|
|
{
|
|
return;
|
|
}
|
|
|
|
// The developer knows what he or she is doing, send the command
|
|
|
|
// Replace all newlines with the kind used by the server
|
|
payload.replace( QRegExp( "\r?\n" ), "\r\n" );
|
|
|
|
// Empty payloads can, and will be replaced with a single newline
|
|
if( payload.isEmpty() )
|
|
{
|
|
payload = "\r\n";
|
|
}
|
|
else if( ! payload.endsWith( "\r\n" ) )
|
|
{
|
|
// Warn the developer that the payloads must end with a newline
|
|
int result = KMessageBox::warningTwoActions( this,
|
|
i18n( "The payload you are trying to send does not end with the "
|
|
"required newline ('\\r\\n')!<br />"
|
|
"Do you want KMess to add it for you?" ),
|
|
i18n( "Network Window" ),
|
|
KGuiItem( i18n( "Yes" ) ),
|
|
KGuiItem( i18n( "No" ) ),
|
|
"noPayloadWarning" );
|
|
// Append the missing newline to the payload
|
|
if( result == KMessageBox::PrimaryAction )
|
|
{
|
|
payload.append( "\r\n" );
|
|
}
|
|
}
|
|
|
|
// Find the currently active tab
|
|
QWidget *currentWidget = connectionTabs_->currentWidget();
|
|
QHashIterator<void*,ConnectionEntry*> it( connectionViews_ );
|
|
void *connection;
|
|
const ConnectionEntry *entry;
|
|
|
|
// Iterate through all connections to find out which one's tab is selected in the UI
|
|
while( it.hasNext() )
|
|
{
|
|
it.next();
|
|
|
|
// Save the entry, we'll need it later
|
|
entry = it.value();
|
|
|
|
// If the current tab is for this connection..
|
|
if( entry->logView == currentWidget )
|
|
{
|
|
// .. disallow sending commands to disconnected connections
|
|
if( entry->isClosed )
|
|
{
|
|
commandEdit_->setFocus();
|
|
return;
|
|
}
|
|
|
|
// ..save the tab's connection to send commands there
|
|
connection = it.key();
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Send the command to the right type of connection
|
|
MsnNotificationConnection *nsConnection;
|
|
switch( entry->type )
|
|
{
|
|
case TYPE_NS:
|
|
case TYPE_SB:
|
|
nsConnection = static_cast<MsnNotificationConnection*>( connection );
|
|
|
|
// Disallow sending commands to disconnected connections
|
|
if( ! nsConnection->isConnected() )
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Send the type of command the developer wants
|
|
if( sendStandardCmdRadio_->isChecked() )
|
|
{
|
|
nsConnection->sendCommand( command, payload );
|
|
}
|
|
else
|
|
{
|
|
nsConnection->sendMimeMessage( MsnNotificationConnection::ACK_ALWAYS, MimeMessage( payload ) );
|
|
}
|
|
return;
|
|
|
|
// TODO: Enable sending commands to these types of connections.
|
|
case TYPE_UNKNOWN:
|
|
case TYPE_FTP:
|
|
case TYPE_FTP_RAW:
|
|
case TYPE_DC:
|
|
case TYPE_HTTP:
|
|
default:
|
|
KMessageBox::error( 0, i18n( "Cannot send commands to this kind of connection!" ) );
|
|
return;
|
|
}
|
|
}
|