46f98b3142
The core plugins whose version was attached to GS's were reseted to 2.0.0. 2.0.0 was chosen as reset version for plugins because it is higher than the one that was set by inheriting GS version. Furthermore, it's a major change from prior plugin versioning system thus it also makes semantic sense. Justification for version bump: == GS == 9a4ab31f26 1.19.0c13b935201
1.18.3c13b935201
1.18.218fc39d2cf
1.18.1c083a8bcc2
1.18.0e8783d46d0
1.17.1d9a42550ff
1.17.01536d3ef29
1.16.0c03ed457a6
1.15.0d2e6519bad
1.14.2fe411e8138
1.14.1b17e0b4169
1.14.0daa5f87fd4
1.13.0d75b5d2f4a
1.11.7f6dbf66983
1.11.66cf674f8f8
1.11.57845a09b34
1.11.4e4d432295d
1.11.3339204f1ee
1.11.2a4e679a118
1.11.17967db6ff5
1.11.0bc030da320
1.10.19cc7df51d6
1.10.0bf7f17474d
1.9.28a07edec5f
1.9.10042971d74
1.9.06b5450b7e6
1.8.05dcc98d1c6
1.7.0e6667db0cd
1.6.03290227b50
1.5.0a59c439b46
1.4.0496ab8c920
1.3.10986030060b
1.3.91d529c021a
1.3.8f89c052cf8
1.3.738f2ecefac
1.3.6e473937cb9
1.3.59a39ebe66f
1.3.4ddc3cecfc0
1.3.32b43d484eb
1.3.2e8e487187e
1.3.1 == Plugins == XMPP plugine0887220b0
bump patche186ad57d0
bump patch OStatuse186ad57d0
bump patch Nodeinfoceae66a30f
bump minor586fb5a517
bump major195296846e
bump minor
482 lines
17 KiB
PHP
482 lines
17 KiB
PHP
<?php
|
|
/**
|
|
* StatusNet - the distributed open-source microblogging tool
|
|
* Copyright (C) 2009, StatusNet, Inc.
|
|
*
|
|
* Send and receive notices using the XMPP network
|
|
*
|
|
* PHP version 7
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
* @category IM
|
|
* @package StatusNet
|
|
* @author Evan Prodromou <evan@status.net>
|
|
* @copyright 2009 StatusNet, Inc.
|
|
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
|
|
* @link http://status.net/
|
|
*/
|
|
|
|
if (!defined('STATUSNET')) {
|
|
// This check helps protect against security problems;
|
|
// your code file can't be executed directly from the web.
|
|
exit(1);
|
|
}
|
|
|
|
/**
|
|
* Plugin for XMPP
|
|
*
|
|
* @category Plugin
|
|
* @package StatusNet
|
|
* @author Evan Prodromou <evan@status.net>
|
|
* @copyright 2009 StatusNet, Inc.
|
|
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
|
|
* @link http://status.net/
|
|
*/
|
|
class XmppPlugin extends ImPlugin
|
|
{
|
|
const PLUGIN_VERSION = '2.0.0';
|
|
|
|
public $server = null;
|
|
public $port = 5222;
|
|
public $user = 'update';
|
|
public $resource = 'gnusocial';
|
|
public $encryption = true;
|
|
public $password = null;
|
|
public $host = null; // only set if != server
|
|
public $debug = false; // print extra debug info
|
|
|
|
public $transport = 'xmpp';
|
|
|
|
function getDisplayName()
|
|
{
|
|
// TRANS: Plugin display name.
|
|
return _m('XMPP/Jabber');
|
|
}
|
|
|
|
function daemonScreenname()
|
|
{
|
|
$ret = $this->user . '@' . $this->server;
|
|
if ($this->resource) {
|
|
return $ret . '/' . $this->resource;
|
|
} else {
|
|
return $ret;
|
|
}
|
|
}
|
|
|
|
function validate($screenname)
|
|
{
|
|
return $this->validateBaseJid($screenname, common_config('email', 'check_domain'));
|
|
}
|
|
|
|
/**
|
|
* Checks whether a string is a syntactically valid base Jabber ID (JID).
|
|
* A base JID won't include a resource specifier on the end; since we
|
|
* take it off when reading input we can't really use them reliably
|
|
* to direct outgoing messages yet (sorry guys!)
|
|
*
|
|
* Note that a bare domain can be a valid JID.
|
|
*
|
|
* @param string $jid string to check
|
|
* @param bool $check_domain whether we should validate that domain...
|
|
*
|
|
* @return boolean whether the string is a valid JID
|
|
*/
|
|
protected function validateBaseJid($jid, $check_domain = false)
|
|
{
|
|
try {
|
|
$parts = $this->splitJid($jid);
|
|
if ($check_domain) {
|
|
if (!$this->checkDomain($parts['domain'])) {
|
|
return false;
|
|
}
|
|
}
|
|
return ($parts['resource'] === null); // missing; empty ain't kosher
|
|
} catch (Exception $e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Splits a Jabber ID (JID) into node, domain, and resource portions.
|
|
*
|
|
* Based on validation routine submitted by:
|
|
* @param string $jid string to check
|
|
*
|
|
* @return array with "node", "domain", and "resource" indices
|
|
* @throws Exception if input is not valid
|
|
* @license Licensed under ISC-L, which is compatible with everything else that keeps the copyright notice intact.
|
|
*
|
|
* @copyright 2009 Patrick Georgi <patrick@georgi-clan.de>
|
|
*/
|
|
protected function splitJid($jid)
|
|
{
|
|
$chars = '';
|
|
/* the following definitions come from stringprep, Appendix C,
|
|
which is used in its entirety by nodeprop, Chapter 5, "Prohibited Output" */
|
|
/* C1.1 ASCII space characters */
|
|
$chars .= "\x{20}";
|
|
/* C1.2 Non-ASCII space characters */
|
|
$chars .= "\x{a0}\x{1680}\x{2000}-\x{200b}\x{202f}\x{205f}\x{3000a}";
|
|
/* C2.1 ASCII control characters */
|
|
$chars .= "\x{00}-\x{1f}\x{7f}";
|
|
/* C2.2 Non-ASCII control characters */
|
|
$chars .= "\x{80}-\x{9f}\x{6dd}\x{70f}\x{180e}\x{200c}\x{200d}\x{2028}\x{2029}\x{2060}-\x{2063}\x{206a}-\x{206f}\x{feff}\x{fff9}-\x{fffc}\x{1d173}-\x{1d17a}";
|
|
/* C3 - Private Use */
|
|
$chars .= "\x{e000}-\x{f8ff}\x{f0000}-\x{ffffd}\x{100000}-\x{10fffd}";
|
|
/* C4 - Non-character code points */
|
|
$chars .= "\x{fdd0}-\x{fdef}\x{fffe}\x{ffff}\x{1fffe}\x{1ffff}\x{2fffe}\x{2ffff}\x{3fffe}\x{3ffff}\x{4fffe}\x{4ffff}\x{5fffe}\x{5ffff}\x{6fffe}\x{6ffff}\x{7fffe}\x{7ffff}\x{8fffe}\x{8ffff}\x{9fffe}\x{9ffff}\x{afffe}\x{affff}\x{bfffe}\x{bffff}\x{cfffe}\x{cffff}\x{dfffe}\x{dffff}\x{efffe}\x{effff}\x{ffffe}\x{fffff}\x{10fffe}\x{10ffff}";
|
|
/* C5 - Surrogate codes */
|
|
// We can't use preg_match to check this, fix below
|
|
// $chars .= "\x{d800}-\x{dfff}";
|
|
/* C6 - Inappropriate for plain text */
|
|
$chars .= "\x{fff9}-\x{fffd}";
|
|
/* C7 - Inappropriate for canonical representation */
|
|
$chars .= "\x{2ff0}-\x{2ffb}";
|
|
/* C8 - Change display properties or are deprecated */
|
|
$chars .= "\x{340}\x{341}\x{200e}\x{200f}\x{202a}-\x{202e}\x{206a}-\x{206f}";
|
|
/* C9 - Tagging characters */
|
|
$chars .= "\x{e0001}\x{e0020}-\x{e007f}";
|
|
|
|
/* Nodeprep forbids some more characters */
|
|
$nodeprepchars = $chars;
|
|
$nodeprepchars .= "\x{22}\x{26}\x{27}\x{2f}\x{3a}\x{3c}\x{3e}\x{40}";
|
|
|
|
$parts = explode("/", $jid, 2);
|
|
if (count($parts) > 1) {
|
|
$resource = $parts[1];
|
|
// if ($resource == '') then
|
|
// Warning: empty resource isn't legit.
|
|
// But if we're normalizing, we may as well take it...
|
|
} else {
|
|
$resource = null;
|
|
}
|
|
|
|
$node = explode("@", $parts[0]);
|
|
if ((count($node) > 2) || (count($node) == 0)) {
|
|
// TRANS: Exception thrown when using too many @ signs in a Jabber ID.
|
|
throw new Exception(_m('Invalid JID: too many @s.'));
|
|
} else if (count($node) == 1) {
|
|
$domain = $node[0];
|
|
$node = null;
|
|
} else {
|
|
$domain = $node[1];
|
|
$node = $node[0];
|
|
if ($node == '') {
|
|
// TRANS: Exception thrown when using @ sign not followed by a Jabber ID.
|
|
throw new Exception(_m('Invalid JID: @ but no node'));
|
|
}
|
|
}
|
|
|
|
if ($node !== null) {
|
|
// Length limits per http://xmpp.org/rfcs/rfc3920.html#addressing
|
|
if (strlen($node) > 1023) {
|
|
// TRANS: Exception thrown when using too long a Jabber ID (>1023).
|
|
throw new Exception(_m('Invalid JID: node too long.'));
|
|
}
|
|
// C5 - Surrogate codes is ensured by encoding check
|
|
if (preg_match("/[" . $nodeprepchars . "]/u", $node) || mb_detect_encoding($node, 'UTF-8', true) != 'UTF-8') {
|
|
// TRANS: Exception thrown when using an invalid Jabber ID.
|
|
// TRANS: %s is the invalid Jabber ID.
|
|
throw new Exception(sprintf(_m('Invalid JID node "%s".'), $node));
|
|
}
|
|
}
|
|
|
|
if (strlen($domain) > 1023) {
|
|
// TRANS: Exception thrown when using too long a Jabber domain (>1023).
|
|
throw new Exception(_m('Invalid JID: domain too long.'));
|
|
}
|
|
if (!common_valid_domain($domain)) {
|
|
// TRANS: Exception thrown when using an invalid Jabber domain name.
|
|
// TRANS: %s is the invalid domain name.
|
|
throw new Exception(sprintf(_m('Invalid JID domain name "%s".'), $domain));
|
|
}
|
|
|
|
if ($resource !== null) {
|
|
if (strlen($resource) > 1023) {
|
|
// TRANS: Exception thrown when using too long a resource (>1023).
|
|
throw new Exception("Invalid JID: resource too long.");
|
|
}
|
|
if (preg_match("/[" . $chars . "]/u", $resource)) {
|
|
// TRANS: Exception thrown when using an invalid Jabber resource.
|
|
// TRANS: %s is the invalid resource.
|
|
throw new Exception(sprintf(_m('Invalid JID resource "%s".'), $resource));
|
|
}
|
|
}
|
|
|
|
return array('node' => is_null($node) ? null : mb_strtolower($node),
|
|
'domain' => is_null($domain) ? null : mb_strtolower($domain),
|
|
'resource' => $resource);
|
|
}
|
|
|
|
/**
|
|
* Check if this domain's got some legit DNS record
|
|
* @param $domain
|
|
* @return bool
|
|
*/
|
|
protected function checkDomain($domain)
|
|
{
|
|
if (checkdnsrr("_xmpp-server._tcp." . $domain, "SRV")) {
|
|
return true;
|
|
}
|
|
if (checkdnsrr($domain, "ANY")) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Load related modules when needed
|
|
*
|
|
* @param string $cls Name of the class to be loaded
|
|
*
|
|
* @return boolean hook value; true means continue processing, false means stop.
|
|
*/
|
|
|
|
function onAutoload($cls)
|
|
{
|
|
switch ($cls) {
|
|
case 'XMPPHP_XMPP':
|
|
require_once __DIR__ . '/extlib/XMPPHP/XMPP.php';
|
|
return false;
|
|
}
|
|
|
|
return parent::onAutoload($cls);
|
|
}
|
|
|
|
function onStartImDaemonIoManagers(&$classes)
|
|
{
|
|
parent::onStartImDaemonIoManagers($classes);
|
|
$classes[] = new XmppManager($this); // handles pings/reconnects
|
|
return true;
|
|
}
|
|
|
|
function sendMessage($screenname, $body)
|
|
{
|
|
$this->queuedConnection()->message($screenname, $body, 'chat');
|
|
}
|
|
|
|
/**
|
|
* Build a queue-proxied XMPP interface object. Any outgoing messages
|
|
* will be run back through us for enqueing rather than sent directly.
|
|
*
|
|
* @return QueuedXMPP
|
|
* @throws Exception if server settings are invalid.
|
|
*/
|
|
function queuedConnection()
|
|
{
|
|
if (!isset($this->server)) {
|
|
// TRANS: Exception thrown when the plugin configuration is incorrect.
|
|
throw new Exception(_m('You must specify a server in the configuration.'));
|
|
}
|
|
if (!isset($this->port)) {
|
|
// TRANS: Exception thrown when the plugin configuration is incorrect.
|
|
throw new Exception(_m('You must specify a port in the configuration.'));
|
|
}
|
|
if (!isset($this->user)) {
|
|
// TRANS: Exception thrown when the plugin configuration is incorrect.
|
|
throw new Exception(_m('You must specify a user in the configuration.'));
|
|
}
|
|
if (!isset($this->password)) {
|
|
// TRANS: Exception thrown when the plugin configuration is incorrect.
|
|
throw new Exception(_m('You must specify a password in the configuration.'));
|
|
}
|
|
|
|
return new QueuedXMPP($this, $this->host ?
|
|
$this->host :
|
|
$this->server,
|
|
$this->port,
|
|
$this->user,
|
|
$this->password,
|
|
$this->resource,
|
|
$this->server,
|
|
$this->debug ?
|
|
true : false,
|
|
$this->debug ?
|
|
\XMPPHP\Log::LEVEL_VERBOSE : null
|
|
);
|
|
}
|
|
|
|
function sendNotice($screenname, Notice $notice)
|
|
{
|
|
try {
|
|
$msg = $this->formatNotice($notice);
|
|
$entry = $this->format_entry($notice);
|
|
} catch (Exception $e) {
|
|
common_log(LOG_ERR, __METHOD__ . ": Discarding outgoing stanza because of exception: {$e->getMessage()}");
|
|
return false; // return value of sendNotice is never actually used as of now
|
|
}
|
|
$this->queuedConnection()->message($screenname, $msg, 'chat', null, $entry);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* extra information for XMPP messages, as defined by Twitter
|
|
*
|
|
* @param Notice $notice Notice being sent
|
|
*
|
|
* @return string Extra information (Atom, HTML, addresses) in string format
|
|
*/
|
|
protected function format_entry(Notice $notice)
|
|
{
|
|
$profile = $notice->getProfile();
|
|
|
|
$entry = $notice->asAtomEntry(true, true);
|
|
|
|
$xs = new XMLStringer();
|
|
$xs->elementStart('html', array('xmlns' => 'http://jabber.org/protocol/xhtml-im'));
|
|
$xs->elementStart('body', array('xmlns' => 'http://www.w3.org/1999/xhtml'));
|
|
$xs->element('a', array('href' => $profile->profileurl), $profile->nickname);
|
|
try {
|
|
$parent = $notice->getParent();
|
|
$orig_profile = $parent->getProfile();
|
|
$orig_profurl = $orig_profile->getUrl();
|
|
$xs->text(" => ");
|
|
$xs->element('a', array('href' => $orig_profurl), $orig_profile->nickname);
|
|
$xs->text(": ");
|
|
} catch (NoParentNoticeException $e) {
|
|
$xs->text(": ");
|
|
}
|
|
// FIXME: Why do we replace \t with ''? is it just to make it pretty? shouldn't whitespace be handled well...?
|
|
$xs->raw(str_replace("\t", "", $notice->getRendered()));
|
|
$xs->text(" ");
|
|
$xs->element('a', array(
|
|
'href' => common_local_url('conversation',
|
|
array('id' => $notice->conversation)) . '#notice-' . $notice->id),
|
|
// TRANS: Link description to notice in conversation.
|
|
// TRANS: %s is a notice ID.
|
|
sprintf(_m('[%u]'), $notice->id));
|
|
$xs->elementEnd('body');
|
|
$xs->elementEnd('html');
|
|
|
|
$html = $xs->getString();
|
|
|
|
return $html . ' ' . $entry;
|
|
}
|
|
|
|
function receiveRawMessage($pl)
|
|
{
|
|
$from = $this->normalize($pl['from']);
|
|
|
|
if ($pl['type'] != 'chat') {
|
|
$this->log(LOG_WARNING, "Ignoring message of type " . $pl['type'] . " from $from: " . $pl['xml']->toString());
|
|
return true;
|
|
}
|
|
|
|
if (mb_strlen($pl['body']) == 0) {
|
|
$this->log(LOG_WARNING, "Ignoring message with empty body from $from: " . $pl['xml']->toString());
|
|
return true;
|
|
}
|
|
|
|
$this->handleIncoming($from, $pl['body']);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Normalizes a Jabber ID for comparison, dropping the resource component if any.
|
|
*
|
|
* @param string $jid JID to check
|
|
* @return string an equivalent JID in normalized (lowercase) form
|
|
*/
|
|
function normalize($jid)
|
|
{
|
|
try {
|
|
$parts = $this->splitJid($jid);
|
|
if ($parts['node'] !== null) {
|
|
return $parts['node'] . '@' . $parts['domain'];
|
|
} else {
|
|
return $parts['domain'];
|
|
}
|
|
} catch (Exception $e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add XMPP plugin daemon to the list of daemon to start
|
|
*
|
|
* @param array $daemons the list of daemons to run
|
|
*
|
|
* @return boolean hook return
|
|
*/
|
|
function onGetValidDaemons(&$daemons)
|
|
{
|
|
if (isset($this->server) &&
|
|
isset($this->port) &&
|
|
isset($this->user) &&
|
|
isset($this->password)) {
|
|
|
|
array_push(
|
|
$daemons,
|
|
INSTALLDIR
|
|
. '/scripts/imdaemon.php'
|
|
);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Plugin Nodeinfo information
|
|
*
|
|
* @param array $protocols
|
|
* @return bool hook true
|
|
*/
|
|
public function onNodeInfoProtocols(array &$protocols)
|
|
{
|
|
$protocols[] = "xmpp";
|
|
return true;
|
|
}
|
|
|
|
function onPluginVersion(array &$versions)
|
|
{
|
|
$versions[] = array('name' => 'XMPP',
|
|
'version' => self::PLUGIN_VERSION,
|
|
'author' => 'Craig Andrews, Evan Prodromou',
|
|
'homepage' => 'https://git.gnu.io/gnu/gnu-social/tree/master/plugins/XMPP',
|
|
'rawdescription' =>
|
|
// TRANS: Plugin description.
|
|
_m('The XMPP plugin allows users to send and receive notices over the XMPP/Jabber network.'));
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Checks whether a string is a syntactically valid Jabber ID (JID),
|
|
* either with or without a resource.
|
|
*
|
|
* Note that a bare domain can be a valid JID.
|
|
*
|
|
* @param string $jid string to check
|
|
* @param bool $check_domain whether we should validate that domain...
|
|
*
|
|
* @return boolean whether the string is a valid JID
|
|
*/
|
|
protected function validateFullJid($jid, $check_domain = false)
|
|
{
|
|
try {
|
|
$parts = $this->splitJid($jid);
|
|
if ($check_domain) {
|
|
if (!$this->checkDomain($parts['domain'])) {
|
|
return false;
|
|
}
|
|
}
|
|
return $parts['resource'] !== ''; // missing or present; empty ain't kosher
|
|
} catch (Exception $e) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|