Completed
Push — master ( 6428e9...f0e2d5 )
by Kacper
03:20
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\Module\Authenticator;
24
use Kadet\Xmpp\Module\Binding;
25
use Kadet\Xmpp\Module\ClientModule;
26
use Kadet\Xmpp\Module\ClientModuleInterface;
27
use Kadet\Xmpp\Module\SaslAuthenticator;
28
use Kadet\Xmpp\Module\TlsEnabler;
29
use Kadet\Xmpp\Network\Connector;
30
use Kadet\Xmpp\Stream\Features;
31
use Kadet\Xmpp\Utils\Accessors;
32
use Kadet\Xmpp\Utils\filter as with;
33
use Kadet\Xmpp\Utils\ObservableCollection;
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
40
/**
41
 * Class XmppClient
42
 * @package Kadet\Xmpp
43
 *
44
 * @property Features           $features  Features provided by that stream
45
 * @property XmlParser          $parser    XmlParser instance used to process data from stream, can be exchanged only
46
 *                                         when client is not connected to server.
47
 * @property string             $resource  Client's jid resource
48
 * @property Jid                $jid       Client's jid (Jabber Identifier) address.
49
 * @property ContainerInterface $container Dependency container used for module management.
50
 * @property string             $language  Stream language (reflects xml:language attribute)
51
 * @property string             $state     Current client state
52
 *                                         `disconnected`   - not connected to any server,
53
 *                                         `connected`      - connected to server, but nothing happened yet,
54
 *                                         `secured`        - [optional] TLS negotiation succeeded, after stream restart
55
 *                                         `authenticated`  - authentication succeeded,
56
 *                                         `bound`          - resource binding succeeded,
57
 *                                         `ready`          - client is ready to operate
58
 *
59
 *                                         However modules can add custom states.
60
 *
61
 * @property Connector    $connector Connector used for obtaining stream
62
 * @property-write string       $password  Password used for client authentication
63
 */
64
class XmppClient extends XmlStream implements ContainerInterface
65
{
66
    use ServiceManager, Accessors;
67
68
    /**
69
     * Connector used to instantiate stream connection to server.
70
     *
71
     * @var Connector
72
     */
73
    private $_connector;
74
75
    /**
76
     * Client's jid (Jabber Identifier) address.
77
     *
78
     * @var Jid
79
     */
80
    private $_jid;
81
82
    /**
83
     * Dependency container used as service manager.
84
     *
85
     * @var Container
86
     */
87
    private $_container;
88
89
    /**
90
     * Features provided by that stream
91
     *
92
     * @var Features
93
     */
94
    private $_features;
95
96
    /**
97
     * Current client state.
98
     *
99
     * @var string
100
     */
101
    private $_state = 'disconnected';
102
    private $_attributes = [];
103
    private $_lang;
104
105
    /**
106
     * XmppClient constructor.
107
     * @param Jid              $jid
108
     * @param array            $options {
109
     *     @var XmlParser          $parser          Parser used for interpreting streams.
110
     *     @var ClientModule[]     $modules         Additional modules registered when creating client.
111
     *     @var string             $language        Stream language (reflects xml:language attribute)
112
     *     @var ContainerInterface $container       Dependency container used for module management.
113
     *     @var bool               $default-modules Load default modules or not
114
     * }
115
     */
116
    public function __construct(Jid $jid, array $options = [])
117
    {
118
        $options = array_replace([
119
            'parser'    => new XmlParser(new XmlElementFactory()),
120
            'language'  => 'en',
121
            'container' => ContainerBuilder::buildDevContainer(),
122
            'connector' => $options['connector'] ?? new Connector\TcpXmppConnector($jid->domain, $options['loop']),
123
            'jid'       => $jid,
124
125
            'modules'         => [],
126
            'default-modules' => true,
127
        ], $options);
128
129
        parent::__construct($options['parser'], null);
130
        unset($options['parser']);
131
132
        $this->applyOptions($options);
133
134
        $this->on('element', function (Features $element) {
135
            $this->_features = $element;
136
            $this->emit('features', [$element]);
137
        }, Features::class);
138
139
        $this->on('close', function (Features $element) {
140
            $this->state = 'disconnected';
141
        }, Features::class);
142
    }
143
144
    public function applyOptions(array $options)
145
    {
146
        $options = \Kadet\Xmpp\Utils\helper\rearrange($options, [
147
            'container' => 6,
148
            'jid'       => 5,
149
            'connector' => 4,
150
            'modules'   => 3,
151
            'password'  => -1
152
        ]);
153
154
        if ($options['default-modules']) {
155
            $options['modules'] = array_merge([
156
                TlsEnabler::class    => new TlsEnabler(),
157
                Binding::class       => new Binding(),
158
                Authenticator::class => new SaslAuthenticator()
159
            ], $options['modules']);
160
        }
161
162
        foreach ($options as $name => $value) {
163
            $this->$name = $value;
164
        }
165
    }
166
167
    public function restart()
168
    {
169
        $this->getLogger()->debug('Restarting stream', $this->_attributes);
170
        $this->start($this->_attributes);
171
    }
172
173
    public function start(array $attributes = [])
174
    {
175
        $this->_attributes = $attributes;
176
177
        parent::start(array_merge([
178
            'xmlns'    => 'jabber:client',
179
            'version'  => '1.0',
180
            'xml:lang' => $this->_lang
181
        ], $attributes));
182
    }
183
184
    public function connect()
185
    {
186
        $this->getLogger()->debug("Connecting to {$this->_jid->domain}");
187
188
        $this->_connector->connect();
189
    }
190
191
    public function bind($jid)
192
    {
193
        $this->jid = new Jid($jid);
194
        $this->emit('bind', [$jid]);
195
196
        $this->state = 'bound';
197
198
        $queue = new ObservableCollection();
199
        $queue->on('empty', function () {
200
            $this->state = 'ready';
201
        });
202
203
        $this->emit('init', [$queue]);
204
    }
205
206
    /**
207
     * Registers module in client's dependency container.
208
     *
209
     * @param ClientModuleInterface $module Module to be registered
210
     * @param bool|string           $alias  Module alias, class name by default.
211
     *                                      `true` for aliasing interfaces and parents too,
212
     *                                      `false` for aliasing as class name only
213
     *                                      array for multiple aliases,
214
     *                                      and any string for alias name
215
     */
216
    public function register(ClientModuleInterface $module, $alias = true)
217
    {
218
        $module->setClient($this);
219
        if ($alias === true) {
220
            $this->_container->set(get_class($module), $module);
221
222
            $this->register($module, array_merge(class_implements($module), array_slice(class_parents($module), 1)));
0 ignored issues
show
array_merge(class_implem...s_parents($module), 1)) is of type array, but the function expects a boolean|string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
223
        } elseif(is_array($alias)) {
224
            foreach ($alias as $name) {
225
                if (!$this->has($name)) {
226
                    $this->register($module, $name);
227
                }
228
            }
229
        } else {
230
            $this->_container->set($alias === false ? get_class($module) : $alias, $module);
231
        }
232
    }
233
234
    private function handleConnect($stream)
235
    {
236
        $this->exchangeStream($stream);
237
238
        $this->getLogger()->info("Connected to {$this->_jid->domain}");
239
        $this->start([
240
            'from' => (string)$this->_jid,
241
            'to'   => $this->_jid->domain
242
        ]);
243
244
        $this->state = 'connected';
245
246
        return $this->emit('connect');
247
    }
248
249
    //region Features
250
    public function getFeatures()
251
    {
252
        return $this->_features;
253
    }
254
    //endregion
255
256
    //region Parser
257
    public function setParser(XmlParser $parser)
258
    {
259
        if($this->state !== "disconnected") {
260
            throw new \BadMethodCallException('Parser can be changed only when client is disconnected.');
261
        }
262
263
        parent::setParser($parser);
264
        $this->_parser->factory->load(require __DIR__ . '/XmlElementLookup.php');
265
    }
266
267
    public function getParser()
268
    {
269
        return $this->_parser;
270
    }
271
    //endregion
272
273
    //region Connector
274
    protected function setConnector($connector)
275
    {
276
        if ($connector instanceof LoopInterface) {
277
            $this->_connector = new Connector\TcpXmppConnector($this->_jid->domain, $connector);
278
        } elseif ($connector instanceof Connector) {
279
            $this->_connector = $connector;
280
        } else {
281
            throw new InvalidArgumentException(sprintf(
282
                '$connector must be either %s or %s instance, %s given.',
283
                LoopInterface::class, Connector::class, \Kadet\Xmpp\Utils\helper\typeof($connector)
284
            ));
285
        }
286
287
        $this->_connector->on('connect', function ($stream) {
288
            return $this->handleConnect($stream);
289
        });
290
    }
291
292
    public function getConnector()
293
    {
294
        return $this->_connector;
295
    }
296
    //endregion
297
298
    //region Resource
299
    public function setResource(string $resource)
300
    {
301
        $this->_jid = new Jid($this->_jid->domain, $this->_jid->local, $resource);
302
    }
303
304
    public function getResource()
305
    {
306
        return $this->_jid->resource;
307
    }
308
    //endregion
309
310
    //region Password
311
    public function setPassword(string $password)
312
    {
313
        $this->get(Authenticator::class)->setPassword($password);
314
    }
315
316
    public function getPassword()
317
    {
318
        throw new WriteOnlyException("Password can't be obtained.");
319
    }
320
    //endregion
321
322
    //region Modules
323
    public function setModules(array $modules)
324
    {
325
        foreach ($modules as $name => $module) {
326
            $this->register($module, is_string($name) ? $name : true);
327
        }
328
    }
329
    //endregion
330
331
    //region State
332
    public function setState($state)
333
    {
334
        $this->_state = $state;
335
        $this->emit('state', [$state]);
336
    }
337
338
    public function getState()
339
    {
340
        return $this->_state;
341
    }
342
    //endregion
343
344
    //region Container
345
    protected function getContainer() : ContainerInterface
346
    {
347
        return $this->_container;
348
    }
349
350
    protected function setContainer(Container $container)
351
    {
352
        $this->_container = $container;
353
    }
354
    //endregion
355
356
    //region Language
357
    public function getLanguage(): string
358
    {
359
        return $this->_lang;
360
    }
361
362
    public function setLanguage(string $language)
363
    {
364
        $this->_lang = $language;
365
    }
366
    //endregion
367
368
    //region JID
369
    public function getJid()
370
    {
371
        return $this->_jid;
372
    }
373
374
    protected function setJid(Jid $jid)
375
    {
376
        $this->_jid = $jid;
377
    }
378
    //endregion
379
}
380