/* * xmlprotocol.cpp - state machine for 'jabber-like' protocols * Copyright (C) 2004 Justin Karneges * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1307 USA * */ #include"xmlprotocol.h" #include"bytestream.h" using namespace XMPP; // stripExtraNS // // This function removes namespace information from various nodes for // display purposes only (the element is pretty much useless for processing // after this). We do this because QXml is a bit overzealous about outputting // redundant namespaces. static QDomElement stripExtraNS(const QDomElement &e) { // find closest parent with a namespace QDomNode par = e.parentNode(); while(!par.isNull() && par.namespaceURI().isNull()) par = par.parentNode(); bool noShowNS = false; if(!par.isNull() && par.namespaceURI() == e.namespaceURI()) noShowNS = true; // build qName (prefix:localName) QString qName; if(!e.prefix().isEmpty()) qName = e.prefix() + ':' + e.localName(); else qName = e.tagName(); QDomElement i; uint x; if(noShowNS) i = e.ownerDocument().createElement(qName); else i = e.ownerDocument().createElementNS(e.namespaceURI(), qName); // copy attributes QDomNamedNodeMap al = e.attributes(); for(x = 0; x < al.count(); ++x) { QDomAttr a = al.item(x).cloneNode().toAttr(); // don't show xml namespace if(a.namespaceURI() == NS_XML) i.setAttribute(QString("xml:") + a.name(), a.value()); else i.setAttributeNodeNS(a); } // copy children QDomNodeList nl = e.childNodes(); for(x = 0; x < nl.count(); ++x) { QDomNode n = nl.item(x); if(n.isElement()) i.appendChild(stripExtraNS(n.toElement())); else i.appendChild(n.cloneNode()); } return i; } // xmlToString // // This function converts a QDomElement into a QString, using stripExtraNS // to make it pretty. static QString xmlToString(const QDomElement &e, const QString &fakeNS, const QString &fakeQName, bool clip) { QDomElement i = e.cloneNode().toElement(); // It seems QDom can only have one namespace attribute at a time (see docElement 'HACK'). // Fortunately we only need one kind depending on the input, so it is specified here. QDomElement fake = e.ownerDocument().createElementNS(fakeNS, fakeQName); fake.appendChild(i); fake = stripExtraNS(fake); QString out; { QTextStream ts(&out, IO_WriteOnly); fake.firstChild().save(ts, 0); } // 'clip' means to remove any unwanted (and unneeded) characters, such as a trailing newline if(clip) { int n = out.findRev('>'); out.truncate(n+1); } return out; } // createRootXmlTags // // This function creates three QStrings, one being an processing // instruction, and the others being the opening and closing tags of an // element, and . This basically allows us to get the raw XML // text needed to open/close an XML stream, without resorting to generating // the XML ourselves. This function uses QDom to do the generation, which // ensures proper encoding and entity output. static void createRootXmlTags(const QDomElement &root, QString *xmlHeader, QString *tagOpen, QString *tagClose) { QDomElement e = root.cloneNode(false).toElement(); // insert a dummy element to ensure open and closing tags are generated QDomElement dummy = e.ownerDocument().createElement("dummy"); e.appendChild(dummy); // convert to xml->text QString str; { QTextStream ts(&str, IO_WriteOnly); e.save(ts, 0); } // parse the tags out int n = str.find('<'); int n2 = str.find('>', n); ++n2; *tagOpen = str.mid(n, n2-n); n2 = str.findRev('>'); n = str.findRev('<'); ++n2; *tagClose = str.mid(n, n2-n); // generate a nice xml processing header *xmlHeader = ""; } //---------------------------------------------------------------------------- // Protocol //---------------------------------------------------------------------------- XmlProtocol::TransferItem::TransferItem() { } XmlProtocol::TransferItem::TransferItem(const QString &_str, bool sent, bool external) { isString = true; isSent = sent; isExternal = external; str = _str; } XmlProtocol::TransferItem::TransferItem(const QDomElement &_elem, bool sent, bool external) { isString = false; isSent = sent; isExternal = external; elem = _elem; } XmlProtocol::XmlProtocol() { init(); } XmlProtocol::~XmlProtocol() { } void XmlProtocol::init() { incoming = false; peerClosed = false; closeWritten = false; } void XmlProtocol::reset() { init(); elem = QDomElement(); tagOpen = QString(); tagClose = QString(); xml.reset(); outData.resize(0); trackQueue.clear(); transferItemList.clear(); } void XmlProtocol::addIncomingData(const QByteArray &a) { xml.appendData(a); } QByteArray XmlProtocol::takeOutgoingData() { QByteArray a = outData.copy(); outData.resize(0); return a; } void XmlProtocol::outgoingDataWritten(int bytes) { for(QValueList::Iterator it = trackQueue.begin(); it != trackQueue.end();) { TrackItem &i = *it; // enough bytes? if(bytes < i.size) { i.size -= bytes; break; } int type = i.type; int id = i.id; int size = i.size; bytes -= i.size; it = trackQueue.remove(it); if(type == TrackItem::Raw) { // do nothing } else if(type == TrackItem::Close) { closeWritten = true; } else if(type == TrackItem::Custom) { itemWritten(id, size); } } } bool XmlProtocol::processStep() { Parser::Event pe; notify = 0; transferItemList.clear(); if(state != Closing && (state == RecvOpen || stepAdvancesParser())) { // if we get here, then it's because we're in some step that advances the parser pe = xml.readNext(); if(!pe.isNull()) { // note: error/close events should be handled for ALL steps, so do them here switch(pe.type()) { case Parser::Event::DocumentOpen: { transferItemList += TransferItem(pe.actualString(), false); //stringRecv(pe.actualString()); break; } case Parser::Event::DocumentClose: { transferItemList += TransferItem(pe.actualString(), false); //stringRecv(pe.actualString()); if(incoming) { sendTagClose(); event = ESend; peerClosed = true; state = Closing; } else { event = EPeerClosed; } return true; } case Parser::Event::Element: { transferItemList += TransferItem(pe.element(), false); //elementRecv(pe.element()); break; } case Parser::Event::Error: { if(incoming) { // If we get a parse error during the initial element exchange, // flip immediately into 'open' mode so that we can report an error. if(state == RecvOpen) { sendTagOpen(); state = Open; } return handleError(); } else { event = EError; errorCode = ErrParse; return true; } } } } else { if(state == RecvOpen || stepRequiresElement()) { need = NNotify; notify |= NRecv; return false; } } } return baseStep(pe); } QString XmlProtocol::xmlEncoding() const { return xml.encoding(); } QString XmlProtocol::elementToString(const QDomElement &e, bool clip) { if(elem.isNull()) elem = elemDoc.importNode(docElement(), true).toElement(); // Determine the appropriate 'fakeNS' to use QString ns; // first, check root namespace QString pre = e.prefix(); if(pre.isNull()) pre = ""; if(pre == elem.prefix()) { ns = elem.namespaceURI(); } else { // scan the root attributes for 'xmlns' (oh joyous hacks) QDomNamedNodeMap al = elem.attributes(); uint n; for(n = 0; n < al.count(); ++n) { QDomAttr a = al.item(n).toAttr(); QString s = a.name(); int x = s.find(':'); if(x != -1) s = s.mid(x+1); else s = ""; if(pre == s) { ns = a.value(); break; } } if(n >= al.count()) { // if we get here, then no appropriate ns was found. use root then.. ns = elem.namespaceURI(); } } // build qName QString qn; if(!elem.prefix().isEmpty()) qn = elem.prefix() + ':'; qn += elem.localName(); // make the string return xmlToString(e, ns, qn, clip); } bool XmlProtocol::stepRequiresElement() const { // default returns false return false; } void XmlProtocol::itemWritten(int, int) { // default does nothing } void XmlProtocol::stringSend(const QString &) { // default does nothing } void XmlProtocol::stringRecv(const QString &) { // default does nothing } void XmlProtocol::elementSend(const QDomElement &) { // default does nothing } void XmlProtocol::elementRecv(const QDomElement &) { // default does nothing } void XmlProtocol::startConnect() { incoming = false; state = SendOpen; } void XmlProtocol::startAccept() { incoming = true; state = RecvOpen; } bool XmlProtocol::close() { sendTagClose(); event = ESend; state = Closing; return true; } int XmlProtocol::writeString(const QString &s, int id, bool external) { transferItemList += TransferItem(s, true, external); return internalWriteString(s, TrackItem::Custom, id); } int XmlProtocol::writeElement(const QDomElement &e, int id, bool external, bool clip) { if(e.isNull()) return 0; transferItemList += TransferItem(e, true, external); //elementSend(e); QString out = elementToString(e, clip); return internalWriteString(out, TrackItem::Custom, id); } QByteArray XmlProtocol::resetStream() { // reset the state if(incoming) state = RecvOpen; else state = SendOpen; // grab unprocessed data before resetting QByteArray spare = xml.unprocessed(); xml.reset(); return spare; } int XmlProtocol::internalWriteData(const QByteArray &a, TrackItem::Type t, int id) { TrackItem i; i.type = t; i.id = id; i.size = a.size(); trackQueue += i; ByteStream::appendArray(&outData, a); return a.size(); } int XmlProtocol::internalWriteString(const QString &s, TrackItem::Type t, int id) { QCString cs = s.utf8(); QByteArray a(cs.length()); memcpy(a.data(), cs.data(), a.size()); return internalWriteData(a, t, id); } void XmlProtocol::sendTagOpen() { if(elem.isNull()) elem = elemDoc.importNode(docElement(), true).toElement(); QString xmlHeader; createRootXmlTags(elem, &xmlHeader, &tagOpen, &tagClose); QString s; s += xmlHeader + '\n'; s += tagOpen + '\n'; transferItemList += TransferItem(xmlHeader, true); transferItemList += TransferItem(tagOpen, true); //stringSend(xmlHeader); //stringSend(tagOpen); internalWriteString(s, TrackItem::Raw); } void XmlProtocol::sendTagClose() { transferItemList += TransferItem(tagClose, true); //stringSend(tagClose); internalWriteString(tagClose, TrackItem::Close); } bool XmlProtocol::baseStep(const Parser::Event &pe) { // Basic if(state == SendOpen) { sendTagOpen(); event = ESend; if(incoming) state = Open; else state = RecvOpen; return true; } else if(state == RecvOpen) { if(incoming) state = SendOpen; else state = Open; // note: event will always be DocumentOpen here handleDocOpen(pe); event = ERecvOpen; return true; } else if(state == Open) { QDomElement e; if(pe.type() == Parser::Event::Element) e = pe.element(); return doStep(e); } // Closing else { if(closeWritten) { if(peerClosed) { event = EPeerClosed; return true; } else return handleCloseFinished(); } need = NNotify; notify = NSend; return false; } } void XmlProtocol::setIncomingAsExternal() { for(QValueList::Iterator it = transferItemList.begin(); it != transferItemList.end(); ++it) { TransferItem &i = *it; // look for elements received if(!i.isString && !i.isSent) i.isExternal = true; } }