Completed
Push — master ( 4159e1...6428e9 )
by Kacper
03:22
created

XmppClient.php (3 issues)

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) {
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...
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);
1 ignored issue
show
It seems like $alias === false ? get_class($module) : $alias can also be of type boolean; however, DI\Container::set() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
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