Completed
Push — master ( 939cef...4aee11 )
by Kacper
03:59
created

XmppClient.php (1 issue)

Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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\Exception\InvalidArgumentException;
22
use Kadet\Xmpp\Exception\WriteOnlyException;
23
use Kadet\Xmpp\Component\Authenticator;
24
use Kadet\Xmpp\Component\Binding;
25
use Kadet\Xmpp\Component\Component;
26
use Kadet\Xmpp\Component\ComponentInterface;
27
use Kadet\Xmpp\Component\SaslAuthenticator;
28
use Kadet\Xmpp\Component\TlsEnabler;
29
use Kadet\Xmpp\Network\Connector;
30
use Kadet\Xmpp\Stanza\Stanza;
31
use Kadet\Xmpp\Stream\Features;
32
use Kadet\Xmpp\Utils\Accessors;
33
use Kadet\Xmpp\Utils\filter as with;
34
use Kadet\Xmpp\Utils\ServiceManager;
35
use Kadet\Xmpp\Xml\XmlElementFactory;
36
use Kadet\Xmpp\Xml\XmlParser;
37
use Kadet\Xmpp\Xml\XmlStream;
38
use React\EventLoop\LoopInterface;
39
use React\Promise\Deferred;
40
use React\Promise\ExtendedPromiseInterface;
41
42
/**
43
 * Class XmppClient
44
 * @package Kadet\Xmpp
45
 *
46
 * @property Features           $features  Features provided by that stream
47
 * @property XmlParser          $parser    XmlParser instance used to process data from stream, can be exchanged only
48
 *                                         when client is not connected to server.
49
 * @property string             $resource  Client's jid resource
50
 * @property Jid                $jid       Client's jid (Jabber Identifier) address.
51
 * @property ContainerInterface $container Dependency container used for module management.
52
 * @property string             $language  Stream language (reflects xml:language attribute)
53
 * @property string             $state     Current client state
54
 *                                         `disconnected`   - not connected to any server,
55
 *                                         `connected`      - connected to server, but nothing happened yet,
56
 *                                         `secured`        - [optional] TLS negotiation succeeded, after stream restart
57
 *                                         `authenticated`  - authentication succeeded,
58
 *                                         `bound`          - resource binding succeeded,
59
 *                                         `ready`          - client is ready to operate
60
 *
61
 *                                         However modules can add custom states.
62
 *
63
 * @property Connector    $connector Connector used for obtaining stream
64
 * @property-write string       $password  Password used for client authentication
65
 */
66
class XmppClient extends XmlStream implements ContainerInterface
67
{
68
    use ServiceManager, Accessors;
69
70
    /**
71
     * Connector used to instantiate stream connection to server.
72
     *
73
     * @var Connector
74
     */
75
    private $_connector;
76
77
    /**
78
     * Client's jid (Jabber Identifier) address.
79
     *
80
     * @var Jid
81
     */
82
    private $_jid;
83
84
    /**
85
     * Dependency container used as service manager.
86
     *
87
     * @var Container
88
     */
89
    private $_container;
90
91
    /**
92
     * Features provided by that stream
93
     *
94
     * @var Features
95
     */
96
    private $_features;
97
98
    /**
99
     * Current client state.
100
     *
101
     * @var string
102
     */
103
    private $_state = 'disconnected';
104
    private $_lang;
105
106
    /**
107
     * XmppClient constructor.
108
     * @param Jid                  $jid
109
     * @param array                $options   {
110
     *     @var XmlParser          $parser    Parser used for interpreting streams.
111
     *     @var Component[]        $modules   Additional modules registered when creating client.
112
     *     @var string             $language  Stream language (reflects xml:language attribute)
113
     *     @var ContainerInterface $container Dependency container used for module management.
114
     *     @var bool               $default   -modules Load default modules or not
115
     * }
116
     */
117
    public function __construct(Jid $jid, array $options = [])
118
    {
119
        $options = array_replace([
120
            'parser'    => new XmlParser(new XmlElementFactory()),
121
            'language'  => 'en',
122
            'container' => ContainerBuilder::buildDevContainer(),
123
            'connector' => $options['connector'] ?? new Connector\TcpXmppConnector($jid->domain, $options['loop']),
124
            'jid'       => $jid,
125
126
            'modules'         => [],
127
            'default-modules' => true,
128
        ], $options);
129
130
        parent::__construct($options['parser'], null);
131
        unset($options['parser']);
132
133
        $this->applyOptions($options);
134
135
        $this->on('element', function (Features $element) {
136
            $this->_features = $element;
137
            $this->emit('features', [$element]);
138
        }, Features::class);
139
140
        $this->on('close', function (Features $element) {
0 ignored issues
show
The parameter $element is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
141
            $this->state = 'disconnected';
142
        }, Features::class);
143
    }
144
145
    public function applyOptions(array $options)
146
    {
147
        $options = \Kadet\Xmpp\Utils\helper\rearrange($options, [
148
            'container' => 6,
149
            'jid'       => 5,
150
            'connector' => 4,
151
            'modules'   => 3,
152
            'password'  => -1
153
        ]);
154
155
        if ($options['default-modules']) {
156
            $options['modules'] = array_merge([
157
                TlsEnabler::class    => new TlsEnabler(),
158
                Binding::class       => new Binding(),
159
                Authenticator::class => new SaslAuthenticator()
160
            ], $options['modules']);
161
        }
162
163
        foreach ($options as $name => $value) {
164
            $this->$name = $value;
165
        }
166
    }
167
168
    public function start(array $attributes = [])
169
    {
170
        parent::start(array_merge([
171
            'xmlns'    => 'jabber:client',
172
            'version'  => '1.0',
173
            'language' => $this->_lang
174
        ], $attributes));
175
    }
176
177
    public function connect()
178
    {
179
        $this->getLogger()->debug("Connecting to {$this->_jid->domain}");
180
181
        $this->_connector->connect();
182
    }
183
184
    public function bind($jid)
185
    {
186
        $this->jid = new Jid($jid);
187
        $this->emit('bind', [$jid]);
188
189
        $this->state = 'bound';
190
191
        $queue = new \SplQueue();
192
        $this->emit('init', [ $queue ]);
193
194
        \React\Promise\all(iterator_to_array($queue))->then(function() {
195
            $this->state = 'ready';
196
        });
197
    }
198
199
    /**
200
     * Registers module in client's dependency container.
201
     *
202
     * @param ComponentInterface $module    Module to be registered
203
     * @param bool|string|array  $alias     Module alias, class name by default.
204
     *                                      `true` for aliasing interfaces and parents too,
205
     *                                      `false` for aliasing as class name only
206
     *                                      array for multiple aliases,
207
     *                                      and any string for alias name
208
     */
209
    public function register(ComponentInterface $module, $alias = true)
210
    {
211
        $module->setClient($this);
212
        if ($alias === true) {
213
            $this->_container->set(get_class($module), $module);
214
215
            $this->_addToContainer($module, array_merge(class_implements($module), array_slice(class_parents($module), 1)));
216
        } elseif(is_array($alias)) {
217
            $this->_addToContainer($module, $alias);
218
        } else {
219
            $this->_addToContainer($module, [ $alias === false ? get_class($module) : $alias ]);
220
        }
221
    }
222
223
    private function _addToContainer(ComponentInterface $module, array $aliases) {
224
        foreach ($aliases as $name) {
225
            if (!$this->has($name)) {
226
                $this->_container->set($name, $module);
227
            }
228
        }
229
    }
230
231
    /**
232
     * Sends stanza to server and returns promise with server response.
233
     *
234
     * @param Stanza $stanza
235
     * @return ExtendedPromiseInterface
236
     */
237
    public function send(Stanza $stanza) : ExtendedPromiseInterface
238
    {
239
        $deferred = new Deferred();
240
241
        $this->once('element', function(Stanza $stanza) use ($deferred) {
242
            if($stanza->type === "error") {
243
                $deferred->reject($stanza);
244
            } else {
245
                $deferred->resolve($stanza);
246
            }
247
        }, with\stanza\id($stanza->id));
248
        $this->write($stanza);
249
250
        return $deferred->promise();
251
    }
252
253
    private function handleConnect($stream)
254
    {
255
        $this->exchangeStream($stream);
256
257
        $this->getLogger()->info("Connected to {$this->_jid->domain}");
258
        $this->start([
259
            'from' => (string)$this->_jid,
260
            'to'   => $this->_jid->domain
261
        ]);
262
263
        $this->state = 'connected';
264
265
        return $this->emit('connect');
266
    }
267
268
    //region Features
269
    public function getFeatures()
270
    {
271
        return $this->_features;
272
    }
273
    //endregion
274
275
    //region Parser
276
    public function setParser(XmlParser $parser)
277
    {
278
        if($this->state !== "disconnected") {
279
            throw new \BadMethodCallException('Parser can be changed only when client is disconnected.');
280
        }
281
282
        parent::setParser($parser);
283
        $this->_parser->factory->load(require __DIR__ . '/XmlElementLookup.php');
284
    }
285
286
    public function getParser()
287
    {
288
        return $this->_parser;
289
    }
290
    //endregion
291
292
    //region Connector
293
    protected function setConnector($connector)
294
    {
295
        if ($connector instanceof LoopInterface) {
296
            $this->_connector = new Connector\TcpXmppConnector($this->_jid->domain, $connector);
297
        } elseif ($connector instanceof Connector) {
298
            $this->_connector = $connector;
299
        } else {
300
            throw new InvalidArgumentException(sprintf(
301
                '$connector must be either %s or %s instance, %s given.',
302
                LoopInterface::class, Connector::class, \Kadet\Xmpp\Utils\helper\typeof($connector)
303
            ));
304
        }
305
306
        $this->_connector->on('connect', function ($stream) {
307
            return $this->handleConnect($stream);
308
        });
309
    }
310
311
    public function getConnector()
312
    {
313
        return $this->_connector;
314
    }
315
    //endregion
316
317
    //region Resource
318
    public function setResource(string $resource)
319
    {
320
        $this->_jid = new Jid($this->_jid->domain, $this->_jid->local, $resource);
321
    }
322
323
    public function getResource()
324
    {
325
        return $this->_jid->resource;
326
    }
327
    //endregion
328
329
    //region Password
330
    public function setPassword(string $password)
331
    {
332
        $this->get(Authenticator::class)->setPassword($password);
333
    }
334
335
    public function getPassword()
336
    {
337
        throw new WriteOnlyException("Password can't be obtained.");
338
    }
339
    //endregion
340
341
    //region Modules
342
    public function setModules(array $modules)
343
    {
344
        foreach ($modules as $name => $module) {
345
            $this->register($module, is_string($name) ? $name : true);
346
        }
347
    }
348
    //endregion
349
350
    //region State
351
    public function setState($state)
352
    {
353
        $this->_state = $state;
354
        $this->emit('state', [$state]);
355
    }
356
357
    public function getState()
358
    {
359
        return $this->_state;
360
    }
361
    //endregion
362
363
    //region Container
364
    protected function getContainer() : ContainerInterface
365
    {
366
        return $this->_container;
367
    }
368
369
    protected function setContainer(Container $container)
370
    {
371
        $this->_container = $container;
372
    }
373
    //endregion
374
375
    //region Language
376
    public function getLanguage(): string
377
    {
378
        return $this->_lang;
379
    }
380
381
    public function setLanguage(string $language)
382
    {
383
        $this->_lang = $language;
384
    }
385
    //endregion
386
387
    //region JID
388
    public function getJid()
389
    {
390
        return $this->_jid;
391
    }
392
393
    protected function setJid(Jid $jid)
394
    {
395
        $this->_jid = $jid;
396
    }
397
    //endregion
398
}
399