Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
PasvTransfer
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 6
56
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 __destruct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getAddress
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDataConnection
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
 send
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 end
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2namespace Apie\FtpServer\Transfers;
3
4use Apie\FtpServer\PassivePortManager;
5use React\EventLoop\Loop;
6use React\Promise\Deferred;
7use React\Promise\PromiseInterface;
8use React\Socket\ConnectionInterface;
9use React\Socket\SocketServer;
10
11class PasvTransfer implements TransferInterface
12{
13    private SocketServer $dataServer;
14    private ?PromiseInterface $lastAction = null;
15
16    public function __construct(
17        private readonly string $passiveMinPort = '49152',
18        private readonly string $passiveMaxPort = '65534',
19    ) {
20        $port = null;
21
22        $this->dataServer = PassivePortManager::getAvailablePort(
23            (int) $passiveMinPort,
24            (int) $passiveMaxPort
25        );
26    }
27
28    public function __destruct()
29    {
30        $this->end();
31    }
32
33    public function getAddress(): string
34    {
35        return $this->dataServer->getAddress();
36    }
37
38    /**
39     * Returns a promise that resolves to the established data connection.
40     */
41    private function getDataConnection(float $timeout = 2.0): PromiseInterface
42    {
43        if ($this->lastAction) {
44            return $this->lastAction;
45        }
46
47        $deferred = new Deferred();
48
49        // Timeout if no connection is made
50        $timer = Loop::get()->addTimer($timeout, function () use ($deferred) {
51            $deferred->reject(new \RuntimeException("Can't open data connection"));
52        });
53
54        $this->dataServer->once('connection', function (ConnectionInterface $conn) use ($deferred, $timer) {
55            Loop::get()->cancelTimer($timer);
56            $deferred->resolve($conn);
57        });
58
59        $this->lastAction = $deferred->promise();
60        return $this->lastAction;
61    }
62
63    public function send(string $data, ?callable $onRejected = null): void
64    {
65        $this->lastAction = $this->getDataConnection()->then(
66            function (ConnectionInterface $conn) use ($data) {
67                $conn->write($data);
68
69                return $conn;
70            },
71            $onRejected
72        );
73    }
74
75    public function end(): void
76    {
77        // Gracefully close the connection and server
78        $promise = $this->getDataConnection();
79        $promise->then(
80            function (ConnectionInterface $conn) {
81                $conn->end();
82            }
83        )->finally(function () {
84            PassivePortManager::release($this->dataServer);
85            $this->lastAction = null;
86        });
87    }
88}