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

XmppClient.php (2 issues)

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