From d6b5ef3cf7b40a62e8ad3d722abc49ed8002077f Mon Sep 17 00:00:00 2001 From: Emmanuel Gil Peyrot Date: Thu, 5 Mar 2020 13:27:49 +0100 Subject: [PATCH 01/20] =?UTF-8?q?Inkboard:=20Import=20Mc=E2=80=99s=20truc?= =?UTF-8?q?=20plugin=20and=20rename=20it.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/extension/plugins/CMakeLists.txt | 1 + src/extension/plugins/xmpp/CMakeLists.txt | 4 + src/extension/plugins/xmpp/libxmpp.inx | 15 +++ src/extension/plugins/xmpp/xmpp.cpp | 139 ++++++++++++++++++++++ src/extension/plugins/xmpp/xmpp.h | 75 ++++++++++++ 5 files changed, 234 insertions(+) create mode 100644 src/extension/plugins/xmpp/CMakeLists.txt create mode 100644 src/extension/plugins/xmpp/libxmpp.inx create mode 100644 src/extension/plugins/xmpp/xmpp.cpp create mode 100644 src/extension/plugins/xmpp/xmpp.h diff --git a/src/extension/plugins/CMakeLists.txt b/src/extension/plugins/CMakeLists.txt index dc15b4ae4c..42b4594fb1 100644 --- a/src/extension/plugins/CMakeLists.txt +++ b/src/extension/plugins/CMakeLists.txt @@ -1,2 +1,3 @@ # SPDX-License-Identifier: GPL-2.0-or-later add_subdirectory(grid2) +add_subdirectory(xmpp) diff --git a/src/extension/plugins/xmpp/CMakeLists.txt b/src/extension/plugins/xmpp/CMakeLists.txt new file mode 100644 index 0000000000..22ead356c7 --- /dev/null +++ b/src/extension/plugins/xmpp/CMakeLists.txt @@ -0,0 +1,4 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +set(xmpp_PART_SRCS xmpp.cpp xmpp.h) +include_directories(${CMAKE_BINARY_DIR}/src) +add_library(xmpp SHARED EXCLUDE_FROM_ALL ${xmpp_PART_SRCS}) diff --git a/src/extension/plugins/xmpp/libxmpp.inx b/src/extension/plugins/xmpp/libxmpp.inx new file mode 100644 index 0000000000..0dc62e4b24 --- /dev/null +++ b/src/extension/plugins/xmpp/libxmpp.inx @@ -0,0 +1,15 @@ + + + +<_name>TRUC +org.inkscape.effect.truc + + all + + + + +<_menu-tip>Draw a path which is a grid + + + diff --git a/src/extension/plugins/xmpp/xmpp.cpp b/src/extension/plugins/xmpp/xmpp.cpp new file mode 100644 index 0000000000..e686320ba3 --- /dev/null +++ b/src/extension/plugins/xmpp/xmpp.cpp @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + \file truc.cpp + + A test plug-in. +*/ +/* + * Copyright (C) 2019 Marc Jeanmougin + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include +#include +#include + +#include "desktop.h" + +#include "2geom/geom.h" +#include "document.h" +#include "object/sp-object.h" +#include "selection.h" + +#include "svg/path-string.h" + +#include "extension/effect.h" +#include "extension/system.h" + +#include "util/units.h" + +#include "xmpp.h" + +namespace Inkscape { +namespace Extension { +namespace Internal { + +void TrucObserver::notifyUndoCommitEvent(Event *ee) +{ + XML::Event *e = ee->event; + std::cout << "UndoCommitEvent" << std::endl; + + while (e) { + // printf("DOCUMENT %p\n", e->repr->document()); + if (e->repr) + printf("AFFECTED %s\n", e->repr->attribute("id")); + XML::EventAdd *eadd; + XML::EventDel *edel; + XML::EventChgAttr *echga; + XML::EventChgContent *echgc; + XML::EventChgOrder *echgo; + XML::EventChgElementName *echgn; + + if ((eadd = dynamic_cast(e))) { + std::cout << "EventAdd" << std::endl; + sp_repr_write_stream(eadd->child, *writer, 0, false, GQuark(0), 0, 0); + printf("\n"); + } else if ((edel = dynamic_cast(e))) { + std::cout << "EventDel" << std::endl; + sp_repr_write_stream(edel->child, *writer, 0, false, GQuark(0), 0, 0); + printf("\n"); + } else if ((echga = dynamic_cast(e))) { + std::cout << "EventChgAttr" << std::endl; + printf("%s to %s", &*(echga->oldval), &*(echga->newval)); + printf("\n"); + } else if ((echgc = dynamic_cast(e))) { + std::cout << "EventChgContent" << std::endl; + printf("%s to %s", &*(echgc->oldval), &*(echgc->newval)); + printf("\n"); + } else if ((echgo = dynamic_cast(e))) { + std::cout << "EventChgOrder" << std::endl; + } else if ((echgn = dynamic_cast(e))) { + std::cout << "EventChgElementName" << std::endl; + } else { + std::cout << "Unknown event" << std::endl; + } + + e = e->next; + } +} +void TrucObserver::notifyUndoEvent(Event *e) +{ + std::cout << "UndoEvent" << std::endl; + this->notifyUndoCommitEvent(e); +} +void TrucObserver::notifyRedoEvent(Event *e) +{ + std::cout << "RedoEvent" << std::endl; + this->notifyUndoCommitEvent(e); +} +void TrucObserver::notifyClearUndoEvent() { std::cout << "ClearUndoEvent" << std::endl; } +void TrucObserver::notifyClearRedoEvent() { std::cout << "ClearRedoEvent" << std::endl; } + +/** + \brief A function to allocated anything -- just an example here + \param module Unused + \return Whether the load was successful +*/ +bool Truc::load(Inkscape::Extension::Extension * /*module*/) +{ + this->obs = new TrucObserver(); + this->obs->writer = new IO::StdWriter(); + this->enabled = false; + std::cout << "Hey, I'm TRUE, I'm loading!" << std::endl; + return TRUE; +} + +/** + \brief This actually draws the grid. + \param module The effect that was called (unused) + \param document What should be edited. +*/ +void Truc::effect(Inkscape::Extension::Effect *module, Inkscape::UI::View::View *document, + Inkscape::Extension::Implementation::ImplementationDocumentCache * /*docCache*/) +{ + std::cout << (enabled ? "disabling" : "enabling") << std::endl; + if (!this->enabled) + document->doc()->addUndoObserver(*obs); + else + document->doc()->removeUndoObserver(*obs); + this->enabled = !this->enabled; +} + + + +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ + + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/extension/plugins/xmpp/xmpp.h b/src/extension/plugins/xmpp/xmpp.h new file mode 100644 index 0000000000..f07a6f1d99 --- /dev/null +++ b/src/extension/plugins/xmpp/xmpp.h @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Marc Jeanmougin + * + * Copyright (C) 2019 Authors + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef __TRUC_H + +#include "extension/implementation/implementation.h" + + +#include +#include +#include "inkscape-version.cpp" +#include "undo-stack-observer.h" +#include "io/stream/inkscapestream.h" +#include "xml/event.h" + + + +namespace Inkscape { +namespace Extension { + +class Effect; +class Extension; + +namespace Internal { + +class TrucObserver : public UndoStackObserver { +void notifyUndoCommitEvent(Event* log) override; +void notifyUndoEvent(Event* log) override; +void notifyRedoEvent(Event* log) override; +void notifyClearUndoEvent() override; +void notifyClearRedoEvent() override; + public: + +IO::StdWriter *writer; +}; + + + +class Truc : public Inkscape::Extension::Implementation::Implementation { + + +public: + bool load(Inkscape::Extension::Extension *module) override; + void effect(Inkscape::Extension::Effect *module, Inkscape::UI::View::View *document, Inkscape::Extension::Implementation::ImplementationDocumentCache * docCache) override; + +private: + TrucObserver *obs; + bool enabled; +}; + +}; /* namespace Internal */ +}; /* namespace Extension */ +}; /* namespace Inkscape */ + +extern "C" G_MODULE_EXPORT Inkscape::Extension::Implementation::Implementation* GetImplementation() { return new Inkscape::Extension::Internal::Truc(); } +extern "C" G_MODULE_EXPORT const gchar* GetInkscapeVersion() { return Inkscape::version_string; } +#endif + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : -- GitLab From a9280fcf9dc6ac2927096606fd008c046a52056f Mon Sep 17 00:00:00 2001 From: Emmanuel Gil Peyrot Date: Thu, 5 Mar 2020 13:30:15 +0100 Subject: [PATCH 02/20] Inkboard: Rename Truc into XMPP. --- src/extension/plugins/xmpp/libxmpp.inx | 21 ++++++++++----------- src/extension/plugins/xmpp/xmpp.cpp | 16 ++++++++-------- src/extension/plugins/xmpp/xmpp.h | 8 ++++---- 3 files changed, 22 insertions(+), 23 deletions(-) diff --git a/src/extension/plugins/xmpp/libxmpp.inx b/src/extension/plugins/xmpp/libxmpp.inx index 0dc62e4b24..3db1b19bde 100644 --- a/src/extension/plugins/xmpp/libxmpp.inx +++ b/src/extension/plugins/xmpp/libxmpp.inx @@ -1,15 +1,14 @@ -<_name>TRUC -org.inkscape.effect.truc - - all - - - - -<_menu-tip>Draw a path which is a grid - - + <_name>XMPP + org.inkscape.collaboration.xmpp + + all + + + + <_menu-tip>Start a collaborative edition session + + diff --git a/src/extension/plugins/xmpp/xmpp.cpp b/src/extension/plugins/xmpp/xmpp.cpp index e686320ba3..b69854fc35 100644 --- a/src/extension/plugins/xmpp/xmpp.cpp +++ b/src/extension/plugins/xmpp/xmpp.cpp @@ -34,7 +34,7 @@ namespace Inkscape { namespace Extension { namespace Internal { -void TrucObserver::notifyUndoCommitEvent(Event *ee) +void XMPPObserver::notifyUndoCommitEvent(Event *ee) { XML::Event *e = ee->event; std::cout << "UndoCommitEvent" << std::endl; @@ -77,27 +77,27 @@ void TrucObserver::notifyUndoCommitEvent(Event *ee) e = e->next; } } -void TrucObserver::notifyUndoEvent(Event *e) +void XMPPObserver::notifyUndoEvent(Event *e) { std::cout << "UndoEvent" << std::endl; this->notifyUndoCommitEvent(e); } -void TrucObserver::notifyRedoEvent(Event *e) +void XMPPObserver::notifyRedoEvent(Event *e) { std::cout << "RedoEvent" << std::endl; this->notifyUndoCommitEvent(e); } -void TrucObserver::notifyClearUndoEvent() { std::cout << "ClearUndoEvent" << std::endl; } -void TrucObserver::notifyClearRedoEvent() { std::cout << "ClearRedoEvent" << std::endl; } +void XMPPObserver::notifyClearUndoEvent() { std::cout << "ClearUndoEvent" << std::endl; } +void XMPPObserver::notifyClearRedoEvent() { std::cout << "ClearRedoEvent" << std::endl; } /** \brief A function to allocated anything -- just an example here \param module Unused \return Whether the load was successful */ -bool Truc::load(Inkscape::Extension::Extension * /*module*/) +bool XMPP::load(Inkscape::Extension::Extension * /*module*/) { - this->obs = new TrucObserver(); + this->obs = new XMPPObserver(); this->obs->writer = new IO::StdWriter(); this->enabled = false; std::cout << "Hey, I'm TRUE, I'm loading!" << std::endl; @@ -109,7 +109,7 @@ bool Truc::load(Inkscape::Extension::Extension * /*module*/) \param module The effect that was called (unused) \param document What should be edited. */ -void Truc::effect(Inkscape::Extension::Effect *module, Inkscape::UI::View::View *document, +void XMPP::effect(Inkscape::Extension::Effect *module, Inkscape::UI::View::View *document, Inkscape::Extension::Implementation::ImplementationDocumentCache * /*docCache*/) { std::cout << (enabled ? "disabling" : "enabling") << std::endl; diff --git a/src/extension/plugins/xmpp/xmpp.h b/src/extension/plugins/xmpp/xmpp.h index f07a6f1d99..3bfaec0ff6 100644 --- a/src/extension/plugins/xmpp/xmpp.h +++ b/src/extension/plugins/xmpp/xmpp.h @@ -30,7 +30,7 @@ class Extension; namespace Internal { -class TrucObserver : public UndoStackObserver { +class XMPPObserver : public UndoStackObserver { void notifyUndoCommitEvent(Event* log) override; void notifyUndoEvent(Event* log) override; void notifyRedoEvent(Event* log) override; @@ -43,7 +43,7 @@ IO::StdWriter *writer; -class Truc : public Inkscape::Extension::Implementation::Implementation { +class XMPP : public Inkscape::Extension::Implementation::Implementation { public: @@ -51,7 +51,7 @@ public: void effect(Inkscape::Extension::Effect *module, Inkscape::UI::View::View *document, Inkscape::Extension::Implementation::ImplementationDocumentCache * docCache) override; private: - TrucObserver *obs; + XMPPObserver *obs; bool enabled; }; @@ -59,7 +59,7 @@ private: }; /* namespace Extension */ }; /* namespace Inkscape */ -extern "C" G_MODULE_EXPORT Inkscape::Extension::Implementation::Implementation* GetImplementation() { return new Inkscape::Extension::Internal::Truc(); } +extern "C" G_MODULE_EXPORT Inkscape::Extension::Implementation::Implementation* GetImplementation() { return new Inkscape::Extension::Internal::XMPP(); } extern "C" G_MODULE_EXPORT const gchar* GetInkscapeVersion() { return Inkscape::version_string; } #endif -- GitLab From 08c6a3f876680f687f78a26d9599c3b40d8936a1 Mon Sep 17 00:00:00 2001 From: Emmanuel Gil Peyrot Date: Tue, 10 Mar 2020 18:59:18 +0100 Subject: [PATCH 03/20] Inkboard: Add my copyright --- src/extension/plugins/xmpp/xmpp.cpp | 5 +++-- src/extension/plugins/xmpp/xmpp.h | 6 ++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/extension/plugins/xmpp/xmpp.cpp b/src/extension/plugins/xmpp/xmpp.cpp index b69854fc35..c849cecc68 100644 --- a/src/extension/plugins/xmpp/xmpp.cpp +++ b/src/extension/plugins/xmpp/xmpp.cpp @@ -1,11 +1,12 @@ // SPDX-License-Identifier: GPL-2.0-or-later /** - \file truc.cpp + \file xmpp.cpp - A test plug-in. + A collaborative edition plugin. */ /* * Copyright (C) 2019 Marc Jeanmougin + * Copyright (C) 2020 Emmanuel Gil Peyrot * * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ diff --git a/src/extension/plugins/xmpp/xmpp.h b/src/extension/plugins/xmpp/xmpp.h index 3bfaec0ff6..23380ee485 100644 --- a/src/extension/plugins/xmpp/xmpp.h +++ b/src/extension/plugins/xmpp/xmpp.h @@ -1,9 +1,7 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Authors: - * Marc Jeanmougin - * - * Copyright (C) 2019 Authors + * Copyright (C) 2019 Marc Jeanmougin + * Copyright (C) 2020 Emmanuel Gil Peyrot * * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ -- GitLab From b5e12a1314b1a93b027abe4f981227e205c523d3 Mon Sep 17 00:00:00 2001 From: Emmanuel Gil Peyrot Date: Thu, 5 Mar 2020 13:30:38 +0100 Subject: [PATCH 04/20] Inkboard: Add gloox dependency. --- src/extension/plugins/xmpp/CMakeLists.txt | 5 +++++ src/extension/plugins/xmpp/xmpp.cpp | 3 +++ 2 files changed, 8 insertions(+) diff --git a/src/extension/plugins/xmpp/CMakeLists.txt b/src/extension/plugins/xmpp/CMakeLists.txt index 22ead356c7..fe0fc5f3ae 100644 --- a/src/extension/plugins/xmpp/CMakeLists.txt +++ b/src/extension/plugins/xmpp/CMakeLists.txt @@ -2,3 +2,8 @@ set(xmpp_PART_SRCS xmpp.cpp xmpp.h) include_directories(${CMAKE_BINARY_DIR}/src) add_library(xmpp SHARED EXCLUDE_FROM_ALL ${xmpp_PART_SRCS}) + +find_package(PkgConfig REQUIRED) +pkg_check_modules(Gloox REQUIRED gloox) +target_include_directories(xmpp PRIVATE ${Gloox_INCLUDE_DIRS}) +target_link_libraries(xmpp inkscape_base ${Gloox_LIBRARIES}) diff --git a/src/extension/plugins/xmpp/xmpp.cpp b/src/extension/plugins/xmpp/xmpp.cpp index c849cecc68..ca0aee0b57 100644 --- a/src/extension/plugins/xmpp/xmpp.cpp +++ b/src/extension/plugins/xmpp/xmpp.cpp @@ -11,6 +11,9 @@ * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ +#include +#include + #include #include #include -- GitLab From 938b444ec213ec8e75030e13875f0e96bcec1564 Mon Sep 17 00:00:00 2001 From: Emmanuel Gil Peyrot Date: Thu, 5 Mar 2020 13:31:09 +0100 Subject: [PATCH 05/20] Inkboard: Generate a SXE state change element from an XML::EventAdd. --- src/extension/plugins/xmpp/xmpp.cpp | 52 +++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/src/extension/plugins/xmpp/xmpp.cpp b/src/extension/plugins/xmpp/xmpp.cpp index ca0aee0b57..162cb6faee 100644 --- a/src/extension/plugins/xmpp/xmpp.cpp +++ b/src/extension/plugins/xmpp/xmpp.cpp @@ -24,6 +24,7 @@ #include "document.h" #include "object/sp-object.h" #include "selection.h" +#include "xml/attribute-record.h" #include "svg/path-string.h" @@ -57,6 +58,57 @@ void XMPPObserver::notifyUndoCommitEvent(Event *ee) if ((eadd = dynamic_cast(e))) { std::cout << "EventAdd" << std::endl; sp_repr_write_stream(eadd->child, *writer, 0, false, GQuark(0), 0, 0); + XML::Node *node = eadd->child; + + // TODO: use a real UUID. + char rid[11]; + snprintf(rid, 11, "%d", rand()); + + std::string name = node->name(); + if (name.substr(0, 4) != "svg:") { + printf("Wrong prefix \"%s\"!\n", name.substr(0, 4).c_str()); + abort(); + } + name = name.substr(4); + + gloox::New new_ = { + .rid = rid, + .type = "element", + .name = name.c_str(), + .ns = "http://www.w3.org/2000/svg", + .parent = "", + .chdata = "", + }; + gloox::StateChange change = { + .type = gloox::StateChangeNew, + .new_ = new_, + }; + std::vector state_changes = {}; + state_changes.push_back(change); + + for (Util::List it = node->attributeList(); it; ++it) { + // TODO: use a real UUID. + char attr_rid[11]; + snprintf(attr_rid, 11, "%d", rand()); + + gloox::New new_ = { + .rid = attr_rid, + .type = "attr", + .name = g_quark_to_string(it->key), + .ns = "", + .parent = rid, + .chdata = it->value, + }; + gloox::StateChange change = { + .type = gloox::StateChangeNew, + .new_ = new_, + }; + state_changes.push_back(change); + } + + gloox::Sxe sxe("session", "id", gloox::SxeState, {}, state_changes); + + printf("gloox %s\n", sxe.tag()->xml().c_str()); printf("\n"); } else if ((edel = dynamic_cast(e))) { std::cout << "EventDel" << std::endl; -- GitLab From 0c9ad1e1833d8ee9d6d6a00d6ffaae8c83474639 Mon Sep 17 00:00:00 2001 From: Emmanuel Gil Peyrot Date: Thu, 5 Mar 2020 13:53:51 +0100 Subject: [PATCH 06/20] Inkboard: Make the xmpp extension depend on pkg-config and gloox. --- src/extension/plugins/xmpp/CMakeLists.txt | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/extension/plugins/xmpp/CMakeLists.txt b/src/extension/plugins/xmpp/CMakeLists.txt index fe0fc5f3ae..64c67c43f9 100644 --- a/src/extension/plugins/xmpp/CMakeLists.txt +++ b/src/extension/plugins/xmpp/CMakeLists.txt @@ -1,9 +1,13 @@ # SPDX-License-Identifier: GPL-2.0-or-later -set(xmpp_PART_SRCS xmpp.cpp xmpp.h) -include_directories(${CMAKE_BINARY_DIR}/src) -add_library(xmpp SHARED EXCLUDE_FROM_ALL ${xmpp_PART_SRCS}) +find_package(PkgConfig) +if(PkgConfig_FOUND) + pkg_check_modules(Gloox gloox) + if(Gloox_FOUND) + set(xmpp_PART_SRCS xmpp.cpp xmpp.h) + include_directories(${CMAKE_BINARY_DIR}/src) + add_library(xmpp SHARED EXCLUDE_FROM_ALL ${xmpp_PART_SRCS}) -find_package(PkgConfig REQUIRED) -pkg_check_modules(Gloox REQUIRED gloox) -target_include_directories(xmpp PRIVATE ${Gloox_INCLUDE_DIRS}) -target_link_libraries(xmpp inkscape_base ${Gloox_LIBRARIES}) + target_include_directories(xmpp PRIVATE ${Gloox_INCLUDE_DIRS}) + target_link_libraries(xmpp inkscape_base ${Gloox_LIBRARIES}) + endif() +endif() -- GitLab From 8c853cda03832df2bba13b85c89a98ebd4424147 Mon Sep 17 00:00:00 2001 From: Emmanuel Gil Peyrot Date: Mon, 9 Mar 2020 14:24:18 +0100 Subject: [PATCH 07/20] Inkboard: Connect to XMPP Warning: it is blocking for now! --- src/extension/plugins/xmpp/xmpp.cpp | 136 ++++++++++++++++++++++++---- src/extension/plugins/xmpp/xmpp.h | 32 ++++++- 2 files changed, 147 insertions(+), 21 deletions(-) diff --git a/src/extension/plugins/xmpp/xmpp.cpp b/src/extension/plugins/xmpp/xmpp.cpp index 162cb6faee..44e4a56edf 100644 --- a/src/extension/plugins/xmpp/xmpp.cpp +++ b/src/extension/plugins/xmpp/xmpp.cpp @@ -13,6 +13,9 @@ #include #include +#include +#include +#include #include #include @@ -39,6 +42,73 @@ namespace Inkscape { namespace Extension { namespace Internal { +using namespace gloox; + +std::string get_uuid() +{ + // TODO: use a real UUID. + char attr_rid[11]; + snprintf(attr_rid, 11, "%d", rand()); + return std::string(attr_rid); +} + +InkscapeClient::InkscapeClient(JID jid, const std::string& password) +{ + client = new Client(jid, password); + client->setSASLMechanisms(SaslMechPlain); + client->disco()->setVersion("Inkscape", "version TODO", "Linux"); + client->registerConnectionListener(this); + client->logInstance().registerLogHandler(LogLevelDebug, LogAreaXmlOutgoing | LogAreaXmlIncoming, this); +} + +bool InkscapeClient::connect() +{ + return client->connect(false); +} + +void InkscapeClient::disconnect() +{ + client->disconnect(); + connected = false; +} + +bool InkscapeClient::isConnected() +{ + return connected; +} + +ConnectionError InkscapeClient::recv() +{ + // Timeout every 16ms. + return client->recv(16667); +} + +// From ConnectionListener +void InkscapeClient::onConnect() +{ + printf("connected!\n"); + connected = true; +} + +void InkscapeClient::onDisconnect(ConnectionError e) +{ + printf("disconnected\n"); + connected = false; +} + +bool InkscapeClient::onTLSConnect(const CertInfo& info) +{ + printf("accept cert? yes of course\n"); + return true; +} + +// From LogHandler +void InkscapeClient::handleLog(LogLevel level, LogArea area, const std::string& message) +{ + printf("gloox: %s\n", message.c_str()); + fflush(stdout); +} + void XMPPObserver::notifyUndoCommitEvent(Event *ee) { XML::Event *e = ee->event; @@ -60,10 +130,7 @@ void XMPPObserver::notifyUndoCommitEvent(Event *ee) sp_repr_write_stream(eadd->child, *writer, 0, false, GQuark(0), 0, 0); XML::Node *node = eadd->child; - // TODO: use a real UUID. - char rid[11]; - snprintf(rid, 11, "%d", rand()); - + std::string rid = get_uuid(); std::string name = node->name(); if (name.substr(0, 4) != "svg:") { printf("Wrong prefix \"%s\"!\n", name.substr(0, 4).c_str()); @@ -71,52 +138,62 @@ void XMPPObserver::notifyUndoCommitEvent(Event *ee) } name = name.substr(4); - gloox::New new_ = { - .rid = rid, + SxeNew new_ = { + .rid = rid.c_str(), .type = "element", .name = name.c_str(), .ns = "http://www.w3.org/2000/svg", .parent = "", .chdata = "", }; - gloox::StateChange change = { - .type = gloox::StateChangeNew, + SxeStateChange change = { + .type = SxeStateChangeNew, .new_ = new_, }; - std::vector state_changes = {}; + std::vector state_changes = {}; state_changes.push_back(change); for (Util::List it = node->attributeList(); it; ++it) { - // TODO: use a real UUID. - char attr_rid[11]; - snprintf(attr_rid, 11, "%d", rand()); + std::string attr_rid = get_uuid(); - gloox::New new_ = { - .rid = attr_rid, + SxeNew new_ = { + .rid = attr_rid.c_str(), .type = "attr", .name = g_quark_to_string(it->key), .ns = "", - .parent = rid, + .parent = rid.c_str(), .chdata = it->value, }; - gloox::StateChange change = { - .type = gloox::StateChangeNew, + SxeStateChange change = { + .type = SxeStateChangeNew, .new_ = new_, }; state_changes.push_back(change); } - gloox::Sxe sxe("session", "id", gloox::SxeState, {}, state_changes); + Sxe sxe("session", "id", SxeState, {}, state_changes); printf("gloox %s\n", sxe.tag()->xml().c_str()); printf("\n"); } else if ((edel = dynamic_cast(e))) { std::cout << "EventDel" << std::endl; sp_repr_write_stream(edel->child, *writer, 0, false, GQuark(0), 0, 0); + + SxeStateChange change = { + .type = SxeStateChangeRemove, + .remove = SxeRemove { + .target = "coucou", + }, + }; + std::vector state_changes = {}; + state_changes.push_back(change); + Sxe sxe("session", "id", SxeState, {}, state_changes); + + printf("gloox %s\n", sxe.tag()->xml().c_str()); printf("\n"); } else if ((echga = dynamic_cast(e))) { std::cout << "EventChgAttr" << std::endl; - printf("%s to %s", &*(echga->oldval), &*(echga->newval)); + printf("%s from %s to %s", g_quark_to_string(echga->key), &*(echga->oldval), &*(echga->newval)); printf("\n"); } else if ((echgc = dynamic_cast(e))) { std::cout << "EventChgContent" << std::endl; @@ -156,7 +233,26 @@ bool XMPP::load(Inkscape::Extension::Extension * /*module*/) this->obs = new XMPPObserver(); this->obs->writer = new IO::StdWriter(); this->enabled = false; - std::cout << "Hey, I'm TRUE, I'm loading!" << std::endl; + + // TODO: fetch these from the preferences. + JID jid("test@linkmauve.fr"); + const char *password = "test"; + + //client = std::unique_ptr(new InkscapeClient(jid, password)); + InkscapeClient *client = new InkscapeClient(jid, password); + bool connected = client->connect(); + printf("just attempted to connect, should be 0: %d\n", connected); + while (true) { + ConnectionError err = client->recv(); + if (err != ConnNoError) + break; + + // Only read the queue if we are connected. + if (!client->isConnected()) + continue; + } + printf("finished? :(\n"); + fflush(stdout); return TRUE; } diff --git a/src/extension/plugins/xmpp/xmpp.h b/src/extension/plugins/xmpp/xmpp.h index 23380ee485..409f2eb3be 100644 --- a/src/extension/plugins/xmpp/xmpp.h +++ b/src/extension/plugins/xmpp/xmpp.h @@ -11,14 +11,20 @@ #include "extension/implementation/implementation.h" +#include +#include +#include #include #include +#include #include "inkscape-version.cpp" #include "undo-stack-observer.h" #include "io/stream/inkscapestream.h" #include "xml/event.h" - +namespace gloox { +class Client; +} namespace Inkscape { namespace Extension { @@ -40,6 +46,29 @@ IO::StdWriter *writer; }; +class InkscapeClient : gloox::ConnectionListener, gloox::LogHandler { +public: + InkscapeClient(gloox::JID jid, const std::string& password); + bool connect(); + void disconnect(); + bool isConnected(); + gloox::ConnectionError recv(); + +private: + // From ConnectionListener + void onConnect() override; + void onDisconnect(gloox::ConnectionError e) override; + bool onTLSConnect(const gloox::CertInfo& info) override; + + // From LogHandler + void handleLog(gloox::LogLevel level, gloox::LogArea area, const std::string& message) override; + +private: + //std::unique_ptr client; + gloox::Client *client; + bool connected; +}; + class XMPP : public Inkscape::Extension::Implementation::Implementation { @@ -50,6 +79,7 @@ public: private: XMPPObserver *obs; + std::unique_ptr client; bool enabled; }; -- GitLab From 9d064a5345d0ebebe6e1a95ba4000aee5459ff42 Mon Sep 17 00:00:00 2001 From: Emmanuel Gil Peyrot Date: Mon, 9 Mar 2020 14:55:51 +0100 Subject: [PATCH 08/20] =?UTF-8?q?Inkboard:=20Integrate=20with=20GLib?= =?UTF-8?q?=E2=80=99s=20main=20loop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This calls runLoop() every 16ms instead of integrating gloox’s fd with the main loop, and thus only get called when the socket has data to be read, but that’s good enough for a start. --- src/extension/plugins/xmpp/xmpp.cpp | 30 ++++++++++++++++------------- src/extension/plugins/xmpp/xmpp.h | 1 + 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/extension/plugins/xmpp/xmpp.cpp b/src/extension/plugins/xmpp/xmpp.cpp index 44e4a56edf..b823525b49 100644 --- a/src/extension/plugins/xmpp/xmpp.cpp +++ b/src/extension/plugins/xmpp/xmpp.cpp @@ -79,8 +79,20 @@ bool InkscapeClient::isConnected() ConnectionError InkscapeClient::recv() { - // Timeout every 16ms. - return client->recv(16667); + // Return immediately if no data was available on the socket. + return client->recv(0); +} + +int InkscapeClient::runLoop(void *data) +{ + InkscapeClient *client = static_cast(data); + ConnectionError err = client->recv(); + if (err != ConnNoError) { + printf("Error while receiving on gloox socket: %d\n", err); + return FALSE; + } + + return TRUE; } // From ConnectionListener @@ -242,17 +254,9 @@ bool XMPP::load(Inkscape::Extension::Extension * /*module*/) InkscapeClient *client = new InkscapeClient(jid, password); bool connected = client->connect(); printf("just attempted to connect, should be 0: %d\n", connected); - while (true) { - ConnectionError err = client->recv(); - if (err != ConnNoError) - break; - - // Only read the queue if we are connected. - if (!client->isConnected()) - continue; - } - printf("finished? :(\n"); - fflush(stdout); + + // TODO: find a better way to integrate gloox’s fd into the main loop. + g_timeout_add(16, &InkscapeClient::runLoop, client); return TRUE; } diff --git a/src/extension/plugins/xmpp/xmpp.h b/src/extension/plugins/xmpp/xmpp.h index 409f2eb3be..699e291315 100644 --- a/src/extension/plugins/xmpp/xmpp.h +++ b/src/extension/plugins/xmpp/xmpp.h @@ -53,6 +53,7 @@ public: void disconnect(); bool isConnected(); gloox::ConnectionError recv(); + static int runLoop(void *data); private: // From ConnectionListener -- GitLab From eb8e35bc0a4c6c78fac0f2e3a417422022ab0042 Mon Sep 17 00:00:00 2001 From: Emmanuel Gil Peyrot Date: Tue, 10 Mar 2020 13:39:57 +0100 Subject: [PATCH 09/20] Inkboard: Give XMPP access to the XMPPObserver It can now send messages! --- src/extension/plugins/xmpp/xmpp.cpp | 63 ++++++++++++++++++++--------- src/extension/plugins/xmpp/xmpp.h | 33 ++++++++------- 2 files changed, 62 insertions(+), 34 deletions(-) diff --git a/src/extension/plugins/xmpp/xmpp.cpp b/src/extension/plugins/xmpp/xmpp.cpp index b823525b49..b1724295f3 100644 --- a/src/extension/plugins/xmpp/xmpp.cpp +++ b/src/extension/plugins/xmpp/xmpp.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include @@ -54,11 +55,13 @@ std::string get_uuid() InkscapeClient::InkscapeClient(JID jid, const std::string& password) { - client = new Client(jid, password); + // TODO: use std::make_unique() once we’re C++14. + client = std::unique_ptr(new Client(jid, password)); client->setSASLMechanisms(SaslMechPlain); client->disco()->setVersion("Inkscape", "version TODO", "Linux"); client->registerConnectionListener(this); - client->logInstance().registerLogHandler(LogLevelDebug, LogAreaXmlOutgoing | LogAreaXmlIncoming, this); + //client->logInstance().registerLogHandler(LogLevelDebug, LogAreaXmlOutgoing | LogAreaXmlIncoming, this); + client->logInstance().registerLogHandler(LogLevelDebug, ~0, this); } bool InkscapeClient::connect() @@ -83,6 +86,11 @@ ConnectionError InkscapeClient::recv() return client->recv(0); } +void InkscapeClient::send(Tag *tag) +{ + client->send(tag); +} + int InkscapeClient::runLoop(void *data) { InkscapeClient *client = static_cast(data); @@ -117,7 +125,17 @@ bool InkscapeClient::onTLSConnect(const CertInfo& info) // From LogHandler void InkscapeClient::handleLog(LogLevel level, LogArea area, const std::string& message) { - printf("gloox: %s\n", message.c_str()); + switch (area) { + case LogAreaXmlIncoming: + printf("RECV %s\n", message.c_str()); + break; + case LogAreaXmlOutgoing: + printf("SEND %s\n", message.c_str()); + break; + default: + printf("gloox: %s\n", message.c_str()); + break; + } fflush(stdout); } @@ -140,6 +158,7 @@ void XMPPObserver::notifyUndoCommitEvent(Event *ee) if ((eadd = dynamic_cast(e))) { std::cout << "EventAdd" << std::endl; sp_repr_write_stream(eadd->child, *writer, 0, false, GQuark(0), 0, 0); + printf("\n"); XML::Node *node = eadd->child; std::string rid = get_uuid(); @@ -183,13 +202,15 @@ void XMPPObserver::notifyUndoCommitEvent(Event *ee) state_changes.push_back(change); } - Sxe sxe("session", "id", SxeState, {}, state_changes); - - printf("gloox %s\n", sxe.tag()->xml().c_str()); - printf("\n"); + Message msg(Message::Normal, JID("linkmauve@linkmauve.fr")); + msg.addExtension(new Sxe("session", "id", SxeState, {}, state_changes)); + printf("gloox %s\n", msg.tag()->xml().c_str()); + fflush(stdout); + client->send(msg.tag()); } else if ((edel = dynamic_cast(e))) { std::cout << "EventDel" << std::endl; sp_repr_write_stream(edel->child, *writer, 0, false, GQuark(0), 0, 0); + printf("\n"); SxeStateChange change = { .type = SxeStateChangeRemove, @@ -199,18 +220,18 @@ void XMPPObserver::notifyUndoCommitEvent(Event *ee) }; std::vector state_changes = {}; state_changes.push_back(change); - Sxe sxe("session", "id", SxeState, {}, state_changes); - printf("gloox %s\n", sxe.tag()->xml().c_str()); - printf("\n"); + Message msg(Message::Normal, JID("linkmauve@linkmauve.fr")); + msg.addExtension(new Sxe("session", "id", SxeState, {}, state_changes)); + printf("gloox %s\n", msg.tag()->xml().c_str()); + fflush(stdout); + client->send(msg.tag()); } else if ((echga = dynamic_cast(e))) { std::cout << "EventChgAttr" << std::endl; - printf("%s from %s to %s", g_quark_to_string(echga->key), &*(echga->oldval), &*(echga->newval)); - printf("\n"); + printf("%s from %s to %s\n", g_quark_to_string(echga->key), &*(echga->oldval), &*(echga->newval)); } else if ((echgc = dynamic_cast(e))) { std::cout << "EventChgContent" << std::endl; - printf("%s to %s", &*(echgc->oldval), &*(echgc->newval)); - printf("\n"); + printf("%s to %s\n", &*(echgc->oldval), &*(echgc->newval)); } else if ((echgo = dynamic_cast(e))) { std::cout << "EventChgOrder" << std::endl; } else if ((echgn = dynamic_cast(e))) { @@ -235,6 +256,10 @@ void XMPPObserver::notifyRedoEvent(Event *e) void XMPPObserver::notifyClearUndoEvent() { std::cout << "ClearUndoEvent" << std::endl; } void XMPPObserver::notifyClearRedoEvent() { std::cout << "ClearRedoEvent" << std::endl; } +XMPPObserver::XMPPObserver(std::shared_ptr client) + : client(client) +{} + /** \brief A function to allocated anything -- just an example here \param module Unused @@ -242,21 +267,21 @@ void XMPPObserver::notifyClearRedoEvent() { std::cout << "ClearRedoEvent" << std */ bool XMPP::load(Inkscape::Extension::Extension * /*module*/) { - this->obs = new XMPPObserver(); - this->obs->writer = new IO::StdWriter(); this->enabled = false; // TODO: fetch these from the preferences. JID jid("test@linkmauve.fr"); const char *password = "test"; - //client = std::unique_ptr(new InkscapeClient(jid, password)); - InkscapeClient *client = new InkscapeClient(jid, password); + client = std::make_shared(jid, password); bool connected = client->connect(); printf("just attempted to connect, should be 0: %d\n", connected); // TODO: find a better way to integrate gloox’s fd into the main loop. - g_timeout_add(16, &InkscapeClient::runLoop, client); + g_timeout_add(16, &InkscapeClient::runLoop, client.get()); + + this->obs = new XMPPObserver(client); + this->obs->writer = new IO::StdWriter(); return TRUE; } diff --git a/src/extension/plugins/xmpp/xmpp.h b/src/extension/plugins/xmpp/xmpp.h index 699e291315..06ecc731b5 100644 --- a/src/extension/plugins/xmpp/xmpp.h +++ b/src/extension/plugins/xmpp/xmpp.h @@ -34,18 +34,6 @@ class Extension; namespace Internal { -class XMPPObserver : public UndoStackObserver { -void notifyUndoCommitEvent(Event* log) override; -void notifyUndoEvent(Event* log) override; -void notifyRedoEvent(Event* log) override; -void notifyClearUndoEvent() override; -void notifyClearRedoEvent() override; - public: - -IO::StdWriter *writer; -}; - - class InkscapeClient : gloox::ConnectionListener, gloox::LogHandler { public: InkscapeClient(gloox::JID jid, const std::string& password); @@ -53,6 +41,7 @@ public: void disconnect(); bool isConnected(); gloox::ConnectionError recv(); + void send(gloox::Tag *tag); static int runLoop(void *data); private: @@ -65,12 +54,26 @@ private: void handleLog(gloox::LogLevel level, gloox::LogArea area, const std::string& message) override; private: - //std::unique_ptr client; - gloox::Client *client; + std::unique_ptr client; bool connected; }; +class XMPPObserver : public UndoStackObserver { + void notifyUndoCommitEvent(Event* log) override; + void notifyUndoEvent(Event* log) override; + void notifyRedoEvent(Event* log) override; + void notifyClearUndoEvent() override; + void notifyClearRedoEvent() override; + +public: + XMPPObserver(std::shared_ptr client); + + IO::StdWriter *writer; + std::shared_ptr client; +}; + + class XMPP : public Inkscape::Extension::Implementation::Implementation { @@ -80,7 +83,7 @@ public: private: XMPPObserver *obs; - std::unique_ptr client; + std::shared_ptr client; bool enabled; }; -- GitLab From 4af70fb5c446fe6dd4609e49ef6586c1947d6fcf Mon Sep 17 00:00:00 2001 From: Emmanuel Gil Peyrot Date: Tue, 10 Mar 2020 18:55:44 +0100 Subject: [PATCH 10/20] Inkboard: Expose our Jingle and SXE features. --- src/extension/plugins/xmpp/xmpp.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/extension/plugins/xmpp/xmpp.cpp b/src/extension/plugins/xmpp/xmpp.cpp index b1724295f3..8a695013b2 100644 --- a/src/extension/plugins/xmpp/xmpp.cpp +++ b/src/extension/plugins/xmpp/xmpp.cpp @@ -59,6 +59,9 @@ InkscapeClient::InkscapeClient(JID jid, const std::string& password) client = std::unique_ptr(new Client(jid, password)); client->setSASLMechanisms(SaslMechPlain); client->disco()->setVersion("Inkscape", "version TODO", "Linux"); + client->disco()->addFeature("urn:xmpp:jingle:1"); + client->disco()->addFeature("urn:xmpp:jingle:transports:sxe"); + client->disco()->addFeature("urn:xmpp:sxe:0"); client->registerConnectionListener(this); //client->logInstance().registerLogHandler(LogLevelDebug, LogAreaXmlOutgoing | LogAreaXmlIncoming, this); client->logInstance().registerLogHandler(LogLevelDebug, ~0, this); -- GitLab From 2e480f3707fbfbc354742fa6bc776afaab529937 Mon Sep 17 00:00:00 2001 From: Emmanuel Gil Peyrot Date: Wed, 11 Mar 2020 18:42:42 +0100 Subject: [PATCH 11/20] Inkboard: Slight cleanup. --- src/extension/plugins/xmpp/xmpp.cpp | 23 +++++++++++------------ src/extension/plugins/xmpp/xmpp.h | 4 ++-- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/extension/plugins/xmpp/xmpp.cpp b/src/extension/plugins/xmpp/xmpp.cpp index 8a695013b2..3737696919 100644 --- a/src/extension/plugins/xmpp/xmpp.cpp +++ b/src/extension/plugins/xmpp/xmpp.cpp @@ -29,6 +29,7 @@ #include "object/sp-object.h" #include "selection.h" #include "xml/attribute-record.h" +#include "inkscape-version.h" #include "svg/path-string.h" @@ -57,8 +58,11 @@ InkscapeClient::InkscapeClient(JID jid, const std::string& password) { // TODO: use std::make_unique() once we’re C++14. client = std::unique_ptr(new Client(jid, password)); + // TODO: figure out why SCRAM-SHA-1 isn’t working. client->setSASLMechanisms(SaslMechPlain); - client->disco()->setVersion("Inkscape", "version TODO", "Linux"); + // TODO: fetch the OS properly, instead of hardcoding it to Linux. + client->disco()->setVersion("Inkscape", version_string_without_revision, "Linux"); + client->disco()->setIdentity("collaboration", "whiteboard", "Inkscape"); client->disco()->addFeature("urn:xmpp:jingle:1"); client->disco()->addFeature("urn:xmpp:jingle:transports:sxe"); client->disco()->addFeature("urn:xmpp:sxe:0"); @@ -207,8 +211,6 @@ void XMPPObserver::notifyUndoCommitEvent(Event *ee) Message msg(Message::Normal, JID("linkmauve@linkmauve.fr")); msg.addExtension(new Sxe("session", "id", SxeState, {}, state_changes)); - printf("gloox %s\n", msg.tag()->xml().c_str()); - fflush(stdout); client->send(msg.tag()); } else if ((edel = dynamic_cast(e))) { std::cout << "EventDel" << std::endl; @@ -226,8 +228,6 @@ void XMPPObserver::notifyUndoCommitEvent(Event *ee) Message msg(Message::Normal, JID("linkmauve@linkmauve.fr")); msg.addExtension(new Sxe("session", "id", SxeState, {}, state_changes)); - printf("gloox %s\n", msg.tag()->xml().c_str()); - fflush(stdout); client->send(msg.tag()); } else if ((echga = dynamic_cast(e))) { std::cout << "EventChgAttr" << std::endl; @@ -270,21 +270,20 @@ XMPPObserver::XMPPObserver(std::shared_ptr client) */ bool XMPP::load(Inkscape::Extension::Extension * /*module*/) { - this->enabled = false; + enabled = false; // TODO: fetch these from the preferences. JID jid("test@linkmauve.fr"); const char *password = "test"; client = std::make_shared(jid, password); - bool connected = client->connect(); - printf("just attempted to connect, should be 0: %d\n", connected); + client->connect(); // TODO: find a better way to integrate gloox’s fd into the main loop. g_timeout_add(16, &InkscapeClient::runLoop, client.get()); - this->obs = new XMPPObserver(client); - this->obs->writer = new IO::StdWriter(); + obs = std::unique_ptr(new XMPPObserver(client)); + obs->writer = std::unique_ptr(new IO::StdWriter()); return TRUE; } @@ -297,11 +296,11 @@ void XMPP::effect(Inkscape::Extension::Effect *module, Inkscape::UI::View::View Inkscape::Extension::Implementation::ImplementationDocumentCache * /*docCache*/) { std::cout << (enabled ? "disabling" : "enabling") << std::endl; - if (!this->enabled) + if (!enabled) document->doc()->addUndoObserver(*obs); else document->doc()->removeUndoObserver(*obs); - this->enabled = !this->enabled; + enabled = !enabled; } diff --git a/src/extension/plugins/xmpp/xmpp.h b/src/extension/plugins/xmpp/xmpp.h index 06ecc731b5..6cd9f34cd1 100644 --- a/src/extension/plugins/xmpp/xmpp.h +++ b/src/extension/plugins/xmpp/xmpp.h @@ -69,7 +69,7 @@ class XMPPObserver : public UndoStackObserver { public: XMPPObserver(std::shared_ptr client); - IO::StdWriter *writer; + std::unique_ptr writer; std::shared_ptr client; }; @@ -82,7 +82,7 @@ public: void effect(Inkscape::Extension::Effect *module, Inkscape::UI::View::View *document, Inkscape::Extension::Implementation::ImplementationDocumentCache * docCache) override; private: - XMPPObserver *obs; + std::unique_ptr obs; std::shared_ptr client; bool enabled; }; -- GitLab From 7372530e2789866bff3ea48e8cd1abe470fced9c Mon Sep 17 00:00:00 2001 From: Emmanuel Gil Peyrot Date: Tue, 17 Mar 2020 19:42:56 +0100 Subject: [PATCH 12/20] Inkboard: Rename all SXE structs and enums to be in the Sxe:: class. --- src/extension/plugins/xmpp/xmpp.cpp | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/extension/plugins/xmpp/xmpp.cpp b/src/extension/plugins/xmpp/xmpp.cpp index 3737696919..069d95af5a 100644 --- a/src/extension/plugins/xmpp/xmpp.cpp +++ b/src/extension/plugins/xmpp/xmpp.cpp @@ -176,7 +176,7 @@ void XMPPObserver::notifyUndoCommitEvent(Event *ee) } name = name.substr(4); - SxeNew new_ = { + Sxe::New new_ = { .rid = rid.c_str(), .type = "element", .name = name.c_str(), @@ -184,17 +184,17 @@ void XMPPObserver::notifyUndoCommitEvent(Event *ee) .parent = "", .chdata = "", }; - SxeStateChange change = { - .type = SxeStateChangeNew, + Sxe::StateChange change = { + .type = Sxe::StateChangeNew, .new_ = new_, }; - std::vector state_changes = {}; + std::vector state_changes = {}; state_changes.push_back(change); for (Util::List it = node->attributeList(); it; ++it) { std::string attr_rid = get_uuid(); - SxeNew new_ = { + Sxe::New new_ = { .rid = attr_rid.c_str(), .type = "attr", .name = g_quark_to_string(it->key), @@ -202,32 +202,32 @@ void XMPPObserver::notifyUndoCommitEvent(Event *ee) .parent = rid.c_str(), .chdata = it->value, }; - SxeStateChange change = { - .type = SxeStateChangeNew, + Sxe::StateChange change = { + .type = Sxe::StateChangeNew, .new_ = new_, }; state_changes.push_back(change); } Message msg(Message::Normal, JID("linkmauve@linkmauve.fr")); - msg.addExtension(new Sxe("session", "id", SxeState, {}, state_changes)); + msg.addExtension(new Sxe("session", "id", Sxe::TypeState, {}, state_changes)); client->send(msg.tag()); } else if ((edel = dynamic_cast(e))) { std::cout << "EventDel" << std::endl; sp_repr_write_stream(edel->child, *writer, 0, false, GQuark(0), 0, 0); printf("\n"); - SxeStateChange change = { - .type = SxeStateChangeRemove, - .remove = SxeRemove { + Sxe::StateChange change = { + .type = Sxe::StateChangeRemove, + .remove = Sxe::Remove { .target = "coucou", }, }; - std::vector state_changes = {}; + std::vector state_changes = {}; state_changes.push_back(change); Message msg(Message::Normal, JID("linkmauve@linkmauve.fr")); - msg.addExtension(new Sxe("session", "id", SxeState, {}, state_changes)); + msg.addExtension(new Sxe("session", "id", Sxe::TypeState, {}, state_changes)); client->send(msg.tag()); } else if ((echga = dynamic_cast(e))) { std::cout << "EventChgAttr" << std::endl; -- GitLab From b95964fc0080bdb37300a1cd3a930dcb99796fcc Mon Sep 17 00:00:00 2001 From: Emmanuel Gil Peyrot Date: Mon, 16 Mar 2020 09:43:16 +0100 Subject: [PATCH 13/20] Inkboard: Use Jingle::SessionManager and the new Jingle::SxePlugin. --- src/extension/plugins/xmpp/xmpp.cpp | 102 +++++++++++++++++++++++++++- src/extension/plugins/xmpp/xmpp.h | 11 ++- 2 files changed, 109 insertions(+), 4 deletions(-) diff --git a/src/extension/plugins/xmpp/xmpp.cpp b/src/extension/plugins/xmpp/xmpp.cpp index 069d95af5a..a74bef17a5 100644 --- a/src/extension/plugins/xmpp/xmpp.cpp +++ b/src/extension/plugins/xmpp/xmpp.cpp @@ -17,6 +17,9 @@ #include #include #include +#include +#include +#include #include #include @@ -54,6 +57,51 @@ std::string get_uuid() return std::string(attr_rid); } + +// TODO: this is a hack, this namespace isn’t reserved so shouldn’t be used, we +// probably want to change the XEP to use an application based on the MIME type +// of the document or something like that. +class SvgApplication : public Jingle::Plugin +{ +public: + SvgApplication(const Tag* tag = 0) + : Jingle::Plugin(Jingle::PluginUser) + {} + + // reimplemented from Plugin + const StringList features() const override + { + StringList sl; + sl.push_back("urn:xmpp:jingle:apps:svg"); + return sl; + } + + // reimplemented from Plugin + const std::string& filterString() const override + { + static const std::string filter = "content[@xmlns='" + XMLNS_JINGLE + "']/transport[@xmlns='urn:xmpp:jingle:apps:svg']"; + return filter; + } + + // reimplemented from Plugin + Tag* tag() const override + { + return new Tag("description", XMLNS, "urn:xmpp:jingle:apps:svg"); + } + + // reimplemented from Plugin + Jingle::Plugin* newInstance(const Tag* tag) const override + { + return new SvgApplication(tag); + } + + // reimplemented from Plugin + Jingle::Plugin* clone() const override + { + return new SvgApplication(*this); + } +}; + InkscapeClient::InkscapeClient(JID jid, const std::string& password) { // TODO: use std::make_unique() once we’re C++14. @@ -63,9 +111,9 @@ InkscapeClient::InkscapeClient(JID jid, const std::string& password) // TODO: fetch the OS properly, instead of hardcoding it to Linux. client->disco()->setVersion("Inkscape", version_string_without_revision, "Linux"); client->disco()->setIdentity("collaboration", "whiteboard", "Inkscape"); - client->disco()->addFeature("urn:xmpp:jingle:1"); - client->disco()->addFeature("urn:xmpp:jingle:transports:sxe"); - client->disco()->addFeature("urn:xmpp:sxe:0"); + session_manager = std::unique_ptr(new Jingle::SessionManager(client.get(), this)); + session_manager->registerPlugin(new Jingle::Content()); + session_manager->registerPlugin(new Jingle::SxePlugin()); client->registerConnectionListener(this); //client->logInstance().registerLogHandler(LogLevelDebug, LogAreaXmlOutgoing | LogAreaXmlIncoming, this); client->logInstance().registerLogHandler(LogLevelDebug, ~0, this); @@ -146,6 +194,54 @@ void InkscapeClient::handleLog(LogLevel level, LogArea area, const std::string& fflush(stdout); } +void InkscapeClient::handleSessionAction(Jingle::Action action, Jingle::Session* session, const Jingle::Session::Jingle* jingle) +{ + printf("handleSessionAction(action=%d, session=%p, jingle=%p)\n", action, session, jingle); + + switch (action) { + case Jingle::SessionInitiate: { + std::string name; + printf("plugins: %zd\n", jingle->plugins().size()); + for (const Jingle::Plugin* p : jingle->plugins()) { + printf("- %p\n", p); + // XXX: Don’t assume this is a Jingle::Content… + const Jingle::Content* content = reinterpret_cast(p); + name = content->name(); + break; + } + printf("that’s it!\n"); + fflush(stdout); + if (true/*accept*/) { + std::list plugins_list; + SvgApplication* description = new SvgApplication(); + Jingle::SxePlugin* transport = new Jingle::SxePlugin(client->jid()); + plugins_list.push_front(transport); + plugins_list.push_front(description); + bool ret = session->sessionAccept(new Jingle::Content(name, plugins_list)); + printf("accepted? %d\n", ret); + } else { + bool ret = session->sessionTerminate(new Jingle::Session::Reason(Jingle::Session::Reason::UnsupportedApplications)); + printf("terminated? %d\n", ret); + } + fflush(stdout); + break; + } + default: + printf("Unhandled…\n"); + break; + } +} + +void InkscapeClient::handleSessionActionError(Jingle::Action action, Jingle::Session* session, const Error* error ) +{ + printf("handleSessionActionError(action=%d, session=%p, error=%p)\n", action, session, error); +} + +void InkscapeClient::handleIncomingSession(Jingle::Session* session) +{ + printf("handleIncomingSession(session=%p)\n", session); +} + void XMPPObserver::notifyUndoCommitEvent(Event *ee) { XML::Event *e = ee->event; diff --git a/src/extension/plugins/xmpp/xmpp.h b/src/extension/plugins/xmpp/xmpp.h index 6cd9f34cd1..b3c84c8cae 100644 --- a/src/extension/plugins/xmpp/xmpp.h +++ b/src/extension/plugins/xmpp/xmpp.h @@ -14,6 +14,9 @@ #include #include #include +#include +#include +#include #include #include #include @@ -34,7 +37,7 @@ class Extension; namespace Internal { -class InkscapeClient : gloox::ConnectionListener, gloox::LogHandler { +class InkscapeClient : gloox::ConnectionListener, gloox::LogHandler, gloox::Jingle::SessionHandler { public: InkscapeClient(gloox::JID jid, const std::string& password); bool connect(); @@ -53,8 +56,14 @@ private: // From LogHandler void handleLog(gloox::LogLevel level, gloox::LogArea area, const std::string& message) override; + // From Jingle::SessionHandler + void handleSessionAction(gloox::Jingle::Action action, gloox::Jingle::Session* session, const gloox::Jingle::Session::Jingle* jingle) override; + void handleSessionActionError(gloox::Jingle::Action action, gloox::Jingle::Session* session, const gloox::Error* error) override; + void handleIncomingSession(gloox::Jingle::Session* session) override; + private: std::unique_ptr client; + std::unique_ptr session_manager; bool connected; }; -- GitLab From da09d7378554708b257a525f89d08512b5456092 Mon Sep 17 00:00:00 2001 From: Emmanuel Gil Peyrot Date: Mon, 23 Mar 2020 14:55:35 +0100 Subject: [PATCH 14/20] Inkboard: Use the newly-written gloox sxe session module to accept a session. --- src/extension/plugins/xmpp/xmpp.cpp | 98 ++++++++++++++++++++++------- src/extension/plugins/xmpp/xmpp.h | 12 +++- 2 files changed, 87 insertions(+), 23 deletions(-) diff --git a/src/extension/plugins/xmpp/xmpp.cpp b/src/extension/plugins/xmpp/xmpp.cpp index a74bef17a5..fecd242936 100644 --- a/src/extension/plugins/xmpp/xmpp.cpp +++ b/src/extension/plugins/xmpp/xmpp.cpp @@ -11,7 +11,6 @@ * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ -#include #include #include #include @@ -19,6 +18,8 @@ #include #include #include +#include +#include #include #include @@ -114,6 +115,7 @@ InkscapeClient::InkscapeClient(JID jid, const std::string& password) session_manager = std::unique_ptr(new Jingle::SessionManager(client.get(), this)); session_manager->registerPlugin(new Jingle::Content()); session_manager->registerPlugin(new Jingle::SxePlugin()); + sxe_manager = std::unique_ptr(new SxeSessionManager(client.get(), this)); client->registerConnectionListener(this); //client->logInstance().registerLogHandler(LogLevelDebug, LogAreaXmlOutgoing | LogAreaXmlIncoming, this); client->logInstance().registerLogHandler(LogLevelDebug, ~0, this); @@ -146,6 +148,11 @@ void InkscapeClient::send(Tag *tag) client->send(tag); } +void InkscapeClient::sendChanges(JID recipient, std::string& sid, std::vector state_changes) +{ + sxe_manager->sendChanges(recipient, sid, state_changes); +} + int InkscapeClient::runLoop(void *data) { InkscapeClient *client = static_cast(data); @@ -242,6 +249,41 @@ void InkscapeClient::handleIncomingSession(Jingle::Session* session) printf("handleIncomingSession(session=%p)\n", session); } +std::vector InkscapeClient::getCurrentState(const std::string& session, const std::string& id) +{ + printf("getCurrentState(session=%s, id=%s)\n", session.c_str(), id.c_str()); + std::vector state; + Sxe::DocumentBegin document_begin = { + .prolog = "data:image/svg+xml,", + }; + Sxe::StateChange begin = { + .type = Sxe::StateChangeDocumentBegin, + .document_begin = document_begin, + }; + state.push_back(begin); + m_rid = get_uuid(); + Sxe::New new_ = Sxe::New::Element( + /*rid*/ m_rid.c_str(), + /*ns*/ "http://www.w3.org/2000/svg", + /*name*/ "svg" + ); + Sxe::StateChange change = { + .type = Sxe::StateChangeNew, + .new_ = new_, + }; + state.push_back(change); + Sxe::DocumentEnd document_end = { + .last_sender = "foo@bar/baz", + .last_id = "unknown", + }; + Sxe::StateChange end = { + .type = Sxe::StateChangeDocumentEnd, + .document_end = document_end, + }; + state.push_back(end); + return state; +} + void XMPPObserver::notifyUndoCommitEvent(Event *ee) { XML::Event *e = ee->event; @@ -272,14 +314,12 @@ void XMPPObserver::notifyUndoCommitEvent(Event *ee) } name = name.substr(4); - Sxe::New new_ = { - .rid = rid.c_str(), - .type = "element", - .name = name.c_str(), - .ns = "http://www.w3.org/2000/svg", - .parent = "", - .chdata = "", - }; + Sxe::New new_ = Sxe::New::Element( + /*rid*/ rid.c_str(), + /*parent*/ client->m_rid.c_str(), + /*ns*/ "http://www.w3.org/2000/svg", + /*name*/ name.c_str() + ); Sxe::StateChange change = { .type = Sxe::StateChangeNew, .new_ = new_, @@ -287,27 +327,41 @@ void XMPPObserver::notifyUndoCommitEvent(Event *ee) std::vector state_changes = {}; state_changes.push_back(change); + // XXX: huge hack to keep all rids on the stack… + size_t num_attrs = 0; for (Util::List it = node->attributeList(); it; ++it) { - std::string attr_rid = get_uuid(); - - Sxe::New new_ = { - .rid = attr_rid.c_str(), - .type = "attr", - .name = g_quark_to_string(it->key), - .ns = "", - .parent = rid.c_str(), - .chdata = it->value, - }; + ++num_attrs; + } + std::vector rids; + rids.reserve(num_attrs); + + size_t cur_attr = 0; + for (Util::List it = node->attributeList(); it; ++it) { + rids.push_back(get_uuid()); + + Sxe::New new_ = Sxe::New::Attr( + /*rid*/ rids[cur_attr].c_str(), + /*parent*/ rid.c_str(), + /*name*/ g_quark_to_string(it->key), + /*chdata*/ it->value + ); Sxe::StateChange change = { .type = Sxe::StateChangeNew, .new_ = new_, }; state_changes.push_back(change); + ++cur_attr; } - Message msg(Message::Normal, JID("linkmauve@linkmauve.fr")); - msg.addExtension(new Sxe("session", "id", Sxe::TypeState, {}, state_changes)); - client->send(msg.tag()); + std::vector copy(state_changes); + Sxe* coucou = new Sxe("session", "id", copy); + fprintf(stderr, "coucou: %s\n", coucou->tag()->xml().c_str()); + + //Message msg(Message::Normal, JID("linkmauve@linkmauve.fr")); + //msg.addExtension(new Sxe("session", "id", Sxe::TypeState, {}, state_changes)); + std::string sid = "foo"; + client->sendChanges(JID("test@linkmauve.fr/coucou"), sid, state_changes); + printf("coucou\n"); } else if ((edel = dynamic_cast(e))) { std::cout << "EventDel" << std::endl; sp_repr_write_stream(edel->child, *writer, 0, false, GQuark(0), 0, 0); diff --git a/src/extension/plugins/xmpp/xmpp.h b/src/extension/plugins/xmpp/xmpp.h index b3c84c8cae..3b9cda5986 100644 --- a/src/extension/plugins/xmpp/xmpp.h +++ b/src/extension/plugins/xmpp/xmpp.h @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -37,7 +38,7 @@ class Extension; namespace Internal { -class InkscapeClient : gloox::ConnectionListener, gloox::LogHandler, gloox::Jingle::SessionHandler { +class InkscapeClient : gloox::ConnectionListener, gloox::LogHandler, gloox::Jingle::SessionHandler, gloox::SxeSessionHandler { public: InkscapeClient(gloox::JID jid, const std::string& password); bool connect(); @@ -45,6 +46,7 @@ public: bool isConnected(); gloox::ConnectionError recv(); void send(gloox::Tag *tag); + void sendChanges(gloox::JID recipient, std::string& sid, std::vector state_changes); static int runLoop(void *data); private: @@ -61,10 +63,18 @@ private: void handleSessionActionError(gloox::Jingle::Action action, gloox::Jingle::Session* session, const gloox::Error* error) override; void handleIncomingSession(gloox::Jingle::Session* session) override; + // From SxeSessionHandler + std::vector getCurrentState(const std::string& session, const std::string& id) override; + private: std::unique_ptr client; std::unique_ptr session_manager; + std::unique_ptr sxe_manager; bool connected; + +// XXX: hack +public: + std::string m_rid; }; -- GitLab From 3a3f9ef2140e9160765800e1ee866d82da0e4bcf Mon Sep 17 00:00:00 2001 From: Emmanuel Gil Peyrot Date: Tue, 10 Mar 2020 18:55:19 +0100 Subject: [PATCH 15/20] Web: Add a web viewer. --- src/extension/plugins/xmpp/web/index.xhtml | 30 + src/extension/plugins/xmpp/web/spinner.svg | 7 + src/extension/plugins/xmpp/web/strophe.js | 6101 ++++++++++++++++++++ src/extension/plugins/xmpp/web/svg.js | 300 + 4 files changed, 6438 insertions(+) create mode 100644 src/extension/plugins/xmpp/web/index.xhtml create mode 100644 src/extension/plugins/xmpp/web/spinner.svg create mode 100644 src/extension/plugins/xmpp/web/strophe.js create mode 100644 src/extension/plugins/xmpp/web/svg.js diff --git a/src/extension/plugins/xmpp/web/index.xhtml b/src/extension/plugins/xmpp/web/index.xhtml new file mode 100644 index 0000000000..b11f25044f --- /dev/null +++ b/src/extension/plugins/xmpp/web/index.xhtml @@ -0,0 +1,30 @@ + + + + SVG viewer + + + +

SVG viewer

+ +
+ + + + +
+ + + + + + + diff --git a/src/extension/plugins/xmpp/web/spinner.svg b/src/extension/plugins/xmpp/web/spinner.svg new file mode 100644 index 0000000000..3fbc8b86ea --- /dev/null +++ b/src/extension/plugins/xmpp/web/spinner.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/extension/plugins/xmpp/web/strophe.js b/src/extension/plugins/xmpp/web/strophe.js new file mode 100644 index 0000000000..bd95f2c328 --- /dev/null +++ b/src/extension/plugins/xmpp/web/strophe.js @@ -0,0 +1,6101 @@ +(function webpackUniversalModuleDefinition(root, factory) { + if(typeof exports === 'object' && typeof module === 'object') + module.exports = factory(); + else if(typeof define === 'function' && define.amd) + define([], factory); + else if(typeof exports === 'object') + exports["strophe"] = factory(); + else + root["strophe"] = factory(); +})(window, function() { +return /******/ (function(modules) { // webpackBootstrap +/******/ // The module cache +/******/ var installedModules = {}; +/******/ +/******/ // The require function +/******/ function __webpack_require__(moduleId) { +/******/ +/******/ // Check if module is in cache +/******/ if(installedModules[moduleId]) { +/******/ return installedModules[moduleId].exports; +/******/ } +/******/ // Create a new module (and put it into the cache) +/******/ var module = installedModules[moduleId] = { +/******/ i: moduleId, +/******/ l: false, +/******/ exports: {} +/******/ }; +/******/ +/******/ // Execute the module function +/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); +/******/ +/******/ // Flag the module as loaded +/******/ module.l = true; +/******/ +/******/ // Return the exports of the module +/******/ return module.exports; +/******/ } +/******/ +/******/ +/******/ // expose the modules object (__webpack_modules__) +/******/ __webpack_require__.m = modules; +/******/ +/******/ // expose the module cache +/******/ __webpack_require__.c = installedModules; +/******/ +/******/ // define getter function for harmony exports +/******/ __webpack_require__.d = function(exports, name, getter) { +/******/ if(!__webpack_require__.o(exports, name)) { +/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); +/******/ } +/******/ }; +/******/ +/******/ // define __esModule on exports +/******/ __webpack_require__.r = function(exports) { +/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { +/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); +/******/ } +/******/ Object.defineProperty(exports, '__esModule', { value: true }); +/******/ }; +/******/ +/******/ // create a fake namespace object +/******/ // mode & 1: value is a module id, require it +/******/ // mode & 2: merge all properties of value into the ns +/******/ // mode & 4: return value when already ns object +/******/ // mode & 8|1: behave like require +/******/ __webpack_require__.t = function(value, mode) { +/******/ if(mode & 1) value = __webpack_require__(value); +/******/ if(mode & 8) return value; +/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; +/******/ var ns = Object.create(null); +/******/ __webpack_require__.r(ns); +/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); +/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); +/******/ return ns; +/******/ }; +/******/ +/******/ // getDefaultExport function for compatibility with non-harmony modules +/******/ __webpack_require__.n = function(module) { +/******/ var getter = module && module.__esModule ? +/******/ function getDefault() { return module['default']; } : +/******/ function getModuleExports() { return module; }; +/******/ __webpack_require__.d(getter, 'a', getter); +/******/ return getter; +/******/ }; +/******/ +/******/ // Object.prototype.hasOwnProperty.call +/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; +/******/ +/******/ // __webpack_public_path__ +/******/ __webpack_require__.p = ""; +/******/ +/******/ +/******/ // Load entry module and return exports +/******/ return __webpack_require__(__webpack_require__.s = "./src/strophe.js"); +/******/ }) +/************************************************************************/ +/******/ ({ + +/***/ "./node_modules/webpack/buildin/global.js": +/*!***********************************!*\ + !*** (webpack)/buildin/global.js ***! + \***********************************/ +/*! no static exports found */ +/***/ (function(module, exports) { + +var g; + +// This works in non-strict mode +g = (function() { + return this; +})(); + +try { + // This works if eval is allowed (see CSP) + g = g || Function("return this")() || (1, eval)("this"); +} catch (e) { + // This works if the window reference is available + if (typeof window === "object") g = window; +} + +// g can still be undefined, but nothing to do about it... +// We return undefined, instead of nothing here, so it's +// easier to handle this case. if(!global) { ...} + +module.exports = g; + + +/***/ }), + +/***/ "./src/bosh.js": +/*!*********************!*\ + !*** ./src/bosh.js ***! + \*********************/ +/*! no exports provided */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony import */ var core__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! core */ "./src/core.js"); +/* + This program is distributed under the terms of the MIT license. + Please see the LICENSE file for details. + + Copyright 2006-2008, OGG, LLC +*/ + +/* global window, setTimeout, clearTimeout, XMLHttpRequest, ActiveXObject */ + +var Strophe = core__WEBPACK_IMPORTED_MODULE_0__["default"].Strophe; +var $build = core__WEBPACK_IMPORTED_MODULE_0__["default"].$build; +/** PrivateClass: Strophe.Request + * _Private_ helper class that provides a cross implementation abstraction + * for a BOSH related XMLHttpRequest. + * + * The Strophe.Request class is used internally to encapsulate BOSH request + * information. It is not meant to be used from user's code. + */ + +/** PrivateConstructor: Strophe.Request + * Create and initialize a new Strophe.Request object. + * + * Parameters: + * (XMLElement) elem - The XML data to be sent in the request. + * (Function) func - The function that will be called when the + * XMLHttpRequest readyState changes. + * (Integer) rid - The BOSH rid attribute associated with this request. + * (Integer) sends - The number of times this same request has been sent. + */ + +Strophe.Request = function (elem, func, rid, sends) { + this.id = ++Strophe._requestId; + this.xmlData = elem; + this.data = Strophe.serialize(elem); // save original function in case we need to make a new request + // from this one. + + this.origFunc = func; + this.func = func; + this.rid = rid; + this.date = NaN; + this.sends = sends || 0; + this.abort = false; + this.dead = null; + + this.age = function () { + if (!this.date) { + return 0; + } + + var now = new Date(); + return (now - this.date) / 1000; + }; + + this.timeDead = function () { + if (!this.dead) { + return 0; + } + + var now = new Date(); + return (now - this.dead) / 1000; + }; + + this.xhr = this._newXHR(); +}; + +Strophe.Request.prototype = { + /** PrivateFunction: getResponse + * Get a response from the underlying XMLHttpRequest. + * + * This function attempts to get a response from the request and checks + * for errors. + * + * Throws: + * "parsererror" - A parser error occured. + * "bad-format" - The entity has sent XML that cannot be processed. + * + * Returns: + * The DOM element tree of the response. + */ + getResponse: function getResponse() { + var node = null; + + if (this.xhr.responseXML && this.xhr.responseXML.documentElement) { + node = this.xhr.responseXML.documentElement; + + if (node.tagName === "parsererror") { + Strophe.error("invalid response received"); + Strophe.error("responseText: " + this.xhr.responseText); + Strophe.error("responseXML: " + Strophe.serialize(this.xhr.responseXML)); + throw new Error("parsererror"); + } + } else if (this.xhr.responseText) { + // In React Native, we may get responseText but no responseXML. We can try to parse it manually. + Strophe.debug("Got responseText but no responseXML; attempting to parse it with DOMParser..."); + node = new DOMParser().parseFromString(this.xhr.responseText, 'application/xml').documentElement; + + if (!node) { + throw new Error('Parsing produced null node'); + } else if (node.querySelector('parsererror')) { + Strophe.error("invalid response received: " + node.querySelector('parsererror').textContent); + Strophe.error("responseText: " + this.xhr.responseText); + var error = new Error(); + error.name = Strophe.ErrorCondition.BAD_FORMAT; + throw error; + } + } + + return node; + }, + + /** PrivateFunction: _newXHR + * _Private_ helper function to create XMLHttpRequests. + * + * This function creates XMLHttpRequests across all implementations. + * + * Returns: + * A new XMLHttpRequest. + */ + _newXHR: function _newXHR() { + var xhr = null; + + if (window.XMLHttpRequest) { + xhr = new XMLHttpRequest(); + + if (xhr.overrideMimeType) { + xhr.overrideMimeType("text/xml; charset=utf-8"); + } + } else if (window.ActiveXObject) { + xhr = new ActiveXObject("Microsoft.XMLHTTP"); + } // use Function.bind() to prepend ourselves as an argument + + + xhr.onreadystatechange = this.func.bind(null, this); + return xhr; + } +}; +/** Class: Strophe.Bosh + * _Private_ helper class that handles BOSH Connections + * + * The Strophe.Bosh class is used internally by Strophe.Connection + * to encapsulate BOSH sessions. It is not meant to be used from user's code. + */ + +/** File: bosh.js + * A JavaScript library to enable BOSH in Strophejs. + * + * this library uses Bidirectional-streams Over Synchronous HTTP (BOSH) + * to emulate a persistent, stateful, two-way connection to an XMPP server. + * More information on BOSH can be found in XEP 124. + */ + +/** PrivateConstructor: Strophe.Bosh + * Create and initialize a Strophe.Bosh object. + * + * Parameters: + * (Strophe.Connection) connection - The Strophe.Connection that will use BOSH. + * + * Returns: + * A new Strophe.Bosh object. + */ + +Strophe.Bosh = function (connection) { + this._conn = connection; + /* request id for body tags */ + + this.rid = Math.floor(Math.random() * 4294967295); + /* The current session ID. */ + + this.sid = null; // default BOSH values + + this.hold = 1; + this.wait = 60; + this.window = 5; + this.errors = 0; + this.inactivity = null; + this.lastResponseHeaders = null; + this._requests = []; +}; + +Strophe.Bosh.prototype = { + /** Variable: strip + * + * BOSH-Connections will have all stanzas wrapped in a tag when + * passed to or . + * To strip this tag, User code can set to "body": + * + * > Strophe.Bosh.prototype.strip = "body"; + * + * This will enable stripping of the body tag in both + * and . + */ + strip: null, + + /** PrivateFunction: _buildBody + * _Private_ helper function to generate the wrapper for BOSH. + * + * Returns: + * A Strophe.Builder with a element. + */ + _buildBody: function _buildBody() { + var bodyWrap = $build('body', { + 'rid': this.rid++, + 'xmlns': Strophe.NS.HTTPBIND + }); + + if (this.sid !== null) { + bodyWrap.attrs({ + 'sid': this.sid + }); + } + + if (this._conn.options.keepalive && this._conn._sessionCachingSupported()) { + this._cacheSession(); + } + + return bodyWrap; + }, + + /** PrivateFunction: _reset + * Reset the connection. + * + * This function is called by the reset function of the Strophe Connection + */ + _reset: function _reset() { + this.rid = Math.floor(Math.random() * 4294967295); + this.sid = null; + this.errors = 0; + + if (this._conn._sessionCachingSupported()) { + window.sessionStorage.removeItem('strophe-bosh-session'); + } + + this._conn.nextValidRid(this.rid); + }, + + /** PrivateFunction: _connect + * _Private_ function that initializes the BOSH connection. + * + * Creates and sends the Request that initializes the BOSH connection. + */ + _connect: function _connect(wait, hold, route) { + this.wait = wait || this.wait; + this.hold = hold || this.hold; + this.errors = 0; + + var body = this._buildBody().attrs({ + "to": this._conn.domain, + "xml:lang": "en", + "wait": this.wait, + "hold": this.hold, + "content": "text/xml; charset=utf-8", + "ver": "1.6", + "xmpp:version": "1.0", + "xmlns:xmpp": Strophe.NS.BOSH + }); + + if (route) { + body.attrs({ + 'route': route + }); + } + + var _connect_cb = this._conn._connect_cb; + + this._requests.push(new Strophe.Request(body.tree(), this._onRequestStateChange.bind(this, _connect_cb.bind(this._conn)), body.tree().getAttribute("rid"))); + + this._throttledRequestHandler(); + }, + + /** PrivateFunction: _attach + * Attach to an already created and authenticated BOSH session. + * + * This function is provided to allow Strophe to attach to BOSH + * sessions which have been created externally, perhaps by a Web + * application. This is often used to support auto-login type features + * without putting user credentials into the page. + * + * Parameters: + * (String) jid - The full JID that is bound by the session. + * (String) sid - The SID of the BOSH session. + * (String) rid - The current RID of the BOSH session. This RID + * will be used by the next request. + * (Function) callback The connect callback function. + * (Integer) wait - The optional HTTPBIND wait value. This is the + * time the server will wait before returning an empty result for + * a request. The default setting of 60 seconds is recommended. + * Other settings will require tweaks to the Strophe.TIMEOUT value. + * (Integer) hold - The optional HTTPBIND hold value. This is the + * number of connections the server will hold at one time. This + * should almost always be set to 1 (the default). + * (Integer) wind - The optional HTTBIND window value. This is the + * allowed range of request ids that are valid. The default is 5. + */ + _attach: function _attach(jid, sid, rid, callback, wait, hold, wind) { + this._conn.jid = jid; + this.sid = sid; + this.rid = rid; + this._conn.connect_callback = callback; + this._conn.domain = Strophe.getDomainFromJid(this._conn.jid); + this._conn.authenticated = true; + this._conn.connected = true; + this.wait = wait || this.wait; + this.hold = hold || this.hold; + this.window = wind || this.window; + + this._conn._changeConnectStatus(Strophe.Status.ATTACHED, null); + }, + + /** PrivateFunction: _restore + * Attempt to restore a cached BOSH session + * + * Parameters: + * (String) jid - The full JID that is bound by the session. + * This parameter is optional but recommended, specifically in cases + * where prebinded BOSH sessions are used where it's important to know + * that the right session is being restored. + * (Function) callback The connect callback function. + * (Integer) wait - The optional HTTPBIND wait value. This is the + * time the server will wait before returning an empty result for + * a request. The default setting of 60 seconds is recommended. + * Other settings will require tweaks to the Strophe.TIMEOUT value. + * (Integer) hold - The optional HTTPBIND hold value. This is the + * number of connections the server will hold at one time. This + * should almost always be set to 1 (the default). + * (Integer) wind - The optional HTTBIND window value. This is the + * allowed range of request ids that are valid. The default is 5. + */ + _restore: function _restore(jid, callback, wait, hold, wind) { + var session = JSON.parse(window.sessionStorage.getItem('strophe-bosh-session')); + + if (typeof session !== "undefined" && session !== null && session.rid && session.sid && session.jid && (typeof jid === "undefined" || jid === null || Strophe.getBareJidFromJid(session.jid) === Strophe.getBareJidFromJid(jid) || // If authcid is null, then it's an anonymous login, so + // we compare only the domains: + Strophe.getNodeFromJid(jid) === null && Strophe.getDomainFromJid(session.jid) === jid)) { + this._conn.restored = true; + + this._attach(session.jid, session.sid, session.rid, callback, wait, hold, wind); + } else { + var error = new Error("_restore: no restoreable session."); + error.name = "StropheSessionError"; + throw error; + } + }, + + /** PrivateFunction: _cacheSession + * _Private_ handler for the beforeunload event. + * + * This handler is used to process the Bosh-part of the initial request. + * Parameters: + * (Strophe.Request) bodyWrap - The received stanza. + */ + _cacheSession: function _cacheSession() { + if (this._conn.authenticated) { + if (this._conn.jid && this.rid && this.sid) { + window.sessionStorage.setItem('strophe-bosh-session', JSON.stringify({ + 'jid': this._conn.jid, + 'rid': this.rid, + 'sid': this.sid + })); + } + } else { + window.sessionStorage.removeItem('strophe-bosh-session'); + } + }, + + /** PrivateFunction: _connect_cb + * _Private_ handler for initial connection request. + * + * This handler is used to process the Bosh-part of the initial request. + * Parameters: + * (Strophe.Request) bodyWrap - The received stanza. + */ + _connect_cb: function _connect_cb(bodyWrap) { + var typ = bodyWrap.getAttribute("type"); + + if (typ !== null && typ === "terminate") { + // an error occurred + var cond = bodyWrap.getAttribute("condition"); + Strophe.error("BOSH-Connection failed: " + cond); + var conflict = bodyWrap.getElementsByTagName("conflict"); + + if (cond !== null) { + if (cond === "remote-stream-error" && conflict.length > 0) { + cond = "conflict"; + } + + this._conn._changeConnectStatus(Strophe.Status.CONNFAIL, cond); + } else { + this._conn._changeConnectStatus(Strophe.Status.CONNFAIL, "unknown"); + } + + this._conn._doDisconnect(cond); + + return Strophe.Status.CONNFAIL; + } // check to make sure we don't overwrite these if _connect_cb is + // called multiple times in the case of missing stream:features + + + if (!this.sid) { + this.sid = bodyWrap.getAttribute("sid"); + } + + var wind = bodyWrap.getAttribute('requests'); + + if (wind) { + this.window = parseInt(wind, 10); + } + + var hold = bodyWrap.getAttribute('hold'); + + if (hold) { + this.hold = parseInt(hold, 10); + } + + var wait = bodyWrap.getAttribute('wait'); + + if (wait) { + this.wait = parseInt(wait, 10); + } + + var inactivity = bodyWrap.getAttribute('inactivity'); + + if (inactivity) { + this.inactivity = parseInt(inactivity, 10); + } + }, + + /** PrivateFunction: _disconnect + * _Private_ part of Connection.disconnect for Bosh + * + * Parameters: + * (Request) pres - This stanza will be sent before disconnecting. + */ + _disconnect: function _disconnect(pres) { + this._sendTerminate(pres); + }, + + /** PrivateFunction: _doDisconnect + * _Private_ function to disconnect. + * + * Resets the SID and RID. + */ + _doDisconnect: function _doDisconnect() { + this.sid = null; + this.rid = Math.floor(Math.random() * 4294967295); + + if (this._conn._sessionCachingSupported()) { + window.sessionStorage.removeItem('strophe-bosh-session'); + } + + this._conn.nextValidRid(this.rid); + }, + + /** PrivateFunction: _emptyQueue + * _Private_ function to check if the Request queue is empty. + * + * Returns: + * True, if there are no Requests queued, False otherwise. + */ + _emptyQueue: function _emptyQueue() { + return this._requests.length === 0; + }, + + /** PrivateFunction: _callProtocolErrorHandlers + * _Private_ function to call error handlers registered for HTTP errors. + * + * Parameters: + * (Strophe.Request) req - The request that is changing readyState. + */ + _callProtocolErrorHandlers: function _callProtocolErrorHandlers(req) { + var reqStatus = this._getRequestStatus(req); + + var err_callback = this._conn.protocolErrorHandlers.HTTP[reqStatus]; + + if (err_callback) { + err_callback.call(this, reqStatus); + } + }, + + /** PrivateFunction: _hitError + * _Private_ function to handle the error count. + * + * Requests are resent automatically until their error count reaches + * 5. Each time an error is encountered, this function is called to + * increment the count and disconnect if the count is too high. + * + * Parameters: + * (Integer) reqStatus - The request status. + */ + _hitError: function _hitError(reqStatus) { + this.errors++; + Strophe.warn("request errored, status: " + reqStatus + ", number of errors: " + this.errors); + + if (this.errors > 4) { + this._conn._onDisconnectTimeout(); + } + }, + + /** PrivateFunction: _no_auth_received + * + * Called on stream start/restart when no stream:features + * has been received and sends a blank poll request. + */ + _no_auth_received: function _no_auth_received(callback) { + Strophe.warn("Server did not yet offer a supported authentication " + "mechanism. Sending a blank poll request."); + + if (callback) { + callback = callback.bind(this._conn); + } else { + callback = this._conn._connect_cb.bind(this._conn); + } + + var body = this._buildBody(); + + this._requests.push(new Strophe.Request(body.tree(), this._onRequestStateChange.bind(this, callback), body.tree().getAttribute("rid"))); + + this._throttledRequestHandler(); + }, + + /** PrivateFunction: _onDisconnectTimeout + * _Private_ timeout handler for handling non-graceful disconnection. + * + * Cancels all remaining Requests and clears the queue. + */ + _onDisconnectTimeout: function _onDisconnectTimeout() { + this._abortAllRequests(); + }, + + /** PrivateFunction: _abortAllRequests + * _Private_ helper function that makes sure all pending requests are aborted. + */ + _abortAllRequests: function _abortAllRequests() { + while (this._requests.length > 0) { + var req = this._requests.pop(); + + req.abort = true; + req.xhr.abort(); // jslint complains, but this is fine. setting to empty func + // is necessary for IE6 + + req.xhr.onreadystatechange = function () {}; // jshint ignore:line + + } + }, + + /** PrivateFunction: _onIdle + * _Private_ handler called by Strophe.Connection._onIdle + * + * Sends all queued Requests or polls with empty Request if there are none. + */ + _onIdle: function _onIdle() { + var data = this._conn._data; // if no requests are in progress, poll + + if (this._conn.authenticated && this._requests.length === 0 && data.length === 0 && !this._conn.disconnecting) { + Strophe.info("no requests during idle cycle, sending " + "blank request"); + data.push(null); + } + + if (this._conn.paused) { + return; + } + + if (this._requests.length < 2 && data.length > 0) { + var body = this._buildBody(); + + for (var i = 0; i < data.length; i++) { + if (data[i] !== null) { + if (data[i] === "restart") { + body.attrs({ + "to": this._conn.domain, + "xml:lang": "en", + "xmpp:restart": "true", + "xmlns:xmpp": Strophe.NS.BOSH + }); + } else { + body.cnode(data[i]).up(); + } + } + } + + delete this._conn._data; + this._conn._data = []; + + this._requests.push(new Strophe.Request(body.tree(), this._onRequestStateChange.bind(this, this._conn._dataRecv.bind(this._conn)), body.tree().getAttribute("rid"))); + + this._throttledRequestHandler(); + } + + if (this._requests.length > 0) { + var time_elapsed = this._requests[0].age(); + + if (this._requests[0].dead !== null) { + if (this._requests[0].timeDead() > Math.floor(Strophe.SECONDARY_TIMEOUT * this.wait)) { + this._throttledRequestHandler(); + } + } + + if (time_elapsed > Math.floor(Strophe.TIMEOUT * this.wait)) { + Strophe.warn("Request " + this._requests[0].id + " timed out, over " + Math.floor(Strophe.TIMEOUT * this.wait) + " seconds since last activity"); + + this._throttledRequestHandler(); + } + } + }, + + /** PrivateFunction: _getRequestStatus + * + * Returns the HTTP status code from a Strophe.Request + * + * Parameters: + * (Strophe.Request) req - The Strophe.Request instance. + * (Integer) def - The default value that should be returned if no + * status value was found. + */ + _getRequestStatus: function _getRequestStatus(req, def) { + var reqStatus; + + if (req.xhr.readyState === 4) { + try { + reqStatus = req.xhr.status; + } catch (e) { + // ignore errors from undefined status attribute. Works + // around a browser bug + Strophe.error("Caught an error while retrieving a request's status, " + "reqStatus: " + reqStatus); + } + } + + if (typeof reqStatus === "undefined") { + reqStatus = typeof def === 'number' ? def : 0; + } + + return reqStatus; + }, + + /** PrivateFunction: _onRequestStateChange + * _Private_ handler for Strophe.Request state changes. + * + * This function is called when the XMLHttpRequest readyState changes. + * It contains a lot of error handling logic for the many ways that + * requests can fail, and calls the request callback when requests + * succeed. + * + * Parameters: + * (Function) func - The handler for the request. + * (Strophe.Request) req - The request that is changing readyState. + */ + _onRequestStateChange: function _onRequestStateChange(func, req) { + Strophe.debug("request id " + req.id + "." + req.sends + " state changed to " + req.xhr.readyState); + + if (req.abort) { + req.abort = false; + return; + } + + if (req.xhr.readyState !== 4) { + // The request is not yet complete + return; + } + + var reqStatus = this._getRequestStatus(req); + + this.lastResponseHeaders = req.xhr.getAllResponseHeaders(); + + if (this.disconnecting && reqStatus >= 400) { + this._hitError(reqStatus); + + this._callProtocolErrorHandlers(req); + + return; + } + + var valid_request = reqStatus > 0 && reqStatus < 500; + var too_many_retries = req.sends > this._conn.maxRetries; + + if (valid_request || too_many_retries) { + // remove from internal queue + this._removeRequest(req); + + Strophe.debug("request id " + req.id + " should now be removed"); + } + + if (reqStatus === 200) { + // request succeeded + var reqIs0 = this._requests[0] === req; + var reqIs1 = this._requests[1] === req; // if request 1 finished, or request 0 finished and request + // 1 is over Strophe.SECONDARY_TIMEOUT seconds old, we need to + // restart the other - both will be in the first spot, as the + // completed request has been removed from the queue already + + if (reqIs1 || reqIs0 && this._requests.length > 0 && this._requests[0].age() > Math.floor(Strophe.SECONDARY_TIMEOUT * this.wait)) { + this._restartRequest(0); + } + + this._conn.nextValidRid(Number(req.rid) + 1); + + Strophe.debug("request id " + req.id + "." + req.sends + " got 200"); + func(req); // call handler + + this.errors = 0; + } else if (reqStatus === 0 || reqStatus >= 400 && reqStatus < 600 || reqStatus >= 12000) { + // request failed + Strophe.error("request id " + req.id + "." + req.sends + " error " + reqStatus + " happened"); + + this._hitError(reqStatus); + + this._callProtocolErrorHandlers(req); + + if (reqStatus >= 400 && reqStatus < 500) { + this._conn._changeConnectStatus(Strophe.Status.DISCONNECTING, null); + + this._conn._doDisconnect(); + } + } else { + Strophe.error("request id " + req.id + "." + req.sends + " error " + reqStatus + " happened"); + } + + if (!valid_request && !too_many_retries) { + this._throttledRequestHandler(); + } else if (too_many_retries && !this._conn.connected) { + this._conn._changeConnectStatus(Strophe.Status.CONNFAIL, "giving-up"); + } + }, + + /** PrivateFunction: _processRequest + * _Private_ function to process a request in the queue. + * + * This function takes requests off the queue and sends them and + * restarts dead requests. + * + * Parameters: + * (Integer) i - The index of the request in the queue. + */ + _processRequest: function _processRequest(i) { + var _this = this; + + var req = this._requests[i]; + + var reqStatus = this._getRequestStatus(req, -1); // make sure we limit the number of retries + + + if (req.sends > this._conn.maxRetries) { + this._conn._onDisconnectTimeout(); + + return; + } + + var time_elapsed = req.age(); + var primary_timeout = !isNaN(time_elapsed) && time_elapsed > Math.floor(Strophe.TIMEOUT * this.wait); + var secondary_timeout = req.dead !== null && req.timeDead() > Math.floor(Strophe.SECONDARY_TIMEOUT * this.wait); + var server_error = req.xhr.readyState === 4 && (reqStatus < 1 || reqStatus >= 500); + + if (primary_timeout || secondary_timeout || server_error) { + if (secondary_timeout) { + Strophe.error("Request ".concat(this._requests[i].id, " timed out (secondary), restarting")); + } + + req.abort = true; + req.xhr.abort(); // setting to null fails on IE6, so set to empty function + + req.xhr.onreadystatechange = function () {}; + + this._requests[i] = new Strophe.Request(req.xmlData, req.origFunc, req.rid, req.sends); + req = this._requests[i]; + } + + if (req.xhr.readyState === 0) { + Strophe.debug("request id " + req.id + "." + req.sends + " posting"); + + try { + var content_type = this._conn.options.contentType || "text/xml; charset=utf-8"; + req.xhr.open("POST", this._conn.service, this._conn.options.sync ? false : true); + + if (typeof req.xhr.setRequestHeader !== 'undefined') { + // IE9 doesn't have setRequestHeader + req.xhr.setRequestHeader("Content-Type", content_type); + } + + if (this._conn.options.withCredentials) { + req.xhr.withCredentials = true; + } + } catch (e2) { + Strophe.error("XHR open failed: " + e2.toString()); + + if (!this._conn.connected) { + this._conn._changeConnectStatus(Strophe.Status.CONNFAIL, "bad-service"); + } + + this._conn.disconnect(); + + return; + } // Fires the XHR request -- may be invoked immediately + // or on a gradually expanding retry window for reconnects + + + var sendFunc = function sendFunc() { + req.date = new Date(); + + if (_this._conn.options.customHeaders) { + var headers = _this._conn.options.customHeaders; + + for (var header in headers) { + if (Object.prototype.hasOwnProperty.call(headers, header)) { + req.xhr.setRequestHeader(header, headers[header]); + } + } + } + + req.xhr.send(req.data); + }; // Implement progressive backoff for reconnects -- + // First retry (send === 1) should also be instantaneous + + + if (req.sends > 1) { + // Using a cube of the retry number creates a nicely + // expanding retry window + var backoff = Math.min(Math.floor(Strophe.TIMEOUT * this.wait), Math.pow(req.sends, 3)) * 1000; + setTimeout(function () { + // XXX: setTimeout should be called only with function expressions (23974bc1) + sendFunc(); + }, backoff); + } else { + sendFunc(); + } + + req.sends++; + + if (this._conn.xmlOutput !== Strophe.Connection.prototype.xmlOutput) { + if (req.xmlData.nodeName === this.strip && req.xmlData.childNodes.length) { + this._conn.xmlOutput(req.xmlData.childNodes[0]); + } else { + this._conn.xmlOutput(req.xmlData); + } + } + + if (this._conn.rawOutput !== Strophe.Connection.prototype.rawOutput) { + this._conn.rawOutput(req.data); + } + } else { + Strophe.debug("_processRequest: " + (i === 0 ? "first" : "second") + " request has readyState of " + req.xhr.readyState); + } + }, + + /** PrivateFunction: _removeRequest + * _Private_ function to remove a request from the queue. + * + * Parameters: + * (Strophe.Request) req - The request to remove. + */ + _removeRequest: function _removeRequest(req) { + Strophe.debug("removing request"); + + for (var i = this._requests.length - 1; i >= 0; i--) { + if (req === this._requests[i]) { + this._requests.splice(i, 1); + } + } // IE6 fails on setting to null, so set to empty function + + + req.xhr.onreadystatechange = function () {}; + + this._throttledRequestHandler(); + }, + + /** PrivateFunction: _restartRequest + * _Private_ function to restart a request that is presumed dead. + * + * Parameters: + * (Integer) i - The index of the request in the queue. + */ + _restartRequest: function _restartRequest(i) { + var req = this._requests[i]; + + if (req.dead === null) { + req.dead = new Date(); + } + + this._processRequest(i); + }, + + /** PrivateFunction: _reqToData + * _Private_ function to get a stanza out of a request. + * + * Tries to extract a stanza out of a Request Object. + * When this fails the current connection will be disconnected. + * + * Parameters: + * (Object) req - The Request. + * + * Returns: + * The stanza that was passed. + */ + _reqToData: function _reqToData(req) { + try { + return req.getResponse(); + } catch (e) { + if (e.message !== "parsererror") { + throw e; + } + + this._conn.disconnect("strophe-parsererror"); + } + }, + + /** PrivateFunction: _sendTerminate + * _Private_ function to send initial disconnect sequence. + * + * This is the first step in a graceful disconnect. It sends + * the BOSH server a terminate body and includes an unavailable + * presence if authentication has completed. + */ + _sendTerminate: function _sendTerminate(pres) { + Strophe.info("_sendTerminate was called"); + + var body = this._buildBody().attrs({ + type: "terminate" + }); + + if (pres) { + body.cnode(pres.tree()); + } + + var req = new Strophe.Request(body.tree(), this._onRequestStateChange.bind(this, this._conn._dataRecv.bind(this._conn)), body.tree().getAttribute("rid")); + + this._requests.push(req); + + this._throttledRequestHandler(); + }, + + /** PrivateFunction: _send + * _Private_ part of the Connection.send function for BOSH + * + * Just triggers the RequestHandler to send the messages that are in the queue + */ + _send: function _send() { + var _this2 = this; + + clearTimeout(this._conn._idleTimeout); + + this._throttledRequestHandler(); + + this._conn._idleTimeout = setTimeout(function () { + return _this2._conn._onIdle(); + }, 100); + }, + + /** PrivateFunction: _sendRestart + * + * Send an xmpp:restart stanza. + */ + _sendRestart: function _sendRestart() { + this._throttledRequestHandler(); + + clearTimeout(this._conn._idleTimeout); + }, + + /** PrivateFunction: _throttledRequestHandler + * _Private_ function to throttle requests to the connection window. + * + * This function makes sure we don't send requests so fast that the + * request ids overflow the connection window in the case that one + * request died. + */ + _throttledRequestHandler: function _throttledRequestHandler() { + if (!this._requests) { + Strophe.debug("_throttledRequestHandler called with " + "undefined requests"); + } else { + Strophe.debug("_throttledRequestHandler called with " + this._requests.length + " requests"); + } + + if (!this._requests || this._requests.length === 0) { + return; + } + + if (this._requests.length > 0) { + this._processRequest(0); + } + + if (this._requests.length > 1 && Math.abs(this._requests[0].rid - this._requests[1].rid) < this.window) { + this._processRequest(1); + } + } +}; + +/***/ }), + +/***/ "./src/core.js": +/*!*********************!*\ + !*** ./src/core.js ***! + \*********************/ +/*! exports provided: Strophe, $build, $iq, $msg, $pres, SHA1, MD5, default */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Strophe", function() { return Strophe; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "$build", function() { return $build; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "$iq", function() { return $iq; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "$msg", function() { return $msg; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "$pres", function() { return $pres; }); +/* harmony import */ var md5__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! md5 */ "./src/md5.js"); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "MD5", function() { return md5__WEBPACK_IMPORTED_MODULE_0__["default"]; }); + +/* harmony import */ var sha1__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! sha1 */ "./src/sha1.js"); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "SHA1", function() { return sha1__WEBPACK_IMPORTED_MODULE_1__["default"]; }); + +/* harmony import */ var utils__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! utils */ "./src/utils.js"); +function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread(); } + +function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance"); } + +function _iterableToArray(iter) { if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === "[object Arguments]") return Array.from(iter); } + +function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } } + +function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } + +/* + This program is distributed under the terms of the MIT license. + Please see the LICENSE file for details. + + Copyright 2006-2018, OGG, LLC +*/ + +/*global define, document, sessionStorage, setTimeout, clearTimeout, ActiveXObject, DOMParser, btoa, atob, module */ + + + +/** Function: $build + * Create a Strophe.Builder. + * This is an alias for 'new Strophe.Builder(name, attrs)'. + * + * Parameters: + * (String) name - The root element name. + * (Object) attrs - The attributes for the root element in object notation. + * + * Returns: + * A new Strophe.Builder object. + */ + +function $build(name, attrs) { + return new Strophe.Builder(name, attrs); +} +/** Function: $msg + * Create a Strophe.Builder with a element as the root. + * + * Parameters: + * (Object) attrs - The element attributes in object notation. + * + * Returns: + * A new Strophe.Builder object. + */ + + +function $msg(attrs) { + return new Strophe.Builder("message", attrs); +} +/** Function: $iq + * Create a Strophe.Builder with an element as the root. + * + * Parameters: + * (Object) attrs - The element attributes in object notation. + * + * Returns: + * A new Strophe.Builder object. + */ + + +function $iq(attrs) { + return new Strophe.Builder("iq", attrs); +} +/** Function: $pres + * Create a Strophe.Builder with a element as the root. + * + * Parameters: + * (Object) attrs - The element attributes in object notation. + * + * Returns: + * A new Strophe.Builder object. + */ + + +function $pres(attrs) { + return new Strophe.Builder("presence", attrs); +} +/** Class: Strophe + * An object container for all Strophe library functions. + * + * This class is just a container for all the objects and constants + * used in the library. It is not meant to be instantiated, but to + * provide a namespace for library objects, constants, and functions. + */ + + +var Strophe = { + /** Constant: VERSION */ + VERSION: "1.3.1", + + /** Constants: XMPP Namespace Constants + * Common namespace constants from the XMPP RFCs and XEPs. + * + * NS.HTTPBIND - HTTP BIND namespace from XEP 124. + * NS.BOSH - BOSH namespace from XEP 206. + * NS.CLIENT - Main XMPP client namespace. + * NS.AUTH - Legacy authentication namespace. + * NS.ROSTER - Roster operations namespace. + * NS.PROFILE - Profile namespace. + * NS.DISCO_INFO - Service discovery info namespace from XEP 30. + * NS.DISCO_ITEMS - Service discovery items namespace from XEP 30. + * NS.MUC - Multi-User Chat namespace from XEP 45. + * NS.SASL - XMPP SASL namespace from RFC 3920. + * NS.STREAM - XMPP Streams namespace from RFC 3920. + * NS.BIND - XMPP Binding namespace from RFC 3920. + * NS.SESSION - XMPP Session namespace from RFC 3920. + * NS.XHTML_IM - XHTML-IM namespace from XEP 71. + * NS.XHTML - XHTML body namespace from XEP 71. + */ + NS: { + HTTPBIND: "http://jabber.org/protocol/httpbind", + BOSH: "urn:xmpp:xbosh", + CLIENT: "jabber:client", + AUTH: "jabber:iq:auth", + ROSTER: "jabber:iq:roster", + PROFILE: "jabber:iq:profile", + DISCO_INFO: "http://jabber.org/protocol/disco#info", + DISCO_ITEMS: "http://jabber.org/protocol/disco#items", + MUC: "http://jabber.org/protocol/muc", + SASL: "urn:ietf:params:xml:ns:xmpp-sasl", + STREAM: "http://etherx.jabber.org/streams", + FRAMING: "urn:ietf:params:xml:ns:xmpp-framing", + BIND: "urn:ietf:params:xml:ns:xmpp-bind", + SESSION: "urn:ietf:params:xml:ns:xmpp-session", + VERSION: "jabber:iq:version", + STANZAS: "urn:ietf:params:xml:ns:xmpp-stanzas", + XHTML_IM: "http://jabber.org/protocol/xhtml-im", + XHTML: "http://www.w3.org/1999/xhtml" + }, + + /** Constants: XHTML_IM Namespace + * contains allowed tags, tag attributes, and css properties. + * Used in the createHtml function to filter incoming html into the allowed XHTML-IM subset. + * See http://xmpp.org/extensions/xep-0071.html#profile-summary for the list of recommended + * allowed tags and their attributes. + */ + XHTML: { + tags: ['a', 'blockquote', 'br', 'cite', 'em', 'img', 'li', 'ol', 'p', 'span', 'strong', 'ul', 'body'], + attributes: { + 'a': ['href'], + 'blockquote': ['style'], + 'br': [], + 'cite': ['style'], + 'em': [], + 'img': ['src', 'alt', 'style', 'height', 'width'], + 'li': ['style'], + 'ol': ['style'], + 'p': ['style'], + 'span': ['style'], + 'strong': [], + 'ul': ['style'], + 'body': [] + }, + css: ['background-color', 'color', 'font-family', 'font-size', 'font-style', 'font-weight', 'margin-left', 'margin-right', 'text-align', 'text-decoration'], + + /** Function: XHTML.validTag + * + * Utility method to determine whether a tag is allowed + * in the XHTML_IM namespace. + * + * XHTML tag names are case sensitive and must be lower case. + */ + validTag: function validTag(tag) { + for (var i = 0; i < Strophe.XHTML.tags.length; i++) { + if (tag === Strophe.XHTML.tags[i]) { + return true; + } + } + + return false; + }, + + /** Function: XHTML.validAttribute + * + * Utility method to determine whether an attribute is allowed + * as recommended per XEP-0071 + * + * XHTML attribute names are case sensitive and must be lower case. + */ + validAttribute: function validAttribute(tag, attribute) { + if (typeof Strophe.XHTML.attributes[tag] !== 'undefined' && Strophe.XHTML.attributes[tag].length > 0) { + for (var i = 0; i < Strophe.XHTML.attributes[tag].length; i++) { + if (attribute === Strophe.XHTML.attributes[tag][i]) { + return true; + } + } + } + + return false; + }, + validCSS: function validCSS(style) { + for (var i = 0; i < Strophe.XHTML.css.length; i++) { + if (style === Strophe.XHTML.css[i]) { + return true; + } + } + + return false; + } + }, + + /** Constants: Connection Status Constants + * Connection status constants for use by the connection handler + * callback. + * + * Status.ERROR - An error has occurred + * Status.CONNECTING - The connection is currently being made + * Status.CONNFAIL - The connection attempt failed + * Status.AUTHENTICATING - The connection is authenticating + * Status.AUTHFAIL - The authentication attempt failed + * Status.CONNECTED - The connection has succeeded + * Status.DISCONNECTED - The connection has been terminated + * Status.DISCONNECTING - The connection is currently being terminated + * Status.ATTACHED - The connection has been attached + * Status.REDIRECT - The connection has been redirected + * Status.CONNTIMEOUT - The connection has timed out + */ + Status: { + ERROR: 0, + CONNECTING: 1, + CONNFAIL: 2, + AUTHENTICATING: 3, + AUTHFAIL: 4, + CONNECTED: 5, + DISCONNECTED: 6, + DISCONNECTING: 7, + ATTACHED: 8, + REDIRECT: 9, + CONNTIMEOUT: 10 + }, + ErrorCondition: { + BAD_FORMAT: "bad-format", + CONFLICT: "conflict", + MISSING_JID_NODE: "x-strophe-bad-non-anon-jid", + NO_AUTH_MECH: "no-auth-mech", + UNKNOWN_REASON: "unknown" + }, + + /** Constants: Log Level Constants + * Logging level indicators. + * + * LogLevel.DEBUG - Debug output + * LogLevel.INFO - Informational output + * LogLevel.WARN - Warnings + * LogLevel.ERROR - Errors + * LogLevel.FATAL - Fatal errors + */ + LogLevel: { + DEBUG: 0, + INFO: 1, + WARN: 2, + ERROR: 3, + FATAL: 4 + }, + + /** PrivateConstants: DOM Element Type Constants + * DOM element types. + * + * ElementType.NORMAL - Normal element. + * ElementType.TEXT - Text data element. + * ElementType.FRAGMENT - XHTML fragment element. + */ + ElementType: { + NORMAL: 1, + TEXT: 3, + CDATA: 4, + FRAGMENT: 11 + }, + + /** PrivateConstants: Timeout Values + * Timeout values for error states. These values are in seconds. + * These should not be changed unless you know exactly what you are + * doing. + * + * TIMEOUT - Timeout multiplier. A waiting request will be considered + * failed after Math.floor(TIMEOUT * wait) seconds have elapsed. + * This defaults to 1.1, and with default wait, 66 seconds. + * SECONDARY_TIMEOUT - Secondary timeout multiplier. In cases where + * Strophe can detect early failure, it will consider the request + * failed if it doesn't return after + * Math.floor(SECONDARY_TIMEOUT * wait) seconds have elapsed. + * This defaults to 0.1, and with default wait, 6 seconds. + */ + TIMEOUT: 1.1, + SECONDARY_TIMEOUT: 0.1, + + /** Function: addNamespace + * This function is used to extend the current namespaces in + * Strophe.NS. It takes a key and a value with the key being the + * name of the new namespace, with its actual value. + * For example: + * Strophe.addNamespace('PUBSUB', "http://jabber.org/protocol/pubsub"); + * + * Parameters: + * (String) name - The name under which the namespace will be + * referenced under Strophe.NS + * (String) value - The actual namespace. + */ + addNamespace: function addNamespace(name, value) { + Strophe.NS[name] = value; + }, + + /** Function: forEachChild + * Map a function over some or all child elements of a given element. + * + * This is a small convenience function for mapping a function over + * some or all of the children of an element. If elemName is null, all + * children will be passed to the function, otherwise only children + * whose tag names match elemName will be passed. + * + * Parameters: + * (XMLElement) elem - The element to operate on. + * (String) elemName - The child element tag name filter. + * (Function) func - The function to apply to each child. This + * function should take a single argument, a DOM element. + */ + forEachChild: function forEachChild(elem, elemName, func) { + for (var i = 0; i < elem.childNodes.length; i++) { + var childNode = elem.childNodes[i]; + + if (childNode.nodeType === Strophe.ElementType.NORMAL && (!elemName || this.isTagEqual(childNode, elemName))) { + func(childNode); + } + } + }, + + /** Function: isTagEqual + * Compare an element's tag name with a string. + * + * This function is case sensitive. + * + * Parameters: + * (XMLElement) el - A DOM element. + * (String) name - The element name. + * + * Returns: + * true if the element's tag name matches _el_, and false + * otherwise. + */ + isTagEqual: function isTagEqual(el, name) { + return el.tagName === name; + }, + + /** PrivateVariable: _xmlGenerator + * _Private_ variable that caches a DOM document to + * generate elements. + */ + _xmlGenerator: null, + + /** PrivateFunction: _makeGenerator + * _Private_ function that creates a dummy XML DOM document to serve as + * an element and text node generator. + */ + _makeGenerator: function _makeGenerator() { + var doc; // IE9 does implement createDocument(); however, using it will cause the browser to leak memory on page unload. + // Here, we test for presence of createDocument() plus IE's proprietary documentMode attribute, which would be + // less than 10 in the case of IE9 and below. + + if (document.implementation.createDocument === undefined || document.implementation.createDocument && document.documentMode && document.documentMode < 10) { + doc = this._getIEXmlDom(); + doc.appendChild(doc.createElement('strophe')); + } else { + doc = document.implementation.createDocument('jabber:client', 'strophe', null); + } + + return doc; + }, + + /** Function: xmlGenerator + * Get the DOM document to generate elements. + * + * Returns: + * The currently used DOM document. + */ + xmlGenerator: function xmlGenerator() { + if (!Strophe._xmlGenerator) { + Strophe._xmlGenerator = Strophe._makeGenerator(); + } + + return Strophe._xmlGenerator; + }, + + /** PrivateFunction: _getIEXmlDom + * Gets IE xml doc object + * + * Returns: + * A Microsoft XML DOM Object + * See Also: + * http://msdn.microsoft.com/en-us/library/ms757837%28VS.85%29.aspx + */ + _getIEXmlDom: function _getIEXmlDom() { + var doc = null; + var docStrings = ["Msxml2.DOMDocument.6.0", "Msxml2.DOMDocument.5.0", "Msxml2.DOMDocument.4.0", "MSXML2.DOMDocument.3.0", "MSXML2.DOMDocument", "MSXML.DOMDocument", "Microsoft.XMLDOM"]; + + for (var d = 0; d < docStrings.length; d++) { + if (doc === null) { + try { + doc = new ActiveXObject(docStrings[d]); + } catch (e) { + doc = null; + } + } else { + break; + } + } + + return doc; + }, + + /** Function: xmlElement + * Create an XML DOM element. + * + * This function creates an XML DOM element correctly across all + * implementations. Note that these are not HTML DOM elements, which + * aren't appropriate for XMPP stanzas. + * + * Parameters: + * (String) name - The name for the element. + * (Array|Object) attrs - An optional array or object containing + * key/value pairs to use as element attributes. The object should + * be in the format {'key': 'value'} or {key: 'value'}. The array + * should have the format [['key1', 'value1'], ['key2', 'value2']]. + * (String) text - The text child data for the element. + * + * Returns: + * A new XML DOM element. + */ + xmlElement: function xmlElement(name) { + if (!name) { + return null; + } + + var node = Strophe.xmlGenerator().createElement(name); // FIXME: this should throw errors if args are the wrong type or + // there are more than two optional args + + for (var a = 1; a < arguments.length; a++) { + var arg = arguments[a]; + + if (!arg) { + continue; + } + + if (typeof arg === "string" || typeof arg === "number") { + node.appendChild(Strophe.xmlTextNode(arg)); + } else if (_typeof(arg) === "object" && typeof arg.sort === "function") { + for (var i = 0; i < arg.length; i++) { + var attr = arg[i]; + + if (_typeof(attr) === "object" && typeof attr.sort === "function" && attr[1] !== undefined && attr[1] !== null) { + node.setAttribute(attr[0], attr[1]); + } + } + } else if (_typeof(arg) === "object") { + for (var k in arg) { + if (Object.prototype.hasOwnProperty.call(arg, k) && arg[k] !== undefined && arg[k] !== null) { + node.setAttribute(k, arg[k]); + } + } + } + } + + return node; + }, + + /* Function: xmlescape + * Excapes invalid xml characters. + * + * Parameters: + * (String) text - text to escape. + * + * Returns: + * Escaped text. + */ + xmlescape: function xmlescape(text) { + text = text.replace(/\&/g, "&"); + text = text.replace(//g, ">"); + text = text.replace(/'/g, "'"); + text = text.replace(/"/g, """); + return text; + }, + + /* Function: xmlunescape + * Unexcapes invalid xml characters. + * + * Parameters: + * (String) text - text to unescape. + * + * Returns: + * Unescaped text. + */ + xmlunescape: function xmlunescape(text) { + text = text.replace(/\&/g, "&"); + text = text.replace(/</g, "<"); + text = text.replace(/>/g, ">"); + text = text.replace(/'/g, "'"); + text = text.replace(/"/g, "\""); + return text; + }, + + /** Function: xmlTextNode + * Creates an XML DOM text node. + * + * Provides a cross implementation version of document.createTextNode. + * + * Parameters: + * (String) text - The content of the text node. + * + * Returns: + * A new XML DOM text node. + */ + xmlTextNode: function xmlTextNode(text) { + return Strophe.xmlGenerator().createTextNode(text); + }, + + /** Function: xmlHtmlNode + * Creates an XML DOM html node. + * + * Parameters: + * (String) html - The content of the html node. + * + * Returns: + * A new XML DOM text node. + */ + xmlHtmlNode: function xmlHtmlNode(html) { + var node; //ensure text is escaped + + if (DOMParser) { + var parser = new DOMParser(); + node = parser.parseFromString(html, "text/xml"); + } else { + node = new ActiveXObject("Microsoft.XMLDOM"); + node.async = "false"; + node.loadXML(html); + } + + return node; + }, + + /** Function: getText + * Get the concatenation of all text children of an element. + * + * Parameters: + * (XMLElement) elem - A DOM element. + * + * Returns: + * A String with the concatenated text of all text element children. + */ + getText: function getText(elem) { + if (!elem) { + return null; + } + + var str = ""; + + if (elem.childNodes.length === 0 && elem.nodeType === Strophe.ElementType.TEXT) { + str += elem.nodeValue; + } + + for (var i = 0; i < elem.childNodes.length; i++) { + if (elem.childNodes[i].nodeType === Strophe.ElementType.TEXT) { + str += elem.childNodes[i].nodeValue; + } + } + + return Strophe.xmlescape(str); + }, + + /** Function: copyElement + * Copy an XML DOM element. + * + * This function copies a DOM element and all its descendants and returns + * the new copy. + * + * Parameters: + * (XMLElement) elem - A DOM element. + * + * Returns: + * A new, copied DOM element tree. + */ + copyElement: function copyElement(elem) { + var el; + + if (elem.nodeType === Strophe.ElementType.NORMAL) { + el = Strophe.xmlElement(elem.tagName); + + for (var i = 0; i < elem.attributes.length; i++) { + el.setAttribute(elem.attributes[i].nodeName, elem.attributes[i].value); + } + + for (var _i = 0; _i < elem.childNodes.length; _i++) { + el.appendChild(Strophe.copyElement(elem.childNodes[_i])); + } + } else if (elem.nodeType === Strophe.ElementType.TEXT) { + el = Strophe.xmlGenerator().createTextNode(elem.nodeValue); + } + + return el; + }, + + /** Function: createHtml + * Copy an HTML DOM element into an XML DOM. + * + * This function copies a DOM element and all its descendants and returns + * the new copy. + * + * Parameters: + * (HTMLElement) elem - A DOM element. + * + * Returns: + * A new, copied DOM element tree. + */ + createHtml: function createHtml(elem) { + var el; + + if (elem.nodeType === Strophe.ElementType.NORMAL) { + var tag = elem.nodeName.toLowerCase(); // XHTML tags must be lower case. + + if (Strophe.XHTML.validTag(tag)) { + try { + el = Strophe.xmlElement(tag); + + for (var i = 0; i < Strophe.XHTML.attributes[tag].length; i++) { + var attribute = Strophe.XHTML.attributes[tag][i]; + var value = elem.getAttribute(attribute); + + if (typeof value === 'undefined' || value === null || value === '' || value === false || value === 0) { + continue; + } + + if (attribute === 'style' && _typeof(value) === 'object' && typeof value.cssText !== 'undefined') { + value = value.cssText; // we're dealing with IE, need to get CSS out + } // filter out invalid css styles + + + if (attribute === 'style') { + var css = []; + var cssAttrs = value.split(';'); + + for (var j = 0; j < cssAttrs.length; j++) { + var attr = cssAttrs[j].split(':'); + var cssName = attr[0].replace(/^\s*/, "").replace(/\s*$/, "").toLowerCase(); + + if (Strophe.XHTML.validCSS(cssName)) { + var cssValue = attr[1].replace(/^\s*/, "").replace(/\s*$/, ""); + css.push(cssName + ': ' + cssValue); + } + } + + if (css.length > 0) { + value = css.join('; '); + el.setAttribute(attribute, value); + } + } else { + el.setAttribute(attribute, value); + } + } + + for (var _i2 = 0; _i2 < elem.childNodes.length; _i2++) { + el.appendChild(Strophe.createHtml(elem.childNodes[_i2])); + } + } catch (e) { + // invalid elements + el = Strophe.xmlTextNode(''); + } + } else { + el = Strophe.xmlGenerator().createDocumentFragment(); + + for (var _i3 = 0; _i3 < elem.childNodes.length; _i3++) { + el.appendChild(Strophe.createHtml(elem.childNodes[_i3])); + } + } + } else if (elem.nodeType === Strophe.ElementType.FRAGMENT) { + el = Strophe.xmlGenerator().createDocumentFragment(); + + for (var _i4 = 0; _i4 < elem.childNodes.length; _i4++) { + el.appendChild(Strophe.createHtml(elem.childNodes[_i4])); + } + } else if (elem.nodeType === Strophe.ElementType.TEXT) { + el = Strophe.xmlTextNode(elem.nodeValue); + } + + return el; + }, + + /** Function: escapeNode + * Escape the node part (also called local part) of a JID. + * + * Parameters: + * (String) node - A node (or local part). + * + * Returns: + * An escaped node (or local part). + */ + escapeNode: function escapeNode(node) { + if (typeof node !== "string") { + return node; + } + + return node.replace(/^\s+|\s+$/g, '').replace(/\\/g, "\\5c").replace(/ /g, "\\20").replace(/\"/g, "\\22").replace(/\&/g, "\\26").replace(/\'/g, "\\27").replace(/\//g, "\\2f").replace(/:/g, "\\3a").replace(//g, "\\3e").replace(/@/g, "\\40"); + }, + + /** Function: unescapeNode + * Unescape a node part (also called local part) of a JID. + * + * Parameters: + * (String) node - A node (or local part). + * + * Returns: + * An unescaped node (or local part). + */ + unescapeNode: function unescapeNode(node) { + if (typeof node !== "string") { + return node; + } + + return node.replace(/\\20/g, " ").replace(/\\22/g, '"').replace(/\\26/g, "&").replace(/\\27/g, "'").replace(/\\2f/g, "/").replace(/\\3a/g, ":").replace(/\\3c/g, "<").replace(/\\3e/g, ">").replace(/\\40/g, "@").replace(/\\5c/g, "\\"); + }, + + /** Function: getNodeFromJid + * Get the node portion of a JID String. + * + * Parameters: + * (String) jid - A JID. + * + * Returns: + * A String containing the node. + */ + getNodeFromJid: function getNodeFromJid(jid) { + if (jid.indexOf("@") < 0) { + return null; + } + + return jid.split("@")[0]; + }, + + /** Function: getDomainFromJid + * Get the domain portion of a JID String. + * + * Parameters: + * (String) jid - A JID. + * + * Returns: + * A String containing the domain. + */ + getDomainFromJid: function getDomainFromJid(jid) { + var bare = Strophe.getBareJidFromJid(jid); + + if (bare.indexOf("@") < 0) { + return bare; + } else { + var parts = bare.split("@"); + parts.splice(0, 1); + return parts.join('@'); + } + }, + + /** Function: getResourceFromJid + * Get the resource portion of a JID String. + * + * Parameters: + * (String) jid - A JID. + * + * Returns: + * A String containing the resource. + */ + getResourceFromJid: function getResourceFromJid(jid) { + var s = jid.split("/"); + + if (s.length < 2) { + return null; + } + + s.splice(0, 1); + return s.join('/'); + }, + + /** Function: getBareJidFromJid + * Get the bare JID from a JID String. + * + * Parameters: + * (String) jid - A JID. + * + * Returns: + * A String containing the bare JID. + */ + getBareJidFromJid: function getBareJidFromJid(jid) { + return jid ? jid.split("/")[0] : null; + }, + + /** PrivateFunction: _handleError + * _Private_ function that properly logs an error to the console + */ + _handleError: function _handleError(e) { + if (typeof e.stack !== "undefined") { + Strophe.fatal(e.stack); + } + + if (e.sourceURL) { + Strophe.fatal("error: " + this.handler + " " + e.sourceURL + ":" + e.line + " - " + e.name + ": " + e.message); + } else if (e.fileName) { + Strophe.fatal("error: " + this.handler + " " + e.fileName + ":" + e.lineNumber + " - " + e.name + ": " + e.message); + } else { + Strophe.fatal("error: " + e.message); + } + }, + + /** Function: log + * User overrideable logging function. + * + * This function is called whenever the Strophe library calls any + * of the logging functions. The default implementation of this + * function logs only fatal errors. If client code wishes to handle the logging + * messages, it should override this with + * > Strophe.log = function (level, msg) { + * > (user code here) + * > }; + * + * Please note that data sent and received over the wire is logged + * via Strophe.Connection.rawInput() and Strophe.Connection.rawOutput(). + * + * The different levels and their meanings are + * + * DEBUG - Messages useful for debugging purposes. + * INFO - Informational messages. This is mostly information like + * 'disconnect was called' or 'SASL auth succeeded'. + * WARN - Warnings about potential problems. This is mostly used + * to report transient connection errors like request timeouts. + * ERROR - Some error occurred. + * FATAL - A non-recoverable fatal error occurred. + * + * Parameters: + * (Integer) level - The log level of the log message. This will + * be one of the values in Strophe.LogLevel. + * (String) msg - The log message. + */ + log: function log(level, msg) { + if (level === this.LogLevel.FATAL && _typeof(window.console) === 'object' && typeof window.console.error === 'function') { + window.console.error(msg); + } + }, + + /** Function: debug + * Log a message at the Strophe.LogLevel.DEBUG level. + * + * Parameters: + * (String) msg - The log message. + */ + debug: function debug(msg) { + this.log(this.LogLevel.DEBUG, msg); + }, + + /** Function: info + * Log a message at the Strophe.LogLevel.INFO level. + * + * Parameters: + * (String) msg - The log message. + */ + info: function info(msg) { + this.log(this.LogLevel.INFO, msg); + }, + + /** Function: warn + * Log a message at the Strophe.LogLevel.WARN level. + * + * Parameters: + * (String) msg - The log message. + */ + warn: function warn(msg) { + this.log(this.LogLevel.WARN, msg); + }, + + /** Function: error + * Log a message at the Strophe.LogLevel.ERROR level. + * + * Parameters: + * (String) msg - The log message. + */ + error: function error(msg) { + this.log(this.LogLevel.ERROR, msg); + }, + + /** Function: fatal + * Log a message at the Strophe.LogLevel.FATAL level. + * + * Parameters: + * (String) msg - The log message. + */ + fatal: function fatal(msg) { + this.log(this.LogLevel.FATAL, msg); + }, + + /** Function: serialize + * Render a DOM element and all descendants to a String. + * + * Parameters: + * (XMLElement) elem - A DOM element. + * + * Returns: + * The serialized element tree as a String. + */ + serialize: function serialize(elem) { + if (!elem) { + return null; + } + + if (typeof elem.tree === "function") { + elem = elem.tree(); + } + + var names = _toConsumableArray(Array(elem.attributes.length).keys()).map(function (i) { + return elem.attributes[i].nodeName; + }); + + names.sort(); + var result = names.reduce(function (a, n) { + return "".concat(a, " ").concat(n, "=\"").concat(Strophe.xmlescape(elem.attributes.getNamedItem(n).value), "\""); + }, "<".concat(elem.nodeName)); + + if (elem.childNodes.length > 0) { + result += ">"; + + for (var i = 0; i < elem.childNodes.length; i++) { + var child = elem.childNodes[i]; + + switch (child.nodeType) { + case Strophe.ElementType.NORMAL: + // normal element, so recurse + result += Strophe.serialize(child); + break; + + case Strophe.ElementType.TEXT: + // text element to escape values + result += Strophe.xmlescape(child.nodeValue); + break; + + case Strophe.ElementType.CDATA: + // cdata section so don't escape values + result += ""; + } + } + + result += ""; + } else { + result += "/>"; + } + + return result; + }, + + /** PrivateVariable: _requestId + * _Private_ variable that keeps track of the request ids for + * connections. + */ + _requestId: 0, + + /** PrivateVariable: Strophe.connectionPlugins + * _Private_ variable Used to store plugin names that need + * initialization on Strophe.Connection construction. + */ + _connectionPlugins: {}, + + /** Function: addConnectionPlugin + * Extends the Strophe.Connection object with the given plugin. + * + * Parameters: + * (String) name - The name of the extension. + * (Object) ptype - The plugin's prototype. + */ + addConnectionPlugin: function addConnectionPlugin(name, ptype) { + Strophe._connectionPlugins[name] = ptype; + } +}; +/** Class: Strophe.Builder + * XML DOM builder. + * + * This object provides an interface similar to JQuery but for building + * DOM elements easily and rapidly. All the functions except for toString() + * and tree() return the object, so calls can be chained. Here's an + * example using the $iq() builder helper. + * > $iq({to: 'you', from: 'me', type: 'get', id: '1'}) + * > .c('query', {xmlns: 'strophe:example'}) + * > .c('example') + * > .toString() + * + * The above generates this XML fragment + * > + * > + * > + * > + * > + * The corresponding DOM manipulations to get a similar fragment would be + * a lot more tedious and probably involve several helper variables. + * + * Since adding children makes new operations operate on the child, up() + * is provided to traverse up the tree. To add two children, do + * > builder.c('child1', ...).up().c('child2', ...) + * The next operation on the Builder will be relative to the second child. + */ + +/** Constructor: Strophe.Builder + * Create a Strophe.Builder object. + * + * The attributes should be passed in object notation. For example + * > let b = new Builder('message', {to: 'you', from: 'me'}); + * or + * > let b = new Builder('messsage', {'xml:lang': 'en'}); + * + * Parameters: + * (String) name - The name of the root element. + * (Object) attrs - The attributes for the root element in object notation. + * + * Returns: + * A new Strophe.Builder. + */ + +Strophe.Builder = function (name, attrs) { + // Set correct namespace for jabber:client elements + if (name === "presence" || name === "message" || name === "iq") { + if (attrs && !attrs.xmlns) { + attrs.xmlns = Strophe.NS.CLIENT; + } else if (!attrs) { + attrs = { + xmlns: Strophe.NS.CLIENT + }; + } + } // Holds the tree being built. + + + this.nodeTree = Strophe.xmlElement(name, attrs); // Points to the current operation node. + + this.node = this.nodeTree; +}; + +Strophe.Builder.prototype = { + /** Function: tree + * Return the DOM tree. + * + * This function returns the current DOM tree as an element object. This + * is suitable for passing to functions like Strophe.Connection.send(). + * + * Returns: + * The DOM tree as a element object. + */ + tree: function tree() { + return this.nodeTree; + }, + + /** Function: toString + * Serialize the DOM tree to a String. + * + * This function returns a string serialization of the current DOM + * tree. It is often used internally to pass data to a + * Strophe.Request object. + * + * Returns: + * The serialized DOM tree in a String. + */ + toString: function toString() { + return Strophe.serialize(this.nodeTree); + }, + + /** Function: up + * Make the current parent element the new current element. + * + * This function is often used after c() to traverse back up the tree. + * For example, to add two children to the same element + * > builder.c('child1', {}).up().c('child2', {}); + * + * Returns: + * The Stophe.Builder object. + */ + up: function up() { + this.node = this.node.parentNode; + return this; + }, + + /** Function: root + * Make the root element the new current element. + * + * When at a deeply nested element in the tree, this function can be used + * to jump back to the root of the tree, instead of having to repeatedly + * call up(). + * + * Returns: + * The Stophe.Builder object. + */ + root: function root() { + this.node = this.nodeTree; + return this; + }, + + /** Function: attrs + * Add or modify attributes of the current element. + * + * The attributes should be passed in object notation. This function + * does not move the current element pointer. + * + * Parameters: + * (Object) moreattrs - The attributes to add/modify in object notation. + * + * Returns: + * The Strophe.Builder object. + */ + attrs: function attrs(moreattrs) { + for (var k in moreattrs) { + if (Object.prototype.hasOwnProperty.call(moreattrs, k)) { + if (moreattrs[k] === undefined) { + this.node.removeAttribute(k); + } else { + this.node.setAttribute(k, moreattrs[k]); + } + } + } + + return this; + }, + + /** Function: c + * Add a child to the current element and make it the new current + * element. + * + * This function moves the current element pointer to the child, + * unless text is provided. If you need to add another child, it + * is necessary to use up() to go back to the parent in the tree. + * + * Parameters: + * (String) name - The name of the child. + * (Object) attrs - The attributes of the child in object notation. + * (String) text - The text to add to the child. + * + * Returns: + * The Strophe.Builder object. + */ + c: function c(name, attrs, text) { + var child = Strophe.xmlElement(name, attrs, text); + this.node.appendChild(child); + + if (typeof text !== "string" && typeof text !== "number") { + this.node = child; + } + + return this; + }, + + /** Function: cnode + * Add a child to the current element and make it the new current + * element. + * + * This function is the same as c() except that instead of using a + * name and an attributes object to create the child it uses an + * existing DOM element object. + * + * Parameters: + * (XMLElement) elem - A DOM element. + * + * Returns: + * The Strophe.Builder object. + */ + cnode: function cnode(elem) { + var impNode; + var xmlGen = Strophe.xmlGenerator(); + + try { + impNode = xmlGen.importNode !== undefined; + } catch (e) { + impNode = false; + } + + var newElem = impNode ? xmlGen.importNode(elem, true) : Strophe.copyElement(elem); + this.node.appendChild(newElem); + this.node = newElem; + return this; + }, + + /** Function: t + * Add a child text element. + * + * This *does not* make the child the new current element since there + * are no children of text elements. + * + * Parameters: + * (String) text - The text data to append to the current element. + * + * Returns: + * The Strophe.Builder object. + */ + t: function t(text) { + var child = Strophe.xmlTextNode(text); + this.node.appendChild(child); + return this; + }, + + /** Function: h + * Replace current element contents with the HTML passed in. + * + * This *does not* make the child the new current element + * + * Parameters: + * (String) html - The html to insert as contents of current element. + * + * Returns: + * The Strophe.Builder object. + */ + h: function h(html) { + var fragment = document.createElement('body'); // force the browser to try and fix any invalid HTML tags + + fragment.innerHTML = html; // copy cleaned html into an xml dom + + var xhtml = Strophe.createHtml(fragment); + + while (xhtml.childNodes.length > 0) { + this.node.appendChild(xhtml.childNodes[0]); + } + + return this; + } +}; +/** PrivateClass: Strophe.Handler + * _Private_ helper class for managing stanza handlers. + * + * A Strophe.Handler encapsulates a user provided callback function to be + * executed when matching stanzas are received by the connection. + * Handlers can be either one-off or persistant depending on their + * return value. Returning true will cause a Handler to remain active, and + * returning false will remove the Handler. + * + * Users will not use Strophe.Handler objects directly, but instead they + * will use Strophe.Connection.addHandler() and + * Strophe.Connection.deleteHandler(). + */ + +/** PrivateConstructor: Strophe.Handler + * Create and initialize a new Strophe.Handler. + * + * Parameters: + * (Function) handler - A function to be executed when the handler is run. + * (String) ns - The namespace to match. + * (String) name - The element name to match. + * (String) type - The element type to match. + * (String) id - The element id attribute to match. + * (String) from - The element from attribute to match. + * (Object) options - Handler options + * + * Returns: + * A new Strophe.Handler object. + */ + +Strophe.Handler = function (handler, ns, name, type, id, from, options) { + this.handler = handler; + this.ns = ns; + this.name = name; + this.type = type; + this.id = id; + this.options = options || { + 'matchBareFromJid': false, + 'ignoreNamespaceFragment': false + }; // BBB: Maintain backward compatibility with old `matchBare` option + + if (this.options.matchBare) { + Strophe.warn('The "matchBare" option is deprecated, use "matchBareFromJid" instead.'); + this.options.matchBareFromJid = this.options.matchBare; + delete this.options.matchBare; + } + + if (this.options.matchBareFromJid) { + this.from = from ? Strophe.getBareJidFromJid(from) : null; + } else { + this.from = from; + } // whether the handler is a user handler or a system handler + + + this.user = true; +}; + +Strophe.Handler.prototype = { + /** PrivateFunction: getNamespace + * Returns the XML namespace attribute on an element. + * If `ignoreNamespaceFragment` was passed in for this handler, then the + * URL fragment will be stripped. + * + * Parameters: + * (XMLElement) elem - The XML element with the namespace. + * + * Returns: + * The namespace, with optionally the fragment stripped. + */ + getNamespace: function getNamespace(elem) { + var elNamespace = elem.getAttribute("xmlns"); + + if (elNamespace && this.options.ignoreNamespaceFragment) { + elNamespace = elNamespace.split('#')[0]; + } + + return elNamespace; + }, + + /** PrivateFunction: namespaceMatch + * Tests if a stanza matches the namespace set for this Strophe.Handler. + * + * Parameters: + * (XMLElement) elem - The XML element to test. + * + * Returns: + * true if the stanza matches and false otherwise. + */ + namespaceMatch: function namespaceMatch(elem) { + var _this = this; + + var nsMatch = false; + + if (!this.ns) { + return true; + } else { + Strophe.forEachChild(elem, null, function (elem) { + if (_this.getNamespace(elem) === _this.ns) { + nsMatch = true; + } + }); + return nsMatch || this.getNamespace(elem) === this.ns; + } + }, + + /** PrivateFunction: isMatch + * Tests if a stanza matches the Strophe.Handler. + * + * Parameters: + * (XMLElement) elem - The XML element to test. + * + * Returns: + * true if the stanza matches and false otherwise. + */ + isMatch: function isMatch(elem) { + var from = elem.getAttribute('from'); + + if (this.options.matchBareFromJid) { + from = Strophe.getBareJidFromJid(from); + } + + var elem_type = elem.getAttribute("type"); + + if (this.namespaceMatch(elem) && (!this.name || Strophe.isTagEqual(elem, this.name)) && (!this.type || (Array.isArray(this.type) ? this.type.indexOf(elem_type) !== -1 : elem_type === this.type)) && (!this.id || elem.getAttribute("id") === this.id) && (!this.from || from === this.from)) { + return true; + } + + return false; + }, + + /** PrivateFunction: run + * Run the callback on a matching stanza. + * + * Parameters: + * (XMLElement) elem - The DOM element that triggered the + * Strophe.Handler. + * + * Returns: + * A boolean indicating if the handler should remain active. + */ + run: function run(elem) { + var result = null; + + try { + result = this.handler(elem); + } catch (e) { + Strophe._handleError(e); + + throw e; + } + + return result; + }, + + /** PrivateFunction: toString + * Get a String representation of the Strophe.Handler object. + * + * Returns: + * A String. + */ + toString: function toString() { + return "{Handler: " + this.handler + "(" + this.name + "," + this.id + "," + this.ns + ")}"; + } +}; +/** PrivateClass: Strophe.TimedHandler + * _Private_ helper class for managing timed handlers. + * + * A Strophe.TimedHandler encapsulates a user provided callback that + * should be called after a certain period of time or at regular + * intervals. The return value of the callback determines whether the + * Strophe.TimedHandler will continue to fire. + * + * Users will not use Strophe.TimedHandler objects directly, but instead + * they will use Strophe.Connection.addTimedHandler() and + * Strophe.Connection.deleteTimedHandler(). + */ + +/** PrivateConstructor: Strophe.TimedHandler + * Create and initialize a new Strophe.TimedHandler object. + * + * Parameters: + * (Integer) period - The number of milliseconds to wait before the + * handler is called. + * (Function) handler - The callback to run when the handler fires. This + * function should take no arguments. + * + * Returns: + * A new Strophe.TimedHandler object. + */ + +Strophe.TimedHandler = function (period, handler) { + this.period = period; + this.handler = handler; + this.lastCalled = new Date().getTime(); + this.user = true; +}; + +Strophe.TimedHandler.prototype = { + /** PrivateFunction: run + * Run the callback for the Strophe.TimedHandler. + * + * Returns: + * true if the Strophe.TimedHandler should be called again, and false + * otherwise. + */ + run: function run() { + this.lastCalled = new Date().getTime(); + return this.handler(); + }, + + /** PrivateFunction: reset + * Reset the last called time for the Strophe.TimedHandler. + */ + reset: function reset() { + this.lastCalled = new Date().getTime(); + }, + + /** PrivateFunction: toString + * Get a string representation of the Strophe.TimedHandler object. + * + * Returns: + * The string representation. + */ + toString: function toString() { + return "{TimedHandler: " + this.handler + "(" + this.period + ")}"; + } +}; +/** Class: Strophe.Connection + * XMPP Connection manager. + * + * This class is the main part of Strophe. It manages a BOSH or websocket + * connection to an XMPP server and dispatches events to the user callbacks + * as data arrives. It supports SASL PLAIN, SASL DIGEST-MD5, SASL SCRAM-SHA1 + * and legacy authentication. + * + * After creating a Strophe.Connection object, the user will typically + * call connect() with a user supplied callback to handle connection level + * events like authentication failure, disconnection, or connection + * complete. + * + * The user will also have several event handlers defined by using + * addHandler() and addTimedHandler(). These will allow the user code to + * respond to interesting stanzas or do something periodically with the + * connection. These handlers will be active once authentication is + * finished. + * + * To send data to the connection, use send(). + */ + +/** Constructor: Strophe.Connection + * Create and initialize a Strophe.Connection object. + * + * The transport-protocol for this connection will be chosen automatically + * based on the given service parameter. URLs starting with "ws://" or + * "wss://" will use WebSockets, URLs starting with "http://", "https://" + * or without a protocol will use BOSH. + * + * To make Strophe connect to the current host you can leave out the protocol + * and host part and just pass the path, e.g. + * + * > let conn = new Strophe.Connection("/http-bind/"); + * + * Options common to both Websocket and BOSH: + * ------------------------------------------ + * + * cookies: + * + * The *cookies* option allows you to pass in cookies to be added to the + * document. These cookies will then be included in the BOSH XMLHttpRequest + * or in the websocket connection. + * + * The passed in value must be a map of cookie names and string values. + * + * > { "myCookie": { + * > "value": "1234", + * > "domain": ".example.org", + * > "path": "/", + * > "expires": expirationDate + * > } + * > } + * + * Note that cookies can't be set in this way for other domains (i.e. cross-domain). + * Those cookies need to be set under those domains, for example they can be + * set server-side by making a XHR call to that domain to ask it to set any + * necessary cookies. + * + * mechanisms: + * + * The *mechanisms* option allows you to specify the SASL mechanisms that this + * instance of Strophe.Connection (and therefore your XMPP client) will + * support. + * + * The value must be an array of objects with Strophe.SASLMechanism + * prototypes. + * + * If nothing is specified, then the following mechanisms (and their + * priorities) are registered: + * + * SCRAM-SHA1 - 70 + * DIGEST-MD5 - 60 + * PLAIN - 50 + * OAUTH-BEARER - 40 + * OAUTH-2 - 30 + * ANONYMOUS - 20 + * EXTERNAL - 10 + * + * WebSocket options: + * ------------------ + * + * If you want to connect to the current host with a WebSocket connection you + * can tell Strophe to use WebSockets through a "protocol" attribute in the + * optional options parameter. Valid values are "ws" for WebSocket and "wss" + * for Secure WebSocket. + * So to connect to "wss://CURRENT_HOSTNAME/xmpp-websocket" you would call + * + * > let conn = new Strophe.Connection("/xmpp-websocket/", {protocol: "wss"}); + * + * Note that relative URLs _NOT_ starting with a "/" will also include the path + * of the current site. + * + * Also because downgrading security is not permitted by browsers, when using + * relative URLs both BOSH and WebSocket connections will use their secure + * variants if the current connection to the site is also secure (https). + * + * BOSH options: + * ------------- + * + * By adding "sync" to the options, you can control if requests will + * be made synchronously or not. The default behaviour is asynchronous. + * If you want to make requests synchronous, make "sync" evaluate to true. + * > let conn = new Strophe.Connection("/http-bind/", {sync: true}); + * + * You can also toggle this on an already established connection. + * > conn.options.sync = true; + * + * The *customHeaders* option can be used to provide custom HTTP headers to be + * included in the XMLHttpRequests made. + * + * The *keepalive* option can be used to instruct Strophe to maintain the + * current BOSH session across interruptions such as webpage reloads. + * + * It will do this by caching the sessions tokens in sessionStorage, and when + * "restore" is called it will check whether there are cached tokens with + * which it can resume an existing session. + * + * The *withCredentials* option should receive a Boolean value and is used to + * indicate wether cookies should be included in ajax requests (by default + * they're not). + * Set this value to true if you are connecting to a BOSH service + * and for some reason need to send cookies to it. + * In order for this to work cross-domain, the server must also enable + * credentials by setting the Access-Control-Allow-Credentials response header + * to "true". For most usecases however this setting should be false (which + * is the default). + * Additionally, when using Access-Control-Allow-Credentials, the + * Access-Control-Allow-Origin header can't be set to the wildcard "*", but + * instead must be restricted to actual domains. + * + * The *contentType* option can be set to change the default Content-Type + * of "text/xml; charset=utf-8", which can be useful to reduce the amount of + * CORS preflight requests that are sent to the server. + * + * Parameters: + * (String) service - The BOSH or WebSocket service URL. + * (Object) options - A hash of configuration options + * + * Returns: + * A new Strophe.Connection object. + */ + +Strophe.Connection = function (service, options) { + var _this2 = this; + + // The service URL + this.service = service; // Configuration options + + this.options = options || {}; + var proto = this.options.protocol || ""; // Select protocal based on service or options + + if (service.indexOf("ws:") === 0 || service.indexOf("wss:") === 0 || proto.indexOf("ws") === 0) { + this._proto = new Strophe.Websocket(this); + } else { + this._proto = new Strophe.Bosh(this); + } + /* The connected JID. */ + + + this.jid = ""; + /* the JIDs domain */ + + this.domain = null; + /* stream:features */ + + this.features = null; // SASL + + this._sasl_data = {}; + this.do_session = false; + this.do_bind = false; // handler lists + + this.timedHandlers = []; + this.handlers = []; + this.removeTimeds = []; + this.removeHandlers = []; + this.addTimeds = []; + this.addHandlers = []; + this.protocolErrorHandlers = { + 'HTTP': {}, + 'websocket': {} + }; + this._idleTimeout = null; + this._disconnectTimeout = null; + this.authenticated = false; + this.connected = false; + this.disconnecting = false; + this.do_authentication = true; + this.paused = false; + this.restored = false; + this._data = []; + this._uniqueId = 0; + this._sasl_success_handler = null; + this._sasl_failure_handler = null; + this._sasl_challenge_handler = null; // Max retries before disconnecting + + this.maxRetries = 5; // Call onIdle callback every 1/10th of a second + + this._idleTimeout = setTimeout(function () { + return _this2._onIdle(); + }, 100); + utils__WEBPACK_IMPORTED_MODULE_2__["default"].addCookies(this.options.cookies); + this.registerSASLMechanisms(this.options.mechanisms); // initialize plugins + + for (var k in Strophe._connectionPlugins) { + if (Object.prototype.hasOwnProperty.call(Strophe._connectionPlugins, k)) { + var F = function F() {}; + + F.prototype = Strophe._connectionPlugins[k]; + this[k] = new F(); + this[k].init(this); + } + } +}; + +Strophe.Connection.prototype = { + /** Function: reset + * Reset the connection. + * + * This function should be called after a connection is disconnected + * before that connection is reused. + */ + reset: function reset() { + this._proto._reset(); // SASL + + + this.do_session = false; + this.do_bind = false; // handler lists + + this.timedHandlers = []; + this.handlers = []; + this.removeTimeds = []; + this.removeHandlers = []; + this.addTimeds = []; + this.addHandlers = []; + this.authenticated = false; + this.connected = false; + this.disconnecting = false; + this.restored = false; + this._data = []; + this._requests = []; + this._uniqueId = 0; + }, + + /** Function: pause + * Pause the request manager. + * + * This will prevent Strophe from sending any more requests to the + * server. This is very useful for temporarily pausing + * BOSH-Connections while a lot of send() calls are happening quickly. + * This causes Strophe to send the data in a single request, saving + * many request trips. + */ + pause: function pause() { + this.paused = true; + }, + + /** Function: resume + * Resume the request manager. + * + * This resumes after pause() has been called. + */ + resume: function resume() { + this.paused = false; + }, + + /** Function: getUniqueId + * Generate a unique ID for use in elements. + * + * All stanzas are required to have unique id attributes. This + * function makes creating these easy. Each connection instance has + * a counter which starts from zero, and the value of this counter + * plus a colon followed by the suffix becomes the unique id. If no + * suffix is supplied, the counter is used as the unique id. + * + * Suffixes are used to make debugging easier when reading the stream + * data, and their use is recommended. The counter resets to 0 for + * every new connection for the same reason. For connections to the + * same server that authenticate the same way, all the ids should be + * the same, which makes it easy to see changes. This is useful for + * automated testing as well. + * + * Parameters: + * (String) suffix - A optional suffix to append to the id. + * + * Returns: + * A unique string to be used for the id attribute. + */ + getUniqueId: function getUniqueId(suffix) { + var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + var r = Math.random() * 16 | 0, + v = c === 'x' ? r : r & 0x3 | 0x8; + return v.toString(16); + }); + + if (typeof suffix === "string" || typeof suffix === "number") { + return uuid + ":" + suffix; + } else { + return uuid + ""; + } + }, + + /** Function: addProtocolErrorHandler + * Register a handler function for when a protocol (websocker or HTTP) + * error occurs. + * + * NOTE: Currently only HTTP errors for BOSH requests are handled. + * Patches that handle websocket errors would be very welcome. + * + * Parameters: + * (String) protocol - 'HTTP' or 'websocket' + * (Integer) status_code - Error status code (e.g 500, 400 or 404) + * (Function) callback - Function that will fire on Http error + * + * Example: + * function onError(err_code){ + * //do stuff + * } + * + * let conn = Strophe.connect('http://example.com/http-bind'); + * conn.addProtocolErrorHandler('HTTP', 500, onError); + * // Triggers HTTP 500 error and onError handler will be called + * conn.connect('user_jid@incorrect_jabber_host', 'secret', onConnect); + */ + addProtocolErrorHandler: function addProtocolErrorHandler(protocol, status_code, callback) { + this.protocolErrorHandlers[protocol][status_code] = callback; + }, + + /** Function: connect + * Starts the connection process. + * + * As the connection process proceeds, the user supplied callback will + * be triggered multiple times with status updates. The callback + * should take two arguments - the status code and the error condition. + * + * The status code will be one of the values in the Strophe.Status + * constants. The error condition will be one of the conditions + * defined in RFC 3920 or the condition 'strophe-parsererror'. + * + * The Parameters _wait_, _hold_ and _route_ are optional and only relevant + * for BOSH connections. Please see XEP 124 for a more detailed explanation + * of the optional parameters. + * + * Parameters: + * (String) jid - The user's JID. This may be a bare JID, + * or a full JID. If a node is not supplied, SASL OAUTHBEARER or + * SASL ANONYMOUS authentication will be attempted (OAUTHBEARER will + * process the provided password value as an access token). + * (String) pass - The user's password. + * (Function) callback - The connect callback function. + * (Integer) wait - The optional HTTPBIND wait value. This is the + * time the server will wait before returning an empty result for + * a request. The default setting of 60 seconds is recommended. + * (Integer) hold - The optional HTTPBIND hold value. This is the + * number of connections the server will hold at one time. This + * should almost always be set to 1 (the default). + * (String) route - The optional route value. + * (String) authcid - The optional alternative authentication identity + * (username) if intending to impersonate another user. + * When using the SASL-EXTERNAL authentication mechanism, for example + * with client certificates, then the authcid value is used to + * determine whether an authorization JID (authzid) should be sent to + * the server. The authzid should not be sent to the server if the + * authzid and authcid are the same. So to prevent it from being sent + * (for example when the JID is already contained in the client + * certificate), set authcid to that same JID. See XEP-178 for more + * details. + */ + connect: function connect(jid, pass, callback, wait, hold, route, authcid) { + this.jid = jid; + /** Variable: authzid + * Authorization identity. + */ + + this.authzid = Strophe.getBareJidFromJid(this.jid); + /** Variable: authcid + * Authentication identity (User name). + */ + + this.authcid = authcid || Strophe.getNodeFromJid(this.jid); + /** Variable: pass + * Authentication identity (User password). + */ + + this.pass = pass; + /** Variable: servtype + * Digest MD5 compatibility. + */ + + this.servtype = "xmpp"; + this.connect_callback = callback; + this.disconnecting = false; + this.connected = false; + this.authenticated = false; + this.restored = false; // parse jid for domain + + this.domain = Strophe.getDomainFromJid(this.jid); + + this._changeConnectStatus(Strophe.Status.CONNECTING, null); + + this._proto._connect(wait, hold, route); + }, + + /** Function: attach + * Attach to an already created and authenticated BOSH session. + * + * This function is provided to allow Strophe to attach to BOSH + * sessions which have been created externally, perhaps by a Web + * application. This is often used to support auto-login type features + * without putting user credentials into the page. + * + * Parameters: + * (String) jid - The full JID that is bound by the session. + * (String) sid - The SID of the BOSH session. + * (String) rid - The current RID of the BOSH session. This RID + * will be used by the next request. + * (Function) callback The connect callback function. + * (Integer) wait - The optional HTTPBIND wait value. This is the + * time the server will wait before returning an empty result for + * a request. The default setting of 60 seconds is recommended. + * Other settings will require tweaks to the Strophe.TIMEOUT value. + * (Integer) hold - The optional HTTPBIND hold value. This is the + * number of connections the server will hold at one time. This + * should almost always be set to 1 (the default). + * (Integer) wind - The optional HTTBIND window value. This is the + * allowed range of request ids that are valid. The default is 5. + */ + attach: function attach(jid, sid, rid, callback, wait, hold, wind) { + if (this._proto instanceof Strophe.Bosh) { + this._proto._attach(jid, sid, rid, callback, wait, hold, wind); + } else { + var error = new Error('The "attach" method can only be used with a BOSH connection.'); + error.name = 'StropheSessionError'; + throw error; + } + }, + + /** Function: restore + * Attempt to restore a cached BOSH session. + * + * This function is only useful in conjunction with providing the + * "keepalive":true option when instantiating a new Strophe.Connection. + * + * When "keepalive" is set to true, Strophe will cache the BOSH tokens + * RID (Request ID) and SID (Session ID) and then when this function is + * called, it will attempt to restore the session from those cached + * tokens. + * + * This function must therefore be called instead of connect or attach. + * + * For an example on how to use it, please see examples/restore.js + * + * Parameters: + * (String) jid - The user's JID. This may be a bare JID or a full JID. + * (Function) callback - The connect callback function. + * (Integer) wait - The optional HTTPBIND wait value. This is the + * time the server will wait before returning an empty result for + * a request. The default setting of 60 seconds is recommended. + * (Integer) hold - The optional HTTPBIND hold value. This is the + * number of connections the server will hold at one time. This + * should almost always be set to 1 (the default). + * (Integer) wind - The optional HTTBIND window value. This is the + * allowed range of request ids that are valid. The default is 5. + */ + restore: function restore(jid, callback, wait, hold, wind) { + if (this._sessionCachingSupported()) { + this._proto._restore(jid, callback, wait, hold, wind); + } else { + var error = new Error('The "restore" method can only be used with a BOSH connection.'); + error.name = 'StropheSessionError'; + throw error; + } + }, + + /** PrivateFunction: _sessionCachingSupported + * Checks whether sessionStorage and JSON are supported and whether we're + * using BOSH. + */ + _sessionCachingSupported: function _sessionCachingSupported() { + if (this._proto instanceof Strophe.Bosh) { + if (!JSON) { + return false; + } + + try { + sessionStorage.setItem('_strophe_', '_strophe_'); + sessionStorage.removeItem('_strophe_'); + } catch (e) { + return false; + } + + return true; + } + + return false; + }, + + /** Function: xmlInput + * User overrideable function that receives XML data coming into the + * connection. + * + * The default function does nothing. User code can override this with + * > Strophe.Connection.xmlInput = function (elem) { + * > (user code) + * > }; + * + * Due to limitations of current Browsers' XML-Parsers the opening and closing + * tag for WebSocket-Connoctions will be passed as selfclosing here. + * + * BOSH-Connections will have all stanzas wrapped in a tag. See + * if you want to strip this tag. + * + * Parameters: + * (XMLElement) elem - The XML data received by the connection. + */ + + /* jshint unused:false */ + xmlInput: function xmlInput(elem) { + return; + }, + + /* jshint unused:true */ + + /** Function: xmlOutput + * User overrideable function that receives XML data sent to the + * connection. + * + * The default function does nothing. User code can override this with + * > Strophe.Connection.xmlOutput = function (elem) { + * > (user code) + * > }; + * + * Due to limitations of current Browsers' XML-Parsers the opening and closing + * tag for WebSocket-Connoctions will be passed as selfclosing here. + * + * BOSH-Connections will have all stanzas wrapped in a tag. See + * if you want to strip this tag. + * + * Parameters: + * (XMLElement) elem - The XMLdata sent by the connection. + */ + + /* jshint unused:false */ + xmlOutput: function xmlOutput(elem) { + return; + }, + + /* jshint unused:true */ + + /** Function: rawInput + * User overrideable function that receives raw data coming into the + * connection. + * + * The default function does nothing. User code can override this with + * > Strophe.Connection.rawInput = function (data) { + * > (user code) + * > }; + * + * Parameters: + * (String) data - The data received by the connection. + */ + + /* jshint unused:false */ + rawInput: function rawInput(data) { + return; + }, + + /* jshint unused:true */ + + /** Function: rawOutput + * User overrideable function that receives raw data sent to the + * connection. + * + * The default function does nothing. User code can override this with + * > Strophe.Connection.rawOutput = function (data) { + * > (user code) + * > }; + * + * Parameters: + * (String) data - The data sent by the connection. + */ + + /* jshint unused:false */ + rawOutput: function rawOutput(data) { + return; + }, + + /* jshint unused:true */ + + /** Function: nextValidRid + * User overrideable function that receives the new valid rid. + * + * The default function does nothing. User code can override this with + * > Strophe.Connection.nextValidRid = function (rid) { + * > (user code) + * > }; + * + * Parameters: + * (Number) rid - The next valid rid + */ + + /* jshint unused:false */ + nextValidRid: function nextValidRid(rid) { + return; + }, + + /* jshint unused:true */ + + /** Function: send + * Send a stanza. + * + * This function is called to push data onto the send queue to + * go out over the wire. Whenever a request is sent to the BOSH + * server, all pending data is sent and the queue is flushed. + * + * Parameters: + * (XMLElement | + * [XMLElement] | + * Strophe.Builder) elem - The stanza to send. + */ + send: function send(elem) { + if (elem === null) { + return; + } + + if (typeof elem.sort === "function") { + for (var i = 0; i < elem.length; i++) { + this._queueData(elem[i]); + } + } else if (typeof elem.tree === "function") { + this._queueData(elem.tree()); + } else { + this._queueData(elem); + } + + this._proto._send(); + }, + + /** Function: flush + * Immediately send any pending outgoing data. + * + * Normally send() queues outgoing data until the next idle period + * (100ms), which optimizes network use in the common cases when + * several send()s are called in succession. flush() can be used to + * immediately send all pending data. + */ + flush: function flush() { + // cancel the pending idle period and run the idle function + // immediately + clearTimeout(this._idleTimeout); + + this._onIdle(); + }, + + /** Function: sendPresence + * Helper function to send presence stanzas. The main benefit is for + * sending presence stanzas for which you expect a responding presence + * stanza with the same id (for example when leaving a chat room). + * + * Parameters: + * (XMLElement) elem - The stanza to send. + * (Function) callback - The callback function for a successful request. + * (Function) errback - The callback function for a failed or timed + * out request. On timeout, the stanza will be null. + * (Integer) timeout - The time specified in milliseconds for a + * timeout to occur. + * + * Returns: + * The id used to send the presence. + */ + sendPresence: function sendPresence(elem, callback, errback, timeout) { + var _this3 = this; + + var timeoutHandler = null; + + if (typeof elem.tree === "function") { + elem = elem.tree(); + } + + var id = elem.getAttribute('id'); + + if (!id) { + // inject id if not found + id = this.getUniqueId("sendPresence"); + elem.setAttribute("id", id); + } + + if (typeof callback === "function" || typeof errback === "function") { + var handler = this.addHandler(function (stanza) { + // remove timeout handler if there is one + if (timeoutHandler) { + _this3.deleteTimedHandler(timeoutHandler); + } + + if (stanza.getAttribute('type') === 'error') { + if (errback) { + errback(stanza); + } + } else if (callback) { + callback(stanza); + } + }, null, 'presence', null, id); // if timeout specified, set up a timeout handler. + + if (timeout) { + timeoutHandler = this.addTimedHandler(timeout, function () { + // get rid of normal handler + _this3.deleteHandler(handler); // call errback on timeout with null stanza + + + if (errback) { + errback(null); + } + + return false; + }); + } + } + + this.send(elem); + return id; + }, + + /** Function: sendIQ + * Helper function to send IQ stanzas. + * + * Parameters: + * (XMLElement) elem - The stanza to send. + * (Function) callback - The callback function for a successful request. + * (Function) errback - The callback function for a failed or timed + * out request. On timeout, the stanza will be null. + * (Integer) timeout - The time specified in milliseconds for a + * timeout to occur. + * + * Returns: + * The id used to send the IQ. + */ + sendIQ: function sendIQ(elem, callback, errback, timeout) { + var _this4 = this; + + var timeoutHandler = null; + + if (typeof elem.tree === "function") { + elem = elem.tree(); + } + + var id = elem.getAttribute('id'); + + if (!id) { + // inject id if not found + id = this.getUniqueId("sendIQ"); + elem.setAttribute("id", id); + } + + if (typeof callback === "function" || typeof errback === "function") { + var handler = this.addHandler(function (stanza) { + // remove timeout handler if there is one + if (timeoutHandler) { + _this4.deleteTimedHandler(timeoutHandler); + } + + var iqtype = stanza.getAttribute('type'); + + if (iqtype === 'result') { + if (callback) { + callback(stanza); + } + } else if (iqtype === 'error') { + if (errback) { + errback(stanza); + } + } else { + var error = new Error("Got bad IQ type of ".concat(iqtype)); + error.name = "StropheError"; + throw error; + } + }, null, 'iq', ['error', 'result'], id); // if timeout specified, set up a timeout handler. + + if (timeout) { + timeoutHandler = this.addTimedHandler(timeout, function () { + // get rid of normal handler + _this4.deleteHandler(handler); // call errback on timeout with null stanza + + + if (errback) { + errback(null); + } + + return false; + }); + } + } + + this.send(elem); + return id; + }, + + /** PrivateFunction: _queueData + * Queue outgoing data for later sending. Also ensures that the data + * is a DOMElement. + */ + _queueData: function _queueData(element) { + if (element === null || !element.tagName || !element.childNodes) { + var error = new Error("Cannot queue non-DOMElement."); + error.name = "StropheError"; + throw error; + } + + this._data.push(element); + }, + + /** PrivateFunction: _sendRestart + * Send an xmpp:restart stanza. + */ + _sendRestart: function _sendRestart() { + var _this5 = this; + + this._data.push("restart"); + + this._proto._sendRestart(); + + this._idleTimeout = setTimeout(function () { + return _this5._onIdle(); + }, 100); + }, + + /** Function: addTimedHandler + * Add a timed handler to the connection. + * + * This function adds a timed handler. The provided handler will + * be called every period milliseconds until it returns false, + * the connection is terminated, or the handler is removed. Handlers + * that wish to continue being invoked should return true. + * + * Because of method binding it is necessary to save the result of + * this function if you wish to remove a handler with + * deleteTimedHandler(). + * + * Note that user handlers are not active until authentication is + * successful. + * + * Parameters: + * (Integer) period - The period of the handler. + * (Function) handler - The callback function. + * + * Returns: + * A reference to the handler that can be used to remove it. + */ + addTimedHandler: function addTimedHandler(period, handler) { + var thand = new Strophe.TimedHandler(period, handler); + this.addTimeds.push(thand); + return thand; + }, + + /** Function: deleteTimedHandler + * Delete a timed handler for a connection. + * + * This function removes a timed handler from the connection. The + * handRef parameter is *not* the function passed to addTimedHandler(), + * but is the reference returned from addTimedHandler(). + * + * Parameters: + * (Strophe.TimedHandler) handRef - The handler reference. + */ + deleteTimedHandler: function deleteTimedHandler(handRef) { + // this must be done in the Idle loop so that we don't change + // the handlers during iteration + this.removeTimeds.push(handRef); + }, + + /** Function: addHandler + * Add a stanza handler for the connection. + * + * This function adds a stanza handler to the connection. The + * handler callback will be called for any stanza that matches + * the parameters. Note that if multiple parameters are supplied, + * they must all match for the handler to be invoked. + * + * The handler will receive the stanza that triggered it as its argument. + * *The handler should return true if it is to be invoked again; + * returning false will remove the handler after it returns.* + * + * As a convenience, the ns parameters applies to the top level element + * and also any of its immediate children. This is primarily to make + * matching /iq/query elements easy. + * + * Options + * ~~~~~~~ + * With the options argument, you can specify boolean flags that affect how + * matches are being done. + * + * Currently two flags exist: + * + * - matchBareFromJid: + * When set to true, the from parameter and the + * from attribute on the stanza will be matched as bare JIDs instead + * of full JIDs. To use this, pass {matchBareFromJid: true} as the + * value of options. The default value for matchBareFromJid is false. + * + * - ignoreNamespaceFragment: + * When set to true, a fragment specified on the stanza's namespace + * URL will be ignored when it's matched with the one configured for + * the handler. + * + * This means that if you register like this: + * > connection.addHandler( + * > handler, + * > 'http://jabber.org/protocol/muc', + * > null, null, null, null, + * > {'ignoreNamespaceFragment': true} + * > ); + * + * Then a stanza with XML namespace of + * 'http://jabber.org/protocol/muc#user' will also be matched. If + * 'ignoreNamespaceFragment' is false, then only stanzas with + * 'http://jabber.org/protocol/muc' will be matched. + * + * Deleting the handler + * ~~~~~~~~~~~~~~~~~~~~ + * The return value should be saved if you wish to remove the handler + * with deleteHandler(). + * + * Parameters: + * (Function) handler - The user callback. + * (String) ns - The namespace to match. + * (String) name - The stanza name to match. + * (String|Array) type - The stanza type (or types if an array) to match. + * (String) id - The stanza id attribute to match. + * (String) from - The stanza from attribute to match. + * (String) options - The handler options + * + * Returns: + * A reference to the handler that can be used to remove it. + */ + addHandler: function addHandler(handler, ns, name, type, id, from, options) { + var hand = new Strophe.Handler(handler, ns, name, type, id, from, options); + this.addHandlers.push(hand); + return hand; + }, + + /** Function: deleteHandler + * Delete a stanza handler for a connection. + * + * This function removes a stanza handler from the connection. The + * handRef parameter is *not* the function passed to addHandler(), + * but is the reference returned from addHandler(). + * + * Parameters: + * (Strophe.Handler) handRef - The handler reference. + */ + deleteHandler: function deleteHandler(handRef) { + // this must be done in the Idle loop so that we don't change + // the handlers during iteration + this.removeHandlers.push(handRef); // If a handler is being deleted while it is being added, + // prevent it from getting added + + var i = this.addHandlers.indexOf(handRef); + + if (i >= 0) { + this.addHandlers.splice(i, 1); + } + }, + + /** Function: registerSASLMechanisms + * + * Register the SASL mechanisms which will be supported by this instance of + * Strophe.Connection (i.e. which this XMPP client will support). + * + * Parameters: + * (Array) mechanisms - Array of objects with Strophe.SASLMechanism prototypes + * + */ + registerSASLMechanisms: function registerSASLMechanisms(mechanisms) { + this.mechanisms = {}; + mechanisms = mechanisms || [Strophe.SASLAnonymous, Strophe.SASLExternal, Strophe.SASLMD5, Strophe.SASLOAuthBearer, Strophe.SASLXOAuth2, Strophe.SASLPlain, Strophe.SASLSHA1]; + mechanisms.forEach(this.registerSASLMechanism.bind(this)); + }, + + /** Function: registerSASLMechanism + * + * Register a single SASL mechanism, to be supported by this client. + * + * Parameters: + * (Object) mechanism - Object with a Strophe.SASLMechanism prototype + * + */ + registerSASLMechanism: function registerSASLMechanism(mechanism) { + this.mechanisms[mechanism.prototype.name] = mechanism; + }, + + /** Function: disconnect + * Start the graceful disconnection process. + * + * This function starts the disconnection process. This process starts + * by sending unavailable presence and sending BOSH body of type + * terminate. A timeout handler makes sure that disconnection happens + * even if the BOSH server does not respond. + * If the Connection object isn't connected, at least tries to abort all pending requests + * so the connection object won't generate successful requests (which were already opened). + * + * The user supplied connection callback will be notified of the + * progress as this process happens. + * + * Parameters: + * (String) reason - The reason the disconnect is occuring. + */ + disconnect: function disconnect(reason) { + this._changeConnectStatus(Strophe.Status.DISCONNECTING, reason); + + Strophe.info("Disconnect was called because: " + reason); + + if (this.connected) { + var pres = false; + this.disconnecting = true; + + if (this.authenticated) { + pres = $pres({ + 'xmlns': Strophe.NS.CLIENT, + 'type': 'unavailable' + }); + } // setup timeout handler + + + this._disconnectTimeout = this._addSysTimedHandler(3000, this._onDisconnectTimeout.bind(this)); + + this._proto._disconnect(pres); + } else { + Strophe.info("Disconnect was called before Strophe connected to the server"); + + this._proto._abortAllRequests(); + + this._doDisconnect(); + } + }, + + /** PrivateFunction: _changeConnectStatus + * _Private_ helper function that makes sure plugins and the user's + * callback are notified of connection status changes. + * + * Parameters: + * (Integer) status - the new connection status, one of the values + * in Strophe.Status + * (String) condition - the error condition or null + * (XMLElement) elem - The triggering stanza. + */ + _changeConnectStatus: function _changeConnectStatus(status, condition, elem) { + // notify all plugins listening for status changes + for (var k in Strophe._connectionPlugins) { + if (Object.prototype.hasOwnProperty.call(Strophe._connectionPlugins, k)) { + var plugin = this[k]; + + if (plugin.statusChanged) { + try { + plugin.statusChanged(status, condition); + } catch (err) { + Strophe.error("".concat(k, " plugin caused an exception changing status: ").concat(err)); + } + } + } + } // notify the user's callback + + + if (this.connect_callback) { + try { + this.connect_callback(status, condition, elem); + } catch (e) { + Strophe._handleError(e); + + Strophe.error("User connection callback caused an exception: ".concat(e)); + } + } + }, + + /** PrivateFunction: _doDisconnect + * _Private_ function to disconnect. + * + * This is the last piece of the disconnection logic. This resets the + * connection and alerts the user's connection callback. + */ + _doDisconnect: function _doDisconnect(condition) { + if (typeof this._idleTimeout === "number") { + clearTimeout(this._idleTimeout); + } // Cancel Disconnect Timeout + + + if (this._disconnectTimeout !== null) { + this.deleteTimedHandler(this._disconnectTimeout); + this._disconnectTimeout = null; + } + + Strophe.info("_doDisconnect was called"); + + this._proto._doDisconnect(); + + this.authenticated = false; + this.disconnecting = false; + this.restored = false; // delete handlers + + this.handlers = []; + this.timedHandlers = []; + this.removeTimeds = []; + this.removeHandlers = []; + this.addTimeds = []; + this.addHandlers = []; // tell the parent we disconnected + + this._changeConnectStatus(Strophe.Status.DISCONNECTED, condition); + + this.connected = false; + }, + + /** PrivateFunction: _dataRecv + * _Private_ handler to processes incoming data from the the connection. + * + * Except for _connect_cb handling the initial connection request, + * this function handles the incoming data for all requests. This + * function also fires stanza handlers that match each incoming + * stanza. + * + * Parameters: + * (Strophe.Request) req - The request that has data ready. + * (string) req - The stanza a raw string (optiona). + */ + _dataRecv: function _dataRecv(req, raw) { + var _this6 = this; + + Strophe.info("_dataRecv called"); + + var elem = this._proto._reqToData(req); + + if (elem === null) { + return; + } + + if (this.xmlInput !== Strophe.Connection.prototype.xmlInput) { + if (elem.nodeName === this._proto.strip && elem.childNodes.length) { + this.xmlInput(elem.childNodes[0]); + } else { + this.xmlInput(elem); + } + } + + if (this.rawInput !== Strophe.Connection.prototype.rawInput) { + if (raw) { + this.rawInput(raw); + } else { + this.rawInput(Strophe.serialize(elem)); + } + } // remove handlers scheduled for deletion + + + while (this.removeHandlers.length > 0) { + var hand = this.removeHandlers.pop(); + var i = this.handlers.indexOf(hand); + + if (i >= 0) { + this.handlers.splice(i, 1); + } + } // add handlers scheduled for addition + + + while (this.addHandlers.length > 0) { + this.handlers.push(this.addHandlers.pop()); + } // handle graceful disconnect + + + if (this.disconnecting && this._proto._emptyQueue()) { + this._doDisconnect(); + + return; + } + + var type = elem.getAttribute("type"); + + if (type !== null && type === "terminate") { + // Don't process stanzas that come in after disconnect + if (this.disconnecting) { + return; + } // an error occurred + + + var cond = elem.getAttribute("condition"); + var conflict = elem.getElementsByTagName("conflict"); + + if (cond !== null) { + if (cond === "remote-stream-error" && conflict.length > 0) { + cond = "conflict"; + } + + this._changeConnectStatus(Strophe.Status.CONNFAIL, cond); + } else { + this._changeConnectStatus(Strophe.Status.CONNFAIL, Strophe.ErrorCondition.UNKOWN_REASON); + } + + this._doDisconnect(cond); + + return; + } // send each incoming stanza through the handler chain + + + Strophe.forEachChild(elem, null, function (child) { + // process handlers + var newList = _this6.handlers; + _this6.handlers = []; + + for (var _i5 = 0; _i5 < newList.length; _i5++) { + var _hand = newList[_i5]; // encapsulate 'handler.run' not to lose the whole handler list if + // one of the handlers throws an exception + + try { + if (_hand.isMatch(child) && (_this6.authenticated || !_hand.user)) { + if (_hand.run(child)) { + _this6.handlers.push(_hand); + } + } else { + _this6.handlers.push(_hand); + } + } catch (e) { + // if the handler throws an exception, we consider it as false + Strophe.warn('Removing Strophe handlers due to uncaught exception: ' + e.message); + } + } + }); + }, + + /** Attribute: mechanisms + * SASL Mechanisms available for Connection. + */ + mechanisms: {}, + + /** PrivateFunction: _connect_cb + * _Private_ handler for initial connection request. + * + * This handler is used to process the initial connection request + * response from the BOSH server. It is used to set up authentication + * handlers and start the authentication process. + * + * SASL authentication will be attempted if available, otherwise + * the code will fall back to legacy authentication. + * + * Parameters: + * (Strophe.Request) req - The current request. + * (Function) _callback - low level (xmpp) connect callback function. + * Useful for plugins with their own xmpp connect callback (when they + * want to do something special). + */ + _connect_cb: function _connect_cb(req, _callback, raw) { + Strophe.info("_connect_cb was called"); + this.connected = true; + var bodyWrap; + + try { + bodyWrap = this._proto._reqToData(req); + } catch (e) { + if (e.name !== Strophe.ErrorCondition.BAD_FORMAT) { + throw e; + } + + this._changeConnectStatus(Strophe.Status.CONNFAIL, Strophe.ErrorCondition.BAD_FORMAT); + + this._doDisconnect(Strophe.ErrorCondition.BAD_FORMAT); + } + + if (!bodyWrap) { + return; + } + + if (this.xmlInput !== Strophe.Connection.prototype.xmlInput) { + if (bodyWrap.nodeName === this._proto.strip && bodyWrap.childNodes.length) { + this.xmlInput(bodyWrap.childNodes[0]); + } else { + this.xmlInput(bodyWrap); + } + } + + if (this.rawInput !== Strophe.Connection.prototype.rawInput) { + if (raw) { + this.rawInput(raw); + } else { + this.rawInput(Strophe.serialize(bodyWrap)); + } + } + + var conncheck = this._proto._connect_cb(bodyWrap); + + if (conncheck === Strophe.Status.CONNFAIL) { + return; + } // Check for the stream:features tag + + + var hasFeatures; + + if (bodyWrap.getElementsByTagNameNS) { + hasFeatures = bodyWrap.getElementsByTagNameNS(Strophe.NS.STREAM, "features").length > 0; + } else { + hasFeatures = bodyWrap.getElementsByTagName("stream:features").length > 0 || bodyWrap.getElementsByTagName("features").length > 0; + } + + if (!hasFeatures) { + this._proto._no_auth_received(_callback); + + return; + } + + var matched = []; + var mechanisms = bodyWrap.getElementsByTagName("mechanism"); + + if (mechanisms.length > 0) { + for (var i = 0; i < mechanisms.length; i++) { + var mech = Strophe.getText(mechanisms[i]); + if (this.mechanisms[mech]) matched.push(this.mechanisms[mech]); + } + } + + if (matched.length === 0) { + if (bodyWrap.getElementsByTagName("auth").length === 0) { + // There are no matching SASL mechanisms and also no legacy + // auth available. + this._proto._no_auth_received(_callback); + + return; + } + } + + if (this.do_authentication !== false) { + this.authenticate(matched); + } + }, + + /** Function: sortMechanismsByPriority + * + * Sorts an array of objects with prototype SASLMechanism according to + * their priorities. + * + * Parameters: + * (Array) mechanisms - Array of SASL mechanisms. + * + */ + sortMechanismsByPriority: function sortMechanismsByPriority(mechanisms) { + // Sorting mechanisms according to priority. + for (var i = 0; i < mechanisms.length - 1; ++i) { + var higher = i; + + for (var j = i + 1; j < mechanisms.length; ++j) { + if (mechanisms[j].prototype.priority > mechanisms[higher].prototype.priority) { + higher = j; + } + } + + if (higher !== i) { + var swap = mechanisms[i]; + mechanisms[i] = mechanisms[higher]; + mechanisms[higher] = swap; + } + } + + return mechanisms; + }, + + /** PrivateFunction: _attemptSASLAuth + * + * Iterate through an array of SASL mechanisms and attempt authentication + * with the highest priority (enabled) mechanism. + * + * Parameters: + * (Array) mechanisms - Array of SASL mechanisms. + * + * Returns: + * (Boolean) mechanism_found - true or false, depending on whether a + * valid SASL mechanism was found with which authentication could be + * started. + */ + _attemptSASLAuth: function _attemptSASLAuth(mechanisms) { + mechanisms = this.sortMechanismsByPriority(mechanisms || []); + var mechanism_found = false; + + for (var i = 0; i < mechanisms.length; ++i) { + if (!mechanisms[i].prototype.test(this)) { + continue; + } + + this._sasl_success_handler = this._addSysHandler(this._sasl_success_cb.bind(this), null, "success", null, null); + this._sasl_failure_handler = this._addSysHandler(this._sasl_failure_cb.bind(this), null, "failure", null, null); + this._sasl_challenge_handler = this._addSysHandler(this._sasl_challenge_cb.bind(this), null, "challenge", null, null); + this._sasl_mechanism = new mechanisms[i](); + + this._sasl_mechanism.onStart(this); + + var request_auth_exchange = $build("auth", { + 'xmlns': Strophe.NS.SASL, + 'mechanism': this._sasl_mechanism.name + }); + + if (this._sasl_mechanism.isClientFirst) { + var response = this._sasl_mechanism.onChallenge(this, null); + + request_auth_exchange.t(btoa(response)); + } + + this.send(request_auth_exchange.tree()); + mechanism_found = true; + break; + } + + return mechanism_found; + }, + + /** PrivateFunction: _attemptLegacyAuth + * + * Attempt legacy (i.e. non-SASL) authentication. + * + */ + _attemptLegacyAuth: function _attemptLegacyAuth() { + if (Strophe.getNodeFromJid(this.jid) === null) { + // we don't have a node, which is required for non-anonymous + // client connections + this._changeConnectStatus(Strophe.Status.CONNFAIL, Strophe.ErrorCondition.MISSING_JID_NODE); + + this.disconnect(Strophe.ErrorCondition.MISSING_JID_NODE); + } else { + // Fall back to legacy authentication + this._changeConnectStatus(Strophe.Status.AUTHENTICATING, null); + + this._addSysHandler(this._auth1_cb.bind(this), null, null, null, "_auth_1"); + + this.send($iq({ + 'type': "get", + 'to': this.domain, + 'id': "_auth_1" + }).c("query", { + xmlns: Strophe.NS.AUTH + }).c("username", {}).t(Strophe.getNodeFromJid(this.jid)).tree()); + } + }, + + /** Function: authenticate + * Set up authentication + * + * Continues the initial connection request by setting up authentication + * handlers and starting the authentication process. + * + * SASL authentication will be attempted if available, otherwise + * the code will fall back to legacy authentication. + * + * Parameters: + * (Array) matched - Array of SASL mechanisms supported. + * + */ + authenticate: function authenticate(matched) { + if (!this._attemptSASLAuth(matched)) { + this._attemptLegacyAuth(); + } + }, + + /** PrivateFunction: _sasl_challenge_cb + * _Private_ handler for the SASL challenge + * + */ + _sasl_challenge_cb: function _sasl_challenge_cb(elem) { + var challenge = atob(Strophe.getText(elem)); + + var response = this._sasl_mechanism.onChallenge(this, challenge); + + var stanza = $build('response', { + 'xmlns': Strophe.NS.SASL + }); + + if (response !== "") { + stanza.t(btoa(response)); + } + + this.send(stanza.tree()); + return true; + }, + + /** PrivateFunction: _auth1_cb + * _Private_ handler for legacy authentication. + * + * This handler is called in response to the initial + * for legacy authentication. It builds an authentication and + * sends it, creating a handler (calling back to _auth2_cb()) to + * handle the result + * + * Parameters: + * (XMLElement) elem - The stanza that triggered the callback. + * + * Returns: + * false to remove the handler. + */ + + /* jshint unused:false */ + _auth1_cb: function _auth1_cb(elem) { + // build plaintext auth iq + var iq = $iq({ + type: "set", + id: "_auth_2" + }).c('query', { + xmlns: Strophe.NS.AUTH + }).c('username', {}).t(Strophe.getNodeFromJid(this.jid)).up().c('password').t(this.pass); + + if (!Strophe.getResourceFromJid(this.jid)) { + // since the user has not supplied a resource, we pick + // a default one here. unlike other auth methods, the server + // cannot do this for us. + this.jid = Strophe.getBareJidFromJid(this.jid) + '/strophe'; + } + + iq.up().c('resource', {}).t(Strophe.getResourceFromJid(this.jid)); + + this._addSysHandler(this._auth2_cb.bind(this), null, null, null, "_auth_2"); + + this.send(iq.tree()); + return false; + }, + + /* jshint unused:true */ + + /** PrivateFunction: _sasl_success_cb + * _Private_ handler for succesful SASL authentication. + * + * Parameters: + * (XMLElement) elem - The matching stanza. + * + * Returns: + * false to remove the handler. + */ + _sasl_success_cb: function _sasl_success_cb(elem) { + var _this7 = this; + + if (this._sasl_data["server-signature"]) { + var serverSignature; + var success = atob(Strophe.getText(elem)); + var attribMatch = /([a-z]+)=([^,]+)(,|$)/; + var matches = success.match(attribMatch); + + if (matches[1] === "v") { + serverSignature = matches[2]; + } + + if (serverSignature !== this._sasl_data["server-signature"]) { + // remove old handlers + this.deleteHandler(this._sasl_failure_handler); + this._sasl_failure_handler = null; + + if (this._sasl_challenge_handler) { + this.deleteHandler(this._sasl_challenge_handler); + this._sasl_challenge_handler = null; + } + + this._sasl_data = {}; + return this._sasl_failure_cb(null); + } + } + + Strophe.info("SASL authentication succeeded."); + + if (this._sasl_mechanism) { + this._sasl_mechanism.onSuccess(); + } // remove old handlers + + + this.deleteHandler(this._sasl_failure_handler); + this._sasl_failure_handler = null; + + if (this._sasl_challenge_handler) { + this.deleteHandler(this._sasl_challenge_handler); + this._sasl_challenge_handler = null; + } + + var streamfeature_handlers = []; + + var wrapper = function wrapper(handlers, elem) { + while (handlers.length) { + _this7.deleteHandler(handlers.pop()); + } + + _this7._sasl_auth1_cb(elem); + + return false; + }; + + streamfeature_handlers.push(this._addSysHandler(function (elem) { + return wrapper(streamfeature_handlers, elem); + }, null, "stream:features", null, null)); + streamfeature_handlers.push(this._addSysHandler(function (elem) { + return wrapper(streamfeature_handlers, elem); + }, Strophe.NS.STREAM, "features", null, null)); // we must send an xmpp:restart now + + this._sendRestart(); + + return false; + }, + + /** PrivateFunction: _sasl_auth1_cb + * _Private_ handler to start stream binding. + * + * Parameters: + * (XMLElement) elem - The matching stanza. + * + * Returns: + * false to remove the handler. + */ + _sasl_auth1_cb: function _sasl_auth1_cb(elem) { + // save stream:features for future usage + this.features = elem; + + for (var i = 0; i < elem.childNodes.length; i++) { + var child = elem.childNodes[i]; + + if (child.nodeName === 'bind') { + this.do_bind = true; + } + + if (child.nodeName === 'session') { + this.do_session = true; + } + } + + if (!this.do_bind) { + this._changeConnectStatus(Strophe.Status.AUTHFAIL, null); + + return false; + } else { + this._addSysHandler(this._sasl_bind_cb.bind(this), null, null, null, "_bind_auth_2"); + + var resource = Strophe.getResourceFromJid(this.jid); + + if (resource) { + this.send($iq({ + type: "set", + id: "_bind_auth_2" + }).c('bind', { + xmlns: Strophe.NS.BIND + }).c('resource', {}).t(resource).tree()); + } else { + this.send($iq({ + type: "set", + id: "_bind_auth_2" + }).c('bind', { + xmlns: Strophe.NS.BIND + }).tree()); + } + } + + return false; + }, + + /** PrivateFunction: _sasl_bind_cb + * _Private_ handler for binding result and session start. + * + * Parameters: + * (XMLElement) elem - The matching stanza. + * + * Returns: + * false to remove the handler. + */ + _sasl_bind_cb: function _sasl_bind_cb(elem) { + if (elem.getAttribute("type") === "error") { + Strophe.info("SASL binding failed."); + var conflict = elem.getElementsByTagName("conflict"); + var condition; + + if (conflict.length > 0) { + condition = Strophe.ErrorCondition.CONFLICT; + } + + this._changeConnectStatus(Strophe.Status.AUTHFAIL, condition, elem); + + return false; + } // TODO - need to grab errors + + + var bind = elem.getElementsByTagName("bind"); + + if (bind.length > 0) { + var jidNode = bind[0].getElementsByTagName("jid"); + + if (jidNode.length > 0) { + this.jid = Strophe.getText(jidNode[0]); + + if (this.do_session) { + this._addSysHandler(this._sasl_session_cb.bind(this), null, null, null, "_session_auth_2"); + + this.send($iq({ + type: "set", + id: "_session_auth_2" + }).c('session', { + xmlns: Strophe.NS.SESSION + }).tree()); + } else { + this.authenticated = true; + + this._changeConnectStatus(Strophe.Status.CONNECTED, null); + } + } + } else { + Strophe.info("SASL binding failed."); + + this._changeConnectStatus(Strophe.Status.AUTHFAIL, null, elem); + + return false; + } + }, + + /** PrivateFunction: _sasl_session_cb + * _Private_ handler to finish successful SASL connection. + * + * This sets Connection.authenticated to true on success, which + * starts the processing of user handlers. + * + * Parameters: + * (XMLElement) elem - The matching stanza. + * + * Returns: + * false to remove the handler. + */ + _sasl_session_cb: function _sasl_session_cb(elem) { + if (elem.getAttribute("type") === "result") { + this.authenticated = true; + + this._changeConnectStatus(Strophe.Status.CONNECTED, null); + } else if (elem.getAttribute("type") === "error") { + Strophe.info("Session creation failed."); + + this._changeConnectStatus(Strophe.Status.AUTHFAIL, null, elem); + + return false; + } + + return false; + }, + + /** PrivateFunction: _sasl_failure_cb + * _Private_ handler for SASL authentication failure. + * + * Parameters: + * (XMLElement) elem - The matching stanza. + * + * Returns: + * false to remove the handler. + */ + + /* jshint unused:false */ + _sasl_failure_cb: function _sasl_failure_cb(elem) { + // delete unneeded handlers + if (this._sasl_success_handler) { + this.deleteHandler(this._sasl_success_handler); + this._sasl_success_handler = null; + } + + if (this._sasl_challenge_handler) { + this.deleteHandler(this._sasl_challenge_handler); + this._sasl_challenge_handler = null; + } + + if (this._sasl_mechanism) this._sasl_mechanism.onFailure(); + + this._changeConnectStatus(Strophe.Status.AUTHFAIL, null, elem); + + return false; + }, + + /* jshint unused:true */ + + /** PrivateFunction: _auth2_cb + * _Private_ handler to finish legacy authentication. + * + * This handler is called when the result from the jabber:iq:auth + * stanza is returned. + * + * Parameters: + * (XMLElement) elem - The stanza that triggered the callback. + * + * Returns: + * false to remove the handler. + */ + _auth2_cb: function _auth2_cb(elem) { + if (elem.getAttribute("type") === "result") { + this.authenticated = true; + + this._changeConnectStatus(Strophe.Status.CONNECTED, null); + } else if (elem.getAttribute("type") === "error") { + this._changeConnectStatus(Strophe.Status.AUTHFAIL, null, elem); + + this.disconnect('authentication failed'); + } + + return false; + }, + + /** PrivateFunction: _addSysTimedHandler + * _Private_ function to add a system level timed handler. + * + * This function is used to add a Strophe.TimedHandler for the + * library code. System timed handlers are allowed to run before + * authentication is complete. + * + * Parameters: + * (Integer) period - The period of the handler. + * (Function) handler - The callback function. + */ + _addSysTimedHandler: function _addSysTimedHandler(period, handler) { + var thand = new Strophe.TimedHandler(period, handler); + thand.user = false; + this.addTimeds.push(thand); + return thand; + }, + + /** PrivateFunction: _addSysHandler + * _Private_ function to add a system level stanza handler. + * + * This function is used to add a Strophe.Handler for the + * library code. System stanza handlers are allowed to run before + * authentication is complete. + * + * Parameters: + * (Function) handler - The callback function. + * (String) ns - The namespace to match. + * (String) name - The stanza name to match. + * (String) type - The stanza type attribute to match. + * (String) id - The stanza id attribute to match. + */ + _addSysHandler: function _addSysHandler(handler, ns, name, type, id) { + var hand = new Strophe.Handler(handler, ns, name, type, id); + hand.user = false; + this.addHandlers.push(hand); + return hand; + }, + + /** PrivateFunction: _onDisconnectTimeout + * _Private_ timeout handler for handling non-graceful disconnection. + * + * If the graceful disconnect process does not complete within the + * time allotted, this handler finishes the disconnect anyway. + * + * Returns: + * false to remove the handler. + */ + _onDisconnectTimeout: function _onDisconnectTimeout() { + Strophe.info("_onDisconnectTimeout was called"); + + this._changeConnectStatus(Strophe.Status.CONNTIMEOUT, null); + + this._proto._onDisconnectTimeout(); // actually disconnect + + + this._doDisconnect(); + + return false; + }, + + /** PrivateFunction: _onIdle + * _Private_ handler to process events during idle cycle. + * + * This handler is called every 100ms to fire timed handlers that + * are ready and keep poll requests going. + */ + _onIdle: function _onIdle() { + var _this8 = this; + + // add timed handlers scheduled for addition + // NOTE: we add before remove in the case a timed handler is + // added and then deleted before the next _onIdle() call. + while (this.addTimeds.length > 0) { + this.timedHandlers.push(this.addTimeds.pop()); + } // remove timed handlers that have been scheduled for deletion + + + while (this.removeTimeds.length > 0) { + var thand = this.removeTimeds.pop(); + var i = this.timedHandlers.indexOf(thand); + + if (i >= 0) { + this.timedHandlers.splice(i, 1); + } + } // call ready timed handlers + + + var now = new Date().getTime(); + var newList = []; + + for (var _i6 = 0; _i6 < this.timedHandlers.length; _i6++) { + var _thand = this.timedHandlers[_i6]; + + if (this.authenticated || !_thand.user) { + var since = _thand.lastCalled + _thand.period; + + if (since - now <= 0) { + if (_thand.run()) { + newList.push(_thand); + } + } else { + newList.push(_thand); + } + } + } + + this.timedHandlers = newList; + clearTimeout(this._idleTimeout); + + this._proto._onIdle(); // reactivate the timer only if connected + + + if (this.connected) { + this._idleTimeout = setTimeout(function () { + return _this8._onIdle(); + }, 100); + } + } +}; +/** Class: Strophe.SASLMechanism + * + * encapsulates SASL authentication mechanisms. + * + * User code may override the priority for each mechanism or disable it completely. + * See for information about changing priority and for informatian on + * how to disable a mechanism. + * + * By default, all mechanisms are enabled and the priorities are + * + * OAUTHBEARER - 60 + * SCRAM-SHA1 - 50 + * DIGEST-MD5 - 40 + * PLAIN - 30 + * ANONYMOUS - 20 + * EXTERNAL - 10 + * + * See: Strophe.Connection.addSupportedSASLMechanisms + */ + +/** + * PrivateConstructor: Strophe.SASLMechanism + * SASL auth mechanism abstraction. + * + * Parameters: + * (String) name - SASL Mechanism name. + * (Boolean) isClientFirst - If client should send response first without challenge. + * (Number) priority - Priority. + * + * Returns: + * A new Strophe.SASLMechanism object. + */ + +Strophe.SASLMechanism = function (name, isClientFirst, priority) { + /** PrivateVariable: name + * Mechanism name. + */ + this.name = name; + /** PrivateVariable: isClientFirst + * If client sends response without initial server challenge. + */ + + this.isClientFirst = isClientFirst; + /** Variable: priority + * Determines which is chosen for authentication (Higher is better). + * Users may override this to prioritize mechanisms differently. + * + * In the default configuration the priorities are + * + * SCRAM-SHA1 - 40 + * DIGEST-MD5 - 30 + * Plain - 20 + * + * Example: (This will cause Strophe to choose the mechanism that the server sent first) + * + * > Strophe.SASLMD5.priority = Strophe.SASLSHA1.priority; + * + * See for a list of available mechanisms. + * + */ + + this.priority = priority; +}; + +Strophe.SASLMechanism.prototype = { + /** + * Function: test + * Checks if mechanism able to run. + * To disable a mechanism, make this return false; + * + * To disable plain authentication run + * > Strophe.SASLPlain.test = function() { + * > return false; + * > } + * + * See for a list of available mechanisms. + * + * Parameters: + * (Strophe.Connection) connection - Target Connection. + * + * Returns: + * (Boolean) If mechanism was able to run. + */ + + /* jshint unused:false */ + test: function test(connection) { + return true; + }, + + /* jshint unused:true */ + + /** PrivateFunction: onStart + * Called before starting mechanism on some connection. + * + * Parameters: + * (Strophe.Connection) connection - Target Connection. + */ + onStart: function onStart(connection) { + this._connection = connection; + }, + + /** PrivateFunction: onChallenge + * Called by protocol implementation on incoming challenge. If client is + * first (isClientFirst === true) challenge will be null on the first call. + * + * Parameters: + * (Strophe.Connection) connection - Target Connection. + * (String) challenge - current challenge to handle. + * + * Returns: + * (String) Mechanism response. + */ + + /* jshint unused:false */ + onChallenge: function onChallenge(connection, challenge) { + throw new Error("You should implement challenge handling!"); + }, + + /* jshint unused:true */ + + /** PrivateFunction: onFailure + * Protocol informs mechanism implementation about SASL failure. + */ + onFailure: function onFailure() { + this._connection = null; + }, + + /** PrivateFunction: onSuccess + * Protocol informs mechanism implementation about SASL success. + */ + onSuccess: function onSuccess() { + this._connection = null; + } +}; +/** Constants: SASL mechanisms + * Available authentication mechanisms + * + * Strophe.SASLAnonymous - SASL ANONYMOUS authentication. + * Strophe.SASLPlain - SASL PLAIN authentication. + * Strophe.SASLMD5 - SASL DIGEST-MD5 authentication + * Strophe.SASLSHA1 - SASL SCRAM-SHA1 authentication + * Strophe.SASLOAuthBearer - SASL OAuth Bearer authentication + * Strophe.SASLExternal - SASL EXTERNAL authentication + * Strophe.SASLXOAuth2 - SASL X-OAuth2 authentication + */ +// Building SASL callbacks + +/** PrivateConstructor: SASLAnonymous + * SASL ANONYMOUS authentication. + */ + +Strophe.SASLAnonymous = function () {}; + +Strophe.SASLAnonymous.prototype = new Strophe.SASLMechanism("ANONYMOUS", false, 20); + +Strophe.SASLAnonymous.prototype.test = function (connection) { + return connection.authcid === null; +}; +/** PrivateConstructor: SASLPlain + * SASL PLAIN authentication. + */ + + +Strophe.SASLPlain = function () {}; + +Strophe.SASLPlain.prototype = new Strophe.SASLMechanism("PLAIN", true, 50); + +Strophe.SASLPlain.prototype.test = function (connection) { + return connection.authcid !== null; +}; + +Strophe.SASLPlain.prototype.onChallenge = function (connection) { + var auth_str = connection.authzid; + auth_str = auth_str + "\0"; + auth_str = auth_str + connection.authcid; + auth_str = auth_str + "\0"; + auth_str = auth_str + connection.pass; + return utils__WEBPACK_IMPORTED_MODULE_2__["default"].utf16to8(auth_str); +}; +/** PrivateConstructor: SASLSHA1 + * SASL SCRAM SHA 1 authentication. + */ + + +Strophe.SASLSHA1 = function () {}; + +Strophe.SASLSHA1.prototype = new Strophe.SASLMechanism("SCRAM-SHA-1", true, 70); + +Strophe.SASLSHA1.prototype.test = function (connection) { + return connection.authcid !== null; +}; + +Strophe.SASLSHA1.prototype.onChallenge = function (connection, challenge, test_cnonce) { + var cnonce = test_cnonce || md5__WEBPACK_IMPORTED_MODULE_0__["default"].hexdigest(Math.random() * 1234567890); + var auth_str = "n=" + utils__WEBPACK_IMPORTED_MODULE_2__["default"].utf16to8(connection.authcid); + auth_str += ",r="; + auth_str += cnonce; + connection._sasl_data.cnonce = cnonce; + connection._sasl_data["client-first-message-bare"] = auth_str; + auth_str = "n,," + auth_str; + + this.onChallenge = function (connection, challenge) { + var nonce, salt, iter, Hi, U, U_old, i, k; + var responseText = "c=biws,"; + var authMessage = "".concat(connection._sasl_data["client-first-message-bare"], ",").concat(challenge, ","); + var cnonce = connection._sasl_data.cnonce; + var attribMatch = /([a-z]+)=([^,]+)(,|$)/; + + while (challenge.match(attribMatch)) { + var matches = challenge.match(attribMatch); + challenge = challenge.replace(matches[0], ""); + + switch (matches[1]) { + case "r": + nonce = matches[2]; + break; + + case "s": + salt = matches[2]; + break; + + case "i": + iter = matches[2]; + break; + } + } + + if (nonce.substr(0, cnonce.length) !== cnonce) { + connection._sasl_data = {}; + return connection._sasl_failure_cb(); + } + + responseText += "r=" + nonce; + authMessage += responseText; + salt = atob(salt); + salt += "\x00\x00\x00\x01"; + var pass = utils__WEBPACK_IMPORTED_MODULE_2__["default"].utf16to8(connection.pass); + Hi = U_old = sha1__WEBPACK_IMPORTED_MODULE_1__["default"].core_hmac_sha1(pass, salt); + + for (i = 1; i < iter; i++) { + U = sha1__WEBPACK_IMPORTED_MODULE_1__["default"].core_hmac_sha1(pass, sha1__WEBPACK_IMPORTED_MODULE_1__["default"].binb2str(U_old)); + + for (k = 0; k < 5; k++) { + Hi[k] ^= U[k]; + } + + U_old = U; + } + + Hi = sha1__WEBPACK_IMPORTED_MODULE_1__["default"].binb2str(Hi); + var clientKey = sha1__WEBPACK_IMPORTED_MODULE_1__["default"].core_hmac_sha1(Hi, "Client Key"); + var serverKey = sha1__WEBPACK_IMPORTED_MODULE_1__["default"].str_hmac_sha1(Hi, "Server Key"); + var clientSignature = sha1__WEBPACK_IMPORTED_MODULE_1__["default"].core_hmac_sha1(sha1__WEBPACK_IMPORTED_MODULE_1__["default"].str_sha1(sha1__WEBPACK_IMPORTED_MODULE_1__["default"].binb2str(clientKey)), authMessage); + connection._sasl_data["server-signature"] = sha1__WEBPACK_IMPORTED_MODULE_1__["default"].b64_hmac_sha1(serverKey, authMessage); + + for (k = 0; k < 5; k++) { + clientKey[k] ^= clientSignature[k]; + } + + responseText += ",p=" + btoa(sha1__WEBPACK_IMPORTED_MODULE_1__["default"].binb2str(clientKey)); + return responseText; + }; + + return auth_str; +}; +/** PrivateConstructor: SASLMD5 + * SASL DIGEST MD5 authentication. + */ + + +Strophe.SASLMD5 = function () {}; + +Strophe.SASLMD5.prototype = new Strophe.SASLMechanism("DIGEST-MD5", false, 60); + +Strophe.SASLMD5.prototype.test = function (connection) { + return connection.authcid !== null; +}; +/** PrivateFunction: _quote + * _Private_ utility function to backslash escape and quote strings. + * + * Parameters: + * (String) str - The string to be quoted. + * + * Returns: + * quoted string + */ + + +Strophe.SASLMD5.prototype._quote = function (str) { + return '"' + str.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"'; //" end string workaround for emacs +}; + +Strophe.SASLMD5.prototype.onChallenge = function (connection, challenge, test_cnonce) { + var attribMatch = /([a-z]+)=("[^"]+"|[^,"]+)(?:,|$)/; + var cnonce = test_cnonce || md5__WEBPACK_IMPORTED_MODULE_0__["default"].hexdigest("" + Math.random() * 1234567890); + var realm = ""; + var host = null; + var nonce = ""; + var qop = ""; + + while (challenge.match(attribMatch)) { + var matches = challenge.match(attribMatch); + challenge = challenge.replace(matches[0], ""); + matches[2] = matches[2].replace(/^"(.+)"$/, "$1"); + + switch (matches[1]) { + case "realm": + realm = matches[2]; + break; + + case "nonce": + nonce = matches[2]; + break; + + case "qop": + qop = matches[2]; + break; + + case "host": + host = matches[2]; + break; + } + } + + var digest_uri = connection.servtype + "/" + connection.domain; + + if (host !== null) { + digest_uri = digest_uri + "/" + host; + } + + var cred = utils__WEBPACK_IMPORTED_MODULE_2__["default"].utf16to8(connection.authcid + ":" + realm + ":" + this._connection.pass); + var A1 = md5__WEBPACK_IMPORTED_MODULE_0__["default"].hash(cred) + ":" + nonce + ":" + cnonce; + var A2 = 'AUTHENTICATE:' + digest_uri; + var responseText = ""; + responseText += 'charset=utf-8,'; + responseText += 'username=' + this._quote(utils__WEBPACK_IMPORTED_MODULE_2__["default"].utf16to8(connection.authcid)) + ','; + responseText += 'realm=' + this._quote(realm) + ','; + responseText += 'nonce=' + this._quote(nonce) + ','; + responseText += 'nc=00000001,'; + responseText += 'cnonce=' + this._quote(cnonce) + ','; + responseText += 'digest-uri=' + this._quote(digest_uri) + ','; + responseText += 'response=' + md5__WEBPACK_IMPORTED_MODULE_0__["default"].hexdigest(md5__WEBPACK_IMPORTED_MODULE_0__["default"].hexdigest(A1) + ":" + nonce + ":00000001:" + cnonce + ":auth:" + md5__WEBPACK_IMPORTED_MODULE_0__["default"].hexdigest(A2)) + ","; + responseText += 'qop=auth'; + + this.onChallenge = function () { + return ""; + }; + + return responseText; +}; +/** PrivateConstructor: SASLOAuthBearer + * SASL OAuth Bearer authentication. + */ + + +Strophe.SASLOAuthBearer = function () {}; + +Strophe.SASLOAuthBearer.prototype = new Strophe.SASLMechanism("OAUTHBEARER", true, 40); + +Strophe.SASLOAuthBearer.prototype.test = function (connection) { + return connection.pass !== null; +}; + +Strophe.SASLOAuthBearer.prototype.onChallenge = function (connection) { + var auth_str = 'n,'; + + if (connection.authcid !== null) { + auth_str = auth_str + 'a=' + connection.authzid; + } + + auth_str = auth_str + ','; + auth_str = auth_str + "\x01"; + auth_str = auth_str + 'auth=Bearer '; + auth_str = auth_str + connection.pass; + auth_str = auth_str + "\x01"; + auth_str = auth_str + "\x01"; + return utils__WEBPACK_IMPORTED_MODULE_2__["default"].utf16to8(auth_str); +}; +/** PrivateConstructor: SASLExternal + * SASL EXTERNAL authentication. + * + * The EXTERNAL mechanism allows a client to request the server to use + * credentials established by means external to the mechanism to + * authenticate the client. The external means may be, for instance, + * TLS services. + */ + + +Strophe.SASLExternal = function () {}; + +Strophe.SASLExternal.prototype = new Strophe.SASLMechanism("EXTERNAL", true, 10); + +Strophe.SASLExternal.prototype.onChallenge = function (connection) { + /** According to XEP-178, an authzid SHOULD NOT be presented when the + * authcid contained or implied in the client certificate is the JID (i.e. + * authzid) with which the user wants to log in as. + * + * To NOT send the authzid, the user should therefore set the authcid equal + * to the JID when instantiating a new Strophe.Connection object. + */ + return connection.authcid === connection.authzid ? '' : connection.authzid; +}; +/** PrivateConstructor: SASLXOAuth2 + * SASL X-OAuth2 authentication. + */ + + +Strophe.SASLXOAuth2 = function () {}; + +Strophe.SASLXOAuth2.prototype = new Strophe.SASLMechanism("X-OAUTH2", true, 30); + +Strophe.SASLXOAuth2.prototype.test = function (connection) { + return connection.pass !== null; +}; + +Strophe.SASLXOAuth2.prototype.onChallenge = function (connection) { + var auth_str = "\0"; + + if (connection.authcid !== null) { + auth_str = auth_str + connection.authzid; + } + + auth_str = auth_str + "\0"; + auth_str = auth_str + connection.pass; + return utils__WEBPACK_IMPORTED_MODULE_2__["default"].utf16to8(auth_str); +}; + + +/* harmony default export */ __webpack_exports__["default"] = ({ + 'Strophe': Strophe, + '$build': $build, + '$iq': $iq, + '$msg': $msg, + '$pres': $pres, + 'SHA1': sha1__WEBPACK_IMPORTED_MODULE_1__["default"], + 'MD5': md5__WEBPACK_IMPORTED_MODULE_0__["default"], + 'b64_hmac_sha1': sha1__WEBPACK_IMPORTED_MODULE_1__["default"].b64_hmac_sha1, + 'b64_sha1': sha1__WEBPACK_IMPORTED_MODULE_1__["default"].b64_sha1, + 'str_hmac_sha1': sha1__WEBPACK_IMPORTED_MODULE_1__["default"].str_hmac_sha1, + 'str_sha1': sha1__WEBPACK_IMPORTED_MODULE_1__["default"].str_sha1 +}); + +/***/ }), + +/***/ "./src/md5.js": +/*!********************!*\ + !*** ./src/md5.js ***! + \********************/ +/*! exports provided: default */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default", function() { return MD5; }); +/* + * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message + * Digest Algorithm, as defined in RFC 1321. + * Version 2.1 Copyright (C) Paul Johnston 1999 - 2002. + * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet + * Distributed under the BSD License + * See http://pajhome.org.uk/crypt/md5 for more info. + */ + +/* + * Everything that isn't used by Strophe has been stripped here! + */ + +/* + * Add integers, wrapping at 2^32. This uses 16-bit operations internally + * to work around bugs in some JS interpreters. + */ +var safe_add = function safe_add(x, y) { + var lsw = (x & 0xFFFF) + (y & 0xFFFF); + var msw = (x >> 16) + (y >> 16) + (lsw >> 16); + return msw << 16 | lsw & 0xFFFF; +}; +/* + * Bitwise rotate a 32-bit number to the left. + */ + + +var bit_rol = function bit_rol(num, cnt) { + return num << cnt | num >>> 32 - cnt; +}; +/* + * Convert a string to an array of little-endian words + */ + + +var str2binl = function str2binl(str) { + var bin = []; + + for (var i = 0; i < str.length * 8; i += 8) { + bin[i >> 5] |= (str.charCodeAt(i / 8) & 255) << i % 32; + } + + return bin; +}; +/* + * Convert an array of little-endian words to a string + */ + + +var binl2str = function binl2str(bin) { + var str = ""; + + for (var i = 0; i < bin.length * 32; i += 8) { + str += String.fromCharCode(bin[i >> 5] >>> i % 32 & 255); + } + + return str; +}; +/* + * Convert an array of little-endian words to a hex string. + */ + + +var binl2hex = function binl2hex(binarray) { + var hex_tab = "0123456789abcdef"; + var str = ""; + + for (var i = 0; i < binarray.length * 4; i++) { + str += hex_tab.charAt(binarray[i >> 2] >> i % 4 * 8 + 4 & 0xF) + hex_tab.charAt(binarray[i >> 2] >> i % 4 * 8 & 0xF); + } + + return str; +}; +/* + * These functions implement the four basic operations the algorithm uses. + */ + + +var md5_cmn = function md5_cmn(q, a, b, x, s, t) { + return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s), b); +}; + +var md5_ff = function md5_ff(a, b, c, d, x, s, t) { + return md5_cmn(b & c | ~b & d, a, b, x, s, t); +}; + +var md5_gg = function md5_gg(a, b, c, d, x, s, t) { + return md5_cmn(b & d | c & ~d, a, b, x, s, t); +}; + +var md5_hh = function md5_hh(a, b, c, d, x, s, t) { + return md5_cmn(b ^ c ^ d, a, b, x, s, t); +}; + +var md5_ii = function md5_ii(a, b, c, d, x, s, t) { + return md5_cmn(c ^ (b | ~d), a, b, x, s, t); +}; +/* + * Calculate the MD5 of an array of little-endian words, and a bit length + */ + + +var core_md5 = function core_md5(x, len) { + /* append padding */ + x[len >> 5] |= 0x80 << len % 32; + x[(len + 64 >>> 9 << 4) + 14] = len; + var a = 1732584193; + var b = -271733879; + var c = -1732584194; + var d = 271733878; + var olda, oldb, oldc, oldd; + + for (var i = 0; i < x.length; i += 16) { + olda = a; + oldb = b; + oldc = c; + oldd = d; + a = md5_ff(a, b, c, d, x[i + 0], 7, -680876936); + d = md5_ff(d, a, b, c, x[i + 1], 12, -389564586); + c = md5_ff(c, d, a, b, x[i + 2], 17, 606105819); + b = md5_ff(b, c, d, a, x[i + 3], 22, -1044525330); + a = md5_ff(a, b, c, d, x[i + 4], 7, -176418897); + d = md5_ff(d, a, b, c, x[i + 5], 12, 1200080426); + c = md5_ff(c, d, a, b, x[i + 6], 17, -1473231341); + b = md5_ff(b, c, d, a, x[i + 7], 22, -45705983); + a = md5_ff(a, b, c, d, x[i + 8], 7, 1770035416); + d = md5_ff(d, a, b, c, x[i + 9], 12, -1958414417); + c = md5_ff(c, d, a, b, x[i + 10], 17, -42063); + b = md5_ff(b, c, d, a, x[i + 11], 22, -1990404162); + a = md5_ff(a, b, c, d, x[i + 12], 7, 1804603682); + d = md5_ff(d, a, b, c, x[i + 13], 12, -40341101); + c = md5_ff(c, d, a, b, x[i + 14], 17, -1502002290); + b = md5_ff(b, c, d, a, x[i + 15], 22, 1236535329); + a = md5_gg(a, b, c, d, x[i + 1], 5, -165796510); + d = md5_gg(d, a, b, c, x[i + 6], 9, -1069501632); + c = md5_gg(c, d, a, b, x[i + 11], 14, 643717713); + b = md5_gg(b, c, d, a, x[i + 0], 20, -373897302); + a = md5_gg(a, b, c, d, x[i + 5], 5, -701558691); + d = md5_gg(d, a, b, c, x[i + 10], 9, 38016083); + c = md5_gg(c, d, a, b, x[i + 15], 14, -660478335); + b = md5_gg(b, c, d, a, x[i + 4], 20, -405537848); + a = md5_gg(a, b, c, d, x[i + 9], 5, 568446438); + d = md5_gg(d, a, b, c, x[i + 14], 9, -1019803690); + c = md5_gg(c, d, a, b, x[i + 3], 14, -187363961); + b = md5_gg(b, c, d, a, x[i + 8], 20, 1163531501); + a = md5_gg(a, b, c, d, x[i + 13], 5, -1444681467); + d = md5_gg(d, a, b, c, x[i + 2], 9, -51403784); + c = md5_gg(c, d, a, b, x[i + 7], 14, 1735328473); + b = md5_gg(b, c, d, a, x[i + 12], 20, -1926607734); + a = md5_hh(a, b, c, d, x[i + 5], 4, -378558); + d = md5_hh(d, a, b, c, x[i + 8], 11, -2022574463); + c = md5_hh(c, d, a, b, x[i + 11], 16, 1839030562); + b = md5_hh(b, c, d, a, x[i + 14], 23, -35309556); + a = md5_hh(a, b, c, d, x[i + 1], 4, -1530992060); + d = md5_hh(d, a, b, c, x[i + 4], 11, 1272893353); + c = md5_hh(c, d, a, b, x[i + 7], 16, -155497632); + b = md5_hh(b, c, d, a, x[i + 10], 23, -1094730640); + a = md5_hh(a, b, c, d, x[i + 13], 4, 681279174); + d = md5_hh(d, a, b, c, x[i + 0], 11, -358537222); + c = md5_hh(c, d, a, b, x[i + 3], 16, -722521979); + b = md5_hh(b, c, d, a, x[i + 6], 23, 76029189); + a = md5_hh(a, b, c, d, x[i + 9], 4, -640364487); + d = md5_hh(d, a, b, c, x[i + 12], 11, -421815835); + c = md5_hh(c, d, a, b, x[i + 15], 16, 530742520); + b = md5_hh(b, c, d, a, x[i + 2], 23, -995338651); + a = md5_ii(a, b, c, d, x[i + 0], 6, -198630844); + d = md5_ii(d, a, b, c, x[i + 7], 10, 1126891415); + c = md5_ii(c, d, a, b, x[i + 14], 15, -1416354905); + b = md5_ii(b, c, d, a, x[i + 5], 21, -57434055); + a = md5_ii(a, b, c, d, x[i + 12], 6, 1700485571); + d = md5_ii(d, a, b, c, x[i + 3], 10, -1894986606); + c = md5_ii(c, d, a, b, x[i + 10], 15, -1051523); + b = md5_ii(b, c, d, a, x[i + 1], 21, -2054922799); + a = md5_ii(a, b, c, d, x[i + 8], 6, 1873313359); + d = md5_ii(d, a, b, c, x[i + 15], 10, -30611744); + c = md5_ii(c, d, a, b, x[i + 6], 15, -1560198380); + b = md5_ii(b, c, d, a, x[i + 13], 21, 1309151649); + a = md5_ii(a, b, c, d, x[i + 4], 6, -145523070); + d = md5_ii(d, a, b, c, x[i + 11], 10, -1120210379); + c = md5_ii(c, d, a, b, x[i + 2], 15, 718787259); + b = md5_ii(b, c, d, a, x[i + 9], 21, -343485551); + a = safe_add(a, olda); + b = safe_add(b, oldb); + c = safe_add(c, oldc); + d = safe_add(d, oldd); + } + + return [a, b, c, d]; +}; +/* + * These are the functions you'll usually want to call. + * They take string arguments and return either hex or base-64 encoded + * strings. + */ + + +var MD5 = { + hexdigest: function hexdigest(s) { + return binl2hex(core_md5(str2binl(s), s.length * 8)); + }, + hash: function hash(s) { + return binl2str(core_md5(str2binl(s), s.length * 8)); + } +}; + + +/***/ }), + +/***/ "./src/sha1.js": +/*!*********************!*\ + !*** ./src/sha1.js ***! + \*********************/ +/*! exports provided: default */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default", function() { return SHA1; }); +/* + * A JavaScript implementation of the Secure Hash Algorithm, SHA-1, as defined + * in FIPS PUB 180-1 + * Version 2.1a Copyright Paul Johnston 2000 - 2002. + * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet + * Distributed under the BSD License + * See http://pajhome.org.uk/crypt/md5 for details. + */ + +/* jshint undef: true, unused: true:, noarg: true, latedef: false */ + +/* global define */ + +/* Some functions and variables have been stripped for use with Strophe */ + +/* + * Calculate the SHA-1 of an array of big-endian words, and a bit length + */ +function core_sha1(x, len) { + /* append padding */ + x[len >> 5] |= 0x80 << 24 - len % 32; + x[(len + 64 >> 9 << 4) + 15] = len; + var w = new Array(80); + var a = 1732584193; + var b = -271733879; + var c = -1732584194; + var d = 271733878; + var e = -1009589776; + var i, j, t, olda, oldb, oldc, oldd, olde; + + for (i = 0; i < x.length; i += 16) { + olda = a; + oldb = b; + oldc = c; + oldd = d; + olde = e; + + for (j = 0; j < 80; j++) { + if (j < 16) { + w[j] = x[i + j]; + } else { + w[j] = rol(w[j - 3] ^ w[j - 8] ^ w[j - 14] ^ w[j - 16], 1); + } + + t = safe_add(safe_add(rol(a, 5), sha1_ft(j, b, c, d)), safe_add(safe_add(e, w[j]), sha1_kt(j))); + e = d; + d = c; + c = rol(b, 30); + b = a; + a = t; + } + + a = safe_add(a, olda); + b = safe_add(b, oldb); + c = safe_add(c, oldc); + d = safe_add(d, oldd); + e = safe_add(e, olde); + } + + return [a, b, c, d, e]; +} +/* + * Perform the appropriate triplet combination function for the current + * iteration + */ + + +function sha1_ft(t, b, c, d) { + if (t < 20) { + return b & c | ~b & d; + } + + if (t < 40) { + return b ^ c ^ d; + } + + if (t < 60) { + return b & c | b & d | c & d; + } + + return b ^ c ^ d; +} +/* + * Determine the appropriate additive constant for the current iteration + */ + + +function sha1_kt(t) { + return t < 20 ? 1518500249 : t < 40 ? 1859775393 : t < 60 ? -1894007588 : -899497514; +} +/* + * Calculate the HMAC-SHA1 of a key and some data + */ + + +function core_hmac_sha1(key, data) { + var bkey = str2binb(key); + + if (bkey.length > 16) { + bkey = core_sha1(bkey, key.length * 8); + } + + var ipad = new Array(16), + opad = new Array(16); + + for (var i = 0; i < 16; i++) { + ipad[i] = bkey[i] ^ 0x36363636; + opad[i] = bkey[i] ^ 0x5C5C5C5C; + } + + var hash = core_sha1(ipad.concat(str2binb(data)), 512 + data.length * 8); + return core_sha1(opad.concat(hash), 512 + 160); +} +/* + * Add integers, wrapping at 2^32. This uses 16-bit operations internally + * to work around bugs in some JS interpreters. + */ + + +function safe_add(x, y) { + var lsw = (x & 0xFFFF) + (y & 0xFFFF); + var msw = (x >> 16) + (y >> 16) + (lsw >> 16); + return msw << 16 | lsw & 0xFFFF; +} +/* + * Bitwise rotate a 32-bit number to the left. + */ + + +function rol(num, cnt) { + return num << cnt | num >>> 32 - cnt; +} +/* + * Convert an 8-bit or 16-bit string to an array of big-endian words + * In 8-bit function, characters >255 have their hi-byte silently ignored. + */ + + +function str2binb(str) { + var bin = []; + var mask = 255; + + for (var i = 0; i < str.length * 8; i += 8) { + bin[i >> 5] |= (str.charCodeAt(i / 8) & mask) << 24 - i % 32; + } + + return bin; +} +/* + * Convert an array of big-endian words to a base-64 string + */ + + +function binb2b64(binarray) { + var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + var str = ""; + var triplet, j; + + for (var i = 0; i < binarray.length * 4; i += 3) { + triplet = (binarray[i >> 2] >> 8 * (3 - i % 4) & 0xFF) << 16 | (binarray[i + 1 >> 2] >> 8 * (3 - (i + 1) % 4) & 0xFF) << 8 | binarray[i + 2 >> 2] >> 8 * (3 - (i + 2) % 4) & 0xFF; + + for (j = 0; j < 4; j++) { + if (i * 8 + j * 6 > binarray.length * 32) { + str += "="; + } else { + str += tab.charAt(triplet >> 6 * (3 - j) & 0x3F); + } + } + } + + return str; +} +/* + * Convert an array of big-endian words to a string + */ + + +function binb2str(bin) { + var str = ""; + var mask = 255; + + for (var i = 0; i < bin.length * 32; i += 8) { + str += String.fromCharCode(bin[i >> 5] >>> 24 - i % 32 & mask); + } + + return str; +} +/* + * These are the functions you'll usually want to call + * They take string arguments and return either hex or base-64 encoded strings + */ + + +var SHA1 = { + b64_hmac_sha1: function b64_hmac_sha1(key, data) { + return binb2b64(core_hmac_sha1(key, data)); + }, + b64_sha1: function b64_sha1(s) { + return binb2b64(core_sha1(str2binb(s), s.length * 8)); + }, + binb2str: binb2str, + core_hmac_sha1: core_hmac_sha1, + str_hmac_sha1: function str_hmac_sha1(key, data) { + return binb2str(core_hmac_sha1(key, data)); + }, + str_sha1: function str_sha1(s) { + return binb2str(core_sha1(str2binb(s), s.length * 8)); + } +}; + + +/***/ }), + +/***/ "./src/strophe.js": +/*!************************!*\ + !*** ./src/strophe.js ***! + \************************/ +/*! exports provided: default */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* WEBPACK VAR INJECTION */(function(global) {/* harmony import */ var core__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! core */ "./src/core.js"); +/* harmony import */ var bosh__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! bosh */ "./src/bosh.js"); +/* harmony import */ var websocket__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! websocket */ "./src/websocket.js"); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "default", function() { return core__WEBPACK_IMPORTED_MODULE_0__["default"]; }); + +/*global global*/ + + + +global.Strophe = core__WEBPACK_IMPORTED_MODULE_0__["default"].Strophe; +global.$build = core__WEBPACK_IMPORTED_MODULE_0__["default"].$build; +global.$iq = core__WEBPACK_IMPORTED_MODULE_0__["default"].$iq; +global.$msg = core__WEBPACK_IMPORTED_MODULE_0__["default"].$msg; +global.$pres = core__WEBPACK_IMPORTED_MODULE_0__["default"].$pres; + +/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(/*! ./../node_modules/webpack/buildin/global.js */ "./node_modules/webpack/buildin/global.js"))) + +/***/ }), + +/***/ "./src/utils.js": +/*!**********************!*\ + !*** ./src/utils.js ***! + \**********************/ +/*! exports provided: default */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "default", function() { return utils; }); +function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } + +var utils = { + utf16to8: function utf16to8(str) { + var i, c; + var out = ""; + var len = str.length; + + for (i = 0; i < len; i++) { + c = str.charCodeAt(i); + + if (c >= 0x0000 && c <= 0x007F) { + out += str.charAt(i); + } else if (c > 0x07FF) { + out += String.fromCharCode(0xE0 | c >> 12 & 0x0F); + out += String.fromCharCode(0x80 | c >> 6 & 0x3F); + out += String.fromCharCode(0x80 | c >> 0 & 0x3F); + } else { + out += String.fromCharCode(0xC0 | c >> 6 & 0x1F); + out += String.fromCharCode(0x80 | c >> 0 & 0x3F); + } + } + + return out; + }, + addCookies: function addCookies(cookies) { + /* Parameters: + * (Object) cookies - either a map of cookie names + * to string values or to maps of cookie values. + * + * For example: + * { "myCookie": "1234" } + * + * or: + * { "myCookie": { + * "value": "1234", + * "domain": ".example.org", + * "path": "/", + * "expires": expirationDate + * } + * } + * + * These values get passed to Strophe.Connection via + * options.cookies + */ + cookies = cookies || {}; + + for (var cookieName in cookies) { + if (Object.prototype.hasOwnProperty.call(cookies, cookieName)) { + var expires = ''; + var domain = ''; + var path = ''; + var cookieObj = cookies[cookieName]; + var isObj = _typeof(cookieObj) === "object"; + var cookieValue = escape(unescape(isObj ? cookieObj.value : cookieObj)); + + if (isObj) { + expires = cookieObj.expires ? ";expires=" + cookieObj.expires : ''; + domain = cookieObj.domain ? ";domain=" + cookieObj.domain : ''; + path = cookieObj.path ? ";path=" + cookieObj.path : ''; + } + + document.cookie = cookieName + '=' + cookieValue + expires + domain + path; + } + } + } +}; + + +/***/ }), + +/***/ "./src/websocket.js": +/*!**************************!*\ + !*** ./src/websocket.js ***! + \**************************/ +/*! no exports provided */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony import */ var core__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! core */ "./src/core.js"); +/* + This program is distributed under the terms of the MIT license. + Please see the LICENSE file for details. + + Copyright 2006-2008, OGG, LLC +*/ + +/* global window, clearTimeout, WebSocket, DOMParser */ + +var Strophe = core__WEBPACK_IMPORTED_MODULE_0__["default"].Strophe; +var $build = core__WEBPACK_IMPORTED_MODULE_0__["default"].$build; +/** Class: Strophe.WebSocket + * _Private_ helper class that handles WebSocket Connections + * + * The Strophe.WebSocket class is used internally by Strophe.Connection + * to encapsulate WebSocket sessions. It is not meant to be used from user's code. + */ + +/** File: websocket.js + * A JavaScript library to enable XMPP over Websocket in Strophejs. + * + * This file implements XMPP over WebSockets for Strophejs. + * If a Connection is established with a Websocket url (ws://...) + * Strophe will use WebSockets. + * For more information on XMPP-over-WebSocket see RFC 7395: + * http://tools.ietf.org/html/rfc7395 + * + * WebSocket support implemented by Andreas Guth (andreas.guth@rwth-aachen.de) + */ + +/** PrivateConstructor: Strophe.Websocket + * Create and initialize a Strophe.WebSocket object. + * Currently only sets the connection Object. + * + * Parameters: + * (Strophe.Connection) connection - The Strophe.Connection that will use WebSockets. + * + * Returns: + * A new Strophe.WebSocket object. + */ + +Strophe.Websocket = function (connection) { + this._conn = connection; + this.strip = "wrapper"; + var service = connection.service; + + if (service.indexOf("ws:") !== 0 && service.indexOf("wss:") !== 0) { + // If the service is not an absolute URL, assume it is a path and put the absolute + // URL together from options, current URL and the path. + var new_service = ""; + + if (connection.options.protocol === "ws" && window.location.protocol !== "https:") { + new_service += "ws"; + } else { + new_service += "wss"; + } + + new_service += "://" + window.location.host; + + if (service.indexOf("/") !== 0) { + new_service += window.location.pathname + service; + } else { + new_service += service; + } + + connection.service = new_service; + } +}; + +Strophe.Websocket.prototype = { + /** PrivateFunction: _buildStream + * _Private_ helper function to generate the start tag for WebSockets + * + * Returns: + * A Strophe.Builder with a element. + */ + _buildStream: function _buildStream() { + return $build("open", { + "xmlns": Strophe.NS.FRAMING, + "to": this._conn.domain, + "version": '1.0' + }); + }, + + /** PrivateFunction: _check_streamerror + * _Private_ checks a message for stream:error + * + * Parameters: + * (Strophe.Request) bodyWrap - The received stanza. + * connectstatus - The ConnectStatus that will be set on error. + * Returns: + * true if there was a streamerror, false otherwise. + */ + _check_streamerror: function _check_streamerror(bodyWrap, connectstatus) { + var errors; + + if (bodyWrap.getElementsByTagNameNS) { + errors = bodyWrap.getElementsByTagNameNS(Strophe.NS.STREAM, "error"); + } else { + errors = bodyWrap.getElementsByTagName("stream:error"); + } + + if (errors.length === 0) { + return false; + } + + var error = errors[0]; + var condition = ""; + var text = ""; + var ns = "urn:ietf:params:xml:ns:xmpp-streams"; + + for (var i = 0; i < error.childNodes.length; i++) { + var e = error.childNodes[i]; + + if (e.getAttribute("xmlns") !== ns) { + break; + } + + if (e.nodeName === "text") { + text = e.textContent; + } else { + condition = e.nodeName; + } + } + + var errorString = "WebSocket stream error: "; + + if (condition) { + errorString += condition; + } else { + errorString += "unknown"; + } + + if (text) { + errorString += " - " + text; + } + + Strophe.error(errorString); // close the connection on stream_error + + this._conn._changeConnectStatus(connectstatus, condition); + + this._conn._doDisconnect(); + + return true; + }, + + /** PrivateFunction: _reset + * Reset the connection. + * + * This function is called by the reset function of the Strophe Connection. + * Is not needed by WebSockets. + */ + _reset: function _reset() { + return; + }, + + /** PrivateFunction: _connect + * _Private_ function called by Strophe.Connection.connect + * + * Creates a WebSocket for a connection and assigns Callbacks to it. + * Does nothing if there already is a WebSocket. + */ + _connect: function _connect() { + // Ensure that there is no open WebSocket from a previous Connection. + this._closeSocket(); // Create the new WobSocket + + + this.socket = new WebSocket(this._conn.service, "xmpp"); + this.socket.onopen = this._onOpen.bind(this); + this.socket.onerror = this._onError.bind(this); + this.socket.onclose = this._onClose.bind(this); + this.socket.onmessage = this._connect_cb_wrapper.bind(this); + }, + + /** PrivateFunction: _connect_cb + * _Private_ function called by Strophe.Connection._connect_cb + * + * checks for stream:error + * + * Parameters: + * (Strophe.Request) bodyWrap - The received stanza. + */ + _connect_cb: function _connect_cb(bodyWrap) { + var error = this._check_streamerror(bodyWrap, Strophe.Status.CONNFAIL); + + if (error) { + return Strophe.Status.CONNFAIL; + } + }, + + /** PrivateFunction: _handleStreamStart + * _Private_ function that checks the opening tag for errors. + * + * Disconnects if there is an error and returns false, true otherwise. + * + * Parameters: + * (Node) message - Stanza containing the tag. + */ + _handleStreamStart: function _handleStreamStart(message) { + var error = false; // Check for errors in the tag + + var ns = message.getAttribute("xmlns"); + + if (typeof ns !== "string") { + error = "Missing xmlns in "; + } else if (ns !== Strophe.NS.FRAMING) { + error = "Wrong xmlns in : " + ns; + } + + var ver = message.getAttribute("version"); + + if (typeof ver !== "string") { + error = "Missing version in "; + } else if (ver !== "1.0") { + error = "Wrong version in : " + ver; + } + + if (error) { + this._conn._changeConnectStatus(Strophe.Status.CONNFAIL, error); + + this._conn._doDisconnect(); + + return false; + } + + return true; + }, + + /** PrivateFunction: _connect_cb_wrapper + * _Private_ function that handles the first connection messages. + * + * On receiving an opening stream tag this callback replaces itself with the real + * message handler. On receiving a stream error the connection is terminated. + */ + _connect_cb_wrapper: function _connect_cb_wrapper(message) { + if (message.data.indexOf("\s*)*/, ""); + if (data === '') return; + var streamStart = new DOMParser().parseFromString(data, "text/xml").documentElement; + + this._conn.xmlInput(streamStart); + + this._conn.rawInput(message.data); //_handleStreamSteart will check for XML errors and disconnect on error + + + if (this._handleStreamStart(streamStart)) { + //_connect_cb will check for stream:error and disconnect on error + this._connect_cb(streamStart); + } + } else if (message.data.indexOf("WSS, WS->ANY + + var isSecureRedirect = service.indexOf("wss:") >= 0 && see_uri.indexOf("wss:") >= 0 || service.indexOf("ws:") >= 0; + + if (isSecureRedirect) { + this._conn._changeConnectStatus(Strophe.Status.REDIRECT, "Received see-other-uri, resetting connection"); + + this._conn.reset(); + + this._conn.service = see_uri; + + this._connect(); + } + } else { + this._conn._changeConnectStatus(Strophe.Status.CONNFAIL, "Received closing stream"); + + this._conn._doDisconnect(); + } + } else { + var string = this._streamWrap(message.data); + + var elem = new DOMParser().parseFromString(string, "text/xml").documentElement; + this.socket.onmessage = this._onMessage.bind(this); + + this._conn._connect_cb(elem, null, message.data); + } + }, + + /** PrivateFunction: _disconnect + * _Private_ function called by Strophe.Connection.disconnect + * + * Disconnects and sends a last stanza if one is given + * + * Parameters: + * (Request) pres - This stanza will be sent before disconnecting. + */ + _disconnect: function _disconnect(pres) { + if (this.socket && this.socket.readyState !== WebSocket.CLOSED) { + if (pres) { + this._conn.send(pres); + } + + var close = $build("close", { + "xmlns": Strophe.NS.FRAMING + }); + + this._conn.xmlOutput(close.tree()); + + var closeString = Strophe.serialize(close); + + this._conn.rawOutput(closeString); + + try { + this.socket.send(closeString); + } catch (e) { + Strophe.info("Couldn't send tag."); + } + } + + this._conn._doDisconnect(); + }, + + /** PrivateFunction: _doDisconnect + * _Private_ function to disconnect. + * + * Just closes the Socket for WebSockets + */ + _doDisconnect: function _doDisconnect() { + Strophe.info("WebSockets _doDisconnect was called"); + + this._closeSocket(); + }, + + /** PrivateFunction _streamWrap + * _Private_ helper function to wrap a stanza in a tag. + * This is used so Strophe can process stanzas from WebSockets like BOSH + */ + _streamWrap: function _streamWrap(stanza) { + return "" + stanza + ''; + }, + + /** PrivateFunction: _closeSocket + * _Private_ function to close the WebSocket. + * + * Closes the socket if it is still open and deletes it + */ + _closeSocket: function _closeSocket() { + if (this.socket) { + try { + this.socket.onerror = null; + this.socket.close(); + } catch (e) { + Strophe.debug(e.message); + } + } + + this.socket = null; + }, + + /** PrivateFunction: _emptyQueue + * _Private_ function to check if the message queue is empty. + * + * Returns: + * True, because WebSocket messages are send immediately after queueing. + */ + _emptyQueue: function _emptyQueue() { + return true; + }, + + /** PrivateFunction: _onClose + * _Private_ function to handle websockets closing. + * + * Nothing to do here for WebSockets + */ + _onClose: function _onClose(e) { + if (this._conn.connected && !this._conn.disconnecting) { + Strophe.error("Websocket closed unexpectedly"); + + this._conn._doDisconnect(); + } else if (e && e.code === 1006 && !this._conn.connected && this.socket) { + // in case the onError callback was not called (Safari 10 does not + // call onerror when the initial connection fails) we need to + // dispatch a CONNFAIL status update to be consistent with the + // behavior on other browsers. + Strophe.error("Websocket closed unexcectedly"); + + this._conn._changeConnectStatus(Strophe.Status.CONNFAIL, "The WebSocket connection could not be established or was disconnected."); + + this._conn._doDisconnect(); + } else { + Strophe.info("Websocket closed"); + } + }, + + /** PrivateFunction: _no_auth_received + * + * Called on stream start/restart when no stream:features + * has been received. + */ + _no_auth_received: function _no_auth_received(callback) { + Strophe.error("Server did not offer a supported authentication mechanism"); + + this._changeConnectStatus(Strophe.Status.CONNFAIL, Strophe.ErrorCondition.NO_AUTH_MECH); + + if (callback) { + callback.call(this._conn); + } + + this._conn._doDisconnect(); + }, + + /** PrivateFunction: _onDisconnectTimeout + * _Private_ timeout handler for handling non-graceful disconnection. + * + * This does nothing for WebSockets + */ + _onDisconnectTimeout: function _onDisconnectTimeout() {}, + + /** PrivateFunction: _abortAllRequests + * _Private_ helper function that makes sure all pending requests are aborted. + */ + _abortAllRequests: function _abortAllRequests() {}, + + /** PrivateFunction: _onError + * _Private_ function to handle websockets errors. + * + * Parameters: + * (Object) error - The websocket error. + */ + _onError: function _onError(error) { + Strophe.error("Websocket error " + error); + + this._conn._changeConnectStatus(Strophe.Status.CONNFAIL, "The WebSocket connection could not be established or was disconnected."); + + this._disconnect(); + }, + + /** PrivateFunction: _onIdle + * _Private_ function called by Strophe.Connection._onIdle + * + * sends all queued stanzas + */ + _onIdle: function _onIdle() { + var data = this._conn._data; + + if (data.length > 0 && !this._conn.paused) { + for (var i = 0; i < data.length; i++) { + if (data[i] !== null) { + var stanza = void 0; + + if (data[i] === "restart") { + stanza = this._buildStream().tree(); + } else { + stanza = data[i]; + } + + var rawStanza = Strophe.serialize(stanza); + + this._conn.xmlOutput(stanza); + + this._conn.rawOutput(rawStanza); + + this.socket.send(rawStanza); + } + } + + this._conn._data = []; + } + }, + + /** PrivateFunction: _onMessage + * _Private_ function to handle websockets messages. + * + * This function parses each of the messages as if they are full documents. + * [TODO : We may actually want to use a SAX Push parser]. + * + * Since all XMPP traffic starts with + * + * + * The first stanza will always fail to be parsed. + * + * Additionally, the seconds stanza will always be with + * the stream NS defined in the previous stanza, so we need to 'force' + * the inclusion of the NS in this stanza. + * + * Parameters: + * (string) message - The websocket message. + */ + _onMessage: function _onMessage(message) { + var elem; // check for closing stream + + var close = ''; + + if (message.data === close) { + this._conn.rawInput(close); + + this._conn.xmlInput(message); + + if (!this._conn.disconnecting) { + this._conn._doDisconnect(); + } + + return; + } else if (message.data.search(" tag before we close the connection + + + return; + } + + this._conn._dataRecv(elem, message.data); + }, + + /** PrivateFunction: _onOpen + * _Private_ function to handle websockets connection setup. + * + * The opening stream tag is sent here. + */ + _onOpen: function _onOpen() { + Strophe.info("Websocket open"); + + var start = this._buildStream(); + + this._conn.xmlOutput(start.tree()); + + var startString = Strophe.serialize(start); + + this._conn.rawOutput(startString); + + this.socket.send(startString); + }, + + /** PrivateFunction: _reqToData + * _Private_ function to get a stanza out of a request. + * + * WebSockets don't use requests, so the passed argument is just returned. + * + * Parameters: + * (Object) stanza - The stanza. + * + * Returns: + * The stanza that was passed. + */ + _reqToData: function _reqToData(stanza) { + return stanza; + }, + + /** PrivateFunction: _send + * _Private_ part of the Connection.send function for WebSocket + * + * Just flushes the messages that are in the queue + */ + _send: function _send() { + this._conn.flush(); + }, + + /** PrivateFunction: _sendRestart + * + * Send an xmpp:restart stanza. + */ + _sendRestart: function _sendRestart() { + clearTimeout(this._conn._idleTimeout); + + this._conn._onIdle.bind(this._conn)(); + } +}; + +/***/ }) + +/******/ })["default"]; +}); +//# sourceMappingURL=strophe.js.map \ No newline at end of file diff --git a/src/extension/plugins/xmpp/web/svg.js b/src/extension/plugins/xmpp/web/svg.js new file mode 100644 index 0000000000..8f22fc21ce --- /dev/null +++ b/src/extension/plugins/xmpp/web/svg.js @@ -0,0 +1,300 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +'use strict'; + +const NS = { + xrd: 'http://docs.oasis-open.org/ns/xri/xrd-1.0', + roster: 'jabber:iq:roster', + disco_info: 'http://jabber.org/protocol/disco#info', + pubsub: 'http://jabber.org/protocol/pubsub', + avatar_metadata: 'urn:xmpp:avatar:metadata', + avatar_data: 'urn:xmpp:avatar:data', + nickname: 'http://jabber.org/protocol/nick', + caps: 'http://jabber.org/protocol/caps', + ecaps2: 'urn:xmpp:caps', + hashes: 'urn:xmpp:hashes:2', + xhtml: 'http://www.w3.org/1999/xhtml', + svg: 'http://www.w3.org/2000/svg', +}; + +function nsResolver(prefix) { + return NS[prefix] || null; +} + +function parseXPath(elem, xpath, result) +{ + if (result === undefined) + result = XPathResult.FIRST_ORDERED_NODE_TYPE; + const value = elem.getRootNode().evaluate(xpath, elem, nsResolver, result, null); + if (result == XPathResult.FIRST_ORDERED_NODE_TYPE) + return value.singleNodeValue; + return value; +} + +function parseXPathText(elem, xpath) +{ + const value = parseXPath(elem, xpath); + if (value === null) + return null; + return value.textContent; +} + +function displaySpinner(spinner) { + if ('timeoutid' in spinner.dataset) + clearTimeout(spinner.dataset.timeoutid); + spinner.src = 'spinner.svg'; + spinner.title = ''; + spinner.hidden = false; +} + +function spinnerOk(spinner) { + if ('timeoutid' in spinner.dataset) + clearTimeout(spinner.dataset.timeoutid); + spinner.src = 'ok.svg'; + spinner.title = ''; + spinner.hidden = false; + spinner.dataset.timeoutid = setTimeout(function () { + spinner.hidden = true; + }, 1000); +} + +function spinnerError(spinner, title) { + if ('timeoutid' in spinner.dataset) + clearTimeout(spinner.dataset.timeoutid); + spinner.src = 'error.svg'; + spinner.title = title ? title : ''; + spinner.hidden = false; +} + +function hideSpinner(spinner) { + if ('timeoutid' in spinner.dataset) + clearTimeout(spinner.dataset.timeoutid); + spinner.hidden = true; +} + +document.addEventListener('DOMContentLoaded', function () { + let connection = null; + + const jid_element = document.getElementById('jid'); + const pass_element = document.getElementById('pass'); + const connect_button = document.getElementById('connect'); + const spinner_img = document.getElementById('connect-spinner'); + + const connected_div = document.getElementById('connected'); + const roster_table = document.getElementById('roster'); + + const avatar_img = document.getElementById('avatar'); + + function rawInput(data) + { + console.log('RECV', data); + } + + function rawOutput(data) + { + console.log('SENT', data); + } + + connect_button.addEventListener('click', function (evt) { + if (!connect_button.classList.contains('disconnect')) { + const jid = jid_element.value; + getBOSHService(jid).then((bosh_service) => { + connection = new Strophe.Connection(bosh_service); + connection.rawInput = rawInput; + connection.rawOutput = rawOutput; + connection.connect(jid, + pass_element.value, + onConnect); + }); + } else if (connection != null) { + connection.disconnect(); + } + evt.preventDefault(); + }); + + function getBOSHService(jid) + { + return new Promise((resolve, reject) => { + const [nodepart, domainpart] = jid.split('@', 2); + const url = 'https://' + domainpart + '/.well-known/host-meta'; + const xhr = new XMLHttpRequest(); + xhr.onload = function (evt) { + const xml = evt.target.responseXML; + const links = parseXPath(xml, './xrd:XRD/xrd:Link', XPathResult.ORDERED_NODE_ITERATOR_TYPE); + let bosh_service = null; + while (true) { + const link = links.iterateNext(); + if (!link) + break; + if (link.getAttributeNS(null, 'rel') == 'urn:xmpp:alt-connections:xbosh') { + bosh_service = link.getAttributeNS(null, 'href'); + break; + } + // TODO: also support WebSocket. + } + console.log('bosh_service', bosh_service); + resolve(bosh_service); + }; + xhr.open('GET', url); + xhr.send(); + }); + } + + function onConnect(status) + { + if (status == Strophe.Status.CONNECTING) { + console.log('Strophe is connecting.'); + connect_button.value = 'Log out'; + connect_button.classList.add('disconnect'); + jid_element.disabled = true; + pass_element.disabled = true; + displaySpinner(spinner_img); + } else if (status == Strophe.Status.CONNFAIL) { + console.log('Strophe failed to connect.'); + onDisconnected(); + } else if (status == Strophe.Status.DISCONNECTING) { + console.log('Strophe is disconnecting.'); + displaySpinner(spinner_img); + } else if (status == Strophe.Status.DISCONNECTED) { + console.log('Strophe is disconnected.'); + onDisconnected(); + } else if (status == Strophe.Status.CONNECTED) { + console.log('Strophe is connected.'); + onConnected(); + } + } + + function onConnected() + { + jid_element.hidden = true; + pass_element.hidden = true; + connected_div.hidden = false; + hideSpinner(spinner_img); + initRoster(connection); + } + + function onDisconnected() + { + connect_button.value = 'Log in'; + connect_button.classList.remove('disconnect'); + jid_element.hidden = false; + jid_element.disabled = false; + pass_element.hidden = false; + pass_element.disabled = false; + hideSpinner(spinner_img); + connected_div.hidden = true; + } + + function initRoster(connection) + { + const tr_dict = {}; + + function onRoster(result_iq) + { + const items = parseXPath(result_iq, './roster:query/roster:item', XPathResult.ORDERED_NODE_ITERATOR_TYPE); + while (true) { + const item = items.iterateNext(); + if (!item) + break; + const jid = item.getAttributeNS(null, 'jid'); + const subscription = item.getAttributeNS(null, 'subscription'); + const name = item.getAttributeNS(null, 'name'); + const groups = item.children; + console.log("got contact:", jid, subscription, name, groups); + + const tr = document.createElementNS(NS.xhtml, 'tr'); + tr.setAttributeNS(null, 'style', 'opacity: 50%'); + tr_dict[jid] = tr; + const td = document.createElementNS(NS.xhtml, 'td'); + const a = document.createElementNS(NS.xhtml, 'a'); + a.setAttributeNS(null, 'href', 'xmpp:' + jid); + const text = document.createTextNode(jid); + a.appendChild(text); + td.appendChild(a); + tr.appendChild(td); + roster_table.lastChild.appendChild(tr); + } + } + + function onRosterError(string) + { + console.log('Failed to retrieve your contact list: ' + string); + } + + function onPresence(presence) + { + function onDiscoInfo(result_iq) + { + const features = parseXPath(result_iq, './disco_info:query/disco_info:feature', XPathResult.ORDERED_NODE_ITERATOR_TYPE); + let has_jingle_transport = false; + let has_sxe = false; + while (true) { + const feature = features.iterateNext(); + if (!feature) + break; + const var_ = feature.getAttributeNS(null, 'var'); + if (var_ === 'urn:xmpp:jingle:transports:sxe') + has_jingle_transport = true; + else if (var_ === 'urn:xmpp:sxe:0') + has_sxe = true; + } + if (has_jingle_transport && has_sxe) { + console.log("Hello Inkscape!"); + } + } + + function onDiscoInfoError(string) + { + console.log('Failed to retrieve contact disco#info: ' + string); + } + + const jid = presence.getAttributeNS(null, 'from'); + const [bare, resource] = jid.split('/', 2); + const type = presence.getAttributeNS(null, 'type'); + console.log("got presence:", bare, resource, type); + + // TODO: handle our own presence differently. + // TODO: handle more than one resource per contact. + const tr = tr_dict[bare]; + if (type === null) { + if (tr !== undefined) { + tr.setAttributeNS(null, 'style', 'opacity: 100%'); + } + const caps = parseXPath(presence, './caps:c', XPathResult.ORDERED_NODE_ITERATOR_TYPE).iterateNext(); + if (caps !== null) { + const node = caps.getAttributeNS(null, 'node'); + const ver = caps.getAttributeNS(null, 'ver'); + if (node !== null && ver !== null) { + const iq = $iq({type: 'get', to: jid}) + .c('query', {xmlns: NS.disco_info, node: node + '#' + ver}); + connection.sendIQ(iq, onDiscoInfo, onDiscoInfoError.bind(null, 'disco#info query failed.')); + } + } + // TODO: move to only ecaps2. + /* + const hashes = parseXPath(presence, './ecaps2:c/hashes:hash', XPathResult.ORDERED_NODE_ITERATOR_TYPE); + while (true) { + const hash = hashes.iterateNext(); + if (hash === null) + break; + console.log(hash); + } + */ + } else if (type === 'unavailable') { + if (tr !== undefined) { + tr.setAttributeNS(null, 'style', 'opacity: 50%'); + } + } + + return true; + } + + const iq = $iq({type: 'get'}) + .c('query', {xmlns: NS.roster}); + connection.sendIQ(iq, onRoster, onRosterError.bind(null, 'roster query failed.')); + + const presence = $pres(); + connection.addHandler(onPresence, null, 'presence'); + connection.send(presence); + } +}); -- GitLab From 37f231e3615358e23ea6ccead39af1ba4978d9f8 Mon Sep 17 00:00:00 2001 From: Emmanuel Gil Peyrot Date: Tue, 17 Mar 2020 21:56:27 +0100 Subject: [PATCH 16/20] Web: Start a Jingle session on click. --- src/extension/plugins/xmpp/web/svg.js | 85 +++++++++++++++++++++++---- 1 file changed, 72 insertions(+), 13 deletions(-) diff --git a/src/extension/plugins/xmpp/web/svg.js b/src/extension/plugins/xmpp/web/svg.js index 8f22fc21ce..d63b51ea23 100644 --- a/src/extension/plugins/xmpp/web/svg.js +++ b/src/extension/plugins/xmpp/web/svg.js @@ -15,6 +15,8 @@ const NS = { hashes: 'urn:xmpp:hashes:2', xhtml: 'http://www.w3.org/1999/xhtml', svg: 'http://www.w3.org/2000/svg', + jingle: 'urn:xmpp:jingle:1', + jingle_sxe: 'urn:xmpp:jingle:transports:sxe', }; function nsResolver(prefix) { @@ -187,7 +189,69 @@ document.addEventListener('DOMContentLoaded', function () { function initRoster(connection) { + const resources_dict = {}; const tr_dict = {}; + const [bare, resource] = connection.jid.split('/', 2); + addContact(bare); + + function onJingle(result_iq) + { + // TODO: don’t assume this is a session-accept. + console.log('Assuming session-accept:', result_iq); + const jid = result_iq.getAttributeNS(null, 'from'); + const [bare, resource] = jid.split('/', 2); + const message = $msg({to: jid}) + .c('sxe', {xmlns: 'urn:xmpp:sxe:0', id: 'what-should-this-be?', session: 'foo'}) + .c('connect'); + connection.send(message); + } + + function onJingleError(string) + { + console.log('Failed to initiate Jingle session: ' + string); + } + + function addContact(jid) + { + const tr = document.createElementNS(NS.xhtml, 'tr'); + tr.setAttributeNS(null, 'style', 'opacity: 50%'); + resources_dict[jid] = {}; + console.log(resources_dict); + tr_dict[jid] = tr; + const td = document.createElementNS(NS.xhtml, 'td'); + const a = document.createElementNS(NS.xhtml, 'a'); + a.setAttributeNS(null, 'href', 'xmpp:' + jid); + a.addEventListener('click', function(evt) { + evt.preventDefault(); + const bare = evt.target.href.substr(5); + console.log(bare); + const resources = resources_dict[bare]; + let good_resource = null; + for (let resource in resources) { + if (resources[resource]) { + good_resource = resource; + break; + } + } + if (good_resource === null) + return; + const jid = bare + '/' + good_resource; + const iq = $iq({type: 'set', to: jid}) + .c('jingle', {xmlns: NS.jingle, action: 'session-initiate', initiator: connection.jid, sid: 'foo'}) + .c('content', {creator: 'initiator', name: 'collaborative edition'}) + // XXX: Standardise the application namespaces! + .c('description', {xmlns: 'urn:xmpp:jingle:apps:svg'}).up() + .c('transport', {xmlns: NS.jingle_sxe}) + .c('host') + .t(jid) + connection.sendIQ(iq, onJingle, onJingleError.bind(null, 'Jingle session-initiate failed.')); + }); + const text = document.createTextNode(jid); + a.appendChild(text); + td.appendChild(a); + tr.appendChild(td); + roster_table.lastChild.appendChild(tr); + } function onRoster(result_iq) { @@ -201,18 +265,7 @@ document.addEventListener('DOMContentLoaded', function () { const name = item.getAttributeNS(null, 'name'); const groups = item.children; console.log("got contact:", jid, subscription, name, groups); - - const tr = document.createElementNS(NS.xhtml, 'tr'); - tr.setAttributeNS(null, 'style', 'opacity: 50%'); - tr_dict[jid] = tr; - const td = document.createElementNS(NS.xhtml, 'td'); - const a = document.createElementNS(NS.xhtml, 'a'); - a.setAttributeNS(null, 'href', 'xmpp:' + jid); - const text = document.createTextNode(jid); - a.appendChild(text); - td.appendChild(a); - tr.appendChild(td); - roster_table.lastChild.appendChild(tr); + addContact(jid); } } @@ -239,7 +292,9 @@ document.addEventListener('DOMContentLoaded', function () { has_sxe = true; } if (has_jingle_transport && has_sxe) { - console.log("Hello Inkscape!"); + console.log('Hello Inkscape!', jid); + resources_dict[bare][resource] = true; + console.log(resources_dict); } } @@ -250,6 +305,10 @@ document.addEventListener('DOMContentLoaded', function () { const jid = presence.getAttributeNS(null, 'from'); const [bare, resource] = jid.split('/', 2); + if (resource !== undefined) { + resources_dict[bare][resource] = false; + console.log(resources_dict); + } const type = presence.getAttributeNS(null, 'type'); console.log("got presence:", bare, resource, type); -- GitLab From 562b33344883496ef0a3d2f67de88d07b8f2d959 Mon Sep 17 00:00:00 2001 From: Emmanuel Gil Peyrot Date: Tue, 17 Mar 2020 22:04:28 +0100 Subject: [PATCH 17/20] =?UTF-8?q?Web:=20Add=20Strophe.js=E2=80=99s=20SPDX-?= =?UTF-8?q?license=20field.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also allow MIT files in Inkscape. --- buildtools/check_license_headers.py | 1 + src/extension/plugins/xmpp/web/index.xhtml | 1 + src/extension/plugins/xmpp/web/strophe.js | 1 + 3 files changed, 3 insertions(+) diff --git a/buildtools/check_license_headers.py b/buildtools/check_license_headers.py index cf6796c454..dc4a74a64f 100755 --- a/buildtools/check_license_headers.py +++ b/buildtools/check_license_headers.py @@ -79,6 +79,7 @@ PERMITTED_LICENSES = [ "LGPL-2.1-or-later", "LGPL-3.0-or-later", "CC0", + "MIT", ] diff --git a/src/extension/plugins/xmpp/web/index.xhtml b/src/extension/plugins/xmpp/web/index.xhtml index b11f25044f..2bba6cf556 100644 --- a/src/extension/plugins/xmpp/web/index.xhtml +++ b/src/extension/plugins/xmpp/web/index.xhtml @@ -1,4 +1,5 @@ + SVG viewer diff --git a/src/extension/plugins/xmpp/web/strophe.js b/src/extension/plugins/xmpp/web/strophe.js index bd95f2c328..2cf74faa4e 100644 --- a/src/extension/plugins/xmpp/web/strophe.js +++ b/src/extension/plugins/xmpp/web/strophe.js @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: MIT (function webpackUniversalModuleDefinition(root, factory) { if(typeof exports === 'object' && typeof module === 'object') module.exports = factory(); -- GitLab From 91564c5e2d350bbec9ada26c319aa2836b5caf3d Mon Sep 17 00:00:00 2001 From: Emmanuel Gil Peyrot Date: Mon, 23 Mar 2020 14:54:21 +0100 Subject: [PATCH 18/20] Web: Allow receiving state change messages. --- src/extension/plugins/xmpp/web/index.xhtml | 2 +- src/extension/plugins/xmpp/web/svg.js | 99 ++++++++++++++++++++-- 2 files changed, 94 insertions(+), 7 deletions(-) diff --git a/src/extension/plugins/xmpp/web/index.xhtml b/src/extension/plugins/xmpp/web/index.xhtml index 2bba6cf556..b694abf1bb 100644 --- a/src/extension/plugins/xmpp/web/index.xhtml +++ b/src/extension/plugins/xmpp/web/index.xhtml @@ -22,7 +22,7 @@ - +
diff --git a/src/extension/plugins/xmpp/web/svg.js b/src/extension/plugins/xmpp/web/svg.js index d63b51ea23..94371132d2 100644 --- a/src/extension/plugins/xmpp/web/svg.js +++ b/src/extension/plugins/xmpp/web/svg.js @@ -17,6 +17,7 @@ const NS = { svg: 'http://www.w3.org/2000/svg', jingle: 'urn:xmpp:jingle:1', jingle_sxe: 'urn:xmpp:jingle:transports:sxe', + sxe: 'urn:xmpp:sxe:0', }; function nsResolver(prefix) { @@ -84,6 +85,7 @@ document.addEventListener('DOMContentLoaded', function () { const connected_div = document.getElementById('connected'); const roster_table = document.getElementById('roster'); + const canvas_div = document.getElementById('canvas'); const avatar_img = document.getElementById('avatar'); @@ -104,7 +106,7 @@ document.addEventListener('DOMContentLoaded', function () { connection = new Strophe.Connection(bosh_service); connection.rawInput = rawInput; connection.rawOutput = rawOutput; - connection.connect(jid, + connection.connect(jid + '/coucou', pass_element.value, onConnect); }); @@ -191,6 +193,7 @@ document.addEventListener('DOMContentLoaded', function () { { const resources_dict = {}; const tr_dict = {}; + const rid_dict = {}; const [bare, resource] = connection.jid.split('/', 2); addContact(bare); @@ -200,7 +203,7 @@ document.addEventListener('DOMContentLoaded', function () { console.log('Assuming session-accept:', result_iq); const jid = result_iq.getAttributeNS(null, 'from'); const [bare, resource] = jid.split('/', 2); - const message = $msg({to: jid}) + const message = $msg({to: jid, type: 'chat', id: 'connect'}) .c('sxe', {xmlns: 'urn:xmpp:sxe:0', id: 'what-should-this-be?', session: 'foo'}) .c('connect'); connection.send(message); @@ -293,6 +296,8 @@ document.addEventListener('DOMContentLoaded', function () { } if (has_jingle_transport && has_sxe) { console.log('Hello Inkscape!', jid); + const tr = tr_dict[bare]; + tr.setAttributeNS(null, 'style', 'opacity: 100%'); resources_dict[bare][resource] = true; console.log(resources_dict); } @@ -316,9 +321,6 @@ document.addEventListener('DOMContentLoaded', function () { // TODO: handle more than one resource per contact. const tr = tr_dict[bare]; if (type === null) { - if (tr !== undefined) { - tr.setAttributeNS(null, 'style', 'opacity: 100%'); - } const caps = parseXPath(presence, './caps:c', XPathResult.ORDERED_NODE_ITERATOR_TYPE).iterateNext(); if (caps !== null) { const node = caps.getAttributeNS(null, 'node'); @@ -340,7 +342,17 @@ document.addEventListener('DOMContentLoaded', function () { } */ } else if (type === 'unavailable') { - if (tr !== undefined) { + const resources = resources_dict[bare]; + delete resources[resource]; + let disconnect = true; + for (let resource in resources) { + if (resources[resource]) { + disconnect = true; + break; + } + } + console.log('disconnect?', disconnect); + if (tr !== undefined && disconnect) { tr.setAttributeNS(null, 'style', 'opacity: 50%'); } } @@ -348,6 +360,79 @@ document.addEventListener('DOMContentLoaded', function () { return true; } + function onMessage(message) + { + const jid = message.getAttributeNS(null, 'from'); + const [bare, resource] = jid.split('/', 2); + if (resource !== undefined) { + resources_dict[bare][resource] = false; + console.log(resources_dict); + } + const type = message.getAttributeNS(null, 'type'); + console.log("got message:", bare, resource, type); + + const sxe = parseXPath(message, './sxe:sxe', XPathResult.ORDERED_NODE_ITERATOR_TYPE).iterateNext(); + console.log(sxe); + if (sxe !== null) { + const child = sxe.firstChild; + console.log(child); + if (child.localName == 'state-offer') { + const message = $msg({to: jid, type: 'chat', id: 'state-accept'}) + .c('sxe', {xmlns: 'urn:xmpp:sxe:0', id: 'coucou', session: 'foo'}) + .c('accept-state'); + connection.send(message); + } else if (child.localName == 'state') { + for (let change of child.children) { + console.log(change); + if (change.localName == 'new') { + const rid = change.getAttributeNS(null, 'rid'); + const type = change.getAttributeNS(null, 'type'); + const version = change.getAttributeNS(null, 'version'); + let parent = change.getAttributeNS(null, 'parent'); + const primary_weight = change.getAttributeNS(null, 'primary-weight'); + const ns = change.getAttributeNS(null, 'ns'); + const name = change.getAttributeNS(null, 'name'); + const chdata = change.getAttributeNS(null, 'chdata'); + const pitarget = change.getAttributeNS(null, 'pitarget'); + const pidata = change.getAttributeNS(null, 'pidata'); + const creator = change.getAttributeNS(null, 'creator'); + const last_modified_by = change.getAttributeNS(null, 'last-modified-by'); + + console.log('parent', parent); + if (parent !== null) { + parent = rid_dict[parent]; + if (parent === null) { + console.warn('Invalid parent!'); + return; + } + } else + parent = canvas_div; + console.log('parent2', parent); + + if (type == 'element') { + const elem = document.createElementNS(ns, name); + parent.appendChild(elem); + rid_dict[rid] = elem; + } else if (type == 'attr') { + parent.setAttributeNS(ns, name, chdata); + // TODO: store rid. + } else if (type == 'text') { + const text = document.createTextNode(chdata); + parent.appendChild(text); + rid_dict[rid] = text; + } else if (type == 'comment') { + } else if (type == 'processinginstruction') { + } else { + // TODO: Invalid! + } + } + } + } + } + + return true; + } + const iq = $iq({type: 'get'}) .c('query', {xmlns: NS.roster}); connection.sendIQ(iq, onRoster, onRosterError.bind(null, 'roster query failed.')); @@ -355,5 +440,7 @@ document.addEventListener('DOMContentLoaded', function () { const presence = $pres(); connection.addHandler(onPresence, null, 'presence'); connection.send(presence); + + connection.addHandler(onMessage, null, 'message'); } }); -- GitLab From 9d02e942e944c44c02352123b92814ec3513b4c3 Mon Sep 17 00:00:00 2001 From: Marc Jeanmougin Date: Thu, 25 Jan 2024 17:28:33 +0100 Subject: [PATCH 19/20] add gloox file --- src/extension/plugins/grid2/CMakeLists.txt | 2 + src/extension/plugins/xmpp/CMakeLists.txt | 5 +- src/extension/plugins/xmpp/sxe.cpp | 196 +++++++++++++++++++++ src/extension/plugins/xmpp/sxe.h | 168 ++++++++++++++++++ src/extension/plugins/xmpp/xmpp.cpp | 87 ++++----- src/extension/plugins/xmpp/xmpp.h | 11 +- 6 files changed, 418 insertions(+), 51 deletions(-) create mode 100644 src/extension/plugins/xmpp/sxe.cpp create mode 100644 src/extension/plugins/xmpp/sxe.h diff --git a/src/extension/plugins/grid2/CMakeLists.txt b/src/extension/plugins/grid2/CMakeLists.txt index 1d23d8384e..f6c40ba14b 100644 --- a/src/extension/plugins/grid2/CMakeLists.txt +++ b/src/extension/plugins/grid2/CMakeLists.txt @@ -4,6 +4,8 @@ set(grid_PART_SRCS grid.cpp) include_directories( ${CMAKE_BINARY_DIR}/src ) add_library(grid2 SHARED EXCLUDE_FROM_ALL ${grid_PART_SRCS}) +install(FILES libgrid2.inx DESTINATION ${INKSCAPE_SHARE_INSTALL}/extensions/) +install(TARGETS grid2 DESTINATION ${INKSCAPE_SHARE_INSTALL}/extensions/) target_link_libraries(grid2 inkscape_base) diff --git a/src/extension/plugins/xmpp/CMakeLists.txt b/src/extension/plugins/xmpp/CMakeLists.txt index 64c67c43f9..9d8e8eb54d 100644 --- a/src/extension/plugins/xmpp/CMakeLists.txt +++ b/src/extension/plugins/xmpp/CMakeLists.txt @@ -3,9 +3,12 @@ find_package(PkgConfig) if(PkgConfig_FOUND) pkg_check_modules(Gloox gloox) if(Gloox_FOUND) - set(xmpp_PART_SRCS xmpp.cpp xmpp.h) + set(xmpp_PART_SRCS xmpp.cpp xmpp.h sxe.h sxe.cpp) include_directories(${CMAKE_BINARY_DIR}/src) add_library(xmpp SHARED EXCLUDE_FROM_ALL ${xmpp_PART_SRCS}) + install(FILES libxmpp.inx DESTINATION ${INKSCAPE_SHARE_INSTALL}/extensions/) + install(TARGETS xmpp DESTINATION ${INKSCAPE_SHARE_INSTALL}/extensions/) + target_include_directories(xmpp PRIVATE ${Gloox_INCLUDE_DIRS}) target_link_libraries(xmpp inkscape_base ${Gloox_LIBRARIES}) diff --git a/src/extension/plugins/xmpp/sxe.cpp b/src/extension/plugins/xmpp/sxe.cpp new file mode 100644 index 0000000000..c576d9a3d2 --- /dev/null +++ b/src/extension/plugins/xmpp/sxe.cpp @@ -0,0 +1,196 @@ +// SPDX-License-Identifier: GPL-3.0 +/* + Copyright (c) 2019-2023 by Jakob Schröter + This file is part of the gloox library. http://camaya.net/gloox + + This software is distributed under a license. The full license + agreement can be found in the file LICENSE in this distribution. + This software may not be copied, modified, sold or distributed + other than expressed in the named license agreement. + + This software is distributed without any warranty. +*/ + +#include "sxe.h" +#include + +#include +#include + +namespace gloox +{ + + Sxe::Sxe( const Tag* tag ) + : StanzaExtension( ExtUser+1 ) + { + if( !tag || tag->name() != "sxe" || tag->xmlns() != XMLNS_SXE ) + return; + + m_session = tag->findAttribute( "session" ); + m_id = tag->findAttribute( "id" ); + const TagList& l = tag->children(); + for( TagList::const_iterator it = l.begin(); it != l.end(); ++it ) + { + const Tag* child = *it; + if( child->xmlns() != XMLNS_SXE ) + break; + + if( child->name() == "connect" ) + m_type = SxeConnect; + else if( child->name() == "state-offer" ) + { + const TagList& l = child->children(); + for( TagList::const_iterator it = l.begin(); it != l.end(); ++it ) + { + const Tag* description = *it; + if( description->name() != "description" ) + return; + m_state_offer_xmlns.push_back( description->xmlns() ); + } + m_type = SxeStateOffer; + } + else if( child->name() == "accept-state" ) + m_type = SxeAcceptState; + else if( child->name() == "refuse-state" ) + m_type = SxeRefuseState; + else if( child->name() == "state" ) + { + const TagList& l = child->children(); + for( TagList::const_iterator it = l.begin(); it != l.end(); ++it ) + { + const Tag* child2 = *it; + if( child2->xmlns() != XMLNS_SXE ) + return; + + StateChange change; + if( child2->name() == "document-begin" ) + { + change.type = StateChangeDocumentBegin; + change.document_begin.prolog = child2->findAttribute( "prolog" ).c_str(); + } + else if( child2->name() == "document-end" ) + { + change.type = StateChangeDocumentEnd; + change.document_end.last_sender = child2->findAttribute( "last-sender" ).c_str(); + change.document_end.last_id = child2->findAttribute( "last-id" ).c_str(); + } + else if( child2->name() == "new" ) + { + change.type = StateChangeNew; + change.new_.rid = child2->findAttribute( "rid" ).c_str(); + change.new_.type = child2->findAttribute( "type" ).c_str(); + change.new_.name = child2->findAttribute( "name" ).c_str(); + change.new_.ns = child2->findAttribute( "ns" ).c_str(); + change.new_.chdata = child2->findAttribute( "chdata" ).c_str(); + } + else if( child2->name() == "remove" ) + { + change.type = StateChangeRemove; + change.remove.target = child2->findAttribute( "target" ).c_str(); + } + else if( child2->name() == "set" ) + { + change.type = StateChangeSet; + change.set.target = child2->findAttribute( "target" ).c_str(); + change.set.version = child2->findAttribute( "version" ).c_str(); + change.set.name = child2->findAttribute( "name" ).c_str(); + change.set.ns = child2->findAttribute( "ns" ).c_str(); + change.set.chdata = child2->findAttribute( "chdata" ).c_str(); + } + else + { + return; + } + m_state_changes.push_back( change ); + } + m_type = SxeState; + } + } + } + + Tag* Sxe::tag() const + { + if ( m_type == SxeInvalid ) + return 0; + + Tag* t = new Tag( "sxe" ); + t->setXmlns( XMLNS_SXE ); + t->addAttribute( "session", m_session ); + t->addAttribute( "id", m_id ); + + if( m_type == SxeConnect ) + new Tag( t, "connect" ); + else if( m_type == SxeAcceptState ) + new Tag( t, "accept-state" ); + else if( m_type == SxeRefuseState ) + new Tag( t, "refuse-state" ); + else if( m_type == SxeStateOffer ) + { + Tag* child = new Tag( t, "state-offer" ); + child->setXmlns( XMLNS_SXE ); + for( std::vector::const_iterator it = m_state_offer_xmlns.begin(); it != m_state_offer_xmlns.end(); ++it ) + { + const std::string& xmlns = *it; + Tag* description = new Tag( child, "description" ); + description->setXmlns( xmlns ); + } + } + else if( m_type == SxeState ) + { + Tag* state = new Tag( t, "state" ); + state->setXmlns( XMLNS_SXE ); + for( std::vector::const_iterator it = m_state_changes.begin(); it != m_state_changes.end(); ++it ) + { + const StateChange& change = *it; + Tag* child; + if( change.type == StateChangeDocumentBegin ) + { + child = new Tag( state, "document-begin" ); + child->addAttribute( "prolog", change.document_begin.prolog ); + } + else if( change.type == StateChangeDocumentEnd ) + { + child = new Tag( state, "document-end" ); + child->addAttribute( "last-sender", change.document_end.last_sender ); + child->addAttribute( "last-id", change.document_end.last_id ); + } + else if( change.type == StateChangeNew ) + { + child = new Tag( state, "new" ); + child->addAttribute( "rid", change.new_.rid ); + child->addAttribute( "type", change.new_.type ); + child->addAttribute( "name", change.new_.name ); + child->addAttribute( "ns", change.new_.ns ); + child->addAttribute( "chdata", change.new_.chdata ); + } + else if( change.type == StateChangeRemove ) + { + child = new Tag( state, "remove" ); + child->addAttribute( "target", change.remove.target ); + } + else if( change.type == StateChangeSet ) + { + child = new Tag( state, "set" ); + child->addAttribute( "target", change.set.target ); + child->addAttribute( "version", change.set.version ); + child->addAttribute( "name", change.set.name ); + child->addAttribute( "ns", change.set.ns ); + child->addAttribute( "chdata", change.set.chdata ); + } + else + { + return 0; + } + } + } + + return t; + } + + const std::string& Sxe::filterString() const + { + static const std::string filter = "/message/sxe[@xmlns='" + XMLNS_SXE + "']"; + return filter; + } + +} diff --git a/src/extension/plugins/xmpp/sxe.h b/src/extension/plugins/xmpp/sxe.h new file mode 100644 index 0000000000..c78161b9ba --- /dev/null +++ b/src/extension/plugins/xmpp/sxe.h @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: GPL-3.0 +/* + Copyright (c) 2019-2024 by Jakob Schröter + Copyright (c) 2019-2024 by Emmanuel Gil Peyrot + Copyright (c) 2024 by Marc Jeanmougin + This file is part of the gloox library. http://camaya.net/gloox + + This software is distributed under a license. The full license + agreement can be found in the file LICENSE in this distribution. + This software may not be copied, modified, sold or distributed + other than expressed in the named license agreement. + + This software is distributed without any warranty. +*/ + +#ifndef SXE_H__ +#define SXE_H__ + +#include + +#include +#include +#include +const std::string XMLNS_SXE = "urn:xmpp:sxe:0"; + +namespace gloox +{ + + /** + * @brief An implementation/abstraction of Shared XML Editing (SXE, @xep{0284}) + * + * XEP Version: 0.1.1 + * + * @author Emmanuel Gil Peyrot + * @since 1.0.23 + */ + class GLOOX_API Sxe : public StanzaExtension + { + public: + + enum SxeType + { + SxeInvalid, + SxeConnect, + SxeStateOffer, + SxeAcceptState, + SxeRefuseState, + SxeState + }; + + enum StateChangeType + { + StateChangeDocumentBegin, + StateChangeDocumentEnd, + StateChangeNew, + StateChangeRemove, + StateChangeSet + }; + + struct DocumentBegin + { + const char* prolog; + }; + + struct DocumentEnd + { + const char* last_sender; + const char* last_id; + }; + + struct New + { + const char* rid; + const char* type; + const char* name; + const char* ns; + const char* parent; + const char* chdata; + }; + + struct Remove + { + const char* target; + }; + + struct Set + { + const char* target; + const char* version; + const char* parent; + const char* name; + const char* ns; + const char* chdata; + }; + + struct StateChange + { + StateChangeType type; + union + { + DocumentBegin document_begin; + DocumentEnd document_end; + New new_; + Remove remove; + Set set; + }; + }; + + /** + * Constructs a new object of the given type. + * @param state_changes The changes. + */ + Sxe( std::string session, std::string id, SxeType type, std::vector state_offer_xmlns, std::vector state_changes ) + : StanzaExtension(ExtUser+1), m_session(session), m_id(id), m_type(type), m_state_offer_xmlns(state_offer_xmlns), m_state_changes(state_changes) {}; + + /** + * Creates a new SXE object from the given Tag. + * @param tag The Tag to parse. + */ + Sxe( const Tag* tag = 0 ); + + /** + * Virtual destructor. + */ + virtual ~Sxe() {} + + /** + * Returns a Tag representing a SXE extension. + * @return A Tag representing a SXE extension. + */ + virtual Tag* tag() const; + + /** + * Returns a new instance of SXE. + * @return The new SXE instance. + */ + virtual StanzaExtension* newInstance( const Tag* tag ) const + { + return new Sxe( tag ); + } + + /** + * Returns an identical copy of the current SXE. + * @return an identical copy of the current SXE. + */ + virtual StanzaExtension* clone() const + { + return new Sxe( *this ); + } + + /** + * Returns an XPath expression that describes a path to the SXE element. + * @return The SXE filter string. + */ + virtual const std::string& filterString() const; + + private: + std::string m_session; + std::string m_id; + SxeType m_type; + std::vector m_state_offer_xmlns; + std::vector m_state_changes; + + }; + +} + +#endif // SXE_H__ diff --git a/src/extension/plugins/xmpp/xmpp.cpp b/src/extension/plugins/xmpp/xmpp.cpp index fecd242936..b2798a3f53 100644 --- a/src/extension/plugins/xmpp/xmpp.cpp +++ b/src/extension/plugins/xmpp/xmpp.cpp @@ -18,9 +18,8 @@ #include #include #include -#include -#include -#include +#undef lookup +#include "sxe.h" #include #include @@ -28,7 +27,8 @@ #include "desktop.h" -#include "2geom/geom.h" +#include <2geom/geom.h> +#include "event.h" #include "document.h" #include "object/sp-object.h" #include "selection.h" @@ -105,8 +105,7 @@ public: InkscapeClient::InkscapeClient(JID jid, const std::string& password) { - // TODO: use std::make_unique() once we’re C++14. - client = std::unique_ptr(new Client(jid, password)); + client = std::make_unique(jid, password); // TODO: figure out why SCRAM-SHA-1 isn’t working. client->setSASLMechanisms(SaslMechPlain); // TODO: fetch the OS properly, instead of hardcoding it to Linux. @@ -114,8 +113,6 @@ InkscapeClient::InkscapeClient(JID jid, const std::string& password) client->disco()->setIdentity("collaboration", "whiteboard", "Inkscape"); session_manager = std::unique_ptr(new Jingle::SessionManager(client.get(), this)); session_manager->registerPlugin(new Jingle::Content()); - session_manager->registerPlugin(new Jingle::SxePlugin()); - sxe_manager = std::unique_ptr(new SxeSessionManager(client.get(), this)); client->registerConnectionListener(this); //client->logInstance().registerLogHandler(LogLevelDebug, LogAreaXmlOutgoing | LogAreaXmlIncoming, this); client->logInstance().registerLogHandler(LogLevelDebug, ~0, this); @@ -150,7 +147,9 @@ void InkscapeClient::send(Tag *tag) void InkscapeClient::sendChanges(JID recipient, std::string& sid, std::vector state_changes) { - sxe_manager->sendChanges(recipient, sid, state_changes); + //TODO + // client->send(something) + //TODO sxe_manager->sendChanges(recipient, sid, state_changes); } int InkscapeClient::runLoop(void *data) @@ -221,8 +220,6 @@ void InkscapeClient::handleSessionAction(Jingle::Action action, Jingle::Session* if (true/*accept*/) { std::list plugins_list; SvgApplication* description = new SvgApplication(); - Jingle::SxePlugin* transport = new Jingle::SxePlugin(client->jid()); - plugins_list.push_front(transport); plugins_list.push_front(description); bool ret = session->sessionAccept(new Jingle::Content(name, plugins_list)); printf("accepted? %d\n", ret); @@ -248,27 +245,27 @@ void InkscapeClient::handleIncomingSession(Jingle::Session* session) { printf("handleIncomingSession(session=%p)\n", session); } - -std::vector InkscapeClient::getCurrentState(const std::string& session, const std::string& id) +/* +std::vector InkscapeClient::getCurrentState(const std::string& session, const std::string& id) { printf("getCurrentState(session=%s, id=%s)\n", session.c_str(), id.c_str()); - std::vector state; + std::vector state; Sxe::DocumentBegin document_begin = { .prolog = "data:image/svg+xml,", }; - Sxe::StateChange begin = { - .type = Sxe::StateChangeDocumentBegin, + StateChangeType begin = { + .type = StateChangeTypeDocumentBegin, .document_begin = document_begin, }; state.push_back(begin); m_rid = get_uuid(); - Sxe::New new_ = Sxe::New::Element( - /*rid*/ m_rid.c_str(), - /*ns*/ "http://www.w3.org/2000/svg", - /*name*/ "svg" + gloox::New new_ = gloox::New::Element( + /*rid* / m_rid.c_str(), + /*ns* / "http://www.w3.org/2000/svg", + /*name* / "svg" ); - Sxe::StateChange change = { - .type = Sxe::StateChangeNew, + StateChangeType change = { + .type = StateChangeTypeNew, .new_ = new_, }; state.push_back(change); @@ -276,13 +273,13 @@ std::vector InkscapeClient::getCurrentState(const std::string& .last_sender = "foo@bar/baz", .last_id = "unknown", }; - Sxe::StateChange end = { - .type = Sxe::StateChangeDocumentEnd, + StateChangeType end = { + .type = StateChangeTypeDocumentEnd, .document_end = document_end, }; state.push_back(end); return state; -} +}*/ void XMPPObserver::notifyUndoCommitEvent(Event *ee) { @@ -314,37 +311,41 @@ void XMPPObserver::notifyUndoCommitEvent(Event *ee) } name = name.substr(4); - Sxe::New new_ = Sxe::New::Element( + Sxe::New new_ = { /*rid*/ rid.c_str(), /*parent*/ client->m_rid.c_str(), /*ns*/ "http://www.w3.org/2000/svg", - /*name*/ name.c_str() - ); + /*name*/ name.c_str(), + "", + "" + }; Sxe::StateChange change = { .type = Sxe::StateChangeNew, - .new_ = new_, + .new_ = new_ }; std::vector state_changes = {}; state_changes.push_back(change); // XXX: huge hack to keep all rids on the stack… size_t num_attrs = 0; - for (Util::List it = node->attributeList(); it; ++it) { + for (auto it : node->attributeList()) { ++num_attrs; } std::vector rids; rids.reserve(num_attrs); size_t cur_attr = 0; - for (Util::List it = node->attributeList(); it; ++it) { + for (auto it : node->attributeList()) { rids.push_back(get_uuid()); - Sxe::New new_ = Sxe::New::Attr( + Sxe::New new_ = { /*rid*/ rids[cur_attr].c_str(), /*parent*/ rid.c_str(), - /*name*/ g_quark_to_string(it->key), - /*chdata*/ it->value - ); + /*name*/ g_quark_to_string(it.key), + /*chdata*/ it.value, + "", + "" + }; Sxe::StateChange change = { .type = Sxe::StateChangeNew, .new_ = new_, @@ -354,13 +355,13 @@ void XMPPObserver::notifyUndoCommitEvent(Event *ee) } std::vector copy(state_changes); - Sxe* coucou = new Sxe("session", "id", copy); + Sxe* coucou = new Sxe("session", "id", Sxe::SxeStateOffer, {XMLNS_SXE}, copy); fprintf(stderr, "coucou: %s\n", coucou->tag()->xml().c_str()); //Message msg(Message::Normal, JID("linkmauve@linkmauve.fr")); //msg.addExtension(new Sxe("session", "id", Sxe::TypeState, {}, state_changes)); std::string sid = "foo"; - client->sendChanges(JID("test@linkmauve.fr/coucou"), sid, state_changes); + client->sendChanges(JID("test@xmpp.r2.enst.fr/test2"), sid, state_changes); printf("coucou\n"); } else if ((edel = dynamic_cast(e))) { std::cout << "EventDel" << std::endl; @@ -376,8 +377,8 @@ void XMPPObserver::notifyUndoCommitEvent(Event *ee) std::vector state_changes = {}; state_changes.push_back(change); - Message msg(Message::Normal, JID("linkmauve@linkmauve.fr")); - msg.addExtension(new Sxe("session", "id", Sxe::TypeState, {}, state_changes)); + Message msg(Message::Normal, JID("test@xmpp.r2.enst.fr")); + msg.addExtension(new Sxe("session", "id", Sxe::SxeState, {}, state_changes)); client->send(msg.tag()); } else if ((echga = dynamic_cast(e))) { std::cout << "EventChgAttr" << std::endl; @@ -423,7 +424,7 @@ bool XMPP::load(Inkscape::Extension::Extension * /*module*/) enabled = false; // TODO: fetch these from the preferences. - JID jid("test@linkmauve.fr"); + JID jid("test@xmpp.r2.enst.fr"); const char *password = "test"; client = std::make_shared(jid, password); @@ -442,14 +443,14 @@ bool XMPP::load(Inkscape::Extension::Extension * /*module*/) \param module The effect that was called (unused) \param document What should be edited. */ -void XMPP::effect(Inkscape::Extension::Effect *module, Inkscape::UI::View::View *document, +void XMPP::effect(Inkscape::Extension::Effect *module, SPDesktop *desktop, Inkscape::Extension::Implementation::ImplementationDocumentCache * /*docCache*/) { std::cout << (enabled ? "disabling" : "enabling") << std::endl; if (!enabled) - document->doc()->addUndoObserver(*obs); + desktop->doc()->addUndoObserver(*obs); else - document->doc()->removeUndoObserver(*obs); + desktop->doc()->removeUndoObserver(*obs); enabled = !enabled; } diff --git a/src/extension/plugins/xmpp/xmpp.h b/src/extension/plugins/xmpp/xmpp.h index 3b9cda5986..5116afd3f9 100644 --- a/src/extension/plugins/xmpp/xmpp.h +++ b/src/extension/plugins/xmpp/xmpp.h @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-2.0-or-later /* - * Copyright (C) 2019 Marc Jeanmougin + * Copyright (C) 2019, 2024 Marc Jeanmougin * Copyright (C) 2020 Emmanuel Gil Peyrot * * Released under GNU GPL v2+, read the file 'COPYING' for more information. @@ -17,7 +17,6 @@ #include #include #include -#include #include #include #include @@ -38,7 +37,7 @@ class Extension; namespace Internal { -class InkscapeClient : gloox::ConnectionListener, gloox::LogHandler, gloox::Jingle::SessionHandler, gloox::SxeSessionHandler { +class InkscapeClient : gloox::ConnectionListener, gloox::LogHandler, gloox::Jingle::SessionHandler { public: InkscapeClient(gloox::JID jid, const std::string& password); bool connect(); @@ -63,13 +62,11 @@ private: void handleSessionActionError(gloox::Jingle::Action action, gloox::Jingle::Session* session, const gloox::Error* error) override; void handleIncomingSession(gloox::Jingle::Session* session) override; - // From SxeSessionHandler - std::vector getCurrentState(const std::string& session, const std::string& id) override; + // std::vector getCurrentState(const std::string& session, const std::string& id); private: std::unique_ptr client; std::unique_ptr session_manager; - std::unique_ptr sxe_manager; bool connected; // XXX: hack @@ -98,7 +95,7 @@ class XMPP : public Inkscape::Extension::Implementation::Implementation { public: bool load(Inkscape::Extension::Extension *module) override; - void effect(Inkscape::Extension::Effect *module, Inkscape::UI::View::View *document, Inkscape::Extension::Implementation::ImplementationDocumentCache * docCache) override; + void effect(Inkscape::Extension::Effect *module, SPDesktop *desktop, Inkscape::Extension::Implementation::ImplementationDocumentCache * docCache) override; private: std::unique_ptr obs; -- GitLab From 30ec84df7a2beab7ab6366fa111816bc1373cf7e Mon Sep 17 00:00:00 2001 From: Marc Jeanmougin Date: Fri, 26 Jan 2024 12:02:03 +0100 Subject: [PATCH 20/20] refactor extension start/stop --- src/extension/plugins/xmpp/libxmpp.inx | 7 +++++ src/extension/plugins/xmpp/xmpp.cpp | 43 ++++++++++++++------------ src/extension/plugins/xmpp/xmpp.h | 3 ++ 3 files changed, 34 insertions(+), 19 deletions(-) diff --git a/src/extension/plugins/xmpp/libxmpp.inx b/src/extension/plugins/xmpp/libxmpp.inx index 3db1b19bde..9bb931dd53 100644 --- a/src/extension/plugins/xmpp/libxmpp.inx +++ b/src/extension/plugins/xmpp/libxmpp.inx @@ -3,6 +3,13 @@ <_name>XMPP org.inkscape.collaboration.xmpp + test@xmpp.r2.enst.fr + test + isape@conf.xmpp.r2.enst.fr + + + + all diff --git a/src/extension/plugins/xmpp/xmpp.cpp b/src/extension/plugins/xmpp/xmpp.cpp index b2798a3f53..104f280595 100644 --- a/src/extension/plugins/xmpp/xmpp.cpp +++ b/src/extension/plugins/xmpp/xmpp.cpp @@ -99,7 +99,7 @@ public: // reimplemented from Plugin Jingle::Plugin* clone() const override { - return new SvgApplication(*this); + return new SvgApplication(*this); } }; @@ -361,7 +361,7 @@ void XMPPObserver::notifyUndoCommitEvent(Event *ee) //Message msg(Message::Normal, JID("linkmauve@linkmauve.fr")); //msg.addExtension(new Sxe("session", "id", Sxe::TypeState, {}, state_changes)); std::string sid = "foo"; - client->sendChanges(JID("test@xmpp.r2.enst.fr/test2"), sid, state_changes); + client->sendChanges(JID(client->document_jid), sid, state_changes); printf("coucou\n"); } else if ((edel = dynamic_cast(e))) { std::cout << "EventDel" << std::endl; @@ -377,7 +377,7 @@ void XMPPObserver::notifyUndoCommitEvent(Event *ee) std::vector state_changes = {}; state_changes.push_back(change); - Message msg(Message::Normal, JID("test@xmpp.r2.enst.fr")); + Message msg(Message::Normal, client->jid()); msg.addExtension(new Sxe("session", "id", Sxe::SxeState, {}, state_changes)); client->send(msg.tag()); } else if ((echga = dynamic_cast(e))) { @@ -416,25 +416,13 @@ XMPPObserver::XMPPObserver(std::shared_ptr client) /** \brief A function to allocated anything -- just an example here - \param module Unused \return Whether the load was successful */ -bool XMPP::load(Inkscape::Extension::Extension * /*module*/) +bool XMPP::load(Inkscape::Extension::Extension * module) { enabled = false; + _event_source = 0; - // TODO: fetch these from the preferences. - JID jid("test@xmpp.r2.enst.fr"); - const char *password = "test"; - - client = std::make_shared(jid, password); - client->connect(); - - // TODO: find a better way to integrate gloox’s fd into the main loop. - g_timeout_add(16, &InkscapeClient::runLoop, client.get()); - - obs = std::unique_ptr(new XMPPObserver(client)); - obs->writer = std::unique_ptr(new IO::StdWriter()); return TRUE; } @@ -447,10 +435,27 @@ void XMPP::effect(Inkscape::Extension::Effect *module, SPDesktop *desktop, Inkscape::Extension::Implementation::ImplementationDocumentCache * /*docCache*/) { std::cout << (enabled ? "disabling" : "enabling") << std::endl; - if (!enabled) + if (!enabled) { + JID jid(module->get_param_string("JID")); + const char *password = module->get_param_string("pw"); + + client = std::make_shared(jid, password); + client->document_jid = module->get_param_string("docJID"); + client->connect(); + + // TODO: find a better way to integrate gloox’s fd into the main loop. + _event_source = g_timeout_add(16, &InkscapeClient::runLoop, client.get()); + + obs = std::unique_ptr(new XMPPObserver(client)); + obs->writer = std::unique_ptr(new IO::StdWriter()); desktop->doc()->addUndoObserver(*obs); - else + } else { + g_source_remove(_event_source); + client->disconnect(); desktop->doc()->removeUndoObserver(*obs); + obs.reset(); + client.reset(); + } enabled = !enabled; } diff --git a/src/extension/plugins/xmpp/xmpp.h b/src/extension/plugins/xmpp/xmpp.h index 5116afd3f9..65b57db0c8 100644 --- a/src/extension/plugins/xmpp/xmpp.h +++ b/src/extension/plugins/xmpp/xmpp.h @@ -47,6 +47,7 @@ public: void send(gloox::Tag *tag); void sendChanges(gloox::JID recipient, std::string& sid, std::vector state_changes); static int runLoop(void *data); + gloox::JID jid() {return client->jid();} private: // From ConnectionListener @@ -72,6 +73,7 @@ private: // XXX: hack public: std::string m_rid; + std::string document_jid; }; @@ -100,6 +102,7 @@ public: private: std::unique_ptr obs; std::shared_ptr client; + unsigned int _event_source; bool enabled; }; -- GitLab