GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Test Failed
Push — master ( d13c9c...463153 )
by Charlotte
03:30
created

src/Driver.php (1 issue)

Labels
Severity
1
<?php
2
/**
3
 * Plasma Driver MySQL component
4
 * Copyright 2018 PlasmaPHP, All Rights Reserved
5
 *
6
 * Website: https://github.com/PlasmaPHP
7
 * License: https://github.com/PlasmaPHP/driver-mysql/blob/master/LICENSE
8
*/
9
10
namespace Plasma\Drivers\MySQL;
11
12
/**
13
 * The MySQL Driver.
14
 * @internal
15
 */
16
class Driver implements \Plasma\DriverInterface {
17
    use \Evenement\EventEmitterTrait;
18
    
19
    /**
20
     * @var \React\EventLoop\LoopInterface
21
     */
22
    protected $loop;
23
    
24
    /**
25
     * @var array
26
     */
27
    protected $options = array(
28
        'tls.context' => array(),
29
        'tls.force' => true,
30
        'tls.forceLocal' => false
31
    );
32
    
33
    /**
34
     * @var \React\Socket\ConnectorInterface
35
     */
36
    protected $connector;
37
    
38
    /**
39
     * Internal class is intentional used, as there's no other way currently.
40
     * @var \React\Socket\StreamEncryption
41
     * @see https://github.com/reactphp/socket/issues/180
42
     */
43
    protected $encryption;
44
    
45
    /**
46
     * @var \React\Socket\Connection
47
     */
48
    protected $connection;
49
    
50
    /**
51
     * @var int
52
     */
53
    protected $connectionState = \Plasma\DriverInterface::CONNECTION_CLOSED;
54
    
55
    /**
56
     * @var \Plasma\Drivers\MySQL\ProtocolParser
57
     */
58
    protected $parser;
59
    
60
    /**
61
     * @var array
62
     */
63
    protected $queue;
64
    
65
    /**
66
     * @var int
67
     */
68
    protected $busy = \Plasma\DriverInterface::STATE_IDLE;
69
    
70
    /**
71
     * @var bool
72
     */
73
    protected $transaction = false;
74
    
75
    /**
76
     * @var \React\Promise\Deferred
77
     */
78
    protected $goingAway;
79
    
80
    /**
81
     * Constructor.
82
     */
83 17
    function __construct(\React\EventLoop\LoopInterface $loop, array $options) {
84 17
        $this->validateOptions($options);
85
        
86 17
        $this->loop = $loop;
87 17
        $this->options = \array_merge($this->options, $options);
88
        
89 17
        $this->connector = ($options['connector'] ?? (new \React\Socket\Connector($loop)));
90 17
        $this->encryption = new \React\Socket\StreamEncryption($this->loop, false);
91 17
        $this->queue = array();
92 17
    }
93
    
94
    /**
95
     * Returns the event loop.
96
     * @var \React\EventLoop\LoopInterface
97
     */
98 1
    function getLoop(): \React\EventLoop\LoopInterface {
99 1
        return $this->loop;
100
    }
101
    
102
    /**
103
     * Retrieves the current connection state.
104
     * @return int
105
     */
106 8
    function getConnectionState(): int {
107 8
        return $this->connectionState;
108
    }
109
    
110
    /**
111
     * Retrieves the current busy state.
112
     * @return int
113
     */
114 1
    function getBusyState(): int {
115 1
        return $this->busy;
116
    }
117
    
118
    /**
119
     * Get the length of the driver backlog queue.
120
     * @return int
121
     */
122 1
    function getBacklogLength(): int {
123 1
        return \count($this->queue);
124
    }
125
    
126
    /**
127
     * Connects to the given URI.
128
     * @return \React\Promise\PromiseInterface
129
     */
130 12
    function connect(string $uri): \React\Promise\PromiseInterface {
131 12
        if($this->goingAway || $this->connectionState === \Plasma\DriverInterface::CONNECTION_UNUSABLE) {
132
            return \React\Promise\resolve((new \Plasma\Exception('Connection is going away or unusable')));
133
        }
134
        
135 12
        $uri = 'mysql://'.\ltrim($uri, 'mysql://');
136
        
137 12
        $parts = \parse_url($uri);
138 12
        if(!isset($parts['host'])) {
139
            return \React\Promise\reject((new \InvalidArgumentException('Invalid connect uri given')));
140
        }
141
        
142 12
        $host = $parts['host'].':'.($parts['port'] ?? 3306);
143 12
        $this->connectionState = static::CONNECTION_STARTED;
144 12
        $resolved = false;
145
        
146
        return $this->connector->connect($host)->then(function (\React\Socket\ConnectionInterface $connection) use ($parts, &$resolved) {
147
            // See description of property encryption
148 12
            if(!($connection instanceof \React\Socket\Connection)) {
149
                throw new \LogicException('Custom connection class is NOT supported yet (encryption limitation)');
150
            }
151
            
152 12
            $this->busy = static::STATE_BUSY;
153 12
            $this->connectionState = static::CONNECTION_MADE;
154 12
            $this->connection = $connection;
155
            
156
            $this->connection->on('error', function (\Throwable $error) {
157
                $this->emit('error', array($error));
158 12
            });
159
            
160
            $this->connection->on('close', function () {
161 5
                $this->connection = null;
162 5
                $this->connectionState = static::CONNECTION_UNUSABLE;
163
                
164 5
                $this->emit('close');
165 12
            });
166
            
167 12
            $deferred = new \React\Promise\Deferred();
168 12
            $this->parser = new \Plasma\Drivers\MySQL\ProtocolParser($this, $this->connection);
169
            
170
            $this->parser->on('error', function (\Throwable $error) use (&$deferred, &$resolved) {
171
                if($resolved) {
172
                    $this->emit('error', array($error));
173
                } else {
174
                    $deferred->reject($error);
175
                }
176 12
            });
177
            
178 12
            $user = ($parts['user'] ?? 'root');
179 12
            $password = ($parts['pass'] ?? '');
180 12
            $db = (!empty($parts['path']) ? \ltrim($parts['path'], '/') : '');
181
            
182 12
            $credentials = \compact('user', 'password', 'db');
183
            
184 12
            $this->startHandshake($credentials, $deferred);
185
            return $deferred->promise()->then(function () use (&$resolved) {
186 9
                $this->busy = static::STATE_IDLE;
187 9
                $resolved = true;
188
                
189 9
                if(\count($this->queue) > 0) {
190
                    $this->parser->invokeCommand($this->getNextCommand());
191
                }
192 12
            });
193 12
        });
194
    }
195
    
196
    /**
197
     * Pauses the underlying stream I/O consumption.
198
     * If consumption is already paused, this will do nothing.
199
     * @return bool  Whether the operation was successful.
200
     */
201
    function pauseStreamConsumption(): bool {
202
        if($this->goingAway) {
203
            return false;
204
        }
205
        
206
        $this->connection->pause();
207
        return true;
208
    }
209
    
210
    /**
211
     * Resumes the underlying stream I/O consumption.
212
     * If consumption is not paused, this will do nothing.
213
     * @return bool  Whether the operation was successful.
214
     */
215
    function resumeStreamConsumption(): bool {
216
        if($this->goingAway) {
217
            return false;
218
        }
219
        
220
        $this->connection->resume();
221
        return true;
222
    }
223
    
224
    /**
225
     * Closes all connections gracefully after processing all outstanding requests.
226
     * @return \React\Promise\PromiseInterface
227
     */
228 1
    function close(): \React\Promise\PromiseInterface {
229 1
        if($this->goingAway) {
230
            return $this->goingAway->promise();
231
        }
232
        
233 1
        $this->goingAway = new \React\Promise\Deferred();
234
        
235 1
        if(\count($this->queue) === 0) {
236
            $this->goingAway->resolve();
237
        }
238
        
239
        return $this->goingAway->promise()->then(function () {
240 1
            if($this->connectionState !== static::CONNECTION_OK) {
241
                return;
242
            }
243
            
244 1
            $deferred = new \React\Promise\Deferred();
245
            
246 1
            $quit = new \Plasma\Drivers\MySQL\Commands\QuitCommand();
247
            
248
            $this->connection->once('close', function () use (&$deferred) {
249 1
                $deferred->resolve();
250 1
            });
251
            
252
            $quit->once('end', function () {
253
                $this->connection->close();
254 1
            });
255
            
256 1
            $this->parser->invokeCommand($quit);
257
            
258 1
            return $deferred->promise();
259 1
        });
260
    }
261
    
262
    /**
263
     * Forcefully closes the connection, without waiting for any outstanding requests. This will reject all outstanding requests.
264
     * @return void
265
     */
266 1
    function quit(): void {
267
        /** @var \Plasma\Drivers\MySQL\Commands\CommandInterface  $command */
268 1
        while($command = \array_shift($this->queue)) {
269 1
            $command->emit('error', array((new \Plasma\Exception('Connection is going away'))));
270
        }
271
        
272 1
        if($this->connectionState === static::CONNECTION_OK) {
273 1
            $quit = new \Plasma\Drivers\MySQL\Commands\QuitCommand();
274 1
            $this->parser->invokeCommand($quit);
275
            
276 1
            $this->connection->close();
277
        }
278 1
    }
279
    
280
    /**
281
     * Whether this driver is currently in a transaction.
282
     * @return bool
283
     */
284 1
    function isInTransaction(): bool {
285 1
        return $this->transaction;
286
    }
287
    
288
    /**
289
     * Executes a plain query. Resolves with a `QueryResultInterface` instance.
290
     * When the command is done, the driver must check itself back into the client.
291
     * @param \Plasma\ClientInterface  $client
292
     * @param string                   $query
293
     * @return \React\Promise\PromiseInterface
294
     * @throws \Plasma\Exception
295
     * @see \Plasma\QueryResultInterface
296
     */
297 2
    function query(\Plasma\ClientInterface $client, string $query): \React\Promise\PromiseInterface {
298 2
        if($this->goingAway) {
299
            return \React\Promise\resolve((new \Plasma\Exception('Connection is going away')));
300
        }
301
        
302 2
        $command = new \Plasma\Drivers\MySQL\Commands\QueryCommand($this, $query);
303 2
        $this->executeCommand($command);
304
        
305 2
        if(!$this->transaction) {
306
            $command->once('end', function () use (&$client) {
307 1
                $client->checkinConnection($this);
308 1
            });
309
        }
310
        
311 2
        return $command->getPromise();
312
    }
313
    
314
    /**
315
     * Prepares a query. Resolves with a `StatementInterface` instance.
316
     * When the command is done, the driver must check itself back into the client.
317
     * @param \Plasma\ClientInterface  $client
318
     * @param string                   $query
319
     * @return \React\Promise\PromiseInterface
320
     * @throws \Plasma\Exception
321
     * @see \Plasma\StatementInterface
322
     */
323
    function prepare(\Plasma\ClientInterface $client, string $query): \React\Promise\PromiseInterface {
324
        if($this->goingAway) {
325
            return \React\Promise\resolve((new \Plasma\Exception('Connection is going away')));
326
        }
327
        
328
        $command = new \Plasma\Drivers\MySQL\Commands\PrepareCommand($client, $this, $query);
329
        $this->executeCommand($command);
330
        
331
        return $command->getPromise();
332
    }
333
    
334
    /**
335
     * Prepares and executes a query. Resolves with a `QueryResultInterface` instance.
336
     * This is equivalent to prepare -> execute -> close.
337
     * If you need to execute a query multiple times, prepare the query manually for performance reasons.
338
     * @param \Plasma\ClientInterface  $client
339
     * @param string                   $query
340
     * @param array                    $params
341
     * @return \React\Promise\PromiseInterface
342
     * @throws \Plasma\Exception
343
     * @see \Plasma\StatementInterface
344
     */
345
    function execute(\Plasma\ClientInterface $client, string $query, array $params = array()): \React\Promise\PromiseInterface {
346
        if($this->goingAway) {
347
            return \React\Promise\resolve((new \Plasma\Exception('Connection is going away')));
348
        }
349
        
350
        return $this->prepare($client, $query)->then(function (\Plasma\StatementInterface $statement) use ($params) {
351
            return $statement->execute($params)->then(function (\Plasma\QueryResultInterface $result) use (&$statement) {
352
                return $statement->close()->then(function () use ($result) {
353
                    return $result;
354
                });
355
            }, function (\Throwable $error) use (&$statement) {
356
                return $statement->close()->then(function () use ($error) {
357
                    throw $error;
358
                });
359
            });
360
        })->always(function () use (&$client) {
361
            if(!$this->transaction) {
362
                $client->checkinConnection($this);
363
            }
364
        });
365
    }
366
    
367
    /**
368
     * Quotes the string for use in the query.
369
     * @param string  $str
370
     * @return string
371
     * @throws \LogicException  Thrown if the driver does not support quoting.
372
     * @throws \Plasma\Exception
373
     */
374
    function quote(string $str): string { // TODO
375
        throw new \LogicException('Not implemented yet');
376
    }
377
    
378
    /**
379
     * Begins a transaction. Resolves with a `TransactionInterface` instance.
380
     *
381
     * Checks out a connection until the transaction gets committed or rolled back.
382
     * It must be noted that the user is responsible for finishing the transaction. The client WILL NOT automatically
383
     * check the connection back into the pool, as long as the transaction is not finished.
384
     *
385
     * Some databases, including MySQL, automatically issue an implicit COMMIT when a database definition language (DDL)
386
     * statement such as DROP TABLE or CREATE TABLE is issued within a transaction.
387
     * The implicit COMMIT will prevent you from rolling back any other changes within the transaction boundary.
388
     * @param \Plasma\ClientInterface  $client
389
     * @param int                      $isolation  See the `TransactionInterface` constants.
390
     * @return \React\Promise\PromiseInterface
391
     * @throws \Plasma\Exception
392
     * @see \Plasma\TransactionInterface
393
     */
394 1
    function beginTransaction(\Plasma\ClientInterface $client, int $isolation = \Plasma\TransactionInterface::ISOLATION_COMMITTED): \React\Promise\PromiseInterface {
395 1
        if($this->goingAway) {
396
            return \React\Promise\resolve((new \Plasma\Exception('Connection is going away')));
397
        }
398
        
399 1
        if($this->transaction) {
400
            throw new \Plasma\Exception('Driver is already in transaction');
401
        }
402
        
403
        switch ($isolation) {
404 1
            case \Plasma\TransactionInterface::ISOLATION_UNCOMMITTED:
405
                $query = 'SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED';
406
            break;
407 1
            case \Plasma\TransactionInterface::ISOLATION_COMMITTED:
408 1
                $query = 'SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED';
409 1
            break;
410
            case \Plasma\TransactionInterface::ISOLATION_REPEATABLE:
411
                $query = 'SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ';
412
            break;
413
            case \Plasma\TransactionInterface::ISOLATION_SERIALIZABLE:
414
                $query = 'SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE';
415
            break;
416
            default:
417
                throw new \Plasma\Exception('Invalid isolation level given');
418
            break;
419
        }
420
        
421 1
        $this->transaction = true;
422
        
423
        return $this->query($client, $query)->then(function () use (&$client) {
424 1
            return $this->query($client, 'START TRANSACTION');
425
        })->then(function () use (&$client, $isolation) {
426 1
            return (new \Plasma\Transaction($client, $this, $isolation));
427
        })->otherwise(function (\Throwable $e) {
0 ignored issues
show
The method otherwise() does not exist on React\Promise\PromiseInterface. It seems like you code against a sub-type of said class. However, the method does not exist in React\Promise\SimpleFulfilledTestPromise or React\Promise\SimpleRejectedTestPromise or React\Promise\CancellablePromiseInterface. Are you sure you never get one of those? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

427
        })->/** @scrutinizer ignore-call */ otherwise(function (\Throwable $e) {
Loading history...
428
            $this->transaction = false;
429
            throw $e;
430 1
        });
431
    }
432
    
433
    /**
434
     * Informationally closes a transaction. This method is used by `Transaction` to inform the driver of the end of the transaction.
435
     * @return void
436
     */
437 1
    function endTransaction(): void {
438 1
        $this->transaction = false;
439 1
    }
440
    
441
    /**
442
     * Runs the given command.
443
     * Returns a Promise, which resolves with the `end` event argument (defaults to `null),
444
     * or rejects with the `Throwable` of the `error` event.
445
     * When the command is done, the driver must check itself back into the client.
446
     * @param \Plasma\ClientInterface   $client
447
     * @param \Plasma\CommandInterface  $command
448
     * @return \React\Promise\PromiseInterface
449
     */
450 4
    function runCommand(\Plasma\ClientInterface $client, \Plasma\CommandInterface $command) {
451 4
        if($this->goingAway) {
452
            return \React\Promise\resolve((new \Plasma\Exception('Connection is going away')));
453
        }
454
        
455
        return (new \React\Promise\Promise(function (callable $resolve, callable $reject) use (&$client, &$command) {
456
            $command->once('end', function ($value = null) use (&$client, &$resolve) {
457 2
                if(!$this->transaction) {
458 2
                    $client->checkinConnection($this);
459
                }
460
                
461 2
                $resolve($value);
462 4
            });
463
            
464
            $command->once('error', function (\Throwable $error) use (&$client, &$reject) {
465 2
                if(!$this->transaction) {
466 2
                    $client->checkinConnection($this);
467
                }
468
                
469 2
                $reject($error);
470 4
            });
471
            
472 4
            $this->executeCommand($command);
473 4
        }));
474
    }
475
    
476
    /**
477
     * Executes a command.
478
     * @param \Plasma\CommandInterface  $command
479
     * @return void
480
     */
481 6
    function executeCommand(\Plasma\CommandInterface $command): void {
482 6
        $this->queue[] = $command;
483 6
        echo 'Command '.get_class($command).' added to queue'.PHP_EOL;
484
        
485 6
        if($this->parser && $this->busy === static::STATE_IDLE) {
486 5
            echo 'Command '.get_class($command).' invoked into parser'.PHP_EOL;
487 5
            $this->parser->invokeCommand($this->getNextCommand());
488
        }
489 6
    }
490
    
491
    /**
492
     * Get the next command, or null.
493
     * @return \Plasma\CommandInterface|null
494
     */
495 5
    function getNextCommand(): ?\Plasma\CommandInterface {
496 5
        if(\count($this->queue) === 0) {
497 4
            if($this->goingAway) {
498
                $this->goingAway->resolve();
499
            }
500
            
501 4
            return null;
502 5
        } elseif($this->busy === static::STATE_BUSY) {
503
            return null;
504
        }
505
        
506
        /** @var \Plasma\CommandInterface  $command */
507 5
        $command =  \array_shift($this->queue);
508
        
509 5
        echo 'Unshifted command '.get_class($command).PHP_EOL;
510
        
511 5
        if($command->waitForCompletion()) {
512 5
            $this->busy = static::STATE_BUSY;
513
            
514
            $command->once('error', function () {
515 2
                echo 'Command errored'.PHP_EOL;
516 2
                $this->busy = static::STATE_IDLE;
517
                
518 2
                $this->endCommand();
519 5
            });
520
            
521
            $command->once('end', function () {
522 4
                echo 'Command ended'.PHP_EOL;
523 4
                $this->busy = static::STATE_IDLE;
524
                
525 4
                $this->endCommand();
526 5
            });
527
        } else {
528
            $this->endCommand();
529
        }
530
        
531 5
        return $command;
532
    }
533
    
534
    /**
535
     * Finishes up a command.
536
     * @return void
537
     */
538 5
    protected function endCommand() {
539 5
        if($this->goingAway && \count($this->queue) === 0) {
540 1
            return $this->goingAway->resolve();
541
        }
542
        
543 5
        $this->parser->invokeCommand($this->getNextCommand());
544 5
    }
545
    
546
    /**
547
     * Starts the handshake process.
548
     * @param array                    $credentials
549
     * @param \React\Promise\Deferred  $deferred
550
     * @return void
551
     */
552 12
    protected function startHandshake(array $credentials, \React\Promise\Deferred $deferred) {
553
        $listener = function (\Plasma\Drivers\MySQL\Messages\MessageInterface $message) use ($credentials, &$deferred, &$listener) {
554 12
            if($message instanceof \Plasma\Drivers\MySQL\Messages\HandshakeMessage) {
555 12
                $this->parser->removeListener('message', $listener);
556
                
557 12
                $this->connectionState = static::CONNECTION_SETENV;
558 12
                $clientFlags = \Plasma\Drivers\MySQL\ProtocolParser::CLIENT_CAPABILITIES;
559
                
560 12
                \extract($credentials);
561
                
562 12
                if($db !== '') {
563
                    $clientFlags |= \Plasma\Drivers\MySQL\CapabilityFlags::CLIENT_CONNECT_WITH_DB;
564
                }
565
                
566
                // Check if we support auth plugins
567 12
                $plugins = \Plasma\Drivers\MySQL\DriverFactory::getAuthPlugins();
568 12
                $plugin = null;
569
                
570 12
                foreach($plugins as $key => $plug) {
571 12
                    if(\is_int($key) && ($message->capability & $key) !== 0) {
572
                        $plugin = $plug;
573
                        $clientFlags |= \Plasma\Drivers\MySQL\CapabilityFlags::CLIENT_PLUGIN_AUTH;
574
                        break;
575 12
                    } elseif($key === $message->authPluginName) {
576 12
                        $plugin = $plug;
577 12
                        $clientFlags |= \Plasma\Drivers\MySQL\CapabilityFlags::CLIENT_PLUGIN_AUTH;
578 12
                        break;
579
                    }
580
                }
581
                
582 12
                $remote = \parse_url($this->connection->getRemoteAddress())['host'];
583
                
584 12
                if($remote !== '127.0.0.1' || $this->options['tls.forceLocal']) {
585 2
                    if(($message->capability & \Plasma\Drivers\MySQL\CapabilityFlags::CLIENT_SSL) !== 0) { // If SSL supported, connect through SSL
586
                        $clientFlags |= \Plasma\Drivers\MySQL\CapabilityFlags::CLIENT_SSL;
587
                        
588
                        $ssl = new \Plasma\Drivers\MySQL\Commands\SSLRequestCommand($message, $clientFlags);
589
                        
590
                        $ssl->once('end', function () use ($credentials, $clientFlags, $plugin, &$deferred, &$message) {
591
                            $this->connectionState = static::CONNECTION_SSL_STARTUP;
592
                            
593
                            $this->enableTLS()->then(function () use ($credentials, $clientFlags, $plugin, &$deferred, &$message) {
594
                                $this->createHandshakeResponse($message, $credentials, $clientFlags, $plugin, $deferred);
595
                            }, function (\Throwable $error) use (&$deferred) {
596
                                $deferred->reject($$error);
597
                                $this->connection->close();
598
                            });
599
                        });
600
                        
601
                        return $this->parser->invokeCommand($ssl);
602 2
                    } elseif($this->options['tls.force'] || $this->options['tls.forceLocal']) {
603 2
                        $deferred->reject((new \Plasma\Exception('TLS is not supported by the server')));
604 2
                        $this->connection->close();
605 2
                        return;
606
                    }
607
                }
608
                
609 10
                $this->createHandshakeResponse($message, $credentials, $clientFlags, $plugin, $deferred);
610
            }
611 12
        };
612
        
613 12
        $this->parser->on('message', $listener);
614
        
615
        $this->parser->on('message', function (\Plasma\Drivers\MySQL\Messages\MessageInterface $message) {
616 12
            if($message instanceof \Plasma\Drivers\MySQL\Messages\OkResponseMessage) {
617 9
                $this->connectionState = static::CONNECTION_OK;
618
            }
619
            
620 12
            $this->emit('eventRelay', array('message', $message));
621 12
        });
622 12
    }
623
    
624
    /**
625
     * Enables TLS on the connection.
626
     * @return \React\Promise\PromiseInterface
627
     */
628
    protected function enableTLS(): \React\Promise\PromiseInterface {
629
        // Set required SSL/TLS context options
630
        foreach($this->options['tls.context'] as $name => $value) {
631
            \stream_context_set_option($this->connection->stream, 'ssl', $name, $value);
632
        }
633
        
634
        return $this->encryption->enable($this->connection)->otherwise(function (\Throwable $error) {
635
            $this->connection->close();
636
            throw new \RuntimeException('Connection failed during TLS handshake: '.$error->getMessage(), $error->getCode());
637
        });
638
    }
639
    
640
    /**
641
     * Sends the auth command.
642
     * @param \Plasma\Drivers\MySQL\Messages\HandshakeMessage  $message
643
     * @param array                                            $credentials
644
     * @param int                                              $clientFlags
645
     * @param string|null                                      $plugin
646
     * @param \React\Promise\Deferred                          $deferred
647
     * @return void
648
     */
649 10
    protected function createHandshakeResponse(
650
        \Plasma\Drivers\MySQL\Messages\HandshakeMessage $message, array $credentials, int $clientFlags, ?string $plugin, \React\Promise\Deferred $deferred
651
    ) {
652 10
        \extract($credentials);
653
        
654 10
        $auth = new \Plasma\Drivers\MySQL\Commands\HandshakeResponseCommand($this->parser, $message, $clientFlags, $plugin, $user, $password, $db);
655
        
656
        $auth->once('end', function () use (&$deferred) {
657 9
            $deferred->resolve();
658 10
        });
659
        
660
        $auth->once('error', function (\Throwable $error) use (&$deferred) {
661 1
            $deferred->reject($error);
662 1
            $this->connection->close();
663 10
        });
664
        
665 10
        if($plugin) {
666
            $listener = function (\Plasma\Drivers\MySQL\Messages\MessageInterface $message) use ($password, &$deferred, &$listener) {
667
                /** @var \Plasma\Drivers\MySQL\AuthPlugins\AuthPluginInterface|null  $plugin */
668 10
                static $plugin;
669
                
670 10
                if($message instanceof \Plasma\Drivers\MySQL\Messages\AuthSwitchRequestMessage) {
671
                    $name = $message->authPluginName;
672
                    
673
                    if($name !== null) {
674
                        foreach($plugins as $key => $plug) {
675
                            if($key === $name) {
676
                                $plugin = new $plug($this->parser, $message);
677
                                
678
                                $command = new \Plasma\Drivers\MySQL\Commands\AuthSwitchResponseCommand($message, $plugin, $password);
679
                                return $this->parser->invokeCommand($command);
680
                            }
681
                        }
682
                    }
683
                    
684
                    $deferred->reject((new \Plasma\Exception('Requested authentication method '.($name ? '"'.$name.'" ' : '').'is not supported')));
685 10
                } elseif($message instanceof \Plasma\Drivers\MySQL\Messages\AuthMoreDataMessage) {
686
                    if($plugin === null) {
687
                        $deferred->reject((new \Plasma\Exception('No auth plugin is in use, but we received auth more data packet')));
688
                        return $this->connection->close();
689
                    }
690
                    
691
                    try {
692
                        $command = $plugin->receiveMoreData($message);
693
                        return $this->parser->invokeCommand($command);
694
                    } catch (\Plasma\Exception $e) {
695
                        $deferred->reject($e);
696
                        $this->connection->close();
697
                    }
698 10
                } elseif($message instanceof \Plasma\Drivers\MySQL\Messages\OkResponseMessage) {
699 9
                    $this->parser->removeListener('message', $listener);
700
                }
701 10
            };
702
            
703 10
            $this->parser->on('message', $listener);
704
        }
705
        
706 10
        $this->parser->invokeCommand($auth);
707 10
        $this->connectionState = static::CONNECTION_AWAITING_RESPONSE;
708 10
    }
709
    
710
    /**
711
     * Validates the given options.
712
     * @param array  $options
713
     * @return void
714
     * @throws \InvalidArgumentException
715
     */
716 17
    protected function validateOptions(array $options) {
717 17
        $validator = \CharlotteDunois\Validation\Validator::make($options, array(
718 17
            'connector' => 'class:\React\Socket\ConnectorInterface=object',
719
            'tls.context' => 'array',
720
            'tls.force' => 'boolean',
721
            'tls.forceLocal' => 'boolean'
722
        ));
723
        
724 17
        $validator->throw(\InvalidArgumentException::class);
725 17
    }
726
}
727