XmppClient::getRoster()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 4
ccs 0
cts 2
cp 0
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * Nucleus - XMPP Library for PHP
4
 *
5
 * Copyright (C) 2016, Some rights reserved.
6
 *
7
 * @author Kacper "Kadet" Donat <[email protected]>
8
 *
9
 * Contact with author:
10
 * Xmpp: [email protected]
11
 * E-mail: [email protected]
12
 *
13
 * From Kadet with love.
14
 */
15
16
namespace Kadet\Xmpp;
17
18
use DI\Container;
19
use DI\ContainerBuilder;
20
use Interop\Container\ContainerInterface;
21
use Kadet\Xmpp\Component\Roster;
22
use Kadet\Xmpp\Exception\InvalidArgumentException;
23
use Kadet\Xmpp\Exception\WriteOnlyException;
24
use Kadet\Xmpp\Component\Authenticator;
25
use Kadet\Xmpp\Component\Binding;
26
use Kadet\Xmpp\Component\Component;
27
use Kadet\Xmpp\Component\ComponentInterface;
28
use Kadet\Xmpp\Component\SaslAuthenticator;
29
use Kadet\Xmpp\Component\TlsEnabler;
30
use Kadet\Xmpp\Network\Connector;
31
use Kadet\Xmpp\Stanza\Stanza;
32
use Kadet\Xmpp\Stream\Features;
33
use Kadet\Xmpp\Utils\Accessors;
34
use Kadet\Xmpp\Utils\filter as with;
35
use Kadet\Xmpp\Utils\ServiceManager;
36
use Kadet\Xmpp\Xml\XmlElementFactory;
37
use Kadet\Xmpp\Xml\XmlParser;
38
use Kadet\Xmpp\Xml\XmlStream;
39
use React\EventLoop\LoopInterface;
40
use React\Promise\Deferred;
41
use React\Promise\ExtendedPromiseInterface;
42
43
/**
44
 * Class XmppClient
45
 * @package Kadet\Xmpp
46
 *
47
 * @property Features           $features  Features provided by that stream
48
 * @property XmlParser          $parser    XmlParser instance used to process data from stream, can be exchanged only
49
 *                                         when client is not connected to server.
50
 * @property string             $resource  Client's jid resource
51
 * @property Jid                $jid       Client's jid (Jabber Identifier) address.
52
 * @property ContainerInterface $container Dependency container used for module management.
53
 * @property string             $language  Stream language (reflects xml:language attribute)
54
 * @property string             $state     Current client state
55
 *                                         `disconnected`   - not connected to any server,
56
 *                                         `connected`      - connected to server, but nothing happened yet,
57
 *                                         `secured`        - [optional] TLS negotiation succeeded, after stream restart
58
 *                                         `authenticated`  - authentication succeeded,
59
 *                                         `bound`          - resource binding succeeded,
60
 *                                         `ready`          - client is ready to operate
61
 *
62
 *                                         However modules can add custom states.
63
 *
64
 * @property-read Roster             $roster    Clients roster.
65
 *
66
 * @property Connector    $connector Connector used for obtaining stream
67
 * @property-write string       $password  Password used for client authentication
68
 *
69
 * @event stanza(Stanza $stanza)       Emitted on every incoming stanza regardless of it's kind.
70
 *                                     Equivalent of element event with only instances of Stanza class allowed.
71
 * @event iq(Iq $iq)                   Emitted on every incoming iq stanza.
72
 * @event message(Message $message)    Emitted on every incoming message stanza.
73
 * @event presence(Presence $presence) Emitted on every incoming presence stanza.
74
 *
75
 * @event init(ArrayObject $queue)     Emitted when connection is accomplished, after binding process.
76
 *
77
 * @event state(string $state)         Emitted on state change.
78
 * @event bind(Jid $jid)               Emitted after successful bind.
79
 */
80
class XmppClient extends XmlStream implements ContainerInterface
81
{
82
    use ServiceManager, Accessors;
83
84
    /**
85
     * Connector used to instantiate stream connection to server.
86
     *
87
     * @var Connector
88
     */
89
    private $_connector;
90
91
    /**
92
     * Client's jid (Jabber Identifier) address.
93
     *
94
     * @var Jid
95
     */
96
    private $_jid;
97
98
    /**
99
     * Dependency container used as service manager.
100
     *
101
     * @var Container
102
     */
103
    private $_container;
104
105
    /**
106
     * Features provided by that stream
107
     *
108
     * @var Features
109
     */
110
    private $_features;
111
112
    /**
113
     * Current client state.
114
     *
115
     * @var string
116
     */
117
    private $_state = 'disconnected';
118
    private $_lang;
119
120
    /**
121
     * XmppClient constructor.
122
     * @param Jid                  $jid
123
     * @param array                $options {
124
     *     @var XmlParser          $parser          Parser used for interpreting streams.
125
     *     @var Component[]        $modules         Additional modules registered when creating client.
126
     *     @var string             $language        Stream language (reflects xml:language attribute)
127
     *     @var ContainerInterface $container       Dependency container used for module management.
128
     *     @var bool               $default-modules Load default modules or not
129
     * }
130
     */
131 9
    public function __construct(Jid $jid, array $options = [])
132
    {
133 9
        $container = new ContainerBuilder();
134 9
        $container->useAutowiring(false);
135
136 9
        $options = array_replace([
137 9
            'parser'    => new XmlParser(new XmlElementFactory()),
138 9
            'language'  => 'en',
139 9
            'container' => $container->build(),
140 9
            'connector' => $options['connector'] ?? new Connector\TcpXmppConnector($jid->domain, $options['loop']),
141 9
            'jid'       => $jid,
142
143
            'modules'         => [],
144
            'default-modules' => true,
145
        ], $options);
146
147 9
        parent::__construct($options['parser'], null);
148 9
        unset($options['parser']);
149
150 9
        $this->applyOptions($options);
151
152
        $this->on('element', function (Features $element) {
153
            $this->_features = $element;
154
            $this->emit('features', [$element]);
155 9
        }, Features::class);
156
157
        $this->on('element', function (Stanza $stanza) {
158 3
            $this->emit('stanza', [ $stanza ]);
159 3
            $this->emit($stanza->localName, [ $stanza ]);
160 9
        }, Stanza::class);
161
162
        $this->on('close', function () {
163
            $this->state = 'disconnected';
164 9
        });
165 9
    }
166
167 8
    public function applyOptions(array $options)
168
    {
169 8
        $options = \Kadet\Xmpp\Utils\helper\rearrange($options, [
170 8
            'container' => 6,
171
            'jid'       => 5,
172
            'connector' => 4,
173
            'modules'   => 3,
174
            'password'  => -1
175
        ]);
176
177 8
        if ($options['default-modules']) {
178
            $options['modules'] = array_merge([
179
                TlsEnabler::class    => new TlsEnabler(),
180
                Binding::class       => new Binding(),
181
                Authenticator::class => new SaslAuthenticator(),
182
                Roster::class        => new Roster()
183
            ], $options['modules']);
184
        }
185
186 8
        foreach ($options as $name => $value) {
187 8
            $this->$name = $value;
188
        }
189 8
    }
190
191
    public function start(array $attributes = [])
192
    {
193
        parent::start(array_merge([
194
            'xmlns'    => 'jabber:client',
195
            'version'  => '1.0',
196
            'language' => $this->_lang
197
        ], $attributes));
198
    }
199
200
    public function connect()
201
    {
202
        $this->getLogger()->debug("Connecting to {$this->_jid->domain}");
203
204
        $this->_connector->connect();
205
    }
206
207
    public function bind($jid)
208
    {
209
        $this->jid = new Jid($jid);
210
        $this->emit('bind', [$jid]);
211
212
        $this->state = 'bound';
213
214
        $queue = new \SplQueue();
215
        $this->emit('init', [ $queue ]);
216
217
        \React\Promise\all(iterator_to_array($queue))->then(function() {
218
            $this->state = 'ready';
219
        });
220
    }
221
222
    /**
223
     * Registers module in client's dependency container.
224
     *
225
     * @param ComponentInterface $module    Module to be registered
226
     * @param bool|string|array  $alias     Module alias, class name by default.
227
     *                                      `true` for aliasing interfaces and parents too,
228
     *                                      `false` for aliasing as class name only
229
     *                                      array for multiple aliases,
230
     *                                      and any string for alias name
231
     */
232 8
    public function register(ComponentInterface $module, $alias = true)
233
    {
234 8
        $module->setClient($this);
235 8
        $this->_container->set(get_class($module), $module);
236
237 8
        if ($alias === true) {
238 8
            $this->_addToContainer($module, array_merge(class_implements($module), array_slice(class_parents($module), 1)));
239
        } elseif(is_array($alias)) {
240
            $this->_addToContainer($module, $alias);
241
        } else {
242
            $this->_addToContainer($module, [ $alias === false ? get_class($module) : $alias ]);
243
        }
244 8
    }
245
246 8
    private function _addToContainer(ComponentInterface $module, array $aliases) {
247 8
        foreach ($aliases as $name) {
248 8
            if (!$this->has($name)) {
249 8
                $this->_container->set($name, $module);
250
            }
251
        }
252 8
    }
253
254
    /**
255
     * Sends stanza to server and returns promise with server response.
256
     *
257
     * @param Stanza $stanza
258
     * @return ExtendedPromiseInterface
259
     */
260 3
    public function send(Stanza $stanza) : ExtendedPromiseInterface
261
    {
262 3
        $deferred = new Deferred();
263
264
        $this->once('element', function(Stanza $stanza) use ($deferred) {
265
            if($stanza->type === "error") {
266
                $deferred->reject($stanza);
267
            } else {
268
                $deferred->resolve($stanza);
269
            }
270 3
         }, with\stanza\id($stanza->id));
271 3
        $this->write($stanza);
272
273 3
        return $deferred->promise();
274
    }
275
276
    private function handleConnect($stream)
277
    {
278
        $this->exchangeStream($stream);
279
280
        $this->getLogger()->info("Connected to {$this->_jid->domain}");
281
        $this->start([
282
            'from' => (string)$this->_jid,
283
            'to'   => $this->_jid->domain
284
        ]);
285
286
        $this->state = 'connected';
287
288
        return $this->emit('connect');
289
    }
290
291
    //region Features
292
    public function getFeatures()
293
    {
294
        return $this->_features;
295
    }
296
    //endregion
297
298
    //region Parser
299 8
    public function setParser(XmlParser $parser)
300
    {
301 8
        if($this->state !== "disconnected") {
302
            throw new \BadMethodCallException('Parser can be changed only when client is disconnected.');
303
        }
304
305 8
        parent::setParser($parser);
306 8
        $this->_parser->factory->load(require __DIR__ . '/XmlElementLookup.php');
307 8
    }
308
309
    public function getParser()
310
    {
311
        return $this->_parser;
312
    }
313
    //endregion
314
315
    //region Connector
316 8
    protected function setConnector($connector)
317
    {
318 8
        if ($connector instanceof LoopInterface) {
319
            $this->_connector = new Connector\TcpXmppConnector($this->_jid->domain, $connector);
320
        } elseif ($connector instanceof Connector) {
321 8
            $this->_connector = $connector;
322
        } else {
323
            throw new InvalidArgumentException(sprintf(
324
                '$connector must be either %s or %s instance, %s given.',
325
                LoopInterface::class, Connector::class, \Kadet\Xmpp\Utils\helper\typeof($connector)
326
            ));
327
        }
328
329 8
        $this->_connector->on('connect', function ($stream) {
330
            return $this->handleConnect($stream);
331 8
        });
332 8
    }
333
334 2
    public function getConnector()
335
    {
336 2
        return $this->_connector;
337
    }
338
    //endregion
339
340
    //region Resource
341
    public function setResource(string $resource)
342
    {
343
        $this->_jid = new Jid($this->_jid->domain, $this->_jid->local, $resource);
344
    }
345
346
    public function getResource()
347
    {
348
        return $this->_jid->resource;
349
    }
350
    //endregion
351
352
    //region Password
353
    public function setPassword(string $password)
354
    {
355
        $this->get(Authenticator::class)->setPassword($password);
356
    }
357
358
    public function getPassword()
359
    {
360
        throw new WriteOnlyException("Password can't be obtained.");
361
    }
362
    //endregion
363
364
    //region Modules
365 8
    public function setModules(array $modules)
366
    {
367 8
        foreach ($modules as $name => $module) {
368 8
            $this->register($module, is_string($name) ? $name : true);
369
        }
370 8
    }
371
    //endregion
372
373
    //region State
374
    public function setState($state)
375
    {
376
        $this->_state = $state;
377
        $this->emit('state', [$state]);
378
    }
379
380 8
    public function getState()
381
    {
382 8
        return $this->_state;
383
    }
384
    //endregion
385
386
    //region Container
387 8
    protected function getContainer() : ContainerInterface
388
    {
389 8
        return $this->_container;
390
    }
391
392 8
    protected function setContainer(Container $container)
393
    {
394 8
        $this->_container = $container;
395 8
    }
396
    //endregion
397
398
    //region Language
399
    public function getLanguage(): string
400
    {
401
        return $this->_lang;
402
    }
403
404 8
    public function setLanguage(string $language)
405
    {
406 8
        $this->_lang = $language;
407 8
    }
408
    //endregion
409
410
    //region JID
411
    public function getJid()
412
    {
413
        return $this->_jid;
414
    }
415
416 8
    protected function setJid(Jid $jid)
417
    {
418 8
        $this->_jid = $jid;
419 8
    }
420
    //endregion
421
422
    //region Roster
423
    /**
424
     * @return Roster
425
     */
426
    public function getRoster(): Roster
427
    {
428
        return $this->get(Roster::class);
429
    }
430
    //endregion
431
}
432